# 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 , ptr ` 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