The Race the Types Allowed
The server episode ended with a debt: a shared mutable map had crossed a thread boundary with the type checker's blessing and lost writes under load. Paying that debt meant two fixes, and the second was found only by insisting the first hadn't worked. Mutable containers are now neither Send nor Sync — and the compile path, it turned out, had never been running the safety analyses at all: programs the interpreter rejected sailed through the compilers untouched.
The rule this project holds itself to is that a measured soundness hole outranks every other kind of work, so the debt from the server episode came due before any new feature: the type system had allowed a mutable map to be shared across threads, and the map had raced. The design was never in question. The language’s concurrency discipline classifies every captured value as Sync (share it), Send (move it), or neither (reject the capture), and region-bound mutable containers were always meant to be thread-local — the documentation said so in as many words. The classifier just didn’t implement the intention. Its generic rule judged a constructor by its type arguments, a container’s first argument is its region marker, a region marker is an unresolved type variable, and the rule treated unresolved as safe. Optimism in exactly the wrong place: the one kind of value whose runtime is a lock-free array being mutated in place.
The fix that changed nothing
The repair looked like three lines: Map, Vec, and the string builder are now
explicitly neither Send nor Sync, before any generic rule can look at their arguments.
The owned-vector type keeps its move semantics — it is a drop type, single-owner by
construction, and crossing a thread by moving is exactly what it is for. Channels stay
Send and Sync, because they are the blessed pattern. The satisfying part should have been
re-running the server’s naive draft and watching it fail to compile. It compiled. The
classifier was right, the move checker was right, and the program still built — which
meant the problem was no longer what the analyses concluded but whether anyone asked
them.
The checks that never ran
The answer was embarrassing in the way only infrastructure bugs are. The interpreter’s
pipeline ran the full sequence: type inference, then the channel-element Send
obligations, the borrow-conflict check, and the spawn-capture move analysis. The
compile entry point — shared by the C, LLVM, and WebAssembly backends — ran type
inference and went straight to code generation. Every safety analysis the language had
built for concurrency and regions was simply skipped when producing a binary. A program
that captured a region borrow in a spawned thread was rejected by mere run and built
without comment by mere -c — and had been, for as long as both paths existed. The fix
was to make the compile path run the same three analyses before any backend sees the
program. With that, the naive server draft finally failed to compile, with the error it
should always have had: cannot capture a Map across a thread boundary, it is neither Send
nor Sync.
The server as proof
Two details close the episode properly. First, the actor-model server — the workaround from three episodes ago — compiles unchanged under the stricter rules, because channels are Send and the store never crosses a thread: the workaround was, all along, the design the types now enforce. It sits in the test suite as the living proof that the blessed pattern survives. Second, the honest asymmetry: this hole was found not by an audit but by a program that raced, which is the dogfood method working exactly as intended — and also a reminder that it only finds what you build. A soundness claim is a claim about all programs; a dogfood only ever checks one more. With the type system now saying no in the right places, the last debt standing was the big one — the memory that never came back — and the next two episodes pay it.