Iter 16b.4: LetRec captures of match-arm pattern bindings
Flips the desugar's MatchArm-capture rejection from panic to defer.
The 16b.3 lift_letrecs pass already handled match-arm bindings via
type_check_pattern_for_lift (Pattern::Var, Pattern::Ctor with sub-
patterns, ctor-field substitution against scrutinee args), so this
iter is a single-line classification change in desugar plus the
fixture and tests that exercise it.
- desugar.rs: MatchArm classification now defers (same arm as
LetBound); EnclosingLetRec panic remains for 16b.7.
- examples/local_rec_match_capture.{ailx,ail.json}: enclosing fn
pattern-matches on Pair<Int,Int>, inner LetRec captures both
match-arm bindings simultaneously. Lifts to
loop$lr_0(i: Int, threshold: Int, n: Int) -> Int.
- e2e + check + desugar tests: 110 → 113 (+3).
First fixture lifting more than one capture; subst_call_with_extras
already handled it generically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+118
@@ -3887,3 +3887,121 @@ the post-order traversal in `lift_letrecs` but the mutual case
|
||||
is genuinely harder). 16d (chain-machinery exhaustiveness or
|
||||
`__unreachable__`), 16e (`==` extension to Bool/Str/Unit),
|
||||
17a (per-fn arena, gated) unchanged.
|
||||
|
||||
## Iter 16b.4 — LetRec captures of match-arm pattern bindings
|
||||
|
||||
**Goal.** Lift the 16b.3-era "match-arm capture rejected" panic. A
|
||||
`(let-rec ...)` whose body captures a name bound by a `Term::Match`
|
||||
arm pattern (a `Pattern::Var`, or a `Pattern::Var` sub-pattern of
|
||||
a `Pattern::Ctor`) is now legal. Same architectural path as 16b.3:
|
||||
desugar defers it; `lift_letrecs` resolves the capture's type from
|
||||
the enclosing match's scrutinee type, applying constructor-field
|
||||
substitution against the matched ctor's declared field types.
|
||||
|
||||
**What changed in desugar.** Exactly one classification arm. The
|
||||
LetRec capture loop used to treat `ScopeEntry::MatchArm` as a hard
|
||||
panic (queued for 16b.4); it now sets `needs_defer = true` and
|
||||
falls through to the same defer-arm 16b.3 wrote for `LetBound`.
|
||||
The `has_let_bound` boolean was renamed to `needs_defer` so it
|
||||
covers both LetBound and MatchArm captures uniformly. Mixed scopes
|
||||
(some KnownType + some MatchArm, or some MatchArm + some LetBound)
|
||||
defer just like the 16b.3 mixed case — the all-or-nothing decision
|
||||
is preserved. `EnclosingLetRec` still panics (queued for 16b.7).
|
||||
Total desugar diff: ~25 LOC of comment/classification changes; no
|
||||
new helpers, no new traversals.
|
||||
|
||||
**What `lift_letrecs` already supported.** The post-typecheck pass
|
||||
needed **zero** code changes. 16b.3's `type_check_pattern_for_lift`
|
||||
was already written to handle the full pattern grammar:
|
||||
|
||||
- `Pattern::Wild` / `Pattern::Lit`: bind nothing (no-op).
|
||||
- `Pattern::Var`: binds the entire scrutinee's type.
|
||||
- `Pattern::Ctor`: looks up the ADT in `env.types` (or
|
||||
`env.module_types` for a qualified `module.Type`), validates ctor
|
||||
arity, builds the type-var → arg substitution from
|
||||
`td.vars` zip `s_ty.args`, applies it to each `cdef.fields[i]`,
|
||||
and recurses on each sub-pattern with the substituted field
|
||||
type. The recursion handles arbitrarily-nested Ctor patterns
|
||||
for free (though desugar's 16a flattening means lift_letrecs
|
||||
rarely sees deeply-nested ctor patterns in practice).
|
||||
|
||||
The only reason 16b.3 left the MatchArm path under a `panic!` was
|
||||
to keep 16b.3's scope tight — the machinery for resolving the
|
||||
type was already in place and exercised by 16b.3's own test
|
||||
suite, just gated behind the desugar panic.
|
||||
|
||||
**The fixture: `examples/local_rec_match_capture.ailx` (+ `.ail.json`).**
|
||||
Defines `data Pair (vars a b) (ctor MkPair a b)`. `count_below(p)`
|
||||
takes a `Pair Int Int` (threshold, n), pattern-matches with
|
||||
`(pat-ctor MkPair threshold n)` (so `threshold` and `n` are both
|
||||
match-arm bindings of type `Int` — the ADT's `[a, b]` ctor fields
|
||||
substituted against the scrutinee's `[Int, Int]` type args), and
|
||||
inside that arm runs a recursive `loop` that captures BOTH
|
||||
match-arm bindings to count integers in `1..=n` strictly less than
|
||||
`threshold`. The lift produces a synthetic top-level fn
|
||||
`loop$lr_0(i: Int, threshold: Int, n: Int) -> Int` and rewrites
|
||||
every `(app loop ARGS)` in the arm body to
|
||||
`(app loop$lr_0 ARGS threshold n)`. Drives at `MkPair 10 0`,
|
||||
`MkPair 10 5`, `MkPair 10 15` → output `0\n5\n9\n` (the same
|
||||
numbers as the 16b.3 fixture so the comparison is direct).
|
||||
|
||||
The fixture exercises the full 16b.4 chain: parameterised ADT
|
||||
construction, match on the ctor, two simultaneous match-arm
|
||||
captures (not just one), and `lift_letrecs` resolving both via
|
||||
constructor-field substitution.
|
||||
|
||||
**What this iter exercises that 16b.3 did not.**
|
||||
|
||||
- The MatchArm branch of `type_check_pattern_for_lift` (was
|
||||
unreachable in real programs because desugar always panicked
|
||||
before the lift saw such captures; covered by the 16b.3 test
|
||||
suite synthetically but never end-to-end).
|
||||
- Multi-capture LetRec (16b.2 used a single fn-param capture;
|
||||
16b.3 used a single Let-capture; this is the first fixture with
|
||||
two captures appended in the lifted signature, in a fixed
|
||||
order driven by the BTreeSet's deterministic iteration).
|
||||
- Constructor-field substitution at the lift site (the type args
|
||||
of the scrutinee type `Pair Int Int` flow into the synthetic
|
||||
fn's signature via the substitution map).
|
||||
|
||||
**Tests: 113 → 116 (+3).**
|
||||
|
||||
- e2e: 40 → 41 (`local_rec_match_capture_demo`, asserts the
|
||||
`0/5/9` stdout from the new fixture).
|
||||
- `ailang-check::tests`: 27 → 28
|
||||
(`lift_letrecs_on_match_arm_capture_produces_synthetic_fn`:
|
||||
builds a Pair-using module, asserts the lifted FnDef's name
|
||||
starts with `helper$lr_`, params are `[z, x]`, and the type is
|
||||
`(Int, Int) -> Int`).
|
||||
- `ailang-core::desugar::tests`: 9 → 10
|
||||
(`let_rec_capture_match_arm_is_deferred_to_post_typecheck`:
|
||||
asserts no fn was lifted at desugar time and a Term::LetRec
|
||||
still survives in the outer body for the post-typecheck pass).
|
||||
|
||||
**Cumulative state, post-16b.4.**
|
||||
|
||||
- Stdlib unchanged (5 modules, 29 combinators).
|
||||
- `Term` enum: 11 variants (unchanged). All pre-16b.4 fixture
|
||||
hashes bit-identical (verified via `ail manifest` on the four
|
||||
prior LetRec/lit_pat fixtures: hashes match pre-16b.4 values).
|
||||
- Compiler stages: load → desugar → typecheck → lift_letrecs →
|
||||
codegen (unchanged from 16b.3).
|
||||
- The `unreachable!` arms in `ailang-codegen` and the typing rule
|
||||
in `ailang-check` are unchanged.
|
||||
- Compiler bugs surfaced and fixed in dogfood since 14a: 5/5
|
||||
(unchanged).
|
||||
|
||||
**Queue update post-16b.4.** 16b.4 done. Open: **16b.5** (closure
|
||||
conversion — lifts the "name-as-value-only" restriction for both
|
||||
LetRec and Lam; needs ABI-level work, out of scope for the 16b.x
|
||||
sweep that's been about scope-of-capture restrictions only),
|
||||
**16b.6** (LetRec inside a `Type::Forall`-quantified fn — needs a
|
||||
synthesised `Forall` for the lifted signature plus rigid-var
|
||||
threading; the `lift_letrecs` panic at the Forall-LetRec arm is
|
||||
the entry point), **16b.7** (nested LetRec mutual capture —
|
||||
generalised lifting that accumulates captures across nesting;
|
||||
the post-order traversal in `lift_letrecs` already handles the
|
||||
strictly-nested non-mutual case but the mutual case needs joint
|
||||
lifting). 16d (chain-machinery exhaustiveness or
|
||||
`__unreachable__`), 16e (`==` extension to Bool/Str/Unit),
|
||||
17a (per-fn arena, gated) unchanged.
|
||||
|
||||
Reference in New Issue
Block a user