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.
This commit is contained in:
@@ -2831,6 +2831,26 @@ What **is** supported (and used as the smoke test for the pipeline):
|
||||
arg types (ctor) or the scrutinee's `Type::Con.args` (match). An
|
||||
unresolved `Type::Var` reaching `llvm_type` is a hard error
|
||||
rather than a silent fallback to `ptr`.
|
||||
- **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 — the `alloca` instructions are hoisted to the fn's
|
||||
entry block via a per-fn side buffer spliced after `lower_term` so
|
||||
mem2reg eligibility holds regardless of how deeply nested the
|
||||
originating `Term::Mut` block sits. Exercised end-to-end by
|
||||
`examples/mut_counter.ail` (Int) and `examples/mut_sum_floats.ail`
|
||||
(Float). Mut-vars 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/2026-05-15-mut-local.md`.
|
||||
|
||||
Pipeline regression smoke tests:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user