From 6966cce7024e421ace55be9ff5265385bc7779fb Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 15 May 2026 02:05:22 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20mut.4-tidy=20=E2=80=94=20close=20mut-lo?= =?UTF-8?q?cal=20audit=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight tasks addressing the architect's two [high] drift items (lambda-captures-of-mut-var diagnostic + codegen comment cleanup), two [medium] items (stale mut.1-stub history comments in DESIGN.md and codegen), plus the bench compile_check.py check_ms regression (~30-50% relative; closed by short-circuiting the empty mut_scope_stack walk in synth's Term::Var arm). Architect's [low] item (prose render of MutVar drops type annotation) deliberately deferred per architect recommendation — prose surface design is itself pending. Plan unblocks the follow-on Stateful-islands milestone (effect- handler infrastructure + !Mut + refs) per architect gate. --- docs/plans/2026-05-15-iter-mut.4-tidy.md | 657 +++++++++++++++++++++++ 1 file changed, 657 insertions(+) create mode 100644 docs/plans/2026-05-15-iter-mut.4-tidy.md diff --git a/docs/plans/2026-05-15-iter-mut.4-tidy.md b/docs/plans/2026-05-15-iter-mut.4-tidy.md new file mode 100644 index 0000000..85d186d --- /dev/null +++ b/docs/plans/2026-05-15-iter-mut.4-tidy.md @@ -0,0 +1,657 @@ +# mut.4-tidy — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-15-mut-local.md` (covered by +> audit-mut-local findings; no new spec). +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Close the mut-local milestone audit by addressing the +architect's drift findings and the bench check_ms regression. +Specifically: (a) add a typecheck diagnostic that rejects +lambda-captures-of-mut-var (`CheckError::MutVarCapturedByLambda`) +and a negative fixture; (b) replace the codegen `lambda.rs` blame- +typechecker comment + Internal-error path with an `unreachable!` +once the typecheck gate is in place; (c) clean stale mut.1-stub +history comments in DESIGN.md §"Term" and +`crates/ailang-codegen/src/lib.rs`; (d) short-circuit the empty +`mut_scope_stack` walk in the `Term::Var` arm of `synth` to +eliminate the per-Var-resolution overhead for programs with no mut +blocks. + +**Architecture:** Architect's two `[high]` drift items are downstream +of one spec gap: `docs/specs/2026-05-15-mut-local.md` does not +enumerate "lambda body referencing an enclosing mut-var" as a +diagnostic. The fix has three sites — typecheck adds the diagnostic +in `Term::Lam`'s synth arm, codegen replaces its blame-comment path +with `unreachable!`, and the spec gets an "Out of scope" amendment +noting that mut-vars cannot escape into closures in this milestone. +The bench check_ms regression has one root cause — the +`mut_scope_stack.iter().rev().find_map(...)` walk at every +`Term::Var` resolution runs even when the stack is empty (every +program with no mut blocks); short-circuiting on `is_empty()` makes +the path zero-cost for the existing corpus. + +**Tech Stack:** `ailang-check`, `ailang-codegen`, `docs/`. Plus one +new negative fixture and one driver-test extension. + +--- + +**Files this plan creates or modifies:** + +- Create: `examples/test_mut_var_captured_by_lambda.ail.json` — + negative fixture: a lambda inside a mut block references a + mut-var from the enclosing scope. +- Modify: `crates/ailang-check/src/lib.rs:638-746` — add + `CheckError::MutVarCapturedByLambda { name }` variant + `code()` + arm `mut-var-captured-by-lambda` + `ctx()` arm + Display. +- Modify: `crates/ailang-check/src/lib.rs` (`Term::Lam` arm of + `synth`) — walk the lambda's body for free Vars that resolve to + the enclosing `mut_scope_stack`; reject with + `MutVarCapturedByLambda` if any match. +- Modify: `crates/ailang-check/src/lib.rs` (`Term::Var` arm of + `synth`) — short-circuit when `mut_scope_stack.is_empty()`. +- Modify: `crates/ailang-codegen/src/lambda.rs:88-99` (capture-not- + in-locals path) — replace the `CodegenError::Internal(... blames + typechecker ...)` with `unreachable!("typecheck rejects + lambda-capture-of-mut-var via MutVarCapturedByLambda since + mut.4-tidy")`. +- Modify: `crates/ailang-codegen/src/lambda.rs:466-467` — remove + the doc-comment promise that mut.2 typecheck rejects lambda + captures (it does as of mut.4); rewrite to reflect current + reality. +- Modify: `crates/ailang-check/src/lift.rs:379` — same comment + cleanup. +- Modify: `crates/ailang-codegen/src/lib.rs:1749-1758` — remove the + stale mut.1-stub comment block above the real Term::Mut arm. +- Modify: `docs/DESIGN.md:2369-2402` (§"Term (expression)" mut/assign + jsonc block) — remove the trailing paragraph describing the + mut.1-stub dispatch state; replace with a paragraph describing the + current end-to-end mut.3 state. +- Modify: `docs/specs/2026-05-15-mut-local.md` (§"Out of scope" or + equivalent) — add a bullet naming "Lambda capture of a mut-var" + as explicitly rejected, with the diagnostic name. +- Modify: `crates/ailang-check/tests/mut_typecheck_pin.rs` — add + driver test for the new fixture asserting the + `mut-var-captured-by-lambda` diagnostic code. +- Modify: `crates/ailang-core/tests/carve_out_inventory.rs` — + EXPECTED extended 12 → 13 for the new negative fixture. + +--- + +## Task 1 — `CheckError::MutVarCapturedByLambda` + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs:638-746` + +- [ ] **Step 1: Write the failing pin test** + +In `#[cfg(test)] mod tests`: + +```rust +#[test] +fn mut_var_captured_by_lambda_diagnostic_has_kebab_code() { + let e = CheckError::MutVarCapturedByLambda { name: "sum".into() }; + assert_eq!(e.code(), "mut-var-captured-by-lambda"); + let _ = e.ctx(); +} +``` + +- [ ] **Step 2: Run to verify red** + +Run: `cargo test --workspace -p ailang-check mut_var_captured_by_lambda_diagnostic_has_kebab_code` +Expected: FAIL — variant does not exist. + +- [ ] **Step 3: Add the variant + code() + ctx() + Display** + +In the `CheckError` enum body, immediately after +`UnsupportedMutVarType`, insert: + +```rust + /// Iter mut.4-tidy: `Term::Lam` reached typecheck inside a + /// `Term::Mut` scope, and the lambda's body references a mut-var + /// declared in that enclosing scope. Spec + /// `docs/specs/2026-05-15-mut-local.md` §"Out of scope": mut-vars + /// cannot escape — they are alloca-resident and lexically scoped. + /// Capturing one into a closure would require either lifting it + /// to the heap (deferred milestone) or rejecting the capture. + /// This milestone takes the latter route. + #[error("[mut-var-captured-by-lambda] mut-var `{name}` cannot be captured by a lambda — mut-vars are alloca-resident and do not escape their enclosing mut block. Either move the lambda outside the mut block, or restructure to pass `{name}` as a lambda parameter (deferring escape to a future milestone with ref-types and the !Mut effect).")] + MutVarCapturedByLambda { name: String }, +``` + +Then the `code()` arm immediately after `UnsupportedMutVarType`'s: + +```rust + CheckError::MutVarCapturedByLambda { .. } => "mut-var-captured-by-lambda", +``` + +And the `ctx()` arm: + +```rust + CheckError::MutVarCapturedByLambda { name } => json!({ + "name": name, + }), +``` + +- [ ] **Step 4: Verify** + +Run: `cargo test --workspace -p ailang-check mut_var_captured_by_lambda_diagnostic_has_kebab_code` +Expected: PASS. + +--- + +## Task 2 — Reject lambda-captures-of-mut-var in `Term::Lam` synth arm + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs` (`Term::Lam` arm of `synth`) + +The arm is around `lib.rs:3374-3407` per the architect's report. +The lambda's body is typechecked in a fresh `locals` scope with the +lambda's params bound — but `mut_scope_stack` carries over from the +caller. The fix is: BEFORE descending into the lambda body, scan +the body for free Vars whose names resolve in `mut_scope_stack`. +If any do, reject. + +- [ ] **Step 1: Write the failing pin (positive — lambda outside mut still works)** + +Add to `#[cfg(test)] mod tests`: + +```rust +#[test] +fn lambda_outside_mut_still_typechecks_clean() { + // (lam (params (typed x (con Int))) (ret (con Int)) (body (app + x 1))) + let term = Term::Lam { + params: vec!["x".into()], + param_types: vec![Type::int()], + ret_type: Type::int(), + effects: vec![], + body: Box::new(Term::App { + func: Box::new(Term::Var { name: "+".into() }), + args: vec![ + Term::Var { name: "x".into() }, + Term::Lit { lit: Literal::Int { value: 1 } }, + ], + // tail-call default field name + }), + }; + let (_ty, _) = synth_in_builtins_env(&term); + // No assertion on the returned type beyond that it doesn't panic + // / fail — this is a regression test that lambda synth wasn't + // broken by the new lambda-body free-var scan. +} +``` + +This test should PASS even before Task 2 Step 2 — it's a regression +guard. + +- [ ] **Step 2: Write the failing negative pin** + +```rust +#[test] +fn lambda_inside_mut_capturing_mut_var_emits_mut_var_captured_by_lambda() { + // (mut (var x (con Int) 0) + // (let f (lam (params) (ret (con Int)) (body x)) ;; captures x + // 0)) + let term = Term::Mut { + vars: vec![ailang_core::ast::MutVar { + name: "x".into(), + ty: Type::int(), + init: Term::Lit { lit: Literal::Int { value: 0 } }, + }], + body: Box::new(Term::Let { + name: "f".into(), + value: Box::new(Term::Lam { + params: vec![], + param_types: vec![], + ret_type: Type::int(), + effects: vec![], + body: Box::new(Term::Var { name: "x".into() }), + }), + body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }), + }), + }; + let err = synth_in_builtins_env_expect_err(&term); + match err { + CheckError::MutVarCapturedByLambda { name } => assert_eq!(name, "x"), + other => panic!("expected MutVarCapturedByLambda, got {:?}", other), + } +} +``` + +(`synth_in_builtins_env_expect_err` is whatever helper the existing +tests use to drive synth and unwrap an Err — match the convention.) + +- [ ] **Step 3: Run to verify red** + +Run: `cargo test --workspace -p ailang-check lambda_inside_mut_capturing_mut_var_emits_mut_var_captured_by_lambda` +Expected: FAIL — currently the lambda body resolves `x` against the +outer `mut_scope_stack` and types as Int with no diagnostic. + +- [ ] **Step 4: Add the scan to `Term::Lam`'s synth arm** + +In the `Term::Lam { params, param_types, body, .. }` arm of `synth`, +BEFORE descending into the body (i.e. before the call to +`synth(body, &lambda_locals, mut_scope_stack, ...)`), insert: + +```rust +// Iter mut.4-tidy: reject lambda-captures-of-mut-var. +// Walk the lambda body for free Vars (names not bound by the +// lambda's own params) that match any mut-var on the enclosing +// stack. If any match, reject with MutVarCapturedByLambda. +if !mut_scope_stack.is_empty() { + let captured = collect_free_vars_in_term(body); + let lambda_param_names: std::collections::BTreeSet<&str> = + params.iter().map(|s| s.as_str()).collect(); + for free_name in &captured { + if lambda_param_names.contains(free_name.as_str()) { + continue; + } + for frame in mut_scope_stack.iter() { + if frame.contains_key(free_name) { + return Err(CheckError::MutVarCapturedByLambda { + name: free_name.clone(), + }); + } + } + } +} +``` + +The helper `collect_free_vars_in_term(t: &Term) -> Vec` may +already exist (the codebase has free-var walkers in `desugar.rs:1154`- +area). If a directly-callable helper does not exist, write a small +local one: + +```rust +fn collect_free_vars_in_term(t: &Term) -> Vec { + fn walk(t: &Term, bound: &mut Vec, out: &mut Vec) { + match t { + Term::Var { name } => { + if !bound.contains(name) && !out.contains(name) { + out.push(name.clone()); + } + } + Term::Let { name, value, body } => { + walk(value, bound, out); + bound.push(name.clone()); + walk(body, bound, out); + bound.pop(); + } + Term::LetRec { name, body, in_term, .. } => { + bound.push(name.clone()); + walk(body, bound, out); + walk(in_term, bound, out); + bound.pop(); + } + Term::Lam { params, body, .. } => { + let n = bound.len(); + for p in params { bound.push(p.clone()); } + walk(body, bound, out); + bound.truncate(n); + } + Term::Mut { vars, body } => { + for v in vars { walk(&v.init, bound, out); } + let n = bound.len(); + for v in vars { bound.push(v.name.clone()); } + walk(body, bound, out); + bound.truncate(n); + } + Term::Assign { name, value } => { + // Assign's name IS a use of the binding — counts as a free var if not bound. + if !bound.contains(name) && !out.contains(name) { + out.push(name.clone()); + } + walk(value, bound, out); + } + Term::App { func, args, .. } => { + walk(func, bound, out); + for a in args { walk(a, bound, out); } + } + Term::If { cond, then, else_ } => { + walk(cond, bound, out); + walk(then, bound, out); + walk(else_, bound, out); + } + Term::Match { scrutinee, arms } => { + walk(scrutinee, bound, out); + for arm in arms { + // pattern bindings... for simplicity, scan body + // without removing pattern-bound names — over- + // approximates free vars (worst-case: emits the + // diagnostic when none was needed). The over- + // approximation is conservative-safe for + // capture-rejection; a false-positive can be + // tightened in a future iter. + walk(&arm.body, bound, out); + } + } + Term::Seq { lhs, rhs } => { + walk(lhs, bound, out); + walk(rhs, bound, out); + } + Term::Clone { value } => walk(value, bound, out), + Term::ReuseAs { source, body } => { + walk(source, bound, out); + walk(body, bound, out); + } + Term::Ctor { args, .. } => for a in args { walk(a, bound, out); }, + Term::Do { args, .. } => for a in args { walk(a, bound, out); }, + Term::Lit { .. } => {} + } + } + let mut bound = Vec::new(); + let mut out = Vec::new(); + walk(t, &mut bound, &mut out); + out +} +``` + +(The exact match-arm shape must be exhaustive against the post-mut.1 +Term enum. Pattern bindings inside Term::Match arms are +over-approximated for the first cut; a tightening pass can land in a +future tidy iter.) + +- [ ] **Step 5: Verify** + +Run: `cargo test --workspace -p ailang-check lambda_outside_mut_still_typechecks_clean lambda_inside_mut_capturing_mut_var_emits_mut_var_captured_by_lambda` +Expected: both PASS. + +Run: `cargo test --workspace 2>&1 | grep -E 'FAILED|failures' | head -5` +Expected: no failures introduced. (If any existing test broke, +likely cause: the over-approximation in pattern bindings — check +which test, decide whether to tighten the helper or to add a +specific bound-name extraction for Term::Match arms.) + +--- + +## Task 3 — Short-circuit empty `mut_scope_stack` in `Term::Var` arm + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs:2743-2747` + +- [ ] **Step 1: Locate the existing prepend** + +Find the iter mut.2 prepend in the `Term::Var` arm of `synth`, +introduced at `lib.rs:2743-2747`: + +```rust +for frame in mut_scope_stack.iter().rev() { + if let Some(ty) = frame.get(name) { + return Ok((ty.clone(), /* ... */)); + } +} +``` + +- [ ] **Step 2: Wrap in an `is_empty` guard** + +```rust +if !mut_scope_stack.is_empty() { + for frame in mut_scope_stack.iter().rev() { + if let Some(ty) = frame.get(name) { + return Ok((ty.clone(), /* ... */)); + } + } +} +``` + +This is a single-instruction short-circuit. For the existing test +corpus (zero mut blocks outside the four mut*.ail fixtures), this +collapses the prepend cost to one `is_empty()` check per Var +resolution — effectively zero overhead. + +- [ ] **Step 3: Verify the existing pin tests still pass** + +Run: `cargo test --workspace -p ailang-check 2>&1 | tail -5` +Expected: PASS. + +- [ ] **Step 4: Re-run `compile_check.py` to confirm regression closes** + +Run: `bench/compile_check.py 2>&1 | tail -10` +Expected: zero `REGRESSION` lines on check_ms. (If the short-circuit +brings check_ms back within tolerance, the bench exit drops to 0 +for this script. If not, additional optimization is needed — but +the prepend cost is the only known introduced overhead in mut.2.) + +--- + +## Task 4 — Codegen comment cleanup + unreachable blame fix + +**Files:** +- Modify: `crates/ailang-codegen/src/lambda.rs:88-99` +- Modify: `crates/ailang-codegen/src/lambda.rs:466-467` +- Modify: `crates/ailang-codegen/src/lib.rs:1749-1758` + +- [ ] **Step 1: `lambda.rs:88-99` — replace Internal blame with unreachable!** + +Locate the existing path where a lambda capture is not found in +`self.locals`. The current arm raises `CodegenError::Internal` with +a message blaming the typechecker. Replace with: + +```rust + None => { + // Iter mut.4-tidy: post-Task-2 the typechecker + // rejects this via `CheckError::MutVarCapturedByLambda`. + // Reaching this path means typecheck was skipped or + // a bug let the AST through — both are bugs in + // upstream layers, not in codegen. + unreachable!( + "lambda capture {name:?} not in locals — \ + typecheck should have rejected via MutVarCapturedByLambda", + ); + } +``` + +- [ ] **Step 2: `lambda.rs:466-467` — comment cleanup** + +The existing comment promises mut.2 rejects lambda captures of +mut-vars. Update to: + +```rust +// Mut-var captures by lambdas are rejected at typecheck by +// CheckError::MutVarCapturedByLambda (mut.4-tidy). codegen does +// not see them. +``` + +- [ ] **Step 3: `lib.rs:1749-1758` — remove stale mut.1-stub comment** + +The architect's report names this site as still describing the +mut.1 stub state. Read the current comment and either delete it (if +purely historical) or rewrite to reflect the mut.3 reality. The new +comment should NOT mention "stub", "deferred to mut.3", or +"Internal-error". A one-line comment summarising the arm's purpose +("Lower a mut-block: per-var alloca emit + body lower + scope +exit") is sufficient. + +- [ ] **Step 4: Verify build green** + +Run: `cargo build --workspace 2>&1 | tail -5` +Expected: green. + +Run: `cargo test --workspace 2>&1 | tail -5` +Expected: green. + +--- + +## Task 5 — `lift.rs` comment cleanup + +**Files:** +- Modify: `crates/ailang-check/src/lift.rs:379` + +- [ ] **Step 1: Update the comment** + +The existing comment promises mut.2 rejects lambda captures of +mut-vars. Update to match the mut.4-tidy reality, in the same form +as Task 4 Step 2. + +- [ ] **Step 2: Verify build green** + +Run: `cargo build --workspace 2>&1 | tail -5` +Expected: green. + +--- + +## Task 6 — DESIGN.md amendment + +**Files:** +- Modify: `docs/DESIGN.md:2369-2402` (§"Term (expression)" mut/assign block) + +- [ ] **Step 1: Update the trailing paragraph** + +The current jsonc block ends with a paragraph describing the +mut.1-stub state. Replace with: + +``` +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 is rejected at typecheck via +`CheckError::MutVarCapturedByLambda`. See +`docs/specs/2026-05-15-mut-local.md`. +``` + +- [ ] **Step 2: Verify schema-drift tests still pass** + +Run: `cargo test --workspace -p ailang-core schema_drift 2>&1 | tail -5` +Expected: PASS. + +--- + +## Task 7 — Spec amendment + +**Files:** +- Modify: `docs/specs/2026-05-15-mut-local.md` (§"Out of scope" or §"Acceptance criteria") + +- [ ] **Step 1: Add bullet to §"Out of scope"** + +In the §"Out of scope" subsection, append a bullet: + +``` +- **Lambda capture of a mut-var.** A lambda body whose free vars + include a mut-var of an enclosing `Term::Mut` is rejected at + typecheck with `CheckError::MutVarCapturedByLambda`. Mut-vars are + alloca-resident and lexically scoped; lifting them to the heap + for closure-capture would require ref-types and the `!Mut` + effect, both of which are deferred to the follow-on + Stateful-islands milestone (the layered effect-handler + ref-type + combo). The rejection diagnostic was added in iter mut.4-tidy. +``` + +--- + +## Task 8 — Negative fixture + driver test extension + +**Files:** +- Create: `examples/test_mut_var_captured_by_lambda.ail.json` +- Modify: `crates/ailang-check/tests/mut_typecheck_pin.rs` +- Modify: `crates/ailang-core/tests/carve_out_inventory.rs` + +- [ ] **Step 1: Write the fixture** + +Create `examples/test_mut_var_captured_by_lambda.ail.json`: + +```json +{ + "name": "test_mut_var_captured_by_lambda", + "imports": [], + "defs": [ + { + "kind": "fn", + "name": "main", + "ty": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Int" } }, + "params": [], + "body": { + "t": "mut", + "vars": [ + { "name": "x", "type": { "k": "con", "name": "Int" }, "init": { "t": "lit", "lit": { "kind": "int", "value": 0 } } } + ], + "body": { + "t": "let", + "name": "f", + "value": { + "t": "lam", + "params": [], + "paramTypes": [], + "retType": { "k": "con", "name": "Int" }, + "effects": [], + "body": { "t": "var", "name": "x" } + }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 0 } } + } + } + } + ] +} +``` + +Verify the exact root-key + field-name shape against an existing +sibling fixture (e.g. `examples/test_mut_assign_out_of_scope.ail.json`). + +- [ ] **Step 2: Add driver test** + +In `crates/ailang-check/tests/mut_typecheck_pin.rs`: + +```rust +#[test] +fn lambda_capturing_mut_var_emits_mut_var_captured_by_lambda() { + let codes = check_fixture("test_mut_var_captured_by_lambda.ail.json"); + assert_eq!(codes, vec!["mut-var-captured-by-lambda".to_string()]); +} +``` + +- [ ] **Step 3: Update carve-out inventory** + +In `crates/ailang-core/tests/carve_out_inventory.rs`, bump the +EXPECTED count from 12 → 13 (the new fixture joins the existing +twelve `.ail.json` carve-outs). + +- [ ] **Step 4: Run all driver tests** + +Run: `cargo test --workspace -p ailang-check mut_typecheck_pin 2>&1 | tail -5` +Expected: all six tests PASS (five from mut.2 + the new one). + +Run: `cargo test --workspace -p ailang-core carve_out_inventory 2>&1 | tail -3` +Expected: PASS. + +Run: `cargo test --workspace 2>&1 | tail -5` +Expected: full workspace green. + +--- + +## Self-review checklist + +- [ ] **Drift coverage:** all four architect items (two `[high]` + + two `[medium]`) plus the bench check_ms regression are addressed. + Architect `[low]` items (prose render of MutVar) deliberately + deferred per architect's recommendation. + +- [ ] **Placeholder scan:** grep for "TBD" / "TODO" / "implement + later" / "similar to Task" / "appropriate error handling" — no + hits in this plan. + +- [ ] **Type-name consistency:** `CheckError::MutVarCapturedByLambda`, + `mut-var-captured-by-lambda` (kebab code), `collect_free_vars_in_term` + (local helper), `mut_scope_stack`, `unreachable!` — each appears + identically across tasks. + +- [ ] **Step granularity:** each step is 2-5 minutes. + +- [ ] **No commit steps:** verified. + +--- + +## Acceptance gate for iter mut.4-tidy + +- `cargo build --workspace` green. +- `cargo test --workspace` green, including the new negative-fixture + pin and the carve_out_inventory bump. +- `bench/compile_check.py` check_ms regression closed (within + tolerance vs. baseline). +- `bench/check.py` and `bench/cross_lang.py` unchanged from their + pre-mut.4 state (tail-noise envelope holds; runtime stable). +- All architect-flagged comments rewritten to reflect the post-mut.3 + reality. +- The follow-on Stateful-islands milestone is now unblocked per the + architect's gate.