832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
1131 lines
40 KiB
Markdown
1131 lines
40 KiB
Markdown
# mut.1 — schema + surface — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0029-mut-local.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** Land the AST extension for `Term::Mut`, `Term::Assign`, and
|
|
the nested `MutVar` struct; wire canonical-JSON serde; add Form A
|
|
`(mut …)` / `(var …)` / `(assign …)` productions in the surface parser
|
|
and printer; extend the four drift / coverage tests; amend
|
|
DESIGN.md §"Term (expression)" and `crates/ailang-core/specs/form_a.md`
|
|
with the new shapes; and add `examples/mut.ail` as the round-trip
|
|
fixture. After this iteration, `Term::Mut` and `Term::Assign` exist as
|
|
first-class AST nodes that round-trip cleanly between Form A text and
|
|
canonical JSON, but typecheck still rejects them with an internal-error
|
|
stub (typecheck recognition lands in mut.2).
|
|
|
|
**Architecture:** Two new `Term` variants behind the existing
|
|
`#[serde(tag = "t")]` machinery — no per-variant serde wiring required.
|
|
A new `pub struct MutVar { name, ty, init }` adjacent to `Arm` in
|
|
`ast.rs`. Form A surface gains three productions parsed positionally
|
|
(`(mut …)` peels leading `(var …)` entries then right-folds the
|
|
remaining body sequence into `Term::Seq`; `(assign N V)` is a fixed
|
|
positional pair). Every `Term` exhaustive `match` in the workspace
|
|
(~25 sites carrying a `Term::ReuseAs` arm today) gains a new arm —
|
|
substantive (structural recurse) for every site except the two
|
|
dispatch entry points (`check_term` in `ailang-check/src/lib.rs` and
|
|
`lower_term` in `ailang-codegen/src/lib.rs`), which stub with
|
|
`CheckError::Internal` / `CodegenError::Internal` per the spec's
|
|
"out of iteration" boundary.
|
|
|
|
**Tech Stack:** `ailang-core` (AST + serde + drift tests),
|
|
`ailang-surface` (parse + print), `ailang-check` (walker arms +
|
|
typecheck dispatch stub), `ailang-codegen` (walker arms + codegen
|
|
dispatch stub), `ailang-prose` (walker arms), `ail` (CLI walker arms).
|
|
|
|
---
|
|
|
|
**Files this plan creates or modifies:**
|
|
|
|
- Create: `examples/mut.ail` — round-trip fixture (empty mut, single-
|
|
var, two-var, nested, scalar return-type variants).
|
|
- Modify: `crates/ailang-core/src/ast.rs:386-525` — `Term` enum
|
|
extension + new `MutVar` struct.
|
|
- Modify: `crates/ailang-core/src/desugar.rs` — nine `Term::ReuseAs`-
|
|
adjacent walker arms gain substantive `Term::Mut` + `Term::Assign`
|
|
arms at lines 340, 531, 1154, 1298, 1419, 1477, 1519, 1548, 2660.
|
|
- Modify: `crates/ailang-core/src/workspace.rs:1245, 1367` — two
|
|
walker arms (one of them visits embedded types in `MutVar.ty`).
|
|
- Modify: `crates/ailang-core/specs/form_a.md:253-300` — add `(mut …)`,
|
|
`(var …)`, `(assign …)` productions.
|
|
- Modify: `crates/ailang-surface/src/parse.rs:1183-1254, 1525-1566` —
|
|
dispatcher + two new production helpers `parse_mut` / `parse_assign`,
|
|
plus the EBNF prologue at `parse.rs:7-86`.
|
|
- Modify: `crates/ailang-surface/src/print.rs:401-548` — two new arms
|
|
in `write_term` (exhaustive match).
|
|
- Modify: `crates/ailang-check/src/lib.rs:242, 2572, 3403, 3430` —
|
|
four exhaustive-match sites; line 242 (`substitute_rigids_in_term`),
|
|
line 2572 (`check_term` dispatch — STUBBED to return
|
|
`CheckError::Internal`), line 3403 (deep walker), line 3430
|
|
(variant-name string emitter).
|
|
- Modify: `crates/ailang-check/src/lift.rs:373, 731` — two walker arms.
|
|
- Modify: `crates/ailang-check/src/linearity.rs:555, 778, 900` —
|
|
three walker arms.
|
|
- Modify: `crates/ailang-check/src/mono.rs:1220, 1576` — two walker
|
|
arms.
|
|
- Modify: `crates/ailang-check/src/pre_desugar_validation.rs:119` —
|
|
one walker arm.
|
|
- Modify: `crates/ailang-check/src/reuse_shape.rs:248` — one walker
|
|
arm.
|
|
- Modify: `crates/ailang-check/src/uniqueness.rs:329` — one walker
|
|
arm.
|
|
- Modify: `crates/ailang-codegen/src/escape.rs:187, 358, 469` —
|
|
three walker arms.
|
|
- Modify: `crates/ailang-codegen/src/lambda.rs:428` — one walker arm.
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:1649, 2909` — line 1649
|
|
(`lower_term` dispatch — STUBBED to return `CodegenError::Internal`),
|
|
line 2909 (drop emitter — substantive).
|
|
- Modify: `crates/ailang-prose/src/lib.rs:885, 1079, 1187` — three
|
|
walker arms.
|
|
- Modify: `crates/ail/src/main.rs:1510, 2703` — two walker arms.
|
|
- Modify: `crates/ailang-core/tests/design_schema_drift.rs:43-166` —
|
|
two new exemplars + two new match arms.
|
|
- Modify: `crates/ailang-core/tests/schema_coverage.rs:28-232` —
|
|
two new `VariantTag` entries + `EXPECTED_VARIANTS` extension + two
|
|
new `visit_term` arms.
|
|
- Modify: `crates/ailang-core/tests/spec_drift.rs:90-145` — two new
|
|
exemplars + two new match arms.
|
|
- Modify: `docs/DESIGN.md:2318-2368` — append jsonc schemas for
|
|
`Term::Mut` and `Term::Assign` after the `reuse-as` block (line 2364);
|
|
add the prose paragraph naming the iteration that introduces them.
|
|
- Test: `crates/ailang-core/src/ast.rs` unit tests (new) — canonical-
|
|
bytes pin for empty-`vars` `Term::Mut` serialisation.
|
|
- Test: `crates/ailang-surface/src/parse.rs` unit tests (new) —
|
|
positive parses for `(mut)` + `(assign)` shapes; negative parse for
|
|
`(mut)` with no body.
|
|
|
|
---
|
|
|
|
## Task 1 — AST extension: `Term::Mut`, `Term::Assign`, `MutVar`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/ast.rs:386-525`
|
|
|
|
**Files: write a failing test first.** Pin canonical-bytes for the
|
|
empty-`vars` case (the spec's deliberate non-omission).
|
|
|
|
- [ ] **Step 1: Add the canonical-bytes pin to `ast.rs`**
|
|
|
|
In `#[cfg(test)] mod tests` near the end of `crates/ailang-core/src/ast.rs`,
|
|
add:
|
|
|
|
```rust
|
|
#[test]
|
|
fn term_mut_empty_vars_serialises_with_explicit_vars_field() {
|
|
// Spec 2026-05-15-mut-local §"Canonical-form invariants":
|
|
// the `vars` array stays present rather than being omitted.
|
|
let t = Term::Mut {
|
|
vars: Vec::new(),
|
|
body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
};
|
|
let bytes = serde_json::to_string(&t).expect("serialise");
|
|
assert_eq!(
|
|
bytes,
|
|
r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn term_assign_round_trips_through_json() {
|
|
let t = Term::Assign {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 42 } }),
|
|
};
|
|
let bytes = serde_json::to_string(&t).expect("serialise");
|
|
assert_eq!(
|
|
bytes,
|
|
r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":42}}}"#,
|
|
);
|
|
let back: Term = serde_json::from_str(&bytes).expect("deserialise");
|
|
match back {
|
|
Term::Assign { name, value } => {
|
|
assert_eq!(name, "x");
|
|
match *value {
|
|
Term::Lit { lit: Literal::Int { value: 42 } } => {}
|
|
_ => panic!("inner literal mismatch"),
|
|
}
|
|
}
|
|
_ => panic!("variant mismatch"),
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run to verify red**
|
|
|
|
Run: `cargo test --workspace -p ailang-core term_mut_empty_vars_serialises_with_explicit_vars_field`
|
|
Expected: compile FAIL with `error[E0599]: no variant or associated item named Mut found for enum Term`.
|
|
|
|
- [ ] **Step 3: Add the two variants and the `MutVar` struct to `ast.rs`**
|
|
|
|
Locate the `Term` enum at `crates/ailang-core/src/ast.rs:386-513`.
|
|
After the existing `Term::ReuseAs` variant (currently ending around
|
|
line 512), and before the closing `}` of the enum, insert:
|
|
|
|
```rust
|
|
/// Iter mut.1: local mutable-state block. `vars` declares zero or
|
|
/// more lexically-scoped mutable bindings (initialised in order);
|
|
/// `body` is a single Term evaluated in scope of all vars. The
|
|
/// block's static type is `body`'s static type. `vars` stays
|
|
/// present in canonical JSON even when empty.
|
|
Mut {
|
|
vars: Vec<MutVar>,
|
|
body: Box<Term>,
|
|
},
|
|
|
|
/// Iter mut.1: in-block update of a mut-var. Only legal as a
|
|
/// sub-term of a `Term::Mut` whose `vars` includes a var with the
|
|
/// same `name`. Static type is Unit.
|
|
Assign {
|
|
name: String,
|
|
value: Box<Term>,
|
|
},
|
|
```
|
|
|
|
Then, adjacent to the existing `pub struct Arm { ... }` (around line
|
|
515-525), insert:
|
|
|
|
```rust
|
|
/// Iter mut.1: a mutable binding inside `Term::Mut`. Lives as a
|
|
/// nested field of `Term::Mut`, not as a standalone Term variant —
|
|
/// mut-vars are not first-class values.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct MutVar {
|
|
pub name: String,
|
|
#[serde(rename = "type")]
|
|
pub ty: Type,
|
|
pub init: Term,
|
|
}
|
|
```
|
|
|
|
The `#[serde(rename = "type")]` matches the spec's JSON-AST schema
|
|
(`{ "name": "<id>", "type": Type, "init": Term }`). The struct's
|
|
derives match `Arm`'s for cross-tree consistency.
|
|
|
|
- [ ] **Step 4: Run to verify green for the canonical-bytes test, while red for downstream walker sites**
|
|
|
|
Run: `cargo test --workspace -p ailang-core term_mut_empty_vars_serialises_with_explicit_vars_field`
|
|
Expected: PASS (this test does not touch downstream walkers).
|
|
|
|
Run: `cargo build --workspace 2>&1 | head -40`
|
|
Expected: many `error[E0004]: non-exhaustive patterns: &Term::Mut { .. } not covered` errors across `ailang-core::desugar`, `ailang-check`, `ailang-codegen`, `ailang-surface`, `ailang-prose`, `ail`. These are the walker sites Task 2 and Task 3 fix.
|
|
|
|
---
|
|
|
|
## Task 2 — Substantive walker arms in pre-typecheck + helper crates
|
|
|
|
Adds `Term::Mut` and `Term::Assign` arms to every `Term` exhaustive
|
|
match outside the two dispatch entry points. Each arm structurally
|
|
recurses; no behaviour changes for existing Term variants.
|
|
|
|
The shape per arm depends on whether the walker rebuilds a `Term`
|
|
tree (`substitute_*` family — returns a new Term) or only visits
|
|
(linearity / coverage walkers — returns `()`).
|
|
|
|
**Rebuilder shape** (for sites that map `Term → Term`):
|
|
|
|
```rust
|
|
Term::Mut { vars, body } => Term::Mut {
|
|
vars: vars.iter().map(|v| MutVar {
|
|
name: v.name.clone(),
|
|
ty: walk_ty(&v.ty, /* whatever context */),
|
|
init: walk(&v.init, /* whatever context */),
|
|
}).collect(),
|
|
body: Box::new(walk(body, /* whatever context */)),
|
|
},
|
|
Term::Assign { name, value } => Term::Assign {
|
|
name: name.clone(),
|
|
value: Box::new(walk(value, /* whatever context */)),
|
|
},
|
|
```
|
|
|
|
**Visitor shape** (for sites that walk without rebuilding):
|
|
|
|
```rust
|
|
Term::Mut { vars, body } => {
|
|
for v in vars {
|
|
visit_ty(&v.ty);
|
|
visit(&v.init);
|
|
}
|
|
visit(body);
|
|
}
|
|
Term::Assign { value, .. } => {
|
|
visit(value);
|
|
}
|
|
```
|
|
|
|
Some sites visit types separately; others don't. The implementer
|
|
matches the existing arm conventions at each site (e.g. if the
|
|
existing `Term::ReuseAs` arm at the site only visits `source` and
|
|
`body` without touching types, the new `Term::Mut` arm visits
|
|
`vars[i].init` and `body` without `visit_ty` — same convention).
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/desugar.rs` — nine walker arms.
|
|
- Modify: `crates/ailang-core/src/workspace.rs` — two walker arms
|
|
(one of them visits embedded types; see the `walk_term_embedded_types`
|
|
call at line 1245 — that one must visit `vars[i].ty`).
|
|
- Modify: `crates/ailang-check/src/lib.rs` lines 242, 3403, 3430 —
|
|
three of the four sites in this file (line 2572 is the dispatch
|
|
stub handled in Task 3).
|
|
- Modify: `crates/ailang-check/src/lift.rs` lines 373, 731.
|
|
- Modify: `crates/ailang-check/src/linearity.rs` lines 555, 778, 900.
|
|
- Modify: `crates/ailang-check/src/mono.rs` lines 1220, 1576.
|
|
- Modify: `crates/ailang-check/src/pre_desugar_validation.rs` line 119.
|
|
- Modify: `crates/ailang-check/src/reuse_shape.rs` line 248.
|
|
- Modify: `crates/ailang-check/src/uniqueness.rs` line 329.
|
|
- Modify: `crates/ailang-codegen/src/escape.rs` lines 187, 358, 469.
|
|
- Modify: `crates/ailang-codegen/src/lambda.rs` line 428.
|
|
- Modify: `crates/ailang-codegen/src/lib.rs` line 2909 (the second
|
|
site in this file; line 1649 is the codegen dispatch stub in
|
|
Task 3).
|
|
- Modify: `crates/ailang-prose/src/lib.rs` lines 885, 1079, 1187.
|
|
- Modify: `crates/ail/src/main.rs` lines 1510, 2703.
|
|
|
|
**Site-by-site disposition:**
|
|
|
|
- [ ] **Step 1: `desugar.rs:340` — `collect_used_in_term`**
|
|
|
|
Visitor-shape arm. Recurse into `vars[i].init` and `body` for
|
|
`Term::Mut`; recurse into `value` for `Term::Assign`. No types visited
|
|
(this walker tracks Var usages only).
|
|
|
|
- [ ] **Step 2: `desugar.rs:531` — `subst_var`**
|
|
|
|
Rebuilder-shape arm. The walker substitutes `name → replacement`
|
|
inside a Term. For `Term::Mut`: substitute inside each var's `init`
|
|
and the `body`; var *declarations* are not Var references and do not
|
|
get rewritten. For `Term::Assign`: substitute inside `value`. The
|
|
`name` field of `Term::Assign` is a Var-declaration reference (not
|
|
a Var-use); the spec treats var declarations as fixed by lexical
|
|
position, so this walker does NOT rename them.
|
|
|
|
- [ ] **Step 3: `desugar.rs:1154` — free-vars walker**
|
|
|
|
Visitor-shape returning a `BTreeSet<String>`. For `Term::Mut`: union
|
|
the free vars of each `vars[i].init` plus the body's free vars,
|
|
then **subtract the var names** (they are bound). For `Term::Assign`:
|
|
the value's free vars plus the assigned `name` itself (the var being
|
|
assigned is a *use* of the binding, so it counts as a free var
|
|
unless bound by an enclosing `Term::Mut`).
|
|
|
|
- [ ] **Step 4: `desugar.rs:1298` — `subst_call_with_extras`**
|
|
|
|
Rebuilder-shape. Same as Step 2 but inside a more involved context
|
|
(extra args). Match the `Term::ReuseAs` arm's structure at this site
|
|
and apply to `Term::Mut`/`Term::Assign`.
|
|
|
|
- [ ] **Step 5: `desugar.rs:1419` — analogous rewriter**
|
|
|
|
Rebuilder-shape. Per the recon, this is the analogous companion of
|
|
Step 4. Same pattern.
|
|
|
|
- [ ] **Step 6: `desugar.rs:1477` — `find_non_callee_use`**
|
|
|
|
Visitor-shape. Detects whether a name appears in non-callee position.
|
|
For `Term::Mut`: walk children. For `Term::Assign`: walk `value`; the
|
|
assigned `name` itself is a non-callee use (mut-vars cannot be in
|
|
callee position because their type is restricted to scalars in
|
|
mut.2 — but this site cannot rely on mut.2; for now, treat `name`
|
|
as a use to keep the conservative-safe semantics).
|
|
|
|
- [ ] **Step 7: `desugar.rs:1519` and `desugar.rs:1548` — `any_nested_ctor` and `any_let_rec`**
|
|
|
|
Visitor-shape returning `bool`. For `Term::Mut`: any var init or the
|
|
body. For `Term::Assign`: the value.
|
|
|
|
- [ ] **Step 8: `desugar.rs:2660` — `any_lit_pattern`**
|
|
|
|
Visitor-shape returning `bool`. Same pattern as Step 7.
|
|
|
|
- [ ] **Step 9: `workspace.rs:1245` — `walk_term_embedded_types`**
|
|
|
|
Visitor-shape that visits every `Type` embedded in a Term tree.
|
|
For `Term::Mut`: visit each `vars[i].ty` plus recurse into init and
|
|
body. For `Term::Assign`: recurse into `value` (no embedded types).
|
|
|
|
- [ ] **Step 10: `workspace.rs:1367` — generic Term walker**
|
|
|
|
Visitor-shape per the existing arms at this site.
|
|
|
|
- [ ] **Step 11: `ailang-check/src/lib.rs:242` — `substitute_rigids_in_term`**
|
|
|
|
Rebuilder-shape. For `Term::Mut`: rebuild with each `vars[i].ty`
|
|
substituted via `substitute_rigids_in_type`, each `init` via
|
|
`substitute_rigids_in_term`, and `body` rebuilt. For `Term::Assign`:
|
|
rebuild with `value` recursed.
|
|
|
|
- [ ] **Step 12: `ailang-check/src/lib.rs:3403` — deep walker**
|
|
|
|
Match the existing arm convention at this site. Inspect
|
|
`substitute_rigids_in_term` (Step 11) for the canonical rebuilder
|
|
shape if rebuilding; if visiting-only, use the visitor shape.
|
|
|
|
- [ ] **Step 13: `ailang-check/src/lib.rs:3430` — variant-name string emitter**
|
|
|
|
The existing `Term::ReuseAs { .. } => "reuse-as"` arm at line 3430
|
|
indicates a `Term → &'static str` walker. Add:
|
|
|
|
```rust
|
|
Term::Mut { .. } => "mut",
|
|
Term::Assign { .. } => "assign",
|
|
```
|
|
|
|
- [ ] **Step 14: `ailang-check/src/lift.rs:373` — letrec lift walker (rebuilder)**
|
|
|
|
Rebuilder-shape. Same pattern as Step 11.
|
|
|
|
- [ ] **Step 15: `ailang-check/src/lift.rs:731` — `term_has_letrec`**
|
|
|
|
Visitor-shape returning `bool`. For `Term::Mut`: any `vars[i].init`
|
|
or body. For `Term::Assign`: the value.
|
|
|
|
- [ ] **Step 16: `ailang-check/src/linearity.rs:555` — linearity walker**
|
|
|
|
Visitor-shape per the existing arms at this site. Mut-vars are not
|
|
linear values (they're alloca slots, not RC-managed) but the walker
|
|
should still recurse into `init` / `body` / `value` to track any
|
|
linearity-relevant operations inside them.
|
|
|
|
- [ ] **Step 17: `ailang-check/src/linearity.rs:778` — analogous**
|
|
|
|
Same pattern as Step 16.
|
|
|
|
- [ ] **Step 18: `ailang-check/src/linearity.rs:900` — analogous**
|
|
|
|
Same pattern as Step 16.
|
|
|
|
- [ ] **Step 19: `ailang-check/src/mono.rs:1220` — monomorphiser walker**
|
|
|
|
Rebuilder-shape. Substitute types in `vars[i].ty` using the existing
|
|
mono substitution; recurse into init and body. For Term::Assign:
|
|
recurse into value.
|
|
|
|
- [ ] **Step 20: `ailang-check/src/mono.rs:1576` — second mono walker**
|
|
|
|
Same as Step 19.
|
|
|
|
- [ ] **Step 21: `ailang-check/src/pre_desugar_validation.rs:119`**
|
|
|
|
Visitor-shape. Whatever invariant the existing arms validate, apply
|
|
to the new variants by recursing into children. Mut-vars do not
|
|
participate in pre-desugar validation (they don't exist yet in
|
|
nested-ctor / letrec patterns); a simple recurse-children arm
|
|
suffices.
|
|
|
|
- [ ] **Step 22: `ailang-check/src/reuse_shape.rs:248`**
|
|
|
|
Visitor-shape. The existing `Term::ReuseAs` arm at this site is the
|
|
load-bearing case for reuse-shape inference; `Term::Mut`/`Term::Assign`
|
|
do not participate (no allocating-ctor inside mut-vars in mut.1
|
|
because var types are scalar). Arm: recurse into children for
|
|
defensive depth, but emit no reuse-shape information.
|
|
|
|
- [ ] **Step 23: `ailang-check/src/uniqueness.rs:329`**
|
|
|
|
Visitor-shape. Mut-vars are not RC-managed values in mut.1; the
|
|
uniqueness analyser treats them as not-RC-tracked. Arm: recurse into
|
|
init/body/value children.
|
|
|
|
- [ ] **Step 24: `ailang-codegen/src/escape.rs:187` — escape walker A**
|
|
|
|
Visitor-shape. Mut-vars are alloca-resident by spec (§"Codegen") and
|
|
do not introduce escape edges. Arm: recurse into init/body/value
|
|
children. The walker should NOT mark any allocation site inside the
|
|
mut-var tree as escaping just because it lives inside a mut block —
|
|
the recursion preserves whatever escape decision the inner Terms
|
|
make on their own.
|
|
|
|
- [ ] **Step 25: `ailang-codegen/src/escape.rs:358` — escape walker B**
|
|
|
|
Same as Step 24.
|
|
|
|
- [ ] **Step 26: `ailang-codegen/src/escape.rs:469` — escape walker C**
|
|
|
|
Same as Step 24.
|
|
|
|
- [ ] **Step 27: `ailang-codegen/src/lambda.rs:428`**
|
|
|
|
Visitor-shape. Mut-vars cannot be captured by lambdas in mut.1
|
|
(mut.2 enforces this via the typecheck pass; for now the codegen
|
|
arm just recurses children defensively).
|
|
|
|
- [ ] **Step 28: `ailang-codegen/src/lib.rs:2909` — drop emitter**
|
|
|
|
Visitor-shape per the existing arms at this site (the drop emitter
|
|
walks Term values to discover drop points for RC types). Mut-vars
|
|
are scalar in mut.1, so no drops are emitted for them; the arm
|
|
recurses into children.
|
|
|
|
- [ ] **Step 29: `ailang-prose/src/lib.rs:885` — prose walker A**
|
|
|
|
Rebuilder or visitor shape per the existing arms. Prose is a pretty-
|
|
printer; the new arms should print a minimal but valid prose form
|
|
or, if the prose surface is not yet ready for mut/assign, emit a
|
|
placeholder text (e.g. `"<mut-block>"`). Prose-projection coverage
|
|
is not a gate for mut.1; minimal-correctness is enough.
|
|
|
|
- [ ] **Step 30: `ailang-prose/src/lib.rs:1079` — prose walker B**
|
|
|
|
Same as Step 29.
|
|
|
|
- [ ] **Step 31: `ailang-prose/src/lib.rs:1187` — prose walker C**
|
|
|
|
Same as Step 29.
|
|
|
|
- [ ] **Step 32: `ail/src/main.rs:1510` — CLI describe / prose-merge A**
|
|
|
|
Whatever the existing `Term::ReuseAs` arm does at this site, apply
|
|
analogously. Likely a visitor for `ail describe` or similar; recurse
|
|
children.
|
|
|
|
- [ ] **Step 33: `ail/src/main.rs:2703` — CLI describe / prose-merge B**
|
|
|
|
Same as Step 32.
|
|
|
|
- [ ] **Step 34: Verify build green after Tasks 2's body**
|
|
|
|
Note: this verification is the gate for Task 2's completion **after**
|
|
Task 3 lands. The compiler still complains about the two dispatch
|
|
stubs (Task 3) — proceed to Task 3 before re-running.
|
|
|
|
---
|
|
|
|
## Task 3 — Dispatch-entry stubs in `ailang-check` and `ailang-codegen`
|
|
|
|
Two sites stub `Term::Mut` and `Term::Assign` with an internal-error
|
|
return per the spec §"Iteration mut.1" out-of-iteration boundary.
|
|
After this task plus Task 2, the workspace builds green.
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-check/src/lib.rs:2572` — `check_term` dispatch.
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:1649` — `lower_term` dispatch.
|
|
|
|
- [ ] **Step 1: `check_term` dispatch stub at `lib.rs:2572`**
|
|
|
|
The `Term::ReuseAs` arm at this site is the canonical typechecker
|
|
treatment. Add immediately after it:
|
|
|
|
```rust
|
|
Term::Mut { .. } => {
|
|
return Err(CheckError::Internal {
|
|
message: "Term::Mut not yet supported in typecheck (deferred to iter mut.2)".into(),
|
|
});
|
|
}
|
|
Term::Assign { .. } => {
|
|
return Err(CheckError::Internal {
|
|
message: "Term::Assign not yet supported in typecheck (deferred to iter mut.2)".into(),
|
|
});
|
|
}
|
|
```
|
|
|
|
(The exact `CheckError::Internal` field name may be `msg` or
|
|
`message` — verify against the existing variant declaration around
|
|
line 620 and match it.)
|
|
|
|
- [ ] **Step 2: `lower_term` dispatch stub at `codegen/src/lib.rs:1649`**
|
|
|
|
The `Term::ReuseAs` arm at this site is the canonical codegen
|
|
treatment. Add immediately after it:
|
|
|
|
```rust
|
|
Term::Mut { .. } => {
|
|
return Err(CodegenError::Internal(
|
|
"Term::Mut not yet supported in codegen (deferred to iter mut.3)".into(),
|
|
));
|
|
}
|
|
Term::Assign { .. } => {
|
|
return Err(CodegenError::Internal(
|
|
"Term::Assign not yet supported in codegen (deferred to iter mut.3)".into(),
|
|
));
|
|
}
|
|
```
|
|
|
|
(Verify the exact `CodegenError::Internal` shape — single-field
|
|
tuple variant `Internal(String)` vs. struct variant — and match.)
|
|
|
|
- [ ] **Step 3: Verify build green**
|
|
|
|
Run: `cargo build --workspace 2>&1 | tail -20`
|
|
Expected: no errors. (Warnings about unused fields in the new arms
|
|
are acceptable and will retire when typecheck / codegen land in
|
|
mut.2 / mut.3.)
|
|
|
|
- [ ] **Step 4: Verify the AST tests still pass**
|
|
|
|
Run: `cargo test --workspace -p ailang-core ast::tests`
|
|
Expected: PASS — including the two canonical-bytes tests from
|
|
Task 1.
|
|
|
|
---
|
|
|
|
## Task 4 — Form A surface: parser + printer
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/parse.rs:7-86, 1183-1254, 1525-1566`
|
|
- Modify: `crates/ailang-surface/src/print.rs:401-548`
|
|
|
|
- [ ] **Step 1: Write the failing parser test for the empty-mut form**
|
|
|
|
Add to the `#[cfg(test)] mod tests` block at the end of
|
|
`crates/ailang-surface/src/parse.rs`:
|
|
|
|
```rust
|
|
#[test]
|
|
fn parses_empty_mut_with_int_body() {
|
|
let src = "(mut 0)";
|
|
let t: Term = parse_term(src).expect("parse");
|
|
match t {
|
|
Term::Mut { vars, body } => {
|
|
assert!(vars.is_empty(), "vars should be empty");
|
|
match *body {
|
|
Term::Lit { lit: Literal::Int { value: 0 } } => {}
|
|
other => panic!("expected Lit Int 0, got {:?}", other),
|
|
}
|
|
}
|
|
other => panic!("expected Term::Mut, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_mut_with_one_var_and_one_assign_and_final() {
|
|
let src = "(mut (var x (con Int) 0) (assign x (app + x 1)) x)";
|
|
let t: Term = parse_term(src).expect("parse");
|
|
match t {
|
|
Term::Mut { vars, body } => {
|
|
assert_eq!(vars.len(), 1);
|
|
assert_eq!(vars[0].name, "x");
|
|
// Body should be a Seq of (assign x ...) and x.
|
|
match *body {
|
|
Term::Seq { lhs, rhs } => {
|
|
match *lhs {
|
|
Term::Assign { name, .. } => assert_eq!(name, "x"),
|
|
other => panic!("expected Assign, got {:?}", other),
|
|
}
|
|
match *rhs {
|
|
Term::Var { name } => assert_eq!(name, "x"),
|
|
other => panic!("expected Var x, got {:?}", other),
|
|
}
|
|
}
|
|
other => panic!("expected Seq, got {:?}", other),
|
|
}
|
|
}
|
|
other => panic!("expected Term::Mut, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_mut_with_no_body() {
|
|
let src = "(mut)";
|
|
let err = parse_term(src).expect_err("must reject empty mut");
|
|
let msg = format!("{}", err);
|
|
assert!(msg.contains("mut"), "error mentions mut: {msg}");
|
|
assert!(msg.contains("body") || msg.contains("expression"),
|
|
"error mentions missing body: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_mut_with_only_vars_no_final_expression() {
|
|
let src = "(mut (var x (con Int) 0))";
|
|
let err = parse_term(src).expect_err("must reject vars-only mut");
|
|
let msg = format!("{}", err);
|
|
assert!(msg.contains("body") || msg.contains("expression"),
|
|
"error mentions missing body: {msg}");
|
|
}
|
|
```
|
|
|
|
The helper `parse_term` for tests should mirror the convention used
|
|
by the existing `Term::Clone` / `Term::ReuseAs` test set in the same
|
|
file (see lines 1769-1908). If `parse_term` does not exist with that
|
|
exact name, wrap the existing test-entry function (`parse_module` →
|
|
extract single term) the same way prior tests do.
|
|
|
|
- [ ] **Step 2: Run to verify red**
|
|
|
|
Run: `cargo test --workspace -p ailang-surface parses_empty_mut_with_int_body`
|
|
Expected: FAIL — parser does not recognise `mut` head.
|
|
|
|
- [ ] **Step 3: Add `parse_mut` and `parse_assign` helpers**
|
|
|
|
Locate the term-head dispatcher around `crates/ailang-surface/src/parse.rs:1194-1221`.
|
|
After the `"reuse-as"` arm, add:
|
|
|
|
```rust
|
|
"mut" => self.parse_mut(span)?,
|
|
"assign" => self.parse_assign(span)?,
|
|
```
|
|
|
|
Adjacent to the existing `parse_reuse_as` helper around line 1531,
|
|
add the two new helpers:
|
|
|
|
```rust
|
|
fn parse_mut(&mut self, head_span: Span) -> Result<Term, ParseError> {
|
|
// (mut (var NAME TYPE INIT)* BODY_STMT* FINAL_EXPR)
|
|
//
|
|
// Reads zero or more (var ...) entries off the front, then 1+
|
|
// trailing terms; the trailing terms are right-folded into
|
|
// Term::Seq with the final term as the seed.
|
|
|
|
let mut vars: Vec<MutVar> = Vec::new();
|
|
let mut body_stmts: Vec<Term> = Vec::new();
|
|
|
|
while self.peek_is_open_paren_with_head("var") {
|
|
let var_span = self.expect_open_paren()?;
|
|
self.expect_head("var")?;
|
|
let name = self.parse_ident()?;
|
|
let ty = self.parse_type()?;
|
|
let init = self.parse_term()?;
|
|
self.expect_close_paren(var_span)?;
|
|
vars.push(MutVar { name, ty, init });
|
|
}
|
|
|
|
while !self.peek_is_close_paren() {
|
|
body_stmts.push(self.parse_term()?);
|
|
}
|
|
|
|
if body_stmts.is_empty() {
|
|
return Err(ParseError::at(
|
|
head_span,
|
|
"(mut ...) requires at least one body expression after vars",
|
|
));
|
|
}
|
|
|
|
// Right-fold: [s1, s2, ..., sN, final] becomes Seq(s1, Seq(s2, ... Seq(sN, final)))
|
|
let mut body = body_stmts.pop().expect("non-empty after the check above");
|
|
while let Some(s) = body_stmts.pop() {
|
|
body = Term::Seq { lhs: Box::new(s), rhs: Box::new(body) };
|
|
}
|
|
|
|
Ok(Term::Mut {
|
|
vars,
|
|
body: Box::new(body),
|
|
})
|
|
}
|
|
|
|
fn parse_assign(&mut self, _head_span: Span) -> Result<Term, ParseError> {
|
|
// (assign NAME VALUE)
|
|
let name = self.parse_ident()?;
|
|
let value = self.parse_term()?;
|
|
Ok(Term::Assign {
|
|
name,
|
|
value: Box::new(value),
|
|
})
|
|
}
|
|
```
|
|
|
|
The exact names of the helper methods on `self` (`peek_is_open_paren_with_head`,
|
|
`expect_open_paren`, `expect_head`, `parse_ident`, `parse_type`,
|
|
`parse_term`, `peek_is_close_paren`, `expect_close_paren`,
|
|
`ParseError::at`) must match the existing parser's API. Inspect
|
|
`parse_reuse_as` (around line 1531) and `parse_seq` (around line 1386)
|
|
to read off the canonical names; substitute as needed.
|
|
|
|
- [ ] **Step 4: Run to verify parser tests pass**
|
|
|
|
Run: `cargo test --workspace -p ailang-surface parses_empty_mut_with_int_body parses_mut_with_one_var_and_one_assign_and_final rejects_mut_with_no_body rejects_mut_with_only_vars_no_final_expression`
|
|
Expected: ALL PASS.
|
|
|
|
- [ ] **Step 5: Add print arms to `print.rs`**
|
|
|
|
Locate `write_term` around `crates/ailang-surface/src/print.rs:401`.
|
|
The exhaustive match has a `Term::ReuseAs` arm around line 540-548
|
|
matching the source structure. After it, add:
|
|
|
|
```rust
|
|
Term::Mut { vars, body } => {
|
|
self.write_str("(mut")?;
|
|
for v in vars {
|
|
self.write_str(" (var ")?;
|
|
self.write_str(&v.name)?;
|
|
self.write_str(" ")?;
|
|
self.write_type(&v.ty)?;
|
|
self.write_str(" ")?;
|
|
self.write_term(&v.init)?;
|
|
self.write_str(")")?;
|
|
}
|
|
// Body is a Term — may be Seq-shaped or a single expression.
|
|
// Walk the right-spine of Seq and emit each lhs as a top-level
|
|
// statement, then the final rhs as the trailing expression.
|
|
let mut cursor: &Term = body;
|
|
loop {
|
|
match cursor {
|
|
Term::Seq { lhs, rhs } => {
|
|
self.write_str(" ")?;
|
|
self.write_term(lhs)?;
|
|
cursor = rhs;
|
|
}
|
|
other => {
|
|
self.write_str(" ")?;
|
|
self.write_term(other)?;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
self.write_str(")")?;
|
|
}
|
|
Term::Assign { name, value } => {
|
|
self.write_str("(assign ")?;
|
|
self.write_str(name)?;
|
|
self.write_str(" ")?;
|
|
self.write_term(value)?;
|
|
self.write_str(")")?;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Update the EBNF prologue at `parse.rs:7-86`**
|
|
|
|
The parser file has a Form A EBNF block as a doc-comment header.
|
|
Locate the `term` alternation around line 39-68 and add three new
|
|
alternations:
|
|
|
|
```
|
|
| "(" "mut" (var-decl)* term+ ")"
|
|
| "(" "assign" ident term ")"
|
|
|
|
var-decl ::= "(" "var" ident type term ")"
|
|
```
|
|
|
|
Place these adjacent to the existing `Term::ReuseAs` line in the
|
|
alternation.
|
|
|
|
---
|
|
|
|
## Task 5 — Drift + coverage test extensions
|
|
|
|
The schema-drift, schema-coverage, and spec-drift tests are designed
|
|
to RED when a `Term` variant is added without anchoring it in the
|
|
relevant artefacts. After Tasks 1-4, the build is green but these
|
|
three tests are red. Task 5 makes them green.
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/tests/design_schema_drift.rs:43-166`
|
|
- Modify: `crates/ailang-core/tests/schema_coverage.rs:28-232`
|
|
- Modify: `crates/ailang-core/tests/spec_drift.rs:90-145`
|
|
- Modify: `crates/ailang-core/specs/form_a.md:253-300`
|
|
- Modify: `docs/DESIGN.md:2318-2368`
|
|
|
|
- [ ] **Step 1: Extend `design_schema_drift.rs`**
|
|
|
|
In the `exemplars: Vec<(&str, Term)>` constructor at lines 44-140,
|
|
after the entry for `"(reuse-as"`, append:
|
|
|
|
```rust
|
|
(
|
|
r#""t": "mut""#,
|
|
Term::Mut {
|
|
vars: Vec::new(),
|
|
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "assign""#,
|
|
Term::Assign {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
},
|
|
),
|
|
```
|
|
|
|
In the exhaustive match at lines 145-159 (the `Term::X => "x"`
|
|
variant-name mapping), after the `Term::ReuseAs { .. } => "reuse-as"`
|
|
arm, append:
|
|
|
|
```rust
|
|
Term::Mut { .. } => "mut",
|
|
Term::Assign { .. } => "assign",
|
|
```
|
|
|
|
- [ ] **Step 2: Extend `schema_coverage.rs`**
|
|
|
|
In the `VariantTag` enum at lines 28-70, after `TermReuseAs`, add:
|
|
|
|
```rust
|
|
TermMut,
|
|
TermAssign,
|
|
```
|
|
|
|
In `EXPECTED_VARIANTS` at lines 76-111, append:
|
|
|
|
```rust
|
|
VariantTag::TermMut,
|
|
VariantTag::TermAssign,
|
|
```
|
|
|
|
In `visit_term` at lines 156-232, after the `Term::ReuseAs` arm
|
|
(around line 226), append:
|
|
|
|
```rust
|
|
Term::Mut { vars, body } => {
|
|
observed.insert(VariantTag::TermMut);
|
|
for v in vars {
|
|
visit_type(&v.ty, observed);
|
|
visit_term(&v.init, observed);
|
|
}
|
|
visit_term(body, observed);
|
|
}
|
|
Term::Assign { value, .. } => {
|
|
observed.insert(VariantTag::TermAssign);
|
|
visit_term(value, observed);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Extend `spec_drift.rs`**
|
|
|
|
In the exemplar list around `tests/spec_drift.rs:90-117`, after the
|
|
`"(reuse-as"` entry, append `"(mut"` and `"(assign"` entries with
|
|
canonical exemplars matching the Step 1 shape.
|
|
|
|
In the exhaustive match at lines 124-138, after `Term::ReuseAs { .. } => "reuse-as"`,
|
|
append:
|
|
|
|
```rust
|
|
Term::Mut { .. } => "mut",
|
|
Term::Assign { .. } => "assign",
|
|
```
|
|
|
|
- [ ] **Step 4: Extend `form_a.md` (in `crates/ailang-core/specs/`)**
|
|
|
|
Around lines 281-283 in `crates/ailang-core/specs/form_a.md`, after
|
|
the entries for `seq` / `clone` / `reuse-as`, add three lines:
|
|
|
|
```
|
|
(mut (var NAME TYPE INIT)* BODY)
|
|
(var NAME TYPE INIT) ;; legal only inside (mut ...)
|
|
(assign NAME VALUE) ;; legal only inside (mut ...)
|
|
```
|
|
|
|
After the existing per-term notes around lines 298-300, append a
|
|
short paragraph:
|
|
|
|
```
|
|
The body of `(mut ...)` is a flat sequence of zero or more Unit-
|
|
typed statements followed by exactly one final expression of any
|
|
type. The parser desugars this sequence into a right-folded
|
|
`Term::Seq` chain inside `Term::Mut.body`; the canonical JSON-AST
|
|
always sees a single `body: Term`. A `(mut ...)` form with neither
|
|
vars nor body is rejected at parse; `(var ...)` and `(assign ...)`
|
|
outside a `(mut ...)` are rejected at parse for `var` and at
|
|
typecheck for `assign` (mut.2). See spec
|
|
`docs/specs/0029-mut-local.md`.
|
|
```
|
|
|
|
- [ ] **Step 5: Extend DESIGN.md §"Term (expression)"**
|
|
|
|
Locate the existing jsonc schema block in
|
|
`docs/DESIGN.md:2318-2368`. After the `reuse-as` block (around
|
|
line 2364), append:
|
|
|
|
```jsonc
|
|
// 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. Block static type
|
|
// is body's type. `vars` stays present even when empty
|
|
// (hash-stable when omitted is NOT applied here — the field is
|
|
// always serialised).
|
|
{ "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. See
|
|
// `docs/specs/0029-mut-local.md`.
|
|
{ "t": "assign",
|
|
"name": "<id>",
|
|
"value": Term }
|
|
```
|
|
|
|
The order matters: the `assign` block comes immediately after the
|
|
`mut` block to mirror the order in the enum declaration and the
|
|
exemplar list in `design_schema_drift.rs`.
|
|
|
|
- [ ] **Step 6: Run the three drift tests**
|
|
|
|
Run: `cargo test --workspace -p ailang-core schema_drift`
|
|
Expected: PASS — the `design_md_anchors_every_term_variant` and the
|
|
`data_model_section_is_bounded` tests both green.
|
|
|
|
Run: `cargo test --workspace -p ailang-core schema_coverage`
|
|
Expected: still RED on `every_ast_variant_is_observed_in_the_fixture_corpus`
|
|
because `examples/mut.ail` does not yet exist. Continue to Task 6
|
|
to ship it.
|
|
|
|
Run: `cargo test --workspace -p ailang-core spec_drift`
|
|
Expected: PASS.
|
|
|
|
---
|
|
|
|
## Task 6 — `examples/mut.ail` fixture + round-trip test
|
|
|
|
The round-trip test in `crates/ailang-surface/tests/round_trip.rs`
|
|
auto-globs every `examples/*.ail` fixture; once the fixture exists,
|
|
it gets picked up automatically. Same for `schema_coverage.rs`'s
|
|
fixture-corpus scan.
|
|
|
|
**Files:**
|
|
- Create: `examples/mut.ail`
|
|
|
|
- [ ] **Step 1: Write the fixture**
|
|
|
|
Create `examples/mut.ail` with the following content:
|
|
|
|
```
|
|
(module mut
|
|
|
|
(fn mut_empty
|
|
(doc "Iter mut.1 — empty mut block; body is a single Int literal.")
|
|
(type (fn-type (params) (ret (con Int))))
|
|
(params)
|
|
(body (mut 0)))
|
|
|
|
(fn mut_single_var
|
|
(doc "Iter mut.1 — one var, one assign, final expression reads the var.")
|
|
(type (fn-type (params) (ret (con Int))))
|
|
(params)
|
|
(body
|
|
(mut
|
|
(var x (con Int) 0)
|
|
(assign x (app + x 1))
|
|
x)))
|
|
|
|
(fn mut_two_vars
|
|
(doc "Iter mut.1 — two vars, two assigns, final expression combines them.")
|
|
(type (fn-type (params) (ret (con Float))))
|
|
(params)
|
|
(body
|
|
(mut
|
|
(var sum (con Float) 0.0)
|
|
(var count (con Int) 0)
|
|
(assign sum (app + sum 1.0))
|
|
(assign count (app + count 1))
|
|
(app + sum (app int_to_float count)))))
|
|
|
|
(fn mut_nested_shadow
|
|
(doc "Iter mut.1 — outer var shadowed by inner mut block's var of the same name.")
|
|
(type (fn-type (params) (ret (con Int))))
|
|
(params)
|
|
(body
|
|
(mut
|
|
(var x (con Int) 10)
|
|
(assign x (app + x 1))
|
|
(mut
|
|
(var x (con Int) 100)
|
|
(assign x (app + x 1))
|
|
x))))
|
|
|
|
(fn mut_returns_bool
|
|
(doc "Iter mut.1 — exercise Bool as a supported scalar var type.")
|
|
(type (fn-type (params) (ret (con Bool))))
|
|
(params)
|
|
(body
|
|
(mut
|
|
(var flag (con Bool) false)
|
|
(assign flag true)
|
|
flag)))
|
|
|
|
(fn mut_returns_unit
|
|
(doc "Iter mut.1 — exercise Unit as the supported scalar zero case.")
|
|
(type (fn-type (params) (ret (con Unit))))
|
|
(params)
|
|
(body
|
|
(mut
|
|
(var u (con Unit) (lit-unit))
|
|
(assign u (lit-unit))
|
|
u))))
|
|
```
|
|
|
|
These six fns cover: empty body, single-var, two-var, nested shadow,
|
|
Bool, Unit. The four supported scalar types (Int, Float, Bool, Unit)
|
|
are each represented. The fixture does NOT cover Str (deferred to a
|
|
future milestone per spec §"Out of scope").
|
|
|
|
- [ ] **Step 2: Run round-trip**
|
|
|
|
Run: `cargo test --workspace -p ailang-surface round_trip`
|
|
Expected: PASS — both `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
|
|
and `parse_is_deterministic_over_every_ail_fixture` green, including
|
|
the newly-added `mut.ail` fixture.
|
|
|
|
- [ ] **Step 3: Run schema-coverage**
|
|
|
|
Run: `cargo test --workspace -p ailang-core schema_coverage`
|
|
Expected: PASS — `every_ast_variant_is_observed_in_the_fixture_corpus`
|
|
green now that `examples/mut.ail` declares the two new variants.
|
|
|
|
- [ ] **Step 4: Full workspace test run**
|
|
|
|
Run: `cargo test --workspace 2>&1 | tail -20`
|
|
Expected: every test green. If any test fails, inspect the failure
|
|
before continuing — the iteration's gate is full-workspace-green.
|
|
|
|
---
|
|
|
|
## Self-review checklist
|
|
|
|
- [ ] **Spec coverage:** every section of `docs/specs/0029-mut-local.md`
|
|
§"Iteration mut.1 — Schema + surface" has a corresponding task here.
|
|
Cross-check: AST extension (Task 1), Form A surface (Task 4),
|
|
canonical-form invariants (Task 1 empty-vars pin + Task 5 drift
|
|
tests), DESIGN.md amendment (Task 5 Step 5), round-trip fixture
|
|
(Task 6). All covered.
|
|
|
|
- [ ] **Placeholder scan:** grep this file for "TBD" / "TODO" /
|
|
"implement later" / "similar to Task" / "appropriate error
|
|
handling". Expected: no hits.
|
|
|
|
- [ ] **Type-name consistency:** `Term::Mut`, `Term::Assign`, `MutVar`,
|
|
`CheckError::Internal`, `CodegenError::Internal`, `vars`, `body`,
|
|
`name`, `value`, `init`, `ty`. Each appears with the same spelling
|
|
in every task that references it.
|
|
|
|
- [ ] **Step granularity:** every step is 2-5 minutes. Tasks 1-3
|
|
Step shapes are all "add an exhaustive-match arm of one of two
|
|
canonical shapes (rebuilder or visitor) at a named line"; each is
|
|
bite-sized. Task 4 steps follow TDD shape with code shown. Task
|
|
5 steps are mechanical drift-test extensions. Task 6 steps are
|
|
fixture + test invocations.
|
|
|
|
- [ ] **No commit steps:** verified — the plan contains no
|
|
`git commit` instructions. The implementer leaves work in the
|
|
working tree; the Boss commits at iter end.
|
|
|
|
- [ ] **Boss decisions named:** the three recon open questions are
|
|
resolved in this plan: (1) walker-arm policy is "substantive
|
|
everywhere except `check_term` and `lower_term` dispatch sites
|
|
which stub with Internal" (Tasks 2-3); (2) empty-`vars` canonical
|
|
bytes pinned by the test in Task 1 Step 1; (3) `(mut)` with no
|
|
body rejected at parse via the test in Task 4 Step 1.
|
|
|
|
---
|
|
|
|
## Acceptance gate for iteration mut.1
|
|
|
|
- `cargo build --workspace` green.
|
|
- `cargo test --workspace` green, including:
|
|
- `ast::tests::term_mut_empty_vars_serialises_with_explicit_vars_field`
|
|
- `ast::tests::term_assign_round_trips_through_json`
|
|
- `parses_empty_mut_with_int_body`
|
|
- `parses_mut_with_one_var_and_one_assign_and_final`
|
|
- `rejects_mut_with_no_body`
|
|
- `rejects_mut_with_only_vars_no_final_expression`
|
|
- `design_md_anchors_every_term_variant`
|
|
- `data_model_section_is_bounded`
|
|
- `every_ast_variant_is_observed_in_the_fixture_corpus`
|
|
- `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
|
|
(including `examples/mut.ail`)
|
|
- `parse_is_deterministic_over_every_ail_fixture`
|
|
(including `examples/mut.ail`)
|
|
- `spec_drift`
|
|
- `examples/mut.ail` parses, round-trips, and is observed by the
|
|
fixture-corpus walker.
|
|
- `Term::Mut` and `Term::Assign` reaching typecheck produce
|
|
`CheckError::Internal` with the expected message text.
|
|
- `Term::Mut` and `Term::Assign` reaching codegen produce
|
|
`CodegenError::Internal` with the expected message text.
|
|
|
|
The iteration does NOT include typecheck recognition (deferred to
|
|
mut.2) or codegen lowering (deferred to mut.3); a `mut.ail`-based
|
|
e2e test that *runs* a mut block is therefore out of scope and is
|
|
not part of the gate.
|