letloop v12

release notes · origin/hello-schemer dev · 185 commits · Jun 20 – Aug 7, 2026

v12 is the last release of letloop written in R6RS Scheme. Everything below shipped since hello-schemer — 131 files, +22,665/‑8,362 lines — before the tree is rewritten against seed, a Kernel dialect with John Shutt's vau operative, in v13. Treat this as the final word on the current architecture.


What shipped

Six things worth knowing about

(letloop tea)

A terminal UI framework built from the metal up: a damage-diffing cell grid, a terminfo parser, SGR color, UTF‑8 and East‑Asian width tables, keyboard/mouse/paste input decoding, SIGWINCH resize — all driven by the io_uring event loop instead of blocking reads. (letloop termbox) stays as a compatibility shim over it for anything written against the old binding.

Built on top: letloop review, a real TUI code browser — file tree, jump-to-definition via git grep, fold/unfold, per-line annotations persisted to REVIEW.md.

(letloop flow)

A CML-style event algebra for io_uring — flow-choice, rendezvous channels, timeouts with real cancellation of the loser, async file I/O. Sharding it across OS threads was tried, measured, and reverted for cause — single OS thread is the model this release ships with.

(letloop http server)

A new io_uring HTTP server, plus letloop http serve on the CLI. The response path no longer allocates per character through a Scheme string — status lines are cached, responses assemble straight from bytes.

PostgreSQL, TLS, DNS

An async PostgreSQL driver over io_uring with MD5 and SCRAM‑SHA‑256 authentication, a non-blocking TLS client, and an async DNS resolver — the same event loop underneath all three.

Building letloop got simpler

Every optional shared library — TLS, liburing, Vulkan, sodium, argon2, blake3, picohttpparser, opaque — now dlopens lazily on first use. None of them need to be installed to build letloop or import the library that wraps them.

(letloop asm) / (letloop kernel)

New this week: a verified x86-64 sexp assembler — every instruction form byte-compared against GNU as — that maps machine code into executable pages inside the running image, no C compiler anywhere in the path. On top of it, (letloop kernel) offers two ways in: (assembly ...) for hand-written mnemonics, and (kernel ...), a typed, Scheme-looking loop language compiled through SIB fusion, lea synthesis, CSE and loop-invariant code motion. Every kernel registers its own source, and dubito, a verification pass, doubts it at definition time.

On the LOUDS trie benchmark the simili-Scheme kernel tier now compiles to machine code that runs ahead of the equivalent hand-written C shared object, and within ~4% of hand-scheduled assembly — same source, byte-identical checksums, throughout the whole optimization arc.


Performance

Startup time, halved twice

Two changes did this: letloop's own libraries used to be imported eagerly at startup, now they resolve lazily on first use — that alone took startup from 69.6 ms to 32 ms. Then letloop compile started amalgamating a program and everything it imports into one compilation unit, cutting boot-image loading further.

69.6ms before
33.6ms median, this build

Measured just now — 20 runs of letloop version, median 33.55 ms, best 28.73 ms. That's within noise of Chez's own 33.04 ms startup floor — letloop now costs essentially nothing on top of the runtime it's built on.


Benchmark comparison

Fifteen implementations, one honest harness

Before trusting a number, we audited the comparison itself and fixed what was lying: Common Lisp was silently capped at ~120 connections by a taskmaster default, Java Loom paid a Nagle's‑algorithm penalty worth roughly 2,500× on every response, and the harness itself — bench.sh — was leaking every server process it ever started, letting stale processes contend with whatever it thought it was measuring. All fixed. What follows is peak throughput (GET /, one core, keep-alive, 9 concurrency levels from 1 to 256) after the harness stopped lying.

Peak requests/second

Counter web app · single pinned core · wrk, 10s per level

letloop everything else

C/h2o — a plain single-threaded epoll C server with no async runtime at all — leads at peak (256 connections). letloop leads Rust through the middle of the range (roughly 2× to 32× concurrency) and trails h2o there by single digits, but gives some of that back past 64 concurrent connections — still being chased down, not yet explained. This is the simple case: one endpoint, one in-memory counter, nothing to fetch. Full scaling data across all nine concurrency levels, every implementation, p50 and p99 — lives in the benchmark report.


Case study

Beyond GET /: a real page, three implementations

The City Explorer benchmark takes the comparison further — a page assembled from four concurrent upstream fetches, three JSON parses, and one photograph base64-encoded per request, run against letloop, Rust/axum, and Bun on identical work. letloop now beats axum outright on the largest page at low concurrency (1.5×), and the photo encoding behind that number no longer touches a C compiler: it's (letloop asm) and (letloop kernel) — described above — assembling the AVX2 base64 kernel as machine code inside the running Scheme image, at the same speed as the shared object it replaced. The latest commit hardens the test suite itself: a randomized range-query check (~check-aql-101/random) had a fixed upper bound too narrow to cover its own generated keys, producing an intermittent failure — now fixed.

Caveat emptor

letloop's own ~check-* suite runs at small scale in coroutine mode, which is exactly why it's not what caught the bugs that mattered: a month-long production run of an unrelated large search-index project built on letloop, against a 78 GB dataset, surfaced an unbounded fiber fan-out that stalled warm-up for ~9 minutes, a connect() with no deadline that hung silently for over 90 minutes, and GC pauses invisible without a progress callback — none of which any check would have caught, because none of them are correctness bugs, they're scale bugs. Two smaller footguns worth knowing before you deploy: LD_LIBRARY_PATH must be an absolute path everywhere a letloop binary might dlopen an optional shared object, and overwriting a running binary with cp instead of stopping the service first can silently keep the old process running (Text file busy). None of this is new in v12 — it's what a real, sustained caller found — and it belongs next to the throughput numbers above, not hidden from them.