Skip to content

Latest commit

 

History

History
159 lines (145 loc) · 11 KB

File metadata and controls

159 lines (145 loc) · 11 KB

← ROADMAP

Phase 6 — Term-level enum elimination [L]

Eliminating an enum is a term, not a word-body form. A match composes mid-body, nests inside a quotation, and needs no helper word and no bundle struct, the way every other construct in a concatenative language does. WordBody::Clauses and parse_clauses are deleted; the clause-style word is replaced, not joined, since two elimination forms in the language permanently is worse than one migration.

The form is a generated per-enum eliminator word taking one quotation per variant, each arm annotated with the variant it handles. Arms are matched by declared variant, not by position, so reordering or inserting a variant in a type: declaration cannot silently rebind a call site; a missing or duplicated arm is a named error. Arms are ordinary quotation values, so an arm can be named and reused.

: area ( Shape -- f64 )
  [ ( Circle )  Circle>r dup * 3.14159 * ]
  [ ( Rect )    Rect> * ]
  Shape? ;

An arm's annotation is a stack effect with an elision rule scoped to arm position: ( Circle ) names the variant and unifies inputs-below and outputs across sibling arms, escalating to ( Vm Push -- Vm ) and then ( ..a Vm Push -- ..b ) when an arm needs to say more. The elision is arm-only; a word signature never infers its outputs. Because elision moves a shape error off-site, the disagreement diagnostic names both arms by variant.

