Files
AILang/docs/journals/2026-05-15-iter-mut.1.md
T
Brummel 7b92719244 iter mut.1: AST extension + Form A surface for local mutable state
First iteration of the mut-local milestone (foundation step on the
Stateful-islands roadmap path). Lands the schema + surface tier:
Term::Mut, Term::Assign, and the nested MutVar struct become
first-class AST nodes that round-trip cleanly through Form A.
Typecheck and codegen recognition are deferred to mut.2 and mut.3
per the spec's out-of-iteration boundary; reaching either dispatch
entry point with these variants produces CheckError::Internal /
CodegenError::Internal with a 'deferred to iter mut.{2,3}' message.

Concretely:

- crates/ailang-core/src/ast.rs: two new Term variants behind
  #[serde(tag = 't')]; pub struct MutVar { name, ty, init } adjacent
  to Arm. Two canonical-bytes pin tests for the explicit-empty-vars
  serialisation and the assign round-trip.

- ~25 substantive Term-walker arms across ailang-core/desugar,
  ailang-core/workspace, ailang-check (lib + lift + linearity + mono
  + pre_desugar_validation + reuse_shape + uniqueness),
  ailang-codegen (escape + lambda + lib), ailang-prose, and
  crates/ail/src/main.rs. Universal policy: substantive recurse-into-
  children at every site; only the two dispatch entry points
  (synth in ailang-check, lower_term in ailang-codegen) stub with
  Internal-error. One test-side walker arm in
  crates/ail/tests/codegen_import_map_fallback_pin.rs not
  enumerated by the plan was added as well (defensive recursion).

- ailang-surface: parse_mut + parse_assign helpers; Term::Mut
  body desugared from a flat statement sequence into a right-folded
  Term::Seq chain inside the JSON-AST. Print arms in print.rs match
  the parser convention. EBNF prologue + crates/ailang-core/specs/
  form_a.md productions updated. Four new parser pin tests cover
  the empty-mut, single-var, body-required, and vars-only-no-body
  cases.

- Drift + coverage tests extended: design_schema_drift.rs adds two
  exemplars + match arms; schema_coverage.rs adds two VariantTag
  entries + EXPECTED_VARIANTS + visit_term arms; spec_drift.rs adds
  two exemplars + match arms. DESIGN.md §'Term (expression)' gets
  jsonc-blocked schemas for the two new variants.

- examples/mut.ail: six-fn round-trip fixture exercising empty mut,
  single-var, two-var, nested-shadow, and the four supported scalar
  return types (Int, Float, Bool, Unit). The round_trip auto-glob
  and schema_coverage corpus walker both pick it up.

Plan deviation: the plan named lib.rs:2572 as the typecheck
dispatch stub site, but that line is actually verify_tail_positions
(substantive walker). The real dispatch is synth (3403-area, stub
at 3489); the orchestrator routed correctly.

Tests: 564 → 579 green; cargo build green; round-trip green for
the new fixture; all drift + coverage tests green.

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

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

12 KiB

iter mut.1 — schema + surface for local mutable state

Date: 2026-05-15 Started from: 60e4559e31 Status: DONE Tasks completed: 6 of 6

Summary

Lands the foundational AST + surface forms for the mut-local milestone (first milestone on the Stateful-islands roadmap path). Two new Term variants (Mut and Assign) plus the nested MutVar struct enter ailang-core::ast; canonical-JSON serde round-trips them by virtue of the existing #[serde(tag = "t")] machinery. Form A gains (mut …) / (var …) / (assign …) productions in the parser and the corresponding write arms in the printer; the parser right-folds the trailing body sequence into Term::Seq so the canonical JSON-AST always sees a single body: Term. Every Term exhaustive match in the workspace (~25 walker sites) gains substantive arms, except the two dispatch entry points (synth in ailang-check, lower_term in ailang-codegen) which stub with CheckError::Internal / CodegenError::Internal per the spec's "out of iteration" boundary — typecheck recognition lands in mut.2, codegen lowering in mut.3. The three drift tests (design_schema_drift, spec_drift, schema_coverage) all flip green after the exemplar / variant-tag / visitor extensions, and examples/mut.ail ships as the round-trip fixture covering empty / single-var / two-var / nested-shadow / Bool / Unit cases. Full cargo test --workspace 579/579 green (was 564 + 2 new AST pins + 4 new parser pins + 9 from the extended drift exemplars and the mut.ail-corpus contributions netting to +15).

