intertwingly

It’s just data

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.

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

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.