Files
AILang/docs/journals/2026-05-15-iter-mut.3.md
T
Brummel 03fb633d85 iter mut.3: codegen + e2e — mut-local milestone end-to-end closed
Replaces the iter mut.1 dispatch stubs in lower_term 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. Two
example fixtures run end-to-end printing 55 (Int via mut_counter,
Float via mut_sum_floats).

Concretely:

- Emitter struct gains three new fields: mut_var_allocas:
  BTreeMap<String, (String, Type)> for per-fn name→(alloca SSA,
  AIL type) tracking; pending_entry_allocas: String as a side
  buffer accumulating alloca instructions during body lowering;
  entry_block_end_marker: Option<usize> recording the byte position
  in self.body immediately after the entry: label.

- start_block(label) captures the marker when label == 'entry'.
  emit_fn resets all three new fields per fn body. At the end of
  emit_fn, String::insert_str splices pending_entry_allocas into
  self.body at the marker — alloca instructions land in the entry
  block regardless of where in the source tree Term::Mut was
  encountered (mem2reg eligibility preserved).

- Term::Mut arm: per var, fresh-name an alloca SSA, push the
  alloca instruction into pending_entry_allocas, lower the init at
  the current body position, emit a store to bind. The mut-var
  binding is NOT visible during init (matches typecheck-side
  ordering at lib.rs:3576). After all vars are bound, lower the
  body in the extended scope. On block exit, restore any prior
  bindings via a per-block save stack — supports nested
  shadowing correctly.

- Term::Assign arm: look up the alloca + type via
  mut_var_allocas.get(name); lower the value; emit store; yield
  the canonical Unit SSA per the existing Term::Lit { lit:
  Literal::Unit } convention.

- Term::Var arm: prepended with a mut-var lookup that emits a
  load <ty>, ptr <alloca> and returns the load SSA. Innermost-wins
  shadowing falls out of the BTreeMap's insert-overwrites
  semantics combined with the per-block save/restore.

- examples/mut_counter.ail + examples/mut_sum_floats.ail:
  recursive sum_helper accumulates 1..10 (Int) or 1.0..10.0 (Float)
  inside a mut block whose single var is assigned the helper's
  result. Both run end-to-end printing 55.

- crates/ail/tests/e2e.rs: mut_counter_prints_55 and
  mut_sum_floats_prints_55 #[test] fns using the existing
  build_and_run helper.

- DESIGN.md 'What is supported' subsection: 'Local mutable state'
  bullet describes the new construct, points at the two example
  fixtures, and reaffirms the seal-by-construction invariant under
  Decision 10.

Beyond-plan adjustments absorbed in this iter:
  - synth_with_extras's parallel Term::Var arm in
    ailang-codegen/src/lib.rs needed the same mut-var lookup as
    lower_term's. Plan only specified lower_term's arm.
  - crates/ailang-codegen/src/lambda.rs needed save/restore of all
    three new Emitter fields across the lambda-body boundary plus
    in-thunk splice of pending_entry_allocas at the lambda's own
    entry marker, so mut-blocks inside a closure body hoist into
    the closure's entry block (not the outer fn's).

mut-local milestone end-to-end status:
  - mut.1 (7b92719): AST + Form A surface.
  - mut.2 (b24718a): typecheck.
  - mut.3 (this commit): codegen + e2e.
  Audit follows.

Tests: 592 → 594 green; cargo build green; both fixtures execute
and print 55.

Journal: docs/journals/2026-05-15-iter-mut.3.md.

Refs: docs/specs/2026-05-15-mut-local.md, docs/plans/2026-05-15-iter-mut.3.md.
2026-05-15 01:56:26 +02:00

140 lines
6.8 KiB
Markdown

# 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