intertwingly

It’s just data

Two Compilers, One Subset


Two days ago I argued that Spinel and Roundhouse drew the same metaprogramming cliff from opposite directions — overlapping inputs, non-overlapping value. That was correct as far as it went. The work since suggests the overlap is sharper.

Roundhouse needs a single internal IR shape — what every emitter (Rust, TypeScript, Crystal, Go, Python, Elixir, Ruby) consumes after Rails-DSL idioms have been lowered into explicit method bodies. The shape that solves that problem is metaprogramming-free Ruby with statically resolvable dispatch, explicit per-column accessors, and no eval / send / define_method-over-runtime-data.

That shape is the Spinel subset.

I didn't aim at it. It emerged from solving the per-emitter coverage problem — the same pressures that pushed Matz toward his subset push roundhouse there too. Two practitioners, working independently, arrived at the same intermediate language because the constraints are the same: whole-program inference doesn't work without it; correct code generation in any target — even dynamically-typed ones — doesn't work without it either.


The convergence is concrete

Here is app/models/article.rb from fixtures/real-blog, generated by rails new plus the standard scaffold:

class Article < ApplicationRecord
  has_many :comments, dependent: :destroy

  broadcasts_to ->(_article) { "articles" }, inserts_by: :prepend

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

Eleven lines. Idiomatic Rails. Heavily reflective: has_many defines methods at runtime, validates registers callbacks discoverable only by instance_variable_get, broadcasts_to hooks lifecycle events through metaprogramming.

Here is the same Article in fixtures/spinel-blog — hand-written to the post-lowering shape that every roundhouse emitter is converging on:

class Article < ApplicationRecord
  attr_accessor :title, :body, :created_at, :updated_at

  def self.table_name; "articles"; end
  def self.schema_columns; [:id, :title, :body, :created_at, :updated_at]; end

  def initialize(attrs = {})
    super()
    self.id         = attrs[:id] || 0
    self.title      = attrs[:title]
    self.body       = attrs[:body]
    self.created_at = attrs[:created_at]
    self.updated_at = attrs[:updated_at]
  end

  def validate
    validates_presence_of(:title) { @title }
    validates_presence_of(:body)  { @body }
    validates_length_of(:body, minimum: 10) { @body }
  end

  def comments
    Comment.where(article_id: @id)
  end

  def before_destroy
    comments.each { |c| c.destroy }
  end

  # broadcast hooks elided; ~30 more lines for after_create_commit / after_update_commit / after_destroy_commit
end

A hundred and twenty lines for the full file. Same semantics. Every Rails idiom expanded into an explicit method body. No reflection, no callback registry, no instance_variable_get — block-based attribute access for validators, typed accessors via attr_accessor, lifecycle hooks as plain method overrides.

The bottom shape is what Spinel's whole-program inference can ingest. It is also what every roundhouse emitter has been quietly converging on as its preferred input shape — separately, before the connection was named.


What this changes

The original framing: same cliff, opposite directions, complementary value space. That holds. But the inputs aren't merely overlapping — they're the same intermediate language. That's a stronger claim, and it has practical consequences.

The fixture is now an artifact, not a thought experiment. fixtures/spinel-blog is a complete blog application — models, controllers, views, routes, ActiveRecord-shape framework runtime, Turbo Streams, Tailwind — written in the Spinel subset. About 1,800 lines, 228 minitest assertions passing under CRuby. It exists as roundhouse's contract specimen, but it's a non-trivial Rails-shaped Ruby program in Spinel's subset that didn't exist before. As Spinel's test corpus broadens, this is corpus.

Roundhouse's lowerer pipeline produces this shape from real Rails apps. The work in progress is exactly the transformation from the eleven-line file to the hundred-and-twenty-line one — a per-Rails-idiom translation (validates, has_many, before_action, ERB, resources, …) that emits explicit method bodies. When the lowerers land, the same path scales to any Rails-shape Ruby app: ingest, lower, emit Ruby in Spinel's subset.

Whole-program inference becomes a shared concern. Spinel infers types across its subset to drive C codegen. Roundhouse needs the same inference quality to drive correct codegen across six targets — not just the statically-typed ones, since picking between +, <>, and ++ in Elixir, or between concatenation and stringification in TypeScript, requires knowing operand types just as much as Rust does. Independent implementations, shared interest in the inference reaching as far as possible.


The honest gap

I tested whether Spinel can compile the spinel-blog fixture today. Codegen succeeds; C compilation fails on five categories of inference maturity:

These aren't architectural mismatches. The fixture conforms to Spinel's documented subset; what's incomplete is the inference catching up to its documented capabilities. Some will close in Spinel's normal maturation. Some may benefit from explicit reports.

The first issue is filed — smallest possible repro for the most isolated of the five, six lines of Ruby. A starting point for the dialog.

The bet I'm making: the architectural shape is right, the maturity gaps close on both sides, and within a few iterations the round trip works end to end. If specific categories prove permanent constraints, roundhouse's lowerers learn which patterns to avoid when targeting Spinel — but the universal post-lowering shape stays.


What's offered

The fixture is in the Roundhouse repo. It runs under CRuby today. As a corpus of non-trivial Rails-shaped Ruby in the Spinel subset, it's available to anyone working on the inference question.

The lowerer pipeline — when complete — is the bridge from arbitrary Rails apps to that subset. It's roundhouse's job, not Spinel's. But the output is something Spinel can ingest, and that opens a path that didn't exist before: an arbitrary Rails app, compiled to a native binary by way of two compilers in series.

The conversation. Issue #49 is a starting point. Roundhouse benefits from Spinel's inference maturing; Spinel benefits from a broader Ruby corpus to test inference against. The architectural overlap means both sides have something to offer.


Two compilers. One subset. Discovered independently, named only now. Probably not the last convergence.


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