.bluebook source directly, and eventually compiles it with no Ruby involvedStatus: 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.
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:
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.~/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.
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.
syntax.bluebook had zero Argument rows for two identified_by forms in active use before this work started, and several smaller gaps (an ensures argument row, a group_by variadic-argument column, forward-referenced type names, a ProcessManager blockless on form) surfaced only because a real parser needed the row that wasn’t there. Each was a genuine, narrow, named fix to the self-description itself — not a workaround inside the parser — which is the intended feedback loop: a parser built from the grammar’s own declared facts makes an incomplete declaration a build failure instead of invisible.identifier: value pairs, and a do...end block admitted everywhere { } already was (Ruby treats them as the same block syntax; the parser originally didn’t)..bluebook/.hecksagon files, including every domain example, every framework member, both grammar chapters, all spec fixtures, and the self-hosted language itself) parsing byte-exact — a handful of declared (word, context) pairs (has_many/has_one on Aggregate, reference_to on Entity/Query, provenance on Command, formerly_known_as, several ReadModel/Query options, and the World/Hecksagon open-vocabulary contexts) are genuinely unexercised by anything currently in the corpus, not silently unimplemented. This is the allowlist doing its job — a future bluebook that actually uses one of these becomes a build failure naming exactly what’s missing, not a silent gap.rust/parser/ is a build-time-only sibling crate (rust/parser/, alongside rust/host/ and rust/web/’s own established sibling-crate pattern) — not a member of rust/’s own [workspace] — so none of this inflates the WASM/native/cdylib artifacts rust/src/ actually ships, preserving 0012’s zero-dependency/size discipline for the shipped runtime untouched.bin/project_rust is actually wired to use hecks-parse instead of Ruby’s own Kernel.load-based parsing is a separate, later decision (codegen still runs in Ruby as of this ADR) — this ADR settles that Rust parsing matches Ruby exactly, always, automatically checked, not that Ruby’s own parsing path has been retired yet.~/Projects/hecks/rust) own parser. Rejected on direct evidence, not suspicion: it is for a divergently-evolved dialect, and a live smoke test showed it fails open — exactly the failure mode this decision exists to avoid — and the project that owned it had already stopped trusting it as of the same day this work began.rust/src/, the crate that ships. Rejected to preserve 0012’s size/dependency discipline for the compiled artifact — parsing is a build-time concern with no reason to travel into the shipped WASM/native binary.syntax.bluebook. Rejected as a re-run of the exact failure both prior attempts shared — two independently-maintained fact bases for the same evolving grammar. Generating the table mechanically from syntax.bluebook, checked byte-exact on every test run, is the entire point of why this attempt is different.VALID verdict on a file that silently lost its own identity declaration). An unimplemented construct is a named, honest pending-list entry with its own real diagnostic; it is never accepted and dropped.bin/parity approach. Rejected because that is the specific mechanism that let both prior attempts drift without anyone noticing until it was too late to be cheap to fix. Every real corpus member, every bundle exec rspec run, byte-exact — no sampling, no separate rarely-run comparison script.