Skip to main content

Diagnostics

Almost every diagnostic is an error: if it reports something at error severity, the module will not be emitted. A small, named set are warnings ("lints") instead — listed under Warnings (lints) below — those never block compilation and can be individually suppressed. There is no third "note" severity.

Format

<phase> <severity> at <line>:<col>: <message>
type error at 2:36: field access requires a non-null declared-struct reference, found ?Point
type warning at 4:3: 'next() is &struct' can never be true; the left-hand type rules it out statically

<severity> is error or warning. Line and column are 1-indexed. The position marks where the construct starts — spans have no end, so nothing points at a range.

Types in messages use source spelling (&Foo, ?any, i32), never WAT spelling ((ref $Foo)).

Only one phase is ever reported

Diagnostics are bucketed into six phases, and only the earliest non-empty bucket is shown:

syntax > expansion > resolution > feature > type > lowering

expansion covers compile-time param/comptime for/comptime if/[<...>]/ str!(...) (see Compile-time parameters and control flow) — it runs after parsing and before resolution, so an expansion error on a module with syntax errors elsewhere is still suppressed by the syntax bucket above it, and an expansion error suppresses resolution/feature/type/lowering errors the same way a syntax error does.

Later-phase errors on a broken file are almost always cascades, so they are suppressed entirely. Practical consequence: fixing all your syntax errors can reveal a completely new set of type errors. The count going up is normal, not a regression.

Phases are per-diagnostic, not per-pass — the type-checker emits diagnostics tagged resolution and feature too.

This phase filter applies to errors only. A warning is never suppressed by it and never counts toward "is this phase non-empty" — a warning is only ever produced once its phase has already run to completion, so there's nothing after it to be a cascade of.

Warnings (lints)

Some diagnostics are warnings, not errors — the module still compiles and wat is still produced. Every warning is also a lint: a stable, individually-suppressible name. Pass --allow <name> (repeatable) to reedc build/check to suppress one:

$ reedc build foo.reed --allow unused-local --allow self-assignment

An unknown name is rejected before compilation is attempted, printing the full valid-name list. All lints are Type-phase and default to enabled; there is no way to elevate one to an error, and no source-level #[allow(...)]-style annotation — suppression is CLI-only, per whole file.

The following five lints render faded/dimmed in editors that support LSP's DiagnosticTag (e.g. VS Code): unused-local, unused-param, unnecessary-var, unreachable-code, and unused-decl. In other editors, they appear as regular warnings.

Because an editor dims exactly the range a diagnostic reports, the four unused-flavoured lints deliberately report at the declared name rather than at the whole declaration: unused-decl points at the name after func/struct/global/…, and unused-local/unused-param/unnecessary-var point at the bound name rather than at the whole let/var statement or name: Ty parameter. Dimming a function body or an initializer would read as "this code never runs", which is a different and wrong claim — an unused function's body is perfectly live once something calls it. unreachable-code is the exception, and correctly so: there the whole statement really is dead.

