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 name | Message | Cause |
|---|---|---|
impossible-is-test | '<cond>' can never be true; the left-hand type rules it out statically | a 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' block | an is test on a var, a field, a call result, or any other compound expression |
non-narrowing-is | a type-pattern 'switch' only narrows a bare immutable local or parameter; this scrutinee won't be narrowed inside the case arms | the same restriction, reported at a type switch's scrutinee |
unused-local | local 'x' is never read | a let/var local, a for-loop variable, or a block/loop param never read after declaration |
unused-param | parameter 'x' is never read | same as unused-local, for a function parameter |
unnecessary-var | 'x' is declared 'var' but never reassigned after its initializer; 'let' would do | a var binding that's read but never written again |
unreachable-code | unreachable code: the previous statement never falls through to this point | a 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-decl | struct '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 effect | an 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-unbox | unboxing a nullable '?i31' traps on null at runtime; null-check with 'is &i31' or cast with 'wasm.ref.cast<&i31>(...)' first | unboxing 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
| Message | Cause |
|---|---|
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 literal | misplaced _ (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 [...] place | assigning 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).
| Message | Cause |
|---|---|
'comptime for' range bound must be an integer | a range bound whose compile-time value isn't i32/i64 |
'comptime if' condition must be a boolean | a 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 128 | a comptime for/comptime if nested more than 128 levels deep |
'comptime for' exceeded the maximum of 100000 total iterations across the module | the 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 name | param, 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 integer | a 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 meaning | a 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 boolean | cond ? 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' block | a 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.
| Message | Cause |
|---|---|
comptime func 'F' takes N arguments (p: kind, ...), but M were supplied | arity 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 one | a 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 range | an 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 argument | an 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 instantiated | expansion 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 entity | two comptime funcs, or a comptime func and a macro, sharing a name |
comptime func 'F' declares parameter 'P' twice | reported 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 module | the 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.
| Message | Cause |
|---|---|
macro 'X' is not defined here -- a macro must be defined before it is used | either 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 group | a 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 input | a pattern's literal separator token missing from the invocation |
macro 'X' is already defined | two 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 macro | a 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' body | str! 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>' specifier | a bare $x in a pattern, with no kind |
duplicate capture name '$x' in one arm | the 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 text | a [<...>] paste or str!(...) given a multi-token capture, which cannot form one identifier |
macro expansion nested past the maximum depth of 64 | recursion, usually a macro invoking itself unconditionally |
macro expansion exceeded the maximum of 10000 total expansions across the module | the whole module's expansion budget, not a per-macro limit |
the expansion of macro 'X' is not a valid expression | an 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:
| Message | Cause |
|---|---|
a '$(...)+' repetition needs at least 1 occurrence(s), found N | + given nothing. * accepts zero |
a '$(...)' repetition must contain at least one '$name:kind' capture | e.g. $(,)*. With no metavariable inside, nothing determines how many times the template's group should run |
a '$(...)' repetition body consumed no tokens | a 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 '$(' repetition | an 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 undefined | a template group whose body names no repeated capture |
'$a' has 2 repetition(s) but '$b' has 1 -- a '$(...)' group cannot iterate both | two 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 template | a repeated capture used as a plain $name |
'$x' is bound by a '$(...)' repetition, so it has no single value to paste or stringize | a repeated capture in a [<...>] paste or str!(...) |
'$(...)' repetition is only valid inside a macro template | a $(...) group in a comptime for/comptime if body, where no macro bindings exist |
Resolution
| Message | Cause |
|---|---|
function name 'X' must use snake_case | naming convention (also parameter, global) |
global name 'x' must use PascalCase | naming convention |
type name 'x' must use PascalCase | struct/array/functype naming |
local name 'X' must use snake_case | let/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 list | a 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 31 | the 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_case | naming 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 clause | a 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_case | a 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 array | the 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 receiver | the 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 for | the 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 site | a 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(...)' call | the 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 cycle | a 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 files | pub 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, rand | a 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 unit | a 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 param | a 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 for | unlabelled branch with no target |
If a name "obviously exists" but reports unknown, check the case first — that is the single most common cause.
Feature
| Message | Cause |
|---|---|
'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
| Message | Cause |
|---|---|
value of type X is not assignable to Y | the 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 N | two default arms |
this 'case' arm comes after the 'default' arm at line N; 'default' must be last | an arm stranded after default |
duplicate 'case' value N; it is already handled by the arm at line M | the 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 integer | a case value whose compile-time value is a bool/string |
'case' value N is outside the range of i32 | a case value too large for a table index |
function must return a value of type X on every reachable path | a reachable path falls off the end |
function 'f' expects N argument(s), found M | arity; also indirect call expects …, throw 'X' expects N argument(s), found M |
method 'T.m' expects N argument(s) after the receiver, found M | arity 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 M | arity 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 ?T | narrow the nullable first |
expected a non-null declared-array reference, found … | same, for indexing |
'null' requires a nullable-reference contextual type | null with no ?T context |
integer literal requires a contextual 'i32' or 'i64' type | untyped literal (also the float variant) |
integer literal 'N' out of range for i32 | literal exceeds the width |
global 'X' is not mutable | assign to a non-mut global |
local 'x' is not a mutable 'var' binding | assign to a let |
field 'f' is not declared 'mut' | assign to an immutable field |
array 'A' element is not mutable | assign to an immutable element |
field 'f' was not initialized / … initialized more than once | new must set every field exactly once |
a packed field or array element cannot be read without an explicit '.s' or '.u' suffix | missing suffix |
'.s'/'.u' suffixes are only valid on a packed 'i8'/'i16' field or array element | suffix on a non-packed read |
'is' requires a reference-typed left operand | testing 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 one | multi-value as a statement |
operator requires numeric operands | also '%' requires integer operands, bitwise operators require integer operands, shift operators require integer operands |
cannot determine the operand type for this operator; add a typed binding | no 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 function | a 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 throws | a throwing call is neither fully covered by active handlers nor explicitly propagated |
propagated throw 'X' is not declared by the enclosing function | call()? bubbles X, but the current function does not declare it |
'?' is unnecessary here because this call has no unhandled checked exception to propagate | every callee effect is already handled locally, or the callee is non-throwing |
'X' does not take any immediates | a <...> 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 N | wrong arity on Mem.init<...>(...)/Table.init<...>(...) |
'.drop()' takes no arguments, found N | an 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 expression | a 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.
| Message | Phase | Cause |
|---|---|---|
a global initializer cannot call a function | Type | any function call, including a postfix method call |
a global initializer cannot read a local | Type | a bare snake_case identifier |
a global initializer may only read an imported immutable global | Type | reading a defined or mutable global, via either a bare name or wasm.global.get |
unknown global or param 'X' | Resolution | a 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 initializer | Type | the 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 initializer | Feature | a structurally valid wasm.* opcode that isn't in the constant-eligible set (see Global initializers) |
global 'X' has type A, not assignable to B | Type | an eligible global read whose declared type doesn't fit the initializer's contextual type |
param 'X' has type 'i32'/'i64'/'bool', not assignable to Y | Type | a 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 expression | Type | a 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 struct | Resolution | the 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) | Type | wasm.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) | Type | wasm.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 value | Type | the 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 value | Type | the 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 test | Type | a 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 with | Syntax | new 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 U | Type | the 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 opcodes | Type or Syntax | the 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
| Message | Cause |
|---|---|
'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 Length | a 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' suffix | the 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 reference | the 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 masks | a 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.