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
-61
View File
@@ -1544,26 +1544,6 @@ fn walk_term(
}
walk_term(value, out, builtins, scope);
}
// Iter it.1: loop binder names bind inside the body and later
// binder inits, mirroring `Term::Mut`. Recur args are uses.
Term::Loop { binders, body } => {
let mut newly = Vec::new();
for b in binders {
walk_term(&b.init, out, builtins, scope);
if scope.insert(b.name.clone()) {
newly.push(b.name.clone());
}
}
walk_term(body, out, builtins, scope);
for n in newly {
scope.remove(&n);
}
}
Term::Recur { args } => {
for a in args {
walk_term(a, out, builtins, scope);
}
}
}
}
@@ -2805,47 +2785,6 @@ fn rewrite_def(
changed,
);
}
// Iter it.1: rewrite types embedded in each
// `LoopBinder.ty` (loop binders carry full Type
// annotations like mut-vars) and recurse into each
// binder's `init` and the body. `Term::Recur` has no
// embedded type.
Term::Loop { binders, body } => {
for b in binders {
rewrite_type(
&mut b.ty,
owning_module,
local_types,
import_names,
changed,
);
rewrite_term(
&mut b.init,
owning_module,
local_types,
import_names,
changed,
);
}
rewrite_term(
body,
owning_module,
local_types,
import_names,
changed,
);
}
Term::Recur { args } => {
for a in args {
rewrite_term(
a,
owning_module,
local_types,
import_names,
changed,
);
}
}
}
}
@@ -101,13 +101,6 @@ fn synthesised_print_uses_user_module_show_via_fallback() {
|| contains_xmod_show_var(body)
}
Term::Assign { value, .. } => contains_xmod_show_var(value),
// Iter it.1: a `Term::Loop` cannot itself host a synth'd
// cross-module reference, but recurse defensively.
Term::Loop { binders, body } => {
binders.iter().any(|b| contains_xmod_show_var(&b.init))
|| contains_xmod_show_var(body)
}
Term::Recur { args } => args.iter().any(contains_xmod_show_var),
Term::Lit { .. } => false,
}
}
-50
View File
@@ -2828,56 +2828,6 @@ fn mut_counter_prints_55() {
assert_eq!(stdout.trim(), "55", "mut_counter must print 55, got {stdout:?}");
}
/// Iter it.1: `examples/loop_counter.ail` exercises `Term::Loop` +
/// `Term::Recur` codegen — the loop-header block with one phi per
/// binder and `recur` as a back-edge `br`. The body
/// `(loop ((var acc Int 0) (var i Int 1)) (if (> i 10) acc (recur ...)))`
/// sums 1..10 and prints 55. End-to-end gate for the loop-header /
/// phi / recur-back-edge lowering.
#[test]
fn loop_counter_runs_and_prints_55() {
let stdout = build_and_run("loop_counter.ail");
assert_eq!(stdout.trim(), "55", "loop_counter must print 55, got {stdout:?}");
}
/// Iter it.1: a `Term::Loop` inside a `Term::Lam` body, invoked via
/// a returned closure. Protects the lambda-boundary invariant: the
/// closure thunk scopes its own loop header / phis (the `loop_frames`
/// save+reset+restore across the lambda boundary, mut.3 analogue) —
/// a `recur` inside the lambda must back-edge to the lambda's own
/// loop header, not the outer fn's. The lambda computes x*x by
/// summing x exactly x times; apply 7 prints 49.
#[test]
fn loop_in_lambda_runs_and_prints_49() {
let stdout = build_and_run("loop_in_lambda_e2e.ail");
assert_eq!(stdout.trim(), "49", "loop_in_lambda must print 49, got {stdout:?}");
}
/// Iter it.2 Phase-3: an it.2-clean structural recursion that runs.
/// `struct_rec_sum_e2e` sums [1,2,3,4,5] via plain non-tail
/// recursion on the Cons tail — the it.2 guardedness check
/// classifies it pure + total (no `!Diverge`, no `tail-app`). This
/// gate proves the structural-recursion "total" verdict is
/// behaviourally sound, not merely a typecheck assertion: a plain
/// (musttail-free) structurally-decreasing recursive call lowers
/// and runs to the correct value.
#[test]
fn struct_rec_sum_runs_and_prints_15() {
let stdout = build_and_run("struct_rec_sum_e2e.ail");
assert_eq!(stdout.trim(), "15", "struct_rec_sum must print 15, got {stdout:?}");
}
/// Iter it.2 Phase-3: the `Diverge`-path twin. `loop_needs_diverge`
/// declares `!Diverge` (loop-bearing per DD-4) and runs the it.1
/// accumulator loop to 55 — proving the Diverge-effect injection is
/// purely a typecheck-layer obligation with no codegen impact (the
/// loop lowers and runs identically to its pre-it.2 it.1 form).
#[test]
fn loop_needs_diverge_runs_and_prints_55() {
let stdout = build_and_run("loop_needs_diverge.ail");
assert_eq!(stdout.trim(), "55", "loop_needs_diverge must print 55, got {stdout:?}");
}
/// Iter mut.3: Float twin of `mut_counter_prints_55`. The mut-var
/// is `Float`, init is `0.0`, the recursive helper returns the
/// sum 1.0+...+10.0 = 55.0. The polymorphic `print` routes through
-4
View File
@@ -343,7 +343,6 @@ mod tests {
install(&mut env);
let mut locals: IndexMap<String, Type> = IndexMap::new();
let mut mut_scope_stack: Vec<IndexMap<String, Type>> = Vec::new();
let mut loop_stack: Vec<Vec<Type>> = Vec::new();
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = Subst::default();
let mut counter: u32 = 0;
@@ -355,7 +354,6 @@ mod tests {
&env,
&mut locals,
&mut mut_scope_stack,
&mut loop_stack,
&mut effects,
"<test>",
&mut subst,
@@ -588,7 +586,6 @@ mod tests {
install(&mut env);
let mut locals: IndexMap<String, Type> = IndexMap::new();
let mut mut_scope_stack: Vec<IndexMap<String, Type>> = Vec::new();
let mut loop_stack: Vec<Vec<Type>> = Vec::new();
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = Subst::default();
let mut counter: u32 = 0;
@@ -600,7 +597,6 @@ mod tests {
&env,
&mut locals,
&mut mut_scope_stack,
&mut loop_stack,
&mut effects,
"<test>",
&mut subst,
File diff suppressed because it is too large Load Diff
+1 -28
View File
@@ -397,25 +397,6 @@ impl<'a> Lifter<'a> {
name: name.clone(),
value: Box::new(self.lift_in_term(value, locals, in_def)?),
}),
Term::Loop { binders, body } => Ok(Term::Loop {
binders: binders
.iter()
.map(|b| {
Ok(ailang_core::ast::LoopBinder {
name: b.name.clone(),
ty: b.ty.clone(),
init: Box::new(self.lift_in_term(&b.init, locals, in_def)?),
})
})
.collect::<Result<Vec<_>>>()?,
body: Box::new(self.lift_in_term(body, locals, in_def)?),
}),
Term::Recur { args } => Ok(Term::Recur {
args: args
.iter()
.map(|a| self.lift_in_term(a, locals, in_def))
.collect::<Result<Vec<_>>>()?,
}),
Term::LetRec { name, ty, params, body, in_term } => {
// Iter 16b.3: post-order traversal — lift any inner
// LetRecs first. Within the body's scope, `name` and
@@ -743,11 +724,7 @@ impl<'a> Lifter<'a> {
// term at a time, beginning from a top-of-body position; fresh
// empty mut-scope stack is correct.
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
// Iter it.1 (DD-1): lift's letrec-capture re-entry walks one
// term from a top-of-body position; fresh empty loop_stack is
// correct (a loop captured into a letrec carries its own).
let mut loop_stack: Vec<Vec<crate::Type>> = Vec::new();
let ty = synth(t, &self.env, locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings_discarded)?;
let ty = synth(t, &self.env, locals, &mut mut_scope_stack, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings_discarded)?;
Ok(subst.apply(&ty))
}
}
@@ -784,10 +761,6 @@ fn contains_any_letrec(m: &Module) -> bool {
vars.iter().any(|v| term_has_letrec(&v.init)) || term_has_letrec(body)
}
Term::Assign { value, .. } => term_has_letrec(value),
Term::Loop { binders, body } => {
binders.iter().any(|b| term_has_letrec(&b.init)) || term_has_letrec(body)
}
Term::Recur { args } => args.iter().any(term_has_letrec),
}
}
for def in &m.defs {
-24
View File
@@ -609,17 +609,6 @@ impl<'a> Checker<'a> {
// a tracked binder). Walk the `value` normally.
self.walk(value, Position::Consume);
}
Term::Loop { binders, body } => {
for b in binders {
self.walk(&b.init, Position::Consume);
}
self.walk(body, pos);
}
Term::Recur { args } => {
for a in args {
self.walk(a, Position::Consume);
}
}
}
}
@@ -806,8 +795,6 @@ fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> D
Term::ReuseAs { .. } => "reuse-as",
Term::Mut { .. } => "mut",
Term::Assign { .. } => "assign",
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
};
let replacement = term_to_form_a(body);
Diagnostic::error(
@@ -947,17 +934,6 @@ fn any_sub_binder_consumed_for(
Term::Assign { value, .. } => {
any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors)
}
// Iter it.1: loop binders are scalar like mut-vars; recurse
// into children defensively so a genuine consume-of-`pname`
// inside a binder init / body / recur arg still surfaces.
Term::Loop { binders, body } => {
binders.iter().any(|b| {
any_sub_binder_consumed_for(&b.init, pname, uniq, def_name, ctors)
}) || any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
}
Term::Recur { args } => args
.iter()
.any(|a| any_sub_binder_consumed_for(a, pname, uniq, def_name, ctors)),
}
}
-30
View File
@@ -713,16 +713,11 @@ pub fn collect_mono_targets(
// mut-scope, since any `Term::Mut` will push its own frame as the
// walk descends.
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
// Iter it.1 (DD-1): mono re-synth from top-of-body — empty
// loop_stack; any `Term::Loop` pushes its own frame as the walk
// descends.
let mut loop_stack: Vec<Vec<crate::Type>> = Vec::new();
crate::synth(
&f.body,
&env,
&mut locals,
&mut mut_scope_stack,
&mut loop_stack,
&mut effects,
&f.name,
&mut subst,
@@ -1244,17 +1239,6 @@ fn rewrite_mono_calls(
Term::Assign { value, .. } => {
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::Loop { binders, body } => {
for b in binders.iter_mut() {
rewrite_mono_calls(&mut b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::Recur { args } => {
for a in args.iter_mut() {
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
}
Term::Lit { .. } => {}
}
}
@@ -1359,14 +1343,11 @@ pub(crate) fn collect_residuals_ordered(
// mut-scope, since any `Term::Mut` will push its own frame as the
// walk descends.
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
// Iter it.1 (DD-1): empty loop_stack at top-of-body re-synth.
let mut loop_stack: Vec<Vec<crate::Type>> = Vec::new();
crate::synth(
&f.body,
&env,
&mut locals,
&mut mut_scope_stack,
&mut loop_stack,
&mut effects,
&f.name,
&mut subst,
@@ -1632,17 +1613,6 @@ fn interleave_slots(
Term::Assign { value, .. } => {
interleave_slots(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Loop { binders, body } => {
for b in binders {
interleave_slots(&b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Recur { args } => {
for a in args {
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Lit { .. } => {}
}
}
@@ -133,18 +133,6 @@ fn walk_term(t: &Term) -> Result<(), CheckError> {
walk_term(body)
}
Term::Assign { value, .. } => walk_term(value),
Term::Loop { binders, body } => {
for b in binders {
walk_term(&b.init)?;
}
walk_term(body)
}
Term::Recur { args } => {
for a in args {
walk_term(a)?;
}
Ok(())
}
}
}
-11
View File
@@ -265,17 +265,6 @@ impl<'a> Checker<'a> {
self.walk(body);
}
Term::Assign { value, .. } => self.walk(value),
Term::Loop { binders, body } => {
for b in binders {
self.walk(&b.init);
}
self.walk(body);
}
Term::Recur { args } => {
for a in args {
self.walk(a);
}
}
}
}
-11
View File
@@ -352,17 +352,6 @@ impl<'a> Walker<'a> {
Term::Assign { value, .. } => {
self.walk(value, Position::Consume);
}
Term::Loop { binders, body } => {
for b in binders {
self.walk(&b.init, Position::Consume);
}
self.walk(body, pos);
}
Term::Recur { args } => {
for a in args {
self.walk(a, Position::Consume);
}
}
}
}
@@ -1,60 +0,0 @@
//! Iter it.1 (Task 5): pin tests that the four `recur` negative
//! fixtures produce their exact diagnostic codes, and that the
//! positive `loop_smoke` fixture typechecks clean.
//!
//! Spec: `docs/specs/2026-05-15-iteration-discipline.md`. The four
//! negatives live as canonical `.ail.json` (diagnostic code is the
//! load-bearing assertion, not the surface form), mirroring the
//! `mut_typecheck_pin` carve-out precedent.
use ailang_check::check_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn examples_dir() -> PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
PathBuf::from(manifest)
.parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)")
.parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)")
.join("examples")
}
/// Load the named fixture under `examples/`, run check_workspace, and
/// return the diagnostic code list (one entry per Diagnostic).
fn check_fixture(fixture_name: &str) -> Vec<String> {
let path = examples_dir().join(fixture_name);
let ws = load_workspace(&path)
.unwrap_or_else(|e| panic!("workspace `{fixture_name}` must load: {e:?}"));
let diags = check_workspace(&ws);
diags.iter().map(|d| d.code.clone()).collect()
}
#[test]
fn loop_smoke_typechecks_clean() {
let codes = check_fixture("loop_smoke.ail");
assert!(codes.is_empty(), "expected zero diagnostics, got {codes:?}");
}
#[test]
fn recur_outside_loop_is_rejected() {
let codes = check_fixture("test_recur_outside_loop.ail.json");
assert_eq!(codes, vec!["recur-outside-loop".to_string()]);
}
#[test]
fn recur_arity_mismatch_is_rejected() {
let codes = check_fixture("test_recur_arity_mismatch.ail.json");
assert_eq!(codes, vec!["recur-arity-mismatch".to_string()]);
}
#[test]
fn recur_type_mismatch_is_rejected() {
let codes = check_fixture("test_recur_type_mismatch.ail.json");
assert_eq!(codes, vec!["recur-type-mismatch".to_string()]);
}
#[test]
fn recur_not_in_tail_position_is_rejected() {
let codes = check_fixture("test_recur_not_in_tail_position.ail.json");
assert_eq!(codes, vec!["recur-not-in-tail-position".to_string()]);
}
@@ -1,129 +0,0 @@
//! Iter it.2: pin tests for the structural-recursion guardedness
//! pass + the it.2-only `tail:true` grandfather + the first real
//! `Diverge` effect injection.
//!
//! Spec: `docs/specs/2026-05-15-iteration-discipline.md`. Mirrors the
//! it.1 `loop_recur_pin.rs` harness verbatim (same inline
//! `check_fixture` helper, no `mod common`).
use ailang_check::check_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn examples_dir() -> PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
PathBuf::from(manifest)
.parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)")
.parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)")
.join("examples")
}
/// Load the named fixture under `examples/`, run check_workspace, and
/// return the diagnostic code list (one entry per Diagnostic).
fn check_fixture(fixture_name: &str) -> Vec<String> {
let path = examples_dir().join(fixture_name);
let ws = load_workspace(&path)
.unwrap_or_else(|e| panic!("workspace `{fixture_name}` must load: {e:?}"));
let diags = check_workspace(&ws);
diags.iter().map(|d| d.code.clone()).collect()
}
#[test]
fn structural_list_len_is_clean() {
// Plain non-tail recursion on a Cons sub-component typechecks
// clean: structurally guarded ⇒ pure + total, no diagnostics.
assert!(
check_fixture("struct_rec_list_len.ail").is_empty(),
"expected zero diagnostics for structural list length"
);
}
#[test]
fn foldl_accumulator_is_structural_and_clean() {
// A foldl-shape accumulator walk (structural on the tail,
// unconstrained accumulator) classifies as structural recursion
// and stays clean + Diverge-free (spec D1).
assert!(
check_fixture("struct_rec_foldl_sum.ail").is_empty(),
"expected zero diagnostics for foldl-shape accumulator walk"
);
}
#[test]
fn non_structural_self_call_is_rejected() {
// A self-call passing the un-decreased parameter at every
// candidate position, not `tail`-marked, fires
// `non-structural-recursion`.
assert!(
check_fixture("test_non_structural_recursion.ail.json")
.contains(&"non-structural-recursion".to_string()),
"expected non-structural-recursion diagnostic"
);
}
#[test]
fn tree_forest_mutual_is_clean() {
// Mutual tree/forest recursion over one ADT family (the
// cross-reference edge unions {Tree, Forest}) classifies as a
// mutual structural group: clean, Diverge-free.
assert!(
check_fixture("struct_rec_tree_forest.ail").is_empty(),
"expected zero diagnostics for same-family mutual recursion"
);
}
#[test]
fn mutual_cross_family_is_rejected() {
// Mutual recursion whose members' structural params lie in two
// unrelated ADT families is not a valid mutual structural group
// (DD-3): the cross-call is an unguarded recursive call.
assert!(
check_fixture("test_mutual_cross_family.ail.json")
.contains(&"non-structural-recursion".to_string()),
"expected non-structural-recursion for cross-family mutual recursion"
);
}
#[test]
fn loop_fn_declaring_diverge_is_clean() {
// A loop-bearing fn that declares `!Diverge` in its effect row
// reconciles clean (DD-4 / D2).
assert!(
check_fixture("loop_needs_diverge.ail").is_empty(),
"expected zero diagnostics for loop fn declaring !Diverge"
);
}
#[test]
fn loop_fn_missing_diverge_is_rejected() {
// A loop-bearing fn missing `!Diverge` raises the existing
// `undeclared-effect` (no new diagnostic variant; DD-4).
assert!(
check_fixture("test_loop_missing_diverge.ail.json")
.contains(&"undeclared-effect".to_string()),
"expected undeclared-effect for loop fn missing !Diverge"
);
}
#[test]
fn structural_recursion_is_diverge_free() {
// Structural recursion injects no effect: a structural list walk
// with no `loop` and no declared `!Diverge` stays clean.
assert!(
check_fixture("struct_rec_list_len.ail").is_empty(),
"structural recursion must be Diverge-free"
);
}
#[test]
fn non_structural_recursion_code_is_registered() {
// A CheckError::NonStructuralRecursion must map to the kebab code.
// This step only pins the code() arm exists and returns the exact
// string; fixture-level behaviour is pinned in Task 2+.
use ailang_check::CheckError;
let e = CheckError::NonStructuralRecursion {
callee: "f".into(),
arg: "n".into(),
};
assert_eq!(e.code(), "non-structural-recursion");
}
-40
View File
@@ -201,17 +201,6 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
walk(body, out);
}
Term::Assign { value, .. } => walk(value, out),
Term::Loop { binders, body } => {
for b in binders {
walk(&b.init, out);
}
walk(body, out);
}
Term::Recur { args } => {
for a in args {
walk(a, out);
}
}
Term::Lit { .. } | Term::Var { .. } => {}
}
}
@@ -406,15 +395,6 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
escapes(body, tainted, in_tail)
}
Term::Assign { value, .. } => escapes(value, tainted, false),
Term::Loop { binders, body } => {
for b in binders {
if escapes(&b.init, tainted, false) {
return true;
}
}
escapes(body, tainted, in_tail)
}
Term::Recur { args } => args.iter().any(|a| escapes(a, tainted, false)),
}
}
@@ -544,26 +524,6 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
}
collect_free_vars(value, bound, out);
}
// Iter it.1: loop binder names bind inside the body and later
// binder inits, mirroring `Term::Mut`. Recur args are uses.
Term::Loop { binders, body } => {
let mut newly: Vec<String> = Vec::new();
for b in binders {
collect_free_vars(&b.init, bound, out);
if bound.insert(b.name.clone()) {
newly.push(b.name.clone());
}
}
collect_free_vars(body, bound, out);
for n in newly {
bound.remove(&n);
}
}
Term::Recur { args } => {
for a in args {
collect_free_vars(a, bound, out);
}
}
}
}
-28
View File
@@ -144,12 +144,6 @@ impl<'a> Emitter<'a> {
let saved_mut_allocas = std::mem::take(&mut self.mut_var_allocas);
let saved_pending_allocas = std::mem::take(&mut self.pending_entry_allocas);
let saved_entry_marker = self.entry_block_end_marker.take();
// Iter it.1: a `loop` inside a lambda body scopes its header /
// phis to the closure's own body, not the outer fn's. Save and
// reset `loop_frames` (the exact mut.3 analogue to
// `mut_var_allocas` above) so a `Term::Recur` inside the thunk
// cannot back-edge to an outer fn's loop header.
let saved_loop_frames = std::mem::take(&mut self.loop_frames);
// Iter 18d.4 fix: a lambda thunk is its own fn frame for
// param-mode lookup. The outer fn's params are not in scope
// inside the thunk; the thunk's own params are pushed below
@@ -264,7 +258,6 @@ impl<'a> Emitter<'a> {
self.mut_var_allocas = saved_mut_allocas;
self.pending_entry_allocas = saved_pending_allocas;
self.entry_block_end_marker = saved_entry_marker;
self.loop_frames = saved_loop_frames;
// 3. Emit allocation + capture filling + closure-pair packing
// in the OUTER body. Captures use 8 bytes each; closure-pair
@@ -512,27 +505,6 @@ impl<'a> Emitter<'a> {
}
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
}
// Iter it.1: loop binder names bind inside the body and
// later binder inits — they are NOT captures, mirroring
// the `Term::Mut` arm above. Recur args are uses.
Term::Loop { binders, body } => {
let mut newly_bound: Vec<String> = Vec::new();
for b in binders {
Self::collect_captures(&b.init, bound, captures, captures_set, builtins, top_level);
if bound.insert(b.name.clone()) {
newly_bound.push(b.name.clone());
}
}
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
for n in newly_bound {
bound.remove(&n);
}
}
Term::Recur { args } => {
for a in args {
Self::collect_captures(a, bound, captures, captures_set, builtins, top_level);
}
}
}
}
-164
View File
@@ -735,28 +735,6 @@ struct Emitter<'a> {
/// at fn-body emission. Used to splice `pending_entry_allocas`
/// into the entry block once body lowering completes.
entry_block_end_marker: Option<usize>,
/// Iter it.1: stack of in-flight `Term::Loop` frames. Innermost
/// last. `Term::Recur` consults the innermost frame for the
/// header label and records its back-edge incoming values; the
/// `Term::Loop` arm patches each phi's incoming list once the
/// body (and all its recur sites) has been lowered. Saved /
/// reset / restored across the lambda boundary (a `loop` inside
/// a closure scopes its header to the closure's own body), the
/// exact mut.3 analogue to `mut_var_allocas`.
loop_frames: Vec<LoopFrame>,
}
/// Iter it.1: one in-flight `Term::Loop` being lowered. `phis`
/// carries, per binder, `(phi_ssa, llvm_ty, placeholder)` — the
/// placeholder is a unique token emitted in place of the phi's
/// back-edge incoming list and string-replaced once `recur_edges`
/// is complete. `recur_edges` collects `(pred_block_label,
/// [arg_ssa,...])` one entry per `Term::Recur` reached in the body.
#[derive(Debug, Clone)]
struct LoopFrame {
header: String,
phis: Vec<(String, String, String)>,
recur_edges: Vec<(String, Vec<String>)>,
}
#[derive(Debug, Clone)]
@@ -877,7 +855,6 @@ impl<'a> Emitter<'a> {
mut_var_allocas: BTreeMap::new(),
pending_entry_allocas: String::new(),
entry_block_end_marker: None,
loop_frames: Vec::new(),
}
}
@@ -1083,7 +1060,6 @@ impl<'a> Emitter<'a> {
self.mut_var_allocas.clear();
self.pending_entry_allocas.clear();
self.entry_block_end_marker = None;
self.loop_frames.clear();
for (i, pname) in f.params.iter().enumerate() {
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
self.current_param_modes.insert(pname.clone(), mode);
@@ -1861,141 +1837,6 @@ impl<'a> Emitter<'a> {
));
Ok(("0".into(), "i8".into()))
}
// Iter it.1: a `Term::Loop` lowers to a header block with
// one `phi` per binder. Binder inits flow in from the
// predecessor block; every enclosed `Term::Recur` adds a
// back-edge incoming value. The phi's back-edge incoming
// list is emitted as a unique placeholder token and
// string-replaced once the body (and all its recur sites)
// has been lowered — phi instructions must syntactically
// precede the body, but the recur predecessors are only
// known after lowering it.
Term::Loop { binders, body } => {
// 1. Lower each binder init in the CURRENT block.
let pred = self.current_block.clone();
// Per binder: (name, ail_ty, init_ssa, llvm_ty).
let mut init_ssa: Vec<(String, Type, String, String)> = Vec::new();
for b in binders {
let lty = llvm_type(&b.ty)?;
let (v, vty) = self.lower_term(&b.init)?;
if vty != lty {
return Err(CodegenError::Internal(format!(
"Term::Loop binder `{}`: init LLVM type {vty} != declared {lty}",
b.name
)));
}
init_ssa.push((b.name.clone(), b.ty.clone(), v, lty));
}
let id = self.fresh_id();
let header = format!("loop.header.{id}");
self.body.push_str(&format!(" br label %{header}\n"));
self.start_block(&header);
// 2. One phi per binder. Each phi's back-edge incoming
// list is a placeholder, patched in step 4. Bind the
// phi SSA into `self.locals` so a `Term::Var` for the
// binder name inside the body resolves to it (a loop
// binder shadows like a let binding).
let mut phis: Vec<(String, String, String)> = Vec::new();
let mut saved_locals: Vec<(String, Option<(String, String, String, Type)>)> =
Vec::new();
for (idx, (name, ail_ty, iv, lty)) in init_ssa.iter().enumerate() {
let p = self.fresh_ssa();
let placeholder = format!("/*RECUR_EDGES.{id}.{idx}*/");
self.body.push_str(&format!(
" {p} = phi {lty} [ {iv}, %{pred} ]{placeholder}\n"
));
let prior = self
.locals
.iter()
.rposition(|(n, _, _, _)| n == name)
.map(|pos| self.locals.remove(pos));
self.locals.push((
name.clone(),
p.clone(),
lty.clone(),
ail_ty.clone(),
));
saved_locals.push((name.clone(), prior));
phis.push((p.clone(), lty.clone(), placeholder));
}
// 3. Lower the body inside this loop frame.
self.loop_frames.push(LoopFrame {
header: header.clone(),
phis: phis.clone(),
recur_edges: Vec::new(),
});
let body_result = self.lower_term(body);
let frame = self
.loop_frames
.pop()
.expect("loop frame pushed above must still be on the stack");
// Restore the shadowed locals unconditionally so an
// error during body lowering does not leak the binder
// bindings into the outer scope.
for (name, prior) in saved_locals.into_iter().rev() {
if let Some(pos) = self.locals.iter().rposition(|(n, _, _, _)| n == &name) {
self.locals.remove(pos);
}
if let Some(p) = prior {
self.locals.push(p);
}
}
let (body_v, body_ty) = body_result?;
// 4. Patch each phi placeholder with the collected
// back-edge incomings (one `[ arg, %pred ]` per
// recur site). A loop whose only exit is `recur`
// has zero non-recur predecessors after the entry
// edge — that is fine; the phi still lists the
// entry edge plus the recur edges.
for (binder_idx, (_p, _lty, placeholder)) in frame.phis.iter().enumerate() {
let mut edges = String::new();
for (pred_lbl, args) in &frame.recur_edges {
let arg = args.get(binder_idx).ok_or_else(|| {
CodegenError::Internal(format!(
"recur edge from %{pred_lbl} missing arg #{binder_idx} \
— typecheck guarantees arity"
))
})?;
edges.push_str(&format!(", [ {arg}, %{pred_lbl} ]"));
}
self.body = self.body.replacen(placeholder.as_str(), &edges, 1);
}
// 5. The loop's value is the body value on the
// non-recur exit path. If every path recurred, the
// body block is terminated and the value is unused.
Ok((body_v, body_ty))
}
// Iter it.1: a `Term::Recur` is a back-edge `br` to the
// innermost enclosing loop header. It records its argument
// SSA values + the current block label so the loop arm can
// patch the header phis. `block_terminated = true` is a
// NEW, parallel setter (it.1 additive constraint — it does
// NOT touch the seven existing tail-driven setters).
Term::Recur { args } => {
let mut arg_ssa: Vec<String> = Vec::with_capacity(args.len());
for a in args {
let (v, _ty) = self.lower_term(a)?;
arg_ssa.push(v);
}
let from = self.current_block.clone();
let frame = self.loop_frames.last_mut().ok_or_else(|| {
CodegenError::Internal(
"Term::Recur without an enclosing loop frame — \
typecheck (RecurOutsideLoop) guarantees this cannot happen"
.into(),
)
})?;
let header = frame.header.clone();
frame.recur_edges.push((from, arg_ssa));
self.body.push_str(&format!(" br label %{header}\n"));
self.block_terminated = true;
Ok(("0".into(), "i8".into()))
}
}
}
@@ -3252,11 +3093,6 @@ impl<'a> Emitter<'a> {
// is exhaustive on Term so the arms must exist.
Term::Mut { body, .. } => self.synth_with_extras(body, extras),
Term::Assign { .. } => Ok(Type::unit()),
// Iter it.1: a loop's static type is its body's static
// type (spec §"Data model"); `Term::Recur` does not fall
// through (its synth value is never consumed).
Term::Loop { binders: _, body } => self.synth_with_extras(body, extras),
Term::Recur { .. } => Ok(Type::unit()),
}
}
}
-19
View File
@@ -284,8 +284,6 @@ Parenthesised forms:
(mut (var NAME TYPE INIT)* BODY-TERM+) ; local mutable-state block (Iter mut.1)
(var NAME TYPE INIT) ; mut-var declaration; only inside (mut ...)
(assign NAME VALUE-TERM) ; mut-var update; only inside (mut ...)
(loop ((var NAME TYPE INIT)*) BODY-TERM+) ; named loop head (Iter it.1)
(recur TERM*) ; backward jump to nearest enclosing (loop ...)
```
Notes:
@@ -324,23 +322,6 @@ Notes:
pass (iter mut.2) rejects it with `mut-assign-out-of-scope`. The
expression's static type is Unit. See spec
`docs/specs/2026-05-15-mut-local.md`.
- `loop` introduces one or more named, typed, initialised binders
(the same `(var NAME TYPE INIT)` shape `mut` uses) wrapped in an
explicit binder list `(...)`, followed by one or more body terms
right-folded into `Term::Seq` exactly as `mut` does. Each binder's
`INIT` is evaluated in scope of the outer environment plus the
already-declared binders; the body is in scope of all binders. The
loop's static type is the body's static type.
- `recur` re-enters the lexically nearest enclosing `loop`,
rebinding its binders positionally. It must appear in tail
position of that loop's body. Diagnostics: `recur-outside-loop`
(no enclosing loop), `recur-arity-mismatch` (arg count ≠ binder
count), `recur-type-mismatch` (arg type ≠ binder type),
`recur-not-in-tail-position` (recur reached outside the loop
body's tail context). `loop`/`recur` are additive as of iter it.1;
the structural-recursion restriction and the `Diverge` effect land
in it.2. See spec
`docs/specs/2026-05-15-iteration-discipline.md`.
## Patterns
-61
View File
@@ -537,19 +537,6 @@ pub enum Term {
name: String,
value: Box<Term>,
},
/// Named loop head. The only repetition form besides structural
/// recursion. Iter it.1. `recur` re-enters the lexically
/// nearest enclosing `Loop`, rebinding `binders` positionally.
Loop {
binders: Vec<LoopBinder>,
body: Box<Term>,
},
/// Backward jump to the lexically nearest enclosing `Loop`.
/// Must be in tail position of that loop's body. Iter it.1.
Recur {
#[serde(default)]
args: Vec<Term>,
},
}
/// One arm of a [`Term::Match`].
@@ -593,17 +580,6 @@ pub struct MutVar {
pub init: Term,
}
/// One named, typed, initialised binder of a [`Term::Loop`].
/// Mirrors [`MutVar`] (DD-2): no `PartialEq` — codegen maps the
/// binder name to an SSA value, never compares binders.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoopBinder {
pub name: String,
#[serde(rename = "type")]
pub ty: Type,
pub init: Box<Term>,
}
/// A match pattern.
///
/// The JSON discriminator is the `p` field. Patterns are linear: each
@@ -970,41 +946,4 @@ mod tests {
other => panic!("variant mismatch: {other:?}"),
}
}
/// Iter it.1: round-trip a `Term::Loop` through JSON. Pins the
/// canonical-form shape and the `LoopBinder.ty` serde rename
/// (`"type"`); a future rename or a `skip_serializing_if` on the
/// binder fields would break the content-addressed identity this
/// pin protects.
#[test]
fn term_loop_round_trips_through_json() {
let t = Term::Loop {
binders: vec![LoopBinder {
name: "i".into(),
ty: Type::int(),
init: Box::new(Term::Lit {
lit: Literal::Int { value: 0 },
}),
}],
body: Box::new(Term::Recur {
args: vec![Term::Var { name: "i".into() }],
}),
};
let j = serde_json::to_string(&t).expect("serialise");
assert!(j.contains(r#""t":"loop""#));
assert!(j.contains(r#""type":"#)); // LoopBinder.ty serde rename
let back: Term = serde_json::from_str(&j).expect("deserialise");
assert_eq!(serde_json::to_string(&back).expect("re-serialise"), j);
}
/// Iter it.1: a zero-arg `Term::Recur` round-trips. `args` carries
/// `#[serde(default)]` so an absent `args` key still deserialises;
/// the empty vector serialises explicitly.
#[test]
fn term_recur_empty_args_round_trips() {
let t = Term::Recur { args: vec![] };
let j = serde_json::to_string(&t).expect("serialise");
let back: Term = serde_json::from_str(&j).expect("deserialise");
assert_eq!(serde_json::to_string(&back).expect("re-serialise"), j);
}
}
-129
View File
@@ -363,18 +363,6 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
used.insert(name.clone());
collect_used_in_term(value, used);
}
Term::Loop { binders, body } => {
for b in binders {
used.insert(b.name.clone());
collect_used_in_term(&b.init, used);
}
collect_used_in_term(body, used);
}
Term::Recur { args } => {
for a in args {
collect_used_in_term(a, used);
}
}
}
}
@@ -602,32 +590,6 @@ impl Desugarer {
name: name.clone(),
value: Box::new(self.desugar_term(value, scope)),
},
Term::Loop { binders, body } => {
// Iter it.1: loop binders introduce source-level names
// like mut-vars; extend the scope per-binder with a
// `LetBound` sentinel so a generated fresh name cannot
// collide. Binder inits see earlier binders.
let mut inner = scope.clone();
let new_binders: Vec<LoopBinder> = binders
.iter()
.map(|b| {
let init = self.desugar_term(&b.init, &inner);
inner.insert(b.name.clone(), ScopeEntry::LetBound);
LoopBinder {
name: b.name.clone(),
ty: b.ty.clone(),
init: Box::new(init),
}
})
.collect();
Term::Loop {
binders: new_binders,
body: Box::new(self.desugar_term(body, &inner)),
}
}
Term::Recur { args } => Term::Recur {
args: args.iter().map(|a| self.desugar_term(a, scope)).collect(),
},
Term::LetRec { name, ty, params, body, in_term } => {
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
// Iter 16b.2: extend the lift to the path-1 safe subset —
@@ -1272,23 +1234,6 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
}
free_vars_in_term(value, bound, out);
}
Term::Loop { binders, body } => {
// Iter it.1: a loop binder name binds inside the body.
// Each binder's `init` is evaluated in scope of the OUTER
// environment plus the already-declared binders; the body
// is in scope of all binders. Mirrors `Term::Mut`.
let mut b = bound.clone();
for binder in binders {
free_vars_in_term(&binder.init, &b, out);
b.insert(binder.name.clone());
}
free_vars_in_term(body, &b, out);
}
Term::Recur { args } => {
for a in args {
free_vars_in_term(a, bound, out);
}
}
}
}
@@ -1482,43 +1427,6 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
name: name.clone(),
value: Box::new(subst_var(value, from, to)),
},
Term::Loop { binders, body } => {
// Iter it.1: loop binders lexically shadow outer names,
// exactly like mut-vars (above). Once a binder named
// `from` is declared, later inits AND the body stop
// substituting.
let mut shadowed = false;
let new_binders: Vec<LoopBinder> = binders
.iter()
.map(|b| {
let init = if shadowed {
(*b.init).clone()
} else {
subst_var(&b.init, from, to)
};
if b.name == from {
shadowed = true;
}
LoopBinder {
name: b.name.clone(),
ty: b.ty.clone(),
init: Box::new(init),
}
})
.collect();
let body_rw = if shadowed {
(**body).clone()
} else {
subst_var(body, from, to)
};
Term::Loop {
binders: new_binders,
body: Box::new(body_rw),
}
}
Term::Recur { args } => Term::Recur {
args: args.iter().map(|a| subst_var(a, from, to)).collect(),
},
}
}
@@ -1660,23 +1568,6 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
name: assign_name.clone(),
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
},
Term::Loop { binders, body } => Term::Loop {
binders: binders
.iter()
.map(|b| LoopBinder {
name: b.name.clone(),
ty: b.ty.clone(),
init: Box::new(subst_call_with_extras(&b.init, name, lifted, extras)),
})
.collect(),
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
},
Term::Recur { args } => Term::Recur {
args: args
.iter()
.map(|a| subst_call_with_extras(a, name, lifted, extras))
.collect(),
},
}
}
@@ -1749,14 +1640,6 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
find_non_callee_use(value, name)
}
}
// Iter it.1: scan each binder's init plus the body. Loop
// binders never appear in callee position, so any matching
// `Term::Var` inside a loop is non-callee by construction.
Term::Loop { binders, body } => binders
.iter()
.find_map(|b| find_non_callee_use(&b.init, name))
.or_else(|| find_non_callee_use(body, name)),
Term::Recur { args } => args.iter().find_map(|a| find_non_callee_use(a, name)),
}
}
@@ -1804,10 +1687,6 @@ mod tests {
vars.iter().any(|v| any_nested_ctor(&v.init)) || any_nested_ctor(body)
}
Term::Assign { value, .. } => any_nested_ctor(value),
Term::Loop { binders, body } => {
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
}
Term::Recur { args } => args.iter().any(any_nested_ctor),
}
}
@@ -1838,10 +1717,6 @@ mod tests {
vars.iter().any(|v| any_let_rec(&v.init)) || any_let_rec(body)
}
Term::Assign { value, .. } => any_let_rec(value),
Term::Loop { binders, body } => {
binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body)
}
Term::Recur { args } => args.iter().any(any_let_rec),
}
}
@@ -2963,10 +2838,6 @@ mod tests {
vars.iter().any(|v| any_lit_pattern(&v.init)) || any_lit_pattern(body)
}
Term::Assign { value, .. } => any_lit_pattern(value),
Term::Loop { binders, body } => {
binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body)
}
Term::Recur { args } => args.iter().any(any_lit_pattern),
}
}
-28
View File
@@ -1259,22 +1259,6 @@ where
walk_term_embedded_types(body, f)
}
Term::Assign { value, .. } => walk_term_embedded_types(value, f),
// Iter it.1: each `LoopBinder.ty` is an embedded type — walk
// it, then recurse into each binder's `init` and the body.
// `Term::Recur` carries no embedded type.
Term::Loop { binders, body } => {
for b in binders {
walk_type(&b.ty, f)?;
walk_term_embedded_types(&b.init, f)?;
}
walk_term_embedded_types(body, f)
}
Term::Recur { args } => {
for a in args {
walk_term_embedded_types(a, f)?;
}
Ok(())
}
}
}
@@ -1408,18 +1392,6 @@ where
walk_term(body, f)
}
Term::Assign { value, .. } => walk_term(value, f),
Term::Loop { binders, body } => {
for b in binders {
walk_term(&b.init, f)?;
}
walk_term(body, f)
}
Term::Recur { args } => {
for a in args {
walk_term(a, f)?;
}
Ok(())
}
}
}
@@ -3,7 +3,7 @@
//! under `examples/*.ail.json` after milestone close. The list is
//! hardcoded; any change is a deliberate, brainstorm-level decision.
//!
//! Twenty carve-outs post iter it.2 (2026-05-15):
//! Twelve carve-outs post iter mut.2 (2026-05-15):
//! - §C4 (a) subject-matter: 7 fixtures from form-a.1 milestone
//! close (canonical-form rejection / unbound / class-def rejection).
//! - Iter mut.2: 5 negative typecheck fixtures for the new mut /
@@ -13,14 +13,6 @@
//! fixture convention. The positive nested-shadow case is a
//! .ail.json carve-out for symmetry with its negative siblings
//! (the driver tests all five from one helper).
//! - Iter mut.4-tidy: 1 lambda-capture-of-mut-var negative fixture.
//! - Iter it.1: 4 recur negative typecheck fixtures.
//! - Iter it.2: 3 negative fixtures — self non-structural recursion,
//! mutual cross-family recursion (both fire `non-structural-
//! recursion`), and a loop-bearing fn missing `!Diverge` (fires
//! the existing `undeclared-effect`, no new variant per DD-4). The
//! diagnostic code is the load-bearing assertion, not the surface
//! form.
//! - §C4 (b) compile-time-embed: retired 2026-05-14 by milestone
//! prelude-decouple; the prelude is now embedded as `prelude.ail`
//! in `ailang-surface` and parsed at compile time via
@@ -45,15 +37,6 @@ const EXPECTED: &[&str] = &[
"test_mut_var_unsupported_type.ail.json",
// Iter mut.4-tidy — lambda-capture-of-mut-var rejection
"test_mut_var_captured_by_lambda.ail.json",
// Iter it.1 — recur negative typecheck fixtures
"test_recur_outside_loop.ail.json",
"test_recur_arity_mismatch.ail.json",
"test_recur_type_mismatch.ail.json",
"test_recur_not_in_tail_position.ail.json",
// Iter it.2 — non-structural-recursion negative fixtures
"test_non_structural_recursion.ail.json",
"test_mutual_cross_family.ail.json",
"test_loop_missing_diverge.ail.json",
];
fn examples_dir() -> std::path::PathBuf {
+17 -13
View File
@@ -151,17 +151,6 @@ fn design_md_anchors_every_term_variant() {
value: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
r#""t": "loop""#,
Term::Loop {
binders: Vec::new(),
body: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
r#""t": "recur""#,
Term::Recur { args: Vec::new() },
),
];
for (anchor, term) in exemplars {
@@ -183,8 +172,6 @@ fn design_md_anchors_every_term_variant() {
Term::ReuseAs { .. } => "reuse-as",
Term::Mut { .. } => "mut",
Term::Assign { .. } => "assign",
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
};
assert!(
data_model_section().contains(anchor),
@@ -443,3 +430,20 @@ fn data_model_section_is_bounded() {
section.len()
);
}
#[test]
fn design_md_documents_the_accumulator_over_iteration_idiom() {
let design = std::fs::read_to_string(
concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/DESIGN.md"),
)
.expect("read DESIGN.md");
assert!(
design.contains("Accumulator-over-iteration shape (documented idiom, not an enforced rule)"),
"the F1/F4 documented-idiom note must not be silently dropped from DESIGN.md"
);
assert!(
design.contains("examples/mut_counter.ail is the reference")
|| design.contains("`examples/mut_counter.ail` is the reference"),
"the F1/F4 note must keep pointing at the canonical fallback fixture"
);
}
@@ -49,8 +49,6 @@ enum VariantTag {
TermReuseAs,
TermMut,
TermAssign,
TermLoop,
TermRecur,
// Pattern
PatternWild,
PatternVar,
@@ -98,8 +96,6 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
VariantTag::TermReuseAs,
VariantTag::TermMut,
VariantTag::TermAssign,
VariantTag::TermLoop,
VariantTag::TermRecur,
VariantTag::PatternWild,
VariantTag::PatternVar,
VariantTag::PatternLit,
@@ -248,20 +244,6 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
observed.insert(VariantTag::TermAssign);
visit_term(value, observed);
}
Term::Loop { binders, body } => {
observed.insert(VariantTag::TermLoop);
for b in binders {
visit_type(&b.ty, observed);
visit_term(&b.init, observed);
}
visit_term(body, observed);
}
Term::Recur { args } => {
observed.insert(VariantTag::TermRecur);
for a in args {
visit_term(a, observed);
}
}
}
}
-13
View File
@@ -128,17 +128,6 @@ fn spec_mentions_every_term_variant() {
value: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
"(loop",
Term::Loop {
binders: Vec::new(),
body: Box::new(Term::Lit { lit: Literal::Unit }),
},
),
(
"(recur",
Term::Recur { args: Vec::new() },
),
];
for (anchor, term) in exemplars {
@@ -162,8 +151,6 @@ fn spec_mentions_every_term_variant() {
Term::ReuseAs { .. } => "reuse-as",
Term::Mut { .. } => "mut",
Term::Assign { .. } => "assign",
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
};
assert!(
FORM_A_SPEC.contains(anchor),
-121
View File
@@ -935,37 +935,6 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow
out.push_str(" := ");
write_term(out, value, level, owning_module);
}
Term::Loop { binders, body } => {
// Iter it.1: prose-side minimal-correctness rendering for
// the `(loop ...)` head, mirroring the `mut` block shape
// above. The prose surface for loops is not yet fully
// designed; a follow-on prose iter refines it once the
// LLM-author signal arrives.
out.push_str("loop {\n");
for b in binders {
indent(out, level + 1);
out.push_str("var ");
out.push_str(&b.name);
out.push_str(" = ");
write_term(out, &b.init, level + 1, owning_module);
out.push_str(";\n");
}
indent(out, level + 1);
write_term(out, body, level + 1, owning_module);
out.push('\n');
indent(out, level);
out.push('}');
}
Term::Recur { args } => {
out.push_str("recur(");
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_term(out, a, level, owning_module);
}
out.push(')');
}
}
}
@@ -1164,26 +1133,6 @@ fn count_free_var(name: &str, t: &Term) -> usize {
let n_use = if assign_name == name { 1 } else { 0 };
n_use + count_free_var(name, value)
}
// Iter it.1: a loop binder named `name` shadows the outer
// binding for both later binder inits and the body, exactly
// like `Term::Mut`.
Term::Loop { binders, body } => {
let mut total = 0usize;
let mut shadowed = false;
for b in binders {
if !shadowed {
total += count_free_var(name, &b.init);
}
if b.name == name {
shadowed = true;
}
}
if !shadowed {
total += count_free_var(name, body);
}
total
}
Term::Recur { args } => args.iter().map(|a| count_free_var(name, a)).sum(),
}
}
@@ -1336,39 +1285,6 @@ fn subst_var_with_term(t: &Term, name: &str, replacement: &Term) -> Term {
name: assign_name.clone(),
value: Box::new(subst_var_with_term(value, name, replacement)),
},
Term::Loop { binders, body } => {
let mut shadowed = false;
let new_binders: Vec<ailang_core::ast::LoopBinder> = binders
.iter()
.map(|b| {
let init = if shadowed {
(*b.init).clone()
} else {
subst_var_with_term(&b.init, name, replacement)
};
if b.name == name {
shadowed = true;
}
ailang_core::ast::LoopBinder {
name: b.name.clone(),
ty: b.ty.clone(),
init: Box::new(init),
}
})
.collect();
let body_rw = if shadowed {
(**body).clone()
} else {
subst_var_with_term(body, name, replacement)
};
Term::Loop {
binders: new_binders,
body: Box::new(body_rw),
}
}
Term::Recur { args } => Term::Recur {
args: args.iter().map(|a| subst_var_with_term(a, name, replacement)).collect(),
},
}
}
@@ -2185,43 +2101,6 @@ mod tests {
assert_eq!(render_term(&t), "tail not(x)");
}
/// Iter it.1: prose-projection lockstep for `Term::Loop` /
/// `Term::Recur`. The property protected: the prose renderer
/// projects a loop's binders + body and a recur's args without
/// dropping or reordering them (free-var / subst arms are
/// exercised by the existing prose suite). Prose is a one-way
/// projection (no Form-B parser exists — see CLAUDE.md), so an
/// AST-equality round-trip is not expressible; this render
/// assertion is the feasible lockstep, using the same
/// `render_term` harness the neighbouring render tests use.
#[test]
fn loop_and_recur_project_to_prose() {
let t = Term::Loop {
binders: vec![ailang_core::ast::LoopBinder {
name: "i".into(),
ty: ailang_core::ast::Type::Con {
name: "Int".into(),
args: vec![],
},
init: Box::new(Term::Lit {
lit: ailang_core::ast::Literal::Int { value: 0 },
}),
}],
body: Box::new(Term::Recur {
args: vec![ivar("i")],
}),
};
let rendered = render_term(&t);
assert!(
rendered.contains("loop {") && rendered.contains("var i = 0"),
"loop head not projected, got:\n{rendered}"
);
assert!(
rendered.contains("recur(i)"),
"recur not projected, got:\n{rendered}"
);
}
// ---- Polish 4: long doc-string wrap ----
#[test]
+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(')');
}
}
}