Lint nameMessageCause
impossible-is-test'<cond>' can never be true; the left-hand type rules it out staticallya statically impossible is test (its then arm is dead code)
non-narrowing-is'<cond>' only narrows a bare immutable local or parameter; this operand won't be narrowed inside the 'then' blockan is test on a var, a field, a call result, or any other compound expression
non-narrowing-isa type-pattern 'switch' only narrows a bare immutable local or parameter; this scrutinee won't be narrowed inside the case armsthe same restriction, reported at a type switch's scrutinee
unused-locallocal 'x' is never reada let/var local, a for-loop variable, or a block/loop param never read after declaration
unused-paramparameter 'x' is never readsame as unused-local, for a function parameter
unnecessary-var'x' is declared 'var' but never reassigned after its initializer; 'let' would doa var binding that's read but never written again
unreachable-codeunreachable code: the previous statement never falls through to this pointa statement after one that unconditionally diverges (return/break/continue/br/throw, or a both-arms-diverge if/non-terminating loop). Still checked and lowered exactly as before — same "dead code still compiles" precedent as impossible-is-test — and fires once per dead stretch, not once per statement in it
unused-declstruct 'X' is never used (also array/function type/global/function/table/memory/tag)a module-level declaration never referenced anywhere, and neither exported, start, nor pub. A pub declaration counts as a use: it is the author's statement of the intended surface, so reporting it would be a warning with no available action -- the fix is never to delete it. That is the library-root case, where a file's whole purpose is an API nothing in its own unit calls. Deliberately shallow (non-transitive) — see Limitations if this surprises you on a type that's only referenced by another also-unused type. A tag is marked used by any throw naming it. data/elem declarations are still never checked by this lint — they're consumable (.init<...>(...), .drop(), and segment-sourced new Name[n] data<D>(...)/elem<E>(...)), but this lint isn't wired to track those uses yet — a real, open gap, not a design choice
self-assignment'x = x;' has no effectan assignment whose left and right side are the same bare identifier (local or global). foo->bar = foo->bar;/a[i] = a[i]; are deliberately out of scope — proving those are really a no-op would mean reasoning about whether the receiver/index expressions are side-effect-free
nullable-i31-unboxunboxing a nullable '?i31' traps on null at runtime; null-check with 'is &i31' or cast with 'wasm.ref.cast<&i31>(...)' firstunboxing a statically nullable ?i31 via as i32.s/as i32.u without narrowing to &i31 first (via is) or casting it away
escape-hatch-alternative'wasm.<opcode>' has dedicated syntax: write '<syntax>' instead (<what>)a wasm.<opcode>(...) call whose instruction is also reachable through ordinary Reed syntax — an operator, an as cast, a method call, #, new, or is. Only reported when the replacement is exact: never for an unsigned operator (/ lowers to div_s), never for reinterpret_* (no dedicated form), never inside a global initializer (operators are not constant expressions there), and never where the replacement would reject a nullable receiver the escape hatch accepts
deprecated-call'f' is deprecated: <note> (at a use), or deprecated function 'f' is exported: <note> (at the declaration)a use of a function marked @deprecated. Fires for every route that names it: a call in either method spelling, a direct call, &name, wasm.call<f>/wasm.ref.func<f>, an export, start, a table or elem entry, and a global initializer. The : <note> half appears only when the annotation supplied one. A use inside a function that is itself @deprecated is exempt, since there is no migration to make there
packed-truncation<n> does not fit '<Type>.<field>' (<uN/iN/bool>, <low>..=<high>); it will be stored as <m>a compile-time-known value written to a packed struct field too narrow to hold it, through either write path (new Name { ... } or place = value). The write still masks to the field's width, which is the documented behaviour and what makes it three instructions rather than a branch — the lint only reports what the compiler already computed. A runtime value that overflows is never reported: the compiler cannot know, and rejecting the whole class would make masking unusable for the callers who rely on it

A name starting with _ is exempt from unused-local/unused-param/ unnecessary-var (the same convention as Rust) — no CLI flag needed for a deliberately-unused binding.

reedc build/check print these to stderr but exit successfully; the LSP reports them at DiagnosticSeverity::WARNING; the playground shows them alongside the compiled WAT instead of only on failure. See CLAUDE.md's "Lints" section if you're adding another one.

Syntax

MessageCause
expected an expression, found …generic parse failure
expected a declaration, found …junk at top level
expected identifier, found …missing or malformed name
unexpected character '…'not a valid token
unterminated string literal / … (line break)missing closing "
invalid numeric separator in decimal literalmisplaced _ (also hexadecimal / binary / fractional / exponent variants)
invalid integer literal '0b12'a digit outside the literal's radix. The whole literal is rejected rather than split at the bad digit, which would silently retokenize 0b12 into two valid tokens meaning something else
left-hand side of assignment must be a local name, a ->field place, or an indexed [...] placeassigning to a non-place
expected 'null', '&Type', or '?Type' after 'is'malformed type test
a tuple expression requires at least two elements(x,)
expected 'case', 'default', 'inline', or '}' in a 'switch' body, found …junk between switch arms
unexpected end of input inside a 'switch' body (missing '}')unclosed switch body
'unreachable' is a keyword, not a value or a function …a call or postfix on unreachable (unreachable(), unreachable->x)

Known wart: unhandled token kinds fall through to Rust Debug formatting, so you will occasionally see internal spellings like StringLit("x") or LParen in a message.

Expansion

Compile-time param/comptime for/comptime if/[<...>]/str!(...) (see Compile-time parameters and control flow) and macro definition/expansion (see Macros).

