All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
26 KiB
mut.3 — codegen + e2e — Implementation Plan
Parent spec:
docs/specs/0029-mut-local.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Replace the iter mut.1 dispatch stubs at
crates/ailang-codegen/src/lib.rs:1683-1691 with real LLVM lowering:
mut-vars become alloca slots hoisted to the fn's entry block,
assign lowers to store, mut-var reads lower to load. Extend the
codegen-side Term::Var resolution ladder to consult mut-var allocas
before self.locals. Ship two end-to-end fixtures
(examples/mut_counter.ail + examples/mut_sum_floats.ail) and
two e2e test cases asserting expected stdout. Append a bullet to
DESIGN.md §"What is supported".
Architecture: Mut-var allocas land in the fn's entry block via
a side buffer pending_entry_allocas: String accumulated during
body lowering and flushed once into self.body at a tracked byte
position immediately after start_block("entry") emits the entry:
label. This is a deliberate departure from the existing alloca
sites (match_lower.rs:114, lambda.rs:240/266) which emit allocas
at the current body position — those allocas are one-shot per fn
invocation and don't suffer from being non-entry-block. Mut-var
allocas in deeply-nested blocks (e.g. inside if/match/seq)
must remain accessible across the whole fn body, so mem2reg
eligibility (entry-block placement) matters. Mut-var lookup at
codegen uses a per-fn mut_var_allocas: BTreeMap<String, (String, Type)> mapping name → (alloca SSA name, AIL type). Push entries
on Term::Mut-entry, remove on exit; lexical scoping mirrors the
mut.2 typecheck-side stack.
Tech Stack: ailang-codegen only. No ailang-core,
ailang-surface, ailang-check, ailang-prose, or ail source
changes (one new test fn in crates/ail/tests/e2e.rs). Two new
example fixtures + one DESIGN.md amendment.
Files this plan creates or modifies:
- Create:
examples/mut_counter.ail— sum 1..10 via mut, prints 55. - Create:
examples/mut_sum_floats.ail— Float twin (sum 1.0..10.0), prints the Float total. - Modify:
crates/ailang-codegen/src/lib.rs:716-717— addmut_var_allocas: BTreeMap<String, (String, Type)>andpending_entry_allocas: Stringandentry_block_end_marker: Option<usize>fields toEmitter. - Modify:
crates/ailang-codegen/src/lib.rs:820-842— initialise the three new fields inEmitter::new. - Modify:
crates/ailang-codegen/src/lib.rs:1008-1034— reset the three new fields per fn-body inemit_fn, and capture the entry-block byte marker afterstart_block("entry"). - Modify:
crates/ailang-codegen/src/lib.rs:837-842— extendstart_block(or its caller) to recordentry_block_end_markeron entry-block emission. - Modify:
crates/ailang-codegen/src/lib.rs:1292-1357(Term::Vararm) — prepend a mut-var lookup that emits aloadand returns the SSA name. - Modify:
crates/ailang-codegen/src/lib.rs:1683-1691— replace theTerm::Mut/Term::AssignInternalstubs with real lowering. - Modify:
crates/ailang-codegen/src/lib.rs(end ofemit_fn) — flushpending_entry_allocasintoself.bodyat the marker. - Modify:
crates/ail/tests/e2e.rs— addmut_counter_prints_55andmut_sum_floats_prints_55_dot_0test fns. - Modify:
docs/DESIGN.md— append a bullet to the "What is supported" subsection around line 2680.
Task 1 — Emitter field additions + per-fn reset
Files:
-
Modify:
crates/ailang-codegen/src/lib.rs:716-717(struct fields) -
Modify:
crates/ailang-codegen/src/lib.rs:820-842(new) -
Modify:
crates/ailang-codegen/src/lib.rs:1008-1034(emit_fnreset) -
Step 1: Add three fields to
Emitter
Locate the Emitter struct around crates/ailang-codegen/src/lib.rs:716.
After the existing field current_param_modes: BTreeMap<String, ParamMode>
(line 717), insert:
/// Iter mut.3: per-fn map of mut-var name → (alloca SSA name,
/// AIL element type). Populated when entering a `Term::Mut`
/// block; entries removed when leaving the block. Consulted by
/// the `Term::Var` arm of `lower_term` before `self.locals`,
/// matching the typecheck-side mut-scope-stack precedence.
mut_var_allocas: BTreeMap<String, (String, Type)>,
/// Iter mut.3: side buffer for `alloca` instructions emitted
/// during body lowering but hoisted to the fn's entry block.
/// Flushed once into `self.body` at `entry_block_end_marker`
/// after `lower_term` completes.
pending_entry_allocas: String,
/// Iter mut.3: byte position in `self.body` immediately after
/// the `entry:` label, captured during `start_block("entry")`
/// at fn-body emission. Used to splice `pending_entry_allocas`
/// into the entry block once body lowering completes.
entry_block_end_marker: Option<usize>,
- Step 2: Initialise in
Emitter::new
Locate Emitter::new around crates/ailang-codegen/src/lib.rs:820.
Near the existing closure_drops: BTreeMap::new(), line (around 831),
add:
mut_var_allocas: BTreeMap::new(),
pending_entry_allocas: String::new(),
entry_block_end_marker: None,
- Step 3: Reset in
emit_fn
Locate the per-fn reset block in emit_fn (around
crates/ailang-codegen/src/lib.rs:1008-1034 — where
self.locals.clear(), self.counter = 0, self.moved_slots.clear(),
self.current_param_modes.clear() already live). Append:
self.mut_var_allocas.clear();
self.pending_entry_allocas.clear();
self.entry_block_end_marker = None;
- Step 4: Verify build green
Run: cargo build --workspace 2>&1 | tail -10
Expected: green (the three new fields are added but not yet
consumed; the only consumers are added in Tasks 2-3).
Task 2 — start_block records the entry-block marker
Files:
- Modify:
crates/ailang-codegen/src/lib.rs:837-842(start_block)
start_block is the canonical helper that emits <label>:\n and
positions the body for subsequent instruction emission. The entry
block is the first one emitted per fn. Marker capture happens here.
- Step 1: Locate
start_block
Around crates/ailang-codegen/src/lib.rs:837, find:
fn start_block(&mut self, label: &str) {
self.body.push_str(label);
self.body.push_str(":\n");
}
(Exact body may differ; read the existing form first.)
- Step 2: Capture the entry-block marker
Replace with (or extend) the body so that when label == "entry",
the byte position immediately after the entry:\n is captured:
fn start_block(&mut self, label: &str) {
self.body.push_str(label);
self.body.push_str(":\n");
if label == "entry" {
self.entry_block_end_marker = Some(self.body.len());
}
}
The marker points to the byte index where the next character would
be written — i.e. the splice point for pending_entry_allocas.
Splicing at this position inserts the alloca block immediately after
the entry:\n label and before any subsequent body emission.
- Step 3: Verify build green
Run: cargo build --workspace 2>&1 | tail -5
Expected: green.
Task 3 — Term::Var resolution prepends mut-var lookup
Files:
-
Modify:
crates/ailang-codegen/src/lib.rs:1292-1357(Term::Vararm oflower_term) -
Step 1: Locate the
Term::Vararm
Around crates/ailang-codegen/src/lib.rs:1292, the existing arm
walks: __unreachable__ → self.locals.iter().rev().find(...) →
resolve_top_level_fn → resolve_const → emit UnknownVar. The
mut-var lookup slots immediately before self.locals.
- Step 2: Prepend mut-var lookup
Insert before the self.locals.iter().rev().find(...) call:
// Iter mut.3: mut-vars take precedence over let-bound
// / param resolutions. Innermost-first (BTreeMap iterates
// in sorted order, but per-fn entries are unique per
// name with innermost-wins shadowing already enforced
// at typecheck time, so a single lookup suffices here).
if let Some((alloca_name, ail_ty)) = self.mut_var_allocas.get(name).cloned() {
let llvm_ty = self.llvm_type(&ail_ty);
let load_ssa = self.fresh_ssa_name("mut_load");
self.body.push_str(&format!(
" {load_ssa} = load {llvm_ty}, ptr {alloca_name}\n"
));
return Ok(load_ssa);
}
The exact helper names (self.llvm_type, self.fresh_ssa_name,
self.body, the return-tuple shape) must match the existing arms in
lower_term — the implementer reads adjacent arms (e.g. the
self.locals.iter().rev().find() branch immediately below) to copy
the canonical form. The example above is shape-correct but the
function-call sites (llvm_type / fresh_ssa_name) may be different
in this codebase; verify before writing.
Note: shadowing across nested mut blocks is handled by the
push/remove in Task 4, not by stack ordering here — when an inner
mut block shadows an outer's name, the inner's push overwrites the
BTreeMap entry; the outer's restored on inner-exit.
- Step 3: Verify build green
Run: cargo build --workspace 2>&1 | tail -5
Expected: green.
Task 4 — Term::Mut codegen arm
Files:
-
Modify:
crates/ailang-codegen/src/lib.rs:1683-1687— replace theTerm::Mutstub. -
Step 1: Replace the stub
Around crates/ailang-codegen/src/lib.rs:1683, replace:
Term::Mut { .. } => {
Err(CodegenError::Internal(
"Term::Mut not yet supported in codegen (deferred to iter mut.3)".into(),
))
}
with the real lowering:
Term::Mut { vars, body } => {
// Saved-binding map for proper shadowing on exit.
let mut saved: Vec<(String, Option<(String, Type)>)> = Vec::new();
for v in vars {
let alloca_name = self.fresh_ssa_name(&format!("mut_{}", v.name));
let llvm_ty = self.llvm_type(&v.ty);
// Emit the alloca into the entry-block side
// buffer (hoisted, not at current body position).
self.pending_entry_allocas.push_str(&format!(
" {alloca_name} = alloca {llvm_ty}\n"
));
// Lower the init expression at the current body
// position. The mut-var binding is NOT yet
// visible during init (matches typecheck-side
// ordering at lib.rs:3576).
let init_ssa = self.lower_term(&v.init, /* whatever existing params */)?;
// Store the init result into the alloca at the
// current body position.
self.body.push_str(&format!(
" store {llvm_ty} {init_ssa}, ptr {alloca_name}\n"
));
// Now bind the name → alloca, saving any prior
// entry for restoration on block exit.
let prior = self.mut_var_allocas.insert(
v.name.clone(),
(alloca_name, v.ty.clone()),
);
saved.push((v.name.clone(), prior));
}
// Lower the body in the extended scope.
let body_ssa = self.lower_term(body, /* whatever existing params */)?;
// Restore bindings on exit.
for (name, prior) in saved.into_iter().rev() {
match prior {
Some(p) => { self.mut_var_allocas.insert(name, p); }
None => { self.mut_var_allocas.remove(&name); }
}
}
Ok(body_ssa)
}
The exact lower_term call signature must match the existing arms
in the same match — the implementer reads an adjacent arm (e.g.
Term::Let or Term::Seq) for the canonical form. The init/store
ordering inside the loop is the load-bearing semantic: the spec
demands "vars are checked in order with the in-progress scope" at
typecheck (lib.rs:3576), and codegen mirrors this by binding only
AFTER the init is lowered.
- Step 2: Build + verify the mut.ail typecheck-clean fns now don't reach codegen stub for Mut
Run: cargo build --workspace 2>&1 | tail -5
Expected: green.
Run: cargo run --quiet --bin ail -- build examples/mut.ail 2>&1 | head -20
Expected: this command may fail at runtime for some of the six fns
that use Bool/Unit (not yet covered by Task 5 in lower_term's
Term::Assign arm) — but the build should NOT produce the
"Term::Mut not yet supported" error. If it does, the Task 4 arm
isn't reached; debug before continuing.
Task 5 — Term::Assign codegen arm
Files:
-
Modify:
crates/ailang-codegen/src/lib.rs:1688-1691— replace theTerm::Assignstub. -
Step 1: Replace the stub
Around crates/ailang-codegen/src/lib.rs:1688, replace:
Term::Assign { .. } => {
Err(CodegenError::Internal(
"Term::Assign not yet supported in codegen (deferred to iter mut.3)".into(),
))
}
with the real lowering:
Term::Assign { name, value } => {
// Look up the mut-var's alloca and AIL type.
let (alloca_name, ail_ty) = self.mut_var_allocas
.get(name)
.cloned()
.ok_or_else(|| CodegenError::Internal(
format!("Term::Assign {{ name: {name:?} }} reached codegen without a mut-var alloca — typecheck should have rejected this earlier")
))?;
let llvm_ty = self.llvm_type(&ail_ty);
let value_ssa = self.lower_term(value, /* existing params */)?;
self.body.push_str(&format!(
" store {llvm_ty} {value_ssa}, ptr {alloca_name}\n"
));
// Term::Assign's static type is Unit. Yield the
// canonical Unit SSA value per the codegen
// convention for Unit (verify against an adjacent
// arm — likely a literal "i1 0" or similar; the
// mut.ail mut_returns_unit fixture exercises this).
Ok(self.unit_ssa()) // or whichever helper returns the canonical Unit SSA
}
The self.unit_ssa() helper name is a placeholder; the implementer
identifies the canonical "Unit value" SSA emission convention from
an adjacent codegen arm (e.g. Term::Lit { lit: Literal::Unit })
and uses whichever form matches.
- Step 2: Verify build + mut.ail full pipeline
Run: cargo build --workspace 2>&1 | tail -5
Expected: green.
Run: cargo run --quiet --bin ail -- build examples/mut.ail 2>&1 | head -10
Expected: build succeeds. (Whether the binary runs successfully is
exercised in Task 7 with the dedicated e2e fixtures.)
Task 6 — Flush pending_entry_allocas at the end of emit_fn
Files:
-
Modify:
crates/ailang-codegen/src/lib.rs— the end ofemit_fn(just before the fn body is written out /}is emitted). -
Step 1: Locate the emit_fn end
Around crates/ailang-codegen/src/lib.rs:1008+, find where emit_fn
finishes assembling self.body and is about to flush it to
self.ir (or wherever the final IR text accumulates). This is the
splice point.
- Step 2: Splice
pending_entry_allocasat the marker
Immediately before the body is written out:
// Iter mut.3: splice any deferred entry-block allocas at
// the captured marker (immediately after the `entry:\n`
// label). If no mut-vars were emitted for this fn,
// `pending_entry_allocas` is empty and this is a no-op.
if !self.pending_entry_allocas.is_empty() {
if let Some(marker) = self.entry_block_end_marker {
let allocas = std::mem::take(&mut self.pending_entry_allocas);
self.body.insert_str(marker, &allocas);
} else {
return Err(CodegenError::Internal(
"pending_entry_allocas accumulated but no entry-block marker was captured".into(),
));
}
}
This splices the alloca instructions into self.body at the byte
position recorded by Task 2's start_block extension. The result is
an entry block whose first instructions are alloca for each
mut-var encountered anywhere in the fn body, followed by whatever
the original entry-block instructions are.
- Step 3: Verify build green
Run: cargo build --workspace 2>&1 | tail -5
Expected: green.
Task 7 — examples/mut_counter.ail + examples/mut_sum_floats.ail
Files:
-
Create:
examples/mut_counter.ail -
Create:
examples/mut_sum_floats.ail -
Step 1: Write
examples/mut_counter.ail
Create examples/mut_counter.ail:
(module mut_counter
(fn main
(doc "Iter mut.3 — sum 1..10 via mut/var/assign. Expected stdout: 55.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(app print
(mut
(var i (con Int) 1)
(var sum (con Int) 0)
(assign sum (loop_body i sum)))))))
(fn loop_body
(doc "Helper — recursive sum 1..10. Replaced by a real loop in a future milestone.")
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
(params i acc)
(body
(if (app > i 10)
acc
(app loop_body (app + i 1) (app + acc i))))))
Wait — this requires mut to call a helper, which the mut codegen
supports (init / body / assign-value can be any Term). But the
example's idiomatic form is harder to express without while
(deferred). The simplest accurate form:
(module mut_counter
(fn main
(doc "Iter mut.3 — sum 1..10 via mut + recursive helper. Expected stdout: 55.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(app print
(mut
(var sum (con Int) 0)
(assign sum (app sum_helper 1 10))
sum))))
(fn sum_helper
(doc "Recursive helper — sum from lo through hi inclusive.")
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
(params lo hi)
(body
(if (app > lo hi)
0
(app + lo (tail-app sum_helper (app + lo 1) hi))))))
The mut block exercises one var (sum) with a real assign of a
non-trivial Term result. The recursive helper is the standing
tail-call iteration shape per Decision 8. Expected stdout: 55.
- Step 2: Write
examples/mut_sum_floats.ail
Create examples/mut_sum_floats.ail:
(module mut_sum_floats
(fn main
(doc "Iter mut.3 — Float twin of mut_counter. Expected stdout: 55.0 (or whatever the polymorphic-print form emits for Float 55.0).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(app print
(mut
(var sum (con Float) 0.0)
(assign sum (app sum_helper 1.0 10.0))
sum))))
(fn sum_helper
(doc "Recursive Float helper — sum from lo through hi inclusive.")
(type (fn-type (params (con Float) (con Float)) (ret (con Float))))
(params lo hi)
(body
(if (app > lo hi)
0.0
(app + lo (tail-app sum_helper (app + lo 1.0) hi))))))
Expected stdout: whatever the polymorphic Float print emits for
the value 55.0. The implementer verifies against an existing
Float-print fixture (e.g. examples/floats.ail's known output) to
fix the expected string.
- Step 3: Verify both fixtures round-trip + typecheck clean
Run: cargo run --quiet --bin ail -- check examples/mut_counter.ail 2>&1
Expected: ok (...).
Run: cargo run --quiet --bin ail -- check examples/mut_sum_floats.ail 2>&1
Expected: ok (...).
Run: cargo test --workspace -p ailang-surface round_trip 2>&1 | tail -5
Expected: PASS — the auto-glob round-trip picks up both new fixtures.
Task 8 — e2e tests in crates/ail/tests/e2e.rs
Files:
-
Modify:
crates/ail/tests/e2e.rs -
Step 1: Read the canonical e2e test pattern
Open crates/ail/tests/e2e.rs and locate the build_and_run
helper (around lines 13-38 per the recon). Read the convention for
a #[test] that invokes it and asserts on stdout.
- Step 2: Add the two test fns
At the end of crates/ail/tests/e2e.rs, append:
#[test]
fn mut_counter_prints_55() {
let stdout = build_and_run("mut_counter.ail");
assert_eq!(stdout.trim(), "55", "mut_counter must print 55, got {stdout:?}");
}
#[test]
fn mut_sum_floats_prints_55_dot() {
let stdout = build_and_run("mut_sum_floats.ail");
// The exact float-print form is whatever the polymorphic print
// emits for 55.0 — likely "55" if the formatter drops trailing
// zeros, "55.0" if it preserves them. Verify against
// examples/floats.ail's expected output before pinning.
let expected = "55"; // placeholder — implementer reads the floats.ail e2e for the canonical form
assert_eq!(stdout.trim(), expected, "mut_sum_floats must print {expected}, got {stdout:?}");
}
The "placeholder" comment marks a piece of work the implementer must resolve at execution time by reading whichever existing Float-print e2e test pins the expected stdout — the placeholder MUST NOT survive into the committed file. Replace with the correct expected string before the test passes.
- Step 3: Run the new tests
Run: cargo test --workspace --test e2e mut_counter_prints_55 mut_sum_floats_prints_55_dot 2>&1 | tail -10
Expected: both PASS.
Run: cargo test --workspace 2>&1 | tail -5
Expected: full workspace green.
Task 9 — DESIGN.md amendment
Files:
-
Modify:
docs/DESIGN.md— append to "What is supported" subsection (around line 2680-2800). -
Step 1: Locate the section
Read docs/DESIGN.md:2680-2800. The section starts with:
What **is** supported (and used as the smoke test for the pipeline):
- Int, Bool, Unit, **Str**, **Float** as primitive types.
- `if`, `let`, function calls, recursion.
...
Find a suitable insertion point — likely after the "Anonymous lambdas with capture" bullet (around line 2776) or after "Parameterised ADTs" (around line 2788), depending on where the implementer judges the new bullet thematically fits.
- Step 2: Append the bullet
Insert (with appropriate context):
- **Local mutable state.** `(mut (var <name> <type> <init>) ... <body>)`
declares lexically-scoped mutable bindings whose types are restricted
to the stack-resident scalars `Int`, `Float`, `Bool`, `Unit`. Inside
the block, `(assign <name> <value>)` updates a mut-var; references
to a mut-var name resolve to a `load` at codegen. Mut-vars are
alloca-resident (hoisted to the fn's entry block, exercised by
`examples/mut_counter.ail` and `examples/mut_sum_floats.ail`); they
do not escape the enclosing mut block and do not introduce a `!Mut`
effect onto the surrounding fn signature. Sealed-by-construction:
the spec restricts var element types to non-RC-managed primitives
and forbids first-class references, so Decision 10's "no shared
mutable refs" invariant is preserved (each mut-var is uniquely held
by its enclosing block). Future milestones on the Stateful-islands
path lift the type restriction (heap-RC-managed Str + ADTs), add
escaping references (`ref a` + `!Mut` effect), and introduce
composable transducers (`Stateful a b` + `pipe`). Spec:
`docs/specs/0029-mut-local.md`.
- Step 3: Verify schema-drift tests still pass
Run: cargo test --workspace -p ailang-core schema_drift 2>&1 | tail -5
Expected: PASS — the drift tests scope themselves to §"Data model"
(which is the §"Term (expression)" jsonc-blocked area, already
amended in mut.1), and this new bullet lives outside that scope.
Self-review checklist
-
Spec coverage: every section of spec §"Iteration mut.3 — Codegen + e2e" has a matching task. Cross-check: codegen arms for
Term::Mut(Task 4) andTerm::Assign(Task 5), entry-block- alloca-hoist mechanism (Tasks 1+2+6),Term::Varresolution prepending (Task 3), two example fixtures (Task 7), e2e tests (Task 8), DESIGN.md amendment (Task 9). All covered. -
Placeholder scan: grep for "TBD" / "TODO" / "implement later" / "similar to Task" / "appropriate error handling" / "fill in later". Task 8 Step 2 contains a deliberate "implementer reads the floats.ail e2e for the canonical form" placeholder that must be resolved before the test file is committed — the step explicitly names this. No other placeholders.
-
Type-name consistency:
mut_var_allocas,pending_entry_allocas,entry_block_end_marker,CodegenError::Internal,LLVMBuildAlloca(referenced in spec but in this codebase emitted as text viapush_str),Term::Mut,Term::Assign,MutVar. Each appears identically across tasks. -
Step granularity: each step is 2-5 minutes (a single field addition; a single arm replacement; a single splice point).
-
No commit steps: verified — no
git commitinstructions. -
Boss decisions named: the five recon open questions are resolved: (1) entry-block-hoist via side buffer +
String::insert_strsplice at marker (Tasks 1+2+6); (2) no LLVM helper wrapper — directpush_str(Task 4+5 code shape); (3) e2e incrates/ail/tests/e2e.rs(Task 8); (4) DESIGN.md bullet in "What is supported" subsection (Task 9 Step 1); (5) Unit codegen via the existingTerm::Lit { lit: Unit }arm's canonical SSA emission (Task 5 Step 1 instruction).
Acceptance gate for iteration mut.3
cargo build --workspacegreen.cargo test --workspacegreen.cargo run --bin ail -- check examples/mut.ailclean.cargo run --bin ail -- check examples/mut_counter.ailclean.cargo run --bin ail -- check examples/mut_sum_floats.ailclean.mut_counter.ailbuilds and runs end-to-end via the e2e harness, printing55.mut_sum_floats.ailbuilds and runs end-to-end, printing the canonical Float-print form for55.0(resolved per Task 8 Step 2).- DESIGN.md has the "Local mutable state" bullet under "What is supported".
After this iteration closes, the mut-local milestone is closed end-to-end:
- Schema + surface from mut.1.
- Typecheck from mut.2.
- Codegen + e2e from mut.3. The follow-on Stateful-islands milestones (ref-types, !Mut effect, MutArray, Stateful + pipe) build on top of this. Audit (architect drift + bench regression) follows.