Rust frontend — match pattern-bind + generalized tagged union
- Type: feature — Track A (Track R)
- Status: done
- Owner: Claude (~/frank2, branch
feature/rust-frontend-skeleton) - Opened: 2026-07-03
- Umbrella: [[feature-rust-frontend]] — sub-ticket 2/12. Depends on [[feature-rust-frontend-skeleton]].
What it does
Two coupled gaps, landed together because pattern-bind is meaningless without a payload to bind against:
- Generalized tagged union type.
tyVariant(defs.inc ~625) is a fixed 8-byte-tag + 8-byte-payload slot — fine forOption<Box<T>>(pointer-sized payload), wrong for any struct/array/multi-field payload variant (the common case:Result<T, E>whereTis a real struct). Needs a real variable-size tagged union type: discriminant + largest-variant-sized payload region, alignment computed like a record. matchwith destructuring bind.AN_CASE(defs.inc:124) is const-selector-only — Pascalcasenever binds names.match Some(x) =>needs a new AST node that, per arm, extracts the payload and introducesxas a fresh scoped local for that arm's body only.
Why this order
Real-world usage confirms this is high-frequency, not a corner case:
~/nextlevel/engine alone has 49 match sites in 6.1k LOC. shakmaty's
Move/Square/Role enums are struct-payload variants — anything touching
that crate's shape needs the general union, not tyVariant.
Acceptance
enumwith struct/tuple-payload variants compiles, sized/aligned correctly.matchon such an enum: each arm's bound names are scoped to that arm only, read/write correctly, no leakage across arms.- No exhaustiveness checking required (accepted non-goal — same category as skipping the borrow checker: compiles, doesn't reject incomplete matches).
- Regression corpus: nested payload (
Option<MyStruct>), multi-arm bind, arm using bound field by value and by ref.
Log
- 2026-07-03 — split from [[feature-rust-frontend]] umbrella at ticket-craft time. No code written yet.
- 2026-07-03 — landed, with a scope-narrowed design that needed no new
TTypeKindand no new shared AST node (so: no Track A hand-off ticket — everything below lives incompiler/rparser.inconly):- Tagged union, reusing
UClass. An enum is aUClass(flagged via a new Rust-frontend-privateREnumCiarray) whose fields are a synthetic__tag: i64at offset 0, then every variant's payload fields registered at the same overlapping offset 8 with variant-qualified mangled names ("Circle.0","Rectangle.width").AddUField/FindUField/RecFieldTypenever assumed disjoint offsets — they just do a name lookup in a class's field window — so this is a real union via 100% existing machinery. match, desugared at parse time. NoAN_MATCHnode: each arm compiles to anAN_IFcomparing__tag, folded right-to-left into a nested if/else chain; bound names are ordinary freshAllocVarlocals assigned from the mangled field before the arm body is parsed.- Enum construction is
let-only (let x = Variant(...),let x = Enum::Variant { .. }), same restriction plain struct literals already have — no AST node represents "a whole struct/union value" as a subexpression yet. - Found and fixed one real bug during testing: binding a struct-payload
field (
Box(rect: Rectangle)) didn't setLastTypeRecIdbeforeAllocVar, so the bound local'sRecNamewas wrong and field access on it silently read the wrong bytes instead of erroring — fixed by callingRecFieldRecIdalongsideRecFieldTypeat both bind sites. - Verified: unit/tuple/struct-payload variants, a struct-typed payload
nested inside a variant (stand-in for the ticket's
Option<MyStruct>example — trueOption<T>needs generics, sub-ticket 3, and the RTL, sub-ticket 10, both after this one in the umbrella's own order), wildcard_arm, qualifiedEnum::Variantconstruction/matching, and two arms binding the same name in onematchresolving independently (no cross-arm leakage).make bootstrapself-host stays byte-identical;make -k testgreen except the same pre-existing environment failure noted in sub-ticket 1 (confirmed unrelated). - Known, documented (not silently dropped) narrowing: match scrutinee
must be a plain local variable, not an arbitrary expression; bare
variant names must be unique program-wide or use
Enum::Variant; a match-arm binding does not roll back the symbol table, so (consistent with this compiler having no block scoping anywhere else) a name reused by code after the whole match statement could be shadowed by a leftover arm binding. Next: sub-ticket 3, [[feature-rust-generics-trait-bounds]].
- Tagged union, reusing
- 2026-07-03 — correction: same caveat as sub-ticket 1's log —
correctness verified against the FPC-built compiler, not the self-hosted
one; see [[bug-selfhost-multifn-ifelse-miscompile]] (filed urgent, Track
A, not caused by this frontend). t2.rs/t3.rs in this ticket's own
verification also have 3+ user functions and would very likely reproduce
that bug too if run through a fresh
make bootstrapbinary — re-check once the bug ticket lands a fix. - 2026-07-04 — merged to
master(fast-forward,a71356c) alongside sub-tickets 1 and 3. Unofficial/unsupported — see sub-ticket 1's log for the rationale. Staysworking/; sub-ticket 4 is next. - 2026-07-04 — [[bug-selfhost-multifn-ifelse-miscompile]] fixed; the self-host caveat in the log above no longer applies.
- 2026-07-08 — resolved, commit ea9baa58.