Skip to main content

DiagnosticCode

Enum DiagnosticCode 

Source
pub enum DiagnosticCode {
Show 194 variants E001, E002, E003, E004, E005, E006, E007, E008, E009, E010, E011, E012, E013, E014, E015, E016, E017, E018, E019, E020, E021, E022, E023, E024, E025, E026, E027, E028, E029, E030, E031, E032, E033, E034, E035, E036, E037, E038, E039, E040, E041, E042, E043, E044, E045, E046, E047, E048, E049, E050, E051, E052, E053, E054, E055, E056, E057, E058, E059, E060, E061, E062, E063, E064, E065, E066, E067, E068, E069, E070, E071, E072, E073, E074, E075, E076, E077, E078, E079, E080, E081, E082, E083, E084, E085, E086, E087, E088, E089, E090, E091, E092, E093, E094, E095, E096, E097, E098, E099, E100, E101, E102, E103, E104, E105, E106, E107, E108, E109, E110, E111, E112, E113, E114, E115, E116, E117, E118, E119, E120, E121, E122, E123, E124, E125, E126, E127, E128, E129, E130, E131, E132, E133, E134, E135, E136, E137, E138, E139, E140, E141, E142, E143, E144, E145, E146, E147, E148, E149, E150, E151, E152, E153, E154, E155, E156, E157, E158, E159, E160, E161, E162, E163, E164, E165, E166, E167, E168, E169, E170, E171, E172, E173, E174, E175, E176, E178, E179, E180, E181, E182, E183, E184, E185, E186, E187, E188, E190, E189, E191, E192, E193, E194, E195,
}
Expand description

Stable error codes for brink diagnostics.

Codes are never reused once assigned. Each code has a corresponding explanation file at docs/diagnostics/Exxx.md.

Variants§

§

E001

Knot definition is missing a name.

§

E002

Stitch definition is missing a name.

§

E003

Knot or stitch parameter is missing a name.

§

E004

VAR declaration is missing a name.

§

E005

VAR declaration is missing an initializer.

§

E006

CONST declaration is missing a name.

§

E007

CONST declaration is missing an initializer.

§

E008

LIST declaration is missing a name.

§

E009

LIST member is missing a name.

§

E010

EXTERNAL declaration is missing a name.

§

E011

