Select Wasn't Needed
The classic concurrency toolkit has one famous primitive this language lacks: waiting on several channels at once. A pub/sub broker — the most multi-source program imaginable — went looking for the wall. There was no wall: everything a broker reacts to funnels through one inbox as a variant type, the actor pattern, and the language already had every piece. What the dogfood found instead was a real compiler bug, hiding where two features met: a recursive loop calling a helper whose own nested function closes over the helper's locals leaked those locals into the loop's captures.
Shutdown, timeout — the ending toolkit was two-thirds classical. The third classic is
select: waiting on several channels at once, taking whichever produces first. Go has
it as syntax; most channel libraries grow some form of it. The natural assumption was
that this language needed it too and simply hadn’t been forced yet. So this probe set
out to force it, with the most multi-source program available: a pub/sub broker.
Publishers on one side, subscribers on the other, a broker in the middle reacting to
subscription requests, published messages, and a shutdown signal — three kinds of input,
apparently three channels, apparently a select over them.
The funnel
The wall never came. The broker doesn’t need to wait on three channels, because nothing
forces the three inputs to be three channels: they can be three constructors of one
variant type — Pub, Sub, Stop — flowing through a single inbox, with the broker
blocking on exactly one receive and dispatching on the constructor. This is the actor
pattern, and the language turned out to have every piece it needs: variants carry tuple
payloads across channels, and — the part that genuinely wasn’t obvious — a channel can
itself ride inside a message, so a Sub command carries the subscriber’s own delivery
channel through the inbox. Channels are first-class message payloads. The general escape
is worth stating: any would-be select over sources you control becomes a funnel into
one inbox; sources you don’t control get a forwarder thread each, merging into one. A
convenience remains a convenience. This is the part’s second negative result done
honestly, in the family of “mono is the dictionary”: the missing feature turns out to be
subsumed by what exists, and the finding is recorded instead of the feature being built.
What the dogfood actually caught
A dogfood never comes back empty-handed, though — this one caught a genuine compiler
bug where two features met. The broker’s fan-out helper contains its own nested
recursive function, which closes over the helper’s locals. The broker’s main loop calls
the helper. In the C backend’s closure-lifting pass, a fixpoint threads a callee’s
captured variables through its callers — necessary in general, but it added the helper’s
captures to the loop without checking whether those names were already bound inside the
helper itself. The loop’s emitted signature grew two parameters, hn and hv, that
exist nowhere in its scope: “use of undeclared identifier,” a compile error two
abstraction layers from its cause. The fix is one skip-condition in the fixpoint — a
callee capture that is bound anywhere inside the caller’s own body is already in scope
and must not be threaded — plus a regression test distilled to a channel-free
twenty-liner. The broker now runs its two topics and two subscribers to the correct sums
on both native backends. The arc’s ending toolkit stands at: close, timeout, cancel via
close — and no select, on purpose. One episode remains in this part, and it plays
offense the other way: four classic programs thrown at the language at once, to see if
anything still falls over.