intertwingly

It’s just data

Hover Over the Difference


The Roundhouse demos have been public for a while. I never pointed at them, because pointing at a demo invites the question how is this different from the last one? — and until recently the honest answer was "wait for the parts that make it different." Those parts have landed. So: three demos, and what's underneath each.

A warning up front. If you've read this blog, these will look familiar. There's a blog that runs entirely in your browser with no server — I first showed one of those for Juntos in January. There's a playground that converts Ruby as you type — Ruby2JS has had one of those for years. There's a studio that lets you edit a Rails app and watch it rebuild and run live — Ruby2JS has that too.

The resemblance is not a coincidence. I wrote both. Roundhouse's blog is a faithful port of Juntos's browser deployment target, down to the names in the code. But the family resemblance is skin-deep, and the differences are the reason I'm writing this. Ruby2JS is a Ruby-to-JavaScript transpiler: pattern-match the source, emit JS, lean on the fact that JavaScript is dynamically typed. Roundhouse is a typed cross-compiler: it reads a Rails application into a fully type-inferred intermediate representation and emits from there to nine languages — Rust, Go, Crystal, Kotlin, Swift, Elixir, Python, TypeScript, and Ruby itself. Same surface. Different machine. Here's where the machine shows.

The blog

rubys.github.io/roundhouse/blog is the quintessential Rails blog — articles, comments, Turbo Streams, real-time updates across tabs — running with no server. Open it, write a comment, open a second tab; the comment appears in both. Close the browser and reopen it; your data is still there.

The Juntos demo did this with Dexie and IndexedDB, everything on the main thread. Roundhouse runs SQLite — compiled to WebAssembly — with OPFS persistence, across the three-tier worker architecture I described for Juntos in March: a SharedWorker for the application logic, a dedicated Worker for the database, the main thread for Turbo and Stimulus. The reason for the heavier setup is the database choice. IndexedDB runs fine on the main thread; SQLite's fast file access only exists inside a dedicated Worker, and once the database lives in one place, a SharedWorker is what lets every tab share it. The architecture is Juntos's, ported; what changed is the data tier underneath it.

Superficially: same blog, in your browser, no server. Underneath: a real SQL database where there used to be a key-value store.

The playground

rubys.github.io/roundhouse/playground looks, at first glance, like every Ruby-to-something converter I've ever built: a file tree, an editor, an output pane, transpile-as-you-type. The Ruby2JS converter does exactly this. The difference is two things that converter structurally cannot have, because it has no types.

The first is the target dropdown. The same Rails source emits to TypeScript, Go, Rust, Python, Elixir, Crystal — switch the target and watch the same model become idiomatic code in each. Ruby2JS's converter varies the ES level and the filter set, because JavaScript is its only target. Roundhouse's varies the language, because emitting nine of them is the entire point.

The second is the one I'd actually open it for. Hover over a variable — say comment inside the _comment partial — and the playground tells you its type is Comment. That label is not a guess. The compiler derived it by walking backwards: the partial's local came from render @article.comments in the show view, @article came from Article.find in the controller, .comments resolved through the has_many on the model, and Comment's shape came from db/schema.rb. Five hops, across five files, and the hover is the terminal. Hover comment.commenter and you get String, because the schema says so. Type comment.commenter + 1 and a red marker appears under it, live, because adding an integer to a string is an error and the compiler knows it.

Then do the thing that surprised me the first time. Add a column to db/schema.rb. There's nothing to watch — the playground retranspiles all 67 files in 18 milliseconds, which is another way of saying it was done the moment your finger left the key. Switch the output to the model and the getter and setter are simply there. Use the new column in a view and it's already the right type on hover, already free of errors. The change doesn't ripple from schema to model to view while you watch; by the time you look for it, it has already happened.

Neither of those demos can exist on a transpiler that doesn't infer types. Ruby2JS's converter shows you JavaScript; it never shows you a type, because there isn't one to show. That difference isn't cosmetic — it's the whole bet the project runs on. Ruby's + means integer addition, string concatenation, array concatenation, or a runtime error depending on the operands. To emit correct code for that one operator — in Rust where types are mandatory, but equally in TypeScript and Elixir where they aren't — the compiler has to know the operand types at the site. A heuristic transpiler gets that right most of the time and silently wrong the rest. The playground is that inference, made into something you can hover over and poke at.

It's also a quiet way to check my work. A recent post showed how one Rails line —

@articles = Article.includes(:comments).order(created_at: :desc)

— compiles down to the explicit SQL prepare, the row-by-row drain, and the IN-list eager load. I quoted the output there. In the playground you don't have to take my word for it: open the controller, pick a target, and read what that line becomes — across Rust that must be typed, Ruby that feeds the JVM's JIT a 5–6× dividend, and the TypeScript that runs the blog above.

The studio

rubys.github.io/roundhouse/studio is the blog, editable. Change a view, a controller, a model; the app recompiles in your browser and the running blog updates. The Ruby2JS editor does the same, and does it impressively — it boots a WebContainer, a full Node.js running in WebAssembly, and runs a real Vite dev server inside it. That's faithful: what you see is exactly what a production build produces. It's also heavy — two npm installs, a hundred-plus packages, a Node environment that doesn't have find — and it only renders the preview in Chromium, because the WebContainer needs cross-origin isolation that Firefox won't grant it.

Roundhouse's studio takes the lighter road. The compiler is the Rust transpiler built to WebAssembly — the same one the playground uses, about 3 MB. Bundling is esbuild-wasm, loaded from a CDN. No Node, no container, no npm, no Vite dev server, no cross-origin-isolation headers. The trade is honest: esbuild is not the Vite build the published blog actually ships with, so the studio's output approximates production rather than reproducing it. What you get for that trade is a near-instant edit loop and a studio that runs in Firefox as well as Chrome.

Superficially: edit Rails, watch it run. Underneath: a 3 MB compiler where there used to be an operating system.

What's actually different

Strip the three demos down and they're one compiler with three front-ends. The blog is the compiler's output, deployed. The playground is the compiler's analysis, exposed — the type inference and the emitted code, live. The studio is both, wired into an edit loop. The thing they have in common — a typed intermediate representation that every target is generated from — is exactly the thing the Ruby2JS demos don't have and never needed, because their one target was a language that doesn't ask.

That's the difference worth the post. Not "Roundhouse is faster" or "Roundhouse is lighter," though the studio is lighter. It's that these demos are evidence for a specific set of claims I've been making here for two months — that Rails apps can be completely typed by inference, and that a typed Rails app compiles to nine languages. The playground lets you hover over the first claim and switch the dropdown on the second.

The honest part

The post that introduced those Juntos demos ended by wondering whether to rewrite the whole thing in Rust. Roundhouse is that rewrite — and it turned out to change more than the implementation language.

That's the optimistic read. The honest one: I'm not claiming parity. Roundhouse is a couple of months old and its emit is still calibrated to one fixture — this blog. Ruby2JS has years of accumulated feature coverage and a real test suite; Roundhouse has a single small app it does well. The demos are an invitation to look, not a maturity claim. The next thing I want to know is how far the subset extends, and the only way to find out is to feed it bigger applications — which will be the subject of a later post, not this one.

For now: the blog, the playground, the studio. They look like demos you've seen before. Open the playground, hover over a variable in a partial, and you'll see the part you haven't.


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