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:
@@ -537,6 +537,19 @@ 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`].
|
||||
@@ -580,6 +593,17 @@ 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
|
||||
@@ -946,4 +970,41 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,18 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,6 +602,32 @@ 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 —
|
||||
@@ -1234,6 +1272,23 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,6 +1482,43 @@ 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1568,6 +1660,23 @@ 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1640,6 +1749,14 @@ 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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1687,6 +1804,10 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1717,6 +1838,10 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2838,6 +2963,10 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1259,6 +1259,22 @@ 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1392,6 +1408,18 @@ 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user