MessageCause
'comptime for' range bound must be an integera range bound whose compile-time value isn't i32/i64
'comptime if' condition must be a booleana condition whose compile-time value isn't bool
'X' is not a compile-time-known value here (expected a 'param' or an enclosing 'comptime for' loop variable)long form, fires only for a comptime for range bound or a comptime if condition: a local, global, or call result read where only a param, an enclosing comptime for's loop variable, or a literal is allowed
'X' is not a compile-time-known value here (no parenthetical)short form, fires instead for $name/[<...>]/str!(...) substitution: X isn't a declared param or an enclosing comptime for's loop variable. The most common trigger in practice — usually a typo in a splice/paste/stringize argument, e.g. $mystery where mystery was never declared
'$name' is a bool/string param and can't be spliced directly into an expression -- use it in 'comptime if', '[<...>]', or 'str!(...)' instead$name on a bool/string param, which has no runtime-literal form to splice as
paste produced 'X', which is not a valid identifier[<...>] produced text that isn't a valid identifier, e.g. one starting with a digit
compile-time expansion nested past the maximum depth of 128a comptime for/comptime if nested more than 128 levels deep
'comptime for' exceeded the maximum of 100000 total iterations across the modulethe whole module's comptime for iteration budget, not a per-loop limit
--define value 'X' is not a valid integer in range for 'i32' / ... for 'i64' / --define value 'X' is not 'true' or 'false'a malformed or out-of-range --define NAME=VALUE. The integer form is range-checked against the param's declared width, so --define N=5000000000 against an i32 param is rejected exactly like the equivalent source-level default would be
'comptime for' here was not expanded: a 'comptime for'/'comptime if' inside a 'block'/'loop' expression's body is not supported -- move it outside that body, or restructure (and the 'comptime if' variant)the one place expansion doesn't reach: a comptime for/comptime if inside a block/loop used as a value (a var initializer, a return operand, a call argument). A block/loop written as a bare statement is walked into. See Compile-time parameters and control flow
'X' is already declared as a param at line N; a const cannot reuse that nameparam, const, and enum members share one compile-time value table (see const), so a duplicate name would silently overwrite the earlier binding -- and since a param may be --define-overridden, which value won would depend on declaration order alone
'X' is declared 'bool' but its value is an integera const whose initializer evaluates to a different type than declared
value N of 'X' is out of range for 'i32'an i32 const or enum member whose value does not fit, checked exactly as a param's in-source default is
overflow in a compile-time multiplication (also addition/subtraction/division/shift)compile-time arithmetic is exact rather than wrapping, since a compile-time value has no width until it meets its declared type
shift count N is out of range for a compile-time shift (0..63)WebAssembly's shift masks the count to the operand's width; a compile-time shift has no width to mask against, so an out-of-range count is a mistake rather than a silent 1
'>>>' on the negative value N has no compile-time meaninga logical right shift is defined by the operand's width. As an i64, -8 >>> 28 is 68719476735; as an i32 it is 15. Mask to a width first, or use >>
a compile-time conditional's condition must be a booleancond ? a : b in a compile-time expression whose condition is not bool
enum 'Color' already has a member named 'Red'two members of one enum share a name; both would key the same binding
enum member 'Color.red' must be PascalCase (section 3)naming convention, reported at the enum declaration since the binding's key is the dotted name
enum member 'Edge.Next' has no implicit value: the previous member is already the maximum of 'i32'an implicit member takes the previous value plus one, which has no answer at the representation's maximum. Reported against the member that lacks a value, not the legal maximum before it
unexpected end of input inside a 'comptime' blocka comptime for/comptime if/comptime func body whose closing } was never reached. Reported rather than looped on, per this compiler's bail-at-EOF rule — but the declaration is kept, since this is the ordinary state of a construct being typed. The editor keeps answering inside it: the generator stays in the outline, and hover and completion still work on its parameters and body

Compile-time functions

Instantiation-time checks for comptime func (see Generics). Every one of these names the parameter that rejected the argument, which is the whole reason the feature exists separately from macro.

MessageCause
comptime func 'F' takes N arguments (p: kind, ...), but M were suppliedarity mismatch; the message lists the parameter list so the fix is visible without scrolling back to the definition
comptime func 'F' parameter 'P' expects a type, but 'X' is not onea type argument that does not parse as a complete type. A macro's $T:ty is shape-matched instead, so the equivalent mistake there surfaces inside the expansion
comptime func 'F' parameter 'P' expects a single identifier, found 'X'an ident argument that is not exactly one identifier token
comptime func 'F' parameter 'P' expects i32, found a boolean (and the other kind pairings)a value argument whose comptime type is not the declared one
comptime func 'F' parameter 'P' expects i32, but N is out of rangean integer argument outside the declared width, checked exactly as --define and a param's in-source default are
comptime func 'F' parameter 'P' (kind) was given an empty argumentan argument position with no tokens in it. Note a trailing comma is not this case: f(a,) splits into two arguments, the second empty, so it is reported as an arity mismatch -- which is the honest reading, since it really is a different token sequence from f(a)
comptime func 'F' is not defined here -- it must be defined before it is instantiatedexpansion is a single forward pass, so a generator must precede its use, like a macro. When the name is a macro, the message adds -- 'F' is a macro, so it is invoked as 'F!(...)'
comptime func 'F' instantiates itself (a -> b -> a)direct or mutual recursion between generators; the message names the whole chain
'F' is already defined as a compile-time entitytwo comptime funcs, or a comptime func and a macro, sharing a name
comptime func 'F' declares parameter 'P' twicereported at the definition rather than at a call: the second binding would silently win, so every instantiation would misbehave identically
comptime func 'F' must be snake_case (section 3)a generator is invoked like a call and expands to code, so it lives in the snake_case namespace
comptime instantiation exceeded the maximum of 1000 instantiations across the modulethe whole module's instantiation budget

