Diagnostics
Every diagnostic is an error. There is no warning or note severity anywhere in the compiler — if it reports something, the module will not be emitted.
Format
<phase> error at <line>:<col>: <message>
type error at 2:36: field access requires a non-null declared-struct reference, found ?Point
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 five phases, and only the earliest non-empty bucket is shown:
syntax > resolution > feature > type > lowering
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.
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 hex / fractional / exponent variants) |
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,) |
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.
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 |
duplicate function name 'x' | also type, global, memory, table, tag, data |
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 (wastc-core-gc-0) | unknown or out-of-scope opcode. This is the catch-all arm, so a typo'd opcode lands here, not in resolution |
exception handling ('throw') is not enabled in the selected target feature set | throw |
tag 'T' requires exception handling, which is not enabled | tag declaration |
Type
| Message | Cause |
|---|---|
value of type X is not assignable to Y | the general mismatch |
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 … |
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 |
Global initializers
Reported at type phase, from the constant-expression checker.
| Message |
|---|
a global initializer cannot call a function |
a global initializer cannot read a local |
a global initializer may only read an imported immutable global |
this expression is not a valid constant expression for a global initializer |
'X' is not marked as a constant expression in a global initializer |
global 'X' has type A, not assignable to B |
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
These are silent. Nothing warns you.
- A narrow that doesn't apply.
isnarrows only a bare immutable local. On avar, a field, or a call result, the test still runs and the type simply does not change. - A statically impossible
istest. It folds to a constant-false condition; the true arm becomes dead code, silently. - Ignored opcode immediates.
wasm.i32.add<Junk>(a, b)discards the immediate without complaint.
Reading them in editors
wastc check <file> prints diagnostics without writing output. The LSP
publishes the same phase-ordered set on open/change/close. Because only one
phase surfaces at a time, an editor showing "1 error" on a badly broken file is
expected — the rest are hidden behind the syntax bucket.