WAST Language Specification
1. Status and Terms
This document specifies WAST, a typed source language which compiles to a single WebAssembly module. It is intentionally close to WebAssembly while providing named declarations, expression syntax, structured control flow, and automatic GC type construction.
The words MUST, MUST NOT, SHOULD, and MAY are normative.
An implementation MUST report a source diagnostic for any program that does not satisfy this specification. It MUST NOT emit a module for a program with a semantic error.
Invalid programs are classified as syntax, resolution, feature, type, or lowering errors. Implementations MUST perform those checks in that order, and MUST report all errors found in each completed phase. A lowering error is raised when a well-typed source construct has no valid WebAssembly representation under the selected target features.
2. Target
The initial target feature set is:
- WebAssembly core instructions and value types, excluding SIMD instructions.
- Multi-memory with 32-bit memory indexes and 32-bit addresses.
- Multi-value functions.
- Reference types.
- Garbage-collection types: structs, arrays,
i31, casts, tests, and recursive type groups. - Typed low-level operations, as described in section 12.
Exception handling, SIMD, threads, memory64, runtime strings, and other WebAssembly proposals are outside the initial target. Tail calls are an optional output mode, as specified in section 13; they are not a source feature. Their syntax MAY be reserved, but implementations MUST reject their use until that feature is enabled.
A compilation unit produces exactly one WebAssembly module. Linking modules is the responsibility of the embedding environment through imports and exports.
3. Lexical Structure
Source is UTF-8. Identifiers and keywords use ASCII characters in the initial version.
Whitespace separates tokens and is otherwise insignificant. Line comments
begin with // and continue through the end of the line. Block comments are
not part of the initial language.
An identifier begins with an ASCII letter or _ and is followed by zero or
more ASCII letters, digits, or _. The following words are reserved:
array as block break continue data else export false for func global if import
in is let loop memory mut new null return start struct table throw true type var wasm
The literals null, integer literals, floating-point literals, and string
literals follow the token grammar below. String literals are permitted in
imports, exports, and data declarations.
literal ::= "null" | "true" | "false" | integer-literal | float-literal
integer-literal ::= decimal-integer | hexadecimal-integer
decimal-integer ::= decimal-digit ("_"? decimal-digit)*
hexadecimal-integer ::= "0x" hexadecimal-digit ("_"? hexadecimal-digit)*
float-literal ::= decimal-float
decimal-float ::= decimal-integer "." decimal-integer? exponent?
| decimal-integer exponent
exponent ::= ("e" | "E") ("+" | "-")? decimal-integer
string-literal ::= '"' string-character* '"'
string-character ::= any Unicode scalar value except `"`, `\\`, or a line break
| `\\"` | `\\\\` | `\\n` | `\\r` | `\\t`
| `\\u{` hexadecimal-digit{1,6} `}`
true is the i32 value 1 and false is the i32 value 0. Integer and
floating-point literals require a contextual destination type. An integer
literal is valid only for i32 or i64. A non-negative integer must be at
most 2^width - 1. Negation is parsed as the unary - operator. The emitted
integer is the corresponding width-bit pattern. A floating literal is valid
only for f32 or f64 and rounds using the
corresponding IEEE 754 conversion. A literal without a contextual type is a
type error. Implementations MUST apply maximal-munch tokenization to
multi-character punctuation, including ->, .., >>>, >>, <<, <=,
>=, ==, !=, &&, and ||.
4. Types
4.1 Value Types
The scalar value types are i32, i64, f32, and f64. i8 and i16 are
storage types, not value types: they appear only in struct fields and array
element declarations as specified in section 6.
A function or block may produce no result, one result, or multiple results.
The type () denotes no result. Parenthesized comma-separated types denote
multiple results, for example (i32, i64). A single parenthesized type is
equivalent to that type.
4.2 Heap and Reference Types
Heap types include declared structs, declared arrays, declared function types, and the built-in WebAssembly heap types:
any eq i31 struct array none func nofunc extern noextern
Reference types MUST be written explicitly:
&HeapType // non-null reference
?HeapType // nullable reference
For example, &Widget lowers to (ref $Widget) and ?Widget lowers
to (ref null $Widget). A declared struct name by itself is never a value type
and MUST NOT be used where a type is required.
null denotes the nullable bottom reference. Its concrete source type and
WebAssembly heap annotation are supplied by context as defined in section 4.3.
The legacy spellings such as anyref are not part of WAST. Source using them
MUST use the equivalent explicit form, such as ?any.
4.3 Assignability
Values of type T are assignable to T. A non-null reference &T is
assignable to ?T. A reference to a subtype is assignable to the
corresponding reference to its supertype, preserving nullability.
The built-in heap-type relation is fixed as follows. none is a subtype of
i31, struct, array, eq, and any; i31, struct, and array are
subtypes of eq, and eq is a subtype of any. nofunc is a subtype of
func; noextern is a subtype of extern. The any/eq family, func
family, and extern family are otherwise disjoint. Every declared struct is a
subtype of struct, eq, and any; every declared array is a subtype of
array, eq, and any; declared struct inheritance adds the declared parent
as a direct supertype.
null is contextually typed. For an expected ?T, it lowers to ref.null T;
it has no type outside a context that supplies ?T. It is never assignable to
a non-null reference. This contextual choice is the only null representation
chosen by the compiler; it MUST NOT be inferred from an unrelated use site.
No implicit numeric conversions occur. Numeric conversion, extension, truncation, reinterpretation, and reference casts MUST use an applicable operator or low-level operation.
5. Declarations
A module consists of declarations. Declaration order does not affect name resolution, except that a name MUST be unique within its declaration namespace. Structs, arrays, and function types share the type namespace. Imported and defined functions share the function namespace; imported and defined globals, memories, tables, and tags likewise share their respective namespaces. Labels and locals have separate lexical namespaces.
Local variables, parameters, and functions MUST use snake_case. Globals MUST
use PascalCase. Structs, arrays, and function types MUST use PascalCase.
This convention is part of name resolution, not style guidance: an identifier
in a non-call value expression with snake_case spelling resolves only to a
local, while one with PascalCase spelling resolves only to a global. A
snake_case identifier in direct call position resolves in the function
namespace. Type identifiers resolve only in type positions.
module ::= declaration*
declaration ::= struct-decl | array-decl | func-type-decl | import-decl | func-decl
| table-decl | memory-decl | global-decl | tag-decl
| data-decl | export-decl | start-decl
struct-decl ::= "struct" identifier (":" identifier)? "{" field-decl* "}"
array-decl ::= "array" identifier "{" field-type "}"
field-decl ::= identifier ":" field-type ","?
field-type ::= type | storage-type | "mut" type | "mut" storage-type
storage-type ::= "i8" | "i16"
func-type-decl ::= "type" identifier "=" "func" "(" type-list? ")"
("->" result-type)? ";"
import-decl ::= "import" string-literal "." string-literal "as" import-kind
import-kind ::= "func" identifier "(" parameter-type-list? ")"
("->" result-type)? ";"
| "global" identifier ":" global-type ";"
| "memory" identifier "(" memory-limits ")" ";"
| "table" identifier "(" table-limits type ")" ";"
func-decl ::= "func" identifier "(" parameter-list? ")"
("->" result-type)? block
parameter-list ::= parameter ("," parameter)* ","?
parameter ::= identifier ":" type
parameter-type-list ::= type ("," type)* ","?
result-type ::= type | "(" type-list? ")"
type-list ::= type ("," type)* ","?
type ::= value-type | reference-type
value-type ::= "i32" | "i64" | "f32" | "f64"
reference-type ::= "&" heap-type | "?" heap-type
heap-type ::= identifier | "any" | "eq" | "i31" | "struct" | "array"
| "none" | "func" | "nofunc" | "extern" | "noextern"
table-decl ::= "table" identifier "(" table-limits type ")"
("=" "{" table-entry-list? "}")? ";"
table-entry-list ::= table-entry ("," table-entry)* ","?
table-entry ::= "null" | function-reference
function-reference ::= "&" function-identifier
function-identifier ::= identifier
table-limits ::= integer-literal (integer-literal)?
memory-decl ::= "memory" identifier "(" memory-limits ")" ";"
memory-limits ::= integer-literal (integer-literal)?
data-decl ::= "data" identifier ("at" integer-literal)? "=" string-literal ";"
global-decl ::= "global" identifier ":" global-type "=" expression ";"
global-type ::= ("mut")? type
tag-decl ::= "tag" identifier ("(" parameter-type-list? ")")? ";"
export-decl ::= "export" export-kind identifier "as" string-literal ";"
export-kind ::= "func" | "global" | "memory" | "table"
start-decl ::= "start" identifier ";"
The two strings in an import declaration are its WebAssembly module and field
names. The string in an export declaration is its WebAssembly export name.
export-kind MUST match the referenced declaration's namespace. There are no
implicit ABI names.
The initial value of an immutable or mutable global MUST be a WebAssembly constant expression permitted by the selected target features.
A module MAY contain at most one start declaration. Its name MUST resolve to a
function, including an imported function, with no parameters and result ().
It is emitted in the module's WebAssembly start section and is invoked during
module instantiation.
Every memory declaration and import states its initial page count and optional maximum page count explicitly. The initial count MUST be non-negative and the maximum, when present, MUST not be smaller than the initial count. No implicit memory minimum exists.
A module MAY declare or import any number of memories. Every memory operation
MUST name its target memory in its immediate list; WAST has no implicit default
memory. Memory indexes and addresses are i32; memory64 is not supported.
A function-type declaration emits a named WebAssembly function type and is a
heap type. &Name and ?Name are valid only when Name resolves to a
function-type declaration. Function-reference assignability is invariant:
named function types match only themselves. The parameters and results of a
function assigned to a function reference MUST exactly match the named function
type's signature.
In a heap-type position, an identifier MUST resolve to a declared struct,
array, or function type.
A data declaration decodes its string literal to UTF-8 bytes. Without at, it
emits a passive WebAssembly data segment. data Name at offset = value; emits
an active segment targeting the module's sole memory; it is a lowering error
when the module has zero or more than one memory. The offset is an i32
constant expression. The data name is in a distinct data namespace and is used
by catalog operations such as memory.init and data.drop.
6. Structures and GC Subtyping
A struct declaration defines a nominal WebAssembly GC struct type. A struct with one or more declared children is emitted as non-final. A struct with a parent is emitted as a WebAssembly subtype. All other structs are final; membership in a recursive group does not change finality.
struct Child : Parent {
own: i32,
}
means that Child contains all fields of Parent, in parent-first order,
followed by own. The source MUST NOT repeat inherited fields. The compiler
MUST flatten inherited fields when producing the WebAssembly struct definition
and MUST mark the necessary parent and child types as sub.
Each parent field's type and mutability are inherited unchanged. A child MAY not shadow an inherited field name. Parent links MUST be acyclic.
An i8 or i16 field is packed WebAssembly storage. Its declaration omits a
packed keyword. A packed-field read MUST use an explicit .s or .u suffix,
for example pixel->red.u or pixel->delta.s; it yields an i32 using signed
or unsigned extension respectively. A packed field cannot be read without that
suffix. A packed-field assignment accepts an i32 and stores its low 8 or 16
bits. Non-packed fields do not accept .s or .u suffixes.
The compiler constructs the directed type-dependency graph from declared structs, arrays, and function types. An edge exists for a field, array element, function parameter/result, or declared parent type referring to another declared type. Each strongly connected component is emitted as one recursive type group; singleton components without a self-edge are emitted outside a group. Components are emitted in dependency order. The user does not write recursive type-group annotations.
That grouping is subject to one overriding requirement. WebAssembly canonicalizes recursive type groups structurally, and names carry no identity, so two separately emitted groups with the same structure denote the same runtime type. Emitting each component independently therefore collapses distinct declarations whose fields coincide -- for example two children of one parent whose own fields have identical types -- which contradicts the nominal identity this section requires and makes a section 9 type test match the wrong declaration.
An implementation MUST NOT emit two declared types that canonicalize to the same WebAssembly type. Where the component decomposition above would do so, the affected components MUST be emitted together in a single recursive group, which makes their identity positional and keeps them distinct. An implementation MAY group more components than strictly necessary to satisfy this rule, and MAY group a contiguous span of components in dependency order to preserve that order. Grouping MUST NOT otherwise change field order, mutability, inheritance, or subtyping.
Struct values are allocated explicitly:
new Name { field: expression, ... }
A struct construction has the non-null type &Name.
Every field, including inherited fields, MUST be initialized exactly once.
Unknown and duplicate field initializers are errors.
Field initializer expressions are evaluated once in lexical source order. The
compiler MUST retain and reorder those values into parent-first field order
before emitting struct.new; that reordering MUST NOT change observable
evaluation order. Fields are read with reference->field. The receiver MUST
have a non-null declared-struct reference type; inherited names resolve to the
nearest declared field. Assigning to a field is permitted only when the field
was declared mut.
6.1 Arrays
An array declaration defines a nominal WebAssembly GC array type with one
element field. Its element type is immutable unless preceded by mut.
array I32Values { mut i32 }
let values: &I32Values = new I32Values { 10, 20, 30 };
An array declaration has no parent type and is emitted as a final WebAssembly array type. Like structs, declared arrays are included automatically in any required recursive type group.
new Name { expression, ... } constructs an array when Name resolves to an
array declaration. Its expressions are evaluated left to right; every value
MUST be assignable to the array element type. It lowers to array.new_fixed
with Name and the expression count as instruction immediates. An empty
initializer is valid and lowers to array.new_fixed Name 0.
An array construction has the non-null type &Name.
Array elements use C-style square-bracket syntax:
let second: i32 = samples[1];
samples[1] = 34;
An indexed read requires a non-null declared-array reference and an i32 index.
It yields the array element type and lowers to array.get. An indexed assignment
has the same receiver and index requirements; its right-hand expression must
yield one value assignable to the element type, and the element type MUST be
declared mut. It lowers to array.set.
Reads evaluate the receiver and then the index, each exactly once. Assignments
evaluate the receiver, index, and right-hand expression in that order, each
exactly once. Both operations have WebAssembly's native out-of-bounds trap
behavior. The initial surface syntax provides simple = assignment only;
compound array assignments such as += are not part of the language.
The unary # operator returns an array's current length:
let count: i32 = #samples;
Its operand MUST be a non-null declared-array reference. It evaluates that
operand exactly once, returns i32, and lowers to array.len.
An i8 or i16 array element is packed storage. Its indexed read MUST use an
explicit .s or .u suffix, for example samples[index].u; it yields an
i32 using signed or unsigned extension. Its assignment accepts an i32 and
stores its low 8 or 16 bits when the element is mut. A packed array element
cannot be read without a suffix, and a non-packed indexed read cannot use one.
7. Functions, Locals, and Expressions
Function parameters are immutable locals. A function body may declare locals:
let name: Type = expression;
var name: Type = expression;
let (first, second): (i32, i64) = expression;
let bindings are immutable; var bindings may be assigned with
name = expression;. A single binding requires exactly one result; a
parenthesized binding requires a result sequence of equal arity. Each result
MUST be assignable to its corresponding declared type. Local names are scoped
to their enclosing block and may shadow an outer local.
Reading a PascalCase global lowers to global.get. Assigning to one lowers
to global.set and is valid only when the global was declared mut. A global
cannot be shadowed by a local because their required spellings differ.
The expression grammar below lists the initial high-level expressions. The parser resolves binary precedence from highest to lowest in the order shown.
expression ::= logical-or
logical-or ::= logical-and ("||" logical-and)*
logical-and ::= bitwise-or ("&&" bitwise-or)*
bitwise-or ::= bitwise-xor ("|" bitwise-xor)*
bitwise-xor ::= bitwise-and ("^" bitwise-and)*
bitwise-and ::= equality ("&" equality)*
equality ::= comparison (("==" | "!=") comparison)*
comparison ::= shift (("<" | "<=" | ">" | ">=") shift)*
shift ::= sum (("<<" | ">>" | ">>>") sum)*
sum ::= product (("+" | "-") product)*
product ::= unary (("*" | "/" | "%") unary)*
unary ::= ("!" | "-" | "~" | "#") unary | "&" function-identifier | postfix
postfix ::= primary ("(" expression-list? ")" | "->" identifier storage-read? | "[" expression "]" storage-read?)*
storage-read ::= ".s" | ".u"
primary ::= literal | identifier | "(" expression ")" | tuple
| struct-construction | array-construction | control-expression
| wasm-operation
tuple ::= "(" expression "," expression-list ")"
struct-construction ::= "new" identifier "{" field-initializer-list? "}"
array-construction ::= "new" identifier "{" expression-list? "}"
field-initializer-list ::= field-initializer ("," field-initializer)* ","?
field-initializer ::= identifier ":" expression
The single & token has two roles: it begins a reference type in a type
position and is bitwise-and in an expression position. These uses are
unambiguous from grammar context.
The parser resolves the two new Name { ... } productions after resolving
Name: a struct name requires named field initializers, while an array name
requires an expression list. A name that resolves to neither is an error.
An expression is evaluated left to right. Calls evaluate arguments left to right. A call's argument count and types MUST match the function signature.
Every expression has a result arity. Literals, locals, globals, operators,
field accesses, constructors, and single-result calls have arity one. A
zero-result call may occur only as an expression statement. A multi-result
call, tuple, or control expression may occur only as a return operand or the
right side of a parenthesized local binding with identical arity. Function-call
arguments, operators, field receivers, array initializers, conditions, and
named struct-field initializers each require exactly one result; result
sequences never expand implicitly into an argument list.
A wasm-operation has exactly the result arity declared by its catalog schema
and is subject to the same zero-, single-, and multi-result placement rules as
a call. In particular, a zero-result raw operation cannot be returned or used
as a value, and a multi-result raw operation cannot expand implicitly into call
arguments.
An expression statement must have arity zero or one. A zero-result expression
leaves no stack value. A one-result expression statement lowers to the
expression followed by an explicit drop; a multi-result expression statement
is a type error.
An identifier not immediately followed by a call postfix reads a local or a
global according to the naming rule in section 5. An identifier followed by a
call postfix resolves to a declared function and lowers to direct call. A
non-null &FunctionType expression followed by a call postfix is an indirect
call and lowers to call_ref FunctionType; its arguments and results MUST
exactly match that named function type. A call postfix on any other expression
is a type error. &function_name creates a non-null typed function reference
when context supplies a matching named function type; it lowers to ref.func.
A field postfix is valid only when its receiver has a non-null declared-struct
reference type and lowers to struct.get using the resolved inherited-field
index.
The language provides conventional infix syntax for numeric arithmetic, comparison, bitwise operations, logical negation, and shifts. Its operators are type-directed and lower to the corresponding WebAssembly operation. An operator MUST have operands whose types select exactly one WebAssembly operation; otherwise the expression is invalid. This avoids implicit coercions.
An expected type propagates into a literal, an operator expression, a tuple
component, a call argument, or a constructor initializer. If that propagation
does not select one operand type, the programmer MUST supply context through a
typed binding, parameter, return type, cast operation, or typed wasm
operation. For example, let count: i32 = 1 + 2; is valid, while 1 + 2; is
not because its integer width is not determined.
The following table fixes the high-level operator mapping. Operations not in
the table require wasm.<opcode> syntax.
| Operator | Operand types | Result | Lowering and behavior |
|---|---|---|---|
+, -, * | matching i32 or i64 | same type | add, sub, mul; wraps modulo width |
/, % | matching i32 or i64 | same type | signed div_s, rem_s; divide by zero and signed overflow trap |
+, -, *, / | matching f32 or f64 | same type | IEEE 754 add, sub, mul, div |
- | i32, i64, f32, or f64 | same type | integer 0 - value or floating neg |
~, &, |, ^ | matching i32 or i64 | same type | integer bitwise operation |
<<, >>, >>> | matching integer operands | left type | shl, signed shr_s, unsigned shr_u |
<, <=, >, >= | matching integers | i32 | signed comparison |
<, <=, >, >= | matching floats | i32 | IEEE comparison; false for NaN |
==, != | matching numeric types | i32 | eq/ne; float != is true for NaN |
==, != | nullable references below ?eq | i32 | ref.eq / negated ref.eq |
! | i32 | i32 | eqz; returns canonical 0 or 1 |
# | non-null declared array reference | i32 | array.len |
&&, ` | ` | i32 |
The right operand of && is evaluated only when the left operand is nonzero;
the right operand of || is evaluated only when the left operand is zero.
These operators lower through structured if, not eager bitwise operations.
An assignment evaluates a field receiver before its right-hand expression. An
indexed assignment evaluates its receiver, index, and right-hand expression in
that order. The right-hand expression MUST yield exactly one value assignable to
the place's type. An identifier place is a mutable var local or mutable
global; a field place is a mutable struct field; and an indexed place is a
mutable array element. The compiler MUST evaluate a field receiver exactly once
and lower a field write to struct.set. It MUST evaluate an indexed receiver,
index, and value exactly once in source order and lower an indexed write to
array.set.
return expression; requires a result sequence with the same arity as the
function result; each result must be assignable to its corresponding type.
return; is valid only for a function whose result is ().
Returning a direct or indirect call has the same source semantics as every
other return expression. It does not require tail-call support and MUST NOT
produce a source feature error on a target without tail calls. Section 13
defines the optional tail-call output mode.
Tuple expressions and returns use parentheses, for example return (count, total);. They map directly to WebAssembly's ordered result list; tuples are
not heap allocations.
8. Statements and Structured Control Flow
block ::= "{" statement* "}"
statement ::= local-decl | assignment ";" | expression ";"
| control-expression ";"? | if-statement | for-statement
| branch-statement | return-statement | throw-statement
local-decl ::= ("let" | "var") binding ":" binding-type "=" expression ";"
binding ::= identifier | "(" identifier ("," identifier)+ ","? ")"
binding-type ::= type | "(" type-list ")"
place ::= identifier | postfix "->" identifier | postfix "[" expression "]"
assignment ::= place "=" expression
if-statement ::= "if" "(" condition ")" block ("else" (if-statement | block))?
control-expression ::= ("block" | "loop") identifier? control-signature? block
control-signature ::= "(" control-parameter-list? ")" ("->" result-type)?
control-parameter-list ::= control-parameter ("," control-parameter)* ","?
control-parameter ::= identifier ":" type "=" expression
for-statement ::= "for" identifier "in" expression ".." expression block
branch-statement ::= "break" identifier? branch-values? ";"
| "continue" identifier? branch-values? ";"
| "br" identifier branch-values? ";"
branch-values ::= "(" expression-list? ")"
return-statement ::= "return" expression? ";"
throw-statement ::= "throw" identifier ("(" expression-list? ")")? ";"
expression-list ::= expression ("," expression)* ","?
condition ::= expression | expression "is" reference-type | expression "is" "null"
block and loop are control expressions. Their signature has zero or more
parameters and zero or more results; an omitted signature is equivalent to
() -> (). The initializer for each parameter is evaluated left to right in
the enclosing scope, then the parameter names are bound within the body.
When a block or loop begins a statement, it is parsed as a standalone
control expression and its final semicolon is optional; in every other
expression context, the surrounding construct supplies the terminator.
let answer: i32 = block done() -> i32 {
br done(42);
};
let retries: i32 = loop retry(attempt: i32 = 0) -> i32 {
if (attempt == 3) {
break retry(attempt);
}
continue retry(attempt + 1);
};
A block label targets the end of its block. br label(values); to a block
MUST provide values matching that block's result type. A loop label targets
the beginning of its loop. br label(values); to a loop MUST provide values
matching that loop's parameter types.
Labels are lexical and share a namespace distinct from locals and declarations.
An inner label may shadow an outer label; a duplicate label at the same lexical
depth is an error. br requires an explicit in-scope label. continue targets
the nearest loop or for, or an explicitly named loop; it is invalid for a
block. Its values MUST match the loop parameter types. break targets the
nearest loop or for, or an explicitly named loop. Its values MUST match
the loop result type. For a for, both forms require zero values.
The compiler lowers a loop with results to a WebAssembly result block enclosing
a WebAssembly loop with the declared parameter types.
For example, the done control expression above lowers equivalently to:
(local.set $answer
(block $done (result i32)
(br $done (i32.const 42))))
A control expression with a non-empty result type MUST be used in a context
that consumes that result, such as a local initializer or a return. A
single-result control expression may also be used as a call argument. Reaching
the end of a control body is valid only when its result type is (). A
non-empty result therefore requires a branch to the appropriate block or
loop-exit target, a return, or an unreachable instruction on every reachable
completion path.
A for i in a..b evaluates a and b once, binds an i32 induction
variable, begins at a, and ends before b. Both endpoints MUST be i32.
The range uses signed i32.lt_s: if a >= b, it executes zero times. The
induction binding is immutable, and the compiler increments an internal value
with i32.add after each completed body and after a continue. A for is a
unit loop target:
unlabelled break and continue may target it, but neither accepts values.
An if condition MUST yield exactly one i32. The result of an expression-form
if is not part of the initial language; if is a statement.
A non-unit function MUST not reach the closing brace normally. Every reachable
completion path MUST execute a return with exactly the declared result
sequence, branch to an enclosing non-local target, or be unreachable. A unit
function may reach its closing brace, which lowers to no result.
9. Type Tests and Flow Narrowing
The following conditions have special type-narrowing semantics:
value is &HeapType
value is ?HeapType
value is null
The left operand MUST have a reference type. A non-compile-time-false
heap-type test MUST lower to the appropriate WebAssembly reference test or cast
sequence.
In the true arm, the local named by a simple left operand is narrowed to the
tested non-null or nullable reference type. For value is null, it is narrowed
to the nullable bottom-reference type. The false arm retains the original type;
the initial language does not model complement types.
For value is &T, null never matches and a non-null runtime value must be a
subtype of T. For value is ?T, a matching runtime value or null matches.
value is null is valid for every reference type. A test of a non-null
reference against null, or of reference families with no common subtype, is a
compile-time false condition; its true arm is unreachable.
Narrowing applies only when the left operand is a simple immutable local name.
For every other left operand, the condition is still evaluated exactly once but
does not change any source binding's type. To retain a narrowed value for an
arbitrary expression or mutable local, bind it to a let local before the test.
9.1 Lowering Examples
Compile-time-false tests lower directly to an i32.const 0 condition; their
true arm is unreachable and MUST NOT require br_on_cast or br_on_null.
Every other is &T or is ?T test uses br_on_cast; every other is null
test uses br_on_null.
WebAssembly locals have fixed types. Consequently, narrowing a source local
does not change the type of its emitted WebAssembly local. When the narrowed
value is consumed by an operation requiring the narrower type, the compiler
MUST emit a value with that type. For an is test, it MUST use
br_on_cast, whose branch target receives the narrowed reference on the stack.
It MAY materialize that stack value in a typed branch-local when the value must
be used more than once. For an is null test, it MUST use br_on_null.
For example, this source function tests a nullable top reference and consumes
the resulting non-null i31 reference:
func i31_payload(value: ?any) -> i32 {
if (value is &i31) {
return wasm.i31.get_u(value);
}
return 0;
}
It MUST lower equivalently to the following WAT. Local names in generated WAT are illustrative and are not part of WAST's ABI.
(func $i31_payload (param $value (ref null any)) (result i32)
(local $value.i31 (ref i31))
(block $end
(block $matched (result (ref i31))
(br_on_cast $matched (ref null any) (ref i31) (local.get $value))
;; On a failed cast, the original reference remains on the stack.
(drop)
(br $end))
(local.set $value.i31)
(return (i31.get_u (local.get $value.i31))))
(i32.const 0))
On a successful cast, br_on_cast branches to $matched and supplies a
statically non-null (ref i31) as that block's result. The failed-cast path
falls through with the original (ref null any) value, which the compiler
discards before branching to $end. The compiler MUST NOT emit a separate
ref.test plus ref.cast sequence solely to implement an is test.
A struct-subtype test follows the same pattern:
func integer_value(value: ?WisteriaHeap) -> i64 {
if (value is &WisteriaHeapInteger) {
return value->value;
}
return 0;
}
(func $integer_value (param $value (ref null $WisteriaHeap)) (result i64)
(local $value.integer (ref $WisteriaHeapInteger))
(block $end
(block $matched (result (ref $WisteriaHeapInteger))
(br_on_cast $matched
(ref null $WisteriaHeap) (ref $WisteriaHeapInteger)
(local.get $value))
(drop)
(br $end))
(local.set $value.integer)
(return
(struct.get $WisteriaHeapInteger 1
(local.get $value.integer))))
(i64.const 0))
In this example field index 1 is the derived struct's value field: index
0 is the inherited next_vararg field. This demonstrates the parent-first
field flattening required by section 6.
Null tests lower to br_on_null rather than ref.is_null:
func is_null(value: ?any) -> i32 {
if (value is null) {
return 1;
}
return 0;
}
(func $is_null (param $value (ref null any)) (result i32)
(block $end
(block $matched
(br_on_null $matched (local.get $value))
;; The failed null check leaves a non-null reference on the stack.
(drop)
(br $end))
(return (i32.const 1)))
(i32.const 0))
On success, br_on_null transfers directly to $matched; on failure, it
falls through with a non-null reference that is discarded before branching to
$end. Since a null reference has no payload, br_on_null does not pass a
value to its target. If the source true arm uses the narrowed value, the
compiler MUST synthesize an appropriate ref.null value. The source type
checker still treats value as the nullable bottom-reference type within the
true arm.
10. Tables, Memories, Globals, and Tags
A table declaration or import has type ?FunctionType, where FunctionType
is a named function type. Its optional initializer is an active element segment
at offset zero. Each entry is either null or &function_name; a function
reference MUST exactly match the table's function type. The initializer count
MUST NOT exceed the declared initial size. Remaining entries are initialized to
null.
Table indexing uses the same syntax as arrays. Callbacks[index] evaluates its
receiver and index once, requires an i32 index, returns ?Callback, and
lowers to table.get. Callbacks[index] = &handler requires a matching
?Callback value and lowers to table.set. #Callbacks evaluates to i32
and lowers to table.size. Indexed table access has WebAssembly's native
out-of-bounds trap behavior. A nullable table entry MUST be narrowed or
otherwise made non-null before it can be called.
Memory limits and table limits are measured in WebAssembly pages and elements, respectively. If a maximum is supplied, it MUST be greater than or equal to the initial size.
The initializer of a global is a source constant expression. The permitted
forms are a contextually typed numeric literal, null in a nullable-reference
context, a read of an imported immutable global, and a typed wasm operation
whose instruction schema is marked a constant expression. All other source
expressions, including calls, allocation, local reads, and mutable global reads,
are rejected in a global initializer.
tag Name(types...); declares an exception tag when exception handling is
enabled. throw Name(arguments...); requires arguments matching the tag's
parameter types and lowers to throw. Both forms are rejected by the initial
core-plus-GC feature set unless exception handling is explicitly enabled.
11. Exports
An export declaration exports the item of its explicitly stated kind. Functions, tables, memories, and globals are exportable. Types and tags are not exportable in the initial language. A source item MAY be exported more than once under distinct strings, but a module MUST NOT contain duplicate WebAssembly export names.
12. Low-Level WebAssembly Operations
Every enabled WebAssembly instruction is available through the wasm escape
hatch only when its schema is present in the selected target's instruction
catalog. The compiler invocation or project manifest MUST identify an immutable
catalog ID and the exact WebAssembly core/proposal revisions from which its
schemas are derived. The compiler MUST record that ID in its build output.
Changing the catalog ID selects a different language target. An opcode,
immediate form, or feature missing from the catalog is a feature error; the
compiler MUST NOT guess an instruction's stack effect or immediates. The
canonical form is:
wasm.opcode(arguments...)
For example:
wasm.i32.add(left, right)
wasm.table.get<WisteriaVariadics>(index)
wasm.struct.get<WisteriaHeapInteger, value>(reference)
The exact immediate-argument grammar is:
wasm-operation ::= "wasm" "." opcode ("<" immediate-list ">")?
"(" expression-list? ")"
immediate-list ::= immediate ("," immediate)* ","?
immediate ::= identifier | integer-literal | float-literal | type
| immediate-key "=" immediate | "[" immediate-list? "]"
immediate-key ::= identifier
opcode ::= identifier ("." identifier)*
opcode is the dot-separated WebAssembly instruction name. Each instruction
schema defines its immediates, operand types, result types, and target-feature
requirement, including label arity, memory arguments, and constant-expression
eligibility. A catalog MUST include the complete source-syntax schema for every
opcode it advertises and a revision-pinned mapping to its WebAssembly binary
opcode. The catalog is normative for the selected target; the compiler MUST
type-check an operation against it and MUST reject unknown operations or
operations unavailable in the selected target.
This form is an escape hatch, not a textual WAT splice: it remains name-resolved and type-checked and cannot bypass module validation.
13. Lowering and Validation
The compiler MUST lower a valid program to a WebAssembly module that validates against the selected feature set. It MUST preserve:
- Scalar types and explicit reference nullability.
- Function parameter and result order.
- Struct field order, mutability, inheritance, and subtyping.
- Explicit import module/field strings and export strings.
- Structured control-flow semantics.
- Lexical source evaluation order, including named struct initializers.
- The exact trap behavior of the selected WebAssembly operation.
The compiler MUST NOT introduce implicit numeric conversions, nullable casts, allocations, calls, or observable reads/writes. It MAY introduce temporary locals, blocks, casts guarded by the specified branch-on-cast instructions, and value reordering only when those changes preserve the preceding guarantees. Every generated instruction MUST be attributable to a source construct or to a required structural lowering described in this specification.
An implementation MUST emit readable WebAssembly Text (WAT). The WAT output MUST include comments identifying the original WAST declaration, statement, or expression responsible for each emitted instruction sequence. Comments for compiler-generated structural instructions MUST identify the source construct that required them. The WAT comments are explanatory only and MUST NOT alter the generated module's semantics.
An implementation MAY additionally emit a WebAssembly binary (.wasm). It MAY
provide optimizations behind an explicit flag. Optimized output MUST preserve
the observable behavior and trap behavior specified by this document; without
that flag, the WAT output MUST retain a direct, source-attributable lowering.
An implementation MAY provide a separate --tail-calls output flag. With that
flag, a return whose operand is a direct call with the function's exact result
type MAY lower to return_call; one whose operand is an indirect call MAY lower
to return_call_ref. Without that flag, both MUST lower as an ordinary call
followed by return. The source program is valid in either mode. A compiler
MUST NOT diagnose a source feature error merely because tail calls are disabled
or unavailable in its default target.
An implementation SHOULD validate emitted binaries with wasm-tools validate
and execute runtime fixtures in a conforming WebAssembly engine.
14. Open Extensions
The following require future normative sections before implementation:
- Memory64, shared memory, and atomic operations.
- Exception handling
try,catch,catch_all, anddelegatesyntax. - Function-type subtyping.
- SIMD, relaxed SIMD, and all post-GC proposals other than the optional tail-call output mode.