Language Guide
This guide covers Wax syntax and semantics. For the detailed mapping to WebAssembly instructions, see Correspondence.
Comments
Wax supports C-style comments:
// Single-line comment
/* Multi-line
comment */
Literals
Integers
42 // Decimal
0x2A // Hexadecimal
0o52 // Octal
0b101010 // Binary
Integer literals are typed based on context. Suffix with _i64 for 64-bit:
let x: i64 = 42; // Inferred from type annotation
let y: i64 = 42_i64; // Explicit suffix
Floating Point
3.14 // Decimal
1.0e10 // Scientific notation
0x1.5p10 // Hexadecimal float
inf // Infinity
nan // Not a number
Variables
Local Variables
Declare local variables with let. Variables must be typed:
fn example() -> i32 {
let x: i32;
let y: i32;
x = 10;
y = 20;
x + y
}
You can declare multiple variables:
let x: i32, y: i32, z: f64;
Assignment
Use = for assignment (like local.set):
x = 42;
Use := for assignment that also returns the value (like local.tee):
y = (x := 42) + 1; // x is set to 42, y is set to 43
Global Variables
Globals are declared at module level:
const PI: f64 = 3.14159; // Immutable global
let mut counter: i32 = 0; // Mutable global
Expressions
Wax is expression-oriented. Most constructs produce values.
Arithmetic
x + y // Add
x - y // Subtract
x * y // Multiply
x / y // Divide (float)
x /s y // Divide signed (integer)
x /u y // Divide unsigned (integer)
x %s y // Remainder signed
x %u y // Remainder unsigned
Bitwise
x & y // And
x | y // Or
x ^ y // Xor
x << y // Shift left
x >>s y // Shift right signed
x >>u y // Shift right unsigned
Comparison
x == y // Equal
x != y // Not equal
x < y // Less than (float)
x <s y // Less than signed (integer)
x <u y // Less than unsigned (integer)
x <= y // Less or equal (float)
x <=s y // Less or equal signed
x <=u y // Less or equal unsigned
// Similarly: >, >=, >s, >u, >=s, >=u
Unary Operations
-x // Negate
+x // Positive (no-op for integers)
!x // Logical not / is_null for references
Method-Style Operations
Some operations use method syntax:
x.abs // Absolute value
x.neg // Negate
x.sqrt // Square root
x.floor // Floor
x.ceil // Ceiling
x.trunc // Truncate
x.nearest // Round to nearest
x.clz // Count leading zeros
x.ctz // Count trailing zeros
x.popcnt // Population count
Two-argument operations:
min(x, y)
max(x, y)
copysign(x, y)
rotl(x, y) // Rotate left
rotr(x, y) // Rotate right
Type Conversions
Use as for type conversions:
x as i32 // Wrap i64 to i32
x as i64_s // Sign-extend i32 to i64
x as i64_u // Zero-extend i32 to i64
x as f32 // Demote f64 to f32
x as f64 // Promote f32 to f64
x as i32_s // Truncate float to signed int
x as f32_s // Convert signed int to float
x.to_bits // Reinterpret float as int
x.from_bits // Reinterpret int as float
Conditional Expression
cond ? val_true : val_false
This maps directly to Wasm’s select instruction.
Control Flow
Blocks
A block is a sequence of expressions. The last expression is the block’s value:
{
let x: i32;
x = compute();
x + 1
}
Use do with a type annotation when the block returns a value:
do i32 {
42
}
If Expressions
if condition {
then_branch
} else {
else_branch
}
With a result type:
if condition => i32 {
1
} else {
0
}
Loops
Loops repeat until explicitly broken out of:
loop {
// body
br 'loop; // Continue looping
}
With a label:
'outer: loop {
'inner: loop {
br 'outer; // Break to outer
}
}
Labels and Branches
Labels start with ' and are used with branch instructions:
'my_block: do {
if condition {
br 'my_block; // Exit the block
}
// ...
}
Branch instructions:
br 'label; // Unconditional branch
br_if 'label cond; // Branch if condition is true
br_table ['a, 'b else 'default] index; // Branch table
Return
return value;
Tail Calls
Use become for tail calls (guaranteed not to grow the stack):
fn factorial_helper(n: i32, acc: i32) -> i32 {
if n <= 1 {
acc
} else {
become factorial_helper(n - 1, n * acc)
}
}
Functions
Definition
fn name(param1: type1, param2: type2) -> return_type {
body
}
Functions without a return type return nothing:
fn log_value(x: i32) {
// side effects only
}
Function Types
Function types use fn:
type binary_op = fn(i32, i32) -> i32;
Anonymous parameters use _:
type callback = fn(_: i32) -> i32;
Calls
result = my_function(arg1, arg2);
Indirect Calls
Call through a function reference:
(func_ref as &?callback)(arg)
References
Reference Types
&type // Non-nullable reference
&?type // Nullable reference
Null
null // Null reference (requires type context)
Null Check
!ref // True if ref is null
ref! // Assert non-null (trap if null)
Type Testing and Casting
val is &type // Test if val is of type (returns i32)
val as &type // Cast val to type (trap on failure)
Structs
Definition
type point = { x: i32, y: i32 };
type mutable_point = { mut x: i32, mut y: i32 };
Creation
{point| x: 10, y: 20} // New struct with explicit type
{point| ..} // New struct with default values
Field Access
p.x // Get field
p.x = 42; // Set field (if mutable)
Arrays
Definition
type bytes = [i8];
type mutable_ints = [mut i32];
Creation
[bytes| 0; 100] // New array: 100 elements, all 0
[bytes| ..; 100] // New array: 100 elements, default value
[bytes| 1, 2, 3, 4] // New array with specific values
Element Access
arr[i] // Get element
arr[i] = val; // Set element (if mutable)
arr.length // Array length
Exceptions
Tags
Define exception tags:
tag my_error(code: i32);
Throw
throw my_error(42);
Try/Catch (try_table style)
'handler: do {
try {
might_throw();
} catch [my_error -> 'handler]
}
With reference to exception:
try {
might_throw();
} catch [my_error & -> 'handler] // & captures the exnref
Catch all:
try {
might_throw();
} catch [_ -> 'handler] // Catch any exception
Try/Catch (legacy style)
try {
might_throw();
} catch {
my_error => { handle_error(); }
_ => { handle_any(); }
}
Holes
Wax supports holes (_) as placeholders for values that can be inferred from earlier expressions in a sequence:
fn example() -> i32 {
1; 2; _ + _; // Equivalent to: let a = 1; let b = 2; a + b
}
This is useful for writing concise code where intermediate values flow naturally.