intertwingly

It’s just data

The Compilers Were Ready


A fair question to ask after reading about Roundhouse: why not just use YJIT? Or ZJIT, when it's ready? Or wait for Spinel? Each is a serious project. Each is doing real work. Each has put Ruby ahead of where it was a year ago.

The answer matters because it sharpens what Roundhouse is and isn't.

The compilers and the input

What each is doing:

None of these is weak. All of them are operating near the limit of what their input shape allows.

Take one line from a Rails controller:

@article.update(article_params)

article_params returns an instance of ActionController::Parameters — Rails' wrapper around HashWithIndifferentAccess, populated by a permit call. Inside update, the framework iterates the wrapper, dispatches per-key into typed setters, and persists.

The Parameters wrapper holds polymorphic values; the value type returned at every key access is Object. Each compiler runs into that fact in its own register: V8's inline cache pushes toward megamorphic, YJIT specializes the receiver but not the value, ZJIT's method-level type feedback shows polymorphic value flow, Spinel's whole-program inference can't narrow untyped flowing through a polymorphic container — twenty sp_RbVal errors at every typed boundary in real-blog before the per-resource specialization PR landed last week.

Same Rails idiom. Different compilers. The same fundamental obstacle.

The wrapper isn't structurally wrong for CRuby — Rails has been running this shape for fifteen years, and CRuby's interpreter doesn't care about shape stability the way JITs and AOT compilers do. It is a fine shape for an interpreter. It is a hostile shape for any compiler, regardless of strategy.

The shape problem

What every modern compiler — JIT or AOT — is built to do is specialize. Inline caches, hidden classes, basic-block versioning, monomorphization, type feedback, whole-program inference: these are different names for the same fundamental move. Find a stable shape. Generate optimal code for that shape. Bail to a slow path if the shape changes.

When the shape never stabilizes, the optimal-code path never gets taken. The slow path is always taken. The compiler runs, but its output is the conservative version that handles every possible shape.

Rails idioms produce shape instability by design:

These aren't accidents. They are how Rails buys its expressiveness: one Parameters class handles every controller, one Relation class handles every model, one association DSL handles every relationship. The compiler-hostility is the cost of the abstraction. CRuby pays nothing for it. Every other compiler pays everything.

Two vantage points

Maxime Chevalier-Boisvert, YJIT's lead, has named this problem from the JIT side: a common Rails memoization idiom (@x ||= compute) produces shape transitions that pessimize inline caches. The remedy she proposes is downstream — smarter basic-block versioning, larger inline caches. That's where the YJIT team sits, and from that vantage point it's the right cut.

It's Conway's Law applied to performance work. A JIT engineer sees a Rails shape problem and finds an opportunity to make the JIT smarter. A transpiler author sees the same problem and finds an opportunity to rewrite the idiom. Both responses are reasonable from where each sits. The bet this post argues for is that they compose: lower the idiom upstream, and let the smarter JIT specialize on what's left. The dividend stacks.

What changes when the input is shape-stable

A week ago, the closed-axis specialization PR landed. The set of resources in real-blog (Article, Comment) is closed; lower it. Per-resource ArticleParams { title: String, body: String } and CommentParams { commenter: String, body: String, article_id: Integer } classes get synthesized at lower-time. Per-model ArticleRow / CommentRow carry the schema-derived field types. The constructor and permit call sites get rewritten to use the typed forms.

This week's work pushed the rewrite into the call sites:

What does each compiler now see?

Same lowerer. Every compiler benefits. The dividend doesn't accrue equally — Rust gains the most because it can't compile dynamic code at all; V8 gains less because it could already specialize at runtime — but every compiler benefits, and the work happens once.

Compiler-friendly Rails, automatically

There's a familiar pattern in compiler-heavy communities: writing for the optimizer. C++ programmers avoid pointer aliasing. Rust programmers structure code around monomorphization. Game engine and database authors lay out data for cache locality. The discipline goes by different names but it is the same idea: write source the compiler can do its best work on.

