intertwingly

It’s just data

Roda on Spinel


At RubyConf on Thursday I argued that there is no server — that Rails is a declarative spec, the compiler is the query planner, and the deployment target is the execution plan. Afterwards, Jeremy Evans asked me the question that keeps a thesis honest: could you do Roda too?

It's the right question precisely because Roda is Rails' opposite number. Rails is macros all the way down — has_many, validates, resources — declarations a compiler can read off the page. Roda has no routes file at all. Routing is a tree of plain Ruby: the request threads down r.on and r.is matchers, interior nodes load state and can abort the whole subtree, and the leaf that matches writes the response. Sequel models validate in an ordinary def validate method body. If Roundhouse's intermediate representation were secretly Rails-shaped, this is where it would show: imperative Ruby that does its work instead of declaring it.

So I asked Claude to write up an honest assessment, and posted it as roundhouse#67. What happened next is worth narrating, because the collaboration structure mattered as much as the compiler work.

Jeremy keeps the oracle

Three weeks ago I wrote that maintaining the oracle is the human job in this way of working: extending the standard's coverage where it's blind, deciding how it computes truth, overruling it when a passing artifact should still die. In every episode so far, that keeper was me. The part of this story I most want on the record is that here, it isn't. For this spike, the keeper's seat belongs to Jeremy — he decides whether it lives, dies, or needs to be tweaked — and each of the keeper's powers got exercised by him, visibly, in the issue thread.

He extended the spec where it was blind. My assessment imagined responses at the leaves of the routing tree; Jeremy pointed out they return from any node — access-control checks abort whole subtrees — and that interior nodes routinely set state their sub-branches share. The exemplar we then built, roda-sequel-blog, bakes both seams in deliberately, an article-and-comments blog domain-identical to the Rails fixture Roundhouse already compiles so the two could be diffed through one pipeline.

He decided how correctness gets computed. His line-by-line review made the exemplar more idiomatic and less Rails-shaped — set_fields with an allow-list instead of strong-parameters cosplay, next unless @article = Article[id] as the interior abort, r.post true for path termination. Where a choice existed between mirroring Rails and writing what a Roda developer would actually write, we took the second on his word — and that matters, because "idiomatic Roda" is precisely the kind of behavior my earlier post flagged as the referenced oracle's edge: there is no running system you can query for it. The reference here is a person. Then the app grew an eighteen-check test suite of its own — the full route surface, valid and invalid input, flash across redirects, the hidden-_method form override, both 404 paths, HTML escaping — and that suite became the contract: a transpiled version of this application must pass it unchanged. The oracle was fixed, and blessed by the framework's author, before the first line of compiler code existed.

And he holds the verdict. The plan — CRuby target first as the correctness substrate, native binary second — got his sign-off before anything was built, and the results now sit in the issue awaiting his read. What I kept for myself is the other half of the earlier post's job description: keeping a reachable target in front of the agent, one hold at a time. The division turns out to be natural — the domain's author keeps the standard; the project's owner keeps the route.

A few hours, wall clock

From the first commit — the vendored fixture and a mapping table — to the native binary was a few elapsed hours, spread over a Monday morning. The routing tree linearized: each root-to-leaf path through the matchers became one route, and the interior next unless guard became a synthesized before-filter on both controllers that needed it, with exactly the only: scoping the equivalent Rails app declares by hand. Sequel's imperative validate body turned out to be a closed vocabulary that reads as declarations. The Sequel dataset spellings — Article[id], eager, with_pk — normalized to the one query dialect the rest of the pipeline already speaks. This is the first Futamura projection again, at smaller scale: Roda's per-request walk down the routing tree is interpretation, and specializing the app against static route structure makes the interpreter disappear.

The transpiled app passes all eighteen checks — same runs, same seventy assertions as the source app, both runs now CI gates. But the result I was actually after was the diff. The two blogs are the same app in two dialects, so their compiled forms should converge, and they do: of 130 lowered methods with shared names, 93 are byte-for-byte identical, and every one of the 37 that differ traces to a column-ordering convention, a deliberate dialect difference visible in the source, or a feature one app has and the other doesn't. Zero differences were attributable to the intermediate representation being secretly ActiveRecord-shaped. That was the research question, and it now has an answer: the IR is Ruby-shaped. Roda and Sequel plug in as a front end, not a fork.

And the diff earned its keep in a way the green test suites couldn't: the exemplar's one_to_many :comments, order: Sequel.desc(:created_at) carries an ordering the synthesized association reader doesn't yet apply. Neither test suite asserts comment order, so both passed while the behavior silently differed — only the IR comparison surfaced it. Two green oracles and a defect between them: the strongest argument I know for diffing artifacts, not just outcomes.

There is no framework

With the IR converged, the native binary was one fix away — the layout's flash["notice"] read needed to become the typed channel the strict compiler could see through. Then Spinel took the whole thing to a 559 KB self-contained executable: no Ruby installation, no gems, boots to its first served request in ten milliseconds, serves the full application — validations, flash, method override, 404s — at 4.5 MB of resident memory, every query a SQL string composed at build time.

Hence this section's heading. At request time there is no Roda and no Sequel — no routing tree walked, no dataset algebra built, no ORM dispatched. There is also no Rails in the binary the Rails blog compiles to. The framework is a language you write applications in, and like the server before it, it melts under specialization into the application it was always describing. Jeremy predicted Roda's speedup would be smaller than Rails' — thinner abstractions, less interpretation to remove — and he's right, which is why the number that matters here isn't a request-rate multiple. It's instant boot and single-digit megabytes for a stack whose users already chose it for being close to the metal.

You can try it without installing anything — the in-browser playground now carries the Roda app next to Rails blog, Lobsters, and Mastodon; edit the routing tree and watch it re-emit in any of ten targets. Or build the binary yourself. Note what's absent from the recipe: Ruby is the source language, not a runtime dependency — nothing below installs it.

git clone https://github.com/rubys/roundhouse.git
git clone https://github.com/rubys/roda-sequel-blog.git
git clone https://github.com/matz/spinel.git && (cd spinel && make)

cd roundhouse
cargo run --release --bin roundhouse -- --target spinel ../roda-sequel-blog -o ../blog

cd ../blog
../spinel/bin/spin build
sqlite3 storage/development.sqlite3 < db/seed.sql   # optional demo rows
./build/bin/blog                                    # http://localhost:3000

(Prefer running it on plain CRuby? --target ruby emits a Puma + Rack tree instead; the exemplar's README covers both paths.)

The caveats are the honest kind, kept in a ledger rather than a rug: this is a deliberately small application, the recognized vocabulary is closed (exotic matchers, virtual-row blocks, and dataset-only models are refused loudly at ingest, not mis-compiled), and the association-ordering gap above is real until fixed. What happens next is not mine to declare — that's the point of the seating chart. The results are in front of the keeper, and the spike lives, dies, or gets tweaked on his read; if it lives, the next hold is a larger, real Roda application, where idiomatic breadth will stress the front end in ways a blog can't.

Four days from a hallway question at RubyConf to a framework author's test suite passing against a native binary of his own stack — with the author, not me, holding the definition of correct the whole way. The compiler didn't get smarter this week. The IR was the right shape, and the oracle had the right keeper. That's the finding.


Roundhouse is open source: dual-licensed MIT / Apache-2.0. Issues and discussion welcome.