hecks

Rust parses .bluebook source directly, and eventually compiles it with no Ruby involved

Status: Accepted — parsing half implemented and verified. rust/parser/ (crate, binary hecks-parse), driven by bin/project_parser_table generating its keyword table from syntax.bluebook itself. Marks 0007’s “no Rust parser to update” consequence and 0011’s “parsing stays in Ruby, at build time, permanently” invariant as superseded — both said this on purpose, for reasons that held until the language described enough of itself to make the alternative viable. Codegen (the second half — porting rust/project/*.rb so cargo build needs no Ruby boot at all) is separate follow-on work, tracked on its own; this ADR’s parsing claim stands regardless of where that lands.

Context

Two earlier attempts at “a second Rust implementation of .bluebook parsing” both failed for the same underlying reason, and 0007/0011 wrote their no-Rust-parser stance directly against the first of the two:

  1. hecks’s own first Rust runtime (docs/implemented/rust-experiment.md) — a hand-written parser, dispatcher, evaluator, and adapter stack, kept honest only by bin/parity diffing sampled behavior against Ruby. Retired: parity certified that two hand-written interpreters agreed with each other on the samples checked; it never made either one cheaper to change, and drift crept back in every time Ruby’s own grammar moved.
  2. A sibling project’s own bluebook parser (~/Projects/hecks/rust, a much larger, independently-evolved dialect) — actively maintained until the same day this work started, when that project itself abandoned its own parser as source of truth and began vendoring real hecks (Ruby) instead. A live smoke test against hecks’s own current pizzas.bluebook (using syntax that parser’s grammar never learned) returned VALID while silently dropping identity entirely — it failed open, treating unrecognized syntax as skippable rather than as an error.

Both failures share one root cause: a second, hand-written implementation of an evolving surface, checked against the original only sporadically or not at all. That is exactly the shape 0007/0011 refused to reintroduce, and refusing it was correct at the time — nothing about hecks’s own grammar was declared anywhere Rust could read it, so a Rust parser could only ever be a second, hand-maintained fact base.

What changed: over the course of this project, hecks described itself almost fully. lib/hecks/language/bluebook/syntax.bluebook — part of the language’s own self-hosted meta-domain — declares, as data, nearly every legal keyword, which context each is valid in, and its own argument shape (Keyword/Argument rows). spec/syntax_conformance_spec.rb already read this table to hold Ruby’s own DSL builders honest, bidirectionally, before any Rust work started. That data existing changes what “a second parser” means: it stops being a second fact base racing to keep up with a first one, and becomes a second reader of the same declared facts — checkable for agreement the same way any other generator-from-a-shared-source is.

Decision

rust/parser/ reads .bluebook/.hecksagon source text directly and compiles it to the same canonical IR shape Ruby’s own Exporter produces — fail-closed, always. Unrecognized syntax is a hard parse error naming the legal alternatives, never a silent skip; there is no lenient or best-effort mode, and that absence is deliberate, not an oversight to fix later.

The parser’s own knowledge of the grammar is generated from syntax.bluebook, not hand-typed a second time: bin/project_parser_table reads the same MetaValidator.grammar_registry spec/syntax_conformance_spec.rb already reads, and writes rust/parser/src/keywords.rs, checked in. spec/parser_table_spec.rb regenerates it into memory on every bundle exec rspec run and asserts byte equality — grammar drift fails the normal, always-run test loop immediately, not a separately-remembered check. spec/parser_coverage_spec.rb asserts every (word, context) pair syntax.bluebook declares is either genuinely covered by the parser or named on a small, sorted, reviewable pending allowlist — never silently unaccounted for.

Correctness against Ruby is proven by a byte-exact differential harness inside the normal test loop, not a sampled or separately-run comparison: spec/parser_parity_spec.rb builds hecks-parse once, runs it against every real corpus .bluebook/.hecksagon file — found by Dir.glob, never hand-listed, so a new corpus member is covered automatically — and asserts the emitted ir.json is byte-identical to JSON.pretty_generate(Exporter.call(...)) for the same file. A parse error outside the declared pending list is a spec failure with the parser’s own stderr inlined, never a skip. This ran, unmodified in mechanism, from the first real target (pizzas.bluebook) through the last (hecks’s own nine-file self-hosted grammar, parsing the text description of the very table compiled into its own keywords.rs) — at no point did verification loosen to “close enough” or “spot-checked.”

given/ensures/invariant/identified_by { } predicate bodies are captured as raw text and canonicalized exactly the way Ruby’s own CanonicalForm does, never interpreted — the existing evaluator.rb/resolver.rb/rust/src/kernel/expr.rs pipeline for predicate semantics is untouched and out of scope here, same as 0011 already established. Two narrow, explicitly-named open-vocabulary escapes exist (the .hecksagon adapter-bind shape and .world’s verb-setting shape), both shape-matched and their contents dropped, matching syntax.bluebook’s own documented permanent exclusions.

Consequences

Rejected alternatives