iter remove-mut-var-assign.1: atomic removal of mut/var/assign
mut/var/assign removed from AILang entirely and atomically. Deleted: Term::Mut/Term::Assign/struct MutVar; the three Form-A keywords + parse_mut/parse_assign + grammar EBNF; the 4 mut CheckError variants; the mut_scope_stack synth threading (param dropped from synth + every internal/external/test caller); the two lower_term arms; and every exhaustive no-_ Term::Mut/Term::Assign match arm across 17 source files — cut in lockstep with DESIGN.md, fixtures, the drift trio, carve-out and roadmap so the schema is honest at every commit. No catch-all wildcard introduced (verified). loop/recur + let/if are the surviving forms. The shared codegen alloca machinery survives (loop reuses it): mut_var_allocas renamed binder_allocas (representation-only, loop codegen byte-identical) and the shared Term::Lam escape guard simplified to !loop_stack.is_empty() with the loop half (LoopBinderCapturedByLambda) byte-equivalent. Feature-acceptance applied inverted: the removed feature fails clause 2 (redundant) and clause 3 (IS the iterated-mutable-state bug class). Behaviour preservation is executable: mut_counter/mut_sum_floats still print 55 after the faithful let/if rewrite. The removal is made executable by the new mut_removed_pin.rs (4 must-fail pins). Independent verification: cargo test --workspace 605/0, zero residual mut symbols in any crate source, loop/recur non-regression all green (55 / 500000500000 / infinite-compiles / the lambda_capturing_loop_binder pin), roundtrip_cli PASS. One DONE_WITH_CONCERNS: a 4th recurrence of the recon-undercount class (in-source mod tests + a drift-pin fn + 5 orphaned mut .ail.json carve-outs + a non-enumerated E0599); all resolved within implementer remit, no behaviour change. Milestone-close audit then fieldtest remain. spec docs/specs/2026-05-18-remove-mut-var-assign.md (grounding PASS) plan docs/plans/remove-mut-var-assign.1.md
This commit is contained in:
+6
-54
@@ -2401,26 +2401,6 @@ are real surface forms.
|
||||
// lowers as in-place rewrite under `--alloc=rc`.
|
||||
{ "t": "reuse-as", "source": Term, "body": Term }
|
||||
|
||||
// Iter mut.1: local mutable-state block. `vars` declares zero or
|
||||
// more lexically-scoped mutable bindings (initialised in order);
|
||||
// `body` is a single Term in scope of all vars. The block's static
|
||||
// type is `body`'s static type. The `vars` array stays present in
|
||||
// canonical JSON even when empty (no `skip_serializing_if` —
|
||||
// hash-stable-on-omission is NOT applied here; the field is part of
|
||||
// the shape). See `docs/specs/2026-05-15-mut-local.md`.
|
||||
{ "t": "mut",
|
||||
"vars": [ { "name": "<id>", "type": Type, "init": Term }, ... ],
|
||||
"body": Term }
|
||||
|
||||
// Iter mut.1: in-block update of a mut-var. Legal only as a
|
||||
// sub-term of a `Term::Mut` whose `vars` includes a var with the
|
||||
// same `name`. Static type Unit. The lexical-scope rule is enforced
|
||||
// at typecheck via `mut-assign-out-of-scope`; codegen lowers as a
|
||||
// `store` to the var's entry-block alloca.
|
||||
{ "t": "assign",
|
||||
"name": "<id>",
|
||||
"value": Term }
|
||||
|
||||
// loop-recur iter 1: strict iteration block. `binders` declares
|
||||
// one or more loop parameters (name, type, init), evaluated in
|
||||
// order on loop entry; `body` is in scope of all binders. The
|
||||
@@ -2445,17 +2425,12 @@ In the MVP, `do` is only a direct call to a built-in effect op (no
|
||||
handler). A `lam` term constructs an anonymous function value; free
|
||||
variables of its body are captured from the enclosing scope.
|
||||
|
||||
Both shapes ship in fully-lowered form as of iter mut.3 (2026-05-15):
|
||||
typecheck binds mut-vars in a lexical scope stack, codegen lowers
|
||||
them as entry-block allocas, and `Term::Assign` emits a `store`
|
||||
yielding the canonical Unit SSA. Mut-vars must be of one of the
|
||||
stack-resident scalar types `{Int, Float, Bool, Unit}`; capturing a
|
||||
mut-var into a lambda body is rejected at typecheck via
|
||||
`CheckError::MutVarCapturedByLambda` (iter mut.4-tidy). Loop binders
|
||||
are alloca-resident under the same scheme, so capturing a `loop`
|
||||
binder into a lambda body is rejected by the symmetric
|
||||
`CheckError::LoopBinderCapturedByLambda` (iter loop-recur.tidy). See
|
||||
`docs/specs/2026-05-15-mut-local.md`.
|
||||
Loop binders are alloca-resident: typecheck binds them in the
|
||||
ordinary local scope plus a positional `loop_stack`, and codegen
|
||||
lowers them as entry-block allocas. Capturing a `loop` binder into a
|
||||
lambda body is rejected at typecheck via
|
||||
`CheckError::LoopBinderCapturedByLambda`. See
|
||||
`docs/specs/2026-05-17-loop-recur.md`.
|
||||
|
||||
**`Literal`**:
|
||||
|
||||
@@ -2888,29 +2863,6 @@ 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`.
|
||||
|
||||
**Accumulator-over-iteration shape (documented idiom, not an enforced rule).** `mut`/`var`/`assign` removes friction from *straight-line* and *cascade-of-if* accumulators, but does **not** by itself express the "one running total updated once per loop iteration" shape: a `var` cannot be captured into a lambda (the seal) and there is no loop construct in the language. Until a future iteration-totality milestone lands (gated behind `Nat`/refinement types — see the deferred roadmap entry), the canonical pattern for the per-iteration accumulator is a tail-recursive helper that threads the running total as an explicit parameter; `examples/mut_counter.ail` is the reference. This is a documented authoring idiom, not a typecheck-enforced constraint — wrong code here does not fail to compile, it is merely less ergonomic, which is exactly why this is documentation and not a language rule.
|
||||
|
||||
Pipeline regression smoke tests:
|
||||
|
||||
- `examples/sum.ail.json` → prints 55 (recursion, arithmetic).
|
||||
|
||||
Reference in New Issue
Block a user