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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.