Spinel at Your Fingertips
For anyone arriving cold: Spinel is
Matz's ahead-of-time Ruby compiler. It reads a whole program, infers a
type for every value it can, and emits C; where inference succeeds you get
an unboxed sp_int in a C struct, and where it cannot settle on one type
a value is widened to untyped and takes a boxed, dispatched slow path.
It also refuses outright a documented list of things it does not compile.
Those two verdicts — refused, and widened — are the whole story of whether
a program is fast under Spinel, and until this week the way to see them
was spinel --emit-types writing a JSON file, --emit-rbs writing the
inferred signatures, and the refusals on stderr.
I wanted them at my fingertips. Three things now exist, in one repository, rubys/spinel-ide, and I would like you to try them.
The page
rubys.github.io/spinel-ide is
Spinel in a browser tab. The compiler itself — the same C sources as the
native binary, built by the wasi-sdk — runs as WebAssembly in a worker.
Type a program and it is re-analyzed on every change: hover a name for
the type Spinel inferred, see refusals as red markers and widenings as
yellow ones, and read the RBS it settled on and the C it emitted in the
tabs on the right. Press Run and the program runs, in the tab: the
samples from their precompiled wasm32-wasi builds, an edited program
by compiling the emitted C with clang — also as WebAssembly, fetched
once — in about a second.
Start with the sample called widening — the slow path. Two Item.new
calls, one with an Integer and one with a Float, give @price two types,
and the Diagnostics tab shows the consequence: the initializer and total
both took the boxed path. Make both prices Integers and watch @price
narrow to Integer in the Signatures tab while total keeps its warning,
because a literal array of objects is still a poly array. That one edit is
most of what a person has to learn about writing Ruby that Spinel compiles
well, and the page lets you learn it in thirty seconds instead of a
compile-and-read-the-C cycle.
The refusal sample shows the other verdict. Since last week Spinel reports every refusal in one run rather than stopping at the first — Matz made that change in response to the request that produced this page — and the page marks each one.
The language server and the MCP server
tools/spinel-lsp.rb is the same answers over the Language Server
Protocol: diagnostics as you type, the inferred type on hover, and — the
part I think people will screenshot — an inlay hint after every def
showing the signature the compiler inferred and you never wrote:
def dist2(o) : (untyped) -> Integer
with a code lens above it saying slow path or fast path. It is
read-only, it works on any single-file program or one held together with
require_relative, and it speaks plain LSP: there is a thin VS Code
client in the repository, which is the one editor it has been used from so
far, and the README has the Neovim and Helix configuration, which nobody
has tried yet. The first report from either will be from one of you.
tools/spinel-mcp.rb is the same answers as tools for a coding agent:
wont_compile, diagnostics, type_at, signatures, and c_for, which
hands back the C for one method so an agent can check whether a parameter
arrives boxed. It is stateless — every call re-runs the compiler on what is
on disk — so it is exactly as trustworthy as the compiler and never stale.
The README has the three-line Claude Code configuration.
Both are written in the subset of Ruby that Spinel compiles, and both run
either way: ruby tools/spinel-lsp.rb with no compile step, or spinel tools/spinel-lsp.rb -o spinel-lsp for a static binary. Whenever the
repository rebuilds against a newer Spinel, it compiles both, runs a
scripted session against the CRuby form and the binary, and requires the
answers to agree — two more real programs, of the long-running,
stdin-reading shape, that Spinel has to keep compiling correctly.
What is honest to say about them
They are early. The page handles programs of a size Spinel compiles in
tens of milliseconds, not an application; the language server analyzes
synchronously, so a large program makes the editor wait; there is no
completion and no go-to-definition. The compiler in the tab is a 32-bit
Spinel, so Integer there is 32 bits, as the page's footer says.
Some of those limits are the tools' and will go away with work. The more
interesting ones are the compiler's, and they are the reason the tools
exist. --emit-types records a start position for every node and no end,
so a hover on pts in pts.map { … }.inspect has to show three types and
cannot say which is which; it records no node kind or name, so nothing can
find the other uses of pts; a widening warning names the method and not
the parameter, so the marker sits on def rather than on o; and what
codegen decided at a call site — a direct call, a class switch, a boxed
send — is visible only in the emitted C, which nobody reads. None of those
is an API. Each is a field the compiler could add to a file it already
writes, and now there are three consumers that would show the difference
the day it landed. That ask is
matz/spinel#4522, with the
four items spelled out in the repository's README under
what the compiler doesn't say yet;
it is a better ask with your reports attached.
Credits
Matz built the wasm32-wasi target for Spinel within a day of my raising
it, and declined to carry an IDE or a language server in the compiler's
tree — his principle being that a tool its maintainers do not use every
day does not stay working. This repository is what "build it out of tree
over what the compiler emits" looks like, and I hope it makes the case
the other way. Roundhouse has the same kind of tools, and I have found
them invaluable — not for the times I know there is a problem and go
looking for its cause, but for the serendipitous finds that come from
having the compiler's output always at my fingertips, and for the loop of
making a change and seeing what it did. My hope is that the people who
contribute to Spinel, and the people who use it, come to share that
experience. The browser side is adapted from
Roundhouse's own /ide/: the
worker, its watchdog, and the Monaco wiring. The compiler in the tab is
possible because of @yowasp/clang,
Catherine "whitequark"'s build of clang and wasm-ld as WebAssembly, and
bjorn3's WASI shim runs
everything the page runs. Most of the code was written with Claude over
two days, most of the words in this post too, and the rebuild-and-test
step exists because a demo that is not kept current with the compiler it
demonstrates is a demo that will be broken the week you read about it.
What I would like from you
Try the page with a program of your own — paste it in, hover things, press Run. Try the language server in your editor. If you use Claude Code, wire up the MCP server and ask it whether your program compiles. Then tell me, in an issue on the repository (the README says what a useful report contains): what did you hover over, and what did you expect to see? What did a diagnostic say, and what would have helped? What did Run do that surprised you? A report that says "I expected the type of the whole expression and got three candidates" is worth more to the conversation with Matz than any argument I could make, because it is a person wanting something the compiler could say and does not yet.
![The Spinel IDE on the widening sample: a hover on items shows Array[untyped] · untyped, and the Diagnostics tab lists the two methods that took the slow path](/blog/images/spinel-ide.png)