iter it.1: loop/recur additive — Term::Loop/Term::Recur/LoopBinder end-to-end
Iteration-discipline milestone, 1 of 3. Adds named loop + recur as strictly-additive first-class AST nodes: parse/print/prose/serde/ round-trip/schema lockstep, typecheck (binder typing + recur arity/type unification via loop_stack threaded as mut.2's mut_scope_stack; recur-tail-position via verify_loop_body), codegen (loop-header + one phi per binder; recur back-edge br with a NEW parallel block_terminated setter; lambda-boundary loop_frames save/restore). Four Recur* CheckError variants. Strictly additive: zero deletions touch tail-app/tail-do or the seven existing block_terminated sites — this is what makes the destructive it.3 safe. recur synth = fresh metavar (resolves the plan's flagged Type::unit() risk). loop_counter->55, loop_in_lambda->49, four negatives fire, tail-app byte-identical, cargo test --workspace green. Specfda9b78, plan7381a42.
This commit is contained in:
@@ -935,6 +935,37 @@ 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(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,6 +1164,26 @@ 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1285,6 +1336,39 @@ 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2101,6 +2185,43 @@ 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]
|
||||
|
||||
Reference in New Issue
Block a user