Iter 16b.1 — local recursive let (no-capture)

Add `(let-rec NAME (params ...) (type ...) (body ...) (in ...))`
as a form-A surface and a `Term::LetRec` AST variant. The 16a
desugar pass lifts each LetRec whose body has no captures from
the enclosing scope to a synthetic top-level fn `<hint>$lr_N`
and substitutes the original name; typecheck and codegen never
see LetRec. Capture detection panics at desugar time, queued
for 16b.2.

Tests: 95 → 99 (+1 e2e local_rec_factorial_demo, +2 desugar
unit, +1 parse unit). The new fixture `examples/local_rec_demo`
runs `fact` at n=1, 3, 5 → prints 1, 6, 120.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:33:48 +02:00
parent ee5807d73c
commit 8860600e37
11 changed files with 930 additions and 38 deletions
+77 -2
View File
@@ -36,7 +36,7 @@
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
//! | app-term | tail-app-term | match-term | ctor-term
//! | do-term | tail-do-term | seq-term | lam-term | if-term
//! | let-term
//! | let-term | let-rec-term
//! var-ref ::= ident ; reserved: true/false → bool-lit
//! int-lit ::= integer ; numeric atom
//! str-lit ::= string ; string atom
@@ -56,6 +56,11 @@
//! typed-param ::= "(" "typed" ident type ")"
//! if-term ::= "(" "if" term term term ")"
//! let-term ::= "(" "let" ident term term ")"
//! let-rec-term ::= "(" "let-rec" ident
//! "(" "params" ident* ")"
//! type-attr
//! body-attr
//! "(" "in" term ")" ")"
//!
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
//! pat-var ::= ident
@@ -690,13 +695,14 @@ impl<'a> Parser<'a> {
"lam" => self.parse_lam(),
"if" => self.parse_if(),
"let" => self.parse_let(),
"let-rec" => self.parse_let_rec(),
other => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
production: "term",
message: format!(
"unknown term head `{other}`; expected one of \
`app`, `tail-app`, `lam`, `let`, `if`, `match`, `do`, \
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
`tail-do`, `seq`, `term-ctor`, `lit-unit`"
),
pos,
@@ -943,6 +949,34 @@ impl<'a> Parser<'a> {
})
}
/// Iter 16b.1: `(let-rec NAME (params PARAM*) (type T) (body TERM)
/// (in TERM))` — local recursive fn-shaped binding. Eliminated by
/// the desugar pass (lifted to a synthetic top-level fn) before
/// typecheck. The body's recursive references to `NAME` are
/// rewritten to the lifted name; the `in`-clause becomes the term
/// that replaces the LetRec.
fn parse_let_rec(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("let-rec-term")?;
self.expect_keyword("let-rec")?;
let name = self.expect_ident("let-rec name")?;
let params = self.parse_params_attr()?;
let ty = self.parse_type_attr()?;
let body = self.parse_body_attr()?;
// (in TERM)
self.expect_lparen("let-rec in-clause")?;
self.expect_keyword("in")?;
let in_term = self.parse_term()?;
self.expect_rparen("let-rec in-clause")?;
self.expect_rparen("let-rec-term")?;
Ok(Term::LetRec {
name,
ty,
params,
body: Box::new(body),
in_term: Box::new(in_term),
})
}
// ---- patterns -------------------------------------------------------
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
@@ -1081,4 +1115,45 @@ mod tests {
.unwrap();
assert!(matches!(m.defs.len(), 1));
}
/// Iter 16b.1: minimal `(let-rec ...)` round-trips through the
/// parser into a `Term::LetRec` whose `name`, `params`, `body` and
/// `in_term` line up with the source.
#[test]
fn parses_minimal_let_rec() {
let m = parse(
r#"
(module m
(fn main
(type (fn-type (params) (ret (con Int))))
(params)
(body
(let-rec f
(params x)
(type (fn-type (params (con Int)) (ret (con Int))))
(body x)
(in (app f 1))))))
"#,
)
.unwrap();
let body = match &m.defs[0] {
Def::Fn(fd) => &fd.body,
_ => panic!("expected fn"),
};
match body {
Term::LetRec { name, params, body, in_term, .. } => {
assert_eq!(name, "f");
assert_eq!(params, &vec!["x".to_string()]);
assert!(matches!(body.as_ref(), Term::Var { name } if name == "x"));
match in_term.as_ref() {
Term::App { callee, args, .. } => {
assert!(matches!(callee.as_ref(), Term::Var { name } if name == "f"));
assert_eq!(args.len(), 1);
}
other => panic!("expected App in in-clause, got {other:?}"),
}
}
other => panic!("expected LetRec, got {other:?}"),
}
}
}