brink_ir/hir/diagnostics.rs
1//! Diagnostic codes, severities, and the [`Diagnostic`] record.
2//!
3//! Split out of [`super::types`] (issue #652). The stable [`DiagnosticCode`]
4//! catalogue and its lookup tables are touched by every diagnostic-adding
5//! change, while the HIR node definitions next door are touched by every
6//! language-feature change; keeping the two in separate files keeps those
7//! streams of work from colliding.
8//!
9//! Everything here is re-exported through `hir::*`, so consumers keep
10//! importing these names from `brink_ir::hir` exactly as before.
11
12use rowan::TextRange;
13
14use super::types::FileId;
15
16/// A diagnostic produced during HIR lowering or cross-file analysis.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Diagnostic {
19 /// Which file this diagnostic belongs to.
20 pub file: FileId,
21 /// The source span this diagnostic points at.
22 pub range: TextRange,
23 /// Human-readable message describing the problem.
24 pub message: String,
25 /// Structured error code for documentation and tooling.
26 pub code: DiagnosticCode,
27}
28
29/// How seriously a diagnostic should be treated by a consumer (CLI renderer,
30/// LSP client, editor diagnostics panel).
31///
32/// Until issue #1674, no `DiagnosticCode`'s *default* severity
33/// ([`DiagnosticCode::severity`]) was ever `Info` or `Hint` — the two
34/// advisory tiers existed only so a project's `[lints]` table
35/// (`brink-project-config`'s `LintLevel::Info`/`LintLevel::Hint`, resolved
36/// through `brink_analyzer::effective_severity`) could opt a `Warning`-default
37/// code down to one when a squiggle is too loud (issue #1162). Moving any
38/// *existing* code's default into one of these tiers is a separate decision,
39/// deliberately not made by the issue that introduced the tiers.
40/// [`DiagnosticCode::E157`] (issue #1674) is the first code to default here
41/// directly — RULED "off or info by default" for a narrow, precision-tuned
42/// lint that must not nag a single-shot project.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Severity {
45 /// Blocks compilation / is surfaced as a hard failure.
46 Error,
47 /// Non-fatal; the default tier for advisory diagnostics until a
48 /// `[lints]` override says otherwise.
49 Warning,
50 /// Advisory, LSP `DiagnosticSeverity::INFORMATION` — worth telling the
51 /// author about, but not something they need to act on.
52 Info,
53 /// Advisory and quiet, LSP `DiagnosticSeverity::HINT` — the tier IDEs use
54 /// for things like unused-symbol dimming, where even an info-level
55 /// squiggle is too loud.
56 Hint,
57}
58
59/// Stable error codes for brink diagnostics.
60///
61/// Codes are never reused once assigned. Each code has a corresponding
62/// explanation file at `docs/diagnostics/Exxx.md`.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum DiagnosticCode {
65 // ── Containers ──────────────────────────────────────────────
66 /// Knot definition is missing a name.
67 E001,
68 /// Stitch definition is missing a name.
69 E002,
70 /// Knot or stitch parameter is missing a name.
71 E003,
72
73 // ── Declarations ────────────────────────────────────────────
74 /// `VAR` declaration is missing a name.
75 E004,
76 /// `VAR` declaration is missing an initializer.
77 E005,
78 /// `CONST` declaration is missing a name.
79 E006,
80 /// `CONST` declaration is missing an initializer.
81 E007,
82 /// `LIST` declaration is missing a name.
83 E008,
84 /// `LIST` member is missing a name.
85 E009,
86 /// `EXTERNAL` declaration is missing a name.
87 E010,
88 /// RETIRED (lane-A audit, #709) — the parser always materializes a
89 /// `FILE_PATH` node inside `INCLUDE_STMT` (possibly empty) and reports
90 /// missing path as E037 (`parser/declaration.rs::include_statement`).
91 /// Code kept reserved, not reused.
92 E011,
93
94 // ── Control flow ────────────────────────────────────────────
95 /// Divert is missing a target.
96 E012,
97 /// RETIRED (lane-A audit, #709) — `parser/divert.rs::path` always creates
98 /// a `PATH` node (empty on error + E037), so `ThreadStart::target()` is
99 /// never `None`. Code kept reserved, not reused.
100 E013,
101 /// Logic line has no effect (bare `~`).
102 E014,
103
104 // ── Expressions ─────────────────────────────────────────────
105 /// Expression is missing an operand.
106 E015,
107 /// Unknown or unsupported operator.
108 E016,
109 /// Function call is missing a name.
110 E017,
111 /// RETIRED (lane-A audit, #709) — `parser/divert.rs::path` always creates
112 /// a `PATH` node (empty on error + E037), so `DivertTargetExpr::target()`
113 /// is never `None`. Code kept reserved, not reused.
114 E018,
115
116 // ── Choices ─────────────────────────────────────────────────
117 /// RETIRED (lane-A audit, #709) — the parser only builds a `CHOICE` node
118 /// after seeing a bullet token, so a bullet-less choice CST cannot exist.
119 /// Code kept reserved, not reused.
120 E019,
121
122 // ── Inline logic ────────────────────────────────────────────
123 /// Inline conditional is missing a condition.
124 E020,
125 /// Inline sequence has no branches.
126 E021,
127
128 // ── Cross-file analysis ──────────────────────────────────────
129 /// Duplicate knot definition.
130 E022,
131 /// Duplicate variable/constant definition.
132 E023,
133 /// Unresolved divert target.
134 E024,
135 /// Unresolved variable reference.
136 E025,
137 /// Duplicate list item.
138 E026,
139 /// Ambiguous bare list item reference.
140 E027,
141 /// RETIRED (lane-A audit, #709) — circular INCLUDE is detected at the
142 /// discovery phase and surfaces as `CompileError::CircularInclude`, not as
143 /// a per-construct diagnostic. Code kept reserved, not reused.
144 E028,
145
146 // ── Compile errors ────────────────────────────────────────────
147 /// Choice nested in conditional without explicit divert.
148 E029,
149
150 // ── Warnings ─────────────────────────────────────────────────
151 /// String interpolation in constant initializer is ignored.
152 E030,
153 /// Function call argument count mismatch.
154 E031,
155
156 // ── Structural validation ───────────────────────────────────
157 /// Return statement outside function.
158 E032,
159 /// Unreachable code after divert.
160 E033,
161 /// Choice set has only fallback choices.
162 E034,
163 /// Name shadows a built-in function.
164 E035,
165 /// Expected diagnostic not produced (`// brink-expect`).
166 E036,
167 /// Syntax error reported by the parser (malformed source).
168 E037,
169 /// Malformed `///` doc-comment tag on a declaration.
170 E038,
171
172 // ── Host manifest (external-function vocabulary) ─────────────
173 /// Registered host manifest disagrees with the ink `EXTERNAL` arity.
174 E039,
175 /// Doc-comment / manifest references an unknown semantic type.
176 E040,
177 /// External call argument type mismatches the manifest signature.
178 E041,
179 /// External call argument violates a closed-domain constraint.
180 E042,
181 /// Well-formed `///` doc-comment tag that doesn't apply to this
182 /// declaration kind (e.g. `@kind` on a knot, `@param` on a VAR).
183 E043,
184
185 // ── Directives (`#@…` — docs/directive-annotations-spec.md) ──
186 /// Unknown directive name (e.g. `#@locale`).
187 E044,
188 /// Directive has no valid target in this position.
189 E045,
190 /// Directive contains dynamic inline logic — directives are static text.
191 E046,
192 /// Directive must be the only tag on its line.
193 E047,
194 /// Duplicate directive on one target.
195 E048,
196 /// Directive not supported on this target (e.g. `@local` on CONST).
197 E049,
198 /// Directive does not take arguments or trailing text.
199 E050,
200
201 // ── T1b dialect gate (docs/t1b-surface-spec.md §1) ────────────
202 /// A brink-extension construct (block, sigil literal, indexing) was
203 /// used under the `strict-ink` dialect.
204 E051,
205 /// A brink-extension construct parses and analyzes cleanly under the
206 /// `brink` dialect, but its LIR lowering hasn't landed yet. Originally
207 /// minted for T1b-1 (every T1b construct lowers since T1b-2, #570), then
208 /// revived by T1c-1 (#699) as the `#fn(…)` lowering fence, retired again by
209 /// T1c-2 (#700). **Now the `await` fence** (FS-2,
210 /// docs/flow-suspension-spec.md §3, issue #928): `await <cond>` /
211 /// `while await <cond>` parse to HIR and pass the effect-free purity gate
212 /// (E105), but their runtime spill/restore semantics are FS-3 — every
213 /// `await` construct is fenced here at LIR lowering until that lands. The
214 /// code stays a general "parses/analyzes before its lowering lands" fence,
215 /// reused as each new extension needs it.
216 E052,
217 /// RETIRED (T1b-2, #570) — previously a non-suppressible backstop
218 /// rejecting T1b brink-extension HIR nodes (`LogicBlock`, `ArrayLiteral`,
219 /// `MapLiteral`, `Index`) at LIR lowering. T1b-2 completed real lowering
220 /// for all such constructs, making the backstop obsolete. Code kept
221 /// reserved, not reused, for diagnostic-code stability.
222 E053,
223 /// A block-scoped `temp` (`~ { … }`, docs/t1b-surface-spec.md §2) or
224 /// `for` loop variable shadows an already-visible temp/param — either an
225 /// enclosing `~ { … }` block scope or an outer classic `~ temp`.
226 E054,
227
228 // ── T1b stdlib slice 1 (docs/t1b-surface-spec.md §5) ──────────────
229 /// `push`/`insert`/`remove`'s first argument is not an lvalue (a
230 /// variable, temp, or indexed path) — mutators require a place to
231 /// write the mutated container back into.
232 E055,
233 /// `push`/`insert`/`remove` was used in expression position — they
234 /// return nothing and are only valid as a statement.
235 E056,
236
237 // ── T1b logic blocks (docs/t1b-surface-spec.md §2) ────────────────
238 /// `break`/`continue` used outside any enclosing `while`/`for` loop.
239 E057,
240 /// Collection mutator (`push`/`insert`/`remove`) called with the wrong
241 /// number of arguments — a targeted compile error naming the expected
242 /// signature (replaces the generic `E031` warning + silently-dropped
243 /// RMW lowering, RULED 2026-07-12, see `docs/decision-log.md`).
244 E058,
245
246 // ── Weave-in-inline-content backstop (sibling of #578, #585) ──────
247 /// A choice set, labeled gather block, multi-line conditional, or
248 /// sequence was found nested inside inline content (e.g. a choice's own
249 /// display/bracket/inner text) where it would need a child container
250 /// that position structurally cannot hold.
251 E059,
252
253 // ── Codegen defense-in-depth backstop (#586) ──────────────────────
254 /// `brink-codegen-inkb` refused to emit bytecode for a `Program` that
255 /// violates an invariant an earlier, non-suppressible compiler stage is
256 /// supposed to guarantee (currently: an out-of-loop `LogicBreak`/
257 /// `LogicContinue`, normally rejected at `E057`). Reaching this from a
258 /// normal compile is a compiler bug, not an authoring mistake — this
259 /// code exists so that bug fails loudly instead of silently corrupting
260 /// bytecode.
261 E060,
262
263 // ── TM-2 inline type annotations (docs/typed-mode-spec.md §3) ────
264 /// A type annotation names something that isn't a recognized nominal
265 /// type (`int`/`float`/`bool`/`string`/`divert`/`void`), a `List<L>`
266 /// naming a declared `LIST`, `Array<T>`, or `Map<K, V>` — declared
267 /// struct names arrive in TM-4.
268 E061,
269 /// RETIRED (T1c-1, #699): previously "`fn(T…): R` function-type
270 /// annotation used — parses, but types as reserved until T1c". T1c
271 /// unfroze the form (docs/t1c-spec.md §4: "boundary annotations gain
272 /// the `fn(T…): R` form"), so it now resolves to a real checker type.
273 /// Code kept reserved, not reused, for diagnostic-code stability — no
274 /// longer emitted by any pass.
275 E062,
276 /// A param/return/`VAR` type annotation disagrees with the type
277 /// TM-1's body inference would otherwise derive. Advisory only in this
278 /// slice (gradual policy) — strict-mode severity is TM-3's call.
279 E063,
280
281 // ── TM-3 strict typed-mode policy (docs/typed-mode-spec.md §1/§9-3) ──
282 /// `types = strict` was requested but the project's dialect isn't
283 /// `brink` — strict typing is a brink-dialect extension (its annotation
284 /// syntax is extension syntax), so `types = strict` + `dialect =
285 /// strict-ink` is a config error, not a per-construct diagnostic.
286 E064,
287 /// Under `types = strict`, a def's inferred signature or body slot
288 /// (param, return, or temp) resolved to `Unknown` after the SCC
289 /// fixpoint with no annotation to supply a concrete type — "annotate or
290 /// restructure" (spec §1). Legal under `types = gradual`.
291 E065,
292 /// Under `types = strict`, a def's inferred signature or body slot
293 /// resolved to `Ty::Conflicted` (#627) — the body's own uses disagree
294 /// on the slot's type. Legal (advisory-only, unreported) under `types =
295 /// gradual`.
296 E066,
297 /// Under `types = strict`, a `~ x = f()` / `~ temp x = f()` assigns the
298 /// result of a call whose resolved def is a `void`-returning function
299 /// (docs/typed-mode-spec.md §3: "assigning a `void` call is an error in
300 /// strict mode"). Only the assignment/temp-decl's RHS *root* call is
301 /// checked — a statement-position call (`~ f()`) or a call nested inside
302 /// interpolation is never flagged. Never emitted under `types = gradual`.
303 E067,
304
305 // ── TM-4b structs (docs/typed-mode-spec.md §6) ────────────────────
306 /// A struct construction literal's leading shape name (`Name#{…}`)
307 /// doesn't name any declared `STRUCT`.
308 E068,
309 /// Under `types = strict`, a struct construction literal omits a
310 /// declared field — names the missing field.
311 E069,
312 /// A struct construction literal supplies a field the shape doesn't
313 /// declare — names the extra field.
314 E070,
315 /// Under `types = strict`, a struct construction literal's field
316 /// initializer disagrees with the field's declared type — names the
317 /// field.
318 E071,
319 /// RETIRED (TM-4c, #666): previously a non-suppressible backstop
320 /// rejecting *every* struct construct/field access reaching LIR
321 /// lowering, back when codegen for structs didn't exist yet. Structs
322 /// now lower for real (`E073` is TM-4c's narrower replacement
323 /// backstop). Code kept reserved, not reused, for diagnostic-code
324 /// stability — no longer emitted by any pass.
325 E072,
326 /// Non-suppressible defense-in-depth backstop, mirroring `E053`/`E060`/
327 /// (former) `E072`: a struct construction literal referencing a shape
328 /// name that doesn't resolve to any declared `STRUCT` reached LIR
329 /// lowering. Reaching this from a normal compile means
330 /// `brink-analyzer`'s `resolve::resolve_struct_ref` diagnostic (`E068`)
331 /// was suppressed (`// brink-disable-all`), not a compiler bug on its
332 /// own — `RecordNew` needs a real `ShapeId` at compile time; there is no
333 /// dynamic "construct with unknown shape" concept in this design.
334 E073,
335 /// A field-write target (`p.field = expr`) is a *chained* projection —
336 /// `p.a.b = v` or a mixed `p.a[i].b = v` — never a bare `ident.field`
337 /// on a resolvable root. TM-4c ships single-level field writes only
338 /// (mirrors `lower_indexed_assignment`'s `n == 1` fast path); chained
339 /// writes are an explicit, permanent T1e boundary (`docs/
340 /// typed-mode-spec.md` §6), not a "not implemented yet" gap — this is a
341 /// real, reachable, non-suppressible diagnostic authors can hit by
342 /// writing ordinary (if currently unsupported) ink, not a defensive
343 /// backstop for a suppressed analysis diagnostic.
344 ///
345 /// Also covers a write ending in an *index* rather than a field, whose
346 /// index chain's root is itself a struct-field projection or a mixed
347 /// index/field access — `p.field[i] = v`, `p.a[i].b[j] = v`, or the
348 /// mutator spelling (`push(p.field[i], v)`) — same T1e boundary,
349 /// reached via `reject_field_projection_index_root` (issue #2121) from
350 /// `lower_indexed_assignment`/`lower_lvalue_container_chain` rather than
351 /// `try_lower_field_assignment`.
352 E074,
353
354 // ── decls constant-folding backstops (#673) ───────────────────────
355 /// A struct construction literal used as a `VAR`/`CONST` declaration
356 /// default doesn't match its declared shape: it omits a declared field,
357 /// or supplies one the shape doesn't declare.
358 ///
359 /// A *well-formed* construction literal is a legal declaration default
360 /// (issue #1530): `eval_const_struct_literal` folds it into
361 /// `lir::ConstValue::Record`, which is what makes a struct-typed durable
362 /// global — and therefore the T1e projection-receiver path
363 /// (`docs/t1e-spec.md` §2, which requires a durable root) — spellable at
364 /// all. Before #1530 this code was the blanket refusal of *every* struct
365 /// literal in that position, because `ConstValue` had no record-carrying
366 /// variant.
367 ///
368 /// Mid-story `p = Point#{…}` construction with a mismatched shape is a
369 /// runtime construction fault (`RecordNew` against an invalid shape id,
370 /// value-model-spec §11c's gradual path); a declaration default is baked
371 /// into `StoryData` with no runtime construction step to fault at, so
372 /// this is the compile-time equivalent — a real, non-suppressible error,
373 /// never a half-built record. Under `types = strict` `brink-analyzer`'s
374 /// `structs::check` reports the more precise [`Self::E069`]/
375 /// [`Self::E070`] for the same literal; this backstop is
376 /// policy-independent.
377 E075,
378 /// A map literal used as a `VAR`/`CONST` declaration default has a key
379 /// that isn't a compile-time-constant scalar in the ratified map-key
380 /// domain (int/string/bool — value-model-spec §4). Mid-story map
381 /// construction (`MapNew`) faults on this at runtime
382 /// (`InvalidMapKeyType`); a declaration default has no runtime
383 /// construction step to fault at, so this is the compile-time
384 /// equivalent — a real error, never a silent `Null`.
385 E076,
386 /// An array element, map value, struct field, or `#fn` bound `val` arg
387 /// nested inside a `VAR`/`CONST` declaration default has a source
388 /// expression kind that can never constant-fold — a function call,
389 /// postfix indexing, field access, `++`/`--`, or (#743) a bare
390 /// reference to another `VAR`. A declaration default is baked into
391 /// `StoryData` at compile time, so there is no runtime construction
392 /// step left to evaluate the element at; without this diagnostic the
393 /// element recursed into `eval_const_expr`'s `Path`
394 /// (`SymbolKind::Variable`) arm or catch-all and silently became `Null`
395 /// — #673's silent-`Null` bug one level down, inside the literal (#679
396 /// review; the `Path`-to-`Variable` case one level in was left
397 /// deliberately unchanged there and closed by #743). Keyed off the
398 /// source expression *kind*, never the folded result: an `Expr::Null`
399 /// produced by HIR error recovery must not double-report, and a `Path`
400 /// resolving to a `CONST`/list item/knot/stitch/function still folds
401 /// for real and is not flagged — only a resolved `SymbolKind::Variable`
402 /// (or an unresolved path, left to the analyzer's own diagnostic) is
403 /// exempt from the fold-for-real behavior, matching
404 /// `is_const_foldable_decl_default`'s top-level twin (`E083`). (Since
405 /// #1530 a struct literal at this position folds for real, so a
406 /// never-foldable *field* of a nested construction literal reaches this
407 /// arm exactly as an array element or map value does; before #1530 the
408 /// whole literal was unconditionally `E075` regardless of field
409 /// content.)
410 E077,
411 // ── TM-3 completion: conversion intrinsics (docs/typed-mode-spec.md
412 // §4, maintainer ruling 2026-07-13, issue #659) ──────────────────────
413 /// Under `types = strict`, an unresolved (builtin, not author-shadowed)
414 /// call to `int(x)`/`float(x)` where `x` is statically a divert-target,
415 /// LIST, array, map, or struct construction literal — outside the
416 /// permissive numeric+bool domain (ruling 2: "compile error under
417 /// `types = strict`, runtime fault under gradual"). `string(x)` accepts
418 /// every type and is never checked here.
419 E078,
420
421 // ── T1c function values (docs/t1c-spec.md §2/§8, issue #699) ─────
422 /// `#fn(name, …)`'s target does not resolve to a statically-named
423 /// function definition (`=== function name ===`) — it resolved to a
424 /// variable/list/external/label/non-function knot or stitch, or it
425 /// names a builtin/stdlib intrinsic (which has no definition to take a
426 /// token of). Only fires under `dialect = brink` — under `strict-ink`
427 /// the whole literal is already rejected as extension syntax (E051),
428 /// and content diagnostics on rejected syntax are noise (the TM-2
429 /// suppression precedent, maintainer ruling 2026-07-13).
430 E079,
431 /// A `ref` param of a `#fn` target is not bound in the creation-site
432 /// prefix, or is bound to a non-durable lvalue. All `ref` params must
433 /// be bound at creation, and each must capture a durable cell — a
434 /// global `VAR` (flow-local `#@local` VARs included); a `temp`/param
435 /// is a compile error (temps die with the frame, value-model §11), a
436 /// `CONST` is not a mutable cell, and a bare (unmarked) rvalue/field
437 /// reference is not a cell at all.
438 ///
439 /// T1e (docs/t1e-spec.md §2/§6, issue #831) extends this same code —
440 /// "reuse the E080-family message shape" — to the explicit `ref
441 /// lvalue-path` projection form (`heal(ref npc.hp, 5)`,
442 /// `#fn(heal, ref party[leader].hp)`, `bind(f, ref inventory[idx])`):
443 /// the *root* of the path (the innermost variable the segments walk
444 /// from) must still be a durable global `VAR`, by the same rule —
445 /// `temp`/param roots remain a compile error, a `CONST` root is not a
446 /// mutable cell. A projection's own *segments* (dotted fields, `[…]`
447 /// indices) are a separate check (`E098`, strict-mode statically-known
448 /// shapes only) — this code is the root-durability obligation alone.
449 E080,
450 /// `#fn(name, args…)` binds more arguments than the target declares —
451 /// the bound-arg row is a *prefix* of the declared param row
452 /// (docs/t1c-spec.md §2: "binding more args than the target declares
453 /// is a compile error").
454 E081,
455
456 // ── T1b block-temp scoping (docs/t1b-surface-spec.md §2, issue #680) ──
457 /// A T1b block-scoped `temp` (`~ { … }`) — or a `for`-loop variable,
458 /// which desugars the same way — was referenced (by value or by `ref`
459 /// argument) after its own `~ { … }`/`while`/`for`/`if` block already
460 /// closed. Root-caused for #680: LIR lowering's fallback for "temp not
461 /// currently visible" (used for inklecate-compat forward-reference
462 /// emulation of *classic* temps) previously also caught this case,
463 /// silently emitting a phantom hashed `GetGlobal`/`RefGlobal` id that
464 /// was never registered as a real global — a runtime-only
465 /// `UnresolvedGlobal` fault with no compile diagnostic.
466 E082,
467
468 // ── Declaration-default constness, top level (issue #692, sibling to
469 // #673/#679's collection-element E075/E076/E077) ─────────────────────
470 /// A scalar `VAR`/`CONST` declaration default whose *source expression
471 /// kind* can never be a compile-time constant — a bare reference to
472 /// another `VAR` (`VAR x = someOtherVar`) or a function call
473 /// (`VAR x = f()`), including either wrapped in a prefix/infix
474 /// operation. `eval_const_expr`'s `Path` arm (`SymbolKind::Variable`)
475 /// and its catch-all previously folded both silently to `Null` with no
476 /// diagnostic — the same silent-fold bug #673/#679 fixed one level
477 /// down, inside array/map/struct literals, left unfixed at this top
478 /// level. Keyed off the source expression kind, never the folded
479 /// result, same as `E077`. Does not fire for a `Path` nested inside a
480 /// collection/struct/fn literal (array element, map value, struct
481 /// field, `#fn` argument) — those recurse through their own existing
482 /// `E075`/`E076`/`E077` per-element checks one level in, which
483 /// deliberately still leave a `VAR`-reference gap unchanged (#679 scope
484 /// notes) pending its own follow-up.
485 E083,
486
487 // ── TM-5 struct construction literals (docs/typed-mode-spec.md §6,
488 // decision-log "Struct construction literals: source-order evaluation,
489 // duplicate field is a compile error" 2026-07-14, issues #675/#676) ──
490 /// A struct construction literal (`Name#{…}`) supplies the same field
491 /// name more than once. Previously a silent last-wins: only the final
492 /// initializer's value was placed, and — because the well-formed
493 /// `RecordNew` lowering path discarded every non-placed lowered
494 /// expression tree wholesale — an earlier duplicate's initializer
495 /// (including any observable side effect, e.g. a function call) never
496 /// actually ran at all, with no diagnostic (#675's RCA). Now a real
497 /// compile error naming the repeated field, under both
498 /// `types = gradual` and `types = strict` — unlike `E069`/`E070`/
499 /// `E071` (which need a resolved shape to check missing/extra/mistyped
500 /// fields against, and are strict-mode-only), a duplicate field is a
501 /// structural authoring mistake detectable from the literal alone,
502 /// independent of type-checking policy or whether the shape name even
503 /// resolves.
504 E084,
505
506 // ── M-1 modules (docs/modules-spec.md §1/§5) ──────────────────
507 /// An *undeclared* file whose module (its file stem) collides with a
508 /// *declared* module's name (`#@module(name)` elsewhere). Accidental
509 /// membership with mixed visibility defaults is the one footgun the
510 /// module model forbids (modules-spec §1). Fix: declare the file with
511 /// the same `#@module(name)`, or rename it.
512 E085,
513 /// A malformed `#@module(…)` directive: a missing or empty name
514 /// argument, or a second `#@module` in the same file. `#@module`
515 /// takes exactly one non-empty module name and appears at most once
516 /// per file (modules-spec §1).
517 E086,
518
519 // ── M-2 imports + visibility (docs/modules-spec.md §2/§4/§7) ───
520 /// A reference resolves to a `#@private` definition in another module.
521 /// Private names are module-internal; the referrer is outside that
522 /// module. Fix: make the definition `#@public` and `IMPORT` it, or move
523 /// the reference into the module (modules-spec §4/§7).
524 E087,
525 /// A bare-form `IMPORT { name } FROM mod` / native `use mod::name;`
526 /// whose trailing segment `name` names neither a definition `mod`
527 /// publicly exports **nor a declared submodule of `mod`** (dual-reading,
528 /// issue #1592 — a trailing segment that resolves to a module licenses
529 /// it instead, matching Rust's `use`; §13.2). Only enforced against
530 /// *declared* modules — an import naming an unknown/undeclared module is
531 /// not itself flagged by this code, since that module's export/submodule
532 /// set isn't visible to the check (modules-spec §2/§7).
533 E088,
534 /// An `IMPORT` brings the same local name into scope twice (a repeated
535 /// bare import, or two imports whose names/aliases collide) — the
536 /// reference would be ambiguous (modules-spec §2/§7).
537 E089,
538 /// An `IMPORT` names the importing file's own module — a module cannot
539 /// import itself; its own names are already bare (modules-spec §2/§7).
540 E090,
541 /// A qualified access `a.b` is ambiguous: `a` is both a module imported
542 /// in this file and a visible definition. Fix with an `AS` alias — no
543 /// silent precedence (modules-spec §2/§7).
544 E091,
545 /// A `#@public`/`#@private` override that restates the module's default
546 /// (e.g. `#@public` in an undeclared module, `#@private` in a declared
547 /// one) — redundant, no effect (warning, modules-spec §4/§7).
548 E092,
549 /// Conflicting or repeated visibility directives on one declaration
550 /// (both `#@private` and `#@public`, or the same one twice). A
551 /// declaration takes at most one visibility directive (modules-spec §4).
552 E093,
553
554 // ── M-3 renames (docs/modules-spec.md §5/§7) ────────────────────
555 /// A malformed `#@was(…)` directive: a missing or empty old-name
556 /// argument (`#@was`, `#@was()`). `#@was` takes exactly one non-empty
557 /// name (modules-spec §5).
558 E094,
559 /// `#@was(name)` names the thing's own *current* name — a self-alias
560 /// that would be a no-op entry in the compiled alias table. Nothing to
561 /// migrate; likely a stale directive left over from a previous rename
562 /// (warning, modules-spec §5/§7).
563 E095,
564
565 // ── M-2c cross-module collisions (issue #784, decision-log
566 // "Cross-module name collisions" 2026-07-14) ────────────────────────
567 /// Two *declared* modules (`#@module(name)`, different names) each
568 /// define a same-name, same-kind symbol. Escalated from the
569 /// `E022`/`E023`/`E026` inklecate-compat duplicate warning to a hard
570 /// error under `dialect = brink` only: flat resolution (unchanged by
571 /// this stopgap — true import-scoped resolution is #790's job) binds a
572 /// bare name to whichever declared-module definition merge happens to
573 /// see first, so two declared modules sharing a name make that binding
574 /// silently order-dependent for one of them. A duplicate *within* one
575 /// module (same declared module name across its files, or any
576 /// undeclared/legacy file) keeps the existing warning — this code
577 /// fires only when both colliding definitions' owning files declared
578 /// *different* modules. Reported once per colliding definition (both
579 /// spans), under `strict-ink` this code never fires (compat corpus
580 /// untouched).
581 E096,
582
583 // ── T1e-1 path projections (docs/t1e-spec.md §2/§6, issue #831,
584 // tracking #828) ──────────────────────────────────────────────────
585 /// A `ref lvalue-path` projection expression (`ref npc.hp`,
586 /// `ref inventory[idx]`) appears somewhere other than ref-argument
587 /// position (a direct argument of a call, `#fn(…)`, or `bind(…)`) — a
588 /// standalone projection value (`temp r = ref a[0]`), one nested inside
589 /// another expression, or any other position. Deliberate v1 posture
590 /// (t1e-spec §2: "projections exist only where `ref` already exists:
591 /// argument binding"); first-class standalone projection values are a
592 /// future round, tracked as icebox #825 — not a permanent rejection.
593 E097,
594 /// A `ref lvalue-path` projection's segment (a dotted field, or a
595 /// `[…]` index) disagrees with the root's statically-known shape, under
596 /// `types = strict` only — a dotted field the declared `STRUCT` shape
597 /// doesn't have, or a `[…]` index against a declared shape that isn't a
598 /// collection (mirrors `structs::check`'s missing/extra-field
599 /// machinery, `E069`–`E071`, applied to path segments instead of
600 /// construction-literal fields; "Unknown never disagrees" for any
601 /// segment whose base type isn't statically known this way — silently
602 /// unchecked, same spirit as `E071`).
603 E098,
604 /// A `ref lvalue-path` projection with at least one path segment
605 /// (dotted field or `[…]` index — a *real* projection, not a bare
606 /// single-name `ref`) reached LIR lowering. T1e-1 (docs/t1e-spec.md §8
607 /// sequencing item 1) ships grammar + HIR + analyzer only — the
608 /// `MakeProjection`/`ProjRead`/`ProjWrite` opcodes a projection needs to
609 /// actually run land in T1e-2 (tracking #828). The E052-fence pattern:
610 /// every other check (`E080` durable root, `E097` position, `E098`
611 /// strict segment shape) already ran and passed, so this is a clean,
612 /// deliberate "not yet lowerable" stop, not a silent drop or a
613 /// miscompile — see `brink-ir::lir::lower::mod`'s backstop doctrine. A
614 /// bare single-name `ref x` (zero segments) never hits this — it lowers
615 /// exactly like today's unmarked ref-argument binding.
616 E099,
617
618 // ── T2-2 `#@effects(…)` assertion surface (docs/effects-spec.md §10,
619 // issue #861) ──────────────────────────────────────────────────
620 /// `#@effects` with no argument at all (`#@effects`, `#@effects()`, or
621 /// an argument that parses to nothing) — the directive always requires
622 /// either `pure` or at least one `reads:`/`writes:`/`calls:` clause.
623 E100,
624 /// A malformed `#@effects(…)` argument: an unrecognized clause keyword
625 /// (only `reads`/`writes`/`calls` are valid), a value that isn't a bare
626 /// identifier, or a bare value with no preceding clause to attach to.
627 E101,
628 /// A `#@effects(…)` clause names an identifier that isn't a declared
629 /// global `VAR`/`CONST` (for `reads`/`writes`) or a declared `EXTERNAL`
630 /// (for `calls`) anywhere in the project.
631 E102,
632 /// **The exceedance error** (docs/effects-spec.md §10, sitting 2,
633 /// 2026-07-14 ruling): the definition's inferred effect row is not
634 /// covered by (`⊄`) its `#@effects(…)` assertion's declared upper
635 /// bound. Per the ruling, this is the *only* diagnostic the assertion
636 /// surface ever produces — an inferred row that is narrower than the
637 /// bound is silent; there is no drift policy.
638 E103,
639
640 // ── Computed-callee call attempt (docs/t1c-spec.md §3/§10, issue #869) ──
641 /// A call `expr(args…)` whose callee isn't a bare variable/temp/param
642 /// name (an `INDEX_EXPR`, `FIELD_ACCESS_EXPR`, chained call result,
643 /// parenthesized expr, …). Direct-call syntax is RULED (t1c-spec §3) to
644 /// a bare-name callee only; "method-call syntax" through a computed
645 /// callee is explicitly out of T1c (§10). Always rejected — every
646 /// dialect, every mode — pointing at the ratified `call(f, args…)`
647 /// form, which already dispatches through exactly this class of
648 /// expression correctly. Replaces the pre-existing silent drop (the
649 /// parser used to leave `(args…)` unconsumed, so it resurfaced as
650 /// trailing prose text on the content line and the call itself
651 /// vanished) with a loud, unconditional compile error.
652 E104,
653
654 // ── `await` condition purity gate (docs/flow-suspension-spec.md §3/§5, ──
655 // ── issue #928, FS-2) ─────────────────────────────────────────────────
656 /// An `await <cond>` / `while await <cond>` condition is not effect-free.
657 /// The condition is captured as a compiler-synthesized *pure* function
658 /// (docs/flow-suspension-spec.md §5): its effect row must be read-only —
659 /// reads are the wake map's dependency set, but a transitive **write** to a
660 /// global cell, or an effectful host **call**, makes the condition
661 /// re-evaluation itself observable, which the wake contract forbids. Built
662 /// on the effects machinery (#859): the condition's transitive effect row
663 /// (via the whole-project [`crate`]-level effect table) must have empty
664 /// `writes`/`calls` and not be opaque. Brink-only (under strict-ink the
665 /// whole `await` is already `E051`); a bare fn-value reference used as a
666 /// dynamic condition (`await some_fn_value`, no call syntax) is read-only
667 /// by construction and never flagged.
668 E105,
669
670 // ── T1b map-literal key-domain warning (docs/t1b-surface-spec.md §3,
671 // issue #598) ──────────────────────────────────────────────────────
672 /// A `#{key: expr, …}` map-literal key is a statically-classifiable
673 /// literal outside the ratified int/string/bool key domain — a float,
674 /// array (`#[...]`), nested map (`#{...}`), struct (`Name#{...}`),
675 /// function-value (`#fn(...)`), ink `LIST`, or divert-target literal
676 /// used directly as a key. §3 rules the key domain to
677 /// int/string/bool at runtime (`RuntimeError::InvalidMapKeyType`) and
678 /// says the analyzer warns on statically-visible non-key types; this was
679 /// the missing half (`MapLiteral` lowering did zero key-domain checking).
680 /// A dynamic key (a variable, call, index, or any other non-literal
681 /// expression) is not statically visible and is never flagged here —
682 /// the runtime fault remains the sole backstop for those.
683 E106,
684
685 // ── NS-A1 Option[T] (docs/stdlib-spec.md §1.4, issue #1107) ────────
686 /// A fresh, un-annotated declaration (`VAR x = none`, `CONST x = none`,
687 /// `~ temp x = none`) whose initializer is the bare `none` Option
688 /// literal. §1.4's ruled rule: "a bare `none` needs a type from
689 /// context (concrete sites fine; a fresh un-annotated `var x = none`
690 /// errors — the empty-collection posture)." A declaration site IS the
691 /// slot's type origin, so there is no context to take the element type
692 /// from — the fix is to initialize from a real Option-producing
693 /// expression (`some(x)`, or an Option-returning verb like
694 /// `find`/`get`/`pop`). Error in both dialects and both `types`
695 /// policies: the rule is part of the Option package itself, not a
696 /// strict-mode refinement.
697 E107,
698
699 // ── NS-A2 effect-row extension (issue #1108; docs/stdlib-spec.md
700 // §1.2/§9.2, issues #1087/#1097) ───────────────────────────────────
701 /// `@[effects(silent)]` exceedance: the definition's inferred row can
702 /// produce content (`emits`, incl. transitively through callees, or an
703 /// opaque/unbounded row). Exceedance-only, like `E103` — asserting less
704 /// than reality is legal, asserting more is not.
705 E108,
706 /// `@[effects(total)]` exceedance: the definition's inferred row can
707 /// raise a turn-terminating fault (`faults`, incl. transitively, or an
708 /// opaque/unbounded row). Exceedance-only, like `E103`.
709 E109,
710 /// The deprecated `#@effects(…)` tag-channel spelling — superseded by
711 /// the `@[effects(…)]` annotation final form (stdlib-spec §9.2, ruled
712 /// 2026-07-18). Warning: the alias keeps parsing (it shipped in
713 /// released surface, `@brink-lang/web@0.11.1`).
714 E110,
715 /// An `@[…]` annotation line naming something outside the channel's
716 /// closed name set: `effects` on the ink surface, `effects` or the
717 /// file-level `was` on the native `.brink` surface. Tag-channel
718 /// directive names do not alias into it.
719 E111,
720 /// An `@[…]` annotation line outside a recognized placement — ink's
721 /// leading run at the top of a knot/stitch body, or native's Rust-shaped
722 /// position directly above a `flow`/`fn` declaration (issue #1563; the
723 /// file-level `@[was]` record for native modules). Never a silent drop,
724 /// never content — the `E045` posture, on the annotation channel.
725 E112,
726
727 // ── NS-A3 protocol registry (issue #1109; docs/stdlib-spec.md §9.6)
728 /// A declaration named after a registry protocol method — `display`,
729 /// `compare`, or `next` (F6, ruled 2026-07-19): the names are RESERVED
730 /// under the brink dialect, and an author declaration of any callable
731 /// or value-bindable kind (knot/stitch/function, param, temp, VAR,
732 /// CONST, EXTERNAL, for-loop variable) is a **hard error**, not an
733 /// E035-lineage shadowing warning — a shadowed `display` would make
734 /// interpolation untrustworthy.
735 E113,
736 /// A registered protocol impl's inferred effect row exceeds its
737 /// protocol's effect contract (`display`/`compare`: pure·silent·total;
738 /// `iterate`'s `next`: writes-receiver·silent·total — the receiver is
739 /// a `ref` param, invisible to the global row, so every v1 contract
740 /// bounds the *global* row at empty). Exceedance-only, the
741 /// `E103`/`E108`/`E109` posture; an opaque row exceeds every contract.
742 E114,
743 /// An ill-formed protocol impl registration: the named type isn't a
744 /// declared `STRUCT`, the impl target isn't a declared function, the
745 /// signature shape is wrong (arity, `ref`-ness, or a contradicting
746 /// type annotation), or the (protocol, type) pair is already
747 /// registered.
748 E115,
749
750 // ── F27: Option has no truthiness (docs/stdlib-spec.md §1.6, ruled
751 // 2026-07-19, issue #1120) ─────────────────────────────────────────
752 /// A condition-position expression (an `if`/`while` condition, a
753 /// `{cond: …}` conditional branch, a choice guard, an `await`
754 /// condition) whose statically-known type is `Option[T]`. Option has
755 /// **no** truthiness — truthiness is a quiet coercion of exactly the
756 /// kind `Option[T] ≠ T` exists to ban — so a strict-mode author writes
757 /// `== none` / `== some(x)`, or the `as`-binding (B1b, issue #1475,
758 /// `brink-analyzer::option_conditions::check_binding_condition`); a
759 /// bound condition never fires this check. Strict-mode-only,
760 /// best-effort static (the "Unknown never disagrees"
761 /// posture: an unclassifiable condition stays silently unchecked);
762 /// under `types = gradual` the same condition is the
763 /// `RuntimeError::OptionTruthiness` turn-terminating fault — the
764 /// runtime backstop that catches every case either way. Supersedes
765 /// NS-A1's shipped falsy-none truthiness.
766 E116,
767 // ── NS-A5 the inhabited-range refinement (issue #1111;
768 // docs/stdlib-spec.md §7, F7/F8 ruled 2026-07-19) ──────────────────
769 /// A range-refinement violation under `types = strict` (the E078
770 /// precedent — strict-only; gradual mode is inert and leaves the
771 /// runtime fault residual, F8's general rule): `int(r)` demands
772 /// `NonEmptyRange` evidence, and either (a) the range literal in
773 /// argument position is **provably empty** (`0..0`, `5..=2` — bounds
774 /// fold statically, CONST refs included), or (b) the argument's type
775 /// carries no inhabitedness evidence (a possibly-empty range — route
776 /// computed bounds through `non_empty(r)`, parse-don't-validate).
777 E117,
778
779 // ── NS-A8: the numeric tower (docs/tower-mini-spec.md, issue #1114) ──
780 /// A protocol impl registration named a numeric-tower kind
781 /// (`vec2`/`vec3`/`vec4`/`quat`/`mat2`/`mat3`/`mat4`) as its type.
782 /// Tower kinds are compiler-known value kinds, not user structs: their
783 /// `display` is the fixed structural form, their equality is
784 /// componentwise IEEE (T4), and they are NOT orderable — a `compare`
785 /// impl for a tower kind would contradict the ruled §4b doctrine, and
786 /// `display`/`iterate` impls would shadow compiler-owned behavior. The
787 /// rejection is unconditional — it wins even over a user STRUCT
788 /// declared with the same name (tower type names are global like
789 /// `int`).
790 E118,
791
792 // ── NS-A4: the ordering doctrine (docs/stdlib-spec.md §4b, issue
793 // #1110) ─────────────────────────────────────────────────────────────
794 /// A `sort_by`/`sorted_by` comparator provably breaks the pure·silent
795 /// contract (§4b: "the comparator falls under the trio's pure·silent
796 /// rule plus the consistent-total-order LAW"). Exceedance-only, the
797 /// E114 posture: flagged when the comparator is a statically-named
798 /// `#fn(target)` whose inferred row shows a global read/write, an
799 /// external call, a content emission, or a tag touch — an opaque or
800 /// unresolvable comparator is not *proven* in violation and passes
801 /// (the gradual posture; the VM's isolation and
802 /// `ComparatorEscaped` fault are the runtime residual).
803 E119,
804 /// NS-A7 `Weighted[T]` construction refusal (`docs/stdlib-spec.md` §8,
805 /// issue #1113): the compile-classifiable half of the E078-style
806 /// evidence-by-construction split. Fired by the `weighted(…)` lowering
807 /// for a statically-malformed table — an empty pair row, an odd
808 /// (dangling-weight) argument count, or a **literal** weight that is
809 /// not a positive int (zero, negative, float/string/bool). Computed
810 /// weights are not classifiable here; they carry the construction
811 /// *fault* residual instead (`RuntimeError::WeightedBadWeight`), so a
812 /// table that exists is always rollable.
813 E120,
814
815 // ── B0.3 HIR admission validator (docs/hir-admission-contract.md §4.2) ──
816 //
817 // Reserved range for the loud, non-suppressible `validate_admission`
818 // pass wired at the AST→HIR seam (issue #1172, docs/b0-sequencing.md
819 // §B0.3). Each check is a hard error — a malformed `(HirFile,
820 // SymbolManifest)` triple is a frontend bug, not a story-author mistake,
821 // so these never carry the warning-severity carve-out other codes do.
822 /// Contract §4.2 check 1a (manifest ⇄ HIR agreement): an
823 /// `UnresolvedRef.range` in the manifest has no matching
824 /// referencing-expression range anywhere in the file's HIR body — the
825 /// range-equality resolution join (Q2(a)) would silently fail to find
826 /// this reference at all.
827 E121,
828 /// Contract §4.2 check 1b (manifest ⇄ HIR agreement): a manifest-declared
829 /// symbol has no corresponding HIR declaration node of the same name —
830 /// the manifest and the HIR body have drifted apart.
831 E122,
832 /// Contract §4.2 check 1c (manifest ⇄ HIR agreement, F-I#4): a `Knot`'s
833 /// `is_function` flag disagrees with whether its declared symbol carries
834 /// the `"function"` detail sentinel.
835 E123,
836 /// Contract §4.2 check 2a (range well-formedness): a HIR node's source
837 /// range is empty or extends past the end of the source file — ranges
838 /// are resolution join keys and IDE geometry, so a garbage range would
839 /// otherwise corrupt resolution silently instead of erroring loudly.
840 /// Exempts the `Option<Provenance>`-carrying synthesized nodes
841 /// (`Content.ptr`/`Divert.ptr`/`Return.ptr`) when `None` (B0.1 finding
842 /// F-B2) — this fires only on a range that is present but malformed.
843 E124,
844 /// Contract §4.2 check 2b (join-key uniqueness, Q2(a)): two distinct
845 /// `UnresolvedRef` entries in the manifest share an identical source
846 /// range — the range-equality join can no longer distinguish them.
847 E125,
848 /// Contract §4.2 check 3 (name-convention conformance, F-I#3): a
849 /// declared symbol's qualified name does not match the dot-qualification
850 /// shape its `SymbolKind` requires (bare for knots/globals, `knot.stitch`
851 /// for stitches, `List.item` for list items, `knot[.stitch].label` for
852 /// labels).
853 E126,
854 /// Contract §4.2 check 4 (control-flow classification, F-I#7): a
855 /// terminal statement (`Divert`/`Return`) is not the last statement in
856 /// an inline conditional or sequence branch.
857 E127,
858 /// Contract §4.2 check 5 (provenance-kind ⇄ `SymbolKind` consistency,
859 /// F-I#5, the #626 floating-stitch trap): a `Knot`/`Stitch` HIR node's
860 /// provenance class disagrees with the `SymbolKind` bucket its declared
861 /// symbol was indexed under in the manifest.
862 E128,
863
864 // ── B0.6 native frontend (docs/b0-sequencing.md §B0.6) ──
865 //
866 // The native `.brink` declaration lowering (`hir::lower_native`) is
867 // deliberately partial — bodies are B0.7/B0.8, and a handful of
868 // declaration-layer constructs (nested modules, `fn` nested below top
869 // level, the `@[…]` annotation channel, lambda expressions in value
870 // position) have no HIR representation yet. Per the contract's §4.4
871 // additive-open/closed-to-silent-extension posture, every such
872 // construct is a loud diagnostic, never a silent drop.
873 /// A native construct parses cleanly but has no HIR lowering yet in
874 /// this slice (a nested `module { … }` block, a `fn` declared below top
875 /// level, an `@[…]` annotation line, a lambda expression in value
876 /// position, or any other CST shape `hir::lower_native` does not yet
877 /// recognize). The construct is skipped — not silently: this diagnostic
878 /// names exactly what was skipped and why.
879 ///
880 /// Also raised by `brink_analyzer::modules::check` (issue #1592,
881 /// #1686 review) for the whole-project-only instance of the same gap:
882 /// a bare `use`/`IMPORT` item's trailing segment that is both aliased
883 /// and — only knowable once whole-project module data resolves the
884 /// dual-reading — a declared **submodule**. Aliasing an entire
885 /// imported module's export set has no `Import`/`ImportItem`
886 /// representation, same as the single-segment `use a as m;` form
887 /// `lower_native::import::lower_use_decl` already rejects with this
888 /// code; this later firing exists only because that verdict isn't
889 /// decidable until the analyzer's whole-project pass.
890 E129,
891 /// A native `flow` is declared more than two levels deep (a `flow`
892 /// nested inside another nested `flow`'s body) — the contract's Q4(b)
893 /// fence (`docs/hir-admission-contract.md` §5 Q4): exactly two
894 /// container levels for v1, addressing model written to generalize.
895 /// Depth-3+ nesting parses and is rejected here, never silently
896 /// flattened into a 2-level shape.
897 E130,
898 /// `<-` (splice) used outside a choice point (issue #1263, ruled
899 /// #1260 on #1256): charter §11 narrows threads to scoped splices
900 /// inside `{? … }` choice points, so this has no structural meaning —
901 /// but `<-` can also be literal dialogue punctuation, so this is
902 /// **warning severity, never blocking** (see `DiagnosticCode::severity`
903 /// below). The construct still parses as ordinary text; nothing is
904 /// dropped or rejected. `brink-syntax-native`'s
905 /// `parser::choice::splice_outside_choice_point` raises the
906 /// `ParseSeverity::Warning` diagnostic this code carries once it
907 /// reaches `brink-db`'s `lower_native_file`.
908 E131,
909 /// A native file-level `@[was(…)]` rename record (issue #1286) carries no
910 /// quoted old module path — a missing argument, or one that is not a
911 /// string literal. Native module paths are `::`-separated and travel as a
912 /// string (`::` is not annotation-argument grammar), so the migration
913 /// target must be spelled `@[was("story::old::path")]`. **Warning
914 /// severity, never blocking** (see `DiagnosticCode::severity`): the
915 /// malformed directive is skipped — no alias is produced — but the file
916 /// still compiles. `brink-ir::hir::lower_native::module::lower_file_module`
917 /// raises it rather than silently dropping the authored record.
918 E132,
919
920 // ── B0.9 native accept-list admission gate (docs/hir-admission-contract.md
921 // §4.4/§5 Q6, docs/b0-sequencing.md §B0.9, issue #1179) ──
922 //
923 // The inverse of the ink `dialect_gate` reject-list: `brink_analyzer::
924 // validate_native_accept_list` enumerates the HIR shapes a well-formed
925 // native lowering is allowed to produce and refuses everything else,
926 // loudly, at the same non-suppressible seam B0.3's `validate_admission`
927 // runs at. Native-only — never raised against ink-produced HIR.
928 /// A native file's `root_content` carries something other than the one
929 /// documented shape a native lowering may leave there: empty, or the
930 /// single synthesized `flow main()` entry divert (maintainer-ruled
931 /// 2026-07-21, `docs/decision-log.md` "Native story-entry convention").
932 /// Anything else — real weave content, more than one statement, a
933 /// source-backed divert — is ink-only baggage: ink's pre-first-knot root
934 /// weave has no native equivalent.
935 E133,
936 /// A native file's HIR carries an `IncludeSite` — native has no textual
937 /// `INCLUDE` graph (charter §13.2, "the tree is the compilation
938 /// universe"); `hir::lower_native::lower` always leaves `includes`
939 /// empty, so any entry here is ink-only baggage that reached native HIR
940 /// some other way.
941 E134,
942 /// A `ThreadStart` (`<- target`) appears somewhere other than the two
943 /// legal native splice positions B0.7's choice-point lowering produces:
944 /// immediately preceding the `ChoiceSet` it preambles, or as the
945 /// trailing statement(s) of a `Choice`'s own body
946 /// (`hir::lower_native::choice::lower_choice_point`). An "ambient"
947 /// thread start anywhere else has no structural meaning on the native
948 /// surface (charter §11 narrows threads to scoped splices inside `{?
949 /// … }` choice points).
950 E135,
951 /// A native `ChoiceSet` carries a `depth`/`context` other than the
952 /// B0.7-documented neutral values (`depth = 0`, `context = Inline`,
953 /// `docs/hir-admission-contract.md` §3 D4) every native choice set
954 /// stamps uniformly — native has no weave fold to report a real value
955 /// from, so any other value means a weave-fold concept leaked in from
956 /// somewhere it shouldn't have.
957 E136,
958 /// The B0.9 native strict-only enforcement point (docs/b0-sequencing.md
959 /// §B0.9, decision-log 2026-07-19 "Typing posture ruled"): a native
960 /// `.brink` file was compiled with an explicit `types = gradual` knob.
961 /// Gradual typing does not exist on the native surface — `types` is not
962 /// a project knob there the way it is for the transitional brink
963 /// dialect, so an explicit `gradual` setting reaching a `.brink` compile
964 /// is refused, loudly, rather than silently accepted.
965 E137,
966
967 // ── B5: the construction initializer (issue #1464, #1103 RULED
968 // 2026-07-23, `docs/stdlib-spec.md` §9.6) ────────────────────────
969 /// A map literal supplies the same key twice (`Map { k: 1, k: 2 }`).
970 /// The E076-lineage cascade ruling (A) of #1103: a duplicate key is a
971 /// **compile error**, consistent with a struct literal's duplicate
972 /// field ([`Self::E084`]) — last-wins would silently swallow the typo.
973 /// Only *statically comparable* literal keys can collide here
974 /// (int/string/bool, the `E106` key domain); a dynamic key is left to
975 /// the runtime, exactly as the key-domain check leaves it.
976 E138,
977 /// A construction literal's entries are not in the form its target type
978 /// constructs from — `Map { a }` (element form for a key/value target)
979 /// or `Flags { A: 1 }` (key/value form for an element target). The
980 /// brace *tokens* are one fixed grammar; the entry form each type
981 /// consumes is the `construct` protocol's business
982 /// ([`crate::hir::construct::ConstructTarget::form`]), so a mismatch is
983 /// caught at dispatch rather than by the parser.
984 E139,
985
986 // ── B3a: UFCS resolution (issue #1482, D1–D5 RULED 2026-07-26,
987 // `docs/decision-log.md` "UFCS resolution pass designed") ────────
988 /// **D1**: `recv.name(args)`'s receiver type declares a field `name`,
989 /// but that field is not function-typed. Field access *wins outright* —
990 /// a matching-but-non-callable field is a hard error, never a silent
991 /// fall-through to a free function of the same name, so that a call's
992 /// meaning can never hinge on a field's type.
993 E140,
994 /// `recv.name(args)` resolved as neither: the receiver's type declares
995 /// no field `name`, **and** no free function `name` is visible in
996 /// ordinary lexical scope (D4 — the candidate set is lexical scope only;
997 /// there are no method sets or inherent impls). One diagnostic naming
998 /// both attempts, so the author sees the whole search that failed.
999 E141,
1000 /// **D3**: `recv.name(args)`'s receiver type is not known at the
1001 /// resolution point, so field-access-wins is unanswerable. An annotation
1002 /// is demanded rather than the resolution being deferred (E107-family
1003 /// posture). Explicitly a *for now* trade — smarter inference ordering
1004 /// is planned and additive when it lands.
1005 E142,
1006 /// **D5**: `recv.name(args)` resolved to a free function whose first
1007 /// parameter is declared `ref`, so the receiver is auto-ref'd
1008 /// (`party.leader.heal(5)` → `heal(ref party.leader, 5)`, issue
1009 /// #1462) — but *this* receiver cannot be written through: a `CONST`, or
1010 /// a projection whose root is a frame-local (T1e's durable-root rule,
1011 /// `docs/t1e-spec.md` §2), or — once the grammar can spell them — an
1012 /// rvalue such as `[1,2].push(3)`. Refused rather than silently
1013 /// desugared by value, which would drop the mutation. A non-`ref` first
1014 /// parameter never reaches this code: the by-value desugar puts no
1015 /// lvalue requirement on its receiver.
1016 E143,
1017 /// A UFCS call site that `brink-analyzer::ufcs` **resolved** cleanly has
1018 /// reached LIR lowering, which does not consume the verdict side table
1019 /// yet. Refused loudly rather than lowered: the callee path's resolution
1020 /// record names the *receiver* (the D2 side table is what names the real
1021 /// target), so lowering it as an ordinary call would emit a call against
1022 /// a local's id and silently produce a wrong program. Same "parses/
1023 /// resolves but has no lowering yet" posture as [`Self::E129`], one
1024 /// layer further down.
1025 E144,
1026
1027 // ── B1b: the `as` binding (issue #1475, RULED `docs/decision-log.md`
1028 // 2026-07-26 "The `as` binding") ─────────────────────────────────
1029 /// The v1 whole-condition restriction: an `as` binding was written over
1030 /// a `&&`/`||` composition (`if a && find(x) as s { … }`). The ruling
1031 /// fixes the binding as the **entire** condition for v1 — let-chains
1032 /// can land later, additively — so a boolean composition under the
1033 /// binding is refused rather than silently binding the composite (which
1034 /// is never an `Option[T]` anyway). The mirror spelling, an operator
1035 /// *after* the binding (`if find(x) as s && …`), is caught one layer
1036 /// earlier as a parse error (`brink-syntax-native::parser::binding`).
1037 E145,
1038 /// RETIRED (issue #1508) — previously "an `as` binding in a choice
1039 /// guard (`* {if EXPR as name} [text]`) is ruled but not yet
1040 /// implemented". `hir::lower_native::choice::lower_choice` now lowers
1041 /// it for real: capture-at-presentation, by-value COW
1042 /// (`docs/decision-log.md` 2026-07-26, "Choice-guard `as`
1043 /// un-deferred"), reusing the same `OptionBind`/frame-slot machinery
1044 /// `IfStmt::binding` already used — the guard's `OptionBind` writes
1045 /// into the same frame `BeginChoice`'s `fork_thread` snapshots into
1046 /// the pending choice, so the captured value rides along with no
1047 /// separate wire-level capture needed. Code kept reserved, not reused,
1048 /// for diagnostic-code stability — no longer emitted by any pass.
1049 E146,
1050 /// An `as` binding whose condition is a statically-known **non-Option**
1051 /// type (`if 5 as n { … }`). The binding unwraps `Option[T]` to `T`;
1052 /// there is nothing to unwrap here. Strict-mode-only and
1053 /// classification-gated, exactly like its F27 twin [`Self::E116`]: an
1054 /// `Unknown`/`Conflicted` condition stays unjudged rather than
1055 /// guessing.
1056 E147,
1057 /// A write to an `as` binding — `if find(s) as i { i = 0; }`, `pop(i)`,
1058 /// `i[0] = x`, `bump(ref i)`, `b.field = v`, `push(b.field, v)`, … The
1059 /// binding is **immutable** by ruling (`docs/decision-log.md`
1060 /// 2026-07-26): it names the unwrapped payload the condition proved
1061 /// present, and rebinding it would make the narrowing guarantee a lie.
1062 /// Raised via the shared `lir::lower::stmts::reject_as_binding_write`
1063 /// check (issue #2122) for: plain/compound assignment, an
1064 /// indexed-assignment root, and a bare in-place mutator, all via
1065 /// `lir::lower::stmts::lower_assign_target` itself; a single-level
1066 /// struct-field write (`lir::lower::blocks::lower_single_level_field_write`)
1067 /// and a struct-field mutator (`lir::lower::blocks::lower_field_mutator`),
1068 /// which resolve a `Param`/`Temp` root's slot independently of
1069 /// `lower_assign_target` (their root is the *head* of a two-segment
1070 /// path, not the whole target) and so call the shared check directly
1071 /// instead; and separately at the `ref`-argument choke points
1072 /// (`lir::lower::expr::lower_ref_path_call_arg`,
1073 /// `lower_ref_projection_arg`), since passing the binding by `ref`
1074 /// hands the callee a raw pointer to the slot without ever routing
1075 /// through ordinary assignment lowering.
1076 E148,
1077 /// A `remove(a, i)` call whose first argument is statically known to be
1078 /// an array (issue #1532, the #1501 review's migration-tail finding):
1079 /// `remove` went map-only in #1484 (identity-based, idempotent-total
1080 /// key removal; `docs/t1b-surface-spec.md` §5), and the array-index leg
1081 /// it used to also serve moved to its own verb, `remove_at(a, i)`. With
1082 /// no compatibility shim, an un-migrated `remove(array, i)` call site
1083 /// still parses and type-checks as a call to the (now map-only)
1084 /// builtin — `infer::body`'s `remove` arm already has `Ty::Array` in
1085 /// hand at the call site — and previously reached codegen clean, only
1086 /// faulting at runtime against `MapRemove`'s domain check. Strict-mode-
1087 /// only (`infer::body::InferPass::array_remove_calls`,
1088 /// `strict::check_array_remove_calls`), matching every other TM-3
1089 /// typed-mismatch check in this range — the brink dialect's own
1090 /// implicit default is `types = strict` (issue #1127), so this fires
1091 /// for the common case; under `types = gradual` the `MapRemove`
1092 /// runtime fault stays the backstop, same posture as the rest of TM-3.
1093 E149,
1094 /// A def (function or value-returning flow/stitch) declares a non-`void`
1095 /// return type but its body may fall through without ever executing a
1096 /// value-carrying `return <expr>` (issue #1551, `docs/decision-log.md`
1097 /// 2026-07-22 implicit-end ruling item 3: "a flow that declares a
1098 /// return type must produce a value... falling through without a value
1099 /// is a checker error", ratified for a return-typed flow/stitch and now
1100 /// extended to the identical `fn` shape). Strict-mode-only
1101 /// (`strict::check_def`'s escape check, extended by #1551 to run for
1102 /// any def carrying a declared return type, not just `is_function`);
1103 /// deliberately distinct from [`Self::E065`] Unknown-escape — the
1104 /// annotation-fallback in `infer::body::infer_def_body` backfills a
1105 /// no-return body's inferred return type from the annotation itself,
1106 /// so the type comes out concrete (`Clean`, not `Unknown`) and E065's
1107 /// classification can never see this mistake; only a direct
1108 /// `has_value_return` check catches it. An implicit `-> DONE` is never
1109 /// treated as satisfying this — DONE ends the *turn*, not the value
1110 /// contract.
1111 E150,
1112
1113 // ── Native lint: asymmetric choice-branch dead-end (issue #1219,
1114 // decision-log 2026-07-22 "Flows end implicitly (native)" item 4) ──
1115 /// A native `{? … }` choice's own body falls through (no divert/return)
1116 /// while a sibling choice in the same set diverts onward, at a genuine
1117 /// dead end (nothing follows the choice point to reconverge into) — the
1118 /// residual value of ink's retired "ran out of content" error,
1119 /// relocated to a narrow, **opt-in, warning-severity** lint
1120 /// (`brink_analyzer::native_choice_dead_end`) rather than a blocking
1121 /// runtime fault. Fires only for the *mixed* case — some siblings
1122 /// divert, at least one doesn't — never for a choice set where every
1123 /// branch falls through (an ordinary menu that ends) or where the
1124 /// choice set's `continuation` is non-empty (native has no gather,
1125 /// `docs/native-surface-charter.md` §5 — a non-empty continuation is
1126 /// the dissolved gather, and every falling-through branch reconverging
1127 /// there is ordinary weave structure, not a mistake).
1128 E151,
1129
1130 /// A `contains(m, needle)` call whose `needle` argument is statically
1131 /// visible as outside the map key domain (int/string/bool) while `m`
1132 /// is statically visible as a map — companion to the #580 ruling
1133 /// (`docs/decision-log.md` 2026-07-12 "contains(map, non-key-domain
1134 /// needle) returns false"): the call can never do anything but return
1135 /// `false` at runtime, so the always-false result is a compile-time
1136 /// warning rather than a silent footgun. Strict-mode-only
1137 /// (`brink_analyzer::contains_domain`, wired into `strict::check`
1138 /// alongside `conversions`/`range_refinement` — the same
1139 /// inference-substrate-backed domain-check family): needs the
1140 /// project's whole-program `InferenceResult`
1141 /// (`structs::classify_expr_ty`) to classify a variable/call/
1142 /// index-valued needle, which is only ever computed under `types =
1143 /// strict`. Under `types = gradual` this stays silent and the
1144 /// runtime's total `false` return is the sole (correct, non-faulting)
1145 /// backstop. `Warning`-severity like `E106`'s map-literal-key sibling
1146 /// check, so it flows through the ordinary suppressible `diagnostics`
1147 /// channel and is re-levelable via the project's `[lints]` table.
1148 E152,
1149
1150 // ── `@[allow(…)]` source-level suppression (issue #1161) ───────
1151 /// An `@[allow(…)]` argument is not a diagnostic code this compiler
1152 /// knows (`DiagnosticCode::from_str_code` says no) — a typo like
1153 /// `@[allow(E1511)]` or a name like `@[allow(dead_code)]`.
1154 ///
1155 /// A hard error by construction, and deliberately so: the whole point
1156 /// of a suppression directive is that the author believes a diagnostic
1157 /// is being silenced, so a misspelled code that silently does nothing
1158 /// is the worst possible outcome (the #1374 reserved-keys lesson, and
1159 /// the `@`-namespace rule in `docs/directive-annotations-spec.md` §1.1
1160 /// — every `@`-mark is a valid directive in a valid placement or a hard
1161 /// error).
1162 E153,
1163
1164 /// An `@[allow(…)]` names a real diagnostic code that is **not
1165 /// suppressible**: one whose default severity
1166 /// ([`DiagnosticCode::severity`]) is `Error`.
1167 ///
1168 /// Source-level suppression only ever reaches the warning/lint tier. An
1169 /// error means the compiler cannot produce a correct artifact, so
1170 /// letting an annotation silence one would be a way to ship broken
1171 /// code; the B0.3 admission-validator family (`E121`–`E128`) is covered
1172 /// by the same rule (all `Error`-severity) *and* structurally, since
1173 /// admission diagnostics never route through
1174 /// [`crate::suppressions::apply_suppressions`] at all. This mirrors the
1175 /// `[lints]` table's own hard-error exemption (issue #1160, step 2 of
1176 /// `brink_analyzer::effective_severity`): rather than curating which
1177 /// `Error` codes are "safe" to relax, none of them are reachable.
1178 E154,
1179
1180 /// An `@[allow(…)]` whose argument list is missing, empty, or not a
1181 /// flat list of bare code identifiers (`@[allow]`, `@[allow()]`,
1182 /// `@[allow("E151")]`, `@[allow(reads(x))]`).
1183 ///
1184 /// The grammar counterpart of `E100` on the `@[effects(…)]` channel:
1185 /// the annotation parses as an annotation but declares nothing this
1186 /// channel can act on.
1187 E155,
1188
1189 // ── Lambdas (native surface, issue #1685) ──────────────────────
1190 /// A lambda body assigns to a **captured binding** — a `let`/param
1191 /// binding declared outside the lambda and read inside it.
1192 ///
1193 /// A hard error by the 2026-07-19 ruling ("assignment to a captured
1194 /// binding is a compile error"): brink lambdas capture BY VALUE always
1195 /// (Rust's `move` as the only mode, no keyword, no ref captures in v1),
1196 /// so the binding a lambda body writes to is its own *snapshot* — the
1197 /// write can never be observed by the enclosing scope. A snapshot write
1198 /// is always a lost write, and this kills the closure-mutation
1199 /// confusion structurally rather than letting authors discover it as a
1200 /// silent no-op at runtime.
1201 ///
1202 /// Writes to a *global* (a module-level `var` cell) are not captures
1203 /// and are not flagged: a global is a durable cell reached by name, not
1204 /// a snapshotted binding.
1205 E156,
1206
1207 // ── Anonymous-container state lint (issue #1674, gap 4 of the identity
1208 // cluster; ruled 2026-07-27 in PR #1670) ─────────────────────────
1209 /// An unnamed once-only choice, or an unnamed sequence (`{cycle: …}` /
1210 /// `{stopping: …}` / `{once: …}` / `{shuffle: …}` and combinations), that
1211 /// genuinely carries durable visit/turn-count state with no author name
1212 /// to anchor it — the choice/sequence's compiled scope id is purely
1213 /// structural (a positional hash, `brink_ir::hir::stamp`), so a content
1214 /// edit anywhere earlier in the same scope can shift it, orphaning the
1215 /// saved count under the old id. The observable fallout is bounded (only
1216 /// visit/turn counts key on a scope id — see
1217 /// `brink_format::LoadReport::anonymous_states_dropped`): a once-only
1218 /// choice may reappear, or a sequence may restart from its first branch.
1219 ///
1220 /// Naming is the opt-in fix — a labeled choice (`* (label) …`) resolves
1221 /// its identity by name instead of position (`stamp::stamp_stmt`'s
1222 /// `lookup_label_id` branch), immune to this drift. Sequences have no
1223 /// label syntax of their own; the mitigation is structural (isolate the
1224 /// sequence in its own small, stably-named stitch so nothing can be
1225 /// inserted ahead of it).
1226 ///
1227 /// **Off/info by default, tier-able through `[lints]`** like any other
1228 /// diagnostic (`brink_analyzer::strict::effective_severity` — this is
1229 /// the one code whose *default* severity is `Info`, not `Warning`; see
1230 /// that function's doc for how `[lints]` still reaches it). A
1231 /// single-shot project that never patches its content is never nagged;
1232 /// a live-ops/UGC project can raise it to `warn`/`deny`.
1233 ///
1234 /// Precision over recall (`brink_analyzer::anonymous_stateful`): a `+`
1235 /// sticky/repeatable choice never triggers this (no once-only gating,
1236 /// no state) and a single-branch, non-`once` sequence never triggers
1237 /// this either (its computed index is `0` regardless of visit count —
1238 /// genuinely stateless despite the syntax).
1239 E157,
1240
1241 // ── Lambda lifting, LIR (issue #1709 review) ───────────────────
1242 /// A lambda body reads a name that the analyzer resolved as a
1243 /// `Temp`/`Param` of the enclosing frame, but that lifting's free-name
1244 /// scan cannot see as a capturable local at the point it runs — in
1245 /// practice, the lambda's own not-yet-bound `let` name, read
1246 /// recursively (`let f = |x| f(x - 1);`): the initializer is scanned
1247 /// for captures *before* the `let` finishes binding `f`, so `f` has no
1248 /// temp slot yet in the enclosing frame.
1249 ///
1250 /// A hard error rather than a silent fall-through: an unresolved free
1251 /// name that is not a real local (a global `var`, a knot/function
1252 /// name) is left alone and resolved by name from inside the lifted
1253 /// function, which is correct. But a name the analyzer says *is* a
1254 /// local must not take that same silent path — falling through would
1255 /// let call lowering target the `let`'s own `DefinitionId` as though
1256 /// it were a callable container, a miscompile that only surfaces as a
1257 /// runtime fault. Recursive lambdas are not supported in this slice;
1258 /// this refuses them at compile time instead of shipping a broken
1259 /// call.
1260 E158,
1261
1262 // ── `@[element]` / `@[style]` declaration surface (issue #1719,
1263 // `docs/prose-dialect-spec.md` §3.5b sitting 4 addenda 2–4) ───────
1264 /// An `@[element(…)]` annotation whose `args` clause is missing, or
1265 /// whose value is not a quoted string, or whose value does not compile
1266 /// as a portable-regex pattern (`regex::Regex::new`).
1267 ///
1268 /// The grammar counterpart of `E100`/`E155` on the `@[effects]`/
1269 /// `@[allow]` channels: the annotation parses as an annotation but
1270 /// declares a pattern this channel can't act on.
1271 E159,
1272 /// An `@[element(args = "…")]` pattern's named capture group does not
1273 /// match the name of any parameter on the annotated declaration.
1274 ///
1275 /// The capture contract (§3.5b: "named captures bind params by name
1276 /// (compile-checked)") is enforced here, at the declaration, rather
1277 /// than deferred to the `!name` dispatch site — a capture that can
1278 /// never bind anything is a static defect in the pattern itself, not
1279 /// a per-call-site concern.
1280 E160,
1281 /// An `@[style(…)]` clause is not the `key = "value"` shape (a bare
1282 /// identifier, a nested paren-clause, or a non-string value), or the
1283 /// argument list is missing or empty.
1284 E161,
1285 /// An `@[style(…)]` clause's key is neither `line`, `dispatch`, nor
1286 /// the name of a named capture group in the paired `@[element(…)]`
1287 /// pattern on the same declaration.
1288 ///
1289 /// Validated against the real capture set rather than accepted
1290 /// blind — a typo'd key would otherwise silently style nothing
1291 /// (`CLAUDE.md` "flag silent data drops").
1292 E162,
1293 /// An `@[style(…)]` annotation with no paired `@[element(…)]` on the
1294 /// same declaration.
1295 ///
1296 /// `@[style]` is a *companion* annotation (§3.5b addendum 4): its keys
1297 /// name `@[element]`'s captures (plus the two special keys, `line` and
1298 /// `dispatch`), so a style declaration with nothing to style against
1299 /// is malformed rather than silently inert.
1300 E163,
1301
1302 // ── Host manifest (inline-markup vocabulary, issue #1733,
1303 // `docs/prose-dialect-spec.md` §4.2) ──────────────────────────────
1304 /// An inline markup span (`<name>…</name>`) whose tag name is not
1305 /// declared in the host manifest's markup vocabulary.
1306 ///
1307 /// Only ever reachable once a host *declares* a vocabulary: markup is
1308 /// freeform by default (§4.2), so with no declared span kinds this code
1309 /// cannot fire at all. `Warning` by default and therefore
1310 /// `[lints]`-configurable and `@[allow(E164)]`-suppressible — the
1311 /// "configurable severity" half of §4.2's ruling.
1312 E164,
1313 /// An inline markup span carries an attribute the host manifest does not
1314 /// declare for that span kind.
1315 ///
1316 /// The per-kind counterpart of `E164`, and gated the same way: it fires
1317 /// only for a span whose *name* the manifest does declare (an undeclared
1318 /// name reports `E164` alone rather than cascading one report per
1319 /// attribute).
1320 E165,
1321
1322 // ── `@[element(…, block)]` declaration surface (issue #1839,
1323 // `docs/decision-log.md` 2026-07-31 "Conventions are annotated
1324 // handlers") ──────────────────────────────────────────────────────
1325 /// A `block`-flagged `@[element(…)]` annotation whose declaration has
1326 /// no trailing `content`-typed parameter to receive the captured run,
1327 /// or whose would-be receiver is also one of the pattern's own named
1328 /// captures.
1329 ///
1330 /// `block` widens the same capture contract [`Self::E160`] enforces
1331 /// for `args`' named captures — the ruling's `content` param ("the
1332 /// following run … the same first-class fragment-capture path `!radio`
1333 /// uses for the rest of its line") is a *structural* requirement on
1334 /// the declaration, checked here rather than deferred to dispatch: a
1335 /// `block` annotation with nothing to bind the captured run to is a
1336 /// static defect in the declaration, not a per-call-site concern. The
1337 /// dispatch and capture rewrite itself — matching the terminator,
1338 /// building the `FragmentRef`, calling the handler — is issue #1838's
1339 /// natural-notation dispatch, not yet implemented; see
1340 /// [`crate::ElementAnnotation::block`]'s own doc.
1341 E166,
1342
1343 // ── Natural-notation element dispatch (issue #1838,
1344 // `docs/decision-log.md` 2026-07-31 "Conventions are annotated
1345 // handlers") ───────────────────────────────────────────────────────
1346 /// A natural-notation `@[convention(claims = "…", order = N)]` handler declares a
1347 /// parameter that its pattern never captures, so a claimed line has
1348 /// nothing to bind it to.
1349 ///
1350 /// The other half of `E160`'s contract, and the half only *claiming*
1351 /// handlers need: a `!name`-dispatched handler can be called by hand
1352 /// with ordinary arguments, but a claimed line is rewritten to exactly
1353 /// one call whose every argument comes from a named capture — so the
1354 /// pattern's capture set and the handler's parameter list must match
1355 /// exactly, not merely one-way.
1356 ///
1357 /// Renumbered to `E167` (from a since-vacated `E166`) when this landed
1358 /// alongside issue #1839's `block` declaration surface, which claimed
1359 /// `E166` first (merged into `main` first) — see that code's own doc.
1360 E167,
1361 /// Two `@[convention(claims = "…", order = N)]` handlers declare byte-identical
1362 /// patterns, and the later-declared one never actually won a claim in
1363 /// this file — so it is dead code.
1364 ///
1365 /// Issue #1848: dispatch order is first-match-wins over the module's
1366 /// claiming handlers, ordered by `@[convention]`'s required `order`
1367 /// property (issue #2164 — declaration order, the interim pre-#2164
1368 /// rule, no longer applies; `hir::lower_native::element::try_claim`'s
1369 /// own doc) — an undocumented rule until this issue, and one with no
1370 /// diagnostic when two patterns can both claim the same line. This is
1371 /// the *sound*, narrow slice of that check: identical patterns provably match
1372 /// identical inputs, so the overlap is certain, not merely possible.
1373 ///
1374 /// A byte-identical twin is not *unconditionally* dead, though:
1375 /// `try_claim` excludes a handler from claiming lines inside its own
1376 /// declaration (the staging rule), and that exclusion does not extend
1377 /// to a later twin — the later twin is exactly the handler that *can*
1378 /// claim a line living inside the earlier one's own body. So this
1379 /// diagnosis runs after the whole file is lowered and only fires when
1380 /// the later twin produced zero actual claims
1381 /// (`hir::lower_native::element::diagnose_duplicate_patterns`'s own
1382 /// doc) — a later twin that is live for even one line is not flagged.
1383 ///
1384 /// General overlap between two *different* patterns (e.g. one whose
1385 /// matches are a strict subset of the other's) is real and valuable —
1386 /// the issue's own framing calls it "the genuinely valuable half" —
1387 /// but is **not** detected here: proving it soundly needs either a
1388 /// witness string both patterns can be shown to accept or a full
1389 /// regex-intersection analysis, neither of which this slice builds.
1390 /// Tracked as a follow-up, not silently out of scope — see the
1391 /// issue thread. `Warning` by default (`@[allow(E168)]`-suppressible,
1392 /// like every other `Warning`-tier code) since a duplicate claim is
1393 /// dead code, not a hard error the way an unregistered claim (`E112`)
1394 /// is.
1395 E168,
1396 /// A top-level `fn` carries an `@[convention(claims = "…", order = N)]` pattern-
1397 /// claiming annotation, but this file is not the project's configured
1398 /// conventions module — the module half of the 2026-07-31 §9.1 ruling's
1399 /// item (4) asymmetry (issue #1844; #1838 landed the *placement* half,
1400 /// `E112`, and #1847 the module-nesting corner of it): "pattern-
1401 /// claiming is confined to ONE module — the conventions module named in
1402 /// `brink.toml`. `!name`-dispatched handlers stay legal anywhere
1403 /// precisely because they self-announce." A sigil-dispatched line
1404 /// announces itself at the call site; a claiming pattern can silently
1405 /// reinterpret ordinary prose, so the auditability the ruling protects
1406 /// depends on every claim living in the one file an author (or
1407 /// reviewer) knows to open.
1408 ///
1409 /// Only fires when `brink.toml`'s `[project] conventions` key (renamed
1410 /// from `elements` by issue #2180) names a project-relative `.brink`
1411 /// path (`conventions = "conventions.brink"`) — a bare built-in preset
1412 /// name (`conventions = "screenplay"`) points at a
1413 /// `std::conventions::*` module with no project file to compare
1414 /// against, and an unset `conventions` key means no conventions module
1415 /// is configured at all, so there is nothing to confine against yet
1416 /// (`brink_db::queries::analysis::conventions_confinement_diagnostics_query`'s
1417 /// own doc). `Error` by default, the same posture as `E112`: a
1418 /// misplaced claim is not a style nit, it is a claim that violates the
1419 /// one property the whole mechanism depends on.
1420 E169,
1421 /// Two claiming handlers' patterns are textually different but can both
1422 /// match the same line of prose — they silently race, with the earlier
1423 /// one winning (issue #1859, follow-up to #1848).
1424 ///
1425 /// `E168` catches the narrow case: byte-identical patterns provably
1426 /// match identical inputs. This code catches the more common and more
1427 /// valuable instance: two *different* patterns whose matched-line sets
1428 /// overlap (one a strict subset of the other, an alternation branch
1429 /// shared between them, two competing prefixes, etc.).
1430 ///
1431 /// Detection uses a sound-but-incomplete heuristic: finding a concrete
1432 /// witness string demonstrably accepted by both compiled patterns. The
1433 /// heuristic checks:
1434 /// - Whether the patterns share a literal prefix (both start with the
1435 /// same fixed text, before any regex metacharacters)
1436 /// - Whether one pattern's literal parts are a subset of the other's
1437 /// (e.g. `^A$` is subsumed by `^AB?$` if the second can match `A`)
1438 /// - Whether generated test strings match both patterns
1439 ///
1440 /// A witness found proves overlap; none found does not prove they
1441 /// never overlap. This is the safer alternative to a textual heuristic
1442 /// that could produce false positives (e.g. "shares a literal prefix"
1443 /// without confirming both patterns actually match anything starting
1444 /// with that prefix — `^A$` and `^AB$` share the prefix `A` but never
1445 /// both match any input). Only reports what it can prove.
1446 ///
1447 /// Reported **at most once** per later-handler, against the first
1448 /// earlier handler it provably overlaps with. A handler that overlaps
1449 /// multiple earlier ones is not re-reported. `Warning` severity, like
1450 /// `E168`, since a silent race is a real problem but not a hard error.
1451 ///
1452 /// See also: `E168` (byte-identical patterns), `docs/prose-dialect-
1453 /// spec.md` §3.5b ("pattern power proportional to auditability").
1454 E170,
1455 /// A natural-notation `@[convention(claims = "…", order = N)]` handler declares a
1456 /// parameter, bound by a named capture, whose declared type is neither
1457 /// `string` nor absent nor `content` — `int`, `float`, `bool`, a
1458 /// struct name, a generic, or a `fn` type.
1459 ///
1460 /// Filed from adversarial review of PR #1845 (issue #1849, itself
1461 /// closing part of #1838): `hir::lower_native::element::try_claim`
1462 /// binds **every** matched capture as a plain `Expr::String` literal,
1463 /// unconditionally, regardless of the receiving parameter's declared
1464 /// type — so `@[convention(claims = "^Take (?<n>\\d+)$", order = N)] fn
1465 /// take(n: int)` could never actually receive an `int`. Numeric capture
1466 /// coercion is `docs/prose-dialect-spec.md` §3.5b's own Deferred list
1467 /// — the underlying gap is ruled-deferred, not itself a bug — but
1468 /// leaving the mismatch silent is: without this check it was, and
1469 /// remains, silent — nothing checks a direct call's arguments against
1470 /// the callee's declared parameter types yet. That generic check
1471 /// (`E063` for this shape) is exactly what open issue #1864 asks to
1472 /// build.
1473 ///
1474 /// `content` is deliberately **not** in this code's target set even
1475 /// though a capture can no more produce a `FragmentRef` than an `int`
1476 /// — it already has an established, ruled, and tested story of its
1477 /// own (the spec's own `fn radio(chan: string, text: content)`
1478 /// example, and the `tier1-native/annotations-element` golden
1479 /// fixture, both compile clean today); see `hir::lower_native::
1480 /// annotation::is_satisfiable_by_a_string_capture`'s own doc for why.
1481 ///
1482 /// Reported at the declaration — the same static-defect-in-the-
1483 /// declaration posture `E160`/`E166`/`E167` already take — pointing
1484 /// at the offending param's own type annotation range (an untyped or
1485 /// `content`-typed param never triggers this code, so the annotation
1486 /// is always present and non-`content` when it fires). `Error` by
1487 /// default: unlike `E168`/`E170`'s "silent race" posture, this is a
1488 /// param that can never receive a value of its declared type, not a
1489 /// stylistic ambiguity.
1490 E171,
1491 /// A native tag (`#…`) whose text begins with `@` — the shape of an
1492 /// ink-dialect compiler directive (`#@private`, `#@was(…)`,
1493 /// `#@local`, `#@module(…)`, `#@effects(…)`) — is lowered by a `.brink`
1494 /// file (issue #1835).
1495 ///
1496 /// `#@…` is not its own grammar production in either dialect: it is an
1497 /// ordinary tag (`HASH` + free text), and only ink's HIR lowerer
1498 /// (`hir::lower::directive::parse_directive_tag`) gives a leading `@`
1499 /// special, compile-time-consumed meaning. `#` is already the runtime-
1500 /// tag sigil in native content position (that is exactly *why* `#@…`
1501 /// parses as a tag rather than a directive there too), and
1502 /// `hir::lower_native` has no matching check — so before this code, an
1503 /// author porting a file from ink, or splitting time between the two
1504 /// dialects, got no error and no warning: the directive text became
1505 /// ordinary tag content on the compiled story, silently, which is worse
1506 /// than a plain no-op because it surfaces as mysterious runtime output
1507 /// rather than a compile-time failure.
1508 ///
1509 /// `Warning` by default, not `Error`: a literal `@`-led tag can be a
1510 /// deliberate runtime convention for a host that wants one (the issue's
1511 /// own caution), so the diagnostic is `[lints]`-configurable and
1512 /// `@[allow(E172)]`-suppressible rather than blocking, the same posture
1513 /// as `E132`/`E168`/`E170`. `hir::lower_native::body::lower_tag` raises
1514 /// it, naming the native spelling to use instead when the tag names a
1515 /// real ink directive that has one (`@[was(…)]`, `@[effects(…)]`) and
1516 /// saying so plainly when it does not (`module`, `public`, `private`,
1517 /// `local` have no native annotation counterpart yet). `#@allow` is
1518 /// its own case — ink's directive recognizer does not know `allow`
1519 /// either, so the message never calls it an ink-dialect spelling; it
1520 /// only notes that native's own `@[allow(…)]` annotation (an unrelated
1521 /// diagnostic-suppression channel) happens to share the name. Any other
1522 /// unrecognized name gets a shape-only wording that never asserts ink
1523 /// membership.
1524 E172,
1525
1526 // ── Required markup attributes (issue #1780 gap 1, ruled by #1997)
1527 // ────────────────────────────────────────────────────────────────
1528 /// An inline markup span of a declared kind is missing an attribute the
1529 /// host manifest marks `required` for that kind.
1530 ///
1531 /// The counterpart `E164`/`E165` never caught: `attrs` was an
1532 /// *allow*-list only until #1997, so a declared attribute simply absent
1533 /// from a span went undiagnosed. Gated the same way as `E165`: it only
1534 /// ever fires for a span whose *name* the manifest does declare (an
1535 /// undeclared name reports `E164` alone), and only for attributes the
1536 /// declaring kind actually marks `required` — a kind with none required
1537 /// never raises this for any span of that kind. One report per missing
1538 /// attribute, not one combined message, mirroring `E165`'s
1539 /// one-per-attribute posture rather than `E164`'s one-per-span.
1540 ///
1541 /// `Warning` by default, the same posture as `E164`/`E165`, for the same
1542 /// reason: only a `Warning`-base code is `[lints]`-configurable and
1543 /// `@[allow(…)]`-suppressible, and a host that wants a required
1544 /// attribute to be binding raises it with `[lints] E173 = "deny"`.
1545 E173,
1546 /// A lambda's own **written annotation** (a param's `: T` or the
1547 /// lambda's `: R` return annotation) disagrees with its body-derived
1548 /// type (issue #1994, RULED 2026-08-01, closing #1932: "the written
1549 /// annotation takes priority... an incompatible body is an eager error
1550 /// at the lambda, not a deferred surprise at the call site").
1551 ///
1552 /// `#1910`/PR #1928 made `infer::body::InferPass::infer_lambda` read a
1553 /// lambda's body-derived param/return types back — the same overlay
1554 /// `infer_def_body` already applies for a top-level `fn`/`flow` — which
1555 /// silently let a *wrong* body derivation override a *correct* written
1556 /// annotation with no diagnostic anywhere (a standalone `let f = |k:
1557 /// int|: int { "wrong" };` with no call site produced nothing at all).
1558 /// This code closes that gap for the annotated case specifically: a
1559 /// lambda's own written per-param/return annotation now always governs
1560 /// that slot's resulting type, and this diagnostic fires the moment the
1561 /// body-derived type (when it resolves to anything concrete) disagrees
1562 /// with it — deliberately **not** gradual/advisory like `E063`, since
1563 /// the annotation is the ruled source of truth for a lambda's own
1564 /// signature, not a hint to double-check later.
1565 ///
1566 /// `#1910`'s own fix is unchanged for the *unannotated* case — a
1567 /// lambda param/return with no written annotation still exports
1568 /// whatever its body derives, exactly as before.
1569 ///
1570 /// Native-only (`LAMBDA_EXPR` has no `brink-syntax` counterpart, same
1571 /// posture as `E156`/`E158`): raised only from
1572 /// `infer::body::InferPass::infer_lambda`, reported by
1573 /// `strict::check_lambda_annotation_mismatches` under `types = strict`.
1574 E174,
1575 /// RETIRED (issue #2165) — was `register`'s comptime-only-intrinsic
1576 /// confinement check (issue #1840 Q5): `register` was legal only inside
1577 /// the project's configured conventions module's `fn conventions()`,
1578 /// enforced here. The 2026-08-03 ruling (`docs/decision-log.md`, "`fn
1579 /// conventions()` is DISSOLVED") removed `fn conventions()` and
1580 /// `register` from the design entirely — precedence is now a static
1581 /// `order` property on `@[convention]` (issue #2164), needing no
1582 /// comptime evaluator and so no confinement diagnostic to raise. Code
1583 /// kept reserved, not reused.
1584 E175,
1585 /// A divert-with-args site (`-> knot(args)`, `->-> tunnel(args)`, or
1586 /// `<- thread(args)`) supplies a number of arguments that does not
1587 /// match its resolved target's declared parameter count (issue #2156).
1588 ///
1589 /// PR #2150 (issue #2136) wired native's `-> knot(args)` call-args
1590 /// syntax into `DivertTarget::args` for the first time — before that,
1591 /// the shape hard-failed `E129` on native and never reached this check
1592 /// at all. Investigating that newly-reachable path found the arity gap
1593 /// was real on **both** dialects: `brink_ir::symbols::project`'s
1594 /// `walk_divert_target`/`Expr::DivertTarget` ref-pushing sites always
1595 /// recorded `arg_count: None` for a `RefKind::Divert` reference,
1596 /// unconditionally discarding `DivertTarget::args.len()` — so
1597 /// `brink_analyzer::resolve::check_arity` (`E031`, gated on
1598 /// `arg_count.is_some()`) could never fire for a divert, on either
1599 /// dialect, regardless of how many arguments were supplied. `E176` is
1600 /// `E031`'s sibling for the divert call shape, kept as its own code
1601 /// (rather than widening `E031`'s own message, which names *function*
1602 /// calls) so the two diagnostics can be told apart and suppressed
1603 /// independently.
1604 ///
1605 /// Scoped to a resolution that names a `Knot`/`Stitch`/`Label` (the
1606 /// only symbol kinds with their own declared parameter row) —
1607 /// deliberately **not** checked when the divert resolves through a
1608 /// `Variable` or a divert-typed local `Param` (a stored/forwarded
1609 /// divert-target value, e.g. the ink docs' `-> generic_sleep (->
1610 /// waking_in_the_hut)` — see "Advanced: sending divert targets as
1611 /// parameters"), whose underlying target's arity is not known
1612 /// statically at the indirection site. `resolve_function`'s own
1613 /// `check_arity` call sites already draw this same line (only
1614 /// `External`/`Knot` resolutions are checked; `Variable`/local
1615 /// resolutions are not).
1616 ///
1617 /// `Warning`-tier by default, matching `E031`'s own severity precedent
1618 /// for the identical arity-mismatch shape at an ordinary call site —
1619 /// `lir::lower::stmts::lower_divert_target` still lowers a mismatched
1620 /// site (`lower_call_args` pushes exactly as many `CallArg`s as the
1621 /// divert supplies, not the target's declared count), so this stays
1622 /// advisory rather than blocking, and is `[lints]`-configurable /
1623 /// `@[allow(E176)]`-suppressible like every other `Warning`-base code.
1624 E176,
1625
1626 // ── `@[convention]` / `@[element]` split (issue #2164,
1627 // `docs/decision-log.md` 2026-08-03) — E177 was reserved for #2156
1628 // at the time this range was assigned; #2156 landed as E176 only
1629 // (see above), leaving E177 itself unclaimed and unused ────────
1630 /// A `@[convention(claims = "…")]` annotation with no `order` clause.
1631 ///
1632 /// `order` is **required**, not optional (`docs/decision-log.md`
1633 /// 2026-08-03 "`order` is REQUIRED on `@[convention]`…"): a claiming
1634 /// handler competes for lines it did not announce, so its precedence
1635 /// against every other claiming handler in the same module must be
1636 /// total, explicit, and authored — there is no default to fall back
1637 /// to, and the compiler never infers one from declaration position.
1638 /// Reported at the annotation line, the same posture `E159` already
1639 /// takes for a missing/malformed `claims` value; yields no
1640 /// `ConventionAnnotation` at all (never a partial one with a made-up
1641 /// order).
1642 E178,
1643 /// Two `@[convention]` declarations in the same module carry the same
1644 /// `order` value.
1645 ///
1646 /// Ties are **rejected**, not resolved (the same ruling as `E178`):
1647 /// "there is no tie-breaking rule, because ties are rejected rather
1648 /// than resolved." Reported against **every** conflicting declaration —
1649 /// the duplicate-definition posture, not a single "first one wins,
1650 /// second one is the problem" report — so an author sees the whole
1651 /// conflicting group regardless of which one they open first.
1652 E179,
1653 /// A `@[convention(…, attach = StructName)]` clause names a struct the
1654 /// declaration's own return type does not agree with (issue #2178,
1655 /// split from #2164's 2026-08-03 design-backport comment "item 2":
1656 /// "The attachment schema is a STRUCT — do not invent a DSL").
1657 ///
1658 /// `docs/decision-log.md` 2026-08-03 states the governing split
1659 /// plainly: *"keys are declared, values are computed"* — `attach`
1660 /// declares which keys a handler attaches and their types by naming an
1661 /// ordinary `struct`; the handler body computes the values. That only
1662 /// holds if the handler's own declared return type actually **is**
1663 /// the named struct — a mismatch (a different type, a generic, a
1664 /// `fn` type, or no declared return type at all) means the projection
1665 /// and the handler's real output could never agree, so this is
1666 /// reported at the `attach` clause's own value rather than silently
1667 /// trusting the name. Reported the same "never a partial one" way
1668 /// `E159`/`E178` are: no `ConventionAnnotation` at all results, rather
1669 /// than one carrying a schema its own declaration cannot honor.
1670 ///
1671 /// Declaration-surface-only, like every other check in this module:
1672 /// this compares `attach`'s name against the return type's own bare
1673 /// name, and never checks whether a struct of that name is actually
1674 /// *declared* anywhere — that is real name resolution's job (out of
1675 /// scope for this code, same posture `E171`'s own doc explains for
1676 /// captured-parameter types).
1677 E180,
1678
1679 // ── TM-4c struct-shape resolution backstop (docs/typed-mode-spec.md
1680 // §6, issue #2240) ─────────────────────────────────────────────
1681 /// `lir::lower::structs::build_shape_table`'s own
1682 /// `decls::lookup_global(index, file_id, name, SymbolKind::Struct)`
1683 /// call — resolving a declared `STRUCT`'s **own** `DefinitionId`,
1684 /// using its own declaring file as referrer — came back `None`.
1685 ///
1686 /// This is a non-suppressible defense-in-depth backstop, the
1687 /// `E060`/`E073` posture: it should never fire from a normal compile.
1688 /// The exact-file arm always matches a struct against itself *unless*
1689 /// `brink-analyzer` already dropped this HIR decl's own symbol entry
1690 /// as a true intra-module duplicate (`E023`, same declared module as
1691 /// an earlier same-name declaration) — and even then,
1692 /// `lookup_global`'s unscoped fallback normally rescues the
1693 /// surviving sibling's id (which is exactly what lets
1694 /// `build_shape_table`'s own `by_def.contains_key` dedup recognize
1695 /// "true intra-module duplicate" and skip it a second time, rather
1696 /// than minting a fresh, wrong shape). This code fires only in the
1697 /// narrower case the fallback itself cannot rescue: **every**
1698 /// surviving same-name candidate is std-declared, so the fallback's
1699 /// own std-exclusion (issue #2197) empties the search too. Before
1700 /// this code existed, that combination silently dropped the struct
1701 /// from both `ShapeTable` and `NameTable` seeding with no diagnostic
1702 /// at all — shifting every subsequent `ShapeId`/`NameId` and the
1703 /// bytecode built from them (CLAUDE.md: "silent drops are always
1704 /// bugs until proven otherwise").
1705 ///
1706 /// Reachable **today**, not merely in principle (review finding on
1707 /// #2240): any project whose own declaring file is not all-native
1708 /// (`project_is_all_native`) and whose own `STRUCT`/`struct` shares a
1709 /// name with one the std-mounted screenplay preset declares (`Cue`,
1710 /// `Parenthetical`) collides with it — neither side needs a `#@module`
1711 /// for this to happen. `symbol_index_query` builds the shared index
1712 /// from every `set_file`-registered file regardless of the
1713 /// compilation closure, so the mounted std declaration sits in the
1714 /// index even for an ink entry whose LIR closure never reaches it.
1715 /// With neither declaration module-qualified (or the project simply
1716 /// not `Dialect::Brink`), M-2d cross-declared-module coexistence
1717 /// (`is_cross_declared_module_collision`) never applies, so the pair
1718 /// collapses to an ordinary same-module duplicate — and it is the
1719 /// *project's* declaration that gets dropped whenever its own file
1720 /// sorts after the std key in `FileId`-mint order (any project file
1721 /// named e.g. `story.ink`, `world.ink`, `types.ink` does, since
1722 /// `"std/…"` sorts first). See `brink-environment`'s
1723 /// `e181_is_reachable_from_an_ordinary_ink_project_colliding_with_a_std_preset_name`
1724 /// for this compiled end to end through the real analyzer drop, not a
1725 /// hand-built `SymbolIndex`.
1726 ///
1727 /// `build_struct_shape_data` (the `NameId`-free, cutoff-friendly
1728 /// twin `struct_shape_data_query` memoizes for the per-knot chunk
1729 /// lowering path) performs the textually identical lookup and has no
1730 /// diagnostic sink of its own to push into — it is a pure,
1731 /// `Eq`-cutoff salsa data query, not a lowering pass threading a
1732 /// `Vec<Diagnostic>` accumulator. It is deliberately left silent
1733 /// there rather than given a redundant sink: every real compile
1734 /// (`brink-db`'s `lir_query`) always computes `build_shape_table`
1735 /// (via `lir_prelude_decls_query`) and `build_struct_shape_data` (via
1736 /// `struct_shape_data_query` → `chunk_lowering_ctx_query` →
1737 /// `lir_knot_chunk_query`) in the same salsa revision, over the same
1738 /// `resolutions_index_query` index and the same files' `structs`
1739 /// HIR — so the exact same drop condition always fires this
1740 /// diagnostic from the prelude side in the same compile. See that
1741 /// function's own doc comment for the full argument.
1742 E181,
1743
1744 // ── #2179: `@[convention]` no-world-reads fence ────────────────────
1745 /// A `@[convention]` handler's transitive call closure reaches an
1746 /// `EXTERNAL` classified [`crate::ExternalKind::Query`] (a world read)
1747 /// or left [`crate::ExternalKind::Plain`] (unclassified) —
1748 /// `docs/decision-log.md` 2026-08-06 "No-world-reads fence: analyzer
1749 /// effect-row check; unclassified externals are diagnosed".
1750 ///
1751 /// A claiming handler competes for lines it never announced, which
1752 /// only holds together if classification is a pure function of the
1753 /// text: if it depended on game state, the editor could never display
1754 /// it, the projection could never be cached, and explain-match would
1755 /// depend on a save file. So a handler may call pure functions and
1756 /// [`crate::ExternalKind::Effect`]/[`crate::ExternalKind::Presentation`]
1757 /// externals ("commands"), but never one that reads world state — and
1758 /// an unclassified (`Plain`, the default) external is treated the same
1759 /// as a proven read, not the same as a proven pure call: "unprovable
1760 /// is not passable." The fix is classifying the external, via an
1761 /// inline `@kind` doc tag or the registered host manifest.
1762 ///
1763 /// Computed by `brink_analyzer::no_world_reads` over the **transitive**
1764 /// call closure — a handler calling a helper `fn` that itself calls a
1765 /// `Query`/`Plain` external is diagnosed exactly like a direct call —
1766 /// reusing the same call-graph substrate
1767 /// (`brink_analyzer::infer::{collect_defs,call_edges,def_body}`) T2-1's
1768 /// effect rows are built from, per the #2179 decline comment's finding
1769 /// that the aggregated row/`compute_container_access` route has no
1770 /// span to diagnose with: this walks for the real call-site span the
1771 /// aggregated row structurally cannot carry. Reported at the offending
1772 /// call's own site, which may be inside a different definition (and a
1773 /// different file) than the handler's own declaration.
1774 E182,
1775 /// `brink_ir::lir::lower::expr::lower_call`'s resolved-target match
1776 /// found a symbol kind that is not callable — a `ListItem`, `Label`,
1777 /// `Stitch`, `Param`, `Temp`, or `Struct` sitting at a call position
1778 /// (issue #2837, filed from the #2836/w187 review). `SymbolKind::Knot`
1779 /// is the one non-`External`/`List`/`Variable`/`Constant` kind that
1780 /// *is* callable — ink allows any knot as a function via tunnels, per
1781 /// `brink_analyzer::resolve::resolve_function`'s own comment — so it
1782 /// keeps its own `lir::ExprKind::Call` arm.
1783 ///
1784 /// This *is* reachable from ordinary author source, not only from a
1785 /// hypothetical future resolution regression: `Temp`/`Param` reach this
1786 /// arm whenever `ctx.temp_slot` does not have the name open at the call
1787 /// site — which is the normal, expected shape of two ordinary author
1788 /// mistakes, not a `temp_slot` bug. Calling a T1b block-scoped temp
1789 /// (`~ { … }`) after its own block has closed is diverted to
1790 /// [`Self::E082`] instead (mirroring `lower_path`'s own guard for the
1791 /// same case), but a genuine forward reference — calling a name before
1792 /// its declaring `temp`/param binding — falls through to this arm and
1793 /// reports `E183` today; that reproduces on the plain `.ink` surface
1794 /// with no `--dialect brink` needed. `Stitch`, `ListItem`, `Label`, and
1795 /// `Struct` remain analyzer-unreachable for a real call site as far as
1796 /// this code can tell (`resolve_function` never hands back `Stitch`/
1797 /// `Label`/`Struct` there, and only hands back a bare `ListItem` for a
1798 /// `#fn(target)` literal, which never reaches `lower_call` at all) —
1799 /// those four are the defensive-backstop part of this diagnostic.
1800 ///
1801 /// Refused loudly rather than silently emitting `lir::ExprKind::Call`
1802 /// against the resolved id: that catch-all is exactly the mechanism
1803 /// that let PR #2836's first attempt compile a program clean — 7,941
1804 /// tests, the oracle ratchet, and clippy all green — while it then
1805 /// faulted at runtime with `UnresolvedDefinition(ListItem(..))`. Same
1806 /// "compile error over runtime fault" posture as [`Self::E144`]'s UFCS
1807 /// refusal in the same module.
1808 E183,
1809
1810 // ── issue #2262: E181's own drop class, for every OTHER declaration
1811 // kind `lir::lower::decls::lookup_global` self-resolves ─────────
1812 /// `lir::lower::decls`'s own `lookup_global(index, file_id, name,
1813 /// kind)` self-declaration lookup — for a `CONST`
1814 /// (`collect_globals`'s constants pass), a `VAR` (`collect_globals`'s
1815 /// variables pass), or an `EXTERNAL` (`collect_externals`) — came back
1816 /// `None`.
1817 ///
1818 /// The exact same non-suppressible defense-in-depth posture as
1819 /// [`Self::E181`], for the exact same reason: this is [`Self::E181`]'s
1820 /// own struct-shape drop class recurring at three more call sites in
1821 /// the same file, all sharing `lookup_global`'s doc comment and none
1822 /// fixed by #2240/#2258 (issue #2262, filed from that PR's own review —
1823 /// "#2240 under-captured the class"). The exact-file arm always
1824 /// matches a declaration against itself *unless* `brink-analyzer`
1825 /// already dropped this HIR decl's own symbol entry as a true
1826 /// intra-module duplicate (`E023`) — and even then, `lookup_global`'s
1827 /// unscoped fallback normally rescues the surviving sibling's id. This
1828 /// code fires only when that fallback also misses: **every** surviving
1829 /// same-name/same-kind candidate is std-declared, so the fallback's
1830 /// own std-visibility carve-out (issue #2197) excludes it too. Before
1831 /// this code existed, that combination silently dropped the
1832 /// `CONST`/`VAR`/`EXTERNAL` from `PreludeDecls` (no `lir::GlobalDef`
1833 /// or `lir::ExternalDef` at all) with no diagnostic whatsoever
1834 /// (CLAUDE.md: "silent drops are always bugs until proven otherwise").
1835 ///
1836 /// **Reachable today**, exactly as [`Self::E181`]'s own doc found for
1837 /// `STRUCT` (review finding on #2240): an ordinary project — no
1838 /// `#@module`, no `dialect` override even needed for `EXTERNAL` (core
1839 /// ink syntax, unlike `STRUCT`) — that declares its own `EXTERNAL
1840 /// scene_entered(…)` collides with the std-mounted screenplay
1841 /// preset's own `extern scene_entered`
1842 /// (`std/conventions/screenplay.brink`). Neither declares a module, so
1843 /// M-2d cross-declared-module coexistence never applies and
1844 /// `insert_symbol` treats the pair as a true intra-module duplicate,
1845 /// dropping whichever one's `FileId` sorts after the other's in mint
1846 /// order. See `brink-environment`'s
1847 /// `external_self_declaration_silently_drops_when_colliding_with_a_std_preset_name`
1848 /// for this compiled end to end through the real analyzer drop. `std`
1849 /// declares no `CONST`/`VAR` today, so the `CONST`/`VAR` call sites
1850 /// stay reachable only in principle (a future std module adding one),
1851 /// same status `E181` itself carried before its own reachable case was
1852 /// found — not a reason to leave them undiagnosed.
1853 E184,
1854
1855 /// Issue #1944: a plain dotted assignment target (`~ p.bogus = 1`)
1856 /// names a field its receiver's *resolved* struct shape doesn't
1857 /// declare — the `E070` mirror for a construction-literal's own
1858 /// unknown-field check (`structs::check`'s "Extra" case,
1859 /// `docs/typed-mode-spec.md` §6), but for `Stmt::Assignment`/
1860 /// `BlockStmt::Assignment` targets instead of a `#{...}` literal.
1861 ///
1862 /// PR #1939's `check_declared_field_assign_target` deliberately stays
1863 /// silent on an unresolvable field — it only compares a *resolved*
1864 /// field's declared type against the RHS ("Unknown never disagrees").
1865 /// `ref_projection::check_strict`'s `E098` covers an unknown segment
1866 /// only in `ref`-argument position (`ref npc.bogus`), not a plain
1867 /// assignment target. Before this code existed, the issue's exact
1868 /// repro —
1869 ///
1870 /// ```text
1871 /// STRUCT Point = #{x: float, y: float}
1872 /// VAR p: Point = Point#{x: 0.0, y: 0.0}
1873 /// ~ p.bogus = 1
1874 /// -> DONE
1875 /// ```
1876 ///
1877 /// — compiled clean under `types = strict` with zero diagnostics.
1878 ///
1879 /// Reported from `structs::check_field_assign_mismatch`, the same
1880 /// function `E063` (field-type mismatch on a *resolved* field) comes
1881 /// from — fired only once the walk has resolved the receiver's shape
1882 /// (`shapes.resolve` succeeded) and the shape itself declares no field
1883 /// by this name. An Unknown/untyped root never reaches this arm at
1884 /// all: the walk's own guard above (`let Ty::Struct(shape_name) =
1885 /// ¤t else { return; }`) returns silently the moment `current`
1886 /// isn't a resolved struct type, so "Unknown never disagrees" holds
1887 /// for the receiver exactly as it does for `E063`. A chained target
1888 /// (`o.i.a = v`, 3+ segments) never reaches this function at all —
1889 /// `check_declared_field_assign_target`'s own `segments.len() == 2`
1890 /// fence means no `FieldAssignMismatch` fact is ever recorded for one;
1891 /// LIR's `try_lower_field_assignment` already rejects it outright with
1892 /// the non-suppressible `E074`, regardless of whether the field name
1893 /// exists.
1894 E185,
1895
1896 /// Issue #2264: a `@[convention(…)]` handler declares BOTH `block` and
1897 /// `attach = StructName` on the same declaration — `parse_convention`
1898 /// (`annotation.rs`) now rejects the combination outright rather than
1899 /// silently accepting it. Before this code existed, nothing diagnosed
1900 /// the co-occurrence at all: `try_claim`'s dispatch (`element.rs`) is
1901 /// an `if is_block { .. } else if is_attach { .. }` with no exclusivity
1902 /// check anywhere upstream — `block` always won the `if`, `attach` was
1903 /// parsed and stored on `ConventionAnnotation` but never consulted, and
1904 /// the author got zero signal that half of what they wrote did
1905 /// nothing (verified: `red_probe_block_and_attach_together_compile_clean_with_attach_inert_today`
1906 /// in `lower_native::tests`, run BEFORE this code existed, proves the
1907 /// silent-drop shape end to end).
1908 ///
1909 /// This is deliberately a hard rejection, not an attempt to define what
1910 /// "wrap AND attach" would mean together — that is an open design
1911 /// question (issue #2264's own body: "Define what the combination is
1912 /// supposed to mean and implement it — but that's a design question
1913 /// (rule 7), not a good first assumption") with no ruling and no test
1914 /// pinning any combined
1915 /// semantics, so nothing here invents one. `parse_convention` returns
1916 /// `None` (never a partial `ConventionAnnotation`) — the same "never a
1917 /// partial one" posture `E159`/`E166`/`E167`/`E178`/`E180` already take
1918 /// — so a handler declaring both is never registered as a claiming
1919 /// handler at all, not merely warned about.
1920 ///
1921 /// Also reachable through the compact-cue desugar (`@NAME: text`,
1922 /// issue #2079) — it dispatches through the exact same `try_claim`
1923 /// function, so a compact-cue-claiming handler declaring both clauses
1924 /// hits this same check (confirmed on the issue by PR #2341's review).
1925 E186,
1926
1927 /// Issue #2201: a write to a `CONST` — plain/compound assignment, a
1928 /// postfix `++`/`--`, an indexed-assignment root, a bare in-place
1929 /// mutator (`pop`/`heap_pop`), a struct-field write/mutator whose root
1930 /// is a `CONST`, or passing the `CONST` by `ref` (bare or as a
1931 /// projection root). ink semantics (`ink/compiler/ParsedHierarchy/
1932 /// VariableAssignment.cs`, "Can't re-assign to a constant") reject this
1933 /// at compile time; before this code existed, `lir::lower::stmts::
1934 /// lower_assign_target` treated `SymbolKind::Constant` identically to
1935 /// `SymbolKind::Variable` — every one of the write channels above
1936 /// silently mutated the constant's storage cell with zero diagnostics
1937 /// anywhere in the pipeline.
1938 ///
1939 /// Raised via the shared `lir::lower::stmts::reject_const_write` check —
1940 /// the `CONST` analog of [`Self::E148`]'s `reject_as_binding_write` —
1941 /// called from every choke point that resolves a `Global` write root's
1942 /// `SymbolInfo`: `lower_assign_target` itself (plain/compound
1943 /// assignment, postfix's bare-target conversion, the
1944 /// indexed-assignment root via `lower_indexed_assignment`, and a bare
1945 /// mutator's root via `pop`/`heap_pop`, all of which call
1946 /// `lower_assign_target` for their root); `lower_single_level_field_write`/
1947 /// `lower_field_mutator` (their two-segment field-root
1948 /// `SymbolKind::Constant` arm, which resolves the root independently of
1949 /// `lower_assign_target` — the same reason `reject_as_binding_write`
1950 /// needs a direct call there too, per #2122); and the `ref`-argument
1951 /// choke points `lower_ref_path_call_arg`/`lower_ref_projection_arg`
1952 /// (passing a `CONST` by `ref` hands the callee a raw pointer to the
1953 /// cell, bypassing assignment lowering entirely).
1954 ///
1955 /// Deliberately a LIR-lowering refusal (this code's precedent is
1956 /// [`Self::E074`]/[`Self::E148`], not an analyzer diagnostic like
1957 /// [`Self::E185`]): the write-channel enumeration above already lives
1958 /// entirely in `lir::lower` — duplicating it in `brink-analyzer` would
1959 /// re-run the exact same channel-undercounting risk that made this
1960 /// issue's own premise true (#2122 named only two of the seven channels
1961 /// `CONST` reassignment turned out to have). Applies to both surfaces —
1962 /// `.ink` and `.brink` — since this mirrors ink's own compile-time
1963 /// rejection, not a native-only extension; `SymbolKind::Constant` is
1964 /// resolved identically for both frontends by the time LIR lowering
1965 /// sees it.
1966 E187,
1967
1968 // ── TM-2 reserved-type-name shadowing (issue #1865) ───────────────
1969 /// A declared `STRUCT`'s own name collides with one of the fixed names
1970 /// `annotations::resolve`'s `TypeExpr::Named` arm resolves *before* it
1971 /// ever consults `names.structs` — a builtin leaf
1972 /// (`int`/`float`/`bool`/`string`/`content`/`divert`) or an NS-A8 tower
1973 /// kind (`vec2`/`vec3`/`vec4`/`quat`/`mat2`/`mat3`/`mat4`). That
1974 /// ordering is deliberate and unchanged by this code (`resolve`'s own
1975 /// doc: "checked before the struct lookup... the same ordering that
1976 /// keeps int/float unshadowable") — this diagnostic does not re-order
1977 /// resolution, it names the consequence: every bare type annotation
1978 /// spelling the colliding name (`VAR v: content = ...`, a param/return
1979 /// annotation, …) silently resolves to the builtin/tower type, never to
1980 /// the struct, with previously no diagnostic in either direction.
1981 ///
1982 /// Deliberately does **not** cover the generic heads
1983 /// (`List`/`Array`/`Map`/`Option`/`Weighted`/`Handle`): those names are
1984 /// special-cased only inside `TypeExpr::Generic`'s own dispatch (e.g.
1985 /// `Array<T>`) — a *bare* `Named` reference to a struct sharing one of
1986 /// those names (`f: Array`, no `<...>`) still falls through to the
1987 /// ordinary `names.structs` lookup and resolves to the struct
1988 /// correctly; there is no actual collision to diagnose for those names.
1989 /// Also does not cover `void` — unlike the leaves above, `resolve`'s
1990 /// `Named` arm has no explicit `"void"` case at all, so a struct named
1991 /// `void` resolves fine too. Also does not cover a name shared with a
1992 /// declared `LIST` or a registered `Handle<K>` kind: `names.lists`/
1993 /// `names.handles` are only ever consulted inside `List<L>`/`Handle<K>`'s
1994 /// own generic-argument position, never against a bare `Named`
1995 /// annotation — a different namespace, no collision.
1996 ///
1997 /// A construction literal (`Name#{...}`) is unaffected by this
1998 /// shadowing for every name this code covers:
1999 /// `resolve::resolve_struct_ref`/`resolve_type_ref` resolve a `STRUCT`
2000 /// reference by ordinary `SymbolKind::Struct` lookup alone, with no
2001 /// builtin/tower precedence check at all — so `content#{...}` still
2002 /// constructs the user's struct even though `VAR v: content = ...`
2003 /// cannot name it.
2004 ///
2005 /// Warning-tier, not a rejected declaration (matches [`Self::E035`]'s
2006 /// "name shadows a built-in function" precedent, and the "deliberate"
2007 /// framing `resolve`'s own doc already gives this exact ordering) — a
2008 /// `STRUCT` named this way still compiles and constructs normally; only
2009 /// its *annotation* spelling is shadowed.
2010 E188,
2011
2012 /// Renaming an `EXTERNAL` changes the host binding (ruled 2026-08-24,
2013 /// "External renames: allowed behind the always-unsafe Force gate").
2014 ///
2015 /// Synthesized by the IDE's safe-rename gate, never emitted by
2016 /// compilation: an external's name is the story↔engine contract, so the
2017 /// story-side rename is always reported as breakage — the engine must
2018 /// re-register the function under the new name — and applies only
2019 /// through the report's Force path.
2020 E190,
2021
2022 /// An ink `TODO:` author note (issue #3050).
2023 ///
2024 /// Not a defect at all: `AUTHOR_WARNING` lines are the language's own
2025 /// work-remains marker, and until #3050 lowering dropped them silently.
2026 /// Surfacing each as an `Info`-default diagnostic (the [`Self::E157`]
2027 /// tier precedent) puts TODOs in the Problems panel and gives the
2028 /// studio's TODO panel a single source to consume, while never gating a
2029 /// compile and staying `[lints]`-tierable like every other code.
2030 E189,
2031
2032 /// A content line's inline stateful alternatives enumerate to more
2033 /// whole-line variants than the variant-group cap admits (#3274).
2034 ///
2035 /// The stage-2 flip compiles a line of textual alternatives into one
2036 /// enumerated variant group — each variant a real line-table entry, a
2037 /// translation unit, and a VO slot — so the product of the
2038 /// alternatives' branch counts is bounded
2039 /// (`lir::lower::recognize::VARIANT_CAP`). Breaching it is a worded
2040 /// hard error, never a silent fallback: an author whose line quietly
2041 /// stopped being VO-addressable would have no way to notice. The fix
2042 /// is to split the line or move an alternative to its own line.
2043 E191,
2044
2045 /// A `brink-`prefixed comment the suppression parser did not understand
2046 /// (#3259).
2047 ///
2048 /// Directives were matched by exact string equality and anything else
2049 /// was dropped in silence — so `// brink-disable-file E157`, which looks
2050 /// exactly like the line-scoped form that DOES take codes, suppressed
2051 /// nothing and reported nothing. The author got neither the behaviour
2052 /// they asked for nor a reason, which is the silent-drop shape this
2053 /// project treats as a bug by default.
2054 ///
2055 /// `Warning`-tier: the file still compiles. The harm is that a
2056 /// suppression the author believes is in force is not.
2057 E192,
2058
2059 /// A `~ temp` is read on a path its declaration does not dominate
2060 /// (#3354, RULED 2026-09-01 option C).
2061 ///
2062 /// The declaration and the read live in the same call frame, so the
2063 /// read resolves to the temp's own slot — but nothing guarantees the
2064 /// declaring statement ran first. The three shapes the ruling names are
2065 /// a sibling choice branch, a gather reached from a branch that did not
2066 /// declare, and a read written textually ahead of the declaration. (A
2067 /// fourth shape the ruling originally enumerated — a stitch reading a
2068 /// temp declared at its knot's root — is not a dominance question at
2069 /// all: the PR #3369 review found it warns on a knot/stitch divert that
2070 /// runs the declaration and then plays correctly, and the 2026-09-01
2071 /// follow-up ruling on #3373 moved it out of `E193` entirely into its
2072 /// own compat-deny code, [`Self::E194`].)
2073 ///
2074 /// `Warning`-tier, `[lints]`-overridable: the story still runs. The
2075 /// runtime reads an uninitialized slot as ink's missing-variable
2076 /// default (`0`, which is also `false`) and warns — matching the C#
2077 /// reference, so what plays in Inky plays in brink — and this
2078 /// diagnostic is what tells the author before they play.
2079 E193,
2080
2081 /// A knot's `~ temp` (native `~ let`) is read from one of that knot's
2082 /// stitches (#3373, RULED 2026-09-01) — split out of [`Self::E193`]'s
2083 /// former shape 4 during PR #3369's review.
2084 ///
2085 /// Brink's `lir::lower::temps::alloc_temps` treats a knot and every one
2086 /// of its stitches as one shared call frame with one `TempMap`, so a
2087 /// stitch's reference to a name the knot's root declares resolves to
2088 /// that same slot and the story plays correctly. Ink's own compiler
2089 /// does not extend a knot's `~ temp` visibility into its stitches at
2090 /// all — the identical program is a compile-time
2091 /// `Unresolved variable` error in inklecate. This is brink accepting a
2092 /// strict superset of ink, not a defect in either compiler, which makes
2093 /// it the first member of the **compat-deny** tier (`docs/compiler-spec.md`
2094 /// "Compat-deny diagnostics"): `Error` by default (inklecate rejects
2095 /// the program, so brink does too until a project opts in) but, unlike
2096 /// every other `Error`-default code, `[lints]`-overridable — all the
2097 /// way to `allow` — because the admission invariant that tier requires
2098 /// is met: downgraded, brink produces a working program.
2099 E194,
2100
2101 /// A choice with neither display/bracket text nor a divert (#3365),
2102 /// matching inklecate's own "Choice is completely empty" warning
2103 /// (`InkParser/InkParser_Choices.cs:84-86`; line 90 guards a different
2104 /// warning — "Blank choice", on the `* [] some text` shape — which this
2105 /// code deliberately does not cover).
2106 ///
2107 /// Raised from `hir::lower::choice::LowerChoice::lower_choice` (the ink
2108 /// surface only — see this code's doc page for why the native `{? … }`
2109 /// surface is deliberately not wired to it), where the same-line
2110 /// evidence the check needs — whether a `->`/divert token was written at
2111 /// all, even an empty one — is still available. Once lowered into
2112 /// `hir::Choice`, an explicit-but-empty divert (`* ->`) and no divert at
2113 /// all (`* []`) are indistinguishable (both leave no `Stmt::Divert` in
2114 /// the choice's body), so the check cannot be reconstructed later from
2115 /// the HIR alone the way `E034`'s all-fallback check can.
2116 ///
2117 /// Fires only when the choice has none of: a same-line divert (with or
2118 /// without a target), a tag directly on the choice line, or actual text
2119 /// in any of its three content regions (`start`/`bracket`/`inner`). A
2120 /// `(label)` or `{condition}` guard does NOT exempt a choice — matching
2121 /// the reference, which has no such carve-out either. `Warning`,
2122 /// `[lints]`-overridable, matching the sibling markup/shadow-warning
2123 /// family (`E164`/`E188`/…) — the story still compiles.
2124 E195,
2125}
2126
2127impl DiagnosticCode {
2128 /// Every `DiagnosticCode` variant, in declaration order.
2129 ///
2130 /// Kept in sync with the enum by hand (there is no derive-based
2131 /// enumeration here), but exercised by
2132 /// `brink-test-harness/tests/diagnostic_docs_validation.rs`'s
2133 /// `diagnostic_codes_are_unique` test: that test asserts `ALL.len()`
2134 /// matches the number of code strings `from_str_code` recognizes, so a
2135 /// variant added to the enum but missed here fails CI immediately
2136 /// instead of silently under-covering the uniqueness/round-trip checks.
2137 pub const ALL: &'static [Self] = &[
2138 Self::E001,
2139 Self::E002,
2140 Self::E003,
2141 Self::E004,
2142 Self::E005,
2143 Self::E006,
2144 Self::E007,
2145 Self::E008,
2146 Self::E009,
2147 Self::E010,
2148 Self::E011,
2149 Self::E012,
2150 Self::E013,
2151 Self::E014,
2152 Self::E015,
2153 Self::E016,
2154 Self::E017,
2155 Self::E018,
2156 Self::E019,
2157 Self::E020,
2158 Self::E021,
2159 Self::E022,
2160 Self::E023,
2161 Self::E024,
2162 Self::E025,
2163 Self::E026,
2164 Self::E027,
2165 Self::E028,
2166 Self::E029,
2167 Self::E030,
2168 Self::E031,
2169 Self::E032,
2170 Self::E033,
2171 Self::E034,
2172 Self::E035,
2173 Self::E036,
2174 Self::E037,
2175 Self::E038,
2176 Self::E039,
2177 Self::E040,
2178 Self::E041,
2179 Self::E042,
2180 Self::E043,
2181 Self::E044,
2182 Self::E045,
2183 Self::E046,
2184 Self::E047,
2185 Self::E048,
2186 Self::E049,
2187 Self::E050,
2188 Self::E051,
2189 Self::E052,
2190 Self::E053,
2191 Self::E054,
2192 Self::E055,
2193 Self::E056,
2194 Self::E057,
2195 Self::E058,
2196 Self::E059,
2197 Self::E060,
2198 Self::E061,
2199 Self::E062,
2200 Self::E063,
2201 Self::E064,
2202 Self::E065,
2203 Self::E066,
2204 Self::E067,
2205 Self::E068,
2206 Self::E069,
2207 Self::E070,
2208 Self::E071,
2209 Self::E072,
2210 Self::E073,
2211 Self::E074,
2212 Self::E075,
2213 Self::E076,
2214 Self::E077,
2215 Self::E078,
2216 Self::E079,
2217 Self::E080,
2218 Self::E081,
2219 Self::E082,
2220 Self::E083,
2221 Self::E084,
2222 Self::E085,
2223 Self::E086,
2224 Self::E087,
2225 Self::E088,
2226 Self::E089,
2227 Self::E090,
2228 Self::E091,
2229 Self::E092,
2230 Self::E093,
2231 Self::E094,
2232 Self::E095,
2233 Self::E096,
2234 Self::E097,
2235 Self::E098,
2236 Self::E099,
2237 Self::E100,
2238 Self::E101,
2239 Self::E102,
2240 Self::E103,
2241 Self::E104,
2242 Self::E105,
2243 Self::E106,
2244 Self::E107,
2245 Self::E108,
2246 Self::E109,
2247 Self::E110,
2248 Self::E111,
2249 Self::E112,
2250 Self::E113,
2251 Self::E114,
2252 Self::E115,
2253 Self::E116,
2254 Self::E117,
2255 Self::E118,
2256 Self::E119,
2257 Self::E120,
2258 Self::E121,
2259 Self::E122,
2260 Self::E123,
2261 Self::E124,
2262 Self::E125,
2263 Self::E126,
2264 Self::E127,
2265 Self::E128,
2266 Self::E129,
2267 Self::E130,
2268 Self::E131,
2269 Self::E132,
2270 Self::E133,
2271 Self::E134,
2272 Self::E135,
2273 Self::E136,
2274 Self::E137,
2275 Self::E138,
2276 Self::E139,
2277 Self::E140,
2278 Self::E141,
2279 Self::E142,
2280 Self::E143,
2281 Self::E144,
2282 Self::E145,
2283 Self::E146,
2284 Self::E147,
2285 Self::E148,
2286 Self::E149,
2287 Self::E150,
2288 Self::E151,
2289 Self::E152,
2290 Self::E153,
2291 Self::E154,
2292 Self::E155,
2293 Self::E156,
2294 Self::E157,
2295 Self::E158,
2296 Self::E159,
2297 Self::E160,
2298 Self::E161,
2299 Self::E162,
2300 Self::E163,
2301 Self::E164,
2302 Self::E165,
2303 Self::E166,
2304 Self::E167,
2305 Self::E168,
2306 Self::E169,
2307 Self::E170,
2308 Self::E171,
2309 Self::E172,
2310 Self::E173,
2311 Self::E174,
2312 Self::E175,
2313 Self::E176,
2314 Self::E178,
2315 Self::E179,
2316 Self::E180,
2317 Self::E181,
2318 Self::E182,
2319 Self::E183,
2320 Self::E184,
2321 Self::E185,
2322 Self::E186,
2323 Self::E187,
2324 Self::E188,
2325 Self::E189,
2326 Self::E190,
2327 Self::E191,
2328 Self::E192,
2329 Self::E193,
2330 Self::E194,
2331 Self::E195,
2332 ];
2333
2334 /// The stable string representation (e.g., `"E001"`).
2335 #[must_use]
2336 #[expect(
2337 clippy::too_many_lines,
2338 reason = "a flat one-arm-per-code table that necessarily grows with the diagnostic set"
2339 )]
2340 pub fn as_str(self) -> &'static str {
2341 match self {
2342 Self::E001 => "E001",
2343 Self::E002 => "E002",
2344 Self::E003 => "E003",
2345 Self::E004 => "E004",
2346 Self::E005 => "E005",
2347 Self::E006 => "E006",
2348 Self::E007 => "E007",
2349 Self::E008 => "E008",
2350 Self::E009 => "E009",
2351 Self::E010 => "E010",
2352 Self::E011 => "E011",
2353 Self::E012 => "E012",
2354 Self::E013 => "E013",
2355 Self::E014 => "E014",
2356 Self::E015 => "E015",
2357 Self::E016 => "E016",
2358 Self::E017 => "E017",
2359 Self::E018 => "E018",
2360 Self::E019 => "E019",
2361 Self::E020 => "E020",
2362 Self::E021 => "E021",
2363 Self::E022 => "E022",
2364 Self::E023 => "E023",
2365 Self::E024 => "E024",
2366 Self::E025 => "E025",
2367 Self::E026 => "E026",
2368 Self::E027 => "E027",
2369 Self::E028 => "E028",
2370 Self::E029 => "E029",
2371 Self::E030 => "E030",
2372 Self::E031 => "E031",
2373 Self::E032 => "E032",
2374 Self::E033 => "E033",
2375 Self::E034 => "E034",
2376 Self::E035 => "E035",
2377 Self::E036 => "E036",
2378 Self::E037 => "E037",
2379 Self::E038 => "E038",
2380 Self::E039 => "E039",
2381 Self::E040 => "E040",
2382 Self::E041 => "E041",
2383 Self::E042 => "E042",
2384 Self::E043 => "E043",
2385 Self::E044 => "E044",
2386 Self::E045 => "E045",
2387 Self::E046 => "E046",
2388 Self::E047 => "E047",
2389 Self::E048 => "E048",
2390 Self::E049 => "E049",
2391 Self::E050 => "E050",
2392 Self::E051 => "E051",
2393 Self::E052 => "E052",
2394 Self::E053 => "E053",
2395 Self::E054 => "E054",
2396 Self::E055 => "E055",
2397 Self::E056 => "E056",
2398 Self::E057 => "E057",
2399 Self::E058 => "E058",
2400 Self::E059 => "E059",
2401 Self::E060 => "E060",
2402 Self::E061 => "E061",
2403 Self::E062 => "E062",
2404 Self::E063 => "E063",
2405 Self::E064 => "E064",
2406 Self::E065 => "E065",
2407 Self::E066 => "E066",
2408 Self::E067 => "E067",
2409 Self::E068 => "E068",
2410 Self::E069 => "E069",
2411 Self::E070 => "E070",
2412 Self::E071 => "E071",
2413 Self::E072 => "E072",
2414 Self::E073 => "E073",
2415 Self::E074 => "E074",
2416 Self::E075 => "E075",
2417 Self::E076 => "E076",
2418 Self::E077 => "E077",
2419 Self::E078 => "E078",
2420 Self::E079 => "E079",
2421 Self::E080 => "E080",
2422 Self::E081 => "E081",
2423 Self::E082 => "E082",
2424 Self::E083 => "E083",
2425 Self::E084 => "E084",
2426 Self::E085 => "E085",
2427 Self::E086 => "E086",
2428 Self::E087 => "E087",
2429 Self::E088 => "E088",
2430 Self::E089 => "E089",
2431 Self::E090 => "E090",
2432 Self::E091 => "E091",
2433 Self::E092 => "E092",
2434 Self::E093 => "E093",
2435 Self::E094 => "E094",
2436 Self::E095 => "E095",
2437 Self::E096 => "E096",
2438 Self::E097 => "E097",
2439 Self::E098 => "E098",
2440 Self::E099 => "E099",
2441 Self::E100 => "E100",
2442 Self::E101 => "E101",
2443 Self::E102 => "E102",
2444 Self::E103 => "E103",
2445 Self::E104 => "E104",
2446 Self::E105 => "E105",
2447 Self::E106 => "E106",
2448 Self::E107 => "E107",
2449 Self::E108 => "E108",
2450 Self::E109 => "E109",
2451 Self::E110 => "E110",
2452 Self::E111 => "E111",
2453 Self::E112 => "E112",
2454 Self::E113 => "E113",
2455 Self::E114 => "E114",
2456 Self::E115 => "E115",
2457 Self::E116 => "E116",
2458 Self::E117 => "E117",
2459 Self::E118 => "E118",
2460 Self::E119 => "E119",
2461 Self::E120 => "E120",
2462 Self::E121 => "E121",
2463 Self::E122 => "E122",
2464 Self::E123 => "E123",
2465 Self::E124 => "E124",
2466 Self::E125 => "E125",
2467 Self::E126 => "E126",
2468 Self::E127 => "E127",
2469 Self::E128 => "E128",
2470 Self::E129 => "E129",
2471 Self::E130 => "E130",
2472 Self::E131 => "E131",
2473 Self::E132 => "E132",
2474 Self::E133 => "E133",
2475 Self::E134 => "E134",
2476 Self::E135 => "E135",
2477 Self::E136 => "E136",
2478 Self::E137 => "E137",
2479 Self::E138 => "E138",
2480 Self::E139 => "E139",
2481 Self::E140 => "E140",
2482 Self::E141 => "E141",
2483 Self::E142 => "E142",
2484 Self::E143 => "E143",
2485 Self::E144 => "E144",
2486 Self::E145 => "E145",
2487 Self::E146 => "E146",
2488 Self::E147 => "E147",
2489 Self::E148 => "E148",
2490 Self::E149 => "E149",
2491 Self::E150 => "E150",
2492 Self::E151 => "E151",
2493 Self::E152 => "E152",
2494 Self::E153 => "E153",
2495 Self::E154 => "E154",
2496 Self::E155 => "E155",
2497 Self::E156 => "E156",
2498 Self::E157 => "E157",
2499 Self::E158 => "E158",
2500 Self::E159 => "E159",
2501 Self::E160 => "E160",
2502 Self::E161 => "E161",
2503 Self::E162 => "E162",
2504 Self::E163 => "E163",
2505 Self::E164 => "E164",
2506 Self::E165 => "E165",
2507 Self::E166 => "E166",
2508 Self::E167 => "E167",
2509 Self::E168 => "E168",
2510 Self::E169 => "E169",
2511 Self::E170 => "E170",
2512 Self::E171 => "E171",
2513 Self::E172 => "E172",
2514 Self::E173 => "E173",
2515 Self::E174 => "E174",
2516 Self::E175 => "E175",
2517 Self::E176 => "E176",
2518 Self::E178 => "E178",
2519 Self::E179 => "E179",
2520 Self::E180 => "E180",
2521 Self::E181 => "E181",
2522 Self::E182 => "E182",
2523 Self::E183 => "E183",
2524 Self::E184 => "E184",
2525 Self::E185 => "E185",
2526 Self::E186 => "E186",
2527 Self::E187 => "E187",
2528 Self::E188 => "E188",
2529 Self::E189 => "E189",
2530 Self::E190 => "E190",
2531 Self::E191 => "E191",
2532 Self::E192 => "E192",
2533 Self::E193 => "E193",
2534 Self::E194 => "E194",
2535 Self::E195 => "E195",
2536 }
2537 }
2538
2539 /// Short human-readable title for this diagnostic code.
2540 #[must_use]
2541 #[expect(
2542 clippy::too_many_lines,
2543 reason = "a flat one-arm-per-code message table that necessarily grows with the diagnostic set"
2544 )]
2545 pub fn title(self) -> &'static str {
2546 match self {
2547 Self::E001 => "knot is missing a name",
2548 Self::E002 => "stitch is missing a name",
2549 Self::E003 => "parameter is missing a name",
2550 Self::E004 => "VAR declaration is missing a name",
2551 Self::E005 => "VAR declaration is missing an initializer",
2552 Self::E006 => "CONST declaration is missing a name",
2553 Self::E007 => "CONST declaration is missing an initializer",
2554 Self::E008 => "LIST declaration is missing a name",
2555 Self::E009 => "LIST member is missing a name",
2556 Self::E010 => "EXTERNAL declaration is missing a name",
2557 Self::E011 => "retired (lane-A audit) — parser always creates FILE_PATH",
2558 Self::E012 => "divert is missing a target",
2559 Self::E013 | Self::E018 => "retired (lane-A audit) — parser always creates PATH node",
2560 Self::E014 => "logic line has no effect",
2561 Self::E015 => "expression is missing an operand",
2562 Self::E016 => "unknown or unsupported operator",
2563 Self::E017 => "function call is missing a name",
2564 Self::E019 => "retired (lane-A audit) — parser guarantees bullet markers",
2565 Self::E020 => "inline conditional is missing a condition",
2566 Self::E021 => "inline sequence has no branches",
2567 Self::E022 => "duplicate knot definition",
2568 Self::E023 => "duplicate variable/constant definition",
2569 Self::E024 => "unresolved divert target",
2570 Self::E025 => "unresolved variable reference",
2571 Self::E026 => "duplicate list item",
2572 Self::E027 => "ambiguous bare list item reference",
2573 Self::E028 => "retired (lane-A audit) — circular INCLUDE surfaces as CompileError",
2574 Self::E029 => "choice in conditional must explicitly divert",
2575 Self::E030 => "string interpolation in constant initializer is ignored",
2576 Self::E031 => "function call argument count mismatch",
2577 Self::E032 => "return statement outside function",
2578 Self::E033 => "unreachable code after divert",
2579 Self::E034 => "choice set has only fallback choices",
2580 Self::E035 => "name shadows a built-in function",
2581 Self::E036 => "expected diagnostic not produced",
2582 Self::E037 => "syntax error",
2583 Self::E038 => "malformed doc-comment tag",
2584 Self::E039 => "manifest disagrees with EXTERNAL arity",
2585 Self::E040 => "unknown semantic type",
2586 Self::E041 => "external argument type mismatch",
2587 Self::E042 => "external argument out of domain",
2588 Self::E043 => "doc-comment tag not applicable to this declaration",
2589 Self::E044 => "unknown directive",
2590 Self::E045 => "directive has no valid target here",
2591 Self::E046 => "directive must be static text",
2592 Self::E047 => "directive must be the only tag on its line",
2593 Self::E048 => "duplicate directive",
2594 Self::E049 => "directive not supported on this target",
2595 Self::E050 => "directive does not take arguments",
2596 Self::E051 => "brink extension used under strict-ink dialect",
2597 Self::E052 => "brink extension not yet implemented",
2598 Self::E053 => "retired (T1b-2) — T1b extension lowering is complete",
2599 Self::E054 => "block-scoped temp shadows an already-visible temp",
2600 Self::E055 => "collection mutator's first argument is not an lvalue",
2601 Self::E056 => "collection mutator used in expression position",
2602 Self::E057 => "break/continue outside a loop",
2603 Self::E058 => "collection mutator argument count mismatch",
2604 Self::E059 => "choice/gather construct nested inside inline content",
2605 Self::E060 => "internal codegen error",
2606 Self::E061 => "unknown type name in annotation",
2607 Self::E062 => "retired (T1c-1) — fn(T…): R annotations now resolve for real",
2608 Self::E063 => "type annotation disagrees with inferred type",
2609 Self::E064 => "strict types require the brink dialect",
2610 Self::E065 => "type escapes strict inference as Unknown",
2611 Self::E066 => "type is Conflicted under strict inference",
2612 Self::E067 => "assigning the result of a void function",
2613 Self::E068 => "struct construction literal names an undeclared STRUCT",
2614 Self::E069 => "struct construction literal is missing a declared field",
2615 Self::E070 => "struct construction literal supplies an undeclared field",
2616 Self::E071 => "struct construction literal field disagrees with the declared type",
2617 Self::E072 => "retired (TM-4c) — struct constructs now lower for real",
2618 Self::E073 => {
2619 "struct construction literal names an unresolved STRUCT shape at LIR lowering"
2620 }
2621 Self::E074 => "chained field-write projection (p.a.b = v) is not supported",
2622 Self::E075 => {
2623 "struct construction literal in a VAR/CONST declaration default does not match its declared shape"
2624 }
2625 Self::E076 => {
2626 "map literal key in a VAR/CONST declaration default is not a compile-time-constant scalar (int/string/bool)"
2627 }
2628 Self::E077 => {
2629 "array element, map value, or #fn bound value argument in a VAR/CONST declaration default is not a compile-time-constant expression"
2630 }
2631 Self::E078 => "int()/float() argument is outside the permissive numeric+bool domain",
2632 Self::E079 => "#fn target is not a statically-named function definition",
2633 Self::E080 => {
2634 "ref-argument (#fn, call, or bind) does not bind a durable cell at creation"
2635 }
2636 Self::E081 => "#fn binds more arguments than the target declares",
2637 Self::E082 => "block-scoped temp referenced after its block has closed",
2638 Self::E083 => "VAR/CONST declaration default is not a compile-time-constant expression",
2639 Self::E084 => "struct construction literal supplies a duplicate field",
2640 Self::E085 => {
2641 "file's module (its stem) collides with a declared module of the same name"
2642 }
2643 Self::E086 => {
2644 "`#@module` requires exactly one module name and may appear at most once per file"
2645 }
2646 Self::E087 => "reference to a `#@private` definition in another module",
2647 Self::E088 => {
2648 "bare `IMPORT { name } FROM mod` names a definition the declared module does not export"
2649 }
2650 Self::E089 => "`IMPORT` brings the same name into scope more than once",
2651 Self::E090 => "a module cannot `IMPORT` itself",
2652 Self::E091 => {
2653 "qualified access is ambiguous: the name is both an imported module and a definition"
2654 }
2655 Self::E092 => "redundant `#@public`/`#@private` — restates the module default",
2656 Self::E093 => "conflicting or repeated visibility directives on one declaration",
2657 Self::E094 => "`#@was` requires exactly one non-empty old-name argument",
2658 Self::E095 => "`#@was` names the definition's own current name — nothing to migrate",
2659 Self::E096 => "duplicate definition declared in two different modules",
2660 Self::E097 => "`ref` projection expression outside ref-argument position",
2661 Self::E098 => "ref-argument path segment disagrees with the statically-known shape",
2662 Self::E099 => "path-projection ref-argument is not yet lowerable (T1e-2, #828)",
2663 Self::E100 => "`#@effects` requires `pure` or at least one reads/writes/calls clause",
2664 Self::E101 => "malformed `#@effects` clause (unknown keyword or non-identifier value)",
2665 Self::E102 => "`#@effects` clause names an unknown global cell or external",
2666 Self::E103 => "inferred effects exceed the `#@effects` assertion's declared bound",
2667 Self::E104 => {
2668 "direct-call syntax requires a bare variable/temp/param callee — use `call(f, args…)` for a computed callee"
2669 }
2670 Self::E105 => {
2671 "`await` condition must be effect-free (read-only) — it writes a global or performs an effectful call"
2672 }
2673 Self::E106 => "map-literal key is outside the int/string/bool key domain",
2674 Self::E107 => "bare `none` needs a type from context",
2675 Self::E108 => {
2676 "inferred effects exceed the `@[effects(silent)]` assertion (the definition can produce content)"
2677 }
2678 Self::E109 => {
2679 "inferred effects exceed the `@[effects(total)]` assertion (the definition can raise a turn-terminating fault)"
2680 }
2681 Self::E110 => {
2682 "`#@effects(…)` is deprecated; use the `@[effects(…)]` annotation spelling"
2683 }
2684 Self::E111 => {
2685 "unknown annotation name (the `@[…]` channel recognizes `effects`, plus `was` and `allow` on the native surface)"
2686 }
2687 Self::E112 => {
2688 "annotation line outside a recognized placement (ink: top of a knot/stitch body; native: directly above a `flow`/`fn`, or above any declaration or statement for `allow`)"
2689 }
2690 Self::E113 => {
2691 "reserved protocol method name (`display`/`compare`/`next` belong to the protocol registry)"
2692 }
2693 Self::E114 => "protocol impl exceeds its protocol's effect contract",
2694 Self::E115 => "ill-formed protocol impl registration",
2695 Self::E116 => {
2696 "an `Option[T]` has no truthiness — test `== none` / `== some(x)` in the condition"
2697 }
2698 Self::E117 => "`int(r)` requires an inhabited range (NonEmptyRange)",
2699 Self::E118 => {
2700 "numeric-tower kinds are compiler-known and cannot implement registry protocols"
2701 }
2702 // Two verb families share this code because one sitting ruled
2703 // both: NS-A4's `sort_by`/`sorted_by` comparators and the
2704 // fn-value verb layer's pure trio `map`/`filter`/`fold`
2705 // (issue #1679). The title names the shared requirement; the
2706 // per-site message names the verb and its callback's role.
2707 Self::E119 => "callback must be a pure, silent function",
2708 Self::E120 => "`weighted` requires weight/value pairs with positive int weights",
2709 Self::E121 => {
2710 "admission: unresolved reference has no matching referencing expression in the HIR body"
2711 }
2712 Self::E122 => "admission: declared symbol has no corresponding HIR declaration node",
2713 Self::E123 => {
2714 "admission: knot's `is_function` disagrees with its indexed function sentinel"
2715 }
2716 Self::E124 => "admission: node range is empty or extends past the end of the file",
2717 Self::E125 => "admission: two references share an identical source range",
2718 Self::E126 => {
2719 "admission: declared symbol's name does not match its kind's qualification shape"
2720 }
2721 Self::E127 => {
2722 "admission: divert or return is not the last statement in an inline conditional/sequence branch"
2723 }
2724 Self::E128 => {
2725 "admission: container's provenance kind disagrees with its indexed symbol kind"
2726 }
2727 Self::E129 => "native: construct parses but has no HIR lowering yet",
2728 Self::E130 => "native: `flow` nested more than two levels deep is not yet supported",
2729 Self::E131 => "native: `<-` (splice) used outside a choice point has no effect",
2730 Self::E132 => {
2731 "native: `@[was]` needs a quoted old module path, e.g. `@[was(\"story::old::path\")]`"
2732 }
2733 Self::E133 => {
2734 "native accept-list: root_content must be empty or the synthesized `flow main()` entry divert"
2735 }
2736 Self::E134 => {
2737 "native accept-list: INCLUDE sites are ink-only baggage, never legal in native HIR"
2738 }
2739 Self::E135 => "native accept-list: thread-start outside choice-point splice position",
2740 Self::E136 => "native accept-list: choice set carries a non-neutral weave-fold value",
2741 Self::E137 => "native .brink compile requires types = strict",
2742 Self::E138 => "map construction literal supplies a duplicate key",
2743 Self::E139 => "construction literal entries do not match the target type's form",
2744 Self::E140 => "method-call syntax matched a field that is not callable",
2745 Self::E141 => "method-call syntax matched neither a field nor a free function",
2746 Self::E142 => "method-call receiver type is unknown — annotate it",
2747 Self::E143 => "method-call auto-ref needs a receiver that can be written through",
2748 Self::E144 => "native: method call resolves but has no LIR lowering yet",
2749 Self::E145 => {
2750 "the `as` binding must be the entire condition (no `&&`/`||` composition)"
2751 }
2752 Self::E146 => "retired (issue #1508) — choice-guard `as` bindings now lower for real",
2753 Self::E147 => "the `as` binding requires an `Option[T]` condition",
2754 Self::E148 => "an `as` binding is immutable and cannot be assigned to",
2755 Self::E149 => "`remove` is map-only — did you mean `remove_at`?",
2756 Self::E150 => {
2757 "declares a return type but the body may fall through without returning a value"
2758 }
2759 Self::E151 => {
2760 "native: this choice branch falls through while a sibling diverts — did you mean to add `-> …`, or `-> DONE` to end deliberately?"
2761 }
2762 Self::E152 => {
2763 "`contains`'s needle is statically outside the map key domain — this call always returns `false`"
2764 }
2765 Self::E153 => "`@[allow(…)]` names a diagnostic code this compiler does not know",
2766 Self::E154 => {
2767 "`@[allow(…)]` names a non-suppressible diagnostic — only codes whose default severity is not `Error` can be silenced at the source"
2768 }
2769 Self::E155 => {
2770 "`@[allow(…)]` needs at least one bare diagnostic code, e.g. `@[allow(E151)]`"
2771 }
2772 Self::E156 => {
2773 "a lambda cannot assign to a captured binding — captures are by value, so the write would be lost"
2774 }
2775 Self::E157 => {
2776 "this once-only choice or sequence carries durable state but has no name to anchor its identity across edits"
2777 }
2778 Self::E158 => {
2779 "a lambda cannot capture this local here — most likely its own `let` name read recursively, before the `let` finishes binding"
2780 }
2781 Self::E159 => {
2782 "`@[element(…)]` needs exactly one of `args = \"…\"` / `claims = \"…\"`, whose value compiles as a portable-regex pattern"
2783 }
2784 Self::E160 => {
2785 "`@[element(…)]`'s pattern names a capture group that does not match any parameter on the annotated declaration"
2786 }
2787 Self::E161 => {
2788 "`@[style(…)]` clauses must be `key = \"value\"` pairs, e.g. `@[style(line = \"dim\")]`"
2789 }
2790 Self::E162 => {
2791 "`@[style(…)]` names a key that is neither `line`, `dispatch`, nor a capture declared by the paired `@[element(…)]`"
2792 }
2793 Self::E163 => "`@[style(…)]` needs a paired `@[element(…)]` on the same declaration",
2794 Self::E164 => {
2795 "inline markup tag is not declared in the host manifest's markup vocabulary"
2796 }
2797 Self::E165 => {
2798 "inline markup attribute is not declared for this span kind in the host manifest"
2799 }
2800 Self::E166 => {
2801 "a block `@[element(…, block)]` / `@[convention(…, block)]` needs a trailing `content`-typed parameter that is not one of its own named captures"
2802 }
2803 Self::E167 => {
2804 "a `@[convention(claims = \"…\", order = N)]` handler declares a parameter its pattern never captures"
2805 }
2806 Self::E168 => {
2807 "this `@[convention(claims = \"…\", order = N)]` pattern is byte-identical to an earlier-declared handler's, and never won a claim of its own — it is dead code"
2808 }
2809 Self::E169 => {
2810 "a pattern-claiming `@[convention(claims = \"…\", order = N)]` handler is legal only in the project's configured conventions module (`brink.toml`'s `[project] conventions`)"
2811 }
2812 Self::E170 => {
2813 "this `@[convention(claims = \"…\", order = N)]` pattern can overlap with an earlier-declared handler's pattern — they silently race, with the lower-`order` one winning"
2814 }
2815 Self::E171 => {
2816 "a `@[convention(claims = \"…\", order = N)]` handler's captured parameter is declared `string`-incompatible — every capture binds as a plain string literal until numeric coercion lands"
2817 }
2818 Self::E172 => {
2819 "native: a `#…` tag beginning with `@` is the ink-dialect compiler-directive shape (`#@private`/`#@was`/`#@local`/…) — native has no such directive channel, so it lowers as an ordinary runtime tag"
2820 }
2821 Self::E173 => {
2822 "inline markup tag is missing an attribute the host manifest marks required for this span kind"
2823 }
2824 Self::E174 => {
2825 "a lambda's written parameter/return annotation disagrees with the type its body actually infers"
2826 }
2827 Self::E175 => {
2828 "retired (issue #2165) — `fn conventions()`/`register` were dissolved from the design"
2829 }
2830 Self::E176 => {
2831 "a divert-with-args site (`-> knot(args)`, tunnel call, or thread-start) supplies the wrong number of arguments for its resolved target's declared parameters"
2832 }
2833 Self::E178 => "`@[convention(…)]` needs a required `order = N` clause",
2834 Self::E179 => "two `@[convention]` declarations in this module carry the same `order`",
2835 Self::E180 => {
2836 "a `@[convention(…, attach = StructName)]` clause disagrees with the handler's own declared return type"
2837 }
2838 Self::E181 => {
2839 "a declared STRUCT's own definition could not be resolved while building the struct-shape table — every surviving same-name candidate is std-declared"
2840 }
2841 Self::E182 => {
2842 "a `@[convention]` handler's call closure reaches a world-reading (or unclassified) `EXTERNAL` — handlers may call pure functions and commands, but never read world state"
2843 }
2844 Self::E183 => "call target resolved to a symbol kind that is not callable",
2845 Self::E184 => {
2846 "a declared CONST/VAR/EXTERNAL's own definition could not be resolved while lowering — every surviving same-name candidate is std-declared"
2847 }
2848 Self::E185 => "plain assignment target names a field its struct shape does not declare",
2849 Self::E186 => {
2850 "`@[convention(…)]` declares both `block` and `attach = StructName` — mutually exclusive clauses"
2851 }
2852 Self::E187 => {
2853 "write to a CONST — CONST is immutable and can never be reassigned, mutated, or passed by `ref`"
2854 }
2855 Self::E188 => {
2856 "declared STRUCT name collides with a reserved builtin/tower type name and is unreachable in type annotations"
2857 }
2858 Self::E189 => "ink `TODO:` author note — work the author marked as remaining",
2859 Self::E190 => {
2860 "renaming an EXTERNAL changes the host binding — the engine must re-register the new name"
2861 }
2862 Self::E191 => {
2863 "inline alternatives on one line enumerate to more whole-line variants than the cap allows"
2864 }
2865 Self::E192 => {
2866 "unrecognized `brink-` directive comment — it suppresses nothing as written"
2867 }
2868 Self::E193 => "`temp` read on a path its declaration does not dominate",
2869 Self::E194 => "a knot's temp is not visible from its stitches",
2870 Self::E195 => "choice has neither display text nor a divert",
2871 }
2872 }
2873
2874 /// Default severity for this diagnostic code.
2875 #[must_use]
2876 pub fn severity(self) -> Severity {
2877 match self {
2878 Self::E014
2879 | Self::E022
2880 | Self::E023
2881 | Self::E026
2882 | Self::E030
2883 | Self::E031
2884 | Self::E033
2885 | Self::E034
2886 | Self::E035
2887 | Self::E038
2888 | Self::E043
2889 | Self::E054
2890 | Self::E063
2891 | Self::E092
2892 | Self::E095
2893 | Self::E106
2894 | Self::E110
2895 | Self::E131
2896 | Self::E132
2897 | Self::E151
2898 | Self::E152
2899 // Issue #1733 / §4.2: markup vocabulary checks are `Warning` by
2900 // default so they stay `[lints]`-configurable and
2901 // `@[allow(…)]`-suppressible (only `Warning`-base codes are —
2902 // see `crate::suppressions`). A host that wants a declared
2903 // vocabulary to be binding raises them with
2904 // `[lints] E164 = "deny"`. Issue #1780/#1997 adds `E173`
2905 // (missing required attribute) to the same family, same
2906 // rationale.
2907 | Self::E164
2908 | Self::E165
2909 | Self::E173
2910 // Issue #1848: a duplicate claiming pattern is dead code (the
2911 // earlier-declared handler always wins first), not a hard
2912 // error — `Warning`-tier so it stays `[lints]`-configurable and
2913 // `@[allow(E168)]`-suppressible, same posture as E164/E165.
2914 | Self::E168
2915 // Non-identical patterns that can overlap: same rationale as E168.
2916 | Self::E170
2917 // Issue #1835: a project may legitimately want a literal
2918 // `@`-led runtime tag (the issue's own caution) — `Warning`
2919 // plus `@[allow(E172)]` is the escape valve, same posture as
2920 // E132's malformed-directive-tag report.
2921 | Self::E172
2922 // Issue #2156: `E031`'s sibling for a divert-with-args call
2923 // site — same severity precedent as the call-expression arity
2924 // check it extends.
2925 | Self::E176
2926 // Issue #1865: matches E035's "name shadows a built-in
2927 // function" precedent — a declared STRUCT colliding with a
2928 // reserved builtin/tower type name is legal (the declaration
2929 // still compiles and constructs normally), just worth
2930 // surfacing so the author doesn't lose the annotation spelling
2931 // by accident. `resolve`'s own doc already calls this ordering
2932 // "deliberate", the same posture E035's shadow warning takes.
2933 | Self::E188
2934 // E190 (external-rename host-binding breakage, ruled 2026-08-24)
2935 // is Warning-tier: it is the always-unsafe verdict entry behind
2936 // the rename Force gate, synthesized by the IDE, never emitted
2937 // by compilation.
2938 | Self::E190
2939 // E192 (#3259): a directive that suppresses nothing is a
2940 // warning, not an error — the file still compiles, and the harm
2941 // is a suppression the author thinks is in force but is not.
2942 | Self::E192
2943 // E193 (#3354, RULED 2026-09-01 option C): a temp read that its
2944 // declaration does not dominate is a warning, not an error —
2945 // the story still plays (the runtime reads ink's
2946 // missing-variable default and warns), and the ruling asks
2947 // specifically for a `[lints]`-overridable warning so a project
2948 // that leans on the pattern deliberately can turn it down.
2949 | Self::E193
2950 // E195 (#3365): a choice with no text and no divert compiles
2951 // and plays — inklecate's own C# parser only ever *warns* on
2952 // this shape too (`InkParser_Choices.cs`), never rejects it —
2953 // so `Warning`-tier, `[lints]`-overridable like the rest of
2954 // this family.
2955 | Self::E195 => Severity::Warning,
2956 // Issue #1674: the one code whose *default* is the `Info`
2957 // advisory tier rather than `Warning` — RULED "off or info by
2958 // default" (a single-shot project should not be nagged) while
2959 // staying tier-able through `[lints]` like every other code
2960 // (`brink_analyzer::strict::effective_severity` widens its
2961 // overridable set past `Warning`-base codes to cover this one,
2962 // issue #1674).
2963 // E189 (issue #3050): `TODO:` author notes are advisory by
2964 // definition — the same `Info`-default posture, tierable via
2965 // `[lints]` like every other code.
2966 Self::E157 | Self::E189 => Severity::Info,
2967 // E194 (#3373, RULED 2026-09-01) falls through to the `Error`
2968 // default below like every other hard error: inklecate rejects
2969 // the program, so brink does too until a project opts in. What
2970 // makes it different from every other `Error`-default code is
2971 // [`Self::is_overridable`], not `severity` — see that method
2972 // and [`Self::is_compat_deny`].
2973 _ => Severity::Error,
2974 }
2975 }
2976
2977 /// Whether this code is a member of the **compat-deny** tier (#3373,
2978 /// RULED 2026-09-01): "inklecate rejects this; brink can run it; you
2979 /// must opt in." `docs/compiler-spec.md` "Compat-deny diagnostics" owns
2980 /// the tier's admission invariant — a code may join only when brink
2981 /// produces a *working* program with the code downgraded, so every
2982 /// member needs its own fixture proving that.
2983 ///
2984 /// This is the one predicate [`Self::is_overridable`] widens past its
2985 /// old "not `Error`-by-default" rule for: every compat-deny code keeps
2986 /// `severity() == Error` (matching ink's own hard rejection) while
2987 /// still being `[lints]`-overridable, all the way to `allow` — the
2988 /// ruling's explicit ask ("we should allow it to be turned off if the
2989 /// user wants, it's annoying").
2990 #[must_use]
2991 pub fn is_compat_deny(self) -> bool {
2992 matches!(self, Self::E194)
2993 }
2994
2995 /// Whether this code can only ever arise on the NATIVE (`.brink`)
2996 /// surface (#3169).
2997 ///
2998 /// Which surface can produce a diagnostic is a property of the
2999 /// diagnostic, not of any consumer — an ink-only project cannot produce
3000 /// these no matter who is asking, so the answer belongs here rather
3001 /// than in whichever tool happens to want it.
3002 ///
3003 /// **Everything not listed defaults to "both surfaces", deliberately.**
3004 /// No analysis pass declares the surface it can fire on, so this is
3005 /// read from what each diagnostic MEANS — and the two ways of being
3006 /// wrong are not symmetric. Claiming native-only wrongly hides a
3007 /// setting from an author who is actually seeing the diagnostic;
3008 /// claiming both wrongly shows one that cannot fire. The second is
3009 /// clutter, the first is a dead end, so a code earns its place here
3010 /// only when the compiler itself says so, and everything uncertain
3011 /// stays visible.
3012 ///
3013 /// Deliberately a predicate rather than a `Surface` set: nothing is
3014 /// ink-only today, and would be a real surprise if it were — the ink
3015 /// surface is the compatibility floor and native is a superset of it.
3016 /// If an ink-only code ever appears, this wants to become a set rather
3017 /// than gain a second predicate.
3018 #[must_use]
3019 pub fn is_native_only(self) -> bool {
3020 matches!(
3021 self,
3022 // "native is the only frontend that can spell markup"
3023 // — brink-analyzer/src/markup_check.rs
3024 Self::E164 | Self::E165 | Self::E173
3025 // These say it in their own titles.
3026 | Self::E131 // "native: `<-` (splice) used outside a choice point…"
3027 | Self::E132 // "A native file-level `@[was(…)]` rename record…"
3028 | Self::E151 // "A native `{? … }` choice's own body falls through…"
3029 | Self::E172 // "A native (`.brink`) tag whose text begins with `@`…"
3030 )
3031 }
3032
3033 /// The written explanation for this code, or `None` when nobody has
3034 /// written one yet (#3169).
3035 ///
3036 /// The prose lives in `docs/diagnostics/Exxx.md` under `## Explanation`.
3037 /// Every code has a file; only 31 of 189 have that section filled in, so
3038 /// `None` is the common answer and callers must render something else —
3039 /// [`Self::title`] is the intended fallback. Returning `None` rather than
3040 /// an empty string is deliberate: a caller that forgets to check gets a
3041 /// type error instead of a blank panel.
3042 #[must_use]
3043 pub fn explanation(self) -> Option<&'static str> {
3044 super::diagnostic_explanations::EXPLANATIONS
3045 .iter()
3046 .find(|(code, _)| *code == self)
3047 .map(|(_, text)| *text)
3048 }
3049
3050 /// Whether `[lints]` can override this code's severity (#1160).
3051 ///
3052 /// Everything except a hard error: `brink_analyzer::validate_lint_code`
3053 /// accepts any code whose default severity is not `Error`, and refuses
3054 /// the rest with a `ConfigWarning` rather than applying them. You cannot
3055 /// `allow` something that stops the compile.
3056 ///
3057 /// That deliberately INCLUDES the advisory tiers — `E189`, the ink
3058 /// `TODO:` note, is `Info` by default and is exactly the sort of thing
3059 /// an author wants to turn off (ruled 2026-08-27). An earlier version of
3060 /// this predicate said `Warning` only, which silently hid every
3061 /// `Info`-default code from the settings surface; the analyzer would
3062 /// have accepted them all along.
3063 ///
3064 /// It also INCLUDES the **compat-deny** tier (#3373, RULED 2026-09-01):
3065 /// [`Self::is_compat_deny`] members keep `severity() == Error` — brink
3066 /// rejects the program by default, exactly as inklecate does — but stay
3067 /// `[lints]`-overridable specifically because the ruling's admission
3068 /// invariant requires each member to produce a *working* program once
3069 /// downgraded. This is the one deliberate exception to "a hard error
3070 /// can never be downgraded"; every other `Error`-default code stays
3071 /// non-overridable.
3072 ///
3073 /// `agrees_with_the_analyzers_own_gate` in `brink-analyzer` pins this
3074 /// against `apply_lint_overrides` itself rather than against a restated
3075 /// rule — the earlier mistake survived a test that compared this
3076 /// predicate to its own implementation.
3077 #[must_use]
3078 pub fn is_overridable(self) -> bool {
3079 !matches!(self.severity(), Severity::Error) || self.is_compat_deny()
3080 }
3081
3082 /// Parse a diagnostic code from its string representation (e.g., `"E027"`).
3083 #[must_use]
3084 #[expect(
3085 clippy::too_many_lines,
3086 reason = "a flat one-arm-per-code table that necessarily grows with the diagnostic set"
3087 )]
3088 pub fn from_str_code(s: &str) -> Option<Self> {
3089 match s {
3090 "E001" => Some(Self::E001),
3091 "E002" => Some(Self::E002),
3092 "E003" => Some(Self::E003),
3093 "E004" => Some(Self::E004),
3094 "E005" => Some(Self::E005),
3095 "E006" => Some(Self::E006),
3096 "E007" => Some(Self::E007),
3097 "E008" => Some(Self::E008),
3098 "E009" => Some(Self::E009),
3099 "E010" => Some(Self::E010),
3100 "E011" => Some(Self::E011),
3101 "E012" => Some(Self::E012),
3102 "E013" => Some(Self::E013),
3103 "E014" => Some(Self::E014),
3104 "E015" => Some(Self::E015),
3105 "E016" => Some(Self::E016),
3106 "E017" => Some(Self::E017),
3107 "E018" => Some(Self::E018),
3108 "E019" => Some(Self::E019),
3109 "E020" => Some(Self::E020),
3110 "E021" => Some(Self::E021),
3111 "E022" => Some(Self::E022),
3112 "E023" => Some(Self::E023),
3113 "E024" => Some(Self::E024),
3114 "E025" => Some(Self::E025),
3115 "E026" => Some(Self::E026),
3116 "E027" => Some(Self::E027),
3117 "E028" => Some(Self::E028),
3118 "E029" => Some(Self::E029),
3119 "E030" => Some(Self::E030),
3120 "E031" => Some(Self::E031),
3121 "E032" => Some(Self::E032),
3122 "E033" => Some(Self::E033),
3123 "E034" => Some(Self::E034),
3124 "E035" => Some(Self::E035),
3125 "E036" => Some(Self::E036),
3126 "E037" => Some(Self::E037),
3127 "E038" => Some(Self::E038),
3128 "E039" => Some(Self::E039),
3129 "E040" => Some(Self::E040),
3130 "E041" => Some(Self::E041),
3131 "E042" => Some(Self::E042),
3132 "E043" => Some(Self::E043),
3133 "E044" => Some(Self::E044),
3134 "E045" => Some(Self::E045),
3135 "E046" => Some(Self::E046),
3136 "E047" => Some(Self::E047),
3137 "E048" => Some(Self::E048),
3138 "E049" => Some(Self::E049),
3139 "E050" => Some(Self::E050),
3140 "E051" => Some(Self::E051),
3141 "E052" => Some(Self::E052),
3142 "E053" => Some(Self::E053),
3143 "E054" => Some(Self::E054),
3144 "E055" => Some(Self::E055),
3145 "E056" => Some(Self::E056),
3146 "E057" => Some(Self::E057),
3147 "E058" => Some(Self::E058),
3148 "E059" => Some(Self::E059),
3149 "E060" => Some(Self::E060),
3150 "E061" => Some(Self::E061),
3151 "E062" => Some(Self::E062),
3152 "E063" => Some(Self::E063),
3153 "E064" => Some(Self::E064),
3154 "E065" => Some(Self::E065),
3155 "E066" => Some(Self::E066),
3156 "E067" => Some(Self::E067),
3157 "E068" => Some(Self::E068),
3158 "E069" => Some(Self::E069),
3159 "E070" => Some(Self::E070),
3160 "E071" => Some(Self::E071),
3161 "E072" => Some(Self::E072),
3162 "E073" => Some(Self::E073),
3163 "E074" => Some(Self::E074),
3164 "E075" => Some(Self::E075),
3165 "E076" => Some(Self::E076),
3166 "E077" => Some(Self::E077),
3167 "E078" => Some(Self::E078),
3168 "E079" => Some(Self::E079),
3169 "E080" => Some(Self::E080),
3170 "E081" => Some(Self::E081),
3171 "E082" => Some(Self::E082),
3172 "E083" => Some(Self::E083),
3173 "E084" => Some(Self::E084),
3174 "E085" => Some(Self::E085),
3175 "E086" => Some(Self::E086),
3176 "E087" => Some(Self::E087),
3177 "E088" => Some(Self::E088),
3178 "E089" => Some(Self::E089),
3179 "E090" => Some(Self::E090),
3180 "E091" => Some(Self::E091),
3181 "E092" => Some(Self::E092),
3182 "E093" => Some(Self::E093),
3183 "E094" => Some(Self::E094),
3184 "E095" => Some(Self::E095),
3185 "E096" => Some(Self::E096),
3186 "E097" => Some(Self::E097),
3187 "E098" => Some(Self::E098),
3188 "E099" => Some(Self::E099),
3189 "E100" => Some(Self::E100),
3190 "E101" => Some(Self::E101),
3191 "E102" => Some(Self::E102),
3192 "E103" => Some(Self::E103),
3193 "E104" => Some(Self::E104),
3194 "E105" => Some(Self::E105),
3195 "E106" => Some(Self::E106),
3196 "E107" => Some(Self::E107),
3197 "E108" => Some(Self::E108),
3198 "E109" => Some(Self::E109),
3199 "E110" => Some(Self::E110),
3200 "E111" => Some(Self::E111),
3201 "E112" => Some(Self::E112),
3202 "E113" => Some(Self::E113),
3203 "E114" => Some(Self::E114),
3204 "E115" => Some(Self::E115),
3205 "E116" => Some(Self::E116),
3206 "E117" => Some(Self::E117),
3207 "E118" => Some(Self::E118),
3208 "E119" => Some(Self::E119),
3209 "E120" => Some(Self::E120),
3210 "E121" => Some(Self::E121),
3211 "E122" => Some(Self::E122),
3212 "E123" => Some(Self::E123),
3213 "E124" => Some(Self::E124),
3214 "E125" => Some(Self::E125),
3215 "E126" => Some(Self::E126),
3216 "E127" => Some(Self::E127),
3217 "E128" => Some(Self::E128),
3218 "E129" => Some(Self::E129),
3219 "E130" => Some(Self::E130),
3220 "E131" => Some(Self::E131),
3221 "E132" => Some(Self::E132),
3222 "E133" => Some(Self::E133),
3223 "E134" => Some(Self::E134),
3224 "E135" => Some(Self::E135),
3225 "E136" => Some(Self::E136),
3226 "E137" => Some(Self::E137),
3227 "E138" => Some(Self::E138),
3228 "E139" => Some(Self::E139),
3229 "E140" => Some(Self::E140),
3230 "E141" => Some(Self::E141),
3231 "E142" => Some(Self::E142),
3232 "E143" => Some(Self::E143),
3233 "E144" => Some(Self::E144),
3234 "E145" => Some(Self::E145),
3235 "E146" => Some(Self::E146),
3236 "E147" => Some(Self::E147),
3237 "E148" => Some(Self::E148),
3238 "E149" => Some(Self::E149),
3239 "E150" => Some(Self::E150),
3240 "E151" => Some(Self::E151),
3241 "E152" => Some(Self::E152),
3242 "E153" => Some(Self::E153),
3243 "E154" => Some(Self::E154),
3244 "E155" => Some(Self::E155),
3245 "E156" => Some(Self::E156),
3246 "E157" => Some(Self::E157),
3247 "E158" => Some(Self::E158),
3248 "E159" => Some(Self::E159),
3249 "E160" => Some(Self::E160),
3250 "E161" => Some(Self::E161),
3251 "E162" => Some(Self::E162),
3252 "E163" => Some(Self::E163),
3253 "E164" => Some(Self::E164),
3254 "E165" => Some(Self::E165),
3255 "E166" => Some(Self::E166),
3256 "E167" => Some(Self::E167),
3257 "E168" => Some(Self::E168),
3258 "E169" => Some(Self::E169),
3259 "E170" => Some(Self::E170),
3260 "E171" => Some(Self::E171),
3261 "E172" => Some(Self::E172),
3262 "E173" => Some(Self::E173),
3263 "E174" => Some(Self::E174),
3264 "E175" => Some(Self::E175),
3265 "E176" => Some(Self::E176),
3266 "E178" => Some(Self::E178),
3267 "E179" => Some(Self::E179),
3268 "E180" => Some(Self::E180),
3269 "E181" => Some(Self::E181),
3270 "E182" => Some(Self::E182),
3271 "E183" => Some(Self::E183),
3272 "E184" => Some(Self::E184),
3273 "E185" => Some(Self::E185),
3274 "E186" => Some(Self::E186),
3275 "E187" => Some(Self::E187),
3276 "E188" => Some(Self::E188),
3277 "E189" => Some(Self::E189),
3278 "E190" => Some(Self::E190),
3279 "E191" => Some(Self::E191),
3280 "E192" => Some(Self::E192),
3281 "E193" => Some(Self::E193),
3282 "E194" => Some(Self::E194),
3283 "E195" => Some(Self::E195),
3284 _ => None,
3285 }
3286 }
3287}
3288
3289// ── Issue #3169: the registry the settings UI reads ────────────────
3290
3291#[cfg(test)]
3292mod registry_tests {
3293 use super::{DiagnosticCode, Severity};
3294
3295 /// Read the `## Explanation` section out of a doc file, the same way the
3296 /// generator does.
3297 fn doc_explanation(text: &str) -> String {
3298 let Some((_, rest)) = text.split_once("## Explanation\n") else {
3299 return String::new();
3300 };
3301 rest.split("\n## ").next().unwrap_or("").trim().to_owned()
3302 }
3303
3304 fn docs_dir() -> std::path::PathBuf {
3305 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../docs/diagnostics")
3306 }
3307
3308 #[test]
3309 fn explanations_match_the_docs() {
3310 // The table is embedded (the docs live outside this crate's package
3311 // directory, and the wasm build has no filesystem), so nothing keeps
3312 // it in step with the markdown except this test. It runs in the
3313 // workspace, where `docs/` exists — which is also the only place the
3314 // drift can happen.
3315 let root = docs_dir();
3316 assert!(
3317 root.is_dir(),
3318 "expected diagnostics docs at {}",
3319 root.display()
3320 );
3321
3322 let mut wrong = Vec::new();
3323 for code in DiagnosticCode::ALL {
3324 let text = std::fs::read_to_string(root.join(format!("{}.md", code.as_str())))
3325 .unwrap_or_default();
3326 if doc_explanation(&text) != code.explanation().unwrap_or("") {
3327 wrong.push(code.as_str());
3328 }
3329 }
3330 assert!(
3331 wrong.is_empty(),
3332 "diagnostic_explanations.rs is out of step with docs/diagnostics for {wrong:?} \
3333 — regenerate it so the settings UI does not show stale prose"
3334 );
3335 }
3336
3337 #[test]
3338 fn every_code_has_an_explanation_file() {
3339 let root = docs_dir();
3340 let missing: Vec<_> = DiagnosticCode::ALL
3341 .iter()
3342 .filter(|c| !root.join(format!("{}.md", c.as_str())).is_file())
3343 .map(|c| c.as_str())
3344 .collect();
3345 assert!(
3346 missing.is_empty(),
3347 "codes with no explanation file: {missing:?}"
3348 );
3349 }
3350
3351 #[test]
3352 fn no_explanation_is_a_leftover_placeholder() {
3353 // The generated stubs carried bracketed placeholder prose. Embedding
3354 // one would put "[Detailed explanation of this diagnostic...]" in
3355 // front of an author, which is worse than showing nothing at all.
3356 for code in DiagnosticCode::ALL {
3357 if let Some(text) = code.explanation() {
3358 assert!(
3359 !text.contains("[Detailed explanation"),
3360 "{} still carries placeholder text",
3361 code.as_str()
3362 );
3363 assert!(
3364 !text.is_empty(),
3365 "{} has an empty explanation",
3366 code.as_str()
3367 );
3368 }
3369 }
3370 }
3371
3372 #[test]
3373 fn native_only_codes_say_so_in_their_own_text() {
3374 // The list is judgement, so it is held to its own standard: a code
3375 // is native-only only when the compiler itself says so. Every entry
3376 // must be justified by its title or its explanation naming the
3377 // native surface — if one is not, either the claim is wrong or the
3378 // title needs to state what the code is for.
3379 //
3380 // Markup is the documented exception: `markup_check.rs` says
3381 // "native is the only frontend that can spell markup", which is not
3382 // repeated in each code's own title.
3383 const MARKUP: &[DiagnosticCode] = &[
3384 DiagnosticCode::E164,
3385 DiagnosticCode::E165,
3386 DiagnosticCode::E173,
3387 ];
3388 for code in DiagnosticCode::ALL.iter().filter(|c| c.is_native_only()) {
3389 if MARKUP.contains(code) {
3390 continue;
3391 }
3392 let title = code.title().to_lowercase();
3393 let explanation = code.explanation().unwrap_or("").to_lowercase();
3394 assert!(
3395 title.contains("native")
3396 || title.contains(".brink")
3397 || explanation.contains("native")
3398 || explanation.contains(".brink"),
3399 "{} is marked native-only but nothing in its own text says so",
3400 code.as_str()
3401 );
3402 }
3403 }
3404
3405 #[test]
3406 fn native_only_is_the_exception() {
3407 // The default is "both surfaces", and that is load-bearing: hiding
3408 // a code an author is actually seeing is worse than showing one
3409 // that cannot fire. If most codes became native-only, the default
3410 // has stopped being a default and this design wants revisiting
3411 // rather than the list growing.
3412 let native_only = DiagnosticCode::ALL
3413 .iter()
3414 .filter(|c| c.is_native_only())
3415 .count();
3416 assert!(
3417 native_only * 4 < DiagnosticCode::ALL.len(),
3418 "native-only is no longer an exception: {native_only} of {}",
3419 DiagnosticCode::ALL.len()
3420 );
3421 }
3422
3423 #[test]
3424 fn a_hard_error_is_never_overridable_and_an_advisory_always_is() {
3425 // Stated as the RULE, not as a copy of the implementation. The
3426 // version of this test that said `== matches!(severity, Warning)`
3427 // could not catch the predicate being wrong, because it asserted
3428 // the predicate against itself — and it did not catch it: every
3429 // `Info`-default code (`E189`, the ink TODO note) was hidden from
3430 // the settings surface for exactly that reason.
3431 for code in DiagnosticCode::ALL {
3432 match code.severity() {
3433 // #3373's compat-deny tier is the one deliberate exception:
3434 // `Error`-default AND overridable, exactly the members
3435 // `is_compat_deny` names. Every other `Error`-default code
3436 // must stay non-overridable.
3437 Severity::Error => assert_eq!(
3438 code.is_overridable(),
3439 code.is_compat_deny(),
3440 "{}: an Error-default code is overridable only when it is a \
3441 compat-deny tier member",
3442 code.as_str()
3443 ),
3444 Severity::Warning | Severity::Info | Severity::Hint => {
3445 assert!(code.is_overridable(), "{}", code.as_str());
3446 }
3447 }
3448 }
3449 assert!(
3450 DiagnosticCode::E189.is_overridable(),
3451 "the ink TODO: note must be configurable (ruled 2026-08-27)"
3452 );
3453 assert!(
3454 DiagnosticCode::E194.is_overridable() && DiagnosticCode::E194.is_compat_deny(),
3455 "the compat-deny tier's first member must be Error-default yet overridable \
3456 (ruled 2026-09-01, #3373)"
3457 );
3458 let overridable = DiagnosticCode::ALL
3459 .iter()
3460 .filter(|c| c.is_overridable())
3461 .count();
3462 assert!(
3463 overridable > 0 && overridable < DiagnosticCode::ALL.len(),
3464 "a flag that is true (or false) for every code would be pointless: \
3465 {overridable} of {}",
3466 DiagnosticCode::ALL.len()
3467 );
3468 }
3469
3470 #[test]
3471 fn compat_deny_tier_is_error_default_and_overridable() {
3472 // The tier's own invariant, stated directly against every current
3473 // member rather than folded into the general hard-error test above
3474 // — so a future member that gets `is_compat_deny` right but
3475 // `severity` wrong (or vice versa) fails here with a name attached.
3476 for code in DiagnosticCode::ALL.iter().filter(|c| c.is_compat_deny()) {
3477 assert_eq!(
3478 code.severity(),
3479 Severity::Error,
3480 "{}: a compat-deny member's default must match inklecate's rejection",
3481 code.as_str()
3482 );
3483 assert!(
3484 code.is_overridable(),
3485 "{}: a compat-deny member must be [lints]-overridable",
3486 code.as_str()
3487 );
3488 }
3489 }
3490}