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. Specfda9b78, plan7381a42.
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user