Iter 16b.3: post-typecheck lift for LetRec with Let-bound captures

Adds path-2 from the 16b.2 planning entry. Desugar now defers
LetRecs whose captures include Term::Let-bound names; a new
ailang-check::lift_letrecs pass runs after typecheck and uses the
elaborated env to resolve capture types. ailang-check learns a real
Term::LetRec typing rule in synth and verify_tail_positions.

- desugar: Term::LetRec arm gains a defer-arm; helpers promoted to
  pub for reuse by the lift pass; find_non_callee_use moved before
  classification.
- ailang-check::synth/verify_tail_positions: real LetRec rules
  (effect-subset, locals install, recursive name in body+in_term).
- ailang-check::lift.rs (new, 720 LOC): post-typecheck lift with
  post-order traversal, env-walk for capture-type resolution,
  fast-path skip when no LetRec is present.
- ail::main.rs: build path now does load → check → desugar →
  lift_letrecs → codegen.
- examples/local_rec_let_capture.{ailx,ail.json}: new fixture
  capturing a let-bound `threshold` in a recursive helper.
- e2e + check unit tests: 106 → 110 (+4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 22:07:05 +02:00
parent d5f63bc3e5
commit ca30606aec
9 changed files with 1552 additions and 68 deletions
+18
View File
@@ -467,6 +467,24 @@ fn local_rec_capture_demo() {
assert_eq!(lines, vec!["0", "10", "45"]);
}
/// Iter 16b.3: LetRec capture of a `Term::Let`-bound name. Property
/// protected: the desugar pass leaves a LetRec whose only outside-
/// scope captures are Let-bound (type unknown until typecheck) in
/// place; the post-typecheck `lift_letrecs` pass in `ailang-check`
/// resolves capture types from the typechecker's env, lifts to a
/// synthetic top-level fn (`loop$lr_0(i, n, threshold)`), and
/// rewrites every call site. Without 16b.3, the 16b.2-era panic
/// ("16b.3 — let-binding captures") would fire. The threshold here
/// is `(app + 5 5)` so the lift exercises the type-synthesis path
/// (not just a literal). count_below(0)=0, count_below(5)=5,
/// count_below(15)=9 (i in 1..9 are below threshold 10).
#[test]
fn local_rec_let_capture_demo() {
let stdout = build_and_run("local_rec_let_capture.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["0", "5", "9"]);
}
/// Guards `ail diff`: a modified body changes the hash of `sum`, while
/// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
/// `sum`, `unchanged` contains `main`, `added`/`removed` empty.