Skip to content

Decision-tree match compiler#9992

Draft
jaredramirez wants to merge 11 commits into
mainfrom
decision-tree-match-compiler
Draft

Decision-tree match compiler#9992
jaredramirez wants to merge 11 commits into
mainfrom
decision-tree-match-compiler

Conversation

@jaredramirez

@jaredramirez jaredramirez commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Implements projects/big/decision-tree-match-compiler.md: a Maranget-style decision-tree match compiler at the LIR-lowering boundary, as one shared module (src/postcheck/match_tree.zig) consumed by both lowerers. The per-branch backtracking chains and the string-grouping special case are deleted.

What changed

  • src/postcheck/match_tree.zig — pattern matrix normalization + occurrence interning + tree construction + LIR emission, generic over an accessor context so solved_lir_lower.zig (Lifted) and lir_lower.zig (Lambda Mono, incl. callable patterns) share one implementation.
  • An N-branch tag match now lowers to one multiway switch_stmt with one discriminant read (asserted by tests). String literals and interpolation patterns become str_match_set arms; duplicate string shapes merge into one arm with source-order guard retry. Lists dispatch on one length read with exact-length switch arms. Switch-safe int literals form multiway switches; Dec/i128/floats/negative narrow signed ints keep equality chains (executors widen narrow signed switch conditions differently: the interpreter zero-extends, the dev backend sign-extends).
  • Exhaustiveness is consumed, never re-derived: arms covering the committed variant set emit the last arm as the switch default (single-variant unions emit no dispatch at all); open matches keep the comptime_exhaustiveness_failed / runtime_error terminal.
  • Sharing invariant documented and enforced: rows never duplicate during specialization, so bodies/guards lower exactly once and statement count is O(total pattern size); a debug lint asserts a hard multiplier bound per match. design.md now documents the invariant (Monotype DAG ids re-lower per reference; sharing must use join points).
  • Fixes a semantics bug the chain had: a guard failure inside a grouped string arm jumped past the whole group, skipping later branches with the same string pattern; the tree retries them in source order (covered by new conformance tests that main cannot pass).

Testing

  • 30 new cross-engine conformance cases (match-dt: in src/eval/test/eval_match_tests.zig); full five-executor suite green: 1546 passed, 0 failed, 0 crashed.
  • Seeded differential corpus (6 families: tags, lists, strings, records, tuples, small-universe enumeration) compared chain vs tree outputs (compile-time dev-backend evaluation + interpreter) before the chain was deleted; now kept as a generated corpus test.
  • LIR-shape tests: one multiway switch + one discriminant read per N-branch tag match; PR 9707's 3/4/5-branch list family asserted linear (the old desugarer measured 1136/4272/16816).
  • ARC certifier passes on guard/string/list/nominal matches; lowering is deterministic; snapshots regenerate with zero diffs.

Measurements

  • Match-heavy program at --opt=speed: 39 one-armed switches (chain) → 17 switches / 27 branches (tree), −56% switch statements.
  • tag_reachability re-measured per the project doc: removes zero switch edges under either lowering on that program at ~3ms/build; kept enabled (cross-function narrowing can still fire), but the tree removes the intra-function shapes PR 9726 targeted at the source.
  • Column-selection heuristics (longest-run vs first-column) tied on the corpus; longest-run kept (dominates by construction when they differ).

Notes

  • Pre-existing, unrelated: plain fractional literal patterns are broken on main (small-Dec literals panic in Monotype numeric finalization; F32/F64 literal patterns are checker-rejected). Only long-form Dec literal patterns work; they're covered in the corpus.
  • Full zig build minici: every step passes except run-test-cli's 5 RustGlue cases, which are infrastructure errors (cargo is not installed on this machine; they run on CI) — 547 CLI tests passed, 0 failed. All other steps (format, lints, tidy, semantic audit, snapshots, every module test, eval suite, playground, serialization, wasm/dylib/archive, parser coverage) are green.

🤖 Generated with Claude Code

@jaredramirez jaredramirez self-assigned this Jul 6, 2026
@jaredramirez jaredramirez marked this pull request as ready for review July 6, 2026 21:11
@lukewilliamboswell

Copy link
Copy Markdown
Collaborator

Pre-existing, unrelated: plain fractional literal patterns are broken on main (small-Dec literals panic in Monotype numeric finalization; F32/F64 literal patterns are checker-rejected). Only long-form Dec literal patterns work; they're covered in the corpus.

What's up with this? do we have a GH issue tracking these?

Plan for projects/big/decision-tree-match-compiler.md: shared Maranget-style
match compiler at the LIR-lowering boundary, consumed by both lowerers.
28 hand-written match cases locking in branch-chain semantics across all
executors before replacing the match compiler
(projects/big/decision-tree-match-compiler.md): tag dispatch, guards,
int/Dec literals, strings, records/tuples, lists with rests, as-patterns,
nominal patterns (PR 9849 shapes), and exhaustiveness.

