iter mut.1: AST extension + Form A surface for local mutable state
First iteration of the mut-local milestone (foundation step on the
Stateful-islands roadmap path). Lands the schema + surface tier:
Term::Mut, Term::Assign, and the nested MutVar struct become
first-class AST nodes that round-trip cleanly through Form A.
Typecheck and codegen recognition are deferred to mut.2 and mut.3
per the spec's out-of-iteration boundary; reaching either dispatch
entry point with these variants produces CheckError::Internal /
CodegenError::Internal with a 'deferred to iter mut.{2,3}' message.
Concretely:
- crates/ailang-core/src/ast.rs: two new Term variants behind
#[serde(tag = 't')]; pub struct MutVar { name, ty, init } adjacent
to Arm. Two canonical-bytes pin tests for the explicit-empty-vars
serialisation and the assign round-trip.
- ~25 substantive Term-walker arms across ailang-core/desugar,
ailang-core/workspace, ailang-check (lib + lift + linearity + mono
+ pre_desugar_validation + reuse_shape + uniqueness),
ailang-codegen (escape + lambda + lib), ailang-prose, and
crates/ail/src/main.rs. Universal policy: substantive recurse-into-
children at every site; only the two dispatch entry points
(synth in ailang-check, lower_term in ailang-codegen) stub with
Internal-error. One test-side walker arm in
crates/ail/tests/codegen_import_map_fallback_pin.rs not
enumerated by the plan was added as well (defensive recursion).
- ailang-surface: parse_mut + parse_assign helpers; Term::Mut
body desugared from a flat statement sequence into a right-folded
Term::Seq chain inside the JSON-AST. Print arms in print.rs match
the parser convention. EBNF prologue + crates/ailang-core/specs/
form_a.md productions updated. Four new parser pin tests cover
the empty-mut, single-var, body-required, and vars-only-no-body
cases.
- Drift + coverage tests extended: design_schema_drift.rs adds two
exemplars + match arms; schema_coverage.rs adds two VariantTag
entries + EXPECTED_VARIANTS + visit_term arms; spec_drift.rs adds
two exemplars + match arms. DESIGN.md §'Term (expression)' gets
jsonc-blocked schemas for the two new variants.
- examples/mut.ail: six-fn round-trip fixture exercising empty mut,
single-var, two-var, nested-shadow, and the four supported scalar
return types (Int, Float, Bool, Unit). The round_trip auto-glob
and schema_coverage corpus walker both pick it up.
Plan deviation: the plan named lib.rs:2572 as the typecheck
dispatch stub site, but that line is actually verify_tail_positions
(substantive walker). The real dispatch is synth (3403-area, stub
at 3489); the orchestrator routed correctly.
Tests: 564 → 579 green; cargo build green; round-trip green for
the new fixture; all drift + coverage tests green.
Journal: docs/journals/2026-05-15-iter-mut.1.md.
Refs: docs/specs/2026-05-15-mut-local.md, docs/plans/2026-05-15-iter-mut.1.md.
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"iter_id": "mut.1",
|
||||||
|
"date": "2026-05-15",
|
||||||
|
"mode": "standard",
|
||||||
|
"outcome": "DONE",
|
||||||
|
"tasks_total": 6,
|
||||||
|
"tasks_completed": 6,
|
||||||
|
"reloops_per_task": {
|
||||||
|
"1": 0,
|
||||||
|
"2": 0,
|
||||||
|
"3": 0,
|
||||||
|
"4": 0,
|
||||||
|
"5": 0,
|
||||||
|
"6": 0
|
||||||
|
},
|
||||||
|
"review_loops_spec": 0,
|
||||||
|
"review_loops_quality": 0,
|
||||||
|
"blocked_reason": null,
|
||||||
|
"notes_per_task": {
|
||||||
|
"1": "MutVar derives diverged from plan literal (`PartialEq + Eq` omitted; followed Arm's pattern per plan's own stated justification); recorded as concern; no functional impact",
|
||||||
|
"2": "plan-recon misindexed the typecheck dispatch entry — listed lib.rs:2572 (actually `verify_tail_positions`, substantive territory) instead of the real entry `synth` at lib.rs:3403; stub placed at the actual entry; line 2572 received a substantive arm; one test-side walker site (`crates/ail/tests/codegen_import_map_fallback_pin.rs:62`) unenumerated by plan, fixed inline (build-green required); print arms in `print.rs` landed during Task 2 build-unblock rather than waiting for Task 4 (benign sequencing shift)",
|
||||||
|
"3": "dispatch stubs use `CheckError::Internal(String)` and `CodegenError::Internal(String)` (single-field tuple variant); verified against existing variant declarations",
|
||||||
|
"4": "plan pseudo-code used helper names that don't exist (`peek_is_open_paren_with_head`, `expect_head`, `ParseError::at`); substituted actual API (`expect_lparen`, `expect_keyword`, `expect_ident`, `parse_type`, `parse_term`, `expect_rparen`, `peek_head_ident`); 4 RED-first parser tests added, all green post-parser",
|
||||||
|
"5": "all three drift tests green after extensions; `schema_coverage` deliberately RED before Task 6 per plan Step 6",
|
||||||
|
"6": "fixture exercises all four supported scalar types (Int, Float, Bool, Unit) plus empty / single-var / two-var / nested-shadow cases; full workspace 579/579 green"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1513,6 +1513,37 @@ fn walk_term(
|
|||||||
walk_term(source, out, builtins, scope);
|
walk_term(source, out, builtins, scope);
|
||||||
walk_term(body, out, builtins, scope);
|
walk_term(body, out, builtins, scope);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: each `var` introduces a new lexical binding;
|
||||||
|
// its `init` is in the outer scope plus already-declared
|
||||||
|
// vars; the body sees all of them. Mirrors `Term::Lam`'s
|
||||||
|
// newly-bound roll-back convention.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
let mut newly = Vec::new();
|
||||||
|
for v in vars {
|
||||||
|
walk_term(&v.init, out, builtins, scope);
|
||||||
|
if scope.insert(v.name.clone()) {
|
||||||
|
newly.push(v.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk_term(body, out, builtins, scope);
|
||||||
|
for n in newly {
|
||||||
|
scope.remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iter mut.1: the `value` is walked; the assigned `name` is
|
||||||
|
// a use of an in-scope mut-var. If the user wrote an
|
||||||
|
// out-of-scope assign, it surfaces as an unknown identifier
|
||||||
|
// in the dep walk, which is acceptable for the CLI's
|
||||||
|
// dependency view (typecheck would already have rejected).
|
||||||
|
Term::Assign { name, value } => {
|
||||||
|
if !name.contains('.')
|
||||||
|
&& !scope.contains(name)
|
||||||
|
&& !builtins.contains(name.as_str())
|
||||||
|
{
|
||||||
|
out.insert(name.clone());
|
||||||
|
}
|
||||||
|
walk_term(value, out, builtins, scope);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2716,6 +2747,44 @@ fn rewrite_def(
|
|||||||
changed,
|
changed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: rewrite types embedded in each `MutVar.ty`
|
||||||
|
// (via `rewrite_type` — mut-vars carry full Type
|
||||||
|
// annotations) and recurse into each var's `init` and
|
||||||
|
// the block's body. `Term::Assign` has no embedded type.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
rewrite_type(
|
||||||
|
&mut v.ty,
|
||||||
|
owning_module,
|
||||||
|
local_types,
|
||||||
|
import_names,
|
||||||
|
changed,
|
||||||
|
);
|
||||||
|
rewrite_term(
|
||||||
|
&mut v.init,
|
||||||
|
owning_module,
|
||||||
|
local_types,
|
||||||
|
import_names,
|
||||||
|
changed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rewrite_term(
|
||||||
|
body,
|
||||||
|
owning_module,
|
||||||
|
local_types,
|
||||||
|
import_names,
|
||||||
|
changed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => {
|
||||||
|
rewrite_term(
|
||||||
|
value,
|
||||||
|
owning_module,
|
||||||
|
local_types,
|
||||||
|
import_names,
|
||||||
|
changed,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,16 @@ fn synthesised_print_uses_user_module_show_via_fallback() {
|
|||||||
Term::ReuseAs { source, body } => {
|
Term::ReuseAs { source, body } => {
|
||||||
contains_xmod_show_var(source) || contains_xmod_show_var(body)
|
contains_xmod_show_var(source) || contains_xmod_show_var(body)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: a `Term::Mut` cannot itself host a
|
||||||
|
// `show_user_adt.show__<T>` reference (its body is a
|
||||||
|
// mut-block, not a synthesised polymorphic call), but
|
||||||
|
// recurse defensively so any nested reference inside a
|
||||||
|
// var init / body / assign-value still surfaces.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
vars.iter().any(|v| contains_xmod_show_var(&v.init))
|
||||||
|
|| contains_xmod_show_var(body)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => contains_xmod_show_var(value),
|
||||||
Term::Lit { .. } => false,
|
Term::Lit { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,24 @@ pub(crate) fn substitute_rigids_in_term(t: &Term, mapping: &BTreeMap<String, Typ
|
|||||||
source: Box::new(substitute_rigids_in_term(source, mapping)),
|
source: Box::new(substitute_rigids_in_term(source, mapping)),
|
||||||
body: Box::new(substitute_rigids_in_term(body, mapping)),
|
body: Box::new(substitute_rigids_in_term(body, mapping)),
|
||||||
},
|
},
|
||||||
|
// Iter mut.1: rebuild each `MutVar.ty` via the type-level
|
||||||
|
// rigids substitution, each `init` via the term-level
|
||||||
|
// walker, and the body via the term-level walker.
|
||||||
|
Term::Mut { vars, body } => Term::Mut {
|
||||||
|
vars: vars
|
||||||
|
.iter()
|
||||||
|
.map(|v| ailang_core::ast::MutVar {
|
||||||
|
name: v.name.clone(),
|
||||||
|
ty: substitute_rigids(&v.ty, mapping),
|
||||||
|
init: substitute_rigids_in_term(&v.init, mapping),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
body: Box::new(substitute_rigids_in_term(body, mapping)),
|
||||||
|
},
|
||||||
|
Term::Assign { name, value } => Term::Assign {
|
||||||
|
name: name.clone(),
|
||||||
|
value: Box::new(substitute_rigids_in_term(value, mapping)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2577,6 +2595,22 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
|
|||||||
verify_tail_positions(source, false)?;
|
verify_tail_positions(source, false)?;
|
||||||
verify_tail_positions(body, is_tail)
|
verify_tail_positions(body, is_tail)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: var inits are evaluated for side-effect before
|
||||||
|
// the body and are NOT in tail position. The body is the
|
||||||
|
// value of the whole block and inherits the enclosing tail
|
||||||
|
// position (a `(tail-app ...)` as the final expression of a
|
||||||
|
// `(mut ...)` block is itself in tail position because the
|
||||||
|
// block's value is the call's value).
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
verify_tail_positions(&v.init, false)?;
|
||||||
|
}
|
||||||
|
verify_tail_positions(body, is_tail)
|
||||||
|
}
|
||||||
|
// Iter mut.1: `Term::Assign` has static type Unit and cannot
|
||||||
|
// be in tail position. Its `value` is also not in tail
|
||||||
|
// position.
|
||||||
|
Term::Assign { value, .. } => verify_tail_positions(value, false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3428,6 +3462,8 @@ pub(crate) fn synth(
|
|||||||
Term::Seq { .. } => "seq",
|
Term::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
|
Term::Mut { .. } => "mut",
|
||||||
|
Term::Assign { .. } => "assign",
|
||||||
Term::Ctor { .. } | Term::Lam { .. } => unreachable!(),
|
Term::Ctor { .. } | Term::Lam { .. } => unreachable!(),
|
||||||
};
|
};
|
||||||
return Err(CheckError::ReuseAsNonAllocatingBody {
|
return Err(CheckError::ReuseAsNonAllocatingBody {
|
||||||
@@ -3440,6 +3476,25 @@ pub(crate) fn synth(
|
|||||||
// expression.
|
// expression.
|
||||||
synth(body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)
|
synth(body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: typecheck recognition of `Term::Mut` and
|
||||||
|
// `Term::Assign` is deferred to iter mut.2 (see
|
||||||
|
// `docs/specs/2026-05-15-mut-local.md` §"Iteration mut.1"
|
||||||
|
// out-of-iteration boundary). The dispatch entry here stubs
|
||||||
|
// with `CheckError::Internal` so that a well-formed mut block
|
||||||
|
// reaching this point in mut.1 fails fast with a
|
||||||
|
// human-readable message — the Form-A surface parses these
|
||||||
|
// shapes and the AST round-trips them, but no fn body is
|
||||||
|
// expected to *use* them yet.
|
||||||
|
Term::Mut { .. } => {
|
||||||
|
Err(CheckError::Internal(
|
||||||
|
"Term::Mut not yet supported in typecheck (deferred to iter mut.2)".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Term::Assign { .. } => {
|
||||||
|
Err(CheckError::Internal(
|
||||||
|
"Term::Assign not yet supported in typecheck (deferred to iter mut.2)".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -375,6 +375,28 @@ impl<'a> Lifter<'a> {
|
|||||||
source: Box::new(self.lift_in_term(source, locals, in_def)?),
|
source: Box::new(self.lift_in_term(source, locals, in_def)?),
|
||||||
body: Box::new(self.lift_in_term(body, locals, in_def)?),
|
body: Box::new(self.lift_in_term(body, locals, in_def)?),
|
||||||
}),
|
}),
|
||||||
|
// Iter mut.1: lift letrecs out of each var's init and the
|
||||||
|
// body. Mut-vars cannot themselves participate in a
|
||||||
|
// letrec capture set (letrec bodies are fn-typed and
|
||||||
|
// mut-vars are scalar values), so the var bindings do
|
||||||
|
// not extend `locals` for the lift walk.
|
||||||
|
Term::Mut { vars, body } => Ok(Term::Mut {
|
||||||
|
vars: vars
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
Ok(ailang_core::ast::MutVar {
|
||||||
|
name: v.name.clone(),
|
||||||
|
ty: v.ty.clone(),
|
||||||
|
init: self.lift_in_term(&v.init, locals, in_def)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?,
|
||||||
|
body: Box::new(self.lift_in_term(body, locals, in_def)?),
|
||||||
|
}),
|
||||||
|
Term::Assign { name, value } => Ok(Term::Assign {
|
||||||
|
name: name.clone(),
|
||||||
|
value: Box::new(self.lift_in_term(value, locals, in_def)?),
|
||||||
|
}),
|
||||||
Term::LetRec { name, ty, params, body, in_term } => {
|
Term::LetRec { name, ty, params, body, in_term } => {
|
||||||
// Iter 16b.3: post-order traversal — lift any inner
|
// Iter 16b.3: post-order traversal — lift any inner
|
||||||
// LetRecs first. Within the body's scope, `name` and
|
// LetRecs first. Within the body's scope, `name` and
|
||||||
@@ -729,6 +751,12 @@ fn contains_any_letrec(m: &Module) -> bool {
|
|||||||
Term::Clone { value } => term_has_letrec(value),
|
Term::Clone { value } => term_has_letrec(value),
|
||||||
// Iter 18d.1: structural recursion through both children.
|
// Iter 18d.1: structural recursion through both children.
|
||||||
Term::ReuseAs { source, body } => term_has_letrec(source) || term_has_letrec(body),
|
Term::ReuseAs { source, body } => term_has_letrec(source) || term_has_letrec(body),
|
||||||
|
// Iter mut.1: any var init or the body can contain a
|
||||||
|
// letrec; the assign's value can too.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
vars.iter().any(|v| term_has_letrec(&v.init)) || term_has_letrec(body)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => term_has_letrec(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for def in &m.defs {
|
for def in &m.defs {
|
||||||
|
|||||||
@@ -592,6 +592,23 @@ impl<'a> Checker<'a> {
|
|||||||
// standard position rules.
|
// standard position rules.
|
||||||
self.walk(body, pos);
|
self.walk(body, pos);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: mut-vars are alloca-resident scalars (Int /
|
||||||
|
// Float / Bool / Unit per spec §"Iteration mut.1") — they
|
||||||
|
// are not RC-managed and do not participate in the
|
||||||
|
// linearity analysis. Walk children defensively so any
|
||||||
|
// RC-managed value referenced inside an init or the body
|
||||||
|
// is still tracked.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
self.walk(&v.init, Position::Consume);
|
||||||
|
}
|
||||||
|
self.walk(body, pos);
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => {
|
||||||
|
// The assigned `name` is a mut-var (alloca slot, not
|
||||||
|
// a tracked binder). Walk the `value` normally.
|
||||||
|
self.walk(value, Position::Consume);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,6 +793,8 @@ fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> D
|
|||||||
Term::Seq { .. } => "seq",
|
Term::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
|
Term::Mut { .. } => "mut",
|
||||||
|
Term::Assign { .. } => "assign",
|
||||||
};
|
};
|
||||||
let replacement = term_to_form_a(body);
|
let replacement = term_to_form_a(body);
|
||||||
Diagnostic::error(
|
Diagnostic::error(
|
||||||
@@ -901,6 +920,20 @@ fn any_sub_binder_consumed_for(
|
|||||||
any_sub_binder_consumed_for(source, pname, uniq, def_name, ctors)
|
any_sub_binder_consumed_for(source, pname, uniq, def_name, ctors)
|
||||||
|| any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
|| any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: mut-vars are scalar (Int/Float/Bool/Unit) and
|
||||||
|
// do not participate in the consume-tracking heuristic; the
|
||||||
|
// surrounding analysis only cares about heap-typed param
|
||||||
|
// consumption. Recurse into children defensively so any
|
||||||
|
// genuine consume-of-`pname` inside a var init / body /
|
||||||
|
// assign value still surfaces.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
vars.iter().any(|v| {
|
||||||
|
any_sub_binder_consumed_for(&v.init, pname, uniq, def_name, ctors)
|
||||||
|
}) || any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => {
|
||||||
|
any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1221,6 +1221,19 @@ fn rewrite_mono_calls(
|
|||||||
rewrite_mono_calls(source, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
rewrite_mono_calls(source, 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);
|
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: visitor-shape recurse with mutable access.
|
||||||
|
// Mut-vars do not declare class-method or poly-fn names; the
|
||||||
|
// var inits, the body, and assign values can though, so walk
|
||||||
|
// them in place.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars.iter_mut() {
|
||||||
|
rewrite_mono_calls(&mut v.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::Assign { value, .. } => {
|
||||||
|
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
||||||
|
}
|
||||||
Term::Lit { .. } => {}
|
Term::Lit { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1577,6 +1590,19 @@ fn interleave_slots(
|
|||||||
interleave_slots(source, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
|
interleave_slots(source, 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);
|
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);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: visitor-shape recurse, symmetric to
|
||||||
|
// `rewrite_mono_calls` above. Mut-vars themselves do not
|
||||||
|
// contribute slots; their init and body terms (and assign
|
||||||
|
// values) can.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
interleave_slots(&v.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::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::Lit { .. } => {}
|
Term::Lit { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,19 @@ fn walk_term(t: &Term) -> Result<(), CheckError> {
|
|||||||
walk_term(source)?;
|
walk_term(source)?;
|
||||||
walk_term(body)
|
walk_term(body)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: pre-desugar validation has nothing specific to
|
||||||
|
// assert about mut blocks (the only invariant this pass
|
||||||
|
// enforces today is "no Float in literal patterns" — mut
|
||||||
|
// var inits are Terms, but they can hold any literal, and
|
||||||
|
// patterns inside a body are visited via the normal
|
||||||
|
// `Term::Match` arm). Recurse into children.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
walk_term(&v.init)?;
|
||||||
|
}
|
||||||
|
walk_term(body)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => walk_term(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -253,6 +253,18 @@ impl<'a> Checker<'a> {
|
|||||||
self.walk(body);
|
self.walk(body);
|
||||||
self.check_reuse_as(source, body);
|
self.check_reuse_as(source, body);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: mut-vars are scalar (Int/Float/Bool/Unit)
|
||||||
|
// and cannot host an allocating ctor body, so the
|
||||||
|
// reuse-as path-ctor analysis has nothing to refine here.
|
||||||
|
// Recurse defensively into children so any reuse-as
|
||||||
|
// nested inside a var init or the body is still checked.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
self.walk(&v.init);
|
||||||
|
}
|
||||||
|
self.walk(body);
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => self.walk(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,22 @@ impl<'a> Walker<'a> {
|
|||||||
self.walk(source, Position::Consume);
|
self.walk(source, Position::Consume);
|
||||||
self.walk(body, pos);
|
self.walk(body, pos);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: mut-vars are alloca-resident scalars and
|
||||||
|
// are NOT RC-tracked binders. They do not contribute to
|
||||||
|
// uniqueness counts (the alloca slot is the per-fn
|
||||||
|
// storage, not a heap allocation). Walk children
|
||||||
|
// defensively so any RC-managed value passed through a
|
||||||
|
// var init / body / assign-value still updates the
|
||||||
|
// appropriate uniqueness counts.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
self.walk(&v.init, Position::Consume);
|
||||||
|
}
|
||||||
|
self.walk(body, pos);
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => {
|
||||||
|
self.walk(value, Position::Consume);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,17 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
|||||||
walk(source, out);
|
walk(source, out);
|
||||||
walk(body, out);
|
walk(body, out);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: mut-vars are alloca-resident scalars (Int /
|
||||||
|
// Float / Bool / Unit) and never participate in heap escape.
|
||||||
|
// Their `init` and `body` and assign `value` terms may still
|
||||||
|
// contain Let-allocation candidates, so walk through.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
walk(&v.init, out);
|
||||||
|
}
|
||||||
|
walk(body, out);
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => walk(value, out),
|
||||||
Term::Lit { .. } | Term::Var { .. } => {}
|
Term::Lit { .. } | Term::Var { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,6 +378,23 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
|||||||
}
|
}
|
||||||
escapes(body, tainted, in_tail)
|
escapes(body, tainted, in_tail)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: mut-vars are alloca-resident scalars and do
|
||||||
|
// not themselves contribute to heap escape. Var inits are
|
||||||
|
// evaluated for side-effect (their values are stored into
|
||||||
|
// allocas, not used as the block's value), so they are
|
||||||
|
// walked in non-tail mode. The body IS the block's value,
|
||||||
|
// so it inherits `in_tail`. An assign's value is stored
|
||||||
|
// into an alloca, not used as a tail value — walk
|
||||||
|
// non-tail.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
if escapes(&v.init, tainted, false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
escapes(body, tainted, in_tail)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => escapes(value, tainted, false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +499,31 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
|||||||
collect_free_vars(source, bound, out);
|
collect_free_vars(source, bound, out);
|
||||||
collect_free_vars(body, bound, out);
|
collect_free_vars(body, bound, out);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: a mut-var name binds inside the block. Var
|
||||||
|
// inits before the binding see the outer scope; later inits
|
||||||
|
// and the body see the var as bound. Mirrors `Term::Let`'s
|
||||||
|
// add-then-walk-then-remove pattern, extended over N vars.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
let mut newly: Vec<String> = Vec::new();
|
||||||
|
for v in vars {
|
||||||
|
collect_free_vars(&v.init, bound, out);
|
||||||
|
if bound.insert(v.name.clone()) {
|
||||||
|
newly.push(v.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collect_free_vars(body, bound, out);
|
||||||
|
for n in newly {
|
||||||
|
bound.remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iter mut.1: the assigned `name` is a free var unless
|
||||||
|
// bound by an enclosing `Term::Mut`.
|
||||||
|
Term::Assign { name, value } => {
|
||||||
|
if !bound.contains(name) {
|
||||||
|
out.insert(name.clone());
|
||||||
|
}
|
||||||
|
collect_free_vars(value, bound, out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -430,6 +430,43 @@ impl<'a> Emitter<'a> {
|
|||||||
Self::collect_captures(source, bound, captures, captures_set, builtins, top_level);
|
Self::collect_captures(source, bound, captures, captures_set, builtins, top_level);
|
||||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: a mut-var name binds inside the block. Var
|
||||||
|
// inits before the binding see the outer scope; later
|
||||||
|
// inits and the body see the var as bound. Mut-vars
|
||||||
|
// cannot be captured by an inner lambda in mut.1 (the
|
||||||
|
// typecheck pass at mut.2 will enforce this); for the
|
||||||
|
// capture-collection scan the var bindings are tracked
|
||||||
|
// identically to let-bound names.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
let mut newly_bound: Vec<String> = Vec::new();
|
||||||
|
for v in vars {
|
||||||
|
Self::collect_captures(&v.init, bound, captures, captures_set, builtins, top_level);
|
||||||
|
if bound.insert(v.name.clone()) {
|
||||||
|
newly_bound.push(v.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||||
|
for n in newly_bound {
|
||||||
|
bound.remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Assign { name, value, .. } => {
|
||||||
|
// The assigned `name` is a use of a mut-var binding.
|
||||||
|
// If it is bound in scope (it should be, by spec), it
|
||||||
|
// is not a capture. Otherwise — well-formed inputs do
|
||||||
|
// not produce this case (the typecheck pass at mut.2
|
||||||
|
// rejects it via `MutAssignOutOfScope`); we
|
||||||
|
// conservatively treat it as a possible capture so an
|
||||||
|
// ill-formed input still flows through codegen.
|
||||||
|
if !bound.contains(name)
|
||||||
|
&& !builtins.contains(name.as_str())
|
||||||
|
&& !top_level.contains(name)
|
||||||
|
&& captures_set.insert(name.clone())
|
||||||
|
{
|
||||||
|
captures.push(name.clone());
|
||||||
|
}
|
||||||
|
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1669,6 +1669,26 @@ impl<'a> Emitter<'a> {
|
|||||||
};
|
};
|
||||||
self.lower_reuse_as_rc(source, body_type_name, body_ctor, body_args)
|
self.lower_reuse_as_rc(source, body_type_name, body_ctor, body_args)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: codegen recognition of `Term::Mut` and
|
||||||
|
// `Term::Assign` is deferred to iter mut.3 (see
|
||||||
|
// `docs/specs/2026-05-15-mut-local.md` §"Iteration mut.1"
|
||||||
|
// out-of-iteration boundary). The dispatch entry here
|
||||||
|
// stubs with `CodegenError::Internal` so a well-formed
|
||||||
|
// mut block reaching codegen in mut.1 fails fast with a
|
||||||
|
// human-readable message. The typecheck pass also stubs
|
||||||
|
// at `synth` (iter mut.1, same spec subsection), so the
|
||||||
|
// codegen stub is a defense-in-depth — under normal
|
||||||
|
// flow, typecheck has already rejected.
|
||||||
|
Term::Mut { .. } => {
|
||||||
|
Err(CodegenError::Internal(
|
||||||
|
"Term::Mut not yet supported in codegen (deferred to iter mut.3)".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Term::Assign { .. } => {
|
||||||
|
Err(CodegenError::Internal(
|
||||||
|
"Term::Assign not yet supported in codegen (deferred to iter mut.3)".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2911,6 +2931,14 @@ impl<'a> Emitter<'a> {
|
|||||||
// type. The source is dropped at codegen.
|
// type. The source is dropped at codegen.
|
||||||
self.synth_with_extras(body, extras)
|
self.synth_with_extras(body, extras)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: `Term::Mut`'s static type is the body's
|
||||||
|
// static type (spec §"JSON-AST schema"). `Term::Assign`'s
|
||||||
|
// is Unit (spec §"Form A surface"). These won't be hit
|
||||||
|
// along the shipping codegen path because the dispatcher
|
||||||
|
// at `lower_term` stubs both — but `synth_with_extras`
|
||||||
|
// is exhaustive on Term so the arms must exist.
|
||||||
|
Term::Mut { body, .. } => self.synth_with_extras(body, extras),
|
||||||
|
Term::Assign { .. } => Ok(Type::unit()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ Parenthesised forms:
|
|||||||
(seq EFFECTFUL-TERM RESULT-TERM) ; sequence; lhs evaluated for effect
|
(seq EFFECTFUL-TERM RESULT-TERM) ; sequence; lhs evaluated for effect
|
||||||
(clone TERM) ; explicit RC clone (Iter 18c.1)
|
(clone TERM) ; explicit RC clone (Iter 18c.1)
|
||||||
(reuse-as SOURCE-TERM BODY-TERM) ; explicit reuse hint (Iter 18d.1)
|
(reuse-as SOURCE-TERM BODY-TERM) ; explicit reuse hint (Iter 18d.1)
|
||||||
|
(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 ...)
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
@@ -298,6 +301,27 @@ Notes:
|
|||||||
- `reuse-as` requires `SOURCE-TERM` to be a bare variable reference
|
- `reuse-as` requires `SOURCE-TERM` to be a bare variable reference
|
||||||
(a `NAME` in the term grammar). Anything else fails the linearity
|
(a `NAME` in the term grammar). Anything else fails the linearity
|
||||||
check with `reuse-as-source-not-bare-var`.
|
check with `reuse-as-source-not-bare-var`.
|
||||||
|
- `mut` opens a lexically-scoped block of mutable bindings. The
|
||||||
|
body is a flat sequence of zero or more Unit-typed statements
|
||||||
|
followed by exactly one final expression of any supported type;
|
||||||
|
the parser right-folds this trailing sequence into `Term::Seq`
|
||||||
|
inside `Term::Mut.body`, so the canonical JSON-AST always sees a
|
||||||
|
single `body: Term`. The `vars` array stays present in canonical
|
||||||
|
JSON even when empty (a `(mut EXPR)` form with no vars is legal
|
||||||
|
and round-trips uniformly). A `(mut)` form with neither vars nor
|
||||||
|
body is rejected at parse. Mut-var element types are restricted
|
||||||
|
to the stack-resident primitives Int / Float / Bool / Unit in
|
||||||
|
this milestone; the typecheck pass (iter mut.2) rejects other
|
||||||
|
types with `mut-var-unsupported-type`.
|
||||||
|
- `(var NAME TYPE INIT)` is only legal as a leading entry inside a
|
||||||
|
`(mut ...)` block. Outside the parser rejects `var` as an
|
||||||
|
unknown term head.
|
||||||
|
- `(assign NAME VALUE-TERM)` is only legal as a sub-term inside a
|
||||||
|
`(mut ...)` block whose `vars` includes a var with the matching
|
||||||
|
`NAME`. Outside the parser accepts the shape but the typecheck
|
||||||
|
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`.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -510,6 +510,33 @@ pub enum Term {
|
|||||||
source: Box<Term>,
|
source: Box<Term>,
|
||||||
body: Box<Term>,
|
body: Box<Term>,
|
||||||
},
|
},
|
||||||
|
/// Iter mut.1: local mutable-state block. `vars` declares zero or
|
||||||
|
/// more lexically-scoped mutable bindings (initialised in order);
|
||||||
|
/// `body` is a single Term evaluated in scope of all vars. The
|
||||||
|
/// block's static type is `body`'s static type. `vars` stays
|
||||||
|
/// present in canonical JSON even when empty (no
|
||||||
|
/// `skip_serializing_if` — the field is part of the shape).
|
||||||
|
///
|
||||||
|
/// Typecheck and codegen recognition land in iters mut.2 / mut.3;
|
||||||
|
/// in iter mut.1 the dispatch entry points (`synth` in
|
||||||
|
/// `ailang-check`, `lower_term` in `ailang-codegen`) stub these
|
||||||
|
/// variants with `CheckError::Internal` / `CodegenError::Internal`
|
||||||
|
/// respectively per spec `docs/specs/2026-05-15-mut-local.md`
|
||||||
|
/// §"Iteration mut.1".
|
||||||
|
Mut {
|
||||||
|
vars: Vec<MutVar>,
|
||||||
|
body: Box<Term>,
|
||||||
|
},
|
||||||
|
/// Iter mut.1: in-block update of a mut-var. Only legal as a
|
||||||
|
/// sub-term of a `Term::Mut` whose `vars` includes a var with the
|
||||||
|
/// same `name`. Static type is Unit. The lexical-scope invariant
|
||||||
|
/// is enforced at typecheck (iter mut.2,
|
||||||
|
/// `CheckError::MutAssignOutOfScope`); codegen relies on it (iter
|
||||||
|
/// mut.3).
|
||||||
|
Assign {
|
||||||
|
name: String,
|
||||||
|
value: Box<Term>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One arm of a [`Term::Match`].
|
/// One arm of a [`Term::Match`].
|
||||||
@@ -524,6 +551,35 @@ pub struct Arm {
|
|||||||
pub body: Term,
|
pub body: Term,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: a mutable binding inside [`Term::Mut`]. Lives as a
|
||||||
|
/// nested field of `Term::Mut`, not as a standalone `Term` variant —
|
||||||
|
/// mut-vars are not first-class values (they cannot escape the
|
||||||
|
/// enclosing `Term::Mut` scope; see
|
||||||
|
/// `docs/specs/2026-05-15-mut-local.md` §"Why three AST nodes, not
|
||||||
|
/// one fused construct").
|
||||||
|
///
|
||||||
|
/// The JSON field for `ty` is `"type"`, mirroring the spec's schema
|
||||||
|
/// (`{ "name": "<id>", "type": Type, "init": Term }`). Derives match
|
||||||
|
/// [`Arm`]'s for cross-tree consistency.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MutVar {
|
||||||
|
/// The mut-var's lexical name. Within the enclosing `Term::Mut`,
|
||||||
|
/// a `Term::Var { name }` resolves to this binding (innermost-wins
|
||||||
|
/// shadowing per spec §"Term::Var resolution"); a
|
||||||
|
/// `Term::Assign { name, .. }` updates it.
|
||||||
|
pub name: String,
|
||||||
|
/// The mut-var's declared type. Restricted to
|
||||||
|
/// `{ Int, Float, Bool, Unit }` in this milestone — the typecheck
|
||||||
|
/// pass (iter mut.2) rejects other choices with
|
||||||
|
/// `CheckError::UnsupportedMutVarType`.
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub ty: Type,
|
||||||
|
/// Initial value. Evaluated in the outer scope plus the
|
||||||
|
/// already-declared vars of the same `Term::Mut` (vars are
|
||||||
|
/// initialised in declaration order).
|
||||||
|
pub init: Term,
|
||||||
|
}
|
||||||
|
|
||||||
/// A match pattern.
|
/// A match pattern.
|
||||||
///
|
///
|
||||||
/// The JSON discriminator is the `p` field. Patterns are linear: each
|
/// The JSON discriminator is the `p` field. Patterns are linear: each
|
||||||
@@ -834,3 +890,60 @@ impl Eq for Type {}
|
|||||||
fn is_false(b: &bool) -> bool {
|
fn is_false(b: &bool) -> bool {
|
||||||
!*b
|
!*b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Iter mut.1: pin the canonical-bytes shape of an empty-`vars`
|
||||||
|
/// `Term::Mut`. Spec `docs/specs/2026-05-15-mut-local.md`
|
||||||
|
/// §"Canonical-form invariants" requires the `vars` array to stay
|
||||||
|
/// present in canonical JSON rather than being omitted; a future
|
||||||
|
/// addition of `skip_serializing_if = "Vec::is_empty"` to the
|
||||||
|
/// field would silently break this property and is the failure
|
||||||
|
/// mode this pin protects against.
|
||||||
|
#[test]
|
||||||
|
fn term_mut_empty_vars_serialises_with_explicit_vars_field() {
|
||||||
|
let t = Term::Mut {
|
||||||
|
vars: Vec::new(),
|
||||||
|
body: Box::new(Term::Lit {
|
||||||
|
lit: Literal::Int { value: 0 },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let bytes = serde_json::to_string(&t).expect("serialise");
|
||||||
|
assert_eq!(
|
||||||
|
bytes,
|
||||||
|
r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: round-trip a `Term::Assign` through JSON.
|
||||||
|
/// Pins the canonical-form shape `{ "t": "assign", "name": ..., "value": ... }`.
|
||||||
|
#[test]
|
||||||
|
fn term_assign_round_trips_through_json() {
|
||||||
|
let t = Term::Assign {
|
||||||
|
name: "x".into(),
|
||||||
|
value: Box::new(Term::Lit {
|
||||||
|
lit: Literal::Int { value: 42 },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let bytes = serde_json::to_string(&t).expect("serialise");
|
||||||
|
assert_eq!(
|
||||||
|
bytes,
|
||||||
|
r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":42}}}"#,
|
||||||
|
);
|
||||||
|
let back: Term = serde_json::from_str(&bytes).expect("deserialise");
|
||||||
|
match back {
|
||||||
|
Term::Assign { name, value } => {
|
||||||
|
assert_eq!(name, "x");
|
||||||
|
match *value {
|
||||||
|
Term::Lit {
|
||||||
|
lit: Literal::Int { value: 42 },
|
||||||
|
} => {}
|
||||||
|
other => panic!("inner literal mismatch: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("variant mismatch: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -346,6 +346,23 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
|||||||
collect_used_in_term(source, used);
|
collect_used_in_term(source, used);
|
||||||
collect_used_in_term(body, used);
|
collect_used_in_term(body, used);
|
||||||
}
|
}
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
// Iter mut.1: mut-var declarations introduce source-level
|
||||||
|
// names — track them so a generated fresh-name cannot
|
||||||
|
// accidentally collide with one. Then recurse into each
|
||||||
|
// var's `init` and the block's body.
|
||||||
|
for v in vars {
|
||||||
|
used.insert(v.name.clone());
|
||||||
|
collect_used_in_term(&v.init, used);
|
||||||
|
}
|
||||||
|
collect_used_in_term(body, used);
|
||||||
|
}
|
||||||
|
Term::Assign { name, value } => {
|
||||||
|
// Iter mut.1: the assigned `name` is a *use* of the
|
||||||
|
// mut-var; record it and recurse into `value`.
|
||||||
|
used.insert(name.clone());
|
||||||
|
collect_used_in_term(value, used);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +556,40 @@ impl Desugarer {
|
|||||||
source: Box::new(self.desugar_term(source, scope)),
|
source: Box::new(self.desugar_term(source, scope)),
|
||||||
body: Box::new(self.desugar_term(body, scope)),
|
body: Box::new(self.desugar_term(body, scope)),
|
||||||
},
|
},
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
// Iter mut.1: structural recursion. Each `var`'s `init`
|
||||||
|
// is evaluated in scope of the outer environment plus
|
||||||
|
// the *already-declared* vars of the same `Term::Mut`
|
||||||
|
// (spec §"Iteration mut.2" — first-class scoping
|
||||||
|
// formally lands at typecheck). For desugar's purposes
|
||||||
|
// we extend the scope per-var with a `LetBound`
|
||||||
|
// sentinel so a generated fresh name does not collide
|
||||||
|
// with mut-var names; the LetRec lifter does not
|
||||||
|
// consult mut-vars (mut-vars cannot appear in let-rec
|
||||||
|
// capture sets — letrec lifting happens before
|
||||||
|
// typecheck of the mut body).
|
||||||
|
let mut inner = scope.clone();
|
||||||
|
let new_vars: Vec<MutVar> = vars
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
let init = self.desugar_term(&v.init, &inner);
|
||||||
|
inner.insert(v.name.clone(), ScopeEntry::LetBound);
|
||||||
|
MutVar {
|
||||||
|
name: v.name.clone(),
|
||||||
|
ty: v.ty.clone(),
|
||||||
|
init,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Term::Mut {
|
||||||
|
vars: new_vars,
|
||||||
|
body: Box::new(self.desugar_term(body, &inner)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Assign { name, value } => Term::Assign {
|
||||||
|
name: name.clone(),
|
||||||
|
value: Box::new(self.desugar_term(value, scope)),
|
||||||
|
},
|
||||||
Term::LetRec { name, ty, params, body, in_term } => {
|
Term::LetRec { name, ty, params, body, in_term } => {
|
||||||
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
||||||
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
||||||
@@ -1160,6 +1211,29 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
|
|||||||
free_vars_in_term(source, bound, out);
|
free_vars_in_term(source, bound, out);
|
||||||
free_vars_in_term(body, bound, out);
|
free_vars_in_term(body, bound, out);
|
||||||
}
|
}
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
// Iter mut.1: a mut-var name binds inside the block. Each
|
||||||
|
// `var`'s `init` is evaluated in scope of the OUTER
|
||||||
|
// environment plus the already-declared vars (init order
|
||||||
|
// matches declaration order); the body is in scope of all
|
||||||
|
// vars. Mirrors the `Term::Let` pattern but generalised
|
||||||
|
// across N vars.
|
||||||
|
let mut b = bound.clone();
|
||||||
|
for v in vars {
|
||||||
|
free_vars_in_term(&v.init, &b, out);
|
||||||
|
b.insert(v.name.clone());
|
||||||
|
}
|
||||||
|
free_vars_in_term(body, &b, out);
|
||||||
|
}
|
||||||
|
Term::Assign { name, value } => {
|
||||||
|
// Iter mut.1: the assigned `name` is a free var unless
|
||||||
|
// bound by an enclosing `Term::Mut`. The value is walked
|
||||||
|
// as usual.
|
||||||
|
if !bound.contains(name) {
|
||||||
|
out.insert(name.clone());
|
||||||
|
}
|
||||||
|
free_vars_in_term(value, bound, out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1304,6 +1378,55 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
|||||||
source: Box::new(subst_var(source, from, to)),
|
source: Box::new(subst_var(source, from, to)),
|
||||||
body: Box::new(subst_var(body, from, to)),
|
body: Box::new(subst_var(body, from, to)),
|
||||||
},
|
},
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
// Iter mut.1: mut-vars lexically shadow outer names. Walk
|
||||||
|
// each var's `init` substituting only if no earlier var in
|
||||||
|
// the same block already shadows `from`; once a var named
|
||||||
|
// `from` is declared, subsequent inits AND the body see
|
||||||
|
// that var, so substitution must stop.
|
||||||
|
let mut shadowed = false;
|
||||||
|
let new_vars: Vec<MutVar> = vars
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
let init = if shadowed {
|
||||||
|
v.init.clone()
|
||||||
|
} else {
|
||||||
|
subst_var(&v.init, from, to)
|
||||||
|
};
|
||||||
|
if v.name == from {
|
||||||
|
shadowed = true;
|
||||||
|
}
|
||||||
|
MutVar {
|
||||||
|
name: v.name.clone(),
|
||||||
|
ty: v.ty.clone(),
|
||||||
|
init,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let body_rw = if shadowed {
|
||||||
|
(**body).clone()
|
||||||
|
} else {
|
||||||
|
subst_var(body, from, to)
|
||||||
|
};
|
||||||
|
Term::Mut {
|
||||||
|
vars: new_vars,
|
||||||
|
body: Box::new(body_rw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Assign { name, value } => Term::Assign {
|
||||||
|
// Iter mut.1: substitute inside `value`; the `name` field
|
||||||
|
// refers to a mut-var declared by an enclosing
|
||||||
|
// `Term::Mut`. Renaming it here without renaming the
|
||||||
|
// declaration would break the scope link. The desugar
|
||||||
|
// pass's `subst_var` is used only to rewrite let-rec
|
||||||
|
// captures (KnownType / LetBound / MatchArm — never
|
||||||
|
// mut-vars), so leaving the `name` untouched is
|
||||||
|
// semantically correct: any `from` that matches the
|
||||||
|
// assigned `name` is in a different namespace by
|
||||||
|
// construction.
|
||||||
|
name: name.clone(),
|
||||||
|
value: Box::new(subst_var(value, from, to)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1425,6 +1548,26 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
|
|||||||
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
|
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
|
||||||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||||||
},
|
},
|
||||||
|
Term::Mut { vars, body } => Term::Mut {
|
||||||
|
// Iter mut.1: structural recursion through each var's
|
||||||
|
// `init` and the block's body. Mut-var declarations
|
||||||
|
// cannot shadow a top-level fn name (mut-vars are
|
||||||
|
// first-order scalars, fns are not assignable), so the
|
||||||
|
// call-substitution sees through the block uniformly.
|
||||||
|
vars: vars
|
||||||
|
.iter()
|
||||||
|
.map(|v| MutVar {
|
||||||
|
name: v.name.clone(),
|
||||||
|
ty: v.ty.clone(),
|
||||||
|
init: subst_call_with_extras(&v.init, name, lifted, extras),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||||||
|
},
|
||||||
|
Term::Assign { name: assign_name, value } => Term::Assign {
|
||||||
|
name: assign_name.clone(),
|
||||||
|
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1479,6 +1622,24 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
|||||||
Term::ReuseAs { source, body } => {
|
Term::ReuseAs { source, body } => {
|
||||||
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
|
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: scan each var's init plus the body. Mut-vars
|
||||||
|
// are first-order scalars and never appear in callee position,
|
||||||
|
// so any matching `Term::Var` inside a mut block is non-callee
|
||||||
|
// by construction.
|
||||||
|
Term::Mut { vars, body } => vars
|
||||||
|
.iter()
|
||||||
|
.find_map(|v| find_non_callee_use(&v.init, name))
|
||||||
|
.or_else(|| find_non_callee_use(body, name)),
|
||||||
|
// Iter mut.1: the assigned `name` is a non-callee *use* of
|
||||||
|
// the mut-var; if it matches the searched name, surface the
|
||||||
|
// node itself. Otherwise recurse into the value.
|
||||||
|
Term::Assign { name: assign_name, value } => {
|
||||||
|
if assign_name == name {
|
||||||
|
Some(t.clone())
|
||||||
|
} else {
|
||||||
|
find_non_callee_use(value, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1521,6 +1682,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
Term::Clone { value } => any_nested_ctor(value),
|
Term::Clone { value } => any_nested_ctor(value),
|
||||||
Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body),
|
Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body),
|
||||||
|
// Iter mut.1: scan each var's init plus the body.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
vars.iter().any(|v| any_nested_ctor(&v.init)) || any_nested_ctor(body)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => any_nested_ctor(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1546,6 +1712,11 @@ mod tests {
|
|||||||
Term::LetRec { .. } => true,
|
Term::LetRec { .. } => true,
|
||||||
Term::Clone { value } => any_let_rec(value),
|
Term::Clone { value } => any_let_rec(value),
|
||||||
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
|
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
|
||||||
|
// Iter mut.1: scan each var's init plus the body.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
vars.iter().any(|v| any_let_rec(&v.init)) || any_let_rec(body)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => any_let_rec(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2662,6 +2833,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
Term::Clone { value } => any_lit_pattern(value),
|
Term::Clone { value } => any_lit_pattern(value),
|
||||||
Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body),
|
Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body),
|
||||||
|
// Iter mut.1: scan each var's init plus the body.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
vars.iter().any(|v| any_lit_pattern(&v.init)) || any_lit_pattern(body)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => any_lit_pattern(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1247,6 +1247,18 @@ where
|
|||||||
walk_term_embedded_types(source, f)?;
|
walk_term_embedded_types(source, f)?;
|
||||||
walk_term_embedded_types(body, f)
|
walk_term_embedded_types(body, f)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: each `MutVar.ty` is an embedded type — walk
|
||||||
|
// it. Then recurse into each var's `init` and the block's
|
||||||
|
// body. `Term::Assign` carries no embedded type; recurse
|
||||||
|
// into its `value` only.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
walk_type(&v.ty, f)?;
|
||||||
|
walk_term_embedded_types(&v.init, f)?;
|
||||||
|
}
|
||||||
|
walk_term_embedded_types(body, f)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => walk_term_embedded_types(value, f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1369,6 +1381,17 @@ where
|
|||||||
walk_term(source, f)?;
|
walk_term(source, f)?;
|
||||||
walk_term(body, f)
|
walk_term(body, f)
|
||||||
}
|
}
|
||||||
|
// Iter mut.1: `Term::Ctor.type_name` is the only string this
|
||||||
|
// walker reports — mut-vars carry no Ctor-type strings. Just
|
||||||
|
// recurse into each var's `init` and the body, and into
|
||||||
|
// assign's `value`.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
for v in vars {
|
||||||
|
walk_term(&v.init, f)?;
|
||||||
|
}
|
||||||
|
walk_term(body, f)
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => walk_term(value, f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,20 @@ fn design_md_anchors_every_term_variant() {
|
|||||||
body: Box::new(Term::Var { name: "y".into() }),
|
body: Box::new(Term::Var { name: "y".into() }),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
r#""t": "mut""#,
|
||||||
|
Term::Mut {
|
||||||
|
vars: Vec::new(),
|
||||||
|
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#""t": "assign""#,
|
||||||
|
Term::Assign {
|
||||||
|
name: "x".into(),
|
||||||
|
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (anchor, term) in exemplars {
|
for (anchor, term) in exemplars {
|
||||||
@@ -156,6 +170,8 @@ fn design_md_anchors_every_term_variant() {
|
|||||||
Term::Seq { .. } => "seq",
|
Term::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
|
Term::Mut { .. } => "mut",
|
||||||
|
Term::Assign { .. } => "assign",
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
data_model_section().contains(anchor),
|
data_model_section().contains(anchor),
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ enum VariantTag {
|
|||||||
TermSeq,
|
TermSeq,
|
||||||
TermClone,
|
TermClone,
|
||||||
TermReuseAs,
|
TermReuseAs,
|
||||||
|
TermMut,
|
||||||
|
TermAssign,
|
||||||
// Pattern
|
// Pattern
|
||||||
PatternWild,
|
PatternWild,
|
||||||
PatternVar,
|
PatternVar,
|
||||||
@@ -92,6 +94,8 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
|
|||||||
VariantTag::TermSeq,
|
VariantTag::TermSeq,
|
||||||
VariantTag::TermClone,
|
VariantTag::TermClone,
|
||||||
VariantTag::TermReuseAs,
|
VariantTag::TermReuseAs,
|
||||||
|
VariantTag::TermMut,
|
||||||
|
VariantTag::TermAssign,
|
||||||
VariantTag::PatternWild,
|
VariantTag::PatternWild,
|
||||||
VariantTag::PatternVar,
|
VariantTag::PatternVar,
|
||||||
VariantTag::PatternLit,
|
VariantTag::PatternLit,
|
||||||
@@ -228,6 +232,18 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
|
|||||||
visit_term(source, observed);
|
visit_term(source, observed);
|
||||||
visit_term(body, observed);
|
visit_term(body, observed);
|
||||||
}
|
}
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
observed.insert(VariantTag::TermMut);
|
||||||
|
for v in vars {
|
||||||
|
visit_type(&v.ty, observed);
|
||||||
|
visit_term(&v.init, observed);
|
||||||
|
}
|
||||||
|
visit_term(body, observed);
|
||||||
|
}
|
||||||
|
Term::Assign { value, .. } => {
|
||||||
|
observed.insert(VariantTag::TermAssign);
|
||||||
|
visit_term(value, observed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,20 @@ fn spec_mentions_every_term_variant() {
|
|||||||
body: Box::new(Term::Var { name: "y".into() }),
|
body: Box::new(Term::Var { name: "y".into() }),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"(mut",
|
||||||
|
Term::Mut {
|
||||||
|
vars: Vec::new(),
|
||||||
|
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"(assign",
|
||||||
|
Term::Assign {
|
||||||
|
name: "x".into(),
|
||||||
|
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (anchor, term) in exemplars {
|
for (anchor, term) in exemplars {
|
||||||
@@ -135,6 +149,8 @@ fn spec_mentions_every_term_variant() {
|
|||||||
Term::Seq { .. } => "seq",
|
Term::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
|
Term::Mut { .. } => "mut",
|
||||||
|
Term::Assign { .. } => "assign",
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
FORM_A_SPEC.contains(anchor),
|
FORM_A_SPEC.contains(anchor),
|
||||||
|
|||||||
@@ -906,6 +906,35 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow
|
|||||||
out.push_str(&body_buf);
|
out.push_str(&body_buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
// Iter mut.1: prose-side minimal-correctness rendering for
|
||||||
|
// the `(mut ...)` block. The prose-form for mut blocks is
|
||||||
|
// not yet fully designed (the prose surface predates this
|
||||||
|
// milestone); render the block as `mut { var <name>: <ty>
|
||||||
|
// = <init>; ...; <body> }` so a reader can see the shape
|
||||||
|
// without overcommitting to a final prose syntax. Refined
|
||||||
|
// in a follow-on prose iter once the LLM-author signal
|
||||||
|
// arrives.
|
||||||
|
out.push_str("mut {\n");
|
||||||
|
for v in vars {
|
||||||
|
indent(out, level + 1);
|
||||||
|
out.push_str("var ");
|
||||||
|
out.push_str(&v.name);
|
||||||
|
out.push_str(" = ");
|
||||||
|
write_term(out, &v.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::Assign { name, value } => {
|
||||||
|
out.push_str(name);
|
||||||
|
out.push_str(" := ");
|
||||||
|
write_term(out, value, level, owning_module);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,6 +1106,33 @@ fn count_free_var(name: &str, t: &Term) -> usize {
|
|||||||
}
|
}
|
||||||
Term::Clone { value } => count_free_var(name, value),
|
Term::Clone { value } => count_free_var(name, value),
|
||||||
Term::ReuseAs { source, body } => count_free_var(name, source) + count_free_var(name, body),
|
Term::ReuseAs { source, body } => count_free_var(name, source) + count_free_var(name, body),
|
||||||
|
// Iter mut.1: a `Term::Mut` whose `vars` includes a name
|
||||||
|
// matching `name` shadows the outer binding for both later
|
||||||
|
// var inits and the body. Var inits before the shadowing one
|
||||||
|
// still see the outer `name`.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
let mut total = 0usize;
|
||||||
|
let mut shadowed = false;
|
||||||
|
for v in vars {
|
||||||
|
if !shadowed {
|
||||||
|
total += count_free_var(name, &v.init);
|
||||||
|
}
|
||||||
|
if v.name == name {
|
||||||
|
shadowed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !shadowed {
|
||||||
|
total += count_free_var(name, body);
|
||||||
|
}
|
||||||
|
total
|
||||||
|
}
|
||||||
|
// Iter mut.1: the assigned `name` field is a use of the
|
||||||
|
// mut-var binding (which may or may not be `name`); the
|
||||||
|
// value is recursed.
|
||||||
|
Term::Assign { name: assign_name, value } => {
|
||||||
|
let n_use = if assign_name == name { 1 } else { 0 };
|
||||||
|
n_use + count_free_var(name, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,6 +1244,47 @@ fn subst_var_with_term(t: &Term, name: &str, replacement: &Term) -> Term {
|
|||||||
source: Box::new(subst_var_with_term(source, name, replacement)),
|
source: Box::new(subst_var_with_term(source, name, replacement)),
|
||||||
body: Box::new(subst_var_with_term(body, name, replacement)),
|
body: Box::new(subst_var_with_term(body, name, replacement)),
|
||||||
},
|
},
|
||||||
|
// Iter mut.1: lexical-shadow semantics for mut-vars symmetric
|
||||||
|
// to `count_free_var` above: once a var named `name` is
|
||||||
|
// declared, neither later inits nor the body should be
|
||||||
|
// rewritten.
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
let mut shadowed = false;
|
||||||
|
let new_vars: Vec<ailang_core::ast::MutVar> = vars
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
let init = if shadowed {
|
||||||
|
v.init.clone()
|
||||||
|
} else {
|
||||||
|
subst_var_with_term(&v.init, name, replacement)
|
||||||
|
};
|
||||||
|
if v.name == name {
|
||||||
|
shadowed = true;
|
||||||
|
}
|
||||||
|
ailang_core::ast::MutVar {
|
||||||
|
name: v.name.clone(),
|
||||||
|
ty: v.ty.clone(),
|
||||||
|
init,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let body_rw = if shadowed {
|
||||||
|
(**body).clone()
|
||||||
|
} else {
|
||||||
|
subst_var_with_term(body, name, replacement)
|
||||||
|
};
|
||||||
|
Term::Mut {
|
||||||
|
vars: new_vars,
|
||||||
|
body: Box::new(body_rw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iter mut.1: substitute only inside `value`. The `name`
|
||||||
|
// field is a binding reference (cf. the desugar.rs
|
||||||
|
// `subst_var` arm).
|
||||||
|
Term::Assign { name: assign_name, value } => Term::Assign {
|
||||||
|
name: assign_name.clone(),
|
||||||
|
value: Box::new(subst_var_with_term(value, name, replacement)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@
|
|||||||
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
|
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
|
||||||
//! | app-term | tail-app-term | match-term | ctor-term
|
//! | app-term | tail-app-term | match-term | ctor-term
|
||||||
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
||||||
//! | let-term | let-rec-term | clone-term
|
//! | let-term | let-rec-term | clone-term | reuse-as-term
|
||||||
|
//! | mut-term | assign-term
|
||||||
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
||||||
//! int-lit ::= integer ; numeric atom
|
//! int-lit ::= integer ; numeric atom
|
||||||
//! str-lit ::= string ; string atom
|
//! str-lit ::= string ; string atom
|
||||||
@@ -66,6 +67,9 @@
|
|||||||
//! "(" "in" term ")" ")"
|
//! "(" "in" term ")" ")"
|
||||||
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
||||||
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1
|
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1
|
||||||
|
//! mut-term ::= "(" "mut" var-decl* term+ ")" ; Iter mut.1
|
||||||
|
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term
|
||||||
|
//! assign-term ::= "(" "assign" ident term ")" ; Iter mut.1; legal only inside mut-term
|
||||||
//!
|
//!
|
||||||
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
||||||
//! pat-var ::= ident
|
//! pat-var ::= ident
|
||||||
@@ -1206,6 +1210,8 @@ impl<'a> Parser<'a> {
|
|||||||
"let-rec" => self.parse_let_rec(),
|
"let-rec" => self.parse_let_rec(),
|
||||||
"clone" => self.parse_clone(),
|
"clone" => self.parse_clone(),
|
||||||
"reuse-as" => self.parse_reuse_as(),
|
"reuse-as" => self.parse_reuse_as(),
|
||||||
|
"mut" => self.parse_mut(),
|
||||||
|
"assign" => self.parse_assign(),
|
||||||
other => {
|
other => {
|
||||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||||
Err(ParseError::Production {
|
Err(ParseError::Production {
|
||||||
@@ -1213,7 +1219,8 @@ impl<'a> Parser<'a> {
|
|||||||
message: format!(
|
message: format!(
|
||||||
"unknown term head `{other}`; expected one of \
|
"unknown term head `{other}`; expected one of \
|
||||||
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
||||||
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `lit-unit`"
|
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `mut`, \
|
||||||
|
`assign`, `lit-unit`"
|
||||||
),
|
),
|
||||||
pos,
|
pos,
|
||||||
})
|
})
|
||||||
@@ -1565,6 +1572,76 @@ impl<'a> Parser<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: `(mut (var NAME TYPE INIT)* BODY_TERM+)` — local
|
||||||
|
/// mutable-state block. Reads zero or more `(var ...)` entries
|
||||||
|
/// off the front of the body, then ≥ 1 trailing terms. The
|
||||||
|
/// trailing sequence is right-folded into `Term::Seq` with the
|
||||||
|
/// final term as the seed (so the JSON-AST always sees a single
|
||||||
|
/// `body: Term`). Spec `docs/specs/2026-05-15-mut-local.md`
|
||||||
|
/// §"Form A surface" + §"Canonical-form invariants".
|
||||||
|
fn parse_mut(&mut self) -> Result<Term, ParseError> {
|
||||||
|
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||||
|
self.expect_lparen("mut-term")?;
|
||||||
|
self.expect_keyword("mut")?;
|
||||||
|
|
||||||
|
// Pull off all leading (var NAME TYPE INIT) entries.
|
||||||
|
let mut vars: Vec<ailang_core::ast::MutVar> = Vec::new();
|
||||||
|
while matches!(self.peek_head_ident(), Some("var")) {
|
||||||
|
self.expect_lparen("mut-var")?;
|
||||||
|
self.expect_keyword("var")?;
|
||||||
|
let name = self.expect_ident("mut-var-name")?;
|
||||||
|
let ty = self.parse_type()?;
|
||||||
|
let init = self.parse_term()?;
|
||||||
|
self.expect_rparen("mut-var")?;
|
||||||
|
vars.push(ailang_core::ast::MutVar { name, ty, init });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then read ≥ 1 trailing body terms. Right-fold into Seq
|
||||||
|
// with the final term as the seed.
|
||||||
|
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: "mut-term",
|
||||||
|
message: "(mut ...) requires at least one body expression after vars".into(),
|
||||||
|
pos: head_pos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.expect_rparen("mut-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::Mut {
|
||||||
|
vars,
|
||||||
|
body: Box::new(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: `(assign NAME VALUE)` — in-block update of a
|
||||||
|
/// mut-var. Positional shape: name then value. The lexical-scope
|
||||||
|
/// rule ("legal only inside a `(mut ...)` whose `vars` includes
|
||||||
|
/// a var with the same `name`") is enforced at typecheck
|
||||||
|
/// (mut.2, `CheckError::MutAssignOutOfScope`); the parser
|
||||||
|
/// accepts the shape unconditionally.
|
||||||
|
fn parse_assign(&mut self) -> Result<Term, ParseError> {
|
||||||
|
self.expect_lparen("assign-term")?;
|
||||||
|
self.expect_keyword("assign")?;
|
||||||
|
let name = self.expect_ident("assign-name")?;
|
||||||
|
let value = self.parse_term()?;
|
||||||
|
self.expect_rparen("assign-term")?;
|
||||||
|
Ok(Term::Assign {
|
||||||
|
name,
|
||||||
|
value: Box::new(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---- patterns -------------------------------------------------------
|
// ---- patterns -------------------------------------------------------
|
||||||
|
|
||||||
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||||
@@ -2423,4 +2500,87 @@ mod tests {
|
|||||||
other => panic!("expected Term::Lit::Float, got {other:?}"),
|
other => panic!("expected Term::Lit::Float, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: `(mut 0)` parses as a `Term::Mut` with empty `vars`
|
||||||
|
/// and an `Int 0` body. Spec
|
||||||
|
/// `docs/specs/2026-05-15-mut-local.md` §"Canonical-form
|
||||||
|
/// invariants" — the empty-vars form is a legal degenerate case
|
||||||
|
/// the schema accepts uniformly.
|
||||||
|
#[test]
|
||||||
|
fn parses_empty_mut_with_int_body() {
|
||||||
|
let t = parse_term("(mut 0)").expect("parse");
|
||||||
|
match t {
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
assert!(vars.is_empty(), "vars should be empty, got {vars:?}");
|
||||||
|
match *body {
|
||||||
|
Term::Lit { lit: Literal::Int { value: 0 } } => {}
|
||||||
|
other => panic!("expected Lit Int 0, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("expected Term::Mut, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: `(mut (var x ...) (assign x ...) x)` parses with
|
||||||
|
/// the trailing statement-and-final-expression sequence right-
|
||||||
|
/// folded into a `Term::Seq`. The schema's `Term::Mut.body` is
|
||||||
|
/// always a single Term; multi-statement bodies are folded
|
||||||
|
/// through `Seq` at parse time.
|
||||||
|
#[test]
|
||||||
|
fn parses_mut_with_one_var_and_one_assign_and_final() {
|
||||||
|
let src = "(mut (var x (con Int) 0) (assign x (app + x 1)) x)";
|
||||||
|
let t = parse_term(src).expect("parse");
|
||||||
|
match t {
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
assert_eq!(vars.len(), 1, "expected exactly one var");
|
||||||
|
assert_eq!(vars[0].name, "x");
|
||||||
|
match *body {
|
||||||
|
Term::Seq { lhs, rhs } => {
|
||||||
|
match *lhs {
|
||||||
|
Term::Assign { name, .. } => assert_eq!(name, "x"),
|
||||||
|
other => panic!("expected Assign as Seq.lhs, got {other:?}"),
|
||||||
|
}
|
||||||
|
match *rhs {
|
||||||
|
Term::Var { name } => assert_eq!(name, "x"),
|
||||||
|
other => panic!("expected Var x as Seq.rhs, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("expected Seq as Mut.body, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("expected Term::Mut, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: `(mut)` with neither vars nor body is rejected at
|
||||||
|
/// parse — the spec's §"Canonical-form invariants" mandates at
|
||||||
|
/// least one body expression after the `var` entries.
|
||||||
|
#[test]
|
||||||
|
fn rejects_mut_with_no_body() {
|
||||||
|
let err = parse_term("(mut)").expect_err("must reject empty mut");
|
||||||
|
let msg = format!("{err}");
|
||||||
|
assert!(
|
||||||
|
msg.contains("mut"),
|
||||||
|
"diagnostic should mention `mut`, got: {msg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
msg.contains("body") || msg.contains("expression"),
|
||||||
|
"diagnostic should mention the missing body, got: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter mut.1: `(mut (var x ...))` declares a var but has no
|
||||||
|
/// trailing body expression — also rejected (the rule is "≥ 0
|
||||||
|
/// vars followed by ≥ 1 body terms"; zero body terms is the
|
||||||
|
/// rejection trigger).
|
||||||
|
#[test]
|
||||||
|
fn rejects_mut_with_only_vars_no_final_expression() {
|
||||||
|
let err = parse_term("(mut (var x (con Int) 0))")
|
||||||
|
.expect_err("must reject vars-only mut");
|
||||||
|
let msg = format!("{err}");
|
||||||
|
assert!(
|
||||||
|
msg.contains("body") || msg.contains("expression"),
|
||||||
|
"diagnostic should mention missing body, got: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -548,6 +548,52 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
|||||||
write_term(out, body, level);
|
write_term(out, body, level);
|
||||||
out.push(')');
|
out.push(')');
|
||||||
}
|
}
|
||||||
|
Term::Mut { vars, body } => {
|
||||||
|
// Iter mut.1: print as
|
||||||
|
// `(mut (var NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
||||||
|
// where the body's right-spine of `Term::Seq` is walked
|
||||||
|
// and each lhs is printed as a top-level statement; the
|
||||||
|
// terminal expression (the rightmost non-Seq term) is the
|
||||||
|
// block's value. This is the inverse of the parser's
|
||||||
|
// right-fold over the trailing sequence.
|
||||||
|
out.push_str("(mut");
|
||||||
|
for v in vars {
|
||||||
|
out.push_str(" (var ");
|
||||||
|
out.push_str(&v.name);
|
||||||
|
out.push(' ');
|
||||||
|
write_type(out, &v.ty);
|
||||||
|
out.push(' ');
|
||||||
|
write_term(out, &v.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::Assign { name, value } => {
|
||||||
|
// Iter mut.1: print as `(assign NAME VALUE)`. Legal only
|
||||||
|
// inside a `Term::Mut.body`; the parser enforces the
|
||||||
|
// structural rule, the typechecker (mut.2) enforces the
|
||||||
|
// scope rule.
|
||||||
|
out.push_str("(assign ");
|
||||||
|
out.push_str(name);
|
||||||
|
out.push(' ');
|
||||||
|
write_term(out, value, level);
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2365,12 +2365,42 @@ are real surface forms.
|
|||||||
// (typically `ctor` or `lam`); `source` must be a bare `var`. Codegen
|
// (typically `ctor` or `lam`); `source` must be a bare `var`. Codegen
|
||||||
// lowers as in-place rewrite under `--alloc=rc`.
|
// lowers as in-place rewrite under `--alloc=rc`.
|
||||||
{ "t": "reuse-as", "source": Term, "body": Term }
|
{ "t": "reuse-as", "source": Term, "body": Term }
|
||||||
|
|
||||||
|
// Iter mut.1: local mutable-state block. `vars` declares zero or
|
||||||
|
// more lexically-scoped mutable bindings (initialised in order);
|
||||||
|
// `body` is a single Term in scope of all vars. The block's static
|
||||||
|
// type is `body`'s static type. The `vars` array stays present in
|
||||||
|
// canonical JSON even when empty (no `skip_serializing_if` —
|
||||||
|
// hash-stable-on-omission is NOT applied here; the field is part of
|
||||||
|
// the shape). See `docs/specs/2026-05-15-mut-local.md`.
|
||||||
|
{ "t": "mut",
|
||||||
|
"vars": [ { "name": "<id>", "type": Type, "init": Term }, ... ],
|
||||||
|
"body": Term }
|
||||||
|
|
||||||
|
// Iter mut.1: in-block update of a mut-var. Legal only as a
|
||||||
|
// sub-term of a `Term::Mut` whose `vars` includes a var with the
|
||||||
|
// same `name`. Static type Unit. The lexical-scope rule is enforced
|
||||||
|
// at typecheck (iter mut.2, `mut-assign-out-of-scope`); codegen
|
||||||
|
// relies on it (iter mut.3).
|
||||||
|
{ "t": "assign",
|
||||||
|
"name": "<id>",
|
||||||
|
"value": Term }
|
||||||
```
|
```
|
||||||
|
|
||||||
In the MVP, `do` is only a direct call to a built-in effect op (no
|
In the MVP, `do` is only a direct call to a built-in effect op (no
|
||||||
handler). A `lam` term constructs an anonymous function value; free
|
handler). A `lam` term constructs an anonymous function value; free
|
||||||
variables of its body are captured from the enclosing scope.
|
variables of its body are captured from the enclosing scope.
|
||||||
|
|
||||||
|
Iter mut.1 lands `Term::Mut` and `Term::Assign` as first-class AST
|
||||||
|
nodes that round-trip cleanly between Form A and canonical JSON.
|
||||||
|
Typecheck recognition (iter mut.2) and codegen lowering (iter mut.3)
|
||||||
|
are sequenced as separate iterations; in iter mut.1 the typecheck
|
||||||
|
dispatch (`synth` in `ailang-check`) and codegen dispatch
|
||||||
|
(`lower_term` in `ailang-codegen`) stub these variants with
|
||||||
|
`CheckError::Internal` / `CodegenError::Internal` respectively, per
|
||||||
|
spec `docs/specs/2026-05-15-mut-local.md` §"Iteration mut.1"
|
||||||
|
out-of-iteration boundary.
|
||||||
|
|
||||||
**`Literal`**:
|
**`Literal`**:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# iter mut.1 — schema + surface for local mutable state
|
||||||
|
|
||||||
|
**Date:** 2026-05-15
|
||||||
|
**Started from:** 60e4559e31d7f09de77e2679aab2d524de257a61
|
||||||
|
**Status:** DONE
|
||||||
|
**Tasks completed:** 6 of 6
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Lands the foundational AST + surface forms for the mut-local
|
||||||
|
milestone (first milestone on the Stateful-islands roadmap path).
|
||||||
|
Two new `Term` variants (`Mut` and `Assign`) plus the nested
|
||||||
|
`MutVar` struct enter `ailang-core::ast`; canonical-JSON serde
|
||||||
|
round-trips them by virtue of the existing `#[serde(tag = "t")]`
|
||||||
|
machinery. Form A gains `(mut …)` / `(var …)` / `(assign …)`
|
||||||
|
productions in the parser and the corresponding write arms in the
|
||||||
|
printer; the parser right-folds the trailing body sequence into
|
||||||
|
`Term::Seq` so the canonical JSON-AST always sees a single
|
||||||
|
`body: Term`. Every `Term` exhaustive match in the workspace (~25
|
||||||
|
walker sites) gains substantive arms, except the two dispatch
|
||||||
|
entry points (`synth` in `ailang-check`, `lower_term` in
|
||||||
|
`ailang-codegen`) which stub with `CheckError::Internal` /
|
||||||
|
`CodegenError::Internal` per the spec's "out of iteration"
|
||||||
|
boundary — typecheck recognition lands in mut.2, codegen lowering
|
||||||
|
in mut.3. The three drift tests (`design_schema_drift`,
|
||||||
|
`spec_drift`, `schema_coverage`) all flip green after the
|
||||||
|
exemplar / variant-tag / visitor extensions, and `examples/mut.ail`
|
||||||
|
ships as the round-trip fixture covering empty / single-var /
|
||||||
|
two-var / nested-shadow / Bool / Unit cases. Full
|
||||||
|
`cargo test --workspace` 579/579 green (was 564 + 2 new AST pins +
|
||||||
|
4 new parser pins + 9 from the extended drift exemplars and the
|
||||||
|
mut.ail-corpus contributions netting to +15).
|
||||||
|
|
||||||
|
## Per-task notes
|
||||||
|
|
||||||
|
- iter mut.1.1 — AST extension: added `Term::Mut { vars: Vec<MutVar>,
|
||||||
|
body: Box<Term> }`, `Term::Assign { name: String, value: Box<Term> }`,
|
||||||
|
and the `MutVar { name, ty, init }` struct to `crates/ailang-core/src/ast.rs`.
|
||||||
|
Two new unit tests pin the canonical-bytes of the empty-vars mut
|
||||||
|
serialisation and the Assign round-trip. `MutVar` carries Arm's
|
||||||
|
derives (`Debug, Clone, Serialize, Deserialize`) — the plan's
|
||||||
|
literal pseudo-code asked for `PartialEq + Eq` but its own
|
||||||
|
justification ("derives match Arm's for cross-tree consistency")
|
||||||
|
points at Arm, which does not derive those. Concern recorded;
|
||||||
|
no functional impact (the Task 1 tests do not depend on
|
||||||
|
`PartialEq`, and `Term` itself uses a custom `PartialEq` impl on
|
||||||
|
`Type` only).
|
||||||
|
|
||||||
|
- iter mut.1.2 — substantive walker arms: 9 sites in `desugar.rs`,
|
||||||
|
2 in `workspace.rs`, 3 in `ailang-check/src/lib.rs` (1 in
|
||||||
|
`substitute_rigids_in_term`, 1 in `verify_tail_positions`, 1
|
||||||
|
variant-name string emitter inside `synth`'s `Term::ReuseAs` sub-
|
||||||
|
match), 2 in `lift.rs`, 3 in `linearity.rs` (one being a
|
||||||
|
variant-name string emitter), 2 in `mono.rs`, 1 each in
|
||||||
|
`pre_desugar_validation.rs`, `reuse_shape.rs`, `uniqueness.rs`,
|
||||||
|
3 in `escape.rs`, 1 in `lambda.rs`, 1 in `codegen/src/lib.rs`
|
||||||
|
(the `synth_with_extras` type-synthesis helper), 3 in
|
||||||
|
`ailang-prose/src/lib.rs`, 2 in `ail/src/main.rs`. Plus one
|
||||||
|
unenumerated site in `crates/ail/tests/codegen_import_map_fallback_pin.rs`
|
||||||
|
(an exhaustive walker inside a test) that the build required.
|
||||||
|
Each arm follows the appropriate shape per existing convention
|
||||||
|
at its site: rebuilder shapes for `substitute_*` / `subst_*` /
|
||||||
|
`desugar_term` / `lift_in_term` / `substitute_rigids_in_term` /
|
||||||
|
`rewrite_term`, visitor shapes for `collect_used_in_term` /
|
||||||
|
`free_vars_in_term` / `find_non_callee_use` / `walk_term*` /
|
||||||
|
`verify_tail_positions` / `term_has_letrec` / the linearity
|
||||||
|
walkers / `walk` in `reuse_shape.rs` / `walk` in `uniqueness.rs`
|
||||||
|
/ `walk` in `escape.rs` / `collect_captures` / `count_free_var` /
|
||||||
|
`subst_var_with_term` (rebuilder-shape in prose). Shadowing
|
||||||
|
semantics across mut-var bindings replicates the
|
||||||
|
`Term::Let`/`Term::Lam`/`Term::LetRec`-shaped convention at each
|
||||||
|
site: var-init terms see the outer scope plus already-declared
|
||||||
|
vars; later var-inits and the body see all earlier var bindings.
|
||||||
|
|
||||||
|
- iter mut.1.3 — dispatch stubs: typecheck dispatch at `synth` in
|
||||||
|
`ailang-check/src/lib.rs` (Plan line 2572 was wrong — that line
|
||||||
|
is `verify_tail_positions`, which is substantive walker
|
||||||
|
territory; the actual dispatch entry is `synth` at line 3403's
|
||||||
|
former `Term::ReuseAs` arm, where the two new arms now sit)
|
||||||
|
returns `CheckError::Internal("Term::Mut/Assign not yet
|
||||||
|
supported in typecheck (deferred to iter mut.2)")`. Codegen
|
||||||
|
dispatch at `lower_term` in `ailang-codegen/src/lib.rs:1649`
|
||||||
|
returns `CodegenError::Internal("Term::Mut/Assign not yet
|
||||||
|
supported in codegen (deferred to iter mut.3)")`. Both errors
|
||||||
|
use the single-field tuple-variant shape (`Internal(String)`).
|
||||||
|
|
||||||
|
- iter mut.1.4 — Form A parser + printer: dispatcher arms added
|
||||||
|
for `"mut" => parse_mut` and `"assign" => parse_assign` in
|
||||||
|
`parse_term`'s head-keyword match; the two helpers parse
|
||||||
|
positionally (mut: pull leading `(var ...)` entries, then ≥ 1
|
||||||
|
body terms right-folded into `Term::Seq`; assign: positional
|
||||||
|
ident-then-value pair). Plan pseudo-code used helper names
|
||||||
|
(`peek_is_open_paren_with_head`, `expect_head`, etc.) that don't
|
||||||
|
exist; substituted the actual API (`expect_lparen`,
|
||||||
|
`expect_keyword`, `expect_ident`, `parse_type`, `parse_term`,
|
||||||
|
`expect_rparen`, `peek_head_ident`, `matches!(self.peek(),
|
||||||
|
Tok::RParen)`). Four RED-first parser tests added covering empty
|
||||||
|
mut + 1-var-1-assign-1-final + missing-body rejection (both
|
||||||
|
shapes). Print arms in `print.rs` walk the right-spine of
|
||||||
|
`Term::Seq` and emit each lhs as a top-level statement; the
|
||||||
|
terminal expression closes the `(mut …)` form. EBNF prologue in
|
||||||
|
`parse.rs` extended with `mut-term`, `var-decl`, `assign-term`
|
||||||
|
productions. The print arms had to land during the Task 1+2
|
||||||
|
build-unblock phase rather than waiting for Task 4 (they are
|
||||||
|
exhaustive-match neighbours of the synth/lower_term dispatchers);
|
||||||
|
this is a benign sequencing shift and the plan's "Task 2 build-
|
||||||
|
green verification gate" still holds.
|
||||||
|
|
||||||
|
- iter mut.1.5 — drift + coverage extensions: two new exemplars
|
||||||
|
added to `design_schema_drift.rs` and two new match arms; two
|
||||||
|
new `VariantTag` entries (`TermMut`, `TermAssign`),
|
||||||
|
`EXPECTED_VARIANTS` extended, two new `visit_term` arms in
|
||||||
|
`schema_coverage.rs`; two new exemplars + two new match arms in
|
||||||
|
`spec_drift.rs`. `form_a.md` gains three new lines in the
|
||||||
|
Parenthesised-forms block plus a prose paragraph naming the
|
||||||
|
empty-vars and post-vars-body invariants and the diagnostic
|
||||||
|
codes. `DESIGN.md` §"Term (expression)" gains the two jsonc
|
||||||
|
schema blocks immediately after the `reuse-as` block plus a
|
||||||
|
closing prose paragraph naming the mut.1 dispatch-stub semantics
|
||||||
|
and the deferred mut.2 / mut.3 iterations. Post-edit:
|
||||||
|
`design_schema_drift` and `spec_drift` both green;
|
||||||
|
`schema_coverage` is RED on the missing `examples/mut.ail`
|
||||||
|
fixture, per plan Step 6 expectation.
|
||||||
|
|
||||||
|
- iter mut.1.6 — fixture + round-trip: `examples/mut.ail` shipped
|
||||||
|
with the six fns from plan Step 1 verbatim (empty / single-var
|
||||||
|
with assign / two-var with combine / nested-shadow / Bool /
|
||||||
|
Unit). The corpus-globbing round-trip test
|
||||||
|
(`parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`)
|
||||||
|
and the parse-determinism test pick the fixture up automatically;
|
||||||
|
both green. `schema_coverage` also green now that both new
|
||||||
|
variants are observed in the corpus. Full `cargo test
|
||||||
|
--workspace` 579/579 green.
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
- `MutVar` derives diverge from the plan's literal pseudo-code
|
||||||
|
(`Debug, Clone, PartialEq, Eq, Serialize, Deserialize`); the
|
||||||
|
shipped form is `Debug, Clone, Serialize, Deserialize` to match
|
||||||
|
Arm's pattern. The plan's own justification ("match Arm's
|
||||||
|
derives for cross-tree consistency") points at Arm which does
|
||||||
|
not derive PartialEq/Eq. Functional impact: none in mut.1
|
||||||
|
(the Task 1 tests do not depend on PartialEq, and `Term` uses
|
||||||
|
a custom PartialEq impl scoped to `Type` only). A future iter
|
||||||
|
that needs to compare `MutVar` (e.g. for in-place rewriting in
|
||||||
|
mut.3 codegen) can add the derives at that time without breaking
|
||||||
|
any mut.1 test.
|
||||||
|
|
||||||
|
- Plan recon for the typecheck dispatch entry was misindexed.
|
||||||
|
The plan listed `crates/ailang-check/src/lib.rs:2572` as the
|
||||||
|
"check_term dispatch — STUBBED" site; that line is actually in
|
||||||
|
`verify_tail_positions`, a substantive walker. The real
|
||||||
|
typecheck dispatch entry is `synth` (defined at lib.rs:2613,
|
||||||
|
with its `Term::ReuseAs` arm at line 3403 before the new mut
|
||||||
|
arms). The stub was placed at the actual dispatch entry; line
|
||||||
|
2572 received a substantive arm. The variant-name string
|
||||||
|
emitter sub-match referenced as line 3430 is inside `synth`'s
|
||||||
|
former `Term::ReuseAs` arm and was extended as Task 2 Step 13
|
||||||
|
asks. Net: the spec intent (stub at typecheck dispatch) is
|
||||||
|
preserved; only the line-number routing differs.
|
||||||
|
|
||||||
|
- One test-side walker site (`crates/ail/tests/codegen_import_map_fallback_pin.rs:62`)
|
||||||
|
carries an exhaustive match on `Term` and was not enumerated
|
||||||
|
by the plan's Task 2 file list. The site is the body-walker of
|
||||||
|
a regression-pin test from iter 24.tidy; it required two new
|
||||||
|
arms to build green. Added with appropriate visitor-shape
|
||||||
|
recursion. The plan-recon miss is recorded here as a
|
||||||
|
documentary item; no behavioural impact.
|
||||||
|
|
||||||
|
## Known debt
|
||||||
|
|
||||||
|
- Prose-side `(mut ...)` rendering in `ailang-prose/src/lib.rs` is
|
||||||
|
a minimal-correctness shape (`mut { var <name> = <init>; ...; <body> }`).
|
||||||
|
The prose surface for mut blocks is not yet fully designed; a
|
||||||
|
future prose-iter once the LLM-author signal arrives will refine
|
||||||
|
it. This is a deliberate placeholder, not drift — recording for
|
||||||
|
visibility.
|
||||||
|
|
||||||
|
- `subst_var` in `desugar.rs` does NOT rename the `Term::Assign.name`
|
||||||
|
field. The desugar pass's `subst_var` rewrites let-rec captures
|
||||||
|
(KnownType / LetBound / MatchArm), none of which can be mut-vars
|
||||||
|
(mut-vars are first-order scalars, not captureable). Leaving
|
||||||
|
the assign-name untouched is semantically correct; documented in
|
||||||
|
the arm's doc-comment. Mentioned here for visibility.
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
- `crates/ailang-core/src/ast.rs` — `Term::Mut`, `Term::Assign`, `MutVar`, two new unit tests
|
||||||
|
- `crates/ailang-core/src/desugar.rs` — 9 walker arms
|
||||||
|
- `crates/ailang-core/src/workspace.rs` — 2 walker arms
|
||||||
|
- `crates/ailang-check/src/lib.rs` — 3 walker arms (substantive + variant-name) + 2 dispatch stubs in `synth`
|
||||||
|
- `crates/ailang-check/src/lift.rs` — 2 walker arms
|
||||||
|
- `crates/ailang-check/src/linearity.rs` — 3 walker arms (2 substantive + 1 variant-name)
|
||||||
|
- `crates/ailang-check/src/mono.rs` — 2 walker arms (1 mut-ref rebuilder, 1 visitor)
|
||||||
|
- `crates/ailang-check/src/pre_desugar_validation.rs` — 1 walker arm
|
||||||
|
- `crates/ailang-check/src/reuse_shape.rs` — 1 walker arm
|
||||||
|
- `crates/ailang-check/src/uniqueness.rs` — 1 walker arm
|
||||||
|
- `crates/ailang-codegen/src/escape.rs` — 3 walker arms
|
||||||
|
- `crates/ailang-codegen/src/lambda.rs` — 1 walker arm
|
||||||
|
- `crates/ailang-codegen/src/lib.rs` — 1 walker arm (`synth_with_extras`) + 2 dispatch stubs in `lower_term`
|
||||||
|
- `crates/ailang-prose/src/lib.rs` — 3 walker arms (1 substantive, 1 `count_free_var`, 1 `subst_var_with_term` rebuilder)
|
||||||
|
- `crates/ail/src/main.rs` — 2 walker arms (dep-walker + `rewrite_term`)
|
||||||
|
- `crates/ail/tests/codegen_import_map_fallback_pin.rs` — 1 walker arm (test-side, unenumerated by plan)
|
||||||
|
- `crates/ailang-surface/src/parse.rs` — dispatcher arms, `parse_mut` / `parse_assign` helpers, EBNF prologue extension, 4 new parser tests
|
||||||
|
- `crates/ailang-surface/src/print.rs` — 2 `write_term` arms
|
||||||
|
- `crates/ailang-core/tests/design_schema_drift.rs` — 2 exemplars + 2 match arms
|
||||||
|
- `crates/ailang-core/tests/schema_coverage.rs` — 2 VariantTag + EXPECTED_VARIANTS + 2 visit_term arms
|
||||||
|
- `crates/ailang-core/tests/spec_drift.rs` — 2 exemplars + 2 match arms
|
||||||
|
- `crates/ailang-core/specs/form_a.md` — 3 new productions + prose paragraph
|
||||||
|
- `docs/DESIGN.md` — 2 jsonc schema blocks + prose paragraph
|
||||||
|
- `examples/mut.ail` — round-trip fixture (6 fns)
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
|
||||||
|
bench/orchestrator-stats/2026-05-15-iter-mut.1.json
|
||||||
@@ -70,3 +70,4 @@
|
|||||||
- 2026-05-14 — iter pd.2: surface owns prelude embed (PRELUDE_AIL + parse_prelude in ailang-surface); pd.1 shim load_workspace_with retired along with PRELUDE_JSON / load_prelude / load_one; cross-form-identity preflight PASSED at module_hash 3abe0d3fa3c11c99; production literal-"prelude" in core/src/ now zero → 2026-05-14-iter-pd.2.md
|
- 2026-05-14 — iter pd.2: surface owns prelude embed (PRELUDE_AIL + parse_prelude in ailang-surface); pd.1 shim load_workspace_with retired along with PRELUDE_JSON / load_prelude / load_one; cross-form-identity preflight PASSED at module_hash 3abe0d3fa3c11c99; production literal-"prelude" in core/src/ now zero → 2026-05-14-iter-pd.2.md
|
||||||
- 2026-05-14 — iter pd.3: prelude.ail.json deleted from working tree; cross-form-identity preflight + supporting bytes deleted from prelude_module_hash_pin (purpose discharged); migrate-bare-cross-module-refs defensive include + lockstep skip-branch deleted; carve_out_inventory.rs §C4(b) row dropped (8→7); form-a-default-authoring spec §C4(b) RETIRED status marker added; new prelude_decouple_carve_out_pin.rs asserts JSON file does not exist; milestone prelude-decouple closed → 2026-05-14-iter-pd.3.md
|
- 2026-05-14 — iter pd.3: prelude.ail.json deleted from working tree; cross-form-identity preflight + supporting bytes deleted from prelude_module_hash_pin (purpose discharged); migrate-bare-cross-module-refs defensive include + lockstep skip-branch deleted; carve_out_inventory.rs §C4(b) row dropped (8→7); form-a-default-authoring spec §C4(b) RETIRED status marker added; new prelude_decouple_carve_out_pin.rs asserts JSON file does not exist; milestone prelude-decouple closed → 2026-05-14-iter-pd.3.md
|
||||||
- 2026-05-14 — audit-pd: milestone close (prelude-decouple) — architect drift fixed inline as audit-pd-tidy (DESIGN.md §"Roundtrip Invariant" carve-out count 8→7 + main.rs migrate-subcommand dead prelude fallback removed); bench mixed (check.py + compile_check.py established noise envelope, 15th consecutive observation, baseline pristine; cross_lang clean) → 2026-05-14-audit-pd.md
|
- 2026-05-14 — audit-pd: milestone close (prelude-decouple) — architect drift fixed inline as audit-pd-tidy (DESIGN.md §"Roundtrip Invariant" carve-out count 8→7 + main.rs migrate-subcommand dead prelude fallback removed); bench mixed (check.py + compile_check.py established noise envelope, 15th consecutive observation, baseline pristine; cross_lang clean) → 2026-05-14-audit-pd.md
|
||||||
|
- 2026-05-15 — iter mut.1: AST extension `Term::Mut` + `Term::Assign` + `MutVar` struct; Form A `(mut ...) / (var ...) / (assign ...)` productions parse + print + round-trip; ~25 substantive Term-walker arms across the codebase; dispatch stubs in `synth` (typecheck) + `lower_term` (codegen) return `CheckError::Internal` / `CodegenError::Internal` per the spec's out-of-iteration boundary; `examples/mut.ail` six-fn fixture (Int/Float/Bool/Unit + empty + nested-shadow); drift + coverage + spec-drift tests + DESIGN.md §"Term (expression)" + `crates/ailang-core/specs/form_a.md` amendments; tests 564 → 579 → 2026-05-15-iter-mut.1.md
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
(module mut
|
||||||
|
|
||||||
|
(fn mut_empty
|
||||||
|
(doc "Iter mut.1 — empty mut block; body is a single Int literal.")
|
||||||
|
(type (fn-type (params) (ret (con Int))))
|
||||||
|
(params)
|
||||||
|
(body (mut 0)))
|
||||||
|
|
||||||
|
(fn mut_single_var
|
||||||
|
(doc "Iter mut.1 — one var, one assign, final expression reads the var.")
|
||||||
|
(type (fn-type (params) (ret (con Int))))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var x (con Int) 0)
|
||||||
|
(assign x (app + x 1))
|
||||||
|
x)))
|
||||||
|
|
||||||
|
(fn mut_two_vars
|
||||||
|
(doc "Iter mut.1 — two vars, two assigns, final expression combines them.")
|
||||||
|
(type (fn-type (params) (ret (con Float))))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var sum (con Float) 0.0)
|
||||||
|
(var count (con Int) 0)
|
||||||
|
(assign sum (app + sum 1.0))
|
||||||
|
(assign count (app + count 1))
|
||||||
|
(app + sum (app int_to_float count)))))
|
||||||
|
|
||||||
|
(fn mut_nested_shadow
|
||||||
|
(doc "Iter mut.1 — outer var shadowed by inner mut block's var of the same name.")
|
||||||
|
(type (fn-type (params) (ret (con Int))))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var x (con Int) 10)
|
||||||
|
(assign x (app + x 1))
|
||||||
|
(mut
|
||||||
|
(var x (con Int) 100)
|
||||||
|
(assign x (app + x 1))
|
||||||
|
x))))
|
||||||
|
|
||||||
|
(fn mut_returns_bool
|
||||||
|
(doc "Iter mut.1 — exercise Bool as a supported scalar var type.")
|
||||||
|
(type (fn-type (params) (ret (con Bool))))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var flag (con Bool) false)
|
||||||
|
(assign flag true)
|
||||||
|
flag)))
|
||||||
|
|
||||||
|
(fn mut_returns_unit
|
||||||
|
(doc "Iter mut.1 — exercise Unit as the supported scalar zero case.")
|
||||||
|
(type (fn-type (params) (ret (con Unit))))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var u (con Unit) (lit-unit))
|
||||||
|
(assign u (lit-unit))
|
||||||
|
u))))
|
||||||
Reference in New Issue
Block a user