feat: add networking module with TCP Socket/Server and example applications

Implement a full-featured `net` module supporting TCP `Socket` and `Server` classes via libuv, including example applications (chat server, echo server, HTTP client/server, file explorer, system dashboard) and benchmark scripts (fibonacci, n-body). Add GEMINI.md project documentation and Stderr class to io module.
This commit is contained in:
2026-01-24 08:57:59 +00:00
parent abc32676bf
commit 8f1d13a86b
25 changed files with 1371 additions and 1 deletions
+15
View File
@@ -0,0 +1,15 @@
import "os" for Process
var start = System.clock
var fib
fib = Fn.new { |n|
if (n < 2) return n
return fib.call(n - 1) + fib.call(n - 2)
}
for (i in 1..5) {
System.print("fib(28) = " + fib.call(28).toString)
}
System.print("Elapsed: " + (System.clock - start).toString + "s")
+148
View File
@@ -0,0 +1,148 @@
import "os" for Process
class Body {
construct new(x, y, z, vx, vy, vz, mass) {
_x = x
_y = y
_z = z
_vx = vx
_vy = vy
_vz = vz
_mass = mass
}
x { _x }
x=(v) { _x = v }
y { _y }
y=(v) { _y = v }
z { _z }
z=(v) { _z = v }
vx { _vx }
vx=(v) { _vx = v }
vy { _vy }
vy=(v) { _vy = v }
vz { _vz }
vz=(v) { _vz = v }
mass { _mass }
offsetMomentum(px, py, pz) {
_vx = -px / 0.01227246237529089
_vy = -py / 0.01227246237529089
_vz = -pz / 0.01227246237529089
}
}
class NBody {
static init() {
var pi = 3.141592653589793
var solarMass = 4 * pi * pi
var daysPerYear = 365.24
__bodies = [
// Sun
Body.new(0, 0, 0, 0, 0, 0, solarMass),
// Jupiter
Body.new(
4.84143144246472090e+00,
-1.16032004402742839e+00,
-1.03622044471123109e-01,
1.66007664274403694e-03 * daysPerYear,
7.69901118419740425e-03 * daysPerYear,
-6.90460016972063023e-05 * daysPerYear,
9.54791938424326609e-04 * solarMass),
// Saturn
Body.new(
8.34336671824457987e+00,
4.12479856412430479e+00,
-4.03523417114321381e-01,
-2.76742510726862411e-03 * daysPerYear,
4.99852801234917238e-03 * daysPerYear,
2.30417297573763929e-05 * daysPerYear,
2.85885980666130812e-04 * solarMass),
// Uranus
Body.new(
1.28943695618239209e+01,
-1.51111514016986312e+01,
-2.23307578892655734e-01,
2.96460137564761618e-03 * daysPerYear,
2.37847173959480950e-03 * daysPerYear,
-2.96589568540237556e-05 * daysPerYear,
4.36624404335156298e-05 * solarMass),
// Neptune
Body.new(
1.53796971148509165e+01,
-2.59193146099879641e+01,
1.79258772950371181e-01,
2.68067772490389322e-03 * daysPerYear,
1.62824170038242295e-03 * daysPerYear,
-9.51592254519715870e-05 * daysPerYear,
5.15138902046611451e-05 * solarMass)
]
var px = 0
var py = 0
var pz = 0
for (b in __bodies) {
px = px + b.vx * b.mass
py = py + b.vy * b.mass
pz = pz + b.vz * b.mass
}
__bodies[0].offsetMomentum(px, py, pz)
}
static energy() {
var e = 0
for (i in 0...__bodies.count) {
var bi = __bodies[i]
e = e + 0.5 * bi.mass * (bi.vx * bi.vx + bi.vy * bi.vy + bi.vz * bi.vz)
for (j in i + 1...__bodies.count) {
var bj = __bodies[j]
var dx = bi.x - bj.x
var dy = bi.y - bj.y
var dz = bi.z - bj.z
var distance = (dx * dx + dy * dy + dz * dz).sqrt
e = e - (bi.mass * bj.mass) / distance
}
}
return e
}
static advance(dt) {
for (i in 0...__bodies.count) {
var bi = __bodies[i]
for (j in i + 1...__bodies.count) {
var bj = __bodies[j]
var dx = bi.x - bj.x
var dy = bi.y - bj.y
var dz = bi.z - bj.z
var d2 = dx * dx + dy * dy + dz * dz
var mag = dt / (d2 * d2.sqrt)
bi.vx = bi.vx - dx * bj.mass * mag
bi.vy = bi.vy - dy * bj.mass * mag
bi.vz = bi.vz - dz * bj.mass * mag
bj.vx = bj.vx + dx * bi.mass * mag
bj.vy = bj.vy + dy * bi.mass * mag
bj.vz = bj.vz + dz * bi.mass * mag
}
}
for (b in __bodies) {
b.x = b.x + dt * b.vx
b.y = b.y + dt * b.vy
b.z = b.z + dt * b.vz
}
}
static run(n) {
init()
System.print(energy())
for (i in 0...n) advance(0.01)
System.print(energy())
}
}
var start = System.clock
NBody.run(100000)
System.print("Elapsed: " + (System.clock - start).toString + "s")
+57
View File
@@ -0,0 +1,57 @@
// retoor <retoor@molodetz.nl>
import "net" for Server, Socket
import "scheduler" for Scheduler
class ChatServer {
construct new(port) {
_port = port
_clients = []
_server = Server.bind("0.0.0.0", port)
System.print("Chat Server listening on port %(_port)")
}
run() {
while (true) {
var socket = _server.accept()
handleNewClient(socket)
}
}
handleNewClient(socket) {
_clients.add(socket)
var id = _clients.count
System.print("Client %(id) connected. Total: %(_clients.count)")
Fiber.new {
broadcast("Client %(id) joined the chat!\n", socket)
while (true) {
var message = socket.read()
if (message == null || message == "") break
System.print("Client %(id): %(message.trim())")
broadcast("Client %(id): %(message)", socket)
}
System.print("Client %(id) disconnected.")
_clients.remove(socket)
socket.close()
broadcast("Client %(id) left the chat.\n", null)
}.call()
}
broadcast(message, sender) {
for (client in _clients) {
if (client != sender) {
// We use a separate fiber for each write to prevent one slow
// client from blocking the broadcast to others
Fiber.new {
client.write(message)
}.call()
}
}
}
}
var server = ChatServer.new(7070)
server.run()
+25
View File
@@ -0,0 +1,25 @@
import "net" for Server
import "scheduler" for Scheduler
var port = 9090
var server = Server.bind("0.0.0.0", port)
System.print("Echo Server running on port %(port)")
while (true) {
var socket = server.accept()
System.print("New connection accepted")
Fiber.new {
var socket_ = socket // Capture variable
while (true) {
var data = socket_.read()
if (data == null) {
break
}
System.print("Received: %(data.count) bytes")
socket_.write(data)
}
System.print("Connection closed")
socket_.close()
}.call()
}
+57
View File
@@ -0,0 +1,57 @@
// retoor <retoor@molodetz.nl>
import "io" for Directory, File, Stat
class FileExplorer {
static listRecursive(path, depth) {
var indent = " " * depth
var files = []
// We wrap Directory.list in a try to handle permission errors
var fiber = Fiber.new {
files = Directory.list(path)
}
fiber.try()
if (fiber.error != null) {
System.print("\%(indent)[!] Error accessing \%(path)")
return
}
// Sort files manually since String comparison isn't built-in
files.sort(Fn.new {|a, b|
var ba = a.bytes
var bb = b.bytes
var len = ba.count < bb.count ? ba.count : bb.count
for (i in 0...len) {
if (ba[i] < bb[i]) return true
if (ba[i] > bb[i]) return false
}
return ba.count < bb.count
})
for (file in files) {
if (file == "." || file == "..") continue
var fullPath = path + "/" + file
if (path == ".") fullPath = "./" + file
var stat = Stat.path(fullPath)
if (stat.isDirectory) {
System.print("\%(indent)[D] \%(file)/")
listRecursive(fullPath, depth + 1)
} else {
var size = stat.size
var unit = "B"
if (size > 1024) {
size = size / 1024
unit = "KB"
}
System.print("\%(indent)[F] \%(file) (\%(size.round)\%(unit))")
}
}
}
}
System.print("Recursive directory listing for current path:")
System.print("--------------------------------------------")
FileExplorer.listRecursive(".", 0)
+42
View File
@@ -0,0 +1,42 @@
// retoor <retoor@molodetz.nl>
import "net" for Socket
import "scheduler" for Scheduler
class HttpClient {
static get(host, port, path) {
System.print("Connecting to %(host):%(port)...")
var socket = Socket.connect(host, port)
var request = "GET %(path) HTTP/1.1\r\n" +
"Host: %(host)\r\n" +
"User-Agent: Wren-CLI\r\n" +
"Connection: close\r\n" +
"\r\n"
System.print("Sending request...")
socket.write(request)
System.print("Waiting for response...")
var response = ""
while (true) {
var chunk = socket.read()
if (chunk == null) break
response = response + chunk
}
socket.close()
return response
}
}
// Example usage: Fetching from a local server or a known public IP
// Note: This implementation is for raw IP/DNS if supported by uv_tcp_connect.
// Since our net.c uses uv_ip4_addr, we use localhost for the example.
var host = "127.0.0.1"
var port = 8080 // Try to connect to our own http_server example
var path = "/"
var result = HttpClient.get(host, port, path)
System.print("\n--- Response Received ---
")
System.print(result)
+190
View File
@@ -0,0 +1,190 @@
import "net" for Server, Socket
import "io" for File, Directory, Stat
import "scheduler" for Scheduler
class HttpServer {
construct new(port) {
_port = port
_server = Server.bind("0.0.0.0", port)
System.print("HTTP Server listening on http://localhost:%(port)")
}
run() {
while (true) {
var socket = _server.accept()
Fiber.new {
handleClient_(socket)
}.call()
}
}
handleClient_(socket) {
var requestData = socket.read()
if (requestData == null || requestData == "") {
socket.close()
return
}
var lines = requestData.split("\r\n")
if (lines.count == 0) {
socket.close()
return
}
var requestLine = lines[0].split(" ")
if (requestLine.count < 2) {
sendError_(socket, 400, "Bad Request")
return
}
var method = requestLine[0]
var path = requestLine[1]
System.print("%(method) %(path)")
if (method != "GET") {
sendError_(socket, 405, "Method Not Allowed")
return
}
path = path.replace("\%20", " ")
if (path.contains("..")) {
sendError_(socket, 403, "Forbidden")
return
}
var localPath = "." + path
if (localPath.endsWith("/")) localPath = localPath[0..-2]
if (localPath == "") localPath = "."
if (!exists(localPath)) {
sendError_(socket, 404, "Not Found")
return
}
if (isDirectory(localPath)) {
serveDirectory_(socket, localPath, path)
} else {
serveFile_(socket, localPath)
}
socket.close()
}
exists(path) {
if (File.exists(path)) return true
if (Directory.exists(path)) return true
return false
}
isDirectory(path) {
return Directory.exists(path)
}
serveDirectory_(socket, localPath, requestPath) {
var files
var fiber = Fiber.new {
files = Directory.list(localPath)
}
fiber.try()
if (fiber.error != null) {
sendError_(socket, 500, "Internal Server Error: " + fiber.error)
return
}
files.sort(Fn.new {|a, b|
var ba = a.bytes
var bb = b.bytes
var len = ba.count
if (bb.count < len) len = bb.count
for (i in 0...len) {
if (ba[i] < bb[i]) return true
if (ba[i] > bb[i]) return false
}
return ba.count < bb.count
})
var html = "<!DOCTYPE html><html><head><title>Index of %(requestPath)</title></head><body>"
html = html + "<h1>Index of %(requestPath)</h1><hr><ul>"
if (requestPath != "/") {
var parent = requestPath.split("/")
if (parent.count > 1) {
parent.removeAt(-1)
var parentPath = parent.join("/")
if (parentPath == "") parentPath = "/"
html = html + "<li><a href=\"%(parentPath)\">..</a></li>"
}
}
for (file in files) {
var href = requestPath
if (!href.endsWith("/")) href = href + "/"
href = href + file
html = html + "<li><a href=\"%(href)\">%(file)</a></li>"
}
html = html + "</ul><hr></body></html>"
sendResponse_(socket, 200, "OK", "text/html", html)
}
serveFile_(socket, localPath) {
var content
var fiber = Fiber.new {
content = File.read(localPath)
}
fiber.try()
if (fiber.error != null) {
sendError_(socket, 500, "Error reading file: " + fiber.error)
return
}
var contentType = "application/octet-stream"
if (localPath.endsWith(".html")) {
contentType = "text/html"
} else if (localPath.endsWith(".txt")) {
contentType = "text/plain"
} else if (localPath.endsWith(".wren")) {
contentType = "text/plain"
} else if (localPath.endsWith(".c")) {
contentType = "text/plain"
} else if (localPath.endsWith(".h")) {
contentType = "text/plain"
} else if (localPath.endsWith(".md")) {
contentType = "text/markdown"
} else if (localPath.endsWith(".json")) {
contentType = "application/json"
} else if (localPath.endsWith(".png")) {
contentType = "image/png"
} else if (localPath.endsWith(".jpg")) {
contentType = "image/jpeg"
}
sendResponse_(socket, 200, "OK", contentType, content)
}
sendError_(socket, code, message) {
var html = "<h1>%(code) %(message)</h1>"
sendResponse_(socket, code, message, "text/html", html)
socket.close()
}
sendResponse_(socket, code, status, contentType, body) {
var response = "HTTP/1.1 %(code) %(status)\r\n" +
"Content-Type: %(contentType)\r\n" +
"Content-Length: %(body.count)\r\n" +
"Connection: close\r\n" +
"\r\n" +
body
socket.write(response)
}
}
var server = HttpServer.new(8080)
server.run()
+57
View File
@@ -0,0 +1,57 @@
// retoor <retoor@molodetz.nl>
import "os" for Platform, Process
import "io" for Directory
class Dashboard {
static show() {
System.print("========================================")
System.print(" WREN CLI SYSTEM DASHBOARD ")
System.print("========================================")
System.print("PLATFORM INFO:")
System.print(" OS Name: %(Platform.name)")
System.print(" POSIX: %(Platform.isPosix)")
System.print(" Home: %(Platform.homePath)")
System.print("\nPROCESS INFO:")
System.print(" PID: %(Process.pid)")
System.print(" PPID: %(Process.ppid)")
System.print(" CWD: %(Process.cwd)")
System.print(" Version: %(Process.version)")
System.print("\nARGUMENTS:")
if (Process.arguments.count == 0) {
System.print(" (none)")
} else {
for (i in 0...Process.arguments.count) {
System.print(" [%(i)]: %(Process.arguments[i])")
}
}
System.print("\nWORKING DIRECTORY FILES:")
var files = Directory.list(".")
files.sort(Fn.new {|a, b|
var ba = a.bytes
var bb = b.bytes
var len = ba.count < bb.count ? ba.count : bb.count
for (i in 0...len) {
if (ba[i] < bb[i]) return true
if (ba[i] > bb[i]) return false
}
return ba.count < bb.count
})
var count = 0
for (file in files) {
if (count > 10) {
System.print(" ... and %(files.count - 10) more")
break
}
System.print(" - %(file)")
count = count + 1
}
System.print("========================================")
}
}
Dashboard.show()
+51
View File
@@ -0,0 +1,51 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler
import "timer" for Timer
class TaskQueue {
construct new() {
_tasks = []
_running = true
}
add(name, duration) {
System.print("[Queue] Adding task: \%(name) (\%(duration)ms)")
_tasks.add({
System.print("[Worker] Starting \%(name)...")
Timer.sleep(duration)
System.print("[Worker] Finished \%(name).")
})
}
start() {
System.print("[Queue] Starting task runner...")
Fiber.new {
while (_running) {
if (!_tasks.isEmpty) {
var task = _tasks.removeAt(0)
task.call()
}
Timer.sleep(100) // Yield to other fibers
}
}.call()
}
stop() { _running = false }
}
var queue = TaskQueue.new()
// Start the worker in the background
queue.start()
// Add some tasks from the main thread
queue.add("Task A", 1500)
queue.add("Task B", 500)
queue.add("Task C", 1000)
System.print("[Main] All tasks queued. Main fiber is free.")
// Keep the main loop alive for a bit to see the output
Timer.sleep(4000)
queue.stop()
System.print("[Main] Shutdown.")