Devlog · July 2026 · nlvm-demos

Proving parallelism in a browser tab: the HTTP server demo

The first five nlvm-demos each isolate one language feature — closures, generics, enums. None of them touch the standard library's I/O or concurrency surface, and none of them are things you'd actually point a browser at. 06_http_server fixes both: a real TCP server, one OS thread per connection, and a page that lets you watch the threads work.

One thread per connection

Main.main() binds a system.net.TcpListener and loops on accept(). Each accepted TcpStream is handed to a fresh system.thread.Thread that calls server.handle(stream, connId) and is started immediately — the main thread is back at accept() while the previous connection is still being served. No connection queue, no event loop: five clients get five threads.

One Server, shared, mutex-guarded

All five threads call methods on the same Server object — heap objects, including whatever a closure captures, are shared across system.thread.Thread instances rather than cloned (see vm.md § Threading model). That makes the request counter and the connection-id counter a real race unless they're protected, so both live behind a system.thread.Mutex:

public int recordRequest() {
    this.countMutex.lock();
    this.requestCount = this.requestCount + 1;
    int count = this.requestCount;
    this.countMutex.unlock();
    return count;
}

Lock, mutate, read, unlock, all inside the critical section — the pattern repeats for nextConnectionId(). Skip the mutex and two threads land on the same connection id or the same request count, intermittently, exactly the kind of bug that's fun to chase and boring to write demos around.

Routing without a path traversal bug

The whole point of a demo is that people read the source, so the file-serving path had to be obviously safe, not just safe. resolveFile() is a closed match over four literal routes — /, /index.html, /style.css, /script.js — that returns an empty string for anything else:

public string resolveFile(const string path) const {
    return match(path) {
        "/": "index.html",
        "/index.html": "index.html",
        "/style.css": "style.css",
        "/script.js": "script.js",
        default: "",
    };
}

A client-supplied path never reaches system.io.File.readAllText directly — it's translated through a whitelist first, so there's no ../../etc/passwd to worry about. The exhaustive match makes the whitelist visible in one place instead of scattered through if-checks.

Making parallelism visible, not just true

"Runs on real threads" is easy to claim and hard to see. The served page polls /api/stats every second for a live request count, and has a "fire 5 parallel requests" button that hits /api/work — an endpoint that does nothing but system.thread.Thread.sleep(600) — five times at once via Promise.all:

} else if (path == "/api/work") {
    // proves requests run in parallel across worker threads
    // instead of queuing behind each other
    system.thread.Thread.sleep(600);
    string json = "{\"worker\": " + connId + ", \"requests\": " + count + "}";
    this.sendResponse(stream, 200, "OK", "application/json; charset=utf-8", json);
}

On a single-threaded server, five sequential 600 ms sleeps take ~3 s. Here they take ~600 ms total, because five threads are asleep at the same time — and each response carries a distinct worker id, so the JSON itself is evidence. It's the difference between telling someone the server is multi-threaded and letting them click a button and watch the network tab prove it.

What it deliberately doesn't do

Every response sends Connection: close — no keep-alive, no connection reuse. Only GET is handled; anything else gets a 405. Request parsing assumes the full request line arrives in a single read() call. All reasonable simplifications for a 122-line demo whose job is to show off threads and a mutex, not to be a production HTTP server — and the README says so, explicitly, rather than leaving it to a reader's assumptions.


Source, README and the served page's front end are in nlvm-demos/06_http_server. See the demos page for the rest of the set.