Skip to main content

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.

OperatorOperand typesResultLowering and behavior
+, -, *matching i32 or i64same typeadd, sub, mul; wraps modulo width
/, %matching i32 or i64same typesigned div_s, rem_s; divide by zero and signed overflow trap
+, -, *, /matching f32 or f64same typeIEEE 754 add, sub, mul, div
-i32, i64, f32, or f64same typeinteger 0 - value or floating neg
~, &, |, ^matching i32 or i64same typeinteger bitwise operation
<<, >>, >>>matching integer operandsleft typeshl, signed shr_s, unsigned shr_u
<, <=, >, >=matching integersi32signed comparison
<, <=, >, >=matching floatsi32IEEE comparison; false for NaN
==, !=matching numeric typesi32eq/ne; float != is true for NaN
==, !=nullable references below ?eqi32ref.eq / negated ref.eq
!i32i32eqz; returns canonical 0 or 1
#non-null declared array referencei32array.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, and delegate syntax.
  • Function-type subtyping.
  • SIMD, relaxed SIMD, and all post-GC proposals other than the optional tail-call output mode.