03fb633d85
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.
140 lines
6.8 KiB
Markdown
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
|