Per-task notes

  • iter mut.1.1 — AST extension: added Term::Mut { vars: Vec<MutVar>, body: Box<Term> }, Term::Assign { name: String, value: Box<Term> }, and the MutVar { name, ty, init } struct to crates/ailang-core/src/ast.rs. Two new unit tests pin the canonical-bytes of the empty-vars mut serialisation and the Assign round-trip. MutVar carries Arm's derives (Debug, Clone, Serialize, Deserialize) — the plan's literal pseudo-code asked for PartialEq + Eq but its own justification ("derives match Arm's for cross-tree consistency") points at Arm, which does not derive those. Concern recorded; no functional impact (the Task 1 tests do not depend on PartialEq, and Term itself uses a custom PartialEq impl on Type only).

  • iter mut.1.2 — substantive walker arms: 9 sites in desugar.rs, 2 in workspace.rs, 3 in ailang-check/src/lib.rs (1 in substitute_rigids_in_term, 1 in verify_tail_positions, 1 variant-name string emitter inside synth's Term::ReuseAs sub- match), 2 in lift.rs, 3 in linearity.rs (one being a variant-name string emitter), 2 in mono.rs, 1 each in pre_desugar_validation.rs, reuse_shape.rs, uniqueness.rs, 3 in escape.rs, 1 in lambda.rs, 1 in codegen/src/lib.rs (the synth_with_extras type-synthesis helper), 3 in ailang-prose/src/lib.rs, 2 in ail/src/main.rs. Plus one unenumerated site in crates/ail/tests/codegen_import_map_fallback_pin.rs (an exhaustive walker inside a test) that the build required. Each arm follows the appropriate shape per existing convention at its site: rebuilder shapes for substitute_* / subst_* / desugar_term / lift_in_term / substitute_rigids_in_term / rewrite_term, visitor shapes for collect_used_in_term / free_vars_in_term / find_non_callee_use / walk_term* / verify_tail_positions / term_has_letrec / the linearity walkers / walk in reuse_shape.rs / walk in uniqueness.rs / walk in escape.rs / collect_captures / count_free_var / subst_var_with_term (rebuilder-shape in prose). Shadowing semantics across mut-var bindings replicates the Term::Let/Term::Lam/Term::LetRec-shaped convention at each site: var-init terms see the outer scope plus already-declared vars; later var-inits and the body see all earlier var bindings.

  • iter mut.1.3 — dispatch stubs: typecheck dispatch at synth in ailang-check/src/lib.rs (Plan line 2572 was wrong — that line is verify_tail_positions, which is substantive walker territory; the actual dispatch entry is synth at line 3403's former Term::ReuseAs arm, where the two new arms now sit) returns CheckError::Internal("Term::Mut/Assign not yet supported in typecheck (deferred to iter mut.2)"). Codegen dispatch at lower_term in ailang-codegen/src/lib.rs:1649 returns CodegenError::Internal("Term::Mut/Assign not yet supported in codegen (deferred to iter mut.3)"). Both errors use the single-field tuple-variant shape (Internal(String)).

  • iter mut.1.4 — Form A parser + printer: dispatcher arms added for "mut" => parse_mut and "assign" => parse_assign in parse_term's head-keyword match; the two helpers parse positionally (mut: pull leading (var ...) entries, then ≥ 1 body terms right-folded into Term::Seq; assign: positional ident-then-value pair). Plan pseudo-code used helper names (peek_is_open_paren_with_head, expect_head, etc.) that don't exist; substituted the actual API (expect_lparen, expect_keyword, expect_ident, parse_type, parse_term, expect_rparen, peek_head_ident, matches!(self.peek(), Tok::RParen)). Four RED-first parser tests added covering empty mut + 1-var-1-assign-1-final + missing-body rejection (both shapes). Print arms in print.rs walk the right-spine of Term::Seq and emit each lhs as a top-level statement; the terminal expression closes the (mut …) form. EBNF prologue in parse.rs extended with mut-term, var-decl, assign-term productions. The print arms had to land during the Task 1+2 build-unblock phase rather than waiting for Task 4 (they are exhaustive-match neighbours of the synth/lower_term dispatchers); this is a benign sequencing shift and the plan's "Task 2 build- green verification gate" still holds.

  • iter mut.1.5 — drift + coverage extensions: two new exemplars added to design_schema_drift.rs and two new match arms; two new VariantTag entries (TermMut, TermAssign), EXPECTED_VARIANTS extended, two new visit_term arms in schema_coverage.rs; two new exemplars + two new match arms in spec_drift.rs. form_a.md gains three new lines in the Parenthesised-forms block plus a prose paragraph naming the empty-vars and post-vars-body invariants and the diagnostic codes. DESIGN.md §"Term (expression)" gains the two jsonc schema blocks immediately after the reuse-as block plus a closing prose paragraph naming the mut.1 dispatch-stub semantics and the deferred mut.2 / mut.3 iterations. Post-edit: design_schema_drift and spec_drift both green; schema_coverage is RED on the missing examples/mut.ail fixture, per plan Step 6 expectation.

  • iter mut.1.6 — fixture + round-trip: examples/mut.ail shipped with the six fns from plan Step 1 verbatim (empty / single-var with assign / two-var with combine / nested-shadow / Bool / Unit). The corpus-globbing round-trip test (parse_then_print_then_parse_is_idempotent_on_every_ail_fixture) and the parse-determinism test pick the fixture up automatically; both green. schema_coverage also green now that both new variants are observed in the corpus. Full cargo test --workspace 579/579 green.

Concerns

  • MutVar derives diverge from the plan's literal pseudo-code (Debug, Clone, PartialEq, Eq, Serialize, Deserialize); the shipped form is Debug, Clone, Serialize, Deserialize to match Arm's pattern. The plan's own justification ("match Arm's derives for cross-tree consistency") points at Arm which does not derive PartialEq/Eq. Functional impact: none in mut.1 (the Task 1 tests do not depend on PartialEq, and Term uses a custom PartialEq impl scoped to Type only). A future iter that needs to compare MutVar (e.g. for in-place rewriting in mut.3 codegen) can add the derives at that time without breaking any mut.1 test.

  • Plan recon for the typecheck dispatch entry was misindexed. The plan listed crates/ailang-check/src/lib.rs:2572 as the "check_term dispatch — STUBBED" site; that line is actually in verify_tail_positions, a substantive walker. The real typecheck dispatch entry is synth (defined at lib.rs:2613, with its Term::ReuseAs arm at line 3403 before the new mut arms). The stub was placed at the actual dispatch entry; line 2572 received a substantive arm. The variant-name string emitter sub-match referenced as line 3430 is inside synth's former Term::ReuseAs arm and was extended as Task 2 Step 13 asks. Net: the spec intent (stub at typecheck dispatch) is preserved; only the line-number routing differs.

  • One test-side walker site (crates/ail/tests/codegen_import_map_fallback_pin.rs:62) carries an exhaustive match on Term and was not enumerated by the plan's Task 2 file list. The site is the body-walker of a regression-pin test from iter 24.tidy; it required two new arms to build green. Added with appropriate visitor-shape recursion. The plan-recon miss is recorded here as a documentary item; no behavioural impact.

Known debt

  • Prose-side (mut ...) rendering in ailang-prose/src/lib.rs is a minimal-correctness shape (mut { var <name> = <init>; ...; <body> }). The prose surface for mut blocks is not yet fully designed; a future prose-iter once the LLM-author signal arrives will refine it. This is a deliberate placeholder, not drift — recording for visibility.

  • subst_var in desugar.rs does NOT rename the Term::Assign.name field. The desugar pass's subst_var rewrites let-rec captures (KnownType / LetBound / MatchArm), none of which can be mut-vars (mut-vars are first-order scalars, not captureable). Leaving the assign-name untouched is semantically correct; documented in the arm's doc-comment. Mentioned here for visibility.

Files touched

  • crates/ailang-core/src/ast.rsTerm::Mut, Term::Assign, MutVar, two new unit tests
  • crates/ailang-core/src/desugar.rs — 9 walker arms
  • crates/ailang-core/src/workspace.rs — 2 walker arms
  • crates/ailang-check/src/lib.rs — 3 walker arms (substantive + variant-name) + 2 dispatch stubs in synth
  • crates/ailang-check/src/lift.rs — 2 walker arms
  • crates/ailang-check/src/linearity.rs — 3 walker arms (2 substantive + 1 variant-name)
  • crates/ailang-check/src/mono.rs — 2 walker arms (1 mut-ref rebuilder, 1 visitor)
  • crates/ailang-check/src/pre_desugar_validation.rs — 1 walker arm
  • crates/ailang-check/src/reuse_shape.rs — 1 walker arm
  • crates/ailang-check/src/uniqueness.rs — 1 walker arm
  • crates/ailang-codegen/src/escape.rs — 3 walker arms
  • crates/ailang-codegen/src/lambda.rs — 1 walker arm
  • crates/ailang-codegen/src/lib.rs — 1 walker arm (synth_with_extras) + 2 dispatch stubs in lower_term
  • crates/ailang-prose/src/lib.rs — 3 walker arms (1 substantive, 1 count_free_var, 1 subst_var_with_term rebuilder)
  • crates/ail/src/main.rs — 2 walker arms (dep-walker + rewrite_term)
  • crates/ail/tests/codegen_import_map_fallback_pin.rs — 1 walker arm (test-side, unenumerated by plan)
  • crates/ailang-surface/src/parse.rs — dispatcher arms, parse_mut / parse_assign helpers, EBNF prologue extension, 4 new parser tests
  • crates/ailang-surface/src/print.rs — 2 write_term arms
  • crates/ailang-core/tests/design_schema_drift.rs — 2 exemplars + 2 match arms
  • crates/ailang-core/tests/schema_coverage.rs — 2 VariantTag + EXPECTED_VARIANTS + 2 visit_term arms
  • crates/ailang-core/tests/spec_drift.rs — 2 exemplars + 2 match arms
  • crates/ailang-core/specs/form_a.md — 3 new productions + prose paragraph
  • docs/DESIGN.md — 2 jsonc schema blocks + prose paragraph
  • examples/mut.ail — round-trip fixture (6 fns)

Stats

bench/orchestrator-stats/2026-05-15-iter-mut.1.json