check: over-strict-mode lint (iter 19a)
When a fn-param is annotated (own T) but the body never consumes it (consume_count == 0) and never destructures it via match, the linearity pass now emits a Severity::Warning diagnostic with a form-A-rendered (borrow T) rewrite as suggested_rewrite. Pure advisory, Decision 10 unchanged. First Warning-severity diagnostic the typechecker emits; three CLI exit paths gated on Error-only. JOURNAL: design entry + iter 19a shipping entry.
This commit is contained in:
+150
@@ -8591,3 +8591,153 @@ implementation" — it is the literal RED-step that the discipline
|
||||
prescribes. The three fixtures shipped here are exactly that:
|
||||
each pins one of the three carve-out sites, observable via the
|
||||
RC stats line, kept as regression. JOURNAL queue: empty.
|
||||
|
||||
## 2026-05-08 — Design: explicit annotations stay mandatory; `over-strict-mode` diagnostic
|
||||
|
||||
User pushback on the queue-driven dispatch: the orchestrator
|
||||
floated "uniqueness inference replaces fn-signature mode
|
||||
annotations" as a candidate direction. Re-read of DESIGN.md
|
||||
§ "Decision 10" (lines 685–795) shows that pitch was a
|
||||
**Decision-10 reversal**, not an iter within it:
|
||||
|
||||
> *"AILang makes it mandatory because the LLM author can carry
|
||||
> the cognitive cost trivially, and the compiler gains a precise
|
||||
> contract at every call site instead of a probabilistic guess."*
|
||||
|
||||
Three reasons mandatory annotations are not redundancy in the
|
||||
harmful sense:
|
||||
|
||||
1. **Inference picks weakest-supporting; annotation states
|
||||
intent.** These often coincide today but are conceptually
|
||||
different. The annotation captures what the author committed
|
||||
to, not what the current body needs (e.g. `(own T)` reserved
|
||||
for a planned in-place mutation that hasn't landed).
|
||||
2. **Annotation is a drift-bremse.** Body change that flips
|
||||
the inferred mode → caller-side breakage at remote sites.
|
||||
Annotation enforces the local-conflict-error pattern instead.
|
||||
3. **Forcing function specific to LLM authoring.** Without
|
||||
mandatory annotation, an LLM never has to commit to ownership
|
||||
intent before writing the body — the contract becomes a
|
||||
by-product of local code choice. With it, the contract is a
|
||||
first-order variable the body must satisfy.
|
||||
|
||||
The "redundancy" between annotation and body is therefore the
|
||||
feature, not the bug — analogous to test code redundantly
|
||||
restating implementation behaviour, where the redundancy is what
|
||||
catches drift.
|
||||
|
||||
### What survives of the original pitch
|
||||
|
||||
One narrow but real observation: today, when the author writes
|
||||
`(own T)` and the body would compile under `(borrow T)`, the
|
||||
typechecker silently accepts. No warning, no diagnostic. The
|
||||
runtime cost is real (one inc/dec pair per call, plus potential
|
||||
rec-cascade at fn return), but invisible.
|
||||
|
||||
The Rust analogue is `clippy::needless_pass_by_value`: an
|
||||
informational lint with a suppression mechanism. AILang sharpens
|
||||
this to "suppression carries a mandatory reason" — consistent
|
||||
with the explicit-intent philosophy.
|
||||
|
||||
### Iter scope
|
||||
|
||||
Two iters, dispatched separately so 19a's diagnostic frequency
|
||||
on real code informs whether 19b's escape hatch is needed at all:
|
||||
|
||||
- **Iter 19a — `over-strict-mode` diagnostic.** Linearity pass
|
||||
detects: `param_modes[i] == Own` + `consume_count(p) == 0` +
|
||||
no consume of any sub-binder of p in the body. Emits
|
||||
`Severity::Warning` (the first warning-level diagnostic the
|
||||
typechecker has emitted; reserves no longer reserved). Carries
|
||||
a `suggested_rewrite` showing the relaxed `(borrow T)`
|
||||
signature. No schema change; pure detection.
|
||||
|
||||
- **Iter 19b — `mode-strict-because` suppression** (deferred
|
||||
pending 19a's signal). Optional schema field on fn defs:
|
||||
`suppress: [{code: String, because: String}]`. `because` is
|
||||
non-empty (schema-validation error otherwise). The suppress
|
||||
mechanism is generic across diagnostic codes but the only
|
||||
consumer initially is `over-strict-mode`.
|
||||
|
||||
### Why split
|
||||
|
||||
19a alone is shippable: users who see the warning either accept
|
||||
the rewrite or live with the noise on intentional-strict fns.
|
||||
19b is only worth building if real code surfaces enough
|
||||
intentional-strict cases to make the noise unbearable. This lets
|
||||
the data drive whether 19b ships at all.
|
||||
|
||||
### What this is NOT
|
||||
|
||||
Not a relaxation of Decision 10. Annotations stay mandatory.
|
||||
Authors cannot omit `param_modes` / `ret_mode`; the compiler
|
||||
will not infer them. The diagnostic is purely advisory: "you
|
||||
wrote this, here's a tighter alternative." If suppressed (19b),
|
||||
the annotation stays exactly as authored.
|
||||
|
||||
## 2026-05-08 — Iter 19a: `over-strict-mode` diagnostic shipped
|
||||
|
||||
The diagnostic-side of the design entry above. Linearity pass
|
||||
gained a post-walk loop over `Own`-annotated params; uniqueness
|
||||
side-table provides `consume_count`. When `consume_count == 0`
|
||||
*and* the body never destructures the param via match, the lint
|
||||
fires with a form-A-rendered fn-type rewrite as `suggested_rewrite`.
|
||||
|
||||
### Conservative cut
|
||||
|
||||
The precise rule is "no consume of any sub-binder of `p`". The
|
||||
shipped check approximates with "no `match` whose scrutinee is
|
||||
`pname`" — sound (never false-positive) but incomplete (misses
|
||||
match-on-p-without-sub-consume). Documented in `linearity.rs`'s
|
||||
module doc and pinned by the test
|
||||
`over_strict_mode_conservative_skips_match_on_param`. Acceptable
|
||||
because the lint is purely advisory: a missed warning costs
|
||||
nothing, a spurious warning would actively mislead. The precise
|
||||
variant is queued for a follow-up if real-corpus signal warrants.
|
||||
|
||||
### CLI side-effect
|
||||
|
||||
This is the first `Severity::Warning` diagnostic the typechecker
|
||||
emits. Three CLI exit paths (`Cmd::Check` non-JSON, `Cmd::EmitIr`,
|
||||
`build_to`) previously aborted on any non-empty diagnostic list;
|
||||
now they only abort when at least one diagnostic is `Severity::Error`.
|
||||
The `--json` path was already correct (returns the list, doesn't
|
||||
exit). No observable behaviour change for any pre-19a input —
|
||||
nothing emitted Warning before this iter — but the gate is now
|
||||
in place for future warning-level lints.
|
||||
|
||||
### Tests
|
||||
|
||||
Four new tests in `linearity.rs::tests`:
|
||||
|
||||
- `over_strict_mode_fires_when_param_only_borrowed` — positive.
|
||||
- `over_strict_mode_silent_when_body_consumes_param` — negative.
|
||||
- `over_strict_mode_silent_when_param_is_borrow` — negative.
|
||||
- `over_strict_mode_conservative_skips_match_on_param` — pins the
|
||||
conservatism; flips to a positive assertion when the precise
|
||||
variant lands.
|
||||
|
||||
`ailang-check` test count: 49 → 53. e2e unchanged at 69. Workspace
|
||||
build + test green.
|
||||
|
||||
### Files touched
|
||||
|
||||
- `crates/ailang-check/src/diagnostic.rs` — registered
|
||||
`over-strict-mode` code, refreshed `Severity::Warning` doc,
|
||||
added `Diagnostic::warning` ctor.
|
||||
- `crates/ailang-check/src/linearity.rs` — module-doc § 19a, the
|
||||
post-walk loop, helpers (`body_matches_on`,
|
||||
`scrutinee_matches_param`, `make_over_strict_mode`,
|
||||
`relax_param_to_borrow`), `check_fn` signature gained a
|
||||
`&UniquenessTable` arg.
|
||||
- `crates/ailang-surface/src/print.rs` — new
|
||||
`pub fn type_to_form_a(&Type) -> String` (re-exported from `lib.rs`).
|
||||
- `crates/ail/src/main.rs` — three exit-on-Error gates.
|
||||
|
||||
### What stays queued
|
||||
|
||||
- Iter 19b (`mode-strict-because` suppression): waits for
|
||||
real-corpus signal on whether intentional-strict fns are
|
||||
common enough to warrant the schema field.
|
||||
- Precise sub-binder analysis for the lint: waits for evidence
|
||||
that the conservative cut misses real cases.
|
||||
|
||||
Reference in New Issue
Block a user