check: precise sub-binder analysis for over-strict-mode (iter 19a.1)
Corpus-signal upgrade: orchestrator ran 19a's lint across 65 fixtures, zero warnings fired. The conservative match-scrutinee carve-out filtered every (own T) fixture because every such body destructures the param via match. Real-corpus signal warranted the precise variant. Precise rule: for an Own param with consume_count == 0, walk each match arm whose scrutinee is the param; if any HEAP-TYPED pattern-binder has consume_count > 0, stay silent (sub-consume forces ownership). Otherwise fire. Heap-type filter is the implementer's design call (not in the brief): reading a primitive (h: Int) is load-by-value, no RC, no heap-data move; reading heap-typed t: IntList IS a pointer move requiring outer-cell ownership. Documented in module-doc. Corpus signal after upgrade: 5/65 fixtures fire — all RC codegen-test fixtures with deliberate over-strictness for the codegen path. Justifies 19b (mode-strict-because suppression). ailang-check test count: 53 → 55. e2e + everything else green. Known debt: deeply-nested-match-on-sub-binder (no fixture hits).
This commit is contained in:
@@ -9244,3 +9244,94 @@ JOURNAL queue: empty again.
|
||||
- `docs/JOURNAL.md` — this entry.
|
||||
|
||||
No code changes. Build/test status unchanged from 20d (green).
|
||||
|
||||
## 2026-05-08 — Iter 19a.1: precise sub-binder analysis (corpus-driven upgrade)
|
||||
|
||||
The 19a-shipping entry queued a sub-binder-precise variant for "if
|
||||
real-corpus signal warrants." I (orchestrator) ran 19a's lint
|
||||
across all 65 fixtures: **zero warnings fired**. The conservative
|
||||
match-scrutinee carve-out filtered every `(own T)` param because
|
||||
every such fixture's body destructures the param via `match` —
|
||||
even ones that are deliberately over-strict (RC codegen-test
|
||||
fixtures like `head_or_zero` and `use_first`). The conservative
|
||||
cut wasn't "incomplete but valuable"; it was "silent in the only
|
||||
shapes the corpus actually produces." Real-corpus signal warranted.
|
||||
|
||||
### Precise rule
|
||||
|
||||
For an `Own` param `p` with `consume_count == 0`:
|
||||
|
||||
- For every match arm whose scrutinee is `p`, look at each
|
||||
pattern-binder.
|
||||
- If any **heap-typed** pattern-binder has `consume_count > 0`,
|
||||
`p` must be `Own` (sub-consume forces ownership). Lint stays
|
||||
silent.
|
||||
- Otherwise, lint fires.
|
||||
|
||||
### The heap-type filter (design call by implementer)
|
||||
|
||||
The orchestrator's literal brief said "any pattern-binder
|
||||
`consume_count > 0` → silent." That contradicted the brief's own
|
||||
positive test (`head_or_zero` MUST fire), because `match xs {
|
||||
Cons(h, t) => h }` records `consume_count(h) == 1` regardless of
|
||||
`h: Int` being a primitive. Implementer caught this, made the
|
||||
design call, documented it in the module-doc rationale.
|
||||
|
||||
The semantic justification: reading a primitive (`h: Int`) is a
|
||||
load-by-value, no RC traffic, no heap data moved out of the
|
||||
scrutinee's allocation. Reading a heap-typed sub-binder
|
||||
(`t: IntList`) IS a heap-pointer move that requires owning the
|
||||
outer cell. So the filter — "skip primitive-typed pattern-binders
|
||||
when judging sub-consume" — is exactly what makes the lint
|
||||
correct.
|
||||
|
||||
This is a real design point that 19a's brief did not document.
|
||||
Recording it here as a ratification of the implementer's call.
|
||||
|
||||
### Corpus signal after upgrade
|
||||
|
||||
Five of 65 fixtures now produce at least one `over-strict-mode`
|
||||
warning:
|
||||
|
||||
- `rc_app_let_partial_drop_leak`
|
||||
- `rc_drop_iterative_long_list`
|
||||
- `rc_match_arm_partial_drop_leak`
|
||||
- `rc_own_param_drop`
|
||||
- `rc_own_param_partial_drop_leak`
|
||||
|
||||
All five are RC codegen-test fixtures — the `(own T)` annotation
|
||||
is intentional, used to exercise the codegen path that drops Own-
|
||||
params at fn return / arm close. They are NOT incorrect annotations;
|
||||
they are deliberate test infrastructure.
|
||||
|
||||
This is the signal that **justifies 19b** (`mode-strict-because`
|
||||
suppression). Without it, every RC-codegen-test fixture would
|
||||
permanently emit a warning despite being correct-by-design. With
|
||||
it, each fixture annotates its strictness intentionally, the
|
||||
warning suppresses, future-LLM reads the reason, future-iter
|
||||
edits know whether to preserve or relax.
|
||||
|
||||
19b is dispatched next.
|
||||
|
||||
### Known debt
|
||||
|
||||
**Deeply-nested-match-on-sub-binder** (recorded by implementer in
|
||||
the module doc): `match p { Ctor(_, t) => match t { Ctor(_, t2)
|
||||
=> consume(t2) } }` would produce a *spurious* warning under the
|
||||
current rule — `t.consume_count == 0` (matching is Borrow), so
|
||||
the inner consume of `t2` doesn't propagate up to `p`'s view.
|
||||
None of the 65 fixtures hit this shape; queued for follow-up if
|
||||
real corpus surfaces it.
|
||||
|
||||
### Files / tests
|
||||
|
||||
- `crates/ailang-check/src/linearity.rs` — module-doc § 19a.1
|
||||
rationale, `is_heap_type` helper, `any_sub_binder_consumed_for`
|
||||
+ `pattern_has_consumed_heap_binder` walkers, ctor-table
|
||||
threading through `check_module` / `check_fn`.
|
||||
- Tests: `over_strict_mode_conservative_skips_match_on_param`
|
||||
flipped to positive `over_strict_mode_fires_when_match_arm_uses_no_sub_binder`.
|
||||
New `over_strict_mode_silent_when_match_arm_consumes_sub_binder`
|
||||
and `over_strict_mode_fires_when_match_arm_binders_unused`.
|
||||
|
||||
`ailang-check` test count: 53 → 55. Workspace build/test green.
|
||||
|
||||
Reference in New Issue
Block a user