Rails on Roda
Two days ago I ended a post deliberately in mid-air: the Roda spike's results were sitting in front of Jeremy Evans, and whether it lived, died, or got tweaked was his call to make, not mine. That was the point of the seating chart — for this episode, the keeper of the oracle is the framework's author.
The verdict came back Tuesday evening, and it's worth watching what a keeper actually does with the seat. He didn't read the results; he ran them. He installed the toolchain, generated both applications — the CRuby tree and the native binary — booted them, and measured for himself: 45,356 KB for his hand-written Roda/Sequel app, 43,332 KB for the transpiled CRuby version, 4,720 KB for the Spinel binary. Numbers I'd published, now independently reproduced on someone else's machine, by the person with the most standing to be skeptical of them.
Review that runs the artifact
Reproduction came with a review, and the review found what no test
suite of mine was positioned to find — because every item came from
using the artifact rather than checking its assertions. bundle install on the generated Roda tree was installing all of Rails
(dependency baggage of two asset gems the app never loads).
websocket-driver loaded at boot in an app with no websockets. And the
native binary, started when its port was already taken, exited silently
— which on investigation was worse than he reported: the bind failure's
return code was being discarded, so the process exited zero, looking
for all the world like a server that started and vanished.
All of it was fixed by mid-morning — the Gemfile now trims to what the
source application actually uses, the binary fails loudly and grew a
small --help/--port CLI — and none of it is the story. The story is
that the review loop closed at all: the keeper's job description from
Maintaining the Oracle
included overruling green dashboards, and the way you earn that power
is by running the thing itself.
The keeper writes the spec for the next tool
Buried in my previous reply had been a question. His very first comment
in the thread, weeks ago, had floated an "AI-generated conversion of
the Rails example app" as one way to get a Roda exemplar. We built the
exemplar by hand instead — but the idea had a deterministic version:
the intermediate representation already forgets its source (that was
the 93-of-130 finding), so the same machinery that ingests Rails and
emits ten targets could, in principle, ingest Rails and emit idiomatic
Roda/Sequel — re-nesting the flat route table into an r.on tree,
rendering the query IR as Sequel's dataset algebra. Was that worth
building, or a curiosity?
His answer settled it: a Rails ↔ Roda/Sequel translator would be "both interesting and useful" — useful because real applications are stuck in whichever framework they started in; the conversion costs are, his word, prohibitive. And then, without being asked, he did the keeper's whole job in three sentences. He set the usefulness bar: a correct routing tree without duplicate branches would be quite useful by itself, even if the output is visibly machine-shaped. He set the failure policy: convert exactly what converts, and leave everything else as a comment carrying the original code, so a human finishes the residue by hand. And he specified the acceptance test: ingest the original application and the converted application, and "check whether they result in the same IR."
A bar, a policy, and a test — authored by the person the tool would serve, before the first line of it existed. Every episode of this project that has gone well has had that shape.
A morning, wall clock
His comment arrived Tuesday at dinner time; by Wednesday lunch the converter was built, gated, and live in the playground. Three things made a morning sufficient, and none of them was the compiler getting smarter.
First, the converter emits from the ingest-shape IR — the
representation before lowering — and that choice was the morning's one
real design decision. The lowered IR is runtime vocabulary: by the time
the other ten targets see a query, it's already a folded SQL string,
which is exactly what you want for execution and exactly what you
don't want for translation. One level up, the ingested expression still
says Article.includes(:comments).order(created_at: :desc), and at
that altitude the mapping to Sequel is nearly word-for-word. The first
emitted index action produced Article.eager(:comments).reverse(:created_at).all
— character-for-character the line Jeremy's review had put in the
exemplar.
Second, the route tree came out right by construction. The flat route
table rebuilds through a segment trie, and a trie structurally cannot
emit the duplicate branches his bar named. The member :id and the
nested resource's :article_id land on the same tree position and
unify into a single r.on Integer node; the Rails before_action
that loads @article becomes next unless @article = Article[id] at
that interior node — which is precisely the shared-interior-state seam
Jeremy flagged in the ingest direction three weeks ago, now being
emitted in reverse.
Third, and least obvious in advance: the exemplar he reviewed line by
line turned out to be the style guide. "What would a Roda developer
actually write" is the question that makes machine translation hard,
and it had already been answered, construct by construct, by a Roda
developer with commit rights to Roda. form_with blocks become the
hand-rolled <form> with the hidden _method override because that's
what his review put in the exemplar; params.expect becomes
set_fields with the same allow-list for the same reason. Where the
Rails app uses something with no Roda/Sequel equivalent — its Turbo
broadcast declarations, the format.json branches — the converter does
what he specified: the original source rides along as a comment, and
nothing is ever silently mistranslated.
Two gates, and the two failures that matter
His acceptance test is now a committed CI gate, implemented as
specified: ingest fixtures/real-blog, ingest the converted output,
assert the IR converges. It passes — routes, schema, and model shapes
are equal, with each normalization documented where it happens.
The behavioral gate is better. The exemplar carries its own test suite — eighteen checks when the previous post shipped, nineteen now, the new one pinning the 404 his Integer-matcher catch earned — whose header declares the contract: a transpiled version of this application must pass it unchanged. Pointed at the converted Rails app, it scores 17 of 19 — and the two failures are the most informative result of the week. Neither is a converter bug. The exemplar's minimum-length validation carries a custom message; the Rails model has none. The exemplar orders comments newest-first; the Rails association is unordered — the same ordering divergence the previous post disclosed as an open gap, now visible from the other side of the mirror. Two hand-written applications, built to be domain-identical, reviewed and tested green on both sides, genuinely disagree in two places — and the instrument that finally made both disagreements undeniable was one app's suite run against the other app's conversion.
Last week's post argued for diffing artifacts, not just outcomes, after the IR comparison caught a defect two green test suites had missed. This is the same lesson with the arrow reversed: domain-identical is a claim; the diff is the check. The converter, forced to pick a side, sided with its source — and whether the two fixtures should now converge, and in which direction, is a question that sits where every correctness question in this episode has sat: with Jeremy.
Two-way street
The finding worth stating plainly: the intermediate representation is
now load-bearing in both directions. Last week's claim was that the IR
isn't secretly Rails-shaped — Roda plugs in as a front end and
compiles out to anything. This week's claim is stronger: the middle is
a genuine interlingua. Target now includes another framework's own
idiom, running on that framework's real gems — no Roundhouse runtime
anywhere in the output. Rails goes in; an application Jeremy could
bundle install and read comes out.
Notice also what kind of translation this is. The industry's current answer to "convert my app to another framework" is to hand the source to a language model and read the paraphrase carefully. This is the other thing: a deterministic pipeline with a reviewed style reference, a failure mode that quotes rather than invents, and an equivalence test proposed by the framework's own author. It is visibly machine-shaped, exactly as far as the keeper said machine-shaped was acceptable — and it's a starting point a human takes over, which is the use he named for it.
There's a pleasing inversion at the end of this, too. Every other target in the matrix works by making frameworks disappear — the Rails blog compiles to a binary with no Rails in it. This target does the opposite: its entire output is framework, someone else's, as its authors intended it to be written. Specialization taketh away, and specialization giveth back.
Try it
The playground now carries roda in the target dropdown — the Rails blog is the default source, so one selection shows the conversion, live and editable, in the browser. (Choosing the Roda app as the source and roda as the target round-trips it, which is its own kind of test.) Or from a checkout, with Ruby and the real gems — for once, the output's dependencies are the point:
git clone https://github.com/rubys/roundhouse.git
cd roundhouse
cargo run --release --bin roundhouse -- --target roda fixtures/real-blog -o ../blog-roda
cd ../blog-roda
bundle install # roda, sequel, sqlite3 — the real ones
bundle exec ruby -r ./db -e "" # migrations run on load
sqlite3 db/blog.db < db/seed.sql # optional demo rows
bundle exec rackup # http://localhost:9292
The caveats stay in the ledger: the recognized vocabulary is the scaffold surface plus what the blog exercises; a larger application will hit constructs that today degrade to commented originals; the JSON API surface and Turbo live-updates don't convert, they annotate. Each of those is a line item, not a rug.
What happens next is, once again, not mine to declare. The reply that posted these results ends by asking Jeremy what he'd want: the reverse direction — Roda to Rails on real Rails — a larger application through the converter, or hardening of what exists. The keeper who reviewed the exemplar, set the converter's spec, and proposed its acceptance test now holds the roadmap too. Tuesday he asked, in effect, whether the mirror had a second side. Wednesday morning it did. The compiler didn't get smarter overnight — the IR was already the right shape, and this time the spec, the bar, and the test were all waiting before the work began. That's what a morning buys you, when the oracle has the right keeper.
Roundhouse is open source: dual-licensed MIT / Apache-2.0. Issues and discussion welcome.