iter it.1: loop/recur additive — Term::Loop/Term::Recur/LoopBinder end-to-end

Iteration-discipline milestone, 1 of 3. Adds named loop + recur as
strictly-additive first-class AST nodes: parse/print/prose/serde/
round-trip/schema lockstep, typecheck (binder typing + recur
arity/type unification via loop_stack threaded as mut.2's
mut_scope_stack; recur-tail-position via verify_loop_body), codegen
(loop-header + one phi per binder; recur back-edge br with a NEW
parallel block_terminated setter; lambda-boundary loop_frames
save/restore). Four Recur* CheckError variants. Strictly additive:
zero deletions touch tail-app/tail-do or the seven existing
block_terminated sites — this is what makes the destructive it.3
safe. recur synth = fresh metavar (resolves the plan's flagged
Type::unit() risk). loop_counter->55, loop_in_lambda->49, four
negatives fire, tail-app byte-identical, cargo test --workspace
green. Spec fda9b78, plan 7381a42.
This commit is contained in:
2026-05-15 14:38:55 +02:00
parent 7381a4233b
commit 96db54d15d
38 changed files with 1891 additions and 32 deletions
+106 -3
View File
@@ -40,7 +40,7 @@
//! | app-term | tail-app-term | match-term | ctor-term
//! | do-term | tail-do-term | seq-term | lam-term | if-term
//! | let-term | let-rec-term | clone-term | reuse-as-term
//! | mut-term | assign-term
//! | mut-term | assign-term | loop-term | recur-term
//! var-ref ::= ident ; reserved: true/false → bool-lit
//! int-lit ::= integer ; numeric atom
//! str-lit ::= string ; string atom
@@ -68,8 +68,10 @@
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1
//! mut-term ::= "(" "mut" var-decl* term+ ")" ; Iter mut.1
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term / loop-term
//! assign-term ::= "(" "assign" ident term ")" ; Iter mut.1; legal only inside mut-term
//! loop-term ::= "(" "loop" "(" var-decl* ")" term+ ")" ; Iter it.1
//! recur-term ::= "(" "recur" term* ")" ; Iter it.1; tail-position-only in loop
//!
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
//! pat-var ::= ident
@@ -1212,6 +1214,8 @@ impl<'a> Parser<'a> {
"reuse-as" => self.parse_reuse_as(),
"mut" => self.parse_mut(),
"assign" => self.parse_assign(),
"loop" => self.parse_loop(),
"recur" => self.parse_recur(),
other => {
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
@@ -1220,7 +1224,7 @@ impl<'a> Parser<'a> {
"unknown term head `{other}`; expected one of \
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `mut`, \
`assign`, `lit-unit`"
`assign`, `loop`, `recur`, `lit-unit`"
),
pos,
})
@@ -1642,6 +1646,80 @@ impl<'a> Parser<'a> {
})
}
/// Iter it.1: `(loop ((var NAME TYPE INIT)*) BODY-TERM+)` — named
/// loop head. The binder list is an explicit parenthesised group
/// holding zero or more `(var NAME TYPE INIT)` entries (same entry
/// shape `parse_mut` reads); the trailing ≥ 1 body terms are
/// right-folded into `Term::Seq` exactly as `parse_mut` does. The
/// recur arity / type / tail-position rules are enforced at
/// typecheck (it.1 Task 5); the parser accepts the shape.
fn parse_loop(&mut self) -> Result<Term, ParseError> {
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
self.expect_lparen("loop-term")?;
self.expect_keyword("loop")?;
// The binder list is an explicit parenthesised group of
// (var NAME TYPE INIT) entries.
self.expect_lparen("loop-binder-list")?;
let mut binders: Vec<ailang_core::ast::LoopBinder> = Vec::new();
while matches!(self.peek_head_ident(), Some("var")) {
self.expect_lparen("loop-binder")?;
self.expect_keyword("var")?;
let name = self.expect_ident("loop-binder-name")?;
let ty = self.parse_type()?;
let init = self.parse_term()?;
self.expect_rparen("loop-binder")?;
binders.push(ailang_core::ast::LoopBinder {
name,
ty,
init: Box::new(init),
});
}
self.expect_rparen("loop-binder-list")?;
// Then read ≥ 1 trailing body terms, right-folded into Seq.
let mut body_stmts: Vec<Term> = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
body_stmts.push(self.parse_term()?);
}
if body_stmts.is_empty() {
return Err(ParseError::Production {
production: "loop-term",
message: "(loop ...) requires at least one body expression after the binder list"
.into(),
pos: head_pos,
});
}
self.expect_rparen("loop-term")?;
let mut body = body_stmts.pop().expect("non-empty after the check above");
while let Some(s) = body_stmts.pop() {
body = Term::Seq {
lhs: Box::new(s),
rhs: Box::new(body),
};
}
Ok(Term::Loop {
binders,
body: Box::new(body),
})
}
/// Iter it.1: `(recur TERM*)` — backward jump to the lexically
/// nearest enclosing `(loop ...)`. The parser accepts any arg
/// count; the enclosing-loop / arity / type / tail-position rules
/// are enforced at typecheck (it.1 Task 5).
fn parse_recur(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("recur-term")?;
self.expect_keyword("recur")?;
let mut args: Vec<Term> = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
args.push(self.parse_term()?);
}
self.expect_rparen("recur-term")?;
Ok(Term::Recur { args })
}
// ---- patterns -------------------------------------------------------
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
@@ -2583,4 +2661,29 @@ mod tests {
"diagnostic should mention missing body, got: {msg}"
);
}
/// Iter it.1: `(loop ((var i Int 0)) (recur i))` parses to a
/// `Term::Loop` with one binder and a `Term::Recur` body. The
/// binder list is explicitly parenthesised (unlike `mut`).
#[test]
fn parses_loop_with_one_binder_and_recur_body() {
let src = "(loop ((var i (con Int) 0)) (recur i))";
let t = parse_term(src).expect("loop parses");
match t {
Term::Loop { binders, body } => {
assert_eq!(binders.len(), 1);
assert_eq!(binders[0].name, "i");
assert!(matches!(*body, Term::Recur { .. }));
}
other => panic!("expected Term::Loop, got {other:?}"),
}
}
/// Iter it.1: `(recur)` with no args parses to an empty-args
/// `Term::Recur` (arity/scope validity is a typecheck concern).
#[test]
fn parses_recur_zero_args() {
let t = parse_term("(recur)").expect("recur parses");
assert!(matches!(t, Term::Recur { args } if args.is_empty()));
}
}
+48
View File
@@ -594,6 +594,54 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
write_term(out, value, level);
out.push(')');
}
Term::Loop { binders, body } => {
// Iter it.1: print as
// `(loop ((var NAME TYPE INIT)*) STMT* FINAL_EXPR)`
// The binder list is explicitly parenthesised (unlike
// `mut`); the body's `Term::Seq` right-spine is walked the
// same way the `Term::Mut` arm above walks it.
out.push_str("(loop (");
for (k, b) in binders.iter().enumerate() {
if k > 0 {
out.push(' ');
}
out.push_str("(var ");
out.push_str(&b.name);
out.push(' ');
write_type(out, &b.ty);
out.push(' ');
write_term(out, &b.init, level);
out.push(')');
}
out.push(')');
let mut cursor: &Term = body;
loop {
match cursor {
Term::Seq { lhs, rhs } => {
out.push(' ');
write_term(out, lhs, level);
cursor = rhs;
}
other => {
out.push(' ');
write_term(out, other, level);
break;
}
}
}
out.push(')');
}
Term::Recur { args } => {
// Iter it.1: print as `(recur ARG*)`. Legal only in tail
// position of an enclosing `Term::Loop` body; the
// typechecker (it.1 Task 5) enforces that rule.
out.push_str("(recur");
for a in args {
out.push(' ');
write_term(out, a, level);
}
out.push(')');
}
}
}