Payload reaches an arm by generated per-variant accessors: &r/&!r per field (receiver-directed projection, P7.S1's shape, not the retired Circle>r fused spelling) and Rect> destructuring all of them, which retires the positional field binding clause elimination uses. A payload-free arm consumes its variant explicitly (Halt>): the linear spine does not auto-drop.

No open lowering prerequisite. The row-carried-quotation backend crash this phase's design discussion once assumed open (a times/while self-tail loop with a quotation riding untouched in the row) is already fixed and covered by a green golden (5749a14, tests/phase4_slice10b.rs). One narrower case stays unconfirmed rather than fixed: while over a materialized (closure-captured) quotation. Slice 7b's capturing closures are built (examples/capturing_dispatch.sth); the dead-code conclusion holds for a narrower reason, confirmed by direct repro: every combinator's ~ parameter rejects a materialized quotation value on sight regardless of whether one exists elsewhere in the program — not a blocker for Slice 1 below, which checks annotated quotations against ordinary contexts, not materialized ones.

P6.S1 — Phase 6 Slice 1 — quotation effect annotations. [ done ] A quotation literal may carry a declared stack effect ([ ( ..a T -- ..b ) ... ]), checked against its own body standalone and, where it fills a declared quotation parameter, reconciled against that parameter. Reconciliation only does independent work for a poly parameter whose type variable the annotation also names (a concrete/mono parameter's disagreement is already caught by the standalone body check); a shape-changing row (..a -- ..b) in an annotation is rejected outright, standalone or filling a parameter, since nothing in this slice grounds it. Independently useful and independently testable; no enum machinery involved. See docs/roadmap/P6/slice1-spec.md for the full design. Exit: an annotated quotation whose body contradicts its declared effect is a located error, standalone or parameter-filling.

P6.S2 — Phase 6 Slice 2 — variant types and accessors. [ done ] Type::Variant(EnumId, usize, &'static str), legal only as an arm's declared input and as the value inside that arm, so it never becomes a general first-class type: nothing in this slice can construct one from surface syntax, since the eliminator (Slice 3) is its only introducer. The leaked &'static str carries the stable Enum.Variant display name (display_static on VariantDecl, one source of truth read by the sole constructor variant_type), needed so two Type::Variants for the same variant always compare equal. Generated per-variant field accessors and whole-variant destructures follow P7.S1's struct shape (a scalar/aggregate projection via &field/&!field, resolved per call site against the receiver, not a fused &Variant>field spelling), split across three existing mechanisms rather than one: a scalar getter and the whole destructure go through ordinary Sig dispatch (variant_generated_sigs), an aggregate getter through a new check_variant_get_word (interior-address aliasing, like the struct getter), and reference-mode access (&field/&!field on a variant receiver) through a new arm in check_reference_word. All three resolve the variant from the EnumId the operand already carries, not a global name scan: variant names are not unique across enums (only type names are deduped), so a scan would mis-resolve the second enum's same-named variant. Layouts already existed per variant (EnumLayout.variants[vi].fields); nothing changed at runtime. See docs/roadmap/P6/slice2-spec.md for the full design and its review history (three rounds; the operand-EnumId resolution rule and the single-source-of-truth display name both replaced earlier, unsound drafts). Exit: a variant-typed value is reachable only inside an arm, with its fields readable by name. Verified by unit tests against hand-built checker state, not an .sth golden, since no program can construct a Type::Variant until Slice 3 ships; examples/vm.sth is untouched this slice.

P6.S3 — Phase 6 Slice 3 — the eliminator word. [ done ] The generated per-enum eliminator (Shape?), arms matched by declared variant via a leading ( Circle )/ ( &Circle )/( &!Circle ) tag, exhaustiveness and duplication as named errors, arm-position effect elision. Lowers to the existing N-way dispatch (lower_clauses) via a new additive ArmBinding::WholeValue. The scrutinee is mode-polymorphic: owning, &, or &!, resolved from the eliminator call's own operand type, with every arm's mode matching that call's mode uniformly (not a per-arm independent choice). This reuses check_field_projection's and lower_clauses's existing value-vs-reference branching rather than inventing a third mechanism, and forces ir_type_of(Type::Variant) to stop being an unreachable! and erase to its parent enum's IrType::Enum(id) (a reference to a variant is now a real interned referent, not merely a hypothetical one). See docs/roadmap/P6/slice3-spec.md for the full design and its review history (decision 6, the mode-polymorphism, was added post-hoc after the first three review rounds and re-reviewed on its own before implementation). Exit: a match is a term usable mid-body and inside a quotation; reordering a type: declaration's variants leaves every call site correct. Two goldens (examples/eliminator.sth, examples/eliminator_ref.sth) cover owning- and reference-mode dispatch.

P6.S3b — Phase 6 Slice 3b — check-time arm-tag resolution. [ done ] An arm's ( Ok ) tag is resolved to a concrete Type::Variant by the parser, against the concrete enum registry, which makes the eliminator unable to eliminate a generic enum at all: a bare tag carries no type arguments and a generic header contributes no concrete variant to scan. Tag typing moves to check time, against the EnumId the scrutinee operand already carries (the rule Slice 2 settled for accessors, which the parser is the last site to violate); tag recognition stays at parse time as a name-only, module-scoped predicate over the concrete and generic enum registries alike (a variant of an unimported module's enum is not a routing tag, and an in-scope type of the same name takes precedence). The surface syntax does not change: the generic case must read exactly like the concrete one. Required before Slice 4, because clause bodies are today the only working generic-enum elimination mechanism (tests/phase5_generic_enum_elimination.rs), so deleting them first would regress a shipped Phase 5 capability rather than migrate it. eliminator_registry stays keyed by the base family and serves as a family gate; the operative EnumId comes from the scrutinee slot, which is what makes two instantiations independent. See docs/roadmap/P6/slice3b-spec.md. Exit: a generic enum is eliminable with bare tags, Result[i64 bool] and Result[bool i64] eliminate independently in one word, and every Slice 3 diagnostic still fires with unchanged wording (the mode-mismatch check included, since it is the one consumer of the annotation's parse-time input slot).

P6.S4 — Phase 6 Slice 4 — migration. [ done ] Landed in 8ba0477: the eliminator is the only enum-elimination mechanism, every clause-dispatch site moved, and examples/vm.sth's Op dispatch reads through Op?. Every clause-dispatch site moves to the eliminator, and WordBody::Clauses/parse_clauses are deleted, including the Bool print word declared in src/ast.rs and the generic-enum elimination witnesses in tests/phase5_generic_enum_elimination.rs. Deleting the clause path also retires check_clause_word, the clause_bodied_quotation_word_error rule (which exists only because a clause body cannot be spliced by the inliner), and ArmBinding, whose Decompose case has the clause path as its sole caller. Exit: the clause word body no longer parses; the tree builds green without it.

P6.S5 — Phase 6 Slice 5 — nested tag paths. [ deferred ] An arm tag would name a path through nested enums, ( Some[v Circle] ), desugaring to one eliminator call nested inside another's arm body -- exactly what a hand-written program already does today, by probe, in both owning and reference mode, with the current eliminator and no compiler change. Deferred, not planned: the entire payoff is cosmetic (an arm reads as one line instead of a nested call the checker already accepts) while the honest cost is not -- review round 1 forced a choice between a new AST-rewrite pass run before check, or check-time synthesis with no home in check_eliminator_call's current signature, or a split where check validates the path form and a separate pass rewrites it; every option adds a first-of-its-kind mechanism to buy pure syntax sugar, the kind of complexity CLAUDE.md's growth rules ask to notice before building, not after. See docs/roadmap/P6/slice5-brief.md for the settled syntax (field name always required, since a fieldless/tuple variant form may exist later and single-field elision would trap it) and for the full recon. Revisit only if a second consumer motivates the rewrite-pass mechanism itself; a rewrite pass justified by one cosmetic slice alone is not worth building. Exit (if revived): a nested tag path type-checks and lowers to nested eliminator calls; a program matching two levels of enum nesting reads as one match-shaped word body, not two nested word bodies.

Exit: enum elimination is a term, matched by variant name, with the clause-style word body deleted. Dogfood: examples/vm.sth's nine-variant Op dispatch, rewritten, reads no worse than the clause form it replaces.