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.
This commit is contained in:
@@ -2365,12 +2365,42 @@ are real surface forms.
|
||||
// (typically `ctor` or `lam`); `source` must be a bare `var`. Codegen
|
||||
// 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 (iter mut.2, `mut-assign-out-of-scope`); codegen
|
||||
// relies on it (iter mut.3).
|
||||
{ "t": "assign",
|
||||
"name": "<id>",
|
||||
"value": Term }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Iter mut.1 lands `Term::Mut` and `Term::Assign` as first-class AST
|
||||
nodes that round-trip cleanly between Form A and canonical JSON.
|
||||
Typecheck recognition (iter mut.2) and codegen lowering (iter mut.3)
|
||||
are sequenced as separate iterations; in iter mut.1 the typecheck
|
||||
dispatch (`synth` in `ailang-check`) and codegen dispatch
|
||||
(`lower_term` in `ailang-codegen`) stub these variants with
|
||||
`CheckError::Internal` / `CodegenError::Internal` respectively, per
|
||||
spec `docs/specs/2026-05-15-mut-local.md` §"Iteration mut.1"
|
||||
out-of-iteration boundary.
|
||||
|
||||
**`Literal`**:
|
||||
|
||||
```jsonc
|
||||
|
||||
Reference in New Issue
Block a user