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
+45
View File
@@ -201,6 +201,17 @@ 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 { .. } => {}
}
}
@@ -395,6 +406,22 @@ 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 } => {
for a in args {
if escapes(a, tainted, false) {
return true;
}
}
false
}
}
}
@@ -524,6 +551,24 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
}
collect_free_vars(value, bound, out);
}
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);
}
}
}
}
+18
View File
@@ -505,6 +505,24 @@ impl<'a> Emitter<'a> {
}
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
}
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);
}
}
}
}
+15
View File
@@ -1837,6 +1837,14 @@ impl<'a> Emitter<'a> {
));
Ok(("0".into(), "i8".into()))
}
// loop-recur iter 1: real codegen (loop-header block,
// per-binder phi, back-edge br) lands in iter 3. This
// iter stubs the dispatch so the workspace compiles; no
// loop/recur program is codegen'd in iter 1. Mirrors
// mut.1's lower_term stub.
Term::Loop { .. } | Term::Recur { .. } => Err(CodegenError::Internal(
"Term::Loop/Term::Recur lowering lands in loop-recur iter 3".into(),
)),
}
}
@@ -3093,6 +3101,13 @@ 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()),
// loop-recur iter 1: a Term::Loop's static type is the
// body's type (mirror Term::Mut). Term::Recur does not
// fall through; a Unit stub is safe — this arm is never
// hit on the shipping path because lower_term stubs
// Loop/Recur, and iter 1 codegens no loop/recur program.
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
Term::Recur { .. } => Ok(Type::unit()),
}
}
}