RETIRED (lane-A audit, #709) — the parser always materializes a FILE_PATH node inside INCLUDE_STMT (possibly empty) and reports missing path as E037 (parser/declaration.rs::include_statement). Code kept reserved, not reused.

§

E012

Divert is missing a target.

§

E013

RETIRED (lane-A audit, #709) — parser/divert.rs::path always creates a PATH node (empty on error + E037), so ThreadStart::target() is never None. Code kept reserved, not reused.

§

E014

Logic line has no effect (bare ~).

§

E015

Expression is missing an operand.

§

E016

Unknown or unsupported operator.

§

E017

Function call is missing a name.

§

E018

RETIRED (lane-A audit, #709) — parser/divert.rs::path always creates a PATH node (empty on error + E037), so DivertTargetExpr::target() is never None. Code kept reserved, not reused.

§

E019

RETIRED (lane-A audit, #709) — the parser only builds a CHOICE node after seeing a bullet token, so a bullet-less choice CST cannot exist. Code kept reserved, not reused.

§

E020

Inline conditional is missing a condition.

§

E021

Inline sequence has no branches.

§

E022

Duplicate knot definition.

§

E023

Duplicate variable/constant definition.

§

E024

Unresolved divert target.

§

E025

Unresolved variable reference.

§

E026

Duplicate list item.

§

E027

Ambiguous bare list item reference.

§

E028

RETIRED (lane-A audit, #709) — circular INCLUDE is detected at the discovery phase and surfaces as CompileError::CircularInclude, not as a per-construct diagnostic. Code kept reserved, not reused.

§

E029

Choice nested in conditional without explicit divert.

§

E030

String interpolation in constant initializer is ignored.

§

E031

Function call argument count mismatch.

§

E032

Return statement outside function.

§

E033

Unreachable code after divert.

§

E034

Choice set has only fallback choices.

§

E035

Name shadows a built-in function.

§

E036

Expected diagnostic not produced (// brink-expect).

§

E037

Syntax error reported by the parser (malformed source).

§

E038

Malformed /// doc-comment tag on a declaration.

§

E039

Registered host manifest disagrees with the ink EXTERNAL arity.

§

E040

Doc-comment / manifest references an unknown semantic type.

§

E041

External call argument type mismatches the manifest signature.

§

E042

External call argument violates a closed-domain constraint.

§

E043

Well-formed /// doc-comment tag that doesn’t apply to this declaration kind (e.g. @kind on a knot, @param on a VAR).

§

E044

Unknown directive name (e.g. #@locale).

§

E045

Directive has no valid target in this position.

§

E046

Directive contains dynamic inline logic — directives are static text.

§

E047

Directive must be the only tag on its line.

§

E048

Duplicate directive on one target.

§

E049

Directive not supported on this target (e.g. @local on CONST).

§

E050

Directive does not take arguments or trailing text.

§

E051

A brink-extension construct (block, sigil literal, indexing) was used under the strict-ink dialect.

§

E052

A brink-extension construct parses and analyzes cleanly under the brink dialect, but its LIR lowering hasn’t landed yet. Originally minted for T1b-1 (every T1b construct lowers since T1b-2, #570), then revived by T1c-1 (#699) as the #fn(…) lowering fence, retired again by T1c-2 (#700). Now the await fence (FS-2, docs/flow-suspension-spec.md §3, issue #928): await <cond> / while await <cond> parse to HIR and pass the effect-free purity gate (E105), but their runtime spill/restore semantics are FS-3 — every await construct is fenced here at LIR lowering until that lands. The code stays a general “parses/analyzes before its lowering lands” fence, reused as each new extension needs it.

§

E053

RETIRED (T1b-2, #570) — previously a non-suppressible backstop rejecting T1b brink-extension HIR nodes (LogicBlock, ArrayLiteral, MapLiteral, Index) at LIR lowering. T1b-2 completed real lowering for all such constructs, making the backstop obsolete. Code kept reserved, not reused, for diagnostic-code stability.

§

E054

A block-scoped temp (~ { … }, docs/t1b-surface-spec.md §2) or for loop variable shadows an already-visible temp/param — either an enclosing ~ { … } block scope or an outer classic ~ temp.

§

E055

push/insert/remove’s first argument is not an lvalue (a variable, temp, or indexed path) — mutators require a place to write the mutated container back into.

§

E056

push/insert/remove was used in expression position — they return nothing and are only valid as a statement.

§

E057

break/continue used outside any enclosing while/for loop.

§

E058

Collection mutator (push/insert/remove) called with the wrong number of arguments — a targeted compile error naming the expected signature (replaces the generic E031 warning + silently-dropped RMW lowering, RULED 2026-07-12, see docs/decision-log.md).

§

E059

A choice set, labeled gather block, multi-line conditional, or sequence was found nested inside inline content (e.g. a choice’s own display/bracket/inner text) where it would need a child container that position structurally cannot hold.

§

E060

brink-codegen-inkb refused to emit bytecode for a Program that violates an invariant an earlier, non-suppressible compiler stage is supposed to guarantee (currently: an out-of-loop LogicBreak/ LogicContinue, normally rejected at E057). Reaching this from a normal compile is a compiler bug, not an authoring mistake — this code exists so that bug fails loudly instead of silently corrupting bytecode.

§

E061

A type annotation names something that isn’t a recognized nominal type (int/float/bool/string/divert/void), a List<L> naming a declared LIST, Array<T>, or Map<K, V> — declared struct names arrive in TM-4.

§

E062

RETIRED (T1c-1, #699): previously “fn(T…): R function-type annotation used — parses, but types as reserved until T1c”. T1c unfroze the form (docs/t1c-spec.md §4: “boundary annotations gain the fn(T…): R form”), so it now resolves to a real checker type. Code kept reserved, not reused, for diagnostic-code stability — no longer emitted by any pass.

§

E063

A param/return/VAR type annotation disagrees with the type TM-1’s body inference would otherwise derive. Advisory only in this slice (gradual policy) — strict-mode severity is TM-3’s call.

§

E064

types = strict was requested but the project’s dialect isn’t brink — strict typing is a brink-dialect extension (its annotation syntax is extension syntax), so types = strict + dialect = strict-ink is a config error, not a per-construct diagnostic.

§

E065

Under types = strict, a def’s inferred signature or body slot (param, return, or temp) resolved to Unknown after the SCC fixpoint with no annotation to supply a concrete type — “annotate or restructure” (spec §1). Legal under types = gradual.

§

E066

Under types = strict, a def’s inferred signature or body slot resolved to Ty::Conflicted (#627) — the body’s own uses disagree on the slot’s type. Legal (advisory-only, unreported) under types = gradual.

§

E067

Under types = strict, a ~ x = f() / ~ temp x = f() assigns the result of a call whose resolved def is a void-returning function (docs/typed-mode-spec.md §3: “assigning a void call is an error in strict mode”). Only the assignment/temp-decl’s RHS root call is checked — a statement-position call (~ f()) or a call nested inside interpolation is never flagged. Never emitted under types = gradual.

§

E068

A struct construction literal’s leading shape name (Name#{…}) doesn’t name any declared STRUCT.

§

E069

Under types = strict, a struct construction literal omits a declared field — names the missing field.

§

E070

A struct construction literal supplies a field the shape doesn’t declare — names the extra field.

§

E071

Under types = strict, a struct construction literal’s field initializer disagrees with the field’s declared type — names the field.

§

E072

RETIRED (TM-4c, #666): previously a non-suppressible backstop rejecting every struct construct/field access reaching LIR lowering, back when codegen for structs didn’t exist yet. Structs now lower for real (E073 is TM-4c’s narrower replacement backstop). Code kept reserved, not reused, for diagnostic-code stability — no longer emitted by any pass.

§

E073

Non-suppressible defense-in-depth backstop, mirroring E053/E060/ (former) E072: a struct construction literal referencing a shape name that doesn’t resolve to any declared STRUCT reached LIR lowering. Reaching this from a normal compile means brink-analyzer’s resolve::resolve_struct_ref diagnostic (E068) was suppressed (// brink-disable-all), not a compiler bug on its own — RecordNew needs a real ShapeId at compile time; there is no dynamic “construct with unknown shape” concept in this design.

§

E074

A field-write target (p.field = expr) is a chained projection — p.a.b = v or a mixed p.a[i].b = v — never a bare ident.field on a resolvable root. TM-4c ships single-level field writes only (mirrors lower_indexed_assignment’s n == 1 fast path); chained writes are an explicit, permanent T1e boundary (docs/ typed-mode-spec.md §6), not a “not implemented yet” gap — this is a real, reachable, non-suppressible diagnostic authors can hit by writing ordinary (if currently unsupported) ink, not a defensive backstop for a suppressed analysis diagnostic.

Also covers a write ending in an index rather than a field, whose index chain’s root is itself a struct-field projection or a mixed index/field access — p.field[i] = v, p.a[i].b[j] = v, or the mutator spelling (push(p.field[i], v)) — same T1e boundary, reached via reject_field_projection_index_root (issue #2121) from lower_indexed_assignment/lower_lvalue_container_chain rather than try_lower_field_assignment.

§

E075

A struct construction literal used as a VAR/CONST declaration default doesn’t match its declared shape: it omits a declared field, or supplies one the shape doesn’t declare.

A well-formed construction literal is a legal declaration default (issue #1530): eval_const_struct_literal folds it into lir::ConstValue::Record, which is what makes a struct-typed durable global — and therefore the T1e projection-receiver path (docs/t1e-spec.md §2, which requires a durable root) — spellable at all. Before #1530 this code was the blanket refusal of every struct literal in that position, because ConstValue had no record-carrying variant.

Mid-story p = Point#{…} construction with a mismatched shape is a runtime construction fault (RecordNew against an invalid shape id, value-model-spec §11c’s gradual path); a declaration default is baked into StoryData with no runtime construction step to fault at, so this is the compile-time equivalent — a real, non-suppressible error, never a half-built record. Under types = strict brink-analyzer’s structs::check reports the more precise Self::E069/ Self::E070 for the same literal; this backstop is policy-independent.

§

E076

A map literal used as a VAR/CONST declaration default has a key that isn’t a compile-time-constant scalar in the ratified map-key domain (int/string/bool — value-model-spec §4). Mid-story map construction (MapNew) faults on this at runtime (InvalidMapKeyType); a declaration default has no runtime construction step to fault at, so this is the compile-time equivalent — a real error, never a silent Null.

§

E077

An array element, map value, struct field, or #fn bound val arg nested inside a VAR/CONST declaration default has a source expression kind that can never constant-fold — a function call, postfix indexing, field access, ++/--, or (#743) a bare reference to another VAR. A declaration default is baked into StoryData at compile time, so there is no runtime construction step left to evaluate the element at; without this diagnostic the element recursed into eval_const_expr’s Path (SymbolKind::Variable) arm or catch-all and silently became Null — #673’s silent-Null bug one level down, inside the literal (#679 review; the Path-to-Variable case one level in was left deliberately unchanged there and closed by #743). Keyed off the source expression kind, never the folded result: an Expr::Null produced by HIR error recovery must not double-report, and a Path resolving to a CONST/list item/knot/stitch/function still folds for real and is not flagged — only a resolved SymbolKind::Variable (or an unresolved path, left to the analyzer’s own diagnostic) is exempt from the fold-for-real behavior, matching is_const_foldable_decl_default’s top-level twin (E083). (Since #1530 a struct literal at this position folds for real, so a never-foldable field of a nested construction literal reaches this arm exactly as an array element or map value does; before #1530 the whole literal was unconditionally E075 regardless of field content.)

§

E078

Under types = strict, an unresolved (builtin, not author-shadowed) call to int(x)/float(x) where x is statically a divert-target, LIST, array, map, or struct construction literal — outside the permissive numeric+bool domain (ruling 2: “compile error under types = strict, runtime fault under gradual”). string(x) accepts every type and is never checked here.

§

E079

#fn(name, …)’s target does not resolve to a statically-named function definition (=== function name ===) — it resolved to a variable/list/external/label/non-function knot or stitch, or it names a builtin/stdlib intrinsic (which has no definition to take a token of). Only fires under dialect = brink — under strict-ink the whole literal is already rejected as extension syntax (E051), and content diagnostics on rejected syntax are noise (the TM-2 suppression precedent, maintainer ruling 2026-07-13).

§

E080

A ref param of a #fn target is not bound in the creation-site prefix, or is bound to a non-durable lvalue. All ref params must be bound at creation, and each must capture a durable cell — a global VAR (flow-local #@local VARs included); a temp/param is a compile error (temps die with the frame, value-model §11), a CONST is not a mutable cell, and a bare (unmarked) rvalue/field reference is not a cell at all.

T1e (docs/t1e-spec.md §2/§6, issue #831) extends this same code — “reuse the E080-family message shape” — to the explicit ref lvalue-path projection form (heal(ref npc.hp, 5), #fn(heal, ref party[leader].hp), bind(f, ref inventory[idx])): the root of the path (the innermost variable the segments walk from) must still be a durable global VAR, by the same rule — temp/param roots remain a compile error, a CONST root is not a mutable cell. A projection’s own segments (dotted fields, […] indices) are a separate check (E098, strict-mode statically-known shapes only) — this code is the root-durability obligation alone.

§

E081

#fn(name, args…) binds more arguments than the target declares — the bound-arg row is a prefix of the declared param row (docs/t1c-spec.md §2: “binding more args than the target declares is a compile error”).

§

E082

A T1b block-scoped temp (~ { … }) — or a for-loop variable, which desugars the same way — was referenced (by value or by ref argument) after its own ~ { … }/while/for/if block already closed. Root-caused for #680: LIR lowering’s fallback for “temp not currently visible” (used for inklecate-compat forward-reference emulation of classic temps) previously also caught this case, silently emitting a phantom hashed GetGlobal/RefGlobal id that was never registered as a real global — a runtime-only UnresolvedGlobal fault with no compile diagnostic.

§

E083

A scalar VAR/CONST declaration default whose source expression kind can never be a compile-time constant — a bare reference to another VAR (VAR x = someOtherVar) or a function call (VAR x = f()), including either wrapped in a prefix/infix operation. eval_const_expr’s Path arm (SymbolKind::Variable) and its catch-all previously folded both silently to Null with no diagnostic — the same silent-fold bug #673/#679 fixed one level down, inside array/map/struct literals, left unfixed at this top level. Keyed off the source expression kind, never the folded result, same as E077. Does not fire for a Path nested inside a collection/struct/fn literal (array element, map value, struct field, #fn argument) — those recurse through their own existing E075/E076/E077 per-element checks one level in, which deliberately still leave a VAR-reference gap unchanged (#679 scope notes) pending its own follow-up.

§

E084

A struct construction literal (Name#{…}) supplies the same field name more than once. Previously a silent last-wins: only the final initializer’s value was placed, and — because the well-formed RecordNew lowering path discarded every non-placed lowered expression tree wholesale — an earlier duplicate’s initializer (including any observable side effect, e.g. a function call) never actually ran at all, with no diagnostic (#675’s RCA). Now a real compile error naming the repeated field, under both types = gradual and types = strict — unlike E069/E070/ E071 (which need a resolved shape to check missing/extra/mistyped fields against, and are strict-mode-only), a duplicate field is a structural authoring mistake detectable from the literal alone, independent of type-checking policy or whether the shape name even resolves.

§

E085

An undeclared file whose module (its file stem) collides with a declared module’s name (#@module(name) elsewhere). Accidental membership with mixed visibility defaults is the one footgun the module model forbids (modules-spec §1). Fix: declare the file with the same #@module(name), or rename it.

§

E086

A malformed #@module(…) directive: a missing or empty name argument, or a second #@module in the same file. #@module takes exactly one non-empty module name and appears at most once per file (modules-spec §1).

§

E087

A reference resolves to a #@private definition in another module. Private names are module-internal; the referrer is outside that module. Fix: make the definition #@public and IMPORT it, or move the reference into the module (modules-spec §4/§7).

§

E088

A bare-form IMPORT { name } FROM mod / native use mod::name; whose trailing segment name names neither a definition mod publicly exports nor a declared submodule of mod (dual-reading, issue #1592 — a trailing segment that resolves to a module licenses it instead, matching Rust’s use; §13.2). Only enforced against declared modules — an import naming an unknown/undeclared module is not itself flagged by this code, since that module’s export/submodule set isn’t visible to the check (modules-spec §2/§7).

§

E089

An IMPORT brings the same local name into scope twice (a repeated bare import, or two imports whose names/aliases collide) — the reference would be ambiguous (modules-spec §2/§7).

§

E090

An IMPORT names the importing file’s own module — a module cannot import itself; its own names are already bare (modules-spec §2/§7).

§

E091

A qualified access a.b is ambiguous: a is both a module imported in this file and a visible definition. Fix with an AS alias — no silent precedence (modules-spec §2/§7).

§

E092

A #@public/#@private override that restates the module’s default (e.g. #@public in an undeclared module, #@private in a declared one) — redundant, no effect (warning, modules-spec §4/§7).

§

E093

Conflicting or repeated visibility directives on one declaration (both #@private and #@public, or the same one twice). A declaration takes at most one visibility directive (modules-spec §4).

§

E094

A malformed #@was(…) directive: a missing or empty old-name argument (#@was, #@was()). #@was takes exactly one non-empty name (modules-spec §5).

§

E095

#@was(name) names the thing’s own current name — a self-alias that would be a no-op entry in the compiled alias table. Nothing to migrate; likely a stale directive left over from a previous rename (warning, modules-spec §5/§7).

§

E096

Two declared modules (#@module(name), different names) each define a same-name, same-kind symbol. Escalated from the E022/E023/E026 inklecate-compat duplicate warning to a hard error under dialect = brink only: flat resolution (unchanged by this stopgap — true import-scoped resolution is #790’s job) binds a bare name to whichever declared-module definition merge happens to see first, so two declared modules sharing a name make that binding silently order-dependent for one of them. A duplicate within one module (same declared module name across its files, or any undeclared/legacy file) keeps the existing warning — this code fires only when both colliding definitions’ owning files declared different modules. Reported once per colliding definition (both spans), under strict-ink this code never fires (compat corpus untouched).

§

E097

A ref lvalue-path projection expression (ref npc.hp, ref inventory[idx]) appears somewhere other than ref-argument position (a direct argument of a call, #fn(…), or bind(…)) — a standalone projection value (temp r = ref a[0]), one nested inside another expression, or any other position. Deliberate v1 posture (t1e-spec §2: “projections exist only where ref already exists: argument binding”); first-class standalone projection values are a future round, tracked as icebox #825 — not a permanent rejection.

§

E098

A ref lvalue-path projection’s segment (a dotted field, or a […] index) disagrees with the root’s statically-known shape, under types = strict only — a dotted field the declared STRUCT shape doesn’t have, or a […] index against a declared shape that isn’t a collection (mirrors structs::check’s missing/extra-field machinery, E069E071, applied to path segments instead of construction-literal fields; “Unknown never disagrees” for any segment whose base type isn’t statically known this way — silently unchecked, same spirit as E071).

§

E099

A ref lvalue-path projection with at least one path segment (dotted field or […] index — a real projection, not a bare single-name ref) reached LIR lowering. T1e-1 (docs/t1e-spec.md §8 sequencing item 1) ships grammar + HIR + analyzer only — the MakeProjection/ProjRead/ProjWrite opcodes a projection needs to actually run land in T1e-2 (tracking #828). The E052-fence pattern: every other check (E080 durable root, E097 position, E098 strict segment shape) already ran and passed, so this is a clean, deliberate “not yet lowerable” stop, not a silent drop or a miscompile — see brink-ir::lir::lower::mod’s backstop doctrine. A bare single-name ref x (zero segments) never hits this — it lowers exactly like today’s unmarked ref-argument binding.

§

E100

#@effects with no argument at all (#@effects, #@effects(), or an argument that parses to nothing) — the directive always requires either pure or at least one reads:/writes:/calls: clause.

§

E101

A malformed #@effects(…) argument: an unrecognized clause keyword (only reads/writes/calls are valid), a value that isn’t a bare identifier, or a bare value with no preceding clause to attach to.

§

E102

A #@effects(…) clause names an identifier that isn’t a declared global VAR/CONST (for reads/writes) or a declared EXTERNAL (for calls) anywhere in the project.

§

E103

The exceedance error (docs/effects-spec.md §10, sitting 2, 2026-07-14 ruling): the definition’s inferred effect row is not covered by () its #@effects(…) assertion’s declared upper bound. Per the ruling, this is the only diagnostic the assertion surface ever produces — an inferred row that is narrower than the bound is silent; there is no drift policy.

§

E104

A call expr(args…) whose callee isn’t a bare variable/temp/param name (an INDEX_EXPR, FIELD_ACCESS_EXPR, chained call result, parenthesized expr, …). Direct-call syntax is RULED (t1c-spec §3) to a bare-name callee only; “method-call syntax” through a computed callee is explicitly out of T1c (§10). Always rejected — every dialect, every mode — pointing at the ratified call(f, args…) form, which already dispatches through exactly this class of expression correctly. Replaces the pre-existing silent drop (the parser used to leave (args…) unconsumed, so it resurfaced as trailing prose text on the content line and the call itself vanished) with a loud, unconditional compile error.

§

E105

An await <cond> / while await <cond> condition is not effect-free. The condition is captured as a compiler-synthesized pure function (docs/flow-suspension-spec.md §5): its effect row must be read-only — reads are the wake map’s dependency set, but a transitive write to a global cell, or an effectful host call, makes the condition re-evaluation itself observable, which the wake contract forbids. Built on the effects machinery (#859): the condition’s transitive effect row (via the whole-project crate-level effect table) must have empty writes/calls and not be opaque. Brink-only (under strict-ink the whole await is already E051); a bare fn-value reference used as a dynamic condition (await some_fn_value, no call syntax) is read-only by construction and never flagged.

§

E106

A #{key: expr, …} map-literal key is a statically-classifiable literal outside the ratified int/string/bool key domain — a float, array (#[...]), nested map (#{...}), struct (Name#{...}), function-value (#fn(...)), ink LIST, or divert-target literal used directly as a key. §3 rules the key domain to int/string/bool at runtime (RuntimeError::InvalidMapKeyType) and says the analyzer warns on statically-visible non-key types; this was the missing half (MapLiteral lowering did zero key-domain checking). A dynamic key (a variable, call, index, or any other non-literal expression) is not statically visible and is never flagged here — the runtime fault remains the sole backstop for those.

§

E107

A fresh, un-annotated declaration (VAR x = none, CONST x = none, ~ temp x = none) whose initializer is the bare none Option literal. §1.4’s ruled rule: “a bare none needs a type from context (concrete sites fine; a fresh un-annotated var x = none errors — the empty-collection posture).” A declaration site IS the slot’s type origin, so there is no context to take the element type from — the fix is to initialize from a real Option-producing expression (some(x), or an Option-returning verb like find/get/pop). Error in both dialects and both types policies: the rule is part of the Option package itself, not a strict-mode refinement.

§

E108

@[effects(silent)] exceedance: the definition’s inferred row can produce content (emits, incl. transitively through callees, or an opaque/unbounded row). Exceedance-only, like E103 — asserting less than reality is legal, asserting more is not.

§

E109

@[effects(total)] exceedance: the definition’s inferred row can raise a turn-terminating fault (faults, incl. transitively, or an opaque/unbounded row). Exceedance-only, like E103.

§

E110

The deprecated #@effects(…) tag-channel spelling — superseded by the @[effects(…)] annotation final form (stdlib-spec §9.2, ruled 2026-07-18). Warning: the alias keeps parsing (it shipped in released surface, @brink-lang/web@0.11.1).

§

E111

An @[…] annotation line naming something outside the channel’s closed name set: effects on the ink surface, effects or the file-level was on the native .brink surface. Tag-channel directive names do not alias into it.

§

E112

An @[…] annotation line outside a recognized placement — ink’s leading run at the top of a knot/stitch body, or native’s Rust-shaped position directly above a flow/fn declaration (issue #1563; the file-level @[was] record for native modules). Never a silent drop, never content — the E045 posture, on the annotation channel.

§

E113

A declaration named after a registry protocol method — display, compare, or next (F6, ruled 2026-07-19): the names are RESERVED under the brink dialect, and an author declaration of any callable or value-bindable kind (knot/stitch/function, param, temp, VAR, CONST, EXTERNAL, for-loop variable) is a hard error, not an E035-lineage shadowing warning — a shadowed display would make interpolation untrustworthy.

§

E114

A registered protocol impl’s inferred effect row exceeds its protocol’s effect contract (display/compare: pure·silent·total; iterate’s next: writes-receiver·silent·total — the receiver is a ref param, invisible to the global row, so every v1 contract bounds the global row at empty). Exceedance-only, the E103/E108/E109 posture; an opaque row exceeds every contract.

§

E115

An ill-formed protocol impl registration: the named type isn’t a declared STRUCT, the impl target isn’t a declared function, the signature shape is wrong (arity, ref-ness, or a contradicting type annotation), or the (protocol, type) pair is already registered.

§

E116

A condition-position expression (an if/while condition, a {cond: …} conditional branch, a choice guard, an await condition) whose statically-known type is Option[T]. Option has no truthiness — truthiness is a quiet coercion of exactly the kind Option[T] ≠ T exists to ban — so a strict-mode author writes == none / == some(x), or the as-binding (B1b, issue #1475, brink-analyzer::option_conditions::check_binding_condition); a bound condition never fires this check. Strict-mode-only, best-effort static (the “Unknown never disagrees” posture: an unclassifiable condition stays silently unchecked); under types = gradual the same condition is the RuntimeError::OptionTruthiness turn-terminating fault — the runtime backstop that catches every case either way. Supersedes NS-A1’s shipped falsy-none truthiness.

§

E117

A range-refinement violation under types = strict (the E078 precedent — strict-only; gradual mode is inert and leaves the runtime fault residual, F8’s general rule): int(r) demands NonEmptyRange evidence, and either (a) the range literal in argument position is provably empty (0..0, 5..=2 — bounds fold statically, CONST refs included), or (b) the argument’s type carries no inhabitedness evidence (a possibly-empty range — route computed bounds through non_empty(r), parse-don’t-validate).

§

E118

A protocol impl registration named a numeric-tower kind (vec2/vec3/vec4/quat/mat2/mat3/mat4) as its type. Tower kinds are compiler-known value kinds, not user structs: their display is the fixed structural form, their equality is componentwise IEEE (T4), and they are NOT orderable — a compare impl for a tower kind would contradict the ruled §4b doctrine, and display/iterate impls would shadow compiler-owned behavior. The rejection is unconditional — it wins even over a user STRUCT declared with the same name (tower type names are global like int).

§

E119

A sort_by/sorted_by comparator provably breaks the pure·silent contract (§4b: “the comparator falls under the trio’s pure·silent rule plus the consistent-total-order LAW”). Exceedance-only, the E114 posture: flagged when the comparator is a statically-named #fn(target) whose inferred row shows a global read/write, an external call, a content emission, or a tag touch — an opaque or unresolvable comparator is not proven in violation and passes (the gradual posture; the VM’s isolation and ComparatorEscaped fault are the runtime residual).

§

E120

NS-A7 Weighted[T] construction refusal (docs/stdlib-spec.md §8, issue #1113): the compile-classifiable half of the E078-style evidence-by-construction split. Fired by the weighted(…) lowering for a statically-malformed table — an empty pair row, an odd (dangling-weight) argument count, or a literal weight that is not a positive int (zero, negative, float/string/bool). Computed weights are not classifiable here; they carry the construction fault residual instead (RuntimeError::WeightedBadWeight), so a table that exists is always rollable.

§

E121

Contract §4.2 check 1a (manifest ⇄ HIR agreement): an UnresolvedRef.range in the manifest has no matching referencing-expression range anywhere in the file’s HIR body — the range-equality resolution join (Q2(a)) would silently fail to find this reference at all.

§

E122

Contract §4.2 check 1b (manifest ⇄ HIR agreement): a manifest-declared symbol has no corresponding HIR declaration node of the same name — the manifest and the HIR body have drifted apart.

§

E123

Contract §4.2 check 1c (manifest ⇄ HIR agreement, F-I#4): a Knot’s is_function flag disagrees with whether its declared symbol carries the "function" detail sentinel.

§

E124

Contract §4.2 check 2a (range well-formedness): a HIR node’s source range is empty or extends past the end of the source file — ranges are resolution join keys and IDE geometry, so a garbage range would otherwise corrupt resolution silently instead of erroring loudly. Exempts the Option<Provenance>-carrying synthesized nodes (Content.ptr/Divert.ptr/Return.ptr) when None (B0.1 finding F-B2) — this fires only on a range that is present but malformed.

§

E125

Contract §4.2 check 2b (join-key uniqueness, Q2(a)): two distinct UnresolvedRef entries in the manifest share an identical source range — the range-equality join can no longer distinguish them.

§

E126

Contract §4.2 check 3 (name-convention conformance, F-I#3): a declared symbol’s qualified name does not match the dot-qualification shape its SymbolKind requires (bare for knots/globals, knot.stitch for stitches, List.item for list items, knot[.stitch].label for labels).

§

E127

Contract §4.2 check 4 (control-flow classification, F-I#7): a terminal statement (Divert/Return) is not the last statement in an inline conditional or sequence branch.

§

E128

Contract §4.2 check 5 (provenance-kind ⇄ SymbolKind consistency, F-I#5, the #626 floating-stitch trap): a Knot/Stitch HIR node’s provenance class disagrees with the SymbolKind bucket its declared symbol was indexed under in the manifest.

§

E129

A native construct parses cleanly but has no HIR lowering yet in this slice (a nested module { … } block, a fn declared below top level, an @[…] annotation line, a lambda expression in value position, or any other CST shape hir::lower_native does not yet recognize). The construct is skipped — not silently: this diagnostic names exactly what was skipped and why.

Also raised by brink_analyzer::modules::check (issue #1592, #1686 review) for the whole-project-only instance of the same gap: a bare use/IMPORT item’s trailing segment that is both aliased and — only knowable once whole-project module data resolves the dual-reading — a declared submodule. Aliasing an entire imported module’s export set has no Import/ImportItem representation, same as the single-segment use a as m; form lower_native::import::lower_use_decl already rejects with this code; this later firing exists only because that verdict isn’t decidable until the analyzer’s whole-project pass.

§

E130

A native flow is declared more than two levels deep (a flow nested inside another nested flow’s body) — the contract’s Q4(b) fence (docs/hir-admission-contract.md §5 Q4): exactly two container levels for v1, addressing model written to generalize. Depth-3+ nesting parses and is rejected here, never silently flattened into a 2-level shape.

§

E131

<- (splice) used outside a choice point (issue #1263, ruled #1260 on #1256): charter §11 narrows threads to scoped splices inside {? … } choice points, so this has no structural meaning — but <- can also be literal dialogue punctuation, so this is warning severity, never blocking (see DiagnosticCode::severity below). The construct still parses as ordinary text; nothing is dropped or rejected. brink-syntax-native’s parser::choice::splice_outside_choice_point raises the ParseSeverity::Warning diagnostic this code carries once it reaches brink-db’s lower_native_file.

§

E132

A native file-level @[was(…)] rename record (issue #1286) carries no quoted old module path — a missing argument, or one that is not a string literal. Native module paths are ::-separated and travel as a string (:: is not annotation-argument grammar), so the migration target must be spelled @[was("story::old::path")]. Warning severity, never blocking (see DiagnosticCode::severity): the malformed directive is skipped — no alias is produced — but the file still compiles. brink-ir::hir::lower_native::module::lower_file_module raises it rather than silently dropping the authored record.

§

E133

A native file’s root_content carries something other than the one documented shape a native lowering may leave there: empty, or the single synthesized flow main() entry divert (maintainer-ruled 2026-07-21, docs/decision-log.md “Native story-entry convention”). Anything else — real weave content, more than one statement, a source-backed divert — is ink-only baggage: ink’s pre-first-knot root weave has no native equivalent.

§

E134

A native file’s HIR carries an IncludeSite — native has no textual INCLUDE graph (charter §13.2, “the tree is the compilation universe”); hir::lower_native::lower always leaves includes empty, so any entry here is ink-only baggage that reached native HIR some other way.

§

E135

A ThreadStart (<- target) appears somewhere other than the two legal native splice positions B0.7’s choice-point lowering produces: immediately preceding the ChoiceSet it preambles, or as the trailing statement(s) of a Choice’s own body (hir::lower_native::choice::lower_choice_point). An “ambient” thread start anywhere else has no structural meaning on the native surface (charter §11 narrows threads to scoped splices inside {? … } choice points).

§

E136

A native ChoiceSet carries a depth/context other than the B0.7-documented neutral values (depth = 0, context = Inline, docs/hir-admission-contract.md §3 D4) every native choice set stamps uniformly — native has no weave fold to report a real value from, so any other value means a weave-fold concept leaked in from somewhere it shouldn’t have.

§

E137

The B0.9 native strict-only enforcement point (docs/b0-sequencing.md §B0.9, decision-log 2026-07-19 “Typing posture ruled”): a native .brink file was compiled with an explicit types = gradual knob. Gradual typing does not exist on the native surface — types is not a project knob there the way it is for the transitional brink dialect, so an explicit gradual setting reaching a .brink compile is refused, loudly, rather than silently accepted.

§

E138

A map literal supplies the same key twice (Map { k: 1, k: 2 }). The E076-lineage cascade ruling (A) of #1103: a duplicate key is a compile error, consistent with a struct literal’s duplicate field (Self::E084) — last-wins would silently swallow the typo. Only statically comparable literal keys can collide here (int/string/bool, the E106 key domain); a dynamic key is left to the runtime, exactly as the key-domain check leaves it.

§

E139

A construction literal’s entries are not in the form its target type constructs from — Map { a } (element form for a key/value target) or Flags { A: 1 } (key/value form for an element target). The brace tokens are one fixed grammar; the entry form each type consumes is the construct protocol’s business (crate::hir::construct::ConstructTarget::form), so a mismatch is caught at dispatch rather than by the parser.

§

E140

D1: recv.name(args)’s receiver type declares a field name, but that field is not function-typed. Field access wins outright — a matching-but-non-callable field is a hard error, never a silent fall-through to a free function of the same name, so that a call’s meaning can never hinge on a field’s type.

§

E141

recv.name(args) resolved as neither: the receiver’s type declares no field name, and no free function name is visible in ordinary lexical scope (D4 — the candidate set is lexical scope only; there are no method sets or inherent impls). One diagnostic naming both attempts, so the author sees the whole search that failed.

§

E142

D3: recv.name(args)’s receiver type is not known at the resolution point, so field-access-wins is unanswerable. An annotation is demanded rather than the resolution being deferred (E107-family posture). Explicitly a for now trade — smarter inference ordering is planned and additive when it lands.

§

E143

D5: recv.name(args) resolved to a free function whose first parameter is declared ref, so the receiver is auto-ref’d (party.leader.heal(5)heal(ref party.leader, 5), issue #1462) — but this receiver cannot be written through: a CONST, or a projection whose root is a frame-local (T1e’s durable-root rule, docs/t1e-spec.md §2), or — once the grammar can spell them — an rvalue such as [1,2].push(3). Refused rather than silently desugared by value, which would drop the mutation. A non-ref first parameter never reaches this code: the by-value desugar puts no lvalue requirement on its receiver.

§

E144

A UFCS call site that brink-analyzer::ufcs resolved cleanly has reached LIR lowering, which does not consume the verdict side table yet. Refused loudly rather than lowered: the callee path’s resolution record names the receiver (the D2 side table is what names the real target), so lowering it as an ordinary call would emit a call against a local’s id and silently produce a wrong program. Same “parses/ resolves but has no lowering yet” posture as Self::E129, one layer further down.

§

E145

The v1 whole-condition restriction: an as binding was written over a &&/|| composition (if a && find(x) as s { … }). The ruling fixes the binding as the entire condition for v1 — let-chains can land later, additively — so a boolean composition under the binding is refused rather than silently binding the composite (which is never an Option[T] anyway). The mirror spelling, an operator after the binding (if find(x) as s && …), is caught one layer earlier as a parse error (brink-syntax-native::parser::binding).

§

E146

RETIRED (issue #1508) — previously “an as binding in a choice guard (* {if EXPR as name} [text]) is ruled but not yet implemented”. hir::lower_native::choice::lower_choice now lowers it for real: capture-at-presentation, by-value COW (docs/decision-log.md 2026-07-26, “Choice-guard as un-deferred”), reusing the same OptionBind/frame-slot machinery IfStmt::binding already used — the guard’s OptionBind writes into the same frame BeginChoice’s fork_thread snapshots into the pending choice, so the captured value rides along with no separate wire-level capture needed. Code kept reserved, not reused, for diagnostic-code stability — no longer emitted by any pass.

§

E147

An as binding whose condition is a statically-known non-Option type (if 5 as n { … }). The binding unwraps Option[T] to T; there is nothing to unwrap here. Strict-mode-only and classification-gated, exactly like its F27 twin Self::E116: an Unknown/Conflicted condition stays unjudged rather than guessing.

§

E148

A write to an as binding — if find(s) as i { i = 0; }, pop(i), i[0] = x, bump(ref i), b.field = v, push(b.field, v), … The binding is immutable by ruling (docs/decision-log.md 2026-07-26): it names the unwrapped payload the condition proved present, and rebinding it would make the narrowing guarantee a lie. Raised via the shared lir::lower::stmts::reject_as_binding_write check (issue #2122) for: plain/compound assignment, an indexed-assignment root, and a bare in-place mutator, all via lir::lower::stmts::lower_assign_target itself; a single-level struct-field write (lir::lower::blocks::lower_single_level_field_write) and a struct-field mutator (lir::lower::blocks::lower_field_mutator), which resolve a Param/Temp root’s slot independently of lower_assign_target (their root is the head of a two-segment path, not the whole target) and so call the shared check directly instead; and separately at the ref-argument choke points (lir::lower::expr::lower_ref_path_call_arg, lower_ref_projection_arg), since passing the binding by ref hands the callee a raw pointer to the slot without ever routing through ordinary assignment lowering.

§

E149

A remove(a, i) call whose first argument is statically known to be an array (issue #1532, the #1501 review’s migration-tail finding): remove went map-only in #1484 (identity-based, idempotent-total key removal; docs/t1b-surface-spec.md §5), and the array-index leg it used to also serve moved to its own verb, remove_at(a, i). With no compatibility shim, an un-migrated remove(array, i) call site still parses and type-checks as a call to the (now map-only) builtin — infer::body’s remove arm already has Ty::Array in hand at the call site — and previously reached codegen clean, only faulting at runtime against MapRemove’s domain check. Strict-mode- only (infer::body::InferPass::array_remove_calls, strict::check_array_remove_calls), matching every other TM-3 typed-mismatch check in this range — the brink dialect’s own implicit default is types = strict (issue #1127), so this fires for the common case; under types = gradual the MapRemove runtime fault stays the backstop, same posture as the rest of TM-3.

§

E150

A def (function or value-returning flow/stitch) declares a non-void return type but its body may fall through without ever executing a value-carrying return <expr> (issue #1551, docs/decision-log.md 2026-07-22 implicit-end ruling item 3: “a flow that declares a return type must produce a value… falling through without a value is a checker error”, ratified for a return-typed flow/stitch and now extended to the identical fn shape). Strict-mode-only (strict::check_def’s escape check, extended by #1551 to run for any def carrying a declared return type, not just is_function); deliberately distinct from Self::E065 Unknown-escape — the annotation-fallback in infer::body::infer_def_body backfills a no-return body’s inferred return type from the annotation itself, so the type comes out concrete (Clean, not Unknown) and E065’s classification can never see this mistake; only a direct has_value_return check catches it. An implicit -> DONE is never treated as satisfying this — DONE ends the turn, not the value contract.

§

E151

A native {? … } choice’s own body falls through (no divert/return) while a sibling choice in the same set diverts onward, at a genuine dead end (nothing follows the choice point to reconverge into) — the residual value of ink’s retired “ran out of content” error, relocated to a narrow, opt-in, warning-severity lint (brink_analyzer::native_choice_dead_end) rather than a blocking runtime fault. Fires only for the mixed case — some siblings divert, at least one doesn’t — never for a choice set where every branch falls through (an ordinary menu that ends) or where the choice set’s continuation is non-empty (native has no gather, docs/native-surface-charter.md §5 — a non-empty continuation is the dissolved gather, and every falling-through branch reconverging there is ordinary weave structure, not a mistake).

§

E152

A contains(m, needle) call whose needle argument is statically visible as outside the map key domain (int/string/bool) while m is statically visible as a map — companion to the #580 ruling (docs/decision-log.md 2026-07-12 “contains(map, non-key-domain needle) returns false”): the call can never do anything but return false at runtime, so the always-false result is a compile-time warning rather than a silent footgun. Strict-mode-only (brink_analyzer::contains_domain, wired into strict::check alongside conversions/range_refinement — the same inference-substrate-backed domain-check family): needs the project’s whole-program InferenceResult (structs::classify_expr_ty) to classify a variable/call/ index-valued needle, which is only ever computed under types = strict. Under types = gradual this stays silent and the runtime’s total false return is the sole (correct, non-faulting) backstop. Warning-severity like E106’s map-literal-key sibling check, so it flows through the ordinary suppressible diagnostics channel and is re-levelable via the project’s [lints] table.

§

E153

An @[allow(…)] argument is not a diagnostic code this compiler knows (DiagnosticCode::from_str_code says no) — a typo like @[allow(E1511)] or a name like @[allow(dead_code)].

A hard error by construction, and deliberately so: the whole point of a suppression directive is that the author believes a diagnostic is being silenced, so a misspelled code that silently does nothing is the worst possible outcome (the #1374 reserved-keys lesson, and the @-namespace rule in docs/directive-annotations-spec.md §1.1 — every @-mark is a valid directive in a valid placement or a hard error).

§

E154

An @[allow(…)] names a real diagnostic code that is not suppressible: one whose default severity (DiagnosticCode::severity) is Error.

Source-level suppression only ever reaches the warning/lint tier. An error means the compiler cannot produce a correct artifact, so letting an annotation silence one would be a way to ship broken code; the B0.3 admission-validator family (E121E128) is covered by the same rule (all Error-severity) and structurally, since admission diagnostics never route through crate::suppressions::apply_suppressions at all. This mirrors the [lints] table’s own hard-error exemption (issue #1160, step 2 of brink_analyzer::effective_severity): rather than curating which Error codes are “safe” to relax, none of them are reachable.

§

E155

An @[allow(…)] whose argument list is missing, empty, or not a flat list of bare code identifiers (@[allow], @[allow()], @[allow("E151")], @[allow(reads(x))]).

The grammar counterpart of E100 on the @[effects(…)] channel: the annotation parses as an annotation but declares nothing this channel can act on.

§

E156

A lambda body assigns to a captured binding — a let/param binding declared outside the lambda and read inside it.

A hard error by the 2026-07-19 ruling (“assignment to a captured binding is a compile error”): brink lambdas capture BY VALUE always (Rust’s move as the only mode, no keyword, no ref captures in v1), so the binding a lambda body writes to is its own snapshot — the write can never be observed by the enclosing scope. A snapshot write is always a lost write, and this kills the closure-mutation confusion structurally rather than letting authors discover it as a silent no-op at runtime.

Writes to a global (a module-level var cell) are not captures and are not flagged: a global is a durable cell reached by name, not a snapshotted binding.

§

E157

An unnamed once-only choice, or an unnamed sequence ({cycle: …} / {stopping: …} / {once: …} / {shuffle: …} and combinations), that genuinely carries durable visit/turn-count state with no author name to anchor it — the choice/sequence’s compiled scope id is purely structural (a positional hash, brink_ir::hir::stamp), so a content edit anywhere earlier in the same scope can shift it, orphaning the saved count under the old id. The observable fallout is bounded (only visit/turn counts key on a scope id — see brink_format::LoadReport::anonymous_states_dropped): a once-only choice may reappear, or a sequence may restart from its first branch.

Naming is the opt-in fix — a labeled choice (* (label) …) resolves its identity by name instead of position (stamp::stamp_stmt’s lookup_label_id branch), immune to this drift. Sequences have no label syntax of their own; the mitigation is structural (isolate the sequence in its own small, stably-named stitch so nothing can be inserted ahead of it).

Off/info by default, tier-able through [lints] like any other diagnostic (brink_analyzer::strict::effective_severity — this is the one code whose default severity is Info, not Warning; see that function’s doc for how [lints] still reaches it). A single-shot project that never patches its content is never nagged; a live-ops/UGC project can raise it to warn/deny.

Precision over recall (brink_analyzer::anonymous_stateful): a + sticky/repeatable choice never triggers this (no once-only gating, no state) and a single-branch, non-once sequence never triggers this either (its computed index is 0 regardless of visit count — genuinely stateless despite the syntax).

§

E158

A lambda body reads a name that the analyzer resolved as a Temp/Param of the enclosing frame, but that lifting’s free-name scan cannot see as a capturable local at the point it runs — in practice, the lambda’s own not-yet-bound let name, read recursively (let f = |x| f(x - 1);): the initializer is scanned for captures before the let finishes binding f, so f has no temp slot yet in the enclosing frame.

A hard error rather than a silent fall-through: an unresolved free name that is not a real local (a global var, a knot/function name) is left alone and resolved by name from inside the lifted function, which is correct. But a name the analyzer says is a local must not take that same silent path — falling through would let call lowering target the let’s own DefinitionId as though it were a callable container, a miscompile that only surfaces as a runtime fault. Recursive lambdas are not supported in this slice; this refuses them at compile time instead of shipping a broken call.

§

E159

An @[element(…)] annotation whose args clause is missing, or whose value is not a quoted string, or whose value does not compile as a portable-regex pattern (regex::Regex::new).

The grammar counterpart of E100/E155 on the @[effects]/ @[allow] channels: the annotation parses as an annotation but declares a pattern this channel can’t act on.

§

E160

An @[element(args = "…")] pattern’s named capture group does not match the name of any parameter on the annotated declaration.

The capture contract (§3.5b: “named captures bind params by name (compile-checked)”) is enforced here, at the declaration, rather than deferred to the !name dispatch site — a capture that can never bind anything is a static defect in the pattern itself, not a per-call-site concern.

§

E161

An @[style(…)] clause is not the key = "value" shape (a bare identifier, a nested paren-clause, or a non-string value), or the argument list is missing or empty.

§

E162

An @[style(…)] clause’s key is neither line, dispatch, nor the name of a named capture group in the paired @[element(…)] pattern on the same declaration.

Validated against the real capture set rather than accepted blind — a typo’d key would otherwise silently style nothing (CLAUDE.md “flag silent data drops”).

§

E163

An @[style(…)] annotation with no paired @[element(…)] on the same declaration.

@[style] is a companion annotation (§3.5b addendum 4): its keys name @[element]’s captures (plus the two special keys, line and dispatch), so a style declaration with nothing to style against is malformed rather than silently inert.

§

E164

An inline markup span (<name>…</name>) whose tag name is not declared in the host manifest’s markup vocabulary.

Only ever reachable once a host declares a vocabulary: markup is freeform by default (§4.2), so with no declared span kinds this code cannot fire at all. Warning by default and therefore [lints]-configurable and @[allow(E164)]-suppressible — the “configurable severity” half of §4.2’s ruling.

§

E165

An inline markup span carries an attribute the host manifest does not declare for that span kind.

The per-kind counterpart of E164, and gated the same way: it fires only for a span whose name the manifest does declare (an undeclared name reports E164 alone rather than cascading one report per attribute).

§

E166

A block-flagged @[element(…)] annotation whose declaration has no trailing content-typed parameter to receive the captured run, or whose would-be receiver is also one of the pattern’s own named captures.

block widens the same capture contract Self::E160 enforces for args’ named captures — the ruling’s content param (“the following run … the same first-class fragment-capture path !radio uses for the rest of its line”) is a structural requirement on the declaration, checked here rather than deferred to dispatch: a block annotation with nothing to bind the captured run to is a static defect in the declaration, not a per-call-site concern. The dispatch and capture rewrite itself — matching the terminator, building the FragmentRef, calling the handler — is issue #1838’s natural-notation dispatch, not yet implemented; see crate::ElementAnnotation::block’s own doc.

§

E167

A natural-notation @[convention(claims = "…", order = N)] handler declares a parameter that its pattern never captures, so a claimed line has nothing to bind it to.

The other half of E160’s contract, and the half only claiming handlers need: a !name-dispatched handler can be called by hand with ordinary arguments, but a claimed line is rewritten to exactly one call whose every argument comes from a named capture — so the pattern’s capture set and the handler’s parameter list must match exactly, not merely one-way.

Renumbered to E167 (from a since-vacated E166) when this landed alongside issue #1839’s block declaration surface, which claimed E166 first (merged into main first) — see that code’s own doc.

§

E168

Two @[convention(claims = "…", order = N)] handlers declare byte-identical patterns, and the later-declared one never actually won a claim in this file — so it is dead code.

Issue #1848: dispatch order is first-match-wins over the module’s claiming handlers, ordered by @[convention]’s required order property (issue #2164 — declaration order, the interim pre-#2164 rule, no longer applies; hir::lower_native::element::try_claim’s own doc) — an undocumented rule until this issue, and one with no diagnostic when two patterns can both claim the same line. This is the sound, narrow slice of that check: identical patterns provably match identical inputs, so the overlap is certain, not merely possible.

A byte-identical twin is not unconditionally dead, though: try_claim excludes a handler from claiming lines inside its own declaration (the staging rule), and that exclusion does not extend to a later twin — the later twin is exactly the handler that can claim a line living inside the earlier one’s own body. So this diagnosis runs after the whole file is lowered and only fires when the later twin produced zero actual claims (hir::lower_native::element::diagnose_duplicate_patterns’s own doc) — a later twin that is live for even one line is not flagged.

General overlap between two different patterns (e.g. one whose matches are a strict subset of the other’s) is real and valuable — the issue’s own framing calls it “the genuinely valuable half” — but is not detected here: proving it soundly needs either a witness string both patterns can be shown to accept or a full regex-intersection analysis, neither of which this slice builds. Tracked as a follow-up, not silently out of scope — see the issue thread. Warning by default (@[allow(E168)]-suppressible, like every other Warning-tier code) since a duplicate claim is dead code, not a hard error the way an unregistered claim (E112) is.

§

E169

A top-level fn carries an @[convention(claims = "…", order = N)] pattern- claiming annotation, but this file is not the project’s configured conventions module — the module half of the 2026-07-31 §9.1 ruling’s item (4) asymmetry (issue #1844; #1838 landed the placement half, E112, and #1847 the module-nesting corner of it): “pattern- claiming is confined to ONE module — the conventions module named in brink.toml. !name-dispatched handlers stay legal anywhere precisely because they self-announce.” A sigil-dispatched line announces itself at the call site; a claiming pattern can silently reinterpret ordinary prose, so the auditability the ruling protects depends on every claim living in the one file an author (or reviewer) knows to open.

Only fires when brink.toml’s [project] conventions key (renamed from elements by issue #2180) names a project-relative .brink path (conventions = "conventions.brink") — a bare built-in preset name (conventions = "screenplay") points at a std::conventions::* module with no project file to compare against, and an unset conventions key means no conventions module is configured at all, so there is nothing to confine against yet (brink_db::queries::analysis::conventions_confinement_diagnostics_query’s own doc). Error by default, the same posture as E112: a misplaced claim is not a style nit, it is a claim that violates the one property the whole mechanism depends on.

§

E170

Two claiming handlers’ patterns are textually different but can both match the same line of prose — they silently race, with the earlier one winning (issue #1859, follow-up to #1848).

E168 catches the narrow case: byte-identical patterns provably match identical inputs. This code catches the more common and more valuable instance: two different patterns whose matched-line sets overlap (one a strict subset of the other, an alternation branch shared between them, two competing prefixes, etc.).

Detection uses a sound-but-incomplete heuristic: finding a concrete witness string demonstrably accepted by both compiled patterns. The heuristic checks:

  • Whether the patterns share a literal prefix (both start with the same fixed text, before any regex metacharacters)
  • Whether one pattern’s literal parts are a subset of the other’s (e.g. ^A$ is subsumed by ^AB?$ if the second can match A)
  • Whether generated test strings match both patterns

A witness found proves overlap; none found does not prove they never overlap. This is the safer alternative to a textual heuristic that could produce false positives (e.g. “shares a literal prefix” without confirming both patterns actually match anything starting with that prefix — ^A$ and ^AB$ share the prefix A but never both match any input). Only reports what it can prove.

Reported at most once per later-handler, against the first earlier handler it provably overlaps with. A handler that overlaps multiple earlier ones is not re-reported. Warning severity, like E168, since a silent race is a real problem but not a hard error.

See also: E168 (byte-identical patterns), docs/prose-dialect- spec.md §3.5b (“pattern power proportional to auditability”).

§

E171

A natural-notation @[convention(claims = "…", order = N)] handler declares a parameter, bound by a named capture, whose declared type is neither string nor absent nor contentint, float, bool, a struct name, a generic, or a fn type.

Filed from adversarial review of PR #1845 (issue #1849, itself closing part of #1838): hir::lower_native::element::try_claim binds every matched capture as a plain Expr::String literal, unconditionally, regardless of the receiving parameter’s declared type — so @[convention(claims = "^Take (?<n>\\d+)$", order = N)] fn take(n: int) could never actually receive an int. Numeric capture coercion is docs/prose-dialect-spec.md §3.5b’s own Deferred list — the underlying gap is ruled-deferred, not itself a bug — but leaving the mismatch silent is: without this check it was, and remains, silent — nothing checks a direct call’s arguments against the callee’s declared parameter types yet. That generic check (E063 for this shape) is exactly what open issue #1864 asks to build.

content is deliberately not in this code’s target set even though a capture can no more produce a FragmentRef than an int — it already has an established, ruled, and tested story of its own (the spec’s own fn radio(chan: string, text: content) example, and the tier1-native/annotations-element golden fixture, both compile clean today); see hir::lower_native:: annotation::is_satisfiable_by_a_string_capture’s own doc for why.

Reported at the declaration — the same static-defect-in-the- declaration posture E160/E166/E167 already take — pointing at the offending param’s own type annotation range (an untyped or content-typed param never triggers this code, so the annotation is always present and non-content when it fires). Error by default: unlike E168/E170’s “silent race” posture, this is a param that can never receive a value of its declared type, not a stylistic ambiguity.

§

E172

A native tag (#…) whose text begins with @ — the shape of an ink-dialect compiler directive (#@private, #@was(…), #@local, #@module(…), #@effects(…)) — is lowered by a .brink file (issue #1835).

#@… is not its own grammar production in either dialect: it is an ordinary tag (HASH + free text), and only ink’s HIR lowerer (hir::lower::directive::parse_directive_tag) gives a leading @ special, compile-time-consumed meaning. # is already the runtime- tag sigil in native content position (that is exactly why #@… parses as a tag rather than a directive there too), and hir::lower_native has no matching check — so before this code, an author porting a file from ink, or splitting time between the two dialects, got no error and no warning: the directive text became ordinary tag content on the compiled story, silently, which is worse than a plain no-op because it surfaces as mysterious runtime output rather than a compile-time failure.

Warning by default, not Error: a literal @-led tag can be a deliberate runtime convention for a host that wants one (the issue’s own caution), so the diagnostic is [lints]-configurable and @[allow(E172)]-suppressible rather than blocking, the same posture as E132/E168/E170. hir::lower_native::body::lower_tag raises it, naming the native spelling to use instead when the tag names a real ink directive that has one (@[was(…)], @[effects(…)]) and saying so plainly when it does not (module, public, private, local have no native annotation counterpart yet). #@allow is its own case — ink’s directive recognizer does not know allow either, so the message never calls it an ink-dialect spelling; it only notes that native’s own @[allow(…)] annotation (an unrelated diagnostic-suppression channel) happens to share the name. Any other unrecognized name gets a shape-only wording that never asserts ink membership.

§

E173

An inline markup span of a declared kind is missing an attribute the host manifest marks required for that kind.

The counterpart E164/E165 never caught: attrs was an allow-list only until #1997, so a declared attribute simply absent from a span went undiagnosed. Gated the same way as E165: it only ever fires for a span whose name the manifest does declare (an undeclared name reports E164 alone), and only for attributes the declaring kind actually marks required — a kind with none required never raises this for any span of that kind. One report per missing attribute, not one combined message, mirroring E165’s one-per-attribute posture rather than E164’s one-per-span.

Warning by default, the same posture as E164/E165, for the same reason: only a Warning-base code is [lints]-configurable and @[allow(…)]-suppressible, and a host that wants a required attribute to be binding raises it with [lints] E173 = "deny".

§

E174

A lambda’s own written annotation (a param’s : T or the lambda’s : R return annotation) disagrees with its body-derived type (issue #1994, RULED 2026-08-01, closing #1932: “the written annotation takes priority… an incompatible body is an eager error at the lambda, not a deferred surprise at the call site”).

#1910/PR #1928 made infer::body::InferPass::infer_lambda read a lambda’s body-derived param/return types back — the same overlay infer_def_body already applies for a top-level fn/flow — which silently let a wrong body derivation override a correct written annotation with no diagnostic anywhere (a standalone let f = |k: int|: int { "wrong" }; with no call site produced nothing at all). This code closes that gap for the annotated case specifically: a lambda’s own written per-param/return annotation now always governs that slot’s resulting type, and this diagnostic fires the moment the body-derived type (when it resolves to anything concrete) disagrees with it — deliberately not gradual/advisory like E063, since the annotation is the ruled source of truth for a lambda’s own signature, not a hint to double-check later.

#1910’s own fix is unchanged for the unannotated case — a lambda param/return with no written annotation still exports whatever its body derives, exactly as before.

Native-only (LAMBDA_EXPR has no brink-syntax counterpart, same posture as E156/E158): raised only from infer::body::InferPass::infer_lambda, reported by strict::check_lambda_annotation_mismatches under types = strict.

§

E175

RETIRED (issue #2165) — was register’s comptime-only-intrinsic confinement check (issue #1840 Q5): register was legal only inside the project’s configured conventions module’s fn conventions(), enforced here. The 2026-08-03 ruling (docs/decision-log.md, “fn conventions() is DISSOLVED”) removed fn conventions() and register from the design entirely — precedence is now a static order property on @[convention] (issue #2164), needing no comptime evaluator and so no confinement diagnostic to raise. Code kept reserved, not reused.

§

E176

A divert-with-args site (-> knot(args), ->-> tunnel(args), or <- thread(args)) supplies a number of arguments that does not match its resolved target’s declared parameter count (issue #2156).

PR #2150 (issue #2136) wired native’s -> knot(args) call-args syntax into DivertTarget::args for the first time — before that, the shape hard-failed E129 on native and never reached this check at all. Investigating that newly-reachable path found the arity gap was real on both dialects: brink_ir::symbols::project’s walk_divert_target/Expr::DivertTarget ref-pushing sites always recorded arg_count: None for a RefKind::Divert reference, unconditionally discarding DivertTarget::args.len() — so brink_analyzer::resolve::check_arity (E031, gated on arg_count.is_some()) could never fire for a divert, on either dialect, regardless of how many arguments were supplied. E176 is E031’s sibling for the divert call shape, kept as its own code (rather than widening E031’s own message, which names function calls) so the two diagnostics can be told apart and suppressed independently.

Scoped to a resolution that names a Knot/Stitch/Label (the only symbol kinds with their own declared parameter row) — deliberately not checked when the divert resolves through a Variable or a divert-typed local Param (a stored/forwarded divert-target value, e.g. the ink docs’ -> generic_sleep (-> waking_in_the_hut) — see “Advanced: sending divert targets as parameters”), whose underlying target’s arity is not known statically at the indirection site. resolve_function’s own check_arity call sites already draw this same line (only External/Knot resolutions are checked; Variable/local resolutions are not).

Warning-tier by default, matching E031’s own severity precedent for the identical arity-mismatch shape at an ordinary call site — lir::lower::stmts::lower_divert_target still lowers a mismatched site (lower_call_args pushes exactly as many CallArgs as the divert supplies, not the target’s declared count), so this stays advisory rather than blocking, and is [lints]-configurable / @[allow(E176)]-suppressible like every other Warning-base code.

§

E178

A @[convention(claims = "…")] annotation with no order clause.

order is required, not optional (docs/decision-log.md 2026-08-03 “order is REQUIRED on @[convention]…”): a claiming handler competes for lines it did not announce, so its precedence against every other claiming handler in the same module must be total, explicit, and authored — there is no default to fall back to, and the compiler never infers one from declaration position. Reported at the annotation line, the same posture E159 already takes for a missing/malformed claims value; yields no ConventionAnnotation at all (never a partial one with a made-up order).

§

E179

Two @[convention] declarations in the same module carry the same order value.

Ties are rejected, not resolved (the same ruling as E178): “there is no tie-breaking rule, because ties are rejected rather than resolved.” Reported against every conflicting declaration — the duplicate-definition posture, not a single “first one wins, second one is the problem” report — so an author sees the whole conflicting group regardless of which one they open first.

§

E180

A @[convention(…, attach = StructName)] clause names a struct the declaration’s own return type does not agree with (issue #2178, split from #2164’s 2026-08-03 design-backport comment “item 2”: “The attachment schema is a STRUCT — do not invent a DSL”).

docs/decision-log.md 2026-08-03 states the governing split plainly: “keys are declared, values are computed”attach declares which keys a handler attaches and their types by naming an ordinary struct; the handler body computes the values. That only holds if the handler’s own declared return type actually is the named struct — a mismatch (a different type, a generic, a fn type, or no declared return type at all) means the projection and the handler’s real output could never agree, so this is reported at the attach clause’s own value rather than silently trusting the name. Reported the same “never a partial one” way E159/E178 are: no ConventionAnnotation at all results, rather than one carrying a schema its own declaration cannot honor.

Declaration-surface-only, like every other check in this module: this compares attach’s name against the return type’s own bare name, and never checks whether a struct of that name is actually declared anywhere — that is real name resolution’s job (out of scope for this code, same posture E171’s own doc explains for captured-parameter types).

§

E181

lir::lower::structs::build_shape_table’s own decls::lookup_global(index, file_id, name, SymbolKind::Struct) call — resolving a declared STRUCT’s own DefinitionId, using its own declaring file as referrer — came back None.

This is a non-suppressible defense-in-depth backstop, the E060/E073 posture: it should never fire from a normal compile. The exact-file arm always matches a struct against itself unless brink-analyzer already dropped this HIR decl’s own symbol entry as a true intra-module duplicate (E023, same declared module as an earlier same-name declaration) — and even then, lookup_global’s unscoped fallback normally rescues the surviving sibling’s id (which is exactly what lets build_shape_table’s own by_def.contains_key dedup recognize “true intra-module duplicate” and skip it a second time, rather than minting a fresh, wrong shape). This code fires only in the narrower case the fallback itself cannot rescue: every surviving same-name candidate is std-declared, so the fallback’s own std-exclusion (issue #2197) empties the search too. Before this code existed, that combination silently dropped the struct from both ShapeTable and NameTable seeding with no diagnostic at all — shifting every subsequent ShapeId/NameId and the bytecode built from them (CLAUDE.md: “silent drops are always bugs until proven otherwise”).

Reachable today, not merely in principle (review finding on #2240): any project whose own declaring file is not all-native (project_is_all_native) and whose own STRUCT/struct shares a name with one the std-mounted screenplay preset declares (Cue, Parenthetical) collides with it — neither side needs a #@module for this to happen. symbol_index_query builds the shared index from every set_file-registered file regardless of the compilation closure, so the mounted std declaration sits in the index even for an ink entry whose LIR closure never reaches it. With neither declaration module-qualified (or the project simply not Dialect::Brink), M-2d cross-declared-module coexistence (is_cross_declared_module_collision) never applies, so the pair collapses to an ordinary same-module duplicate — and it is the project’s declaration that gets dropped whenever its own file sorts after the std key in FileId-mint order (any project file named e.g. story.ink, world.ink, types.ink does, since "std/…" sorts first). See brink-environment’s e181_is_reachable_from_an_ordinary_ink_project_colliding_with_a_std_preset_name for this compiled end to end through the real analyzer drop, not a hand-built SymbolIndex.

build_struct_shape_data (the NameId-free, cutoff-friendly twin struct_shape_data_query memoizes for the per-knot chunk lowering path) performs the textually identical lookup and has no diagnostic sink of its own to push into — it is a pure, Eq-cutoff salsa data query, not a lowering pass threading a Vec<Diagnostic> accumulator. It is deliberately left silent there rather than given a redundant sink: every real compile (brink-db‘s lir_query) always computes build_shape_table (via lir_prelude_decls_query) and build_struct_shape_data (via struct_shape_data_querychunk_lowering_ctx_querylir_knot_chunk_query) in the same salsa revision, over the same resolutions_index_query index and the same files’ structs HIR — so the exact same drop condition always fires this diagnostic from the prelude side in the same compile. See that function’s own doc comment for the full argument.

§

E182

A @[convention] handler’s transitive call closure reaches an EXTERNAL classified crate::ExternalKind::Query (a world read) or left crate::ExternalKind::Plain (unclassified) — docs/decision-log.md 2026-08-06 “No-world-reads fence: analyzer effect-row check; unclassified externals are diagnosed”.

A claiming handler competes for lines it never announced, which only holds together if classification is a pure function of the text: if it depended on game state, the editor could never display it, the projection could never be cached, and explain-match would depend on a save file. So a handler may call pure functions and crate::ExternalKind::Effect/crate::ExternalKind::Presentation externals (“commands”), but never one that reads world state — and an unclassified (Plain, the default) external is treated the same as a proven read, not the same as a proven pure call: “unprovable is not passable.” The fix is classifying the external, via an inline @kind doc tag or the registered host manifest.

Computed by brink_analyzer::no_world_reads over the transitive call closure — a handler calling a helper fn that itself calls a Query/Plain external is diagnosed exactly like a direct call — reusing the same call-graph substrate (brink_analyzer::infer::{collect_defs,call_edges,def_body}) T2-1’s effect rows are built from, per the #2179 decline comment’s finding that the aggregated row/compute_container_access route has no span to diagnose with: this walks for the real call-site span the aggregated row structurally cannot carry. Reported at the offending call’s own site, which may be inside a different definition (and a different file) than the handler’s own declaration.

§

E183

brink_ir::lir::lower::expr::lower_call’s resolved-target match found a symbol kind that is not callable — a ListItem, Label, Stitch, Param, Temp, or Struct sitting at a call position (issue #2837, filed from the #2836/w187 review). SymbolKind::Knot is the one non-External/List/Variable/Constant kind that is callable — ink allows any knot as a function via tunnels, per brink_analyzer::resolve::resolve_function’s own comment — so it keeps its own lir::ExprKind::Call arm.

This is reachable from ordinary author source, not only from a hypothetical future resolution regression: Temp/Param reach this arm whenever ctx.temp_slot does not have the name open at the call site — which is the normal, expected shape of two ordinary author mistakes, not a temp_slot bug. Calling a T1b block-scoped temp (~ { … }) after its own block has closed is diverted to Self::E082 instead (mirroring lower_path’s own guard for the same case), but a genuine forward reference — calling a name before its declaring temp/param binding — falls through to this arm and reports E183 today; that reproduces on the plain .ink surface with no --dialect brink needed. Stitch, ListItem, Label, and Struct remain analyzer-unreachable for a real call site as far as this code can tell (resolve_function never hands back Stitch/ Label/Struct there, and only hands back a bare ListItem for a #fn(target) literal, which never reaches lower_call at all) — those four are the defensive-backstop part of this diagnostic.

Refused loudly rather than silently emitting lir::ExprKind::Call against the resolved id: that catch-all is exactly the mechanism that let PR #2836’s first attempt compile a program clean — 7,941 tests, the oracle ratchet, and clippy all green — while it then faulted at runtime with UnresolvedDefinition(ListItem(..)). Same “compile error over runtime fault” posture as Self::E144’s UFCS refusal in the same module.

§

E184

lir::lower::decls’s own lookup_global(index, file_id, name, kind) self-declaration lookup — for a CONST (collect_globals’s constants pass), a VAR (collect_globals’s variables pass), or an EXTERNAL (collect_externals) — came back None.

The exact same non-suppressible defense-in-depth posture as Self::E181, for the exact same reason: this is Self::E181’s own struct-shape drop class recurring at three more call sites in the same file, all sharing lookup_global’s doc comment and none fixed by #2240/#2258 (issue #2262, filed from that PR’s own review — “#2240 under-captured the class”). The exact-file arm always matches a declaration against itself unless brink-analyzer already dropped this HIR decl’s own symbol entry as a true intra-module duplicate (E023) — and even then, lookup_global’s unscoped fallback normally rescues the surviving sibling’s id. This code fires only when that fallback also misses: every surviving same-name/same-kind candidate is std-declared, so the fallback’s own std-visibility carve-out (issue #2197) excludes it too. Before this code existed, that combination silently dropped the CONST/VAR/EXTERNAL from PreludeDecls (no lir::GlobalDef or lir::ExternalDef at all) with no diagnostic whatsoever (CLAUDE.md: “silent drops are always bugs until proven otherwise”).

Reachable today, exactly as Self::E181’s own doc found for STRUCT (review finding on #2240): an ordinary project — no #@module, no dialect override even needed for EXTERNAL (core ink syntax, unlike STRUCT) — that declares its own EXTERNAL scene_entered(…) collides with the std-mounted screenplay preset’s own extern scene_entered (std/conventions/screenplay.brink). Neither declares a module, so M-2d cross-declared-module coexistence never applies and insert_symbol treats the pair as a true intra-module duplicate, dropping whichever one’s FileId sorts after the other’s in mint order. See brink-environment’s external_self_declaration_silently_drops_when_colliding_with_a_std_preset_name for this compiled end to end through the real analyzer drop. std declares no CONST/VAR today, so the CONST/VAR call sites stay reachable only in principle (a future std module adding one), same status E181 itself carried before its own reachable case was found — not a reason to leave them undiagnosed.

§

E185

Issue #1944: a plain dotted assignment target (~ p.bogus = 1) names a field its receiver’s resolved struct shape doesn’t declare — the E070 mirror for a construction-literal’s own unknown-field check (structs::check’s “Extra” case, docs/typed-mode-spec.md §6), but for Stmt::Assignment/ BlockStmt::Assignment targets instead of a #{...} literal.

PR #1939’s check_declared_field_assign_target deliberately stays silent on an unresolvable field — it only compares a resolved field’s declared type against the RHS (“Unknown never disagrees”). ref_projection::check_strict’s E098 covers an unknown segment only in ref-argument position (ref npc.bogus), not a plain assignment target. Before this code existed, the issue’s exact repro —

STRUCT Point = #{x: float, y: float}
VAR p: Point = Point#{x: 0.0, y: 0.0}
~ p.bogus = 1
-> DONE

— compiled clean under types = strict with zero diagnostics.

Reported from structs::check_field_assign_mismatch, the same function E063 (field-type mismatch on a resolved field) comes from — fired only once the walk has resolved the receiver’s shape (shapes.resolve succeeded) and the shape itself declares no field by this name. An Unknown/untyped root never reaches this arm at all: the walk’s own guard above (let Ty::Struct(shape_name) = &current else { return; }) returns silently the moment current isn’t a resolved struct type, so “Unknown never disagrees” holds for the receiver exactly as it does for E063. A chained target (o.i.a = v, 3+ segments) never reaches this function at all — check_declared_field_assign_target’s own segments.len() == 2 fence means no FieldAssignMismatch fact is ever recorded for one; LIR’s try_lower_field_assignment already rejects it outright with the non-suppressible E074, regardless of whether the field name exists.

§

E186

Issue #2264: a @[convention(…)] handler declares BOTH block and attach = StructName on the same declaration — parse_convention (annotation.rs) now rejects the combination outright rather than silently accepting it. Before this code existed, nothing diagnosed the co-occurrence at all: try_claim’s dispatch (element.rs) is an if is_block { .. } else if is_attach { .. } with no exclusivity check anywhere upstream — block always won the if, attach was parsed and stored on ConventionAnnotation but never consulted, and the author got zero signal that half of what they wrote did nothing (verified: red_probe_block_and_attach_together_compile_clean_with_attach_inert_today in lower_native::tests, run BEFORE this code existed, proves the silent-drop shape end to end).

This is deliberately a hard rejection, not an attempt to define what “wrap AND attach” would mean together — that is an open design question (issue #2264’s own body: “Define what the combination is supposed to mean and implement it — but that’s a design question (rule 7), not a good first assumption”) with no ruling and no test pinning any combined semantics, so nothing here invents one. parse_convention returns None (never a partial ConventionAnnotation) — the same “never a partial one” posture E159/E166/E167/E178/E180 already take — so a handler declaring both is never registered as a claiming handler at all, not merely warned about.

Also reachable through the compact-cue desugar (@NAME: text, issue #2079) — it dispatches through the exact same try_claim function, so a compact-cue-claiming handler declaring both clauses hits this same check (confirmed on the issue by PR #2341’s review).

§

E187

Issue #2201: a write to a CONST — plain/compound assignment, a postfix ++/--, an indexed-assignment root, a bare in-place mutator (pop/heap_pop), a struct-field write/mutator whose root is a CONST, or passing the CONST by ref (bare or as a projection root). ink semantics (ink/compiler/ParsedHierarchy/ VariableAssignment.cs, “Can’t re-assign to a constant”) reject this at compile time; before this code existed, lir::lower::stmts:: lower_assign_target treated SymbolKind::Constant identically to SymbolKind::Variable — every one of the write channels above silently mutated the constant’s storage cell with zero diagnostics anywhere in the pipeline.

Raised via the shared lir::lower::stmts::reject_const_write check — the CONST analog of Self::E148’s reject_as_binding_write — called from every choke point that resolves a Global write root’s SymbolInfo: lower_assign_target itself (plain/compound assignment, postfix’s bare-target conversion, the indexed-assignment root via lower_indexed_assignment, and a bare mutator’s root via pop/heap_pop, all of which call lower_assign_target for their root); lower_single_level_field_write/ lower_field_mutator (their two-segment field-root SymbolKind::Constant arm, which resolves the root independently of lower_assign_target — the same reason reject_as_binding_write needs a direct call there too, per #2122); and the ref-argument choke points lower_ref_path_call_arg/lower_ref_projection_arg (passing a CONST by ref hands the callee a raw pointer to the cell, bypassing assignment lowering entirely).

Deliberately a LIR-lowering refusal (this code’s precedent is Self::E074/Self::E148, not an analyzer diagnostic like Self::E185): the write-channel enumeration above already lives entirely in lir::lower — duplicating it in brink-analyzer would re-run the exact same channel-undercounting risk that made this issue’s own premise true (#2122 named only two of the seven channels CONST reassignment turned out to have). Applies to both surfaces — .ink and .brink — since this mirrors ink’s own compile-time rejection, not a native-only extension; SymbolKind::Constant is resolved identically for both frontends by the time LIR lowering sees it.

§

E188

A declared STRUCT’s own name collides with one of the fixed names annotations::resolve’s TypeExpr::Named arm resolves before it ever consults names.structs — a builtin leaf (int/float/bool/string/content/divert) or an NS-A8 tower kind (vec2/vec3/vec4/quat/mat2/mat3/mat4). That ordering is deliberate and unchanged by this code (resolve’s own doc: “checked before the struct lookup… the same ordering that keeps int/float unshadowable”) — this diagnostic does not re-order resolution, it names the consequence: every bare type annotation spelling the colliding name (VAR v: content = ..., a param/return annotation, …) silently resolves to the builtin/tower type, never to the struct, with previously no diagnostic in either direction.

Deliberately does not cover the generic heads (List/Array/Map/Option/Weighted/Handle): those names are special-cased only inside TypeExpr::Generic’s own dispatch (e.g. Array<T>) — a bare Named reference to a struct sharing one of those names (f: Array, no <...>) still falls through to the ordinary names.structs lookup and resolves to the struct correctly; there is no actual collision to diagnose for those names. Also does not cover void — unlike the leaves above, resolve’s Named arm has no explicit "void" case at all, so a struct named void resolves fine too. Also does not cover a name shared with a declared LIST or a registered Handle<K> kind: names.lists/ names.handles are only ever consulted inside List<L>/Handle<K>’s own generic-argument position, never against a bare Named annotation — a different namespace, no collision.

A construction literal (Name#{...}) is unaffected by this shadowing for every name this code covers: resolve::resolve_struct_ref/resolve_type_ref resolve a STRUCT reference by ordinary SymbolKind::Struct lookup alone, with no builtin/tower precedence check at all — so content#{...} still constructs the user’s struct even though VAR v: content = ... cannot name it.

Warning-tier, not a rejected declaration (matches Self::E035’s “name shadows a built-in function” precedent, and the “deliberate” framing resolve’s own doc already gives this exact ordering) — a STRUCT named this way still compiles and constructs normally; only its annotation spelling is shadowed.

§

E190

Renaming an EXTERNAL changes the host binding (ruled 2026-08-24, “External renames: allowed behind the always-unsafe Force gate”).

Synthesized by the IDE’s safe-rename gate, never emitted by compilation: an external’s name is the story↔engine contract, so the story-side rename is always reported as breakage — the engine must re-register the function under the new name — and applies only through the report’s Force path.

§

E189

An ink TODO: author note (issue #3050).

Not a defect at all: AUTHOR_WARNING lines are the language’s own work-remains marker, and until #3050 lowering dropped them silently. Surfacing each as an Info-default diagnostic (the Self::E157 tier precedent) puts TODOs in the Problems panel and gives the studio’s TODO panel a single source to consume, while never gating a compile and staying [lints]-tierable like every other code.

§

E191

A content line’s inline stateful alternatives enumerate to more whole-line variants than the variant-group cap admits (#3274).

The stage-2 flip compiles a line of textual alternatives into one enumerated variant group — each variant a real line-table entry, a translation unit, and a VO slot — so the product of the alternatives’ branch counts is bounded (lir::lower::recognize::VARIANT_CAP). Breaching it is a worded hard error, never a silent fallback: an author whose line quietly stopped being VO-addressable would have no way to notice. The fix is to split the line or move an alternative to its own line.

§

E192

A brink-prefixed comment the suppression parser did not understand (#3259).

Directives were matched by exact string equality and anything else was dropped in silence — so // brink-disable-file E157, which looks exactly like the line-scoped form that DOES take codes, suppressed nothing and reported nothing. The author got neither the behaviour they asked for nor a reason, which is the silent-drop shape this project treats as a bug by default.

Warning-tier: the file still compiles. The harm is that a suppression the author believes is in force is not.

§

E193

A ~ temp is read on a path its declaration does not dominate (#3354, RULED 2026-09-01 option C).

The declaration and the read live in the same call frame, so the read resolves to the temp’s own slot — but nothing guarantees the declaring statement ran first. The three shapes the ruling names are a sibling choice branch, a gather reached from a branch that did not declare, and a read written textually ahead of the declaration. (A fourth shape the ruling originally enumerated — a stitch reading a temp declared at its knot’s root — is not a dominance question at all: the PR #3369 review found it warns on a knot/stitch divert that runs the declaration and then plays correctly, and the 2026-09-01 follow-up ruling on #3373 moved it out of E193 entirely into its own compat-deny code, Self::E194.)

Warning-tier, [lints]-overridable: the story still runs. The runtime reads an uninitialized slot as ink’s missing-variable default (0, which is also false) and warns — matching the C# reference, so what plays in Inky plays in brink — and this diagnostic is what tells the author before they play.

§

E194

A knot’s ~ temp (native ~ let) is read from one of that knot’s stitches (#3373, RULED 2026-09-01) — split out of Self::E193’s former shape 4 during PR #3369’s review.

Brink’s lir::lower::temps::alloc_temps treats a knot and every one of its stitches as one shared call frame with one TempMap, so a stitch’s reference to a name the knot’s root declares resolves to that same slot and the story plays correctly. Ink’s own compiler does not extend a knot’s ~ temp visibility into its stitches at all — the identical program is a compile-time Unresolved variable error in inklecate. This is brink accepting a strict superset of ink, not a defect in either compiler, which makes it the first member of the compat-deny tier (docs/compiler-spec.md “Compat-deny diagnostics”): Error by default (inklecate rejects the program, so brink does too until a project opts in) but, unlike every other Error-default code, [lints]-overridable — all the way to allow — because the admission invariant that tier requires is met: downgraded, brink produces a working program.

§

E195

A choice with neither display/bracket text nor a divert (#3365), matching inklecate’s own “Choice is completely empty” warning (InkParser/InkParser_Choices.cs:84-86; line 90 guards a different warning — “Blank choice”, on the * [] some text shape — which this code deliberately does not cover).

Raised from hir::lower::choice::LowerChoice::lower_choice (the ink surface only — see this code’s doc page for why the native {? … } surface is deliberately not wired to it), where the same-line evidence the check needs — whether a ->/divert token was written at all, even an empty one — is still available. Once lowered into hir::Choice, an explicit-but-empty divert (* ->) and no divert at all (* []) are indistinguishable (both leave no Stmt::Divert in the choice’s body), so the check cannot be reconstructed later from the HIR alone the way E034’s all-fallback check can.

Fires only when the choice has none of: a same-line divert (with or without a target), a tag directly on the choice line, or actual text in any of its three content regions (start/bracket/inner). A (label) or {condition} guard does NOT exempt a choice — matching the reference, which has no such carve-out either. Warning, [lints]-overridable, matching the sibling markup/shadow-warning family (E164/E188/…) — the story still compiles.

Implementations§

Source§

impl DiagnosticCode

Source

pub const ALL: &'static [DiagnosticCode]

Every DiagnosticCode variant, in declaration order.

Kept in sync with the enum by hand (there is no derive-based enumeration here), but exercised by brink-test-harness/tests/diagnostic_docs_validation.rs’s diagnostic_codes_are_unique test: that test asserts ALL.len() matches the number of code strings from_str_code recognizes, so a variant added to the enum but missed here fails CI immediately instead of silently under-covering the uniqueness/round-trip checks.

Source

pub fn as_str(self) -> &'static str

The stable string representation (e.g., "E001").

Source

pub fn title(self) -> &'static str

Short human-readable title for this diagnostic code.

Source

pub fn severity(self) -> Severity

Default severity for this diagnostic code.

Source

pub fn is_compat_deny(self) -> bool

Whether this code is a member of the compat-deny tier (#3373, RULED 2026-09-01): “inklecate rejects this; brink can run it; you must opt in.” docs/compiler-spec.md “Compat-deny diagnostics” owns the tier’s admission invariant — a code may join only when brink produces a working program with the code downgraded, so every member needs its own fixture proving that.

This is the one predicate Self::is_overridable widens past its old “not Error-by-default” rule for: every compat-deny code keeps severity() == Error (matching ink’s own hard rejection) while still being [lints]-overridable, all the way to allow — the ruling’s explicit ask (“we should allow it to be turned off if the user wants, it’s annoying”).

Source

pub fn is_native_only(self) -> bool

Whether this code can only ever arise on the NATIVE (.brink) surface (#3169).

Which surface can produce a diagnostic is a property of the diagnostic, not of any consumer — an ink-only project cannot produce these no matter who is asking, so the answer belongs here rather than in whichever tool happens to want it.

Everything not listed defaults to “both surfaces”, deliberately. No analysis pass declares the surface it can fire on, so this is read from what each diagnostic MEANS — and the two ways of being wrong are not symmetric. Claiming native-only wrongly hides a setting from an author who is actually seeing the diagnostic; claiming both wrongly shows one that cannot fire. The second is clutter, the first is a dead end, so a code earns its place here only when the compiler itself says so, and everything uncertain stays visible.

Deliberately a predicate rather than a Surface set: nothing is ink-only today, and would be a real surprise if it were — the ink surface is the compatibility floor and native is a superset of it. If an ink-only code ever appears, this wants to become a set rather than gain a second predicate.

Source

pub fn explanation(self) -> Option<&'static str>

The written explanation for this code, or None when nobody has written one yet (#3169).

The prose lives in docs/diagnostics/Exxx.md under ## Explanation. Every code has a file; only 31 of 189 have that section filled in, so None is the common answer and callers must render something else — Self::title is the intended fallback. Returning None rather than an empty string is deliberate: a caller that forgets to check gets a type error instead of a blank panel.

Source

pub fn is_overridable(self) -> bool

Whether [lints] can override this code’s severity (#1160).

Everything except a hard error: brink_analyzer::validate_lint_code accepts any code whose default severity is not Error, and refuses the rest with a ConfigWarning rather than applying them. You cannot allow something that stops the compile.

That deliberately INCLUDES the advisory tiers — E189, the ink TODO: note, is Info by default and is exactly the sort of thing an author wants to turn off (ruled 2026-08-27). An earlier version of this predicate said Warning only, which silently hid every Info-default code from the settings surface; the analyzer would have accepted them all along.

It also INCLUDES the compat-deny tier (#3373, RULED 2026-09-01): Self::is_compat_deny members keep severity() == Error — brink rejects the program by default, exactly as inklecate does — but stay [lints]-overridable specifically because the ruling’s admission invariant requires each member to produce a working program once downgraded. This is the one deliberate exception to “a hard error can never be downgraded”; every other Error-default code stays non-overridable.

agrees_with_the_analyzers_own_gate in brink-analyzer pins this against apply_lint_overrides itself rather than against a restated rule — the earlier mistake survived a test that compared this predicate to its own implementation.

Source

pub fn from_str_code(s: &str) -> Option<DiagnosticCode>

Parse a diagnostic code from its string representation (e.g., "E027").

Trait Implementations§

Source§

impl Clone for DiagnosticCode

Source§

fn clone(&self) -> DiagnosticCode

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for DiagnosticCode

Source§

impl Debug for DiagnosticCode

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Eq for DiagnosticCode

Source§

impl Hash for DiagnosticCode

Source§

fn hash<__H>(&self, state: &mut __H)
where __H: Hasher,

Feeds this value into the given Hasher. Read more
1.3.0 · Source§

fn hash_slice<H>(data: &[Self], state: &mut H)
where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
Source§

impl PartialEq for DiagnosticCode

Source§

fn eq(&self, other: &DiagnosticCode) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for DiagnosticCode

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> HashEqLike<&T> for T
where T: Hash + Eq,

Source§

fn hash<H>(&self, h: &mut H)
where H: Hasher,

Source§

fn eq(&self, data: &&T) -> bool

Source§

impl<T> HashEqLike<Cow<'_, T>> for T
where T: Hash + Eq + Clone,

Source§

fn hash<H>(&self, h: &mut H)
where H: Hasher,

Source§

fn eq(&self, data: &Cow<'_, T>) -> bool

Source§

impl<T> HashEqLike<T> for T
where T: Hash + Eq,

Source§

fn hash<H>(&self, h: &mut H)
where H: Hasher,

Source§

fn eq(&self, data: &T) -> bool

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Lookup<T> for T

Source§

fn into_owned(self) -> T

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more