hecks

Rust compiles types ahead of time and interprets dispatch data at runtime

Status: Accepted — implemented. rust/ (Cargo crate + Ruby generator, bin/project_rust the driver), branch feat/rust-projection. Supersedes 0007’s central claim.

Context

0007 specified a pure code generator: canonical IR, read once at build time, emits real Rust source for every command — argument gates, role checks, given evaluation, mutation application, event emission, all as compiled control flow. The explicit, load-bearing non-goal was “no generic kernel that parses or interprets canonical IR at runtime — the generator emits real code that decides behavior.”

That was tried, in an early slice of the actual build, against Pizzas. It generated one bespoke Rust function per command shape: a plain creating command, an acting command with a given and an append mutation, and each new shape after that — a lifecycle transition, an ensures, an increment — needed a new hand-written case in the generator. This didn’t converge, and it’s the same failure mode 0007 itself already diagnosed for the retired Rust runtime, one level down: that runtime hand-wrote a second dispatcher to keep in parity with Ruby’s; per-shape codegen was hand-writing a second dispatcher’s worth of cases to keep in parity with CommandInterpreter’s own shapes, just inside a generator instead of inside a runtime. Ruby’s own CommandInterpreter#call isn’t N methods, one per command shape — it’s one method walking DISPATCH_ORDER generically over whatever IR a command carries. Fighting that shape in the generator was fighting the actual structure of the problem.

Decision

Compile type shapes ahead of time; interpret dispatch behavior at runtime, through one small, hand-written, generic interpreter.

bin/project_rust (Ruby, build-time only) emits real Rust types — structs and enums for every value object, entity, and aggregate record, each with a Fielded trait impl (fn field(&self, name: &str) -> Option<Field>) giving generic, name-based field lookup over otherwise fully static, typed data. It also parses each command’s given/ensures/invariant text through the real Ruby parser (Bluebook::Expression::Evaluator.parse — not a re-implementation) and emits the resulting AST as Expr data literals (Expr::Compare { op: ..., left: ..., right: ... }), not as compiled boolean expressions.

rust/src/kernel/{expr.rs,dispatch.rs} is hand-written exactly once and never regenerated. interpret(expr, ctx) walks any Expr tree — a direct structural port of Evaluator/Resolver’s node kinds, not a reimplementation of parsing (parsing still only ever happens once, in Ruby, at generator build time). dispatch(...) walks the real DISPATCH_ORDER steps generically, driven by whatever GivenSpec/EnsuresSpec/mutation-closure a generated function hands it — one function for every command shape, the same way CommandInterpreter#call is one method.

This means given/ensures/invariant text genuinely is interpreted, as data, at the compiled binary’s own runtime — the literal thing 0007’s non-goal ruled out. What’s compiled instead, and what actually gives Rust its value here, is narrower and more honest: the type shapes. A Money value is a real struct Money { cents: i64, currency: String }, not a dynamic map; reading args.amount.cents is a real, typed field access the compiler checks, not a runtime lookup. Only the small, closed set of behavior the expression grammar can express (comparisons, arithmetic, membership, sign tests — the same closed grammar spec/operator_conformance_spec.rb already holds Ruby to) is interpreted, by one hand-written, thoroughly-covered function.

Consequences

Rejected alternatives