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
+1
View File
@@ -0,0 +1 @@
{"defs":[{"body":{"body":{"cond":{"args":[{"name":"i","t":"var"},{"name":"n","t":"var"}],"fn":{"name":">=","t":"var"},"t":"app"},"else":{"args":[{"name":"i","t":"var"},{"args":[{"args":[{"name":"i","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"loop","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"t":"if","then":{"lit":{"kind":"int","value":0},"t":"lit"}},"in":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"loop","t":"var"},"t":"app"},"name":"loop","params":["i"],"t":"letrec","type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},"doc":"Sum 1 + 2 + ... + (n-1). Helper captures n from the enclosing scope.","kind":"fn","name":"sum_below","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"sum_below","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":5},"t":"lit"}],"fn":{"name":"sum_below","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"lit":{"kind":"int","value":10},"t":"lit"}],"fn":{"name":"sum_below","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"doc":"Drive sum_below at 1, 5, 10. Expected (per line): 0, 10, 45.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"local_rec_capture","schema":"ailang/v0"}
+35
View File
@@ -0,0 +1,35 @@
; Iter 16b.2 — LetRec captures of fn-params (path-1 safe subset).
; `sum_below(n)` returns the sum of integers from 1 up to but not
; including n. The recursive helper `loop` captures the upper
; bound `n` from `sum_below`'s param list and recurses on its own
; counter `i`. The 16b.2 desugar pass lifts `loop` to a synthetic
; top-level fn `loop$lr_0(i: Int, n: Int) -> Int` (capture appended)
; and rewrites every `(app loop ARGS)` to `(app loop$lr_0 ARGS n)`.
;
; Expected stdout (one per line): 0 (sum below 1), 10 (1+2+3+4),
; 45 (1+2+...+9).
(module local_rec_capture
(fn sum_below
(doc "Sum 1 + 2 + ... + (n-1). Helper captures n from the enclosing scope.")
(type (fn-type (params (con Int)) (ret (con Int))))
(params n)
(body
(let-rec loop
(params i)
(type (fn-type (params (con Int)) (ret (con Int))))
(body
(if (app >= i n)
0
(app + i (app loop (app + i 1)))))
(in (app loop 1)))))
(fn main
(doc "Drive sum_below at 1, 5, 10. Expected (per line): 0, 10, 45.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(seq (do io/print_int (app sum_below 1))
(seq (do io/print_int (app sum_below 5))
(do io/print_int (app sum_below 10)))))))