Iter 16b.2 — LetRec captures of fn-params (path-1 safe subset)

Lift 16b.1's no-capture restriction. The desugar pass's `scope`
becomes a `BTreeMap<String, ScopeEntry>` carrying `KnownType` for
fn/Lam-params, sentinels for Let-/Match-bindings. LetRec captures
of `KnownType` names are lifted: the synthetic top-level fn's
signature gets the capture types appended, and every call site of
the LetRec name is rewritten via `subst_call_with_extras` to pass
the captures positionally. Captures from let-bindings, match-arm
patterns, polymorphic enclosing fns, and name-as-value uses are
rejected at desugar with panics pointing at 16b.3-16b.7.

Demo: `sum_below(n)` recurses via `(let-rec loop (params i) ...
(body ... uses n ...) (in (app loop 1)))`. Lifts to
`loop$lr_0(i: Int, n: Int) -> Int` with `(app loop X)` rewritten
to `(app loop$lr_0 X n)`. Outputs 0, 10, 45.

Tests: 103 -> 106 (+1 e2e local_rec_capture_demo, +2 desugar unit;
the 16b.1 capture-panic test was repurposed into the positive
fn-param-capture test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 21:00:49 +02:00
parent 3bee7d4c44
commit d5f63bc3e5
5 changed files with 776 additions and 54 deletions
+17
View File
@@ -450,6 +450,23 @@ fn local_rec_factorial_demo() {
assert_eq!(lines, vec!["1", "6", "120"]);
}
/// Iter 16b.2: LetRec capture of a fn-param (path-1 safe subset).
/// Property protected: the desugar pass lifts a LetRec whose body
/// captures one or more enclosing-scope names (here: `n` from
/// `sum_below`'s params) into a synthetic top-level fn whose
/// signature has the capture types appended, and rewrites every
/// call site of the LetRec name in body and in_term to pass the
/// captures positionally as extra args. Without 16b.2, the
/// 16b.1-era panic ("would capture") would fire and the build
/// would fail. Three call sites: sum_below(1)=0, sum_below(5)=10,
/// sum_below(10)=45.
#[test]
fn local_rec_capture_demo() {
let stdout = build_and_run("local_rec_capture.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["0", "10", "45"]);
}
/// 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.