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:
@@ -906,6 +906,35 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow
|
||||
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::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)),
|
||||
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)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user