Spinel on Rails
Spinel is Matz's ahead-of-time Ruby compiler: it reads Ruby, infers types across the whole program, emits C, and from there a native binary with no runtime dependencies. Roundhouse reads a Rails application and emits standalone projects in nine languages — one of which is Ruby. The two were started a month apart, by people who didn't know each other's work. I wrote about that coincidence twice in April: once when it looked like two answers to one moment, and again two days later when the overlap turned out to be sharper than I'd said — not merely two compilers drawing the same line, but two compilers that wanted the same intermediate language.
That second post ended on a bet. Today that bet paid off. But first a quick start:
curl -sL https://rubys.github.io/roundhouse/browse/spinel.tgz | tar xz<br>cd spinel<br>make build<br>sqlite3 storage/development.sqlite3 db/seed.sql<br>./build/blog<br>This is the quintessential Rails blog demo modernized with Tailwind CSS, Turbo Streams, and Action Cable — open multiple windows, make a change, and watch pages update live. All running as a Spinel application which is automatically generated from the original Rails source.
The bet
Here is what I wrote on April 27, after first testing whether Spinel could compile Roundhouse's contract fixture:
The bet I'm making: the architectural shape is right, the maturity gaps close on both sides, and within a few iterations the round trip works end to end.
I listed five places where C compilation failed — module-typed globals not propagating across scope, polymorphic method arguments collapsing to one type, default-argument methods emitting a single signature, empty intermediate inheritance classes dropping parent struct fields, polymorphic instance variables typing as integer and corrupting later assignments. I filed the most isolated one as a six-line repro and called it "a starting point for the dialog."
"Within a few iterations" turned out to be the wrong unit. It took 239 closed issues. But the round trip runs: lower a Rails blog to the Spinel subset, hand the result to Spinel, get back a native executable that serves the application. The bet is settled, and the way it settled is more interesting than a clean win would have been.
What "a few iterations" actually cost
The five gaps I could see in April were the ones visible from outside — the failures you hit reading the documented subset and trying the fixture once. They were the tip.
Feeding a non-trivial Rails-shaped program through to compiled C and running it under load surfaced a long tail the first attempt never reached. Of the 239 issues I've filed against Spinel that are now closed, only seven were requests to widen the subset or questions about process — an FFI, Fiber.storage, GC introspection, a server transport, Hash#to_h, an issue-cadence meta-issue, and a repro-archive workflow. The other 232 were the compiler catching up to its own documented subset: polymorphic dispatch, inference precision, garbage-collector safety, struct layout, and codegen plumbing.
Representative failures, by category:
Category<br>What broke<br>Examples
Polymorphic dispatch<br>A cls_id switch routing to the wrong method body, or dropping a builtin case when an instance method shadowed it<br>#1451, #1447, #1443, #1437
Inference precision<br>A method inferred as Integer emitting a nil box; a nil-guard failing to narrow a nullable type<br>#1432, #1417, #1397
GC safety under load<br>A fresh argument collected before its parameter was rooted; a string-concat loop running RSS to multi-GB; foreign pointers marked as heap objects<br>#1450, #1445, #1439, #1314
Struct & inheritance layout<br>A subclass struct missing a field present on the value-typed parent<br>#1442, #1415
Codegen plumbing<br>Ruby locals emitted with no C declaration; a block losing self; bare super to a module-qualified superclass<br>#1435, #1436, #1413
The honest summary: the documented subset was sound. What was incomplete was the distance between documented and delivered — the inference and codegen catching up to what the subset already promised. That distance was an order of magnitude larger than the five gaps suggested, and it lived almost entirely below the subset line, in the machinery that turns conforming Ruby into correct C.
Two of those buckets are worth dwelling on, because they connect directly to the numbers below. The GC-under-load issues — a use-after-free during row materialization, a string-concatenation loop that leaked to multiple gigabytes — are failures you only see when you push real request traffic at the binary. They're also why the memory story later in this post is trustworthy rather than aspirational: the small resident footprint was earned by closing them, not handed out for free.
The collaboration
On April 28 I opened a meta-issue asking Matz a process question: should I file each gap as its own minimal repro, the way #49 was framed, or batch them, or hold them until other inference work landed?
His answer set the...