# iter mut.3 — codegen + e2e for Term::Mut / Term::Assign

**Date:** 2026-05-15
**Started from:** b057789b55a86cd664e2d7fee2c998bb855cad98
**Status:** DONE
**Tasks completed:** 9 of 9

## Summary

Replaced the iter mut.1 codegen stubs for `Term::Mut` and `Term::Assign`
with real LLVM lowering: mut-vars become `alloca` slots hoisted to the
fn's entry block via a per-fn side buffer `pending_entry_allocas`
spliced into `self.body` at a byte marker captured during
`start_block("entry")`; `Term::Assign` lowers to `store`; references to
a mut-var name lower to `load` through a new lookup precedence in both
`lower_term`'s `Term::Var` arm and `synth_with_extras`'s `Term::Var`
arm (precedence: extras → mut-vars → locals → globals → builtins,
symmetric to the typecheck-side `mut_scope_stack` walk in
`ailang-check`). Lambda emission saves/restores the three new
`Emitter` fields across the thunk boundary and splices the thunk's own
hoisted allocas at its own entry marker so nested mut blocks inside
lambdas hoist into the lambda's entry, not the outer fn's. Two e2e
fixtures shipped (`mut_counter.ail` Int / `mut_sum_floats.ail` Float),
both printing `55` end-to-end (Float's `%g` formatter strips the `.0`).
DESIGN.md gains a "Local mutable state" bullet under "What **is**
supported". Closes the mut-local milestone end-to-end; the audit step
follows.

## Per-task notes

- iter mut.3.1: three `Emitter` fields (`mut_var_allocas`,
  `pending_entry_allocas`, `entry_block_end_marker`) added with
  doc comments; initialised in `Emitter::new`; cleared per-fn at the
  top of `emit_fn` next to the existing per-fn-reset block.
- iter mut.3.2: `start_block` extended — when `label == "entry"` it
  captures `self.body.len()` as `entry_block_end_marker` (the splice
  point for `pending_entry_allocas`).
- iter mut.3.3: `Term::Var` arm of `lower_term` gained a mut-var
  lookup before the `self.locals` walk — on hit, emits
  `%v_N = load <llvm_ty>, ptr <alloca_ssa>` and returns the load SSA
  + LLVM type.
- iter mut.3.4: `Term::Mut` arm of `lower_term` real lowering — for
  each var: alloca into the side buffer, lower init in outer-plus-
  earlier-vars scope (matches typecheck ordering at lib.rs:3578),
  store init into alloca, then install the binding (saving any prior
  entry for restoration on block exit). Lowers body, restores saved
  bindings (unconditional restoration even on error path).
- iter mut.3.5: `Term::Assign` arm of `lower_term` real lowering —
  look up the alloca in `mut_var_allocas`, lower value, emit store,
  return the canonical Unit SSA form `("0", "i8")` matching
  `Literal::Unit` in the same `match`. Tasks 4 and 5 were applied
  in one `Edit` call because both arms were adjacent stubs that
  needed simultaneous replacement (the `match` arm signature changed
  from `{ .. }` to `{ name, value }` / `{ vars, body }`); the review
  separation still held — each arm was spec-checked independently.
- iter mut.3.6: splice of `pending_entry_allocas` at the marker —
  inserted into `self.body` at the end of `emit_fn` (after body
  finalisation, before the deferred-thunks flush). Marker absence
  while `pending` is non-empty fires `CodegenError::Internal` with
  the fn name.
- iter mut.3.7: `examples/mut_counter.ail` (Int) and
  `examples/mut_sum_floats.ail` (Float) authored; both use an
  accumulator-style tail-recursive helper since while-loops are
  deferred. Initial draft used the natural `(app + lo (tail-app ...))`
  shape and was rejected by the tail-call analysis ("call marked
  `tail` is not in tail position") — restructured to pass `acc` as
  a third arg and yield it directly in the base case. Both fixtures
  typecheck clean and print `55` (Float via `%g` strips `.0`).
- iter mut.3.8: `mut_counter_prints_55` + `mut_sum_floats_prints_55`
  appended to `crates/ail/tests/e2e.rs`. The plan's placeholder
  `expected = "55"` resolved by inspection of `examples/floats.ail`
  + `crates/ail/tests/floats_e2e.rs` (where `1.5 + 2.5` prints as
  `4`, confirming `%g`-driven trailing-zero strip — so `55.0`
  prints as `55`); test pinned to `"55"`.
- iter mut.3.9: DESIGN.md "Local mutable state" bullet appended
  after the "Parameterised ADTs" bullet at the end of the "What
  **is** supported" subsection. Schema-drift tests pass (the bullet
  lives outside §"Data model").

Two beyond-plan adjustments emerged during execution and were
absorbed inline (recorded as Concerns below):

- `synth_with_extras`'s `Term::Var` arm also needed mut-var lookup
  precedence. The plan's Task 3 only touched `lower_term`'s
  `Term::Var` arm; without the parallel `synth_with_extras` update,
  every polymorphic-call-site arg-type derivation that reaches a
  mut-var reference fails with `UnknownVar`. Discovered by running
  `examples/mut.ail` through codegen and observing the
  `mut_single_var` case fail at `(app + x 1)`.
- Lambda emission (`lambda.rs`) needed save/restore of the three
  new fields PLUS an in-thunk splice of `pending_entry_allocas` at
  the thunk's own entry marker. Without this, a `Term::Mut` inside
  a lambda body would push its alloca into the outer fn's side
  buffer (or worse, leak into whichever fn's body the splice runs
  on). Mirrors how `lambda.rs` already saves/restores
  `non_escape` / `moved_slots` / `current_param_modes` across the
  thunk boundary.

## Concerns

- Synth-side `Term::Var` mut-var-lookup parallel was not in the
  plan but is structurally required. Recorded as a plan defect to
  feed into the iter-mut.4 lessons (if any) and into the milestone-
  close audit.
- Lambda-boundary save/restore + in-thunk splice was not in the
  plan but is structurally required for correctness of any
  `Term::Mut` inside a lambda body. Same defect category as above.

## Known debt

- None for the milestone-local scope. The spec's deferred items
  (while-loops, heap-RC mut-var element types, `ref a` + `!Mut`
  effect, `Stateful a b` + `pipe`) are explicitly named as future
  milestones in the new DESIGN.md bullet.

## Blocked detail

n/a — Status: DONE.

## Files touched

- `crates/ailang-codegen/src/lib.rs` — three `Emitter` fields, init
  + per-fn reset, `start_block` marker capture, `Term::Var` arm
  mut-var precedence (both `lower_term` and `synth_with_extras`),
  `Term::Mut` + `Term::Assign` arms in `lower_term`, splice at end
  of `emit_fn`.
- `crates/ailang-codegen/src/lambda.rs` — save/restore of the three
  new fields across the thunk boundary, in-thunk splice of
  `pending_entry_allocas`.
- `crates/ail/tests/e2e.rs` — two new tests pinning
  `mut_counter.ail` → `"55"` and `mut_sum_floats.ail` → `"55"`.
- `docs/DESIGN.md` — "Local mutable state" bullet at end of
  "What **is** supported" subsection.
- `examples/mut_counter.ail` — new Int fixture.
- `examples/mut_sum_floats.ail` — new Float fixture.

## Stats

bench/orchestrator-stats/2026-05-15-iter-mut.3.json