Two findings recorded as skipped expected-correct tests: the string-group
special case sends a guard failure to the group miss join, skipping later
branches with the same string pattern (violates source-order fallthrough;
the decision-tree compiler must fix this). Also noted: plain fractional
literal patterns are broken on main independent of this project (small Dec
literals panic in Monotype numeric finalization; F32/F64 literal patterns
are checker-rejected); only long-form Dec literal patterns work and are
covered here.
…art 1)

Pattern matrix normalization, occurrence interning, necessity-based column
selection, and grouped Maranget specialization with exit-based sharing,
generic over an accessor Ctx so both LIR lowerers can drive it. Tree
construction only; LIR emission lands next.
Emit the decision tree as LIR through a generic emit context: multiway
switch_stmt for tags and switch-safe ints, str_match_set string arms
(string literals become exact set arms), list length dispatch, exit joins
for shared continuations, occurrence locals materialized once per
dominating scope, and checker-verdict defaults (exhaustive tag tests emit
the last arm as the switch default; single-variant unions emit no dispatch
at all — the dev backend rejects zero-branch switches). Narrow signed int
literals only use switch_stmt when the sign bit is clear: the interpreter
zero-extends the condition read while the dev backend sign-extends, so
negative values keep the equality-chain lowering.

Integration is behind match_tree.lowering_mode (default .chain until the
migration completes). Includes the debug statement-count lint (machinery
statements bounded by 16*pattern_nodes+24), LIR-shape tests (one multiway
switch + one discriminant read per N-branch tag match), the PR 9707
list-family linear-growth test, ARC certification coverage for guard/
string/list/nominal matches, determinism, and a seeded differential
corpus (six families, chain vs tree, compile-time dev-backend evaluation
plus interpreter) that passes. The full five-executor eval suite passes
under tree mode: 1546 passed, 0 failed, 0 crashed.
Flip match_tree.lowering_mode to .tree and un-skip the two conformance
tests documenting the string-group guard-fallthrough hazard the chain
could not pass (a guard failure inside a grouped string arm skipped later
branches with the same string pattern; the tree merges duplicate string
shapes into one arm and retries later rows in source order). Validated by
the full eval suite across executors.
Adapt the Lambda-Mono debug/test lowerer to the shared decision-tree
match compiler with its own accessor context (callable patterns dispatch
like tags via callableVariantIndex; callable payload locals take the
committed union payload layout from the source local, since callable
payloads may be boxed).
@lukewilliamboswell

Copy link
Copy Markdown
Collaborator

I'll rebase this to clean up the conflicts.

jaredramirez and others added 4 commits July 7, 2026 12:49
Both LIR lowerers now compile matches exclusively through match_tree.
DELETED: lowerBranchChain, the string-grouping special case
(strPatternBranchGroupStart / lowerStrPatternBranchGroup), and
match_tree.lowering_mode from both lowerers, plus chain-only helpers that
became unreachable. The single-pattern refutable path (bindPatternOrCrash
for destructures and try-sequences) keeps lowerPatternThen and
discriminantSwitch — a decision tree for one row is exactly that chain.

The migration-gate differential test becomes a tree-only generated corpus
(compile + interpret; cross-engine comparison lives in run-test-eval), and
the LIR-shape test asserts per-proc discriminant-read counts from proc
dumps instead of chain-vs-tree deltas.
design.md Pattern Lowering: the shared match_tree module, the Monotype
DAG re-lowering/sharing invariant (join points, never re-lowering), and
the debug statement-count lint.
Lint/audit conformance for the new match-compiler code: unnamed
parameters instead of unused-variable suppressions, explicit error sets
in the corpus test, OccKey -> OccDigest and ScopeKey -> Scope (banned
type-name suffix), and precise wording instead of the banned vague term.

Measurements (recorded in match_tree.zig's column-selection comment and
here):

- Column heuristic: longest same-kind run vs plain first-column selection
  produced identical statement totals (5776) on the generated corpus;
  longest-run is kept because it dominates by construction when the two
  differ.
- tag_reachability re-measured on a match-heavy program at --opt=speed:
  the pass removed ZERO switch edges under both the old chain (39
  one-armed switches) and the tree (17 switches, 27 branches — 56% fewer
  switch statements than the chain), at ~3ms per build either way. The
  pass stays enabled per the project doc (cross-function narrowing can
  still fire on other shapes), but the decision tree removes the
  intra-function shapes PR 9726 targeted at the source.
Rebase fallout: main's mutable-store borrow guards changed the
lambda_mono/monotype_lifted span accessors and the type-store field
accessors to return GuardedList borrow spans instead of plain slices.
Adapt the MatchTreeCtx accessors in both lowerers with GuardedList.at,
dupe the spans that cross into slice-typed helpers (strShapeKey,
lowerBranchTree), and query store lengths through len().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukewilliamboswell lukewilliamboswell force-pushed the decision-tree-match-compiler branch from 77f7dd1 to 6950edb Compare July 7, 2026 05:14

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO Remove this file that was accidentally committed... if we do this in a follow up it will skip expensive CI runs

@jaredramirez jaredramirez marked this pull request as draft July 10, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants