Examples
Complete examples demonstrating Wax features and their WebAssembly equivalents.
Named Module
A #![module = "..."] inner attribute names the module.
Wax
#![module = "calculator"]
#[export = "square"]
fn square(x: i32) -> i32 {
x * x;
}
Equivalent WAT
(module $calculator
(func $square (export "square") (param $x i32) (result i32)
local.get $x
local.get $x
i32.mul))
Arithmetic Functions
Wax
#[export = "add"]
fn add(x: i32, y: i32) -> i32 {
x + y;
}
#[export = "multiply"]
fn multiply(x: i32, y: i32) -> i32 {
x * y;
}
Equivalent WAT
(func $add (export "add") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)
(func $multiply (export "multiply") (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.mul)
Factorial with Recursion
Wax
#[export = "factorial"]
fn factorial(n: i32) -> i32 {
if n <=s 1 => i32 {
1;
} else {
n * factorial(n - 1);
}
}
Equivalent WAT
(func $factorial (export "factorial") (param $n i32) (result i32)
local.get $n
i32.const 1
i32.le_s
if (result i32)
i32.const 1
else
local.get $n
local.get $n
i32.const 1
i32.sub
call $factorial
i32.mul
end)
Factorial with Tail Call
Wax
#[export = "factorial"]
fn factorial(n: i32) -> i32 {
become factorial_helper(n, 1);
}
fn factorial_helper(n: i32, acc: i32) -> i32 {
if n <=s 1 => i32 {
acc;
} else {
become factorial_helper(n - 1, n * acc);
}
}
Loop with Early Exit
Wax
type ints = [i32];
#[export = "find_first_zero"]
fn find_first_zero(arr: &ints) -> i32 {
let len = arr.length();
let i = 0;
'search: loop {
if i >=s len {
return -1;
}
if arr[i] == 0 {
return i;
}
i += 1;
br 'search;
}
unreachable;
}
Equivalent WAT
(func $find_first_zero (export "find_first_zero")
(param $arr (ref $array_i32)) (result i32)
(local $len i32)
(local $i i32)
local.get $arr
array.len
local.set $len
i32.const 0
local.set $i
(loop $search
local.get $i
local.get $len
i32.ge_s
if
i32.const -1
return
end
local.get $arr
local.get $i
array.get $array_i32
i32.eqz
if
local.get $i
return
end
local.get $i
i32.const 1
i32.add
local.set $i
br $search))
While Loops
Wax
#[export = "triangle"]
fn triangle(n: i32) -> i32 {
let i: i32 = 0;
let total: i32 = 0;
while i <s n {
i += 1;
total += i;
}
total;
}
#[export = "countdown"]
fn countdown(n: i32) -> i32 {
let steps: i32 = 0;
'loop: loop {
n -= 1;
steps += 1;
br_if 'loop n >s 0;
}
steps;
}
Equivalent WAT
(func $triangle (export "triangle") (param $n i32) (result i32)
(local $i i32) (local $total i32)
(local.set $i (i32.const 0))
(local.set $total (i32.const 0))
(loop $loop
(if (i32.lt_s (local.get $i) (local.get $n))
(then
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(local.set $total (i32.add (local.get $total) (local.get $i)))
(br $loop))))
(local.get $total)
)
(func $countdown (export "countdown") (param $n i32) (result i32)
(local $steps i32)
(local.set $steps (i32.const 0))
(loop $loop
(local.set $n (i32.sub (local.get $n) (i32.const 1)))
(local.set $steps (i32.add (local.get $steps) (i32.const 1)))
(br_if $loop (i32.gt_s (local.get $n) (i32.const 0))))
(local.get $steps)
)
Dispatch (Jump Table)
Wax
#[export = "rgb_channel"]
fn rgb_channel(color: i32, value: i32) -> i32 {
dispatch color ['red 'green 'blue else 'bad] {
'bad: { return -1; }
'blue: { return value & 255; }
'green: { return (value & 255) << 8; }
'red: { return (value & 255) << 16; }
}
}
Equivalent WAT
(func $rgb_channel (export "rgb_channel")
(param $color i32) (param $value i32) (result i32)
(block $red
(block $green
(block $blue
(block $bad (br_table $red $green $blue $bad (local.get $color)))
(return (i32.const -1)))
(return (i32.and (local.get $value) (i32.const 255))))
(return
(i32.shl (i32.and (local.get $value) (i32.const 255)) (i32.const 8))))
(return
(i32.shl (i32.and (local.get $value) (i32.const 255)) (i32.const 16))))
Type Match
Wax
type cell = { value: i32 };
type cons = { head: &eq, tail: &?eq };
#[export = "sum_list"]
fn sum_list(v: &?eq) -> i32 {
match v {
c: &cons => { return (c.head as &cell).value + sum_list(c.tail); }
x: &cell => { return x.value; }
_ => { return 0; }
}
}
Equivalent WAT
The scrutinee is evaluated once and threaded through a br_on_cast chain in the
innermost block; each test branches out to its arm’s block (named by a readable
$arm/$default label, picked fresh so it cannot capture a user branch)
carrying the narrowed value, and the default trails the outer escape block:
(func $sum_list (export "sum_list") (param $v eqref) (result i32)
(local $x (ref $cell)) (local $c (ref $cons))
(block $default
(local.set $x
(block $arm_1 (result (ref $cell))
(local.set $c
(block $arm (result (ref $cons))
(drop
(br_on_cast $arm_1 eqref (ref $cell)
(br_on_cast $arm eqref (ref $cons) (local.get $v))))
(br $default)))
(return
(i32.add
(struct.get $cell $value
(ref.cast (ref $cell) (struct.get $cons $head (local.get $c))))
(call $sum_list (struct.get $cons $tail (local.get $c)))))))
(return (struct.get $cell $value (local.get $x))))
(return (i32.const 0)))
Structs and Methods
Wax
type point = { x: i32, y: i32 };
#[export = "make_point"]
fn make_point(x: i32, y: i32) -> &point {
{point| x, y}; // field shorthand: {x, y} means {x: x, y: y}
}
#[export = "distance_squared"]
fn distance_squared(p1: &point, p2: &point) -> i32 {
let dx = p1.x - p2.x;
let dy = p1.y - p2.y;
dx * dx + dy * dy;
}
Mutable Structs
Wax
type counter = { value: mut i32 };
#[export = "new_counter"]
fn new_counter() -> &counter {
{counter| value: 0};
}
#[export = "increment"]
fn increment(c: &counter) {
c.value = c.value + 1;
}
#[export = "get_value"]
fn get_value(c: &counter) -> i32 {
c.value;
}
Inferring Struct and Array Types
Wax
type point = { x: i32, y: i32 };
type ints = [mut i32];
// Where an expected type fixes the result (here a `let` annotation) the
// struct or array type name may be omitted.
#[export = "origin"]
fn origin() -> &point {
let p: &point = {x: 0, y: 0};
p;
}
#[export = "zeros"]
fn zeros() -> &ints {
let a: &ints = [0; 16];
a;
}
// A struct type can also be inferred from its field set alone, with no
// expected type: only `point` has the fields {x, y}, so its name may be
// dropped here too.
#[export = "make_origin"]
fn make_origin() -> &eq {
{x: 0, y: 0};
}
// The expected type also reaches into both branches of a conditional `?:`, so a
// branch may omit the name: here the `{..}` default is resolved from the
// `-> &point` result type flowing through the `?:`.
#[export = "pick"]
fn pick(c: i32) -> &point {
c ? {..} : {x: 1, y: 2};
}
Inferring Block Result Types
Wax
A block’s result type, => T on an if, or the type after do (and likewise
loop, try, and try_table), may be omitted where it is fixed by the values
the block produces or by the surrounding context:
#[export = "max0"]
fn max0(n: i32) -> i32 {
// `=> i32` omitted: inferred from the branches and the return type
if n <s 0 {
0;
} else {
n;
}
}
#[export = "answer"]
fn answer() -> i32 {
// the `do` block's result type is inferred from the value it leaves
let x = do {
40 + 2;
};
x;
}
Arrays
Wax
type bytes = [mut i8];
#[export = "sum_bytes"]
fn sum_bytes(arr: &bytes) -> i32 {
let sum = 0;
let i = 0;
let len = arr.length();
'loop: loop {
if i >=s len {
return sum;
}
sum += arr[i] as i32_u;
i += 1;
br 'loop;
}
unreachable;
}
#[export = "fill_bytes"]
fn fill_bytes(arr: &bytes, val: i32) {
let i = 0;
let len = arr.length();
'loop: loop {
if i >=s len {
return;
}
arr[i] = val;
i += 1;
br 'loop;
}
}
Exception Handling
Wax
tag divide_by_zero();
tag overflow();
#[export = "safe_divide"]
fn safe_divide(a: i32, b: i32) -> i32 {
if b == 0 {
throw divide_by_zero;
}
a /s b;
}
#[export = "try_divide"]
fn try_divide(a: i32, b: i32) -> i32 {
'on_error: do {
try {
return safe_divide(a, b);
} catch [divide_by_zero -> 'on_error]
}
0; // Return 0 on division by zero
}
Recursive Types (Linked List)
Wax
type list = { value: i32, next: &?list };
#[export = "make_node"]
fn make_node(value: i32, next: &?list) -> &list {
{list| value, next};
}
#[export = "sum_list"]
fn sum_list(head: &?list) -> i32 {
if !head {
return 0;
}
let n = head!;
n.value + sum_list(n.next);
}
Imports and Exports
Wax
// Import a function and a global from the host environment
import "env" {
fn log(value: i32);
const base_value: i32;
}
// Export a function that uses the imports
#[export = "compute_and_log"]
fn compute_and_log(x: i32) -> i32 {
let result = x + base_value;
log(result);
result;
}
Hash Function
A more complex example showing bitwise operations:
Wax
#[export = "hash_mix_int"]
fn hash_mix_int(h: i32, d: i32) -> i32 {
((d * 0xcc9e2d51).rotl(15) * 0x1b873593 ^ h).rotl(13) * 5 + 0xe6546b64;
}
#[export = "hash_finalize"]
fn hash_finalize(h: i32) -> i32 {
h ^= h >>u 16;
h *= 0x85ebca6b;
h ^= h >>u 13;
h *= 0xc2b2ae35;
h ^ h >>u 16;
}
Strings and Characters
A character literal is an i32 code point; a string literal builds an i8 or
i16 array ([mut i8] by default, or a named array type with the T # "…"
prefix). An i8 array holds the raw UTF-8 bytes; an i16 array holds the
UTF-16 code units. See Strings.
Wax
type chars = [i8];
type wide = [i16];
#[export = "newline"]
fn newline() -> i32 {
'\n'; // 10
}
#[export = "greeting"]
fn greeting() -> &chars {
chars # "hi\u{21}"; // the bytes 'h', 'i', '!'
}
#[export = "wide_greeting"]
fn wide_greeting() -> &wide {
wide # "café"; // the UTF-16 units 'c', 'a', 'f', 'é'
}
Equivalent WAT
wax -f wat keeps the literals as (@char …) / (@string …) annotations, so
they round-trip back to Wax unchanged:
(func $newline (export "newline") (result i32) (@char "\n"))
(func $greeting (export "greeting") (result (ref $chars))
(@string $chars "hi!"))
(func $wide_greeting (export "wide_greeting") (result (ref $wide))
(@string $wide "café"))
In a Wasm binary these lower to i32.const 10 and an array.new_fixed: the
i8 greeting holds the three bytes 104, 105, 33, and the i16 greeting the
four UTF-16 units 99, 97, 102, 233. The character comes back as the integer
10, while each string is still recovered (its elements are reasonable text).
Holes (Stack Values)
Holes (_) plug in values that earlier statements leave on the operand stack.
The two holes in _ + _ consume x * 2 and y * 3, left-to-right in the order
they were produced. See Holes for the full rules.
Wax
#[export = "blend"]
fn blend(x: i32, y: i32) -> i32 {
x * 2;
y * 3;
_ + _;
}
Equivalent WAT
(func $blend (export "blend") (param $x i32) (param $y i32) (result i32)
local.get $x
i32.const 2
i32.mul
local.get $y
i32.const 3
i32.mul
i32.add)
Multiple Return Values
A function may return several values, and a let can bind them all at once
from a parenthesised list of names. The call runs once and its results are
stored into the locals. See Local Variables.
Wax
fn divmod(a: i32, b: i32) -> (i32, i32) {
a /s b;
a %s b;
}
#[export = "checksum"]
fn checksum(a: i32, b: i32) -> i32 {
let (q, r) = divmod(a, b);
q + r;
}
Equivalent WAT
(func $divmod (param $a i32) (param $b i32) (result i32 i32)
local.get $a
local.get $b
i32.div_s
local.get $a
local.get $b
i32.rem_s)
(func $checksum (export "checksum") (param $a i32) (param $b i32) (result i32)
(local $q i32) (local $r i32)
local.get $a
local.get $b
call $divmod
local.set $r
local.set $q
local.get $q
local.get $r
i32.add)
Wide Arithmetic
128-bit integer arithmetic uses the i64:: intrinsics, which take and return
their operands as (low, high) pairs of i64 values. Each returns two
results, so a multi-value let binds them.
Wax
#[export = "add_u128"]
fn add_u128(a_lo: i64, a_hi: i64, b_lo: i64, b_hi: i64) -> (i64, i64) {
let (lo, hi) = i64::add128(a_lo, a_hi, b_lo, b_hi);
lo;
hi;
}
#[export = "mul_u64_to_u128"]
fn mul_u64_to_u128(a: i64, b: i64) -> (i64, i64) {
i64::mul_wide_u(a, b);
}
Custom Descriptors
A struct can carry a descriptor: a second struct linked to it by reciprocal
descriptor/describes clauses. The
descriptor instructions never name the target type: it is recovered from the
descriptor operand (the type the operand describes), so all of them share one
descriptor(d) clause. struct.new_desc allocates carrying a descriptor,
.descriptor reads it back, and as [?]descriptor(d) / br_on_cast [?]descriptor(d)
cast on descriptor equality (a leading ? makes the result nullable).
Wax
rec {
type obj = descriptor obj_desc { x: i32 };
type obj_desc = describes obj { kind: i32 };
}
#[export = "make"]
fn make(kind: i32, x: i32) -> &!obj {
{ descriptor({obj_desc| kind})| x };
}
#[export = "kind_of"]
fn kind_of(o: &obj) -> i32 {
o.descriptor.kind;
}
#[export = "narrow"]
fn narrow(v: &?any, d: &!obj_desc) -> &?obj {
v as ?descriptor(d);
}
Branch hints
The branch-hinting proposal marks
a conditional branch likely or unlikely taken. Prefix the branch with
#[likely] / #[unlikely]; the hint is preserved through every conversion (no
feature flag required).
Wax
#[export = "clamp_low"]
fn clamp_low(x: i32) -> i32 {
#[likely] if x >=s 0 => i32 {
x;
} else {
0;
}
}
#[export = "drain"]
fn drain(n: i32) {
'l: loop {
#[unlikely] br_if 'l n;
}
}
Linear Memory
A memory declaration reserves linear memory. load/store methods on it read
and write, with the access width in the method name; a narrow (load8/load16)
load returns raw bits, so it needs an explicit sign/zero-extending cast.
Wax
memory mem: i32 [1];
#[export = "sum_bytes"]
fn sum_bytes(start: i32, end: i32) -> i32 {
let total: i32 = 0;
let p: i32 = start;
while p <u end {
total += mem.load8(p) as i32_u; // byte load, zero-extended
p += 1;
}
total;
}
SIMD
v128 vector operations are method intrinsics with the lane shape baked into the
name (mul_i32x4, add_i32x4, extract_lane_i32x4).
Wax
#[export = "madd_i32x4"]
fn madd_i32x4(a: v128, b: v128, c: v128) -> v128 {
a.mul_i32x4(b).add_i32x4(c); // a * b + c, four i32 lanes at once
}
#[export = "sum_lanes"]
fn sum_lanes(v: v128) -> i32 {
v.extract_lane_i32x4(0) + v.extract_lane_i32x4(1)
+ v.extract_lane_i32x4(2) + v.extract_lane_i32x4(3);
}
Stack Switching
A continuation type wraps a function type with cont. A coroutine suspends
with a value; a driver resumes it, routing the suspended tag to a handler
label that receives the payload and the paused continuation. Note that the
continuation’s type changes after a suspend, since resuming it now needs the
reply value.
Wax
type task = fn() -> i32;
type k = cont task;
type resumed = fn(i32) -> i32; // the continuation's type after a suspend
type kr = cont resumed;
tag yield(i32) -> i32;
// A coroutine: yield 10, then return the reply plus one.
fn worker() -> i32 {
let reply: i32 = suspend yield(10);
reply + 1;
}
// Run the worker until its first suspend and return the value it yielded.
#[export = "first_yield"]
fn first_yield() -> i32 {
let c: &?k = cont_new k (worker);
let (v, rest) =
'on_yield: do () -> (i32, &kr) {
_ = resume k [yield -> 'on_yield] (c!); // worker returned; drop its result
return -1;
};
_ = rest; // `rest` could be resumed with a reply to continue the worker
v;
}