Rails programmers don't do this. Nor should they. Rails' value is precisely that it lets you not. Conventions, DSLs, generic wrappers — these are how Rails buys productivity.

What Roundhouse does is slot the compiler-friendly transformation between the Rails source and any backend compiler. The Rails source stays Rails-shaped. The lowered IR is compiler-friendly. Whatever compiler runs on it next — TurboFan, YJIT, ZJIT, Spinel, rustc, the Crystal compiler, the Go compiler — gets input that lets it do what it was designed to do.

The compilers were ready. The input wasn't.

That is the cleanest one-line summary I have for what Roundhouse occupies in this corner of the design space. It is not a Ruby implementation. It is not competing with the JITs. It is a frontend — in the compiler-construction sense — that produces compiler-friendly Rails, and lets every existing compiler do the rest.

What this changes

The relationship between Roundhouse and the existing Ruby compiler ecosystem is additive, not zero-sum.

The most reachable audience is Rails shops unhappy with current throughput on their existing stack. The deploy step is one extra line in the Dockerfile, alongside the bootsnap precompile most production Rails shops already have:

RUN bundle exec roundhouse build
RUN bundle exec bootsnap precompile app/ lib/

No language migration. No runtime swap. Same Rails app at edit time; shape-stable lowered Ruby downstream of bootsnap, ahead of CRuby and YJIT. This is the first deployment pattern Roundhouse enables that requires zero migration.

Beyond that, Roundhouse opens deployment surfaces the source language can't currently reach: Rails on Cloudflare Workers, Rails as a Crystal binary, Rails as a Rust microservice. There the existing toolchain isn't being made better — it's being replaced because the source language is the wrong language for the deployment surface. Roundhouse's TypeScript emitter is already the substrate for the Workers profile; the Crystal and Rust emitters sit at varying maturity behind it.

Either way, Roundhouse is upstream of the existing compiler ecosystem, not parallel to it.

The next contraction

The Parameters work specializes data shapes — values, fields, rows. The natural next move is specializing methods themselves.

If @article.update(article_params) is the only call site for Article#update carrying ArticleParams, Roundhouse can synthesize an Article#update_from_article_params(params: ArticleParams) method with explicit field-by-field assignment, and rewrite the call site to invoke it. The dispatch surface contracts further: monomorphic receiver, monomorphic argument type, known body shape. What was previously a generic update method behind dynamic dispatch becomes a concrete method behind a single call.

Each level of specialization gives every backend compiler stricter shapes. Each level lands once at the lowerer and benefits every emit target. The thesis at the top of this post — the compilers were ready; the input wasn't — implies the direction: keep contracting the input until the compilers' best work has nothing left to fight.

First slices of this are landing now: per-model _adapter_* primitives replacing generic ConnectionAdapter dispatch, with ActiveRecord::Base's CRUD methods delegating to those primitives; an Arel IR that evaluates query trees at codegen time rather than runtime. The update_from_article_params sketch above is illustrative; the actual landings are different concrete instances of the same compile-time-first move. The principle is the same: contract the dispatch surface, push specialization upstream, hand each backend stricter shapes.

What's still open

The Parameters work is one pattern among several. The Relation lowering, the belongs_to/has_many lowering, and the active_record column-typing each address a different shape-instability source. Each is a per-pattern job. Each lands once, and benefits every backend.

The compare-gate ratchet still drives. Real-blog TypeScript holds at 5/5 paths and 8/8 framework_tests. Real-blog Spinel landed at 6 errors after last week's PR; today's Level-3 and Arel-IR landings on the Roundhouse side surfaced new shape patterns, the spinel side closed thirteen issues from those patterns in the same day, and the residual is concentrated on a handful of recently-filed ones. The current count is mid-flight, not a milestone. Each gate is a forcing function; each forcing function drives the next lowerer.

Rails was already typed. The compilers were already ready. The work in front of us is reading what's in the source and shaping it into what compilers know how to use.


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