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:
2026-05-17 23:00:15 +02:00
parent a5eebcd5fd
commit a179ec30a0
30 changed files with 1113 additions and 4 deletions
+53
View File
@@ -261,6 +261,23 @@ pub(crate) fn substitute_rigids_in_term(t: &Term, mapping: &BTreeMap<String, Typ
name: name.clone(),
value: Box::new(substitute_rigids_in_term(value, mapping)),
},
Term::Loop { binders, body } => Term::Loop {
binders: binders
.iter()
.map(|b| ailang_core::ast::LoopBinder {
name: b.name.clone(),
ty: substitute_rigids(&b.ty, mapping),
init: substitute_rigids_in_term(&b.init, mapping),
})
.collect(),
body: Box::new(substitute_rigids_in_term(body, mapping)),
},
Term::Recur { args } => Term::Recur {
args: args
.iter()
.map(|a| substitute_rigids_in_term(a, mapping))
.collect(),
},
}
}
@@ -2679,6 +2696,31 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
// be in tail position. Its `value` is also not in tail
// position.
Term::Assign { value, .. } => verify_tail_positions(value, false),
// loop-recur iter 1: binder inits are evaluated once on loop
// entry, NOT in tail position (mirror the Term::Mut arm
// above). The loop body inherits the enclosing tail position
// (the loop's value is the body's value on the exiting
// iteration). This arm only defines tail-app descent through
// the new node; it makes NO claim about recur tail-position
// (that is loop-recur iter 2's `verify_loop_body`). The
// tail-app verification role is unchanged — no pre-existing
// fixture contains a `loop`/`recur` node.
Term::Loop { binders, body } => {
for b in binders {
verify_tail_positions(&b.init, false)?;
}
verify_tail_positions(body, is_tail)
}
// recur transfers control and does not fall through, so there
// is no tail position to propagate. Its args are evaluated in
// non-tail position (call-argument-like). Introduces no
// tail-app violation.
Term::Recur { args } => {
for a in args {
verify_tail_positions(a, false)?;
}
Ok(())
}
}
}
@@ -3592,6 +3634,8 @@ pub(crate) fn synth(
Term::ReuseAs { .. } => "reuse-as",
Term::Mut { .. } => "mut",
Term::Assign { .. } => "assign",
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
Term::Ctor { .. } | Term::Lam { .. } => unreachable!(),
};
return Err(CheckError::ReuseAsNonAllocatingBody {
@@ -3687,6 +3731,15 @@ pub(crate) fn synth(
}
}
}
// loop-recur iter 1: typecheck semantics (binder typing,
// recur arity/type unification, verify_loop_body
// tail-position, the four Recur* CheckError variants) land
// in loop-recur iter 2. This iter stubs the dispatch so the
// workspace compiles and round-trips; no loop/recur fixture
// is typechecked in iter 1. Mirrors mut.1's synth stub.
Term::Loop { .. } | Term::Recur { .. } => Err(CheckError::Internal(
"Term::Loop/Term::Recur typecheck lands in loop-recur iter 2".into(),
)),
}
}
+23
View File
@@ -397,6 +397,25 @@ 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: 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
@@ -761,6 +780,10 @@ 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 {
+21
View File
@@ -609,6 +609,17 @@ 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);
}
}
}
}
@@ -795,6 +806,8 @@ 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(
@@ -934,6 +947,14 @@ fn any_sub_binder_consumed_for(
Term::Assign { value, .. } => {
any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors)
}
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)
}),
}
}
+22
View File
@@ -1243,6 +1243,17 @@ 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 { .. } => {}
}
}
@@ -1617,6 +1628,17 @@ 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,6 +133,18 @@ 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,6 +265,17 @@ 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,6 +352,17 @@ 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);
}
}
}
}