iter revert: back out the Iteration-discipline milestone (it.1 + it.2)

One forward iteration; main never rewound. 1ff7e81 (the pre-9973546
commit) is the per-region byte oracle — every reverted source/test
file is byte-identical to it; crates/ailang-check/src/lib.rs fully so.

Root cause being corrected: the Iteration-discipline milestone was an
over-escalation of fieldtest finding F1 (a [friction] item whose own
minimal recommendation was a DESIGN.md note). Its totality dichotomy
made the maximally-LLM-natural build(d:Int)=Node(1,build(d-1),
build(d-1)) inexpressible (it.3 BLOCKED); the only in-thesis escape
(A1/it.2b) conceded the language's first documented-unenforced
totality precondition — a purity-pillar dilution the user rejected in
favour of a full revert + rebuild.

Removed: Term::Loop/Term::Recur/LoopBinder; the verify_structural_
recursion guardedness pass + term_contains_loop + Diverge-injection +
the transitively-it.2 module_fns plumbing; the five Recur*/
NonStructuralRecursion CheckError variants (+ code() + the 3 dedicated
ctx() arms); the it.1 codegen loop-header/phi/back-edge + parallel
block_terminated setter; all Loop/Recur walker arms; 16 it.1/it.2
fixtures; 2 pin files; bench/it3-oracle/. Restored: 2 RC fixtures to
1ff7e81 content.

Surgically kept (not in 1ff7e81, landed with the milestone but
independently sound): feature-acceptance clause 3 in DESIGN.md and
skills/brainstorm/SKILL.md, with its worked example de-claimed from
"shipped" to hypothetical-illustration form; the F3 P2 todo.
bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json kept as
historical record (like journals/plans).

Sole net addition: an honest F1/F4 documented-idiom note in DESIGN.md
(the tail-recursive accumulator fallback; examples/mut_counter.ail),
guarded by a doc-presence test — "a documentation note is not a
reshape", asserts nothing at the typecheck level.

Roadmap: the Iteration-discipline block + blocking-fork section
removed; the genuine total-Int-recursion ambition preserved as a
deferred P2 milestone sequenced behind a future Nat/refinement-types
milestone (not abandoned — correctly sequenced after the type
machinery it needs). 2026-05-15-iteration-discipline.md carries a
superseded header; it.1/it.2/it.3 journals + plans stay as history.

Correctness gate PRISTINE: 164 surviving 1ff7e81-era fixtures
ail check/ail run byte-identical to pre-milestone behaviour (verified
against a 1ff7e81 worktree reference compiler, zero drift);
cargo test --workspace 600/0; zero residual it.1/it.2 production
surface.

Spec docs/specs/2026-05-16-iteration-discipline-revert.md (b3853bf),
plan docs/plans/2026-05-16-iter-revert.md (abf0013).
This commit is contained in:
2026-05-16 01:28:47 +02:00
parent abf00131c1
commit 37ac704bf3
93 changed files with 307 additions and 5014 deletions
+3 -106
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 | loop-term | recur-term
//! | mut-term | assign-term
//! var-ref ::= ident ; reserved: true/false → bool-lit
//! int-lit ::= integer ; numeric atom
//! str-lit ::= string ; string atom
@@ -68,10 +68,8 @@
//! 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 / loop-term
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-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
@@ -1214,8 +1212,6 @@ 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 {
@@ -1224,7 +1220,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`, `loop`, `recur`, `lit-unit`"
`assign`, `lit-unit`"
),
pos,
})
@@ -1646,80 +1642,6 @@ 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> {
@@ -2661,29 +2583,4 @@ 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,54 +594,6 @@ 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(')');
}
}
}