Take out your phone.
rubys.github.io/roundhouse/blog
First words, no preamble: "Create an article. Anything."
First: it's just a blog. Scaffold-familiar — index, show,
new. Nothing surprising here, deliberately: establish
"ordinary Rails app" before revealing anything. Then:
"let me open a second copy."
Now the live wire: create an article in the left pane; it
appears in the right — Turbo Streams, with Action Cable's
role played by a BroadcastChannel out of the shared worker.
Two iframes, one running Rails app, one database. Then the
question, not the answer: "You just wrote to a database.
All of you at once. Whose server is that?"
There Is No Server
Beautiful Ruby, Weird Ruby, and the Robots We Live With
Flash the URL bar — github.io, static hosting. "Nobody's.
Each of you is running the entire Rails application —
router, controllers, Active Record, SQLite — alone, in your
tab. Two hundred people just did two hundred deployments."
Close the projector tab. Reopen — the article is still
there (OPFS). "There is no spoon." Then the devtools
flourish: chrome://inspect/#workers → the SharedWorker →
Sources → articles_controller.rb → breakpoint → click
Create → it hits. Optional encore: unpause, then in the
console `await new app.Article({title:"…",body:"…"}).save()`
— every open tab updates.
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true
scope :published, -> { where(published: true) }
end
Eight lines. Says WHAT, not HOW. has_many says nothing
about query plans. validates says nothing about the
runtime. Nothing on this slide says "JavaScript" — and
that's the load-bearing fact of the whole talk. (These are
also the characters falling in the rain.)
Rails is to web applications what SQL is to data.
The compiler is the query planner.
The deployment target is the execution plan.
"Who here has ever tuned a query without changing the SQL?
Same idea."
The compiler is a WebAssembly binary that just loaded into
this page — the query planner also has no server. One target
at a time: Rust (type `validates :date, presence: true`,
watch it shift) → Elixir ("same spec, different execution
plan") → Go (add a schema attribute, watch the struct grow)
→ Ruby ("the strangest of all — typed, annotated,
.rbs-paired Ruby. Hold that thought.") Then hover an
expression: "that's the compiler's whole-app type inference
answering live — no annotations. It also speaks LSP and MCP,
so your editor and your agent can ask the same questions."
The toolbar carries an app picker (blog · Lobsters ·
Mastodon); this beat stays on blog, and the real-app view
(?app=lobsters) returns in Act VIII and Q&A.
Beautiful Ruby (has_many :comments)
│
▼
Roundhouse IR (typed, effect-annotated)
│
├─▶ Rust ─▶ rustc ─▶ machine code
├─▶ TypeScript ─▶ V8 ─▶ machine code
├─▶ Crystal ─▶ LLVM ─▶ machine code
├─▶ Elixir ─▶ BEAM ─▶ machine code
├─▶ Go ─▶ gc ─▶ machine code
├─▶ Python ─▶ CPython ─▶ machine code
├─▶ C# ─▶ RyuJIT ─▶ machine code
├─▶ Kotlin ─▶ JVM ─▶ machine code
├─▶ Swift ─▶ LLVM ─▶ machine code
└─▶ typed Ruby ─▶ Spinel ─▶ C ─▶ machine code ← hold that thought
The bottom row is always the same. And the gate that makes
ten targets a claim rather than a stunt: every commit, CI
boots every one of these and DOM-diffs every URL against
real Rails — twelve configurations (the ten rows here, plus
plain-MRI Ruby and JRuby serving the same emitted tree).
Eleven of the twelve must be empty to merge. The twelfth
tracks Matz's master, unpinned — it's allowed to go red,
and when it does, that's information. Hold that thought.
One more thing for the folks reading along: every language
on this slide is a live link. Click it and you download
that target's .tgz — the actual transpiled app plus the
test suite CI just ran green. That's the homework; not a
phone experience.
There is no interpreter
Specialize an interpreter to one program,
and the interpreter melts away.
What remains is a compiled program.
— the first Futamura projection, 1971
Rails is the interpreter. Your app is the program.
You just watched Rails melt away. Fifty-year-old theory —
GraalVM runs on it. The weird part was pointing it at
has_many.
Not something I invented, not unproven — Yoshihiko
Futamura, "Partial Evaluation of Computation Process,"
1971. The precise version, for a PL audience: strictly
this is partial evaluation with a hand-written specializer
— a hand-built generating extension, the artifact the
SECOND projection would produce — where GraalVM/Truffle
does it mechanically to real interpreter source. Same
theorem, different labor. And the receipt is one slide
away: the bench page's §2, "Lowerer dividend (ruby vs
rails)," is the projection priced — the measured value of
the interpreter melting.
The studio closes the loop from spec to consequence — no
intermediate code. "Everything so far showed you the
execution plan. Now — spec to consequence." Edit validates
in article.rb (drop presence:, or raise the length minimum)
and submit the form in the running app: the error changes.
Run the tests: red. "You just watched a Rails app, its
behavior, and its test suite respond to a spec change, end
to end, in a static page."
"Matz is building an ahead-of-time Ruby compiler. Ruby to C
to machine code. Self-hosting. What would you bet that
isn't real Ruby?" This page is the project's own live
benchmark report — the transpiled blog served by every
target, spinel's native binary among them, published by the
cron bench service. §1 is throughput across targets, the
spinel row among them; if the Futamura slide landed, §2
"Lowerer dividend (ruby vs rails)" is its receipt — same
app, interpreter present vs melted, priced. (One caveat:
the 11.6× geomean is a DIFFERENT measurement — spinel's own
`make bench` vs miniruby, a compiler workload, not this
server page.) The tenth rung is Roundhouse emitting the
shape Spinel compiles — a Rails blog, through
Matz's compiler, to a native binary. "My CI builds his
compiler from master, unpinned. When Spinel regresses, my
build goes red within the hour. We break each other's
builds on purpose."
Ruby was never the slow part
Same app, same CRuby, same YJIT — only the framework melted: 8×
≥ 87% of a Rails request is the framework re-answering
questions whose answers were fixed at boot.
YJIT's verdict: +33% on the melted version. −3% on Rails.
Fresh Arel trees and polymorphic attribute reads, every request —
the inline caches get nothing to hold.
Hand both to the JVM: the gap widens to 27× .
JITs amplify static shape; Rails erases it.
Language ≈ 2–8×. Architecture ≈ 8–27×. They multiply.
The ladder is not an exit from Ruby — it's Ruby's
exoneration. License: I wrote the Rails book; I'm allowed.
All numbers geomean over the five endpoints on the bench
page still warm behind us; -int cells are literally the
same command minus RUBYOPT=--yjit (scripts/bench). For
Matz: spinel takes another 4× on top of the melt — 32× over
Rails at 12 MB RSS vs 309. For Charles: jruby-on-melted vs
rails-jruby is 27×, and jruby/ruby is 8× — "the Ruby JRuby
was built to run," measured. Fairness: Rails does cache
(statement cache, schema cache, Journey automaton, compiled
ERB) — 8× is what remains despite that; Shopify reports
YJIT +15–30% on their Rails benchmarks, this is one
reference app on SQLite (issue #16 caveats); and the
emitted runtime "does less" per request only because the
compiler answered earlier — same output, DOM-diffed.
The robots we live with
real-blog (Rails) ─▶ Roundhouse ─▶ typed Ruby ─▶ Spinel ─▶ native binary
│
┌────── failure ◀───────────────┘
▼
Claude Code analyzes ─▶ drafts issue
▼
Sam files at matz/spinel
▼
Matz + Claude Code design fix
▼
Sam verifies, reverts the workaround
Two humans, two agents, two compilers.
The robots don't replace either of us —
they collapse the distance between us.
The subtitle's third promise, made concrete — this IS the
"robots we live with." The receipt is closer than the
diagram: every commit in this deck's own repository is
Co-Authored-By: Claude — and the specific model rotates
across the work (Fable, Opus, whichever the week's rate
limits leave open), which is its own small proof of the
theme: you don't live with one robot, you keep a bench. git
log is the checkable appendix. The talk about living with
the robots was assembled living with them — the slides, the
rain, the runbook, the audit of these very notes. And
there's a second face to the theme, surfaced at the
Mastodon IDE: the compiler we built this way is itself a
tool for it — your agent asks it can-this-be-nil over MCP,
no app boot, no annotations. Built with robots here; builds
for them there.
72 minutes
matz/spinel #126 · Module assigned to a module-level attr_accessor reads back as 0
02:46:53Z rubys filed — the accessor reads back as 0
03:14:19Z matz Stage 1 landed in 31bade0 — static constant fold
03:39:47Z matz Stage 2 landed in e61124b — runtime sentinel switch
03:58:33Z rubys verified on master — workaround reverted
- def self.adapter; @adapter; end # the workaround
+ class << self
+ attr_accessor :adapter # the Ruby you meant
+ end
All timestamps verbatim from the issue (2026-04-30; total
71m40s). Two humans, two agents, two compilers, one subset
of Ruby. "Raise your hand if you've shipped a fix to
someone else's compiler in 72 minutes." The exchange,
verbatim: matz 03:14 — "Covers the static-fold case, which
I think matches Roundhouse's adapter-slot pattern"; rubys
03:58 — "verified Stages 1 and 2 against spinel master,
with one correction I owe you." Stage 3 stayed shelved by
mutual agreement.
What doesn't work
The line isn't the keyword — it's whether the
answer exists before the app runs.
method_missing — the answer only exists at runtime. Never.
define_method — a diagnostic today; statically
knowable definitions are tractable.
C extensions — a wall for most targets. Spinel speaks FFI,
so there it's a door.
Every unsupported construct becomes a diagnostic.
The diagnostic list is the roadmap, not a gate.
The nuance, for the skeptics: the boundary is static
determinability, not a keyword blocklist. And: has_many IS
define_method under the hood — the audience has been
watching statically-determinable metaprogramming compile
all talk; Rails DSLs resolve because their inputs are
static symbols in the source. Fed a 7,000-line production
codebase (Lobsters), it ingests everything, types most of
it, and hands back a ledger of exactly what it can't yet
model. "A working system, not a complete one — and it knows
the difference, which is the part that matters." And that
ledger is showable, not just assertable: the Mastodon IDE
surfaces it live on ~83K lines — candor you can point at.
The rewrite
Rails gets you to market.
That part, everyone agrees on.
The startups that survive face the sequel:
the rewrite. Proverbially, in Go.
What if the rewrite were a build target?
Go — or nine others. Readable, idiomatic, yours to evolve.
JetRockets' defense is right about today, and worth saying
plainly: "This year WIRED called Ruby not a serious
language. JetRockets' response conceded both criticisms as
technically true and argued neither is practically
relevant. For most apps, they're correct — most never hit
the ceiling. But take the criticisms seriously, because
there's one moment they genuinely bite: the day success
forces the rewrite. Types and performance are exactly why
that rewrite happens." Honesty carried over from the last
slide: early development, real promise — the ladder emits
Go today for the subset, CI DOM-diffs it against Rails
every commit. And the kicker: unlike a faster runtime, the
output is code — you could take the Go and evolve it
independently. Ejection, not lock-in. The rewrite you were
dreading becomes an artifact you own.
Where this goes
Every chronic Rails weakness lives at a language boundary —
where Ruby ends and JavaScript, WebDriver, a native bridge,
or a C extension begins.
Don't manage the boundary. Compile across it.
System tests: 21/21 in under 100 ms, deterministic, on every save —
no browser in the loop. Kotlin and Swift without the bridge is issue #41.
The same whole-app inference, pointed backwards into your
editor and your agent — types, nil-safety, can-this-be-nil for plain Rails.
No annotations, no app boot. Real today: it answers on lobste.rs in under a second.
Or skip the editor — an IDE in a tab, preloaded with all of Mastodon:
1622 files, ~83K lines, whole-app inference in under 3 s. The IDE has no server either.
— me, 2026 · intertwingly.net → "Live Types for Rails"
Two threads. Seed 1 is the BOUNDARY thesis — every chronic
Rails weakness is a language seam: browser JS (the
bundler/React pull), system tests (DHH's "system tests
have failed" — every flake layer he indicted is BETWEEN
languages: Capybara→WebDriver→Chrome→HTTP; posts 3399/3400:
same Capybara DSL, tens of ms, deterministic, every save),
mobile (Hotwire Native / React Native are bridge-management;
#41 is filed not built — an issue, stay honest), ecosystems
(JRuby's real trade was C extensions FOR the Java world;
every target repeats it). Seed 2 is PRESENT tense — the
hover already seen in the playground is this engine — and
the SAME playground opens on the real thing:
/roundhouse/playground/?app=lobsters hovers can_be_nil on
lobste.rs live. The IDE: /roundhouse/ide/?app=mastodon —
Monaco over the wasm analyzer in a Web Worker, preloaded
with Mastodon, CI browser-smoked per commit. And
CQRS-as-build-flag: reads into the client, writes into the
server, Rails already told the compiler which is which.
The Act VIII "IDE in a tab, ~83K lines of Mastodon" line,
made real. The crescendo is the picker: the room met the
blog (Act I), can meet Lobsters on request, and here is
Mastodon — same engine, growing app, growing ambition.
Correctness → real-world coverage → scale. Open a file,
hover a type ("no annotations — this is inferred"), jump to
a definition, then the honesty payoff: surface the gap
ledger. "Hundreds of things it can't model yet, on the
exact lines. A working system that knows what it isn't —
that list is the roadmap." The thesis: "the type system of
the largest Rails app, resolved in a tab. No server here
either." Honesty guardrail: this shows the ANALYSIS
(rock-solid, CI browser-smoked per commit) — it doesn't
claim Mastodon RUNS; the transpile is partial, and the
visible ledger is exactly that candor. Theme harvest: this
is the SECOND robots-we-live-with — the same type answers
your agent consumes over MCP. Act VI built with robots;
this is building for them.
"The JavaScript you write not only isn't the JavaScript you
run, but under closer examination isn't even JavaScript at
all."
The Ruby you write isn't the Ruby you run.
It's Rust. It's your phone.
It's Matz's C.
Or nothing at all — there is no server.
Continue the argument
Everything here is public, speaker notes included: anyone
who wasn't in the room can read the whole talk, and anyone
who was can pick the argument back up in the hallway or
online. The QR is the deck itself. Things to try: the
studio (edit validates → error + red tests), the
worker-console Article.save() broadcast, can_be_nil on
lobste.rs live (/roundhouse/playground/?app=lobsters — hover
the scope), the in-browser IDE on Mastodon
(/roundhouse/ide/?app=mastodon), CQRS-as-build-flag.
Playground and IDE both carry the app picker; ?app= boots
straight into one, so each real-app view is a single URL.