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, E069–E071, 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 (E121–E128) 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 matchA) - 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 content — int, 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_query → chunk_lowering_ctx_query →
lir_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) = ¤t 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
impl DiagnosticCode
Sourcepub const ALL: &'static [DiagnosticCode]
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.
Sourcepub fn is_compat_deny(self) -> bool
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”).
Sourcepub fn is_native_only(self) -> bool
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.
Sourcepub fn explanation(self) -> Option<&'static str>
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.
Sourcepub fn is_overridable(self) -> bool
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.
Sourcepub fn from_str_code(s: &str) -> Option<DiagnosticCode>
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
impl Clone for DiagnosticCode
Source§fn clone(&self) -> DiagnosticCode
fn clone(&self) -> DiagnosticCode
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for DiagnosticCode
Source§impl Debug for DiagnosticCode
impl Debug for DiagnosticCode
impl Eq for DiagnosticCode
Source§impl Hash for DiagnosticCode
impl Hash for DiagnosticCode
Source§impl PartialEq for DiagnosticCode
impl PartialEq for DiagnosticCode
impl StructuralPartialEq for DiagnosticCode
Auto Trait Implementations§
impl Freeze for DiagnosticCode
impl RefUnwindSafe for DiagnosticCode
impl Send for DiagnosticCode
impl Sync for DiagnosticCode
impl Unpin for DiagnosticCode
impl UnsafeUnpin for DiagnosticCode
impl UnwindSafe for DiagnosticCode
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.