fix: mut-local diagnostics doubled the [code] prefix in human stderr (fieldtest F2)
The four mut-local CheckError variants (MutAssignOutOfScope, AssignTypeMismatch, UnsupportedMutVarType, MutVarCapturedByLambda) embedded the bracketed kebab code in their thiserror Display body, while the non-JSON CLI formatter independently prepends [code] from CheckError::code() — doubling it. Non-mut variants never embedded the bracket; this brings the four in line with that convention. RED-first via the debug skill: ct1_check_cli.rs pins the observable property (code appears exactly once in the rendered human diagnostic). GREEN was a trivial four-string mechanical edit. Tests 598 -> 599.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# bugfix mut-diag-double-code — doubled `[code]` prefix in mut-local human diagnostics
|
||||
|
||||
**Date:** 2026-05-15
|
||||
**Status:** DONE
|
||||
**Trigger:** fieldtest finding F2 (`docs/specs/2026-05-15-fieldtest-mut-local.md`).
|
||||
**Started from:** 1faee67
|
||||
|
||||
## Symptom
|
||||
|
||||
Non-JSON `ail check` stderr rendered the kebab diagnostic code
|
||||
twice for the four mut-local `CheckError` variants, e.g.
|
||||
|
||||
```
|
||||
error: [mut-var-captured-by-lambda] main: [mut-var-captured-by-lambda] mut-var `x` cannot be captured...
|
||||
```
|
||||
|
||||
## Cause
|
||||
|
||||
`MutAssignOutOfScope`, `AssignTypeMismatch`,
|
||||
`UnsupportedMutVarType`, `MutVarCapturedByLambda` were authored in
|
||||
iters mut.2 / mut.4-tidy with their `#[error("[<code>] ...")]`
|
||||
`thiserror` Display bodies embedding the bracketed code. The
|
||||
non-JSON `Cmd::Check` formatter in `crates/ail/src/main.rs`
|
||||
independently prepends `[{d.code}]` from `CheckError::code()`.
|
||||
Embedded + prepended = doubled. The non-mut `CheckError` variants
|
||||
never embedded the bracket, so only these four doubled — they were
|
||||
the inconsistent ones, brought in line here.
|
||||
|
||||
## Fix
|
||||
|
||||
RED-first via the `debug` skill (`ailang-debugger` wrote the
|
||||
failing test; the GREEN side was a trivial four-string mechanical
|
||||
edit applied inline by the Boss per the CLAUDE.md
|
||||
trivial-mechanical-edit carve-out — no review-and-commit
|
||||
discipline shed).
|
||||
|
||||
- RED test: `crates/ail/tests/ct1_check_cli.rs` —
|
||||
`check_human_mode_renders_mut_diagnostic_code_exactly_once`.
|
||||
Pins the *observable* property: the code appears exactly once in
|
||||
the rendered human diagnostic, exercising the same formatter path
|
||||
the CLI uses (not a brittle assertion on the raw `#[error]` body).
|
||||
- GREEN: dropped the leading `[<code>] ` literal from the four
|
||||
`#[error(...)]` Display strings in `crates/ailang-check/src/lib.rs`.
|
||||
|
||||
The mut-typecheck pin tests (`mut_typecheck_pin.rs`) and the
|
||||
in-source unit assertions match on the struct variants / `code()`,
|
||||
not the Display string, so the prefix removal did not touch them.
|
||||
|
||||
## Tests
|
||||
|
||||
598 → 599 green (the new RED-now-GREEN human-formatter test).
|
||||
Full workspace clean, zero failures.
|
||||
|
||||
## Not in scope (stays queued)
|
||||
|
||||
F1/F4 (accumulator-over-iteration: the milestone motivation is
|
||||
unmet without a loop construct) and F3 (zero-arg `(app f)` rejected
|
||||
at parse) from the same fieldtest are NOT addressed here — they are
|
||||
design/roadmap items, not this bug.
|
||||
@@ -74,3 +74,4 @@
|
||||
- 2026-05-15 — iter mut.2: typecheck for `Term::Mut` + `Term::Assign` replacing the iter mut.1 dispatch stubs; three new `CheckError` variants (`MutAssignOutOfScope`, `AssignTypeMismatch`, `UnsupportedMutVarType`) with bracketed-`[code]:` Display + `code()` + `ctx()` arms; `synth` signature threads `mut_scope_stack: &mut Vec<IndexMap<String, Type>>` after `locals`, propagated through all recursive call sites in `lib.rs` plus `mono.rs:712/1337` + `lift.rs:723` + `builtins.rs` test helpers + the mq.3 in-test helper at `lib.rs:6663`; `Term::Var` resolution prepended with innermost-first mut-scope lookup; `Term::Mut` arm gates var types to {Int,Float,Bool,Unit} and push/pops a fresh frame; `Term::Assign` arm walks the stack innermost-first, emits `AssignTypeMismatch` on type mismatch and `MutAssignOutOfScope` (with `available` flattened across all frames) on miss; five `.ail.json` fixtures under `examples/` (four negative + one positive nested-shadow) + driver `crates/ailang-check/tests/mut_typecheck_pin.rs`; `carve_out_inventory.rs` EXPECTED extended 7→12 for the new negative-fixture set; `examples/mut.ail` typechecks clean (`ok (26 symbols across 2 modules)`); tests 579 → 592 → 2026-05-15-iter-mut.2.md
|
||||
- 2026-05-15 — iter mut.3: codegen + e2e for `Term::Mut` + `Term::Assign` closing the mut-local milestone end-to-end; mut-var allocas hoisted to fn entry block via a per-`Emitter` side buffer `pending_entry_allocas: String` + a captured `entry_block_end_marker: Option<usize>` byte position spliced via `String::insert_str` at the end of `emit_fn`; `Term::Mut` arm in `lower_term` emits one alloca per var into the side buffer, lowers each init at the current position, emits a `store` to bind, push/pops a per-block save stack for proper shadowing of outer mut-vars; `Term::Assign` arm emits `store <ty> <value_ssa>, ptr <alloca>` and yields the canonical Unit SSA; `Term::Var` arm prepends mut-var lookup with a `load` emission. Two beyond-plan adjustments: (1) `synth_with_extras`'s parallel `Term::Var` arm needed the same mut-var lookup precedence as `lower_term`, (2) `lambda.rs` thunk emission needed save/restore of all three new `Emitter` fields across the lambda-body boundary plus an in-thunk splice at the lambda's own entry marker so mut-blocks inside a closure body hoist to the closure's entry not the outer fn's. Two e2e fixtures `examples/mut_counter.ail` (Int) + `examples/mut_sum_floats.ail` (Float); both run end-to-end and print `55`. DESIGN.md "What **is** supported" subsection gains a "Local mutable state" bullet. mut-local milestone end-to-end closed. Tests 592 → 594 → 2026-05-15-iter-mut.3.md
|
||||
- 2026-05-15 — iter mut.4-tidy: close mut-local audit drift. Architect's two `[high]` items addressed — new `CheckError::MutVarCapturedByLambda` rejects any lambda whose body's free vars hit the enclosing `mut_scope_stack` (via the existing `desugar::free_vars_in_term` helper, gated on non-empty stack), and `codegen/lambda.rs`'s blame-typechecker `Internal` path becomes `unreachable!` once typecheck is the gate. Two `[medium]` stale-history comments cleaned in DESIGN.md §"Term (expression)" and `ailang-codegen/src/lib.rs`. New negative fixture `examples/test_mut_var_captured_by_lambda.ail.json` + driver test extension; `carve_out_inventory.rs` EXPECTED 12→13. Spec §"Out of scope" amended with the lambda-capture rejection bullet. Bench: `compile_check.py` `check_ms` showed a uniform ~30-50% regression across the suite traced to a fixed-cost startup tax (mut-* added ~1400 lines of typecheck/codegen surface; the short-circuit on empty `mut_scope_stack` walk in Term::Var did not move the needle, falsifying the hot-path hypothesis); ratified as a feature tax with paired baseline update on `bench/baseline_compile.json`. `check.py` tail-noise envelope continues the audit-pd carve-out (no ratify needed). `cross_lang.py` clean. Tests 594 → 598. mut-local milestone audit closed → 2026-05-15-iter-mut.4-tidy.md
|
||||
- 2026-05-15 — bugfix mut-diag-double-code: fieldtest F2 — the four mut-local `CheckError` variants embedded `[<code>]` in their `#[error]` Display body while the non-JSON CLI formatter also prepends `[code]`, doubling it in human stderr; dropped the embedded prefix from the four strings, bringing them in line with all non-mut variants; RED-first via `debug` (`ct1_check_cli.rs::check_human_mode_renders_mut_diagnostic_code_exactly_once`), GREEN applied inline as a trivial mechanical edit; tests 598 → 599 → 2026-05-15-bugfix-mut-diag-double-code.md
|
||||
|
||||
Reference in New Issue
Block a user