Iter 16b.5: LetRec name as value (in_term only) via eta-Lam wrapper

Drops the direct-call-only restriction on the LetRec name when it
appears in `in_term`. No new ABI: the lift wraps the in-term in
`(let f (lam P (app f$lr_N P CAPTURES)) in_term')` so codegen's
existing Iter-8b closure-pair machinery handles it. Body-position
name-as-value still rejected (deferred — eta-of-self is harder).

- desugar.rs / lift.rs: split find_non_callee_use into body
  (panic) and in_term (wrap).
- examples/local_rec_as_value.{ailx,ail.json}: no-capture path
  (factorial passed to apply5 → 120).
- examples/local_rec_as_value_capture.{ailx,ail.json}: capture
  path; lifted fn has captures, eta-Lam picks them up via 8b.
- e2e + desugar tests: 113 → 116 (+3).

Codegen Lam machinery handled the wrapped form on the first try.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 22:27:21 +02:00
parent c0668178bb
commit ca507c9f52
8 changed files with 509 additions and 45 deletions
+32
View File
@@ -505,6 +505,38 @@ fn local_rec_match_capture_demo() {
assert_eq!(lines, vec!["0", "5", "9"]);
}
/// Iter 16b.5: LetRec name as a value in the in-clause (no capture).
/// Property protected: the desugar pass detects a non-callee use of
/// the LetRec name in `in_term` and wraps the rewritten in-term in
/// `(let factorial (lam ...) ...)` whose lam eta-expands the lifted
/// fn. The LetRec name then resolves to a closure-pair value usable
/// anywhere a `Fn(Int) -> Int` is expected (here: passed to a HOF
/// `apply5`). Without 16b.5, the desugar would panic with the
/// "name-as-value not supported" message and the build would fail.
/// Expected stdout: 120 (= apply5 factorial = factorial 5).
#[test]
fn local_rec_as_value_demo() {
let stdout = build_and_run("local_rec_as_value.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["120"]);
}
/// Iter 16b.5: LetRec name as a value in the in-clause WITH capture.
/// Property protected: the eta-Lam wrap composes correctly with the
/// 16b.2 capture-augmentation. The lifted fn has signature
/// `factorial_plus$lr_N(n: Int, base: Int) -> Int`; the eta-Lam
/// wrap binds `factorial_plus` to a `(lam (n) (app
/// factorial_plus$lr_N n base))` whose free var `base` becomes a
/// standard 8b closure-env capture. This is the dynamic-env path:
/// the lifted fn has extra params, the eta-Lam captures them.
/// Expected stdout: 1320 (= 5*4*3*2*(1+10)), 12120 (= 5*4*3*2*(1+100)).
#[test]
fn local_rec_as_value_capture_demo() {
let stdout = build_and_run("local_rec_as_value_capture.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["1320", "12120"]);
}
/// 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.