A generator's body is ordinary Reed, so a mistake inside one is reported by the ordinary phase for that mistake, at the instantiation's own span. array 'XStorage' cannot be default-initialized: element type &Point has no default value is the one to expect most often: a vec_of element type must be defaultable, so use ?Point.

A handful of narrower token-form mistakes inside $name/[<...>]/str!(...) (an unterminated [<, a $ with no name after it, an unclosed str!() round out this phase but are omitted here as self-explanatory from the message text itself.

Macros

Reported when a macro is defined (a malformed pattern) or when one is invoked (a failed match). Both are expansion-phase: the tokens parsed fine.

MessageCause
macro 'X' is not defined here -- a macro must be defined before it is usedeither a typo, or a definition placed after its use. Macros are the one order-dependent declaration kind, since expansion is a single forward pass
no arm of macro 'X' matches this invocation (arm 1: ...; arm 2: ...)no arm's pattern matched. Every arm's own reason is listed, in order, so you can see which one you meant. Each reason is one of the per-capture failures below
'$x:ident' needs an identifier, found '5'an ident capture given something else
'$v:literal' needs a literal, found identifier 'name'a literal capture given a non-literal
'$t:tt' has an unbalanced groupa tt capture starting a (/[/{ that never closes
'$a:expr' matched nothing before ','a zero-width expr capture — usually a missing argument, e.g. m!(, 2). Never silently accepted, since a zero-width match is a mis-parse
unexpected extra argument tokens starting at identifier 'b'the pattern matched but arguments remained, e.g. m!(a b) against ($n:ident)
expected ',', found end of inputa pattern's literal separator token missing from the invocation
macro 'X' is already definedtwo definitions sharing a name
macro 'X' must be snake_case (section 3)a macro expands to code and is invoked like a call, so it lives in the function/local namespace
'str' is reserved: 'str!(...)' is built-in compile-time stringization, not a macroa macro str definition, which would shadow a built-in that is never dispatched through the macro table
'str!(...)' is only valid inside a 'comptime for'/'comptime if' bodystr! used where no compile-time value exists to stringize. Previously a bare syntax error
unknown fragment kind 'X' (expected one of expr, ident, literal, tt, ty)a capture's :kind is not one of the five specifiers
capture '$x' needs a ':<kind>' specifiera bare $x in a pattern, with no kind
duplicate capture name '$x' in one armthe same capture name twice in one pattern
'$a:expr' is immediately followed by '$b:expr' with no separator token between them; add one (e.g. ',')two adjacent captures. An expr scan needs a following token to stop at, so the split would be arbitrary — rejected at definition time rather than mis-matched at a call
'$x' captured more than one token, which cannot be used as identifier texta [<...>] paste or str!(...) given a multi-token capture, which cannot form one identifier
macro expansion nested past the maximum depth of 64recursion, usually a macro invoking itself unconditionally
macro expansion exceeded the maximum of 10000 total expansions across the modulethe whole module's expansion budget, not a per-macro limit
the expansion of macro 'X' is not a valid expressionan expression-position invocation whose template does not form a single expression
unexpected end of input inside a macro arm's pattern / ... inside a macro invocation's arguments / ... inside 'macro X'an unclosed (/{. Reported rather than looped on, per this compiler's bail-at-EOF rule

Repetition ($(...)* / $(...)+) adds these:

MessageCause
a '$(...)+' repetition needs at least 1 occurrence(s), found N+ given nothing. * accepts zero
a '$(...)' repetition must contain at least one '$name:kind' capturee.g. $(,)*. With no metavariable inside, nothing determines how many times the template's group should run
a '$(...)' repetition body consumed no tokensa body that can match zero tokens, reachable via a nested repetition with no separator on the outer one. Without this check the match would loop forever, so it is reported instead
'$(...)' must be followed by '*' or '+'a $(...) group with no repetition operator
unclosed '$(' repetitionan unbalanced $( in a pattern. In practice the parser's own paren balance usually reports first
a '$(...)' template group must mention at least one repeated '$name' -- otherwise the number of repetitions is undefineda template group whose body names no repeated capture
'$a' has 2 repetition(s) but '$b' has 1 -- a '$(...)' group cannot iterate bothtwo repeated names of different lengths in one group. Both are named, since either could be the mistake
'$x' is bound by a '$(...)' repetition, so it can only be used inside a '$(...)' group in the templatea repeated capture used as a plain $name
'$x' is bound by a '$(...)' repetition, so it has no single value to paste or stringizea repeated capture in a [<...>] paste or str!(...)
'$(...)' repetition is only valid inside a macro templatea $(...) group in a comptime for/comptime if body, where no macro bindings exist

Resolution

MessageCause
function name 'X' must use snake_casenaming convention (also parameter, global)
global name 'x' must use PascalCasenaming convention
type name 'x' must use PascalCasestruct/array/functype naming
local name 'X' must use snake_caselet/var/for-loop variable
unknown local 'x'undefined, out of scope, or wrong case
unknown global 'X' / unknown function 'x' / unknown type 'X'undefined name
unknown global or param 'X'a bare PascalCase Ident read in an ordinary expression or a global initializer that matches neither Env.global_names nor Env.params — the read-path counterpart of unknown global 'X', which a param read also goes through (see Compile-time parameters and control flow); constexpr.rs's global-initializer checker reports the identical message for the same situation, see Global initializers below
unknown type alias or packed struct 'X'a bare Name in a type position. Only those two are legal there: a struct, array, or function type needs its reference sigil (&Name/?Name), which is the likelier mistake
'X' is a declared heap type, so it needs a reference sigil: write '&X' or '?X'the sigil-less spelling of a struct, array, or function type
unknown enum 'X' / enum 'Color' has no member 'Blue'; its members are 'Red', 'Green'a member path naming no enum, or no such member. The second lists the real members, since Color.Blue and a method call Color.blue(x) are the same shape and the reader needs to know which half was wrong
'Color.m' is a method, not an enum member -- call it with an argument lista method named through the enum-member spelling, with its arguments omitted
packed struct 'P' already has a field named 'a'duplicate field in a packed struct
packed field 'P.a' must be at least 1 bit wide / ... is N bits, wider than 'i31' can hold (31)a packed field's width. i31's capacity is 31, not 32: an i31 value is a WebAssembly reference and the 32nd bit does not exist
packed field 'P.b' does not fit: it needs bits 16..32 but 'i31' holds only 31the running total overflowed. Reported at the field that overflows, with its bit range, rather than naming only the total at the declaration
packed field name 'X' must use snake_casenaming convention, matching an ordinary struct field
packed struct 'P' has no field 'y'; its fields are 'x'unknown field on a packed receiver
unknown data segment 'X' / unknown elem segment 'X'undefined segment name, e.g. in .init<...>(...) or new Name[n] data<...>(...)/elem<...>(...)
unknown tag 'X'throw of an undeclared tag name
unknown tag 'X' in throws clausea function, function type, or imported function declares an effect whose tag does not exist
duplicate thrown tag 'X'the same tag appears twice in one throws (...) clause
duplicate function name 'x'also type, global, memory, table, tag, data, elem; declaring a default-core qualified name such as i32.clz is a duplicate too
duplicate method name 'i32.x'two methods with the same receiver and name (Methods)
method name 'X' must use snake_casea method's name after the . follows the function convention; its qualifier follows its own type's
'X' in method name 'X.m' is not a type -- a method's receiver must be a value type (i32, i64, f32, f64, v128) or a declared struct or arraythe qualifier names nothing, or names a function type (a signature, not a value with members)
method 'T.m' must take a first parameter of type 'T' (the receiver, conventionally named 'self')a method with no parameters — the receiver is parameter zero
method 'T.m' declares its first parameter as X, but its receiver type is Y -- a method's first parameter is the receiverthe first parameter's type must be exactly the receiver type: the value type itself, or the non-null reference for a struct/array
method 'T.m''s first parameter must be named 'self'a defined method only; a func import declares types with no names
method 'T.m' receives X, so its receiver shorthand is '&self', not 'self' (and the reverse)the self shorthand's sigil must match the receiver's kind: &self for a struct or array, plain self for a value type or packed struct
'&self' is only meaningful in a method: this function's name has no receiver qualifier, so there is no type for it to stand forthe shorthand stands in for whatever the qualifier denotes, so an unqualified func has nothing for it to mean
a method's receiver is never nullable, so there is no '?self' -- write '&self' and narrow the receiver at the call sitea nullable type has no methods at all, so there is nothing a ?self could declare
'copy' is a reserved method-postfix name, so a method named 'Bytes.copy' could never be reached by a '.copy(...)' callthe compiler-reserved method forms are resolved without consulting declared methods, so such a declaration would be unreachable
'm' is not declared in 'util.reed', but the method 'T.m' is -- import it by its full name, 'T.m'pub makes a method visible under its qualified name, so an import must spell it that way (Methods). Only suggested when exactly one visible method matches the bare name
macro 'm' is declared, but after this use -- move the definition earlier, since expansion is a single forward pass. If it is in another file, the imports form a cycle: that is allowed for every declaration kind except macros, so the macro has to move to a file outside the cyclea macro used before its declaration. Distinguished from "no such macro" because the fixes differ: within one file, move the declaration up; across a cycle, no placement works and the macro must move out of the cycle (Circular imports)
'X' is not visible here. It is declared in 'deep.reed'. Importing a file does not re-export what that file itself imported; import it directly, or make that file's import 'pub' (spec 5.1)a name reached through another file's import. Imports are not transitive: the declaration is in the emitted module, since a unit is one flat namespace, but visibility is per file. Either import the declaring file, or mark the intermediate file's import pub to re-export it
'pub' is not allowed on a WebAssembly import (it is already named by the host) -- only a named declaration can be made visible to other filespub on a module import means re-export, but a host import's name comes from the host rather than from a file, so there is nothing to re-export
no standard library module 'X'; available modules are: array, math, bits, ascii, str, list, sort, map, set, randa use std.<module> naming a module that does not exist. The std path root is resolved against the modules built into the compiler, never against the filesystem (Standard library)
'X' is already declared in 'std.list'. Reed compiles every file into one WebAssembly module with source-derived names, so a name must be unique across the whole compilation unita declaration colliding with an imported standard library name. The library shares your flat namespace and is not mangled; import only the names you need, or rename yours
'X' is already declared as a parama global reusing the name of an already-declared param — a param's name and a global's name share one namespace, since a bare PascalCase Ident must resolve to exactly one of them
struct 'S' has no field 'f'unknown field
struct 'S' redeclares field 'f'shadowing an inherited field — not allowed
cyclic struct inheritance involving 'S'parent links must be acyclic
unknown label 'l'branch to an out-of-scope label
'break' outside of a loop or forunlabelled branch with no target

If a name "obviously exists" but reports unknown, check the case first — that is the single most common cause.

Feature

MessageCause
'X' is not available in the selected target's instruction catalog (reedc-core-gc-eh-simd-threads-1)unknown or out-of-scope opcode. This is the catch-all arm, so a typo'd opcode lands here, not in resolution

Type

MessageCause
value of type X is not assignable to Ythe general mismatch
this 'switch' has no 'default' arm; …a switch with no default (required, since br_table needs a default target)
duplicate 'default' arm; this 'switch' already has one at line Ntwo default arms
this 'case' arm comes after the 'default' arm at line N; 'default' must be lastan arm stranded after default
duplicate 'case' value N; it is already handled by the arm at line Mthe same value named twice
'case' value must be compile-time-known …a case value that reads a local, global, or call result
'case' value must be an integera case value whose compile-time value is a bool/string
'case' value N is outside the range of i32a case value too large for a table index
function must return a value of type X on every reachable patha reachable path falls off the end
function 'f' expects N argument(s), found Marity; also indirect call expects …, throw 'X' expects N argument(s), found M
method 'T.m' expects N argument(s) after the receiver, found Marity at a receiver.m(...) call, which supplies the receiver separately from the argument list
method 'T.m' called by name expects N argument(s) (including the receiver), found Marity at a T.m(receiver, ...) call, where the receiver is an ordinary first argument
no method 'm' on &T -- declare one as 'func T.m(self: &T, ...)'a .m(...) call whose receiver type declares no such method (Methods)
method '.m(...)' requires a non-null &T, found ?T -- narrow the receiver first (e.g. with an 'is' test)the method exists on the non-null type; a nullable receiver has none
field access requires a non-null declared-struct reference, found ?Tnarrow the nullable first
expected a non-null declared-array reference, found …same, for indexing
'null' requires a nullable-reference contextual typenull with no ?T context
integer literal requires a contextual 'i32' or 'i64' typeuntyped literal (also the float variant)
integer literal 'N' out of range for i32literal exceeds the width
global 'X' is not mutableassign to a non-mut global
local 'x' is not a mutable 'var' bindingassign to a let
field 'f' is not declared 'mut'assign to an immutable field
array 'A' element is not mutableassign to an immutable element
field 'f' was not initialized / … initialized more than oncenew must set every field exactly once
a packed field or array element cannot be read without an explicit '.s' or '.u' suffixmissing suffix
'.s'/'.u' suffixes are only valid on a packed 'i8'/'i16' field or array elementsuffix on a non-packed read
'is' requires a reference-typed left operandtesting a number
reaching the end of this control body is only valid when its result type is '()'non-unit block/loop falls through
'return;' is only valid for a function with result '()'bare return in a value function
binding declares N name(s) but M type(s)destructuring arity
expression statement produces N results; expected zero or onemulti-value as a statement
operator requires numeric operandsalso '%' requires integer operands, bitwise operators require integer operands, shift operators require integer operands
cannot determine the operand type for this operator; add a typed bindingno contextual type reached the expression
function 'f' does not exactly match the signature of function type 'T'&f — function references are invariant
throw 'X' is not caught here and is not declared by the enclosing functiona direct throw or rethrow escapes every active handler but the current function omits X from throws (...)
call may throw X; catch it here or write '?' to propagate declared throwsa throwing call is neither fully covered by active handlers nor explicitly propagated
propagated throw 'X' is not declared by the enclosing functioncall()? bubbles X, but the current function does not declare it
'?' is unnecessary here because this call has no unhandled checked exception to propagateevery callee effect is already handled locally, or the callee is non-throwing
'X' does not take any immediatesa <...> immediate on an opcode that structurally takes none (wasm.i32.add<Junk>(a, b))
integer immediate 'X' is out of range for 'Y'also covers a --prefixed immediate whose magnitude has no two's-complement representation at that width
'.init<...>(...)' expects 3 arguments (dest_offset, src_offset, len), found Nwrong arity on Mem.init<...>(...)/Table.init<...>(...)
'.drop()' takes no arguments, found Nan argument passed to SomeData.drop()/Handlers.drop()
data segment 'X' only supports '.drop()' / elem segment 'X' only supports '.drop()'any method-postfix other than .drop() on a declared data/elem segment name
'.init<...>(...)' source elem segment 'E' element type ... is not assignable to destination table 'T' element type ...Table.init<E>(...) where E's element type doesn't match the table's
param 'X' has type 'string', which has no runtime value and cannot be used as a value expressiona string param's bare name read in an ordinary expression or a global initializer — a string param has no runtime representation, so it's only usable inside $name/[<...>]/str!(...) splicing, never as a value

Global initializers

All from the constant-expression checker (constexpr.rs), but not all one phase — despite living under this page's "Type" heading for historical reasons, several of these are Syntax- or Resolution-phase, so a syntactically-broken global initializer still reports at the earlier phase per "Only one phase is ever reported" above.

MessagePhaseCause
a global initializer cannot call a functionTypeany function call, including a postfix method call
a global initializer cannot read a localTypea bare snake_case identifier
a global initializer may only read an imported immutable globalTypereading a defined or mutable global, via either a bare name or wasm.global.get
unknown global or param 'X'Resolutiona bare PascalCase Ident matching neither Env.global_names nor Env.params — constexpr's own copy of the same check check_ident does for an ordinary expression (see the Resolution section above)
this expression is not a valid constant expression for a global initializerTypethe catch-all: anything not a literal, null, an eligible identifier read, an eligible wasm op, an as &i31 cast, or new/array-default construction
'X' is not marked as a constant expression in a global initializerFeaturea structurally valid wasm.* opcode that isn't in the constant-eligible set (see Global initializers)
global 'X' has type A, not assignable to BTypean eligible global read whose declared type doesn't fit the initializer's contextual type
param 'X' has type 'i32'/'i64'/'bool', not assignable to YTypea param read whose declared type doesn't fit the initializer's contextual type — the global-initializer counterpart of the ordinary-expression case, which just reports value of type X is not assignable to Y instead since it goes through the general expression checker
param 'X' has type 'string', which has no runtime value and cannot be used as a value expressionTypea string param's bare name read inside a global initializer — same message and same restriction as reading it in an ordinary expression (see the Type section above)
'struct.new'/'struct.new_default' immediate must name a structResolutionthe type-name immediate resolves to a non-struct type (also 'array.new'/'array.new_default'/'array.new_fixed' immediate must name an array)
'struct.new_default' requires every field to have a defaultable type (field 'f' has type T, which has no default value)Typewasm.struct.new_default<S>() (or the new S {} sugar lowering to it) where S has a non-null reference field with no default value
'array.new_default' requires a defaultable element type (found T)Typewasm.array.new_default<A>(len) (or the new A[len]{} sugar) where A's element type is a non-null reference
struct 'S' cannot be default-initialized: field 'f' has non-null reference type T, which has no default valueTypethe new S {} sugar's own defaultability check, worded slightly differently from the raw struct.new_default message above since it names the sugar form, not the opcode
array 'A' cannot be default-initialized: element type T has no default valueTypethe new A[len]{} sugar's own defaultability check, mirroring the struct case above. The filled form new A[len]{v} has no such requirement, since it supplies a value
packed struct 'W' is represented as 'i32', which is not a reference, so there is nothing for 'is' to testTypea type test naming an i32/i64-represented packed struct. Only : i31 has a runtime identity to test
a packed struct value is never null, so there is no '?T' to test for -- write '&T'Type?Tag in a type test. Rejected rather than ignored: the narrowed type is non-null either way, so tolerating it would make the sigil silently meaningless
a sized array construction takes at most one element, the value every slot is filled withSyntaxnew A[n]{a, b} -- a [count] alongside a full element list is either redundant or contradictory. Drop the count to list elements (new A{a, b}), or keep it and give one fill value
'struct.new'/'struct.new_default'/'array.new'/'array.new_default'/'array.new_fixed' produces T, not assignable to UTypethe allocation's produced type doesn't fit the initializer's contextual type
'ref.i31'/'ref.null'/'ref.func' produces ..., and the various expects N argument(s)/expects no arguments/expects exactly one ... immediate shape errors on the same opcodesType or Syntaxthe usual per-opcode arity/immediate-shape checks (see the Type section above), re-run here since a global initializer's wasm.* op is checked independently of a function body's

Packed structs and enums

MessageCause
'Color' is an enum, not a value -- name one of its members, as 'Color.Member'a bare enum name in a value position. Worth its own check: the name is bound as a placeholder for the duplicate-name test, so without it the read compiled silently to i32.const 0
value of type Weight is not assignable to Lengtha packed struct is nominal, so identical layouts are still distinct types. That is the feature: a raw integer reaching a field extraction would read unrelated bits as structured data
'P' is represented as i32, so it converts only to that; found 'as i64'as on a packed value yields its representation's bits and nothing else; a different width would truncate or extend a bit pattern
packed field 'P.x' is declared 'u4', so its signedness is already fixed -- drop the '.s'/'.u' suffixthe suffix belongs to packed i8/i16 storage fields, where the same storage admits both readings. Here the declaration has already chosen
local 'f' is not mutable; declare it with 'var' (on f->field = ...)assigning to a packed field is a read-modify-write of the place, so it needs one that can be written back
cannot assign to a field of this 'P': a packed value is a value, not a referencethe receiver is not a place at all -- a GC struct field, an array element, or a parameter. Rebuild with new
'new P' is not a constant expression: building a packed value needs shifts and masksa global initializer. Write the bits directly, as <bits> as P

Lowering

Only three exist — all module-assembly limits, all reported last.

Message
memory32 limits must not exceed 65536 pages
table32 limits must fit in an unsigned 32-bit integer
active data segment 'D' requires exactly one memory in the module, found N

What produces no diagnostic at all

Nothing currently known. The narrow-that-doesn't-apply and impossible-is-test cases used to be silent and are now the two warnings above; the ignored- immediate case used to be silent and is now the 'X' does not take any immediates error above. See Limitations for the fuller history if you're debugging output built before those fixes.

Reading them in editors

reedc check <file> prints diagnostics without writing output, and exits successfully if the only diagnostics are warnings. The LSP publishes the same phase-ordered set on open/change/close, at DiagnosticSeverity::ERROR or ::WARNING to match. Because only errors are filtered to one phase, an editor showing "1 error" on a badly broken file is expected — the rest are hidden behind the syntax bucket. Warnings never appear next to an error, though, for a related but distinct reason: they're only ever collected once every function has typechecked with zero errors, so a module with any error reports errors only (the phase-filtered set), never a mix of both severities.

The playground has its own checkbox list for toggling individual lints (a "Lints (N/8)" menu in the toolbar) — the browser-only equivalent of the CLI's --allow, useful for seeing what a lint actually flags without leaving the editor.