check: mode-strict-because suppression (iter 19b)

Closes the 19a/19a.1/19b arc. Corpus signal from 19a.1 (5/65
fixtures fire over-strict-mode, all deliberate RC codegen-test
fixtures) justified shipping the suppress mechanism end-to-end.

Schema: Suppress { code, because } on FnDef. Pre-19b hashes
bit-identical via skip_serializing_if. Typechecker drops matching
diagnostics; empty 'because' is Error severity; wrong codes are
silent no-ops (open-set registry).

Form-A: (suppress (code "...") (because "...")) clause, parser
+ printer round-trip clean. Form-B: '// @suppress <code>: <reason>'
above the doc string, lossless contract metadata.

5 RC fixtures migrated (.ail.json + .ailx + .prose.txt). Corpus
signal: 5/65 -> 0/65. Lint still fires on any future fn that's
accidentally over-strict without an authored reason.

Test counts: ailang-check 55->61, ailang-core 26->28, ailang-surface
21->26, ailang-prose 49->52, e2e 70 unchanged.

Known debt: .ailx comment headers lost on regen (ail render's
contract excludes comments); parse_suppress_attr accepts
duplicate code/because attrs without diagnose (bounded by canonical
print order).
This commit is contained in:
2026-05-08 19:36:04 +02:00
parent 9f3f10ce6e
commit 50b68267fe
30 changed files with 942 additions and 304 deletions
+107
View File
@@ -9335,3 +9335,110 @@ real corpus surfaces it.
and `over_strict_mode_fires_when_match_arm_binders_unused`.
`ailang-check` test count: 53 → 55. Workspace build/test green.
## 2026-05-08 — Iter 19b: `mode-strict-because` suppression shipped
The closer to the 19a/19a.1 arc. Corpus signal from 19a.1
(5/65 fixtures fired `over-strict-mode`, all deliberate RC
codegen-test fixtures) justified shipping the suppress mechanism
end-to-end.
### Schema
`Suppress { code, because }` struct in `ailang-core::ast`. New
`FnDef::suppress: Vec<Suppress>` field with
`skip_serializing_if = "Vec::is_empty"` so pre-19b fixtures keep
bit-identical canonical-JSON hashes (regression-pinned by the
existing hash-stability test, plus 2 new ones).
### Typechecker
New `suppress_filter` module in `ailang-check`. Per-module
post-pass:
- For each `Def::Fn` with non-empty `suppress`, drop diagnostics
matching `(def, code)` from the accumulated list.
- Emit `empty-suppress-reason` (Error severity) for any
suppress entry whose `because` is whitespace-only.
- Wrong codes (e.g. suppressing `type-mismatch` when only
`over-strict-mode` would fire) are silent no-ops — open-set
registry rationale documented.
### Form-A surface
Grammar: `(suppress (code "...") (because "..."))`, between fn
name and `(type ...)`. Multiple clauses allowed. Round-trip test
pinning (parse → print → re-parse → canonical-byte equality) holds
on all fixtures including the 5 RC ones that gained suppress.
### Form-B prose
Renders one `// @suppress <code>: <reason>` line per entry, ABOVE
the doc string. Lossless — contract metadata, not stringency
machinery; the LLM-reader of prose needs to see *why* an
annotation that looks over-strict is correct on this def.
Concrete shape from `rc_own_param_drop.prose.txt`:
```
// @suppress over-strict-mode: RC codegen test: exercises Iter B Own-param dec at fn return
/// Take ownership of an IntList; return its head if Cons, else 0...
fn head_or_zero(xs: own IntList) -> Int {
...
}
```
### Fixture migration
Five `.ail.json` files gained suppress entries; `.ailx` siblings
regenerated via `ail render`; three of the four pinned `.prose.txt`
snapshots regenerated (the fourth, `bench_list_sum`, has no Own
params and is unchanged). The migration documented per-fixture
reason text matches the codegen-test path each fixture exercises.
### Corpus signal after migration
`over-strict-mode` warnings across all 65 fixtures: **5 → 0**. All
five RC codegen-test fixtures now check clean while preserving
their `(own T)` annotation as deliberate test infrastructure. The
lint still fires on any future fn that's accidentally over-strict
without an authored reason.
### Test counts
- `ailang-check`: 55 → 61 (6 new `suppress_filter` tests)
- `ailang-core`: 26 → 28 (2 hash-stability tests)
- `ailang-surface`: 21 → 26 (5 parse tests)
- `ailang-prose`: 49 → 52 (3 prose-render tests)
- `e2e`: 70 (unchanged)
### Known debt
- **`.ailx` comment headers lost.** The five regenerated `.ailx`
files lost their hand-written comment headers — `ail render`'s
contract excludes comment preservation. If those headers are
needed back, that's a separate iter (probably a comment-
preserving printer mode).
- **Duplicate-attr detection.** `parse_suppress_attr` accepts
`(code …)` and `(because …)` in either order but does not
detect duplicates within a single `(suppress …)` clause; a
second `(code …)` silently overwrites. The canonical printer
emits in fixed order, so the only way to construct duplicates
is hand-writing weird input — bounded.
- **Cross-iter snapshot coupling** (architect's deferred item 3
from 20e): adding suppress to RC fixtures invalidated three
snapshots. They were already pinned by family 20; this iter
re-rendered them. Same pattern will recur on any future iter
that touches those fixtures.
### Family / arc state
The 19a/19a.1/19b arc is now closed:
- 19a: `over-strict-mode` lint + Severity::Warning surface
- 19a.1: precise sub-binder analysis (heap-type filter)
- 19b: `mode-strict-because` suppression + 5-fixture migration
JOURNAL queue: empty again. Three pre-existing 20b deferrals
(let-inlining, `print` sugar, deeply-nested-match-on-sub-binder
in 19a.1) remain queued; all three need real-corpus signal that
hasn't surfaced.