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:
@@ -1544,6 +1544,24 @@ fn walk_term(
|
||||
}
|
||||
walk_term(value, out, builtins, scope);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2785,6 +2803,42 @@ fn rewrite_def(
|
||||
changed,
|
||||
);
|
||||
}
|
||||
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,6 +101,15 @@ fn synthesised_print_uses_user_module_show_via_fallback() {
|
||||
|| contains_xmod_show_var(body)
|
||||
}
|
||||
Term::Assign { value, .. } => contains_xmod_show_var(value),
|
||||
// loop-recur iter 1: a `Term::Loop` cannot itself host a
|
||||
// synthesised cross-module reference, but recurse
|
||||
// defensively through binder inits / body / recur args,
|
||||
// mirroring the `Term::Mut` arm above.
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,8 @@ 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 (NAME TYPE INIT)* BODY-TERM+) ; strict iteration block (loop-recur)
|
||||
(recur ARG*) ; re-enter enclosing loop (loop-recur)
|
||||
```
|
||||
|
||||
Notes:
|
||||
@@ -324,6 +326,17 @@ 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` opens a strict iteration block. `(NAME TYPE INIT)` binder
|
||||
triples (zero or more) are followed by a body of zero or more
|
||||
Unit-typed statements and exactly one final expression (right-
|
||||
folded into `Term::Seq` like `mut`). `recur` re-enters the
|
||||
lexically innermost enclosing `loop`, rebinding its binders
|
||||
positionally; `(recur ARG*)`'s arg count must equal the binder
|
||||
count and `recur` must be in tail position of the loop body —
|
||||
both enforced at typecheck (`recur-arity-mismatch`,
|
||||
`recur-not-in-tail-position`). `loop`/`recur` make no termination
|
||||
claim: a `loop` with no non-`recur` exit runs forever. See
|
||||
`docs/specs/2026-05-17-loop-recur.md`.
|
||||
|
||||
## Patterns
|
||||
|
||||
|
||||
@@ -540,6 +540,33 @@ pub enum Term {
|
||||
name: String,
|
||||
value: Box<Term>,
|
||||
},
|
||||
/// loop-recur iter 1: a strict iteration block. `binders`
|
||||
/// declares one or more loop parameters (name, type, init),
|
||||
/// evaluated in order on loop entry; `body` is evaluated with
|
||||
/// all binders in scope. The loop's value is `body`'s value on
|
||||
/// the iteration that exits via a non-`recur` branch. Strictly
|
||||
/// additive: pre-existing fixtures hash bit-identically because
|
||||
/// none carry the `"t":"loop"` tag. `binders` has no
|
||||
/// `skip_serializing_if` (mirrors `Term::Mut.vars` — the field
|
||||
/// is part of the shape). Typecheck binder/recur semantics land
|
||||
/// in loop-recur iter 2 (`synth` stubs with `CheckError::Internal`
|
||||
/// in this iter); codegen (loop-header + per-binder phi +
|
||||
/// back-edge) in iter 3 (`lower_term` stubs with
|
||||
/// `CodegenError::Internal`). No totality claim — an infinite
|
||||
/// loop is legal. See `docs/specs/2026-05-17-loop-recur.md`.
|
||||
Loop {
|
||||
binders: Vec<LoopBinder>,
|
||||
body: Box<Term>,
|
||||
},
|
||||
/// loop-recur iter 1: re-enter the lexically innermost enclosing
|
||||
/// `Term::Loop`, rebinding its binders positionally to `args`.
|
||||
/// Transfers control (no fall-through); valid only in tail
|
||||
/// position of its enclosing loop — enforced at typecheck in
|
||||
/// iter 2 (`RecurNotInTailPosition`). Additive `"t":"recur"`
|
||||
/// tag; pre-existing fixtures hash bit-identically.
|
||||
Recur {
|
||||
args: Vec<Term>,
|
||||
},
|
||||
}
|
||||
|
||||
/// One arm of a [`Term::Match`].
|
||||
@@ -583,6 +610,27 @@ pub struct MutVar {
|
||||
pub init: Term,
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: one binder of a [`Term::Loop`]. Mirrors
|
||||
/// [`MutVar`]'s `(name, type, init)` triple exactly so the Form-A
|
||||
/// surface vocabulary is shared (`(NAME TYPE INIT)`); it is a
|
||||
/// nested field of `Term::Loop`, not a first-class `Term` (loop
|
||||
/// binders cannot escape the enclosing loop). `recur` rebinds these
|
||||
/// positionally per iteration. The `ty` JSON field is `"type"`,
|
||||
/// matching `MutVar` and the spec schema.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoopBinder {
|
||||
/// The binder's lexical name. Within the enclosing `Term::Loop`,
|
||||
/// a `Term::Var { name }` resolves to this binding.
|
||||
pub name: String,
|
||||
/// The binder's declared type.
|
||||
#[serde(rename = "type")]
|
||||
pub ty: Type,
|
||||
/// Initial value, evaluated once on loop entry in scope of the
|
||||
/// outer environment plus already-declared binders of the same
|
||||
/// `Term::Loop` (declaration order).
|
||||
pub init: Term,
|
||||
}
|
||||
|
||||
/// A match pattern.
|
||||
///
|
||||
/// The JSON discriminator is the `p` field. Patterns are linear: each
|
||||
@@ -949,4 +997,59 @@ mod tests {
|
||||
other => panic!("variant mismatch: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: pin the canonical-bytes shape of a
|
||||
/// `Term::Loop` with one binder. Mirrors the mut-empty-vars pin:
|
||||
/// `binders` stays present (no `skip_serializing_if`).
|
||||
#[test]
|
||||
fn term_loop_one_binder_serialises_with_explicit_binders_field() {
|
||||
let t = Term::Loop {
|
||||
binders: vec![LoopBinder {
|
||||
name: "i".into(),
|
||||
ty: Type::int(),
|
||||
init: Term::Lit {
|
||||
lit: Literal::Int { value: 0 },
|
||||
},
|
||||
}],
|
||||
body: Box::new(Term::Var { name: "i".into() }),
|
||||
};
|
||||
let bytes = serde_json::to_string(&t).expect("serialise");
|
||||
assert_eq!(
|
||||
bytes,
|
||||
r#"{"t":"loop","binders":[{"name":"i","type":{"k":"con","name":"Int"},"init":{"t":"lit","lit":{"kind":"int","value":0}}}],"body":{"t":"var","name":"i"}}"#,
|
||||
);
|
||||
let back: Term = serde_json::from_str(&bytes).expect("deserialise");
|
||||
match back {
|
||||
Term::Loop { binders, body } => {
|
||||
assert_eq!(binders.len(), 1);
|
||||
assert_eq!(binders[0].name, "i");
|
||||
match *body {
|
||||
Term::Var { name } => assert_eq!(name, "i"),
|
||||
other => panic!("body mismatch: {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("variant mismatch: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: round-trip a `Term::Recur` through JSON.
|
||||
/// Pins `{ "t": "recur", "args": [...] }`.
|
||||
#[test]
|
||||
fn term_recur_round_trips_through_json() {
|
||||
let t = Term::Recur {
|
||||
args: vec![Term::Lit {
|
||||
lit: Literal::Int { value: 1 },
|
||||
}],
|
||||
};
|
||||
let bytes = serde_json::to_string(&t).expect("serialise");
|
||||
assert_eq!(
|
||||
bytes,
|
||||
r#"{"t":"recur","args":[{"t":"lit","lit":{"kind":"int","value":1}}]}"#,
|
||||
);
|
||||
let back: Term = serde_json::from_str(&bytes).expect("deserialise");
|
||||
match back {
|
||||
Term::Recur { args } => assert_eq!(args.len(), 1),
|
||||
other => panic!("variant mismatch: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,37 @@ impl Desugarer {
|
||||
name: name.clone(),
|
||||
value: Box::new(self.desugar_term(value, scope)),
|
||||
},
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: structural recursion mirroring
|
||||
// the Term::Mut arm. Each binder's init is desugared
|
||||
// in scope of the outer env plus already-declared
|
||||
// binders; the body sees all binders. The LetBound
|
||||
// sentinel keeps generated fresh names off binder
|
||||
// names.
|
||||
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,
|
||||
}
|
||||
})
|
||||
.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 +1277,22 @@ 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 } => {
|
||||
// loop-recur iter 1: binder names bind inside the loop —
|
||||
// each init sees the outer env plus already-declared
|
||||
// binders; the body sees all. Mirrors the Term::Mut arm.
|
||||
let mut b = bound.clone();
|
||||
for bd in binders {
|
||||
free_vars_in_term(&bd.init, &b, out);
|
||||
b.insert(bd.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 +1486,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 } => {
|
||||
// loop-recur iter 1: binders lexically shadow outer
|
||||
// names, symmetric to the Term::Mut arm — once a binder
|
||||
// named `from` is declared, later inits and the body
|
||||
// stop being substituted.
|
||||
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,
|
||||
}
|
||||
})
|
||||
.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 +1664,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: 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 +1753,13 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
||||
find_non_callee_use(value, name)
|
||||
}
|
||||
}
|
||||
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 +1807,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 +1841,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 +2966,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,19 @@ where
|
||||
walk_term_embedded_types(body, f)
|
||||
}
|
||||
Term::Assign { value, .. } => walk_term_embedded_types(value, f),
|
||||
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 +1405,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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,17 @@ 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![] },
|
||||
),
|
||||
];
|
||||
|
||||
for (anchor, term) in exemplars {
|
||||
@@ -172,6 +183,8 @@ 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),
|
||||
|
||||
@@ -87,6 +87,28 @@ fn iter13a_schema_extension_preserves_pre_13a_hashes() {
|
||||
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
|
||||
}
|
||||
|
||||
/// loop-recur iter 1 regression: adding `Term::Loop` / `Term::Recur`
|
||||
/// (and `struct LoopBinder`) must NOT change canonical-JSON hashes
|
||||
/// of any pre-loop-recur definition. The additive variant carries a
|
||||
/// new `"t"` tag absent from every pre-existing fixture, so the
|
||||
/// recorded hashes below must stay byte-identical. If this fires,
|
||||
/// the extension is non-additive (a `skip_serializing_if` is missing
|
||||
/// or an existing variant's shape drifted).
|
||||
#[test]
|
||||
fn loop_recur_schema_extension_preserves_pre_loop_recur_hashes() {
|
||||
let examples = examples_dir();
|
||||
|
||||
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
|
||||
.expect("examples/sum.ail loads");
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
|
||||
|
||||
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
|
||||
.expect("examples/list.ail loads");
|
||||
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
|
||||
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
|
||||
}
|
||||
|
||||
/// Iter 19b regression: adding `suppress` to FnDef must NOT change
|
||||
/// canonical-JSON hashes of any pre-19b fn whose `suppress` is empty.
|
||||
/// The `skip_serializing_if = "Vec::is_empty"` predicate on the field
|
||||
|
||||
@@ -49,6 +49,8 @@ enum VariantTag {
|
||||
TermReuseAs,
|
||||
TermMut,
|
||||
TermAssign,
|
||||
TermLoop,
|
||||
TermRecur,
|
||||
// Pattern
|
||||
PatternWild,
|
||||
PatternVar,
|
||||
@@ -96,6 +98,8 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
|
||||
VariantTag::TermReuseAs,
|
||||
VariantTag::TermMut,
|
||||
VariantTag::TermAssign,
|
||||
VariantTag::TermLoop,
|
||||
VariantTag::TermRecur,
|
||||
VariantTag::PatternWild,
|
||||
VariantTag::PatternVar,
|
||||
VariantTag::PatternLit,
|
||||
@@ -244,6 +248,20 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,17 @@ 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![] },
|
||||
),
|
||||
];
|
||||
|
||||
for (anchor, term) in exemplars {
|
||||
@@ -151,6 +162,8 @@ 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),
|
||||
|
||||
@@ -935,6 +935,34 @@ 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 } => {
|
||||
// loop-recur iter 1: minimal-correctness Form-B render.
|
||||
// Prose surface for loop is not yet designed; render the
|
||||
// shape so a reader sees it without overcommitting.
|
||||
out.push_str("loop {\n");
|
||||
for b in binders {
|
||||
indent(out, level + 1);
|
||||
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 +1161,28 @@ 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)
|
||||
}
|
||||
// loop-recur iter 1: a binder named `name` shadows the outer
|
||||
// binding for later binder inits and the body. Inits before
|
||||
// the shadowing binder still see the outer `name`.
|
||||
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 +1335,45 @@ 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)),
|
||||
},
|
||||
// loop-recur iter 1: lexical-shadow semantics symmetric to
|
||||
// count_free_var — once a binder named `name` is declared,
|
||||
// later inits and the body are not rewritten.
|
||||
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,
|
||||
}
|
||||
})
|
||||
.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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1212,6 +1212,8 @@ 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 {
|
||||
@@ -1220,7 +1222,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`, `lit-unit`"
|
||||
`assign`, `loop`, `recur`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
})
|
||||
@@ -1642,6 +1644,97 @@ impl<'a> Parser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: `(loop (NAME TYPE INIT)* BODY_TERM+)` —
|
||||
/// strict iteration block. Reads zero or more `(NAME TYPE INIT)`
|
||||
/// binder triples (bare, no leading keyword — unlike `(var ...)`,
|
||||
/// the loop binder triple has no `var` keyword), then ≥ 1
|
||||
/// trailing terms right-folded into `Term::Seq` (mirrors
|
||||
/// `parse_mut`). The binder/body boundary is disambiguated by the
|
||||
/// inner head: a binder's first inner token is an ident NAME
|
||||
/// (never a term-head keyword), whereas a parenthesised body form
|
||||
/// (`(if …)`, `(recur …)`, `(app …)`, …) opens with a term-head
|
||||
/// keyword — so a leading `(` is treated as a binder only while
|
||||
/// its inner head is not a term-head keyword.
|
||||
fn parse_loop(&mut self) -> Result<Term, ParseError> {
|
||||
fn is_term_head_kw(h: &str) -> bool {
|
||||
matches!(
|
||||
h,
|
||||
"lit-unit"
|
||||
| "app"
|
||||
| "tail-app"
|
||||
| "term-ctor"
|
||||
| "match"
|
||||
| "do"
|
||||
| "tail-do"
|
||||
| "seq"
|
||||
| "lam"
|
||||
| "if"
|
||||
| "let"
|
||||
| "let-rec"
|
||||
| "clone"
|
||||
| "reuse-as"
|
||||
| "mut"
|
||||
| "assign"
|
||||
| "loop"
|
||||
| "recur"
|
||||
)
|
||||
}
|
||||
|
||||
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
self.expect_lparen("loop-term")?;
|
||||
self.expect_keyword("loop")?;
|
||||
|
||||
let mut binders: Vec<ailang_core::ast::LoopBinder> = Vec::new();
|
||||
while matches!(self.peek_head_ident(), Some(h) if !is_term_head_kw(h)) {
|
||||
self.expect_lparen("loop-binder")?;
|
||||
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 });
|
||||
}
|
||||
|
||||
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 binders".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),
|
||||
})
|
||||
}
|
||||
|
||||
/// loop-recur iter 1: `(recur ARG*)` — re-enter the enclosing
|
||||
/// loop. Positional args. The tail-position rule is enforced at
|
||||
/// typecheck (iter 2, `recur-not-in-tail-position`); the parser
|
||||
/// accepts the shape unconditionally.
|
||||
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> {
|
||||
@@ -2583,4 +2676,28 @@ mod tests {
|
||||
"diagnostic should mention missing body, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_loop_and_recur_round_trip_via_term() {
|
||||
let src = "(loop (acc (con Int) 0) (i (con Int) 1) (if (app > i n) acc (recur (app + acc i) (app + i 1))))";
|
||||
let t = parse_term(src).expect("parse loop");
|
||||
match t {
|
||||
ailang_core::ast::Term::Loop { binders, body } => {
|
||||
assert_eq!(binders.len(), 2);
|
||||
assert_eq!(binders[0].name, "acc");
|
||||
assert_eq!(binders[1].name, "i");
|
||||
// body is an `if` whose else-branch is a `recur` of 2 args
|
||||
match *body {
|
||||
ailang_core::ast::Term::If { else_, .. } => match *else_ {
|
||||
ailang_core::ast::Term::Recur { args } => {
|
||||
assert_eq!(args.len(), 2)
|
||||
}
|
||||
other => panic!("else not recur: {other:?}"),
|
||||
},
|
||||
other => panic!("body not if: {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("not a loop: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +594,46 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
write_term(out, value, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 1: print as
|
||||
// `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
||||
// — inverse of parse_loop's binder read + Seq right-fold,
|
||||
// mirroring the Term::Mut printer.
|
||||
out.push_str("(loop");
|
||||
for b in binders {
|
||||
out.push_str(" (");
|
||||
out.push_str(&b.name);
|
||||
out.push(' ');
|
||||
write_type(out, &b.ty);
|
||||
out.push(' ');
|
||||
write_term(out, &b.init, level);
|
||||
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 } => {
|
||||
out.push_str("(recur");
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
write_term(out, a, level);
|
||||
}
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user