iter loop-recur.1: additive Term::Loop / Term::Recur / LoopBinder foundation
First of three iterations of the standalone loop/recur milestone
(spec 97c1ed1). loop/recur become real parseable / printable /
round-trippable / hash-stable strictly-additive AST nodes across
every no-wildcard exhaustive Term match in all six crates, plus
parse/print, prose arms, DESIGN.md + form_a.md schema blocks,
drift/coverage/hash anchors, and the spec's worked sum_to program
as a green round-trip fixture. NO typecheck semantics and NO real
codegen this iter by design: synth and lower_term are stubbed
CheckError::Internal / CodegenError::Internal exactly as mut.1
(real typecheck = iter 2, real loop-header/phi/back-edge = iter 3).
Two binding Boss design calls recorded in the plan header and the
journal: (1) verify_tail_positions "byte-unchanged" = its tail-app
verification role is unchanged, not its source — it is an
exhaustive no-wildcard match so two descent-only arms are
mandatory; acceptance evidence is the tail-app non-regression test.
(2) codegen is in iter-1 scope as stubs/pass-throughs because the
workspace must compile for cargo test --workspace.
Three plan-pseudo-vs-reality substitutions (serde Type::Con
literal, bare Int -> (con Int), unqualified LoopBinder) and one
recon-undercount integration-test site, all intent-preserving and
journalled. Boss forward-fix folded in: the committed specs
themselves wrote bare Int in the loop-binder snippets (parses as
Type::Var) — corrected the three headline snippets to (con Int) for
doc-honesty (no design/schema change).
cargo test --workspace 600 -> 608 / 0 red (Boss-reran
independently); iter13a + new loop_recur hash pins green.
This commit is contained in:
@@ -1212,6 +1212,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 +1222,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 +1644,97 @@ impl<'a> Parser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: `(loop (NAME TYPE INIT)* BODY_TERM+)` —
|
||||
/// strict iteration block. Reads zero or more `(NAME TYPE INIT)`
|
||||
/// binder triples (bare, no leading keyword — unlike `(var ...)`,
|
||||
/// the loop binder triple has no `var` keyword), then ≥ 1
|
||||
/// trailing terms right-folded into `Term::Seq` (mirrors
|
||||
/// `parse_mut`). The binder/body boundary is disambiguated by the
|
||||
/// inner head: a binder's first inner token is an ident NAME
|
||||
/// (never a term-head keyword), whereas a parenthesised body form
|
||||
/// (`(if …)`, `(recur …)`, `(app …)`, …) opens with a term-head
|
||||
/// keyword — so a leading `(` is treated as a binder only while
|
||||
/// its inner head is not a term-head keyword.
|
||||
fn parse_loop(&mut self) -> Result<Term, ParseError> {
|
||||
fn is_term_head_kw(h: &str) -> bool {
|
||||
matches!(
|
||||
h,
|
||||
"lit-unit"
|
||||
| "app"
|
||||
| "tail-app"
|
||||
| "term-ctor"
|
||||
| "match"
|
||||
| "do"
|
||||
| "tail-do"
|
||||
| "seq"
|
||||
| "lam"
|
||||
| "if"
|
||||
| "let"
|
||||
| "let-rec"
|
||||
| "clone"
|
||||
| "reuse-as"
|
||||
| "mut"
|
||||
| "assign"
|
||||
| "loop"
|
||||
| "recur"
|
||||
)
|
||||
}
|
||||
|
||||
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
self.expect_lparen("loop-term")?;
|
||||
self.expect_keyword("loop")?;
|
||||
|
||||
let mut binders: Vec<ailang_core::ast::LoopBinder> = Vec::new();
|
||||
while matches!(self.peek_head_ident(), Some(h) if !is_term_head_kw(h)) {
|
||||
self.expect_lparen("loop-binder")?;
|
||||
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 });
|
||||
}
|
||||
|
||||
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 binders".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),
|
||||
})
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: `(recur ARG*)` — re-enter the enclosing
|
||||
/// loop. Positional args. The tail-position rule is enforced at
|
||||
/// typecheck (iter 2, `recur-not-in-tail-position`); the parser
|
||||
/// accepts the shape unconditionally.
|
||||
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 +2676,28 @@ mod tests {
|
||||
"diagnostic should mention missing body, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_loop_and_recur_round_trip_via_term() {
|
||||
let src = "(loop (acc (con Int) 0) (i (con Int) 1) (if (app > i n) acc (recur (app + acc i) (app + i 1))))";
|
||||
let t = parse_term(src).expect("parse loop");
|
||||
match t {
|
||||
ailang_core::ast::Term::Loop { binders, body } => {
|
||||
assert_eq!(binders.len(), 2);
|
||||
assert_eq!(binders[0].name, "acc");
|
||||
assert_eq!(binders[1].name, "i");
|
||||
// body is an `if` whose else-branch is a `recur` of 2 args
|
||||
match *body {
|
||||
ailang_core::ast::Term::If { else_, .. } => match *else_ {
|
||||
ailang_core::ast::Term::Recur { args } => {
|
||||
assert_eq!(args.len(), 2)
|
||||
}
|
||||
other => panic!("else not recur: {other:?}"),
|
||||
},
|
||||
other => panic!("body not if: {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("not a loop: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +594,46 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
write_term(out, value, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: print as
|
||||
// `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
||||
// — inverse of parse_loop's binder read + Seq right-fold,
|
||||
// mirroring the Term::Mut printer.
|
||||
out.push_str("(loop");
|
||||
for b in binders {
|
||||
out.push_str(" (");
|
||||
out.push_str(&b.name);
|
||||
out.push(' ');
|
||||
write_type(out, &b.ty);
|
||||
out.push(' ');
|
||||
write_term(out, &b.init, level);
|
||||
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 } => {
|
||||
out.push_str("(recur");
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user