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:
@@ -1512,6 +1512,13 @@ fn render_workspace_diff_text(r: &WorkspaceDiffReport) -> String {
|
||||
/// clang. On typecheck failure, prints diagnostics to stderr and exits
|
||||
/// the process with code 1. On clang failure, returns a Result error
|
||||
/// (the .ll path is preserved for post-mortem inspection).
|
||||
///
|
||||
/// Iter 16b.3: between `check_workspace` and `lower_workspace` we run
|
||||
/// `ailang_check::lift_letrecs` per module. The lift eliminates any
|
||||
/// `Term::LetRec` that the desugar pass left in place (specifically:
|
||||
/// LetRecs that capture `Term::Let`-bound names, whose types are
|
||||
/// only known after typecheck). The lifted workspace then goes to
|
||||
/// codegen unchanged.
|
||||
fn build_to(path: &Path, out: Option<PathBuf>, opt: &str) -> Result<PathBuf> {
|
||||
let ws = ailang_core::load_workspace(path)?;
|
||||
let diags = ailang_check::check_workspace(&ws);
|
||||
@@ -1533,6 +1540,22 @@ fn build_to(path: &Path, out: Option<PathBuf>, opt: &str) -> Result<PathBuf> {
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
// Iter 16b.3: run `lift_letrecs` per module on the post-desugar
|
||||
// form. Codegen's internal desugar pass is idempotent on a
|
||||
// module that contains no `Term::LetRec`, so the lifted output
|
||||
// can be handed directly to `lower_workspace`.
|
||||
let mut lifted_modules = std::collections::BTreeMap::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
let desugared = ailang_core::desugar::desugar_module(m);
|
||||
let lifted = ailang_check::lift_letrecs(&desugared)
|
||||
.map_err(|e| anyhow::anyhow!("lift_letrecs in module `{mname}`: {e}"))?;
|
||||
lifted_modules.insert(mname.clone(), lifted);
|
||||
}
|
||||
let ws = ailang_core::Workspace {
|
||||
entry: ws.entry.clone(),
|
||||
modules: lifted_modules,
|
||||
root_dir: ws.root_dir.clone(),
|
||||
};
|
||||
let ir = ailang_codegen::lower_workspace(&ws)?;
|
||||
let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&tmpdir)?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user