Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Wax is a Rust-like syntax for WebAssembly that compiles to standard Wasm binary or text formats. It provides a more familiar programming experience while maintaining a direct correspondence to WebAssembly concepts.

Why Wax?

WebAssembly Text format (WAT) uses S-expressions and stack-based operations, which can be verbose and unfamiliar to most programmers:

(func $add (param $x i32) (param $y i32) (result i32)
  local.get $x
  local.get $y
  i32.add)

Wax provides an expression-oriented syntax that feels more natural:

fn add(x: i32, y: i32) -> i32 {
    x + y
}

Both compile to identical WebAssembly bytecode.

Installation

Requirements: Opam (2.1+) and OCaml 5.0+.

# Install dependencies
opam install . --deps-only

# Build
dune build

# Install globally
opam install .

Quick Start

Create a file hello.wax:

#[export = "add"]
fn add(x: i32, y: i32) -> i32 {
    x + y
}

#[export = "factorial"]
fn factorial(n: i32) -> i32 {
    if n <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}

Compile to WebAssembly binary:

wax hello.wax -o hello.wasm

Or convert to WebAssembly text format to see the generated WAT:

wax hello.wax -f wat

Supported Conversions

Wax supports all 9 combinations of input and output formats:

InputOutputUse Case
.wax.wasmCompile to binary
.wax.watCompile to text
.wax.waxFormat / type-check
.wat.wasmAssemble to binary
.wat.waxDecompile to Wax
.wat.watFormat
.wasm.watDisassemble
.wasm.waxDecompile to Wax
.wasm.wasmRound-trip

Type Checking

Enable type checking with the -v flag:

wax hello.wax -v -o hello.wasm

This catches type errors before generating output:

error: type mismatch
  --> hello.wax:3:5
   |
 3 |     x + y
   |     ^^^^^ expected f32, found i32

Next Steps

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.

Examples

Complete examples demonstrating Wax features and their WebAssembly equivalents.

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 <= 1 {
        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 <= 1 {
        acc
    } else {
        become factorial_helper(n - 1, n * acc)
    }
}

Loop with Early Exit

Wax

#[export = "find_first_zero"]
fn find_first_zero(arr: &[i32]) -> i32 {
    let i: i32;
    let len: i32;
    len = arr.length;
    i = 0;
    'search: loop {
        if i >= len {
            return -1
        }
        if arr[i] == 0 {
            return i
        }
        i = i + 1;
        br 'search;
    }
}

Equivalent WAT

(func $find_first_zero (export "find_first_zero")
      (param $arr (ref $array_i32)) (result i32)
  (local $i i32)
  (local $len 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))

Structs and Methods

Wax

type point = { x: i32, y: i32 };

#[export = "make_point"]
fn make_point(x: i32, y: i32) -> &point {
    {point| x: x, y: y}
}

#[export = "distance_squared"]
fn distance_squared(p1: &point, p2: &point) -> i32 {
    let dx: i32;
    let dy: i32;
    dx = p1.x - p2.x;
    dy = p1.y - p2.y;
    dx * dx + dy * dy
}

Mutable Structs

Wax

type counter = { mut value: 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
}

Arrays

Wax

type bytes = [mut i8];

#[export = "sum_bytes"]
fn sum_bytes(arr: &bytes) -> i32 {
    let sum: i32;
    let i: i32;
    let len: i32;
    sum = 0;
    i = 0;
    len = arr.length;
    loop {
        if i >= len {
            return sum
        }
        sum = sum + arr[i] as i32_u;
        i = i + 1;
        br 'loop;
    }
}

#[export = "fill_bytes"]
fn fill_bytes(arr: &bytes, val: i32) {
    let i: i32;
    let len: i32;
    i = 0;
    len = arr.length;
    loop {
        if i >= len {
            return
        }
        arr[i] = val as i8;
        i = 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 {
    'handler: do i32 {
        try {
            become safe_divide(a, b)
        } catch [divide_by_zero -> 'handler]
        unreachable
    }
    0   // Return 0 on division by zero
}

Recursive Types (Linked List)

Wax

rec {
    type node = { value: i32, next: &?list };
    type list = node;
}

#[export = "make_node"]
fn make_node(value: i32, next: &?list) -> &node {
    {node| value: value, next: next}
}

#[export = "sum_list"]
fn sum_list(head: &?list) -> i32 {
    if !head {
        return 0
    }
    let n: &node;
    n = head!;
    n.value + sum_list(n.next)
}

Imports and Exports

Wax

// Import a function from the host environment
#[import = ("env", "log")]
fn log(value: i32);

// Import a global
#[import = ("env", "base_value")]
const base_value: i32;

// Export a function that uses the imports
#[export = "compute_and_log"]
fn compute_and_log(x: i32) -> i32 {
    let result: i32;
    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 {
    rotl(rotl(d * 0xcc9e2d51, 15) * 0x1b873593 ^ h, 13) * 5 + 0xe6546b64
}

#[export = "hash_finalize"]
fn hash_finalize(h: i32) -> i32 {
    h = h ^ h >>u 16;
    h = h * 0x85ebca6b;
    h = h ^ h >>u 13;
    h = h * 0xc2b2ae35;
    h ^ h >>u 16
}

Introduction

This guide documents the correspondence between WebAssembly (Wasm) and Wax constructs. It is intended for developers who are familiar with Wasm and want to understand how Wax maps to it.

High-level differences

Wax is an expression-oriented language, whereas Wasm is stack-oriented. However, Wax constructs map very closely to Wasm instructions, often one-to-one or with simple desugaring.

Key differences:

  • Expressions vs Stack: Wax uses variables (let) and nested expressions instead of explicit stack manipulation (local.get/set, drop).
  • Control Flow: Wax uses structured control flow (if, loop, do, try) that resembles high-level languages but maps directly to Wasm structured control instructions.
  • Types: Wax types define a direct mapping to Wasm types, with a slightly more concise syntax.
  • Modules: Wax modules use attributes to handle imports and exports, rather than separate import/export definitions.

Types

Wax types map directly to WebAssembly types.

Value Types

WasmWaxNotes
i32i3232-bit integer
i64i6464-bit integer
f32f3232-bit float
f64f6464-bit float
v128v128128-bit vector
(ref null <ht>)&?<ht>Nullable reference to heap type <ht>
(ref <ht>)&<ht>Non-nullable reference to heap type <ht>

Storage Types

Storage types are used in fields of structs and arrays to define packed data.

WasmWaxNotes
i8i88-bit integer (packed)
i16i1616-bit integer (packed)
valtypevaltypeAny value type

Heap Types

WasmWax
funcfunc
externextern
anyany
eqeq
structstruct
arrayarray
i31i31
exnexn
noexternnoextern
nofuncnofunc
noexnnoexn
nonenone
<typeidx><identifier>

Composite Types

Recursive Types

Wax allows defining recursive reference types using rec { ... }.

rec {
    type list = { head: i32, tail: node }
    type node = [list]
}

Structs

type point = { x: i32, y: i32 }
type mutable_point = { mut x: i32, mut y: i32 }

Maps to Wasm (type $point (struct (field i32) (field i32))).

Arrays

type bytes = [i8]
type mutable_bytes = [mut i8]

Maps to Wasm (type $bytes (array i8)).

Functions

type binop = fn(_: i32, _: i32) -> i32

Maps to Wasm (type $binop (func (param i32 i32) (result i32))).

Supertypes and Finality

Types are final by default. To make a type open (extensible), use the open keyword. To specify a supertype, use : supertype before the assignment.

type point = { x: i32, y: i32 }
type point3d : point = { z: i32 }        ;; extend point (if point was open)
type open_point = open { x: i32 }        ;; non-final type
type sub_point : open_point = { y: i32 } ;; ok

Instructions

Wax instructions are expression-oriented.

Numeric Instructions

Binary and unary operations use standard mathematical operators. Signedness is often explicit in the operator.

WasmWax
i32.add / i64.add+
i32.sub / i64.sub-
i32.mul / i64.mul*
i32.div_s / i64.div_s/s
i32.div_u / i64.div_u/u
i32.rem_s / i64.rem_s%s
i32.rem_u / i64.rem_u%u
i32.and / i64.and&
i32.or / i64.or|
i32.xor / i64.xor^
i32.shl / i64.shl<<
i32.shr_s / i64.shr_s>>s
i32.shr_u / i64.shr_u>>u
i32.eqz / i64.eqz!x (logical not)

Floating Point Operations

WasmWax
f32.add / f64.add+
f32.sub / f64.sub-
f32.mul / f64.mul*
f32.div / f64.div/
f32.absf64.sqrtval.abs, val.neg, val.ceil, val.floor, val.trunc, val.nearest, val.sqrt
f32.minf64.copysignmin(v1, v2), max(v1, v2), copysign(v1, v2)

Advanced Integer Operations

WasmWax
i32.clzi64.popcntval.clz, val.ctz, val.popcnt
i32.rotli64.rotrrotl(v1, v2), rotr(v1, v2)

Conversions

WasmWax
i32.wrap_i64val as i32
i64.extend_i32_sval as i64_s
i64.extend_i32_uval as i64_u
i32.trunc_f32_sval as i32_s
f32.convert_i32_sval as f32_s
f32.demote_f64val as f32
f64.promote_f32val as f64
i32.reinterpret_f32val.to_bits
f32.reinterpret_i32val.from_bits

Comparison

WasmWax
eq==
ne!=
lt_s / lt<s / <
lt_u<u
le_s / le<=s / <=
le_u<=u
gt_s / gt>s / >
gt_u>u
ge_s / ge>=s / >=
ge_u>=u

Variable Instructions

WasmWax
local.get $xx
local.set $xx = val
local.tee $xx := val
global.get $gg
global.set $gg = val
(local $x t)let x : t

Control Instructions

WasmWax
blockdo { ... } or { ... }
looploop { ... }
if ... else ...if cond { ... } else { ... }
br $lbr 'l
br_if $lbr_if 'l cond
br_table $l* $ldbr_table ['l* else 'ld] val
br_on_null $lbr_on_null 'l val
br_on_non_null $lbr_on_non_null 'l val
br_on_cast $l $t1 $t2br_on_cast 'l t2 val
br_on_cast_fail $l $t1 $t2br_on_cast_fail 'l t2 val
returnreturn val
call $ff(args)
call_ref $t(val as &?t)(args)
return_call $fbecome f(args)
return_call_ref $tbecome (val as &?t)(args)
unreachableunreachable
nopnop
selectcond ? v1 : v2

Block Labels and Types

Blocks, loops, and ifs can be labeled and typed. Labels are identifiers starting with ' followed by a colon.

'my_block: do { ... }
'my_loop: loop { ... }

Block types (signatures) can be specified using (param) -> result syntax or a single value type. If no type is specified, the do keyword is optional.

do (i32) -> i32 { ... }
do i32 { ... }
{ ... }                  ;; equivalent to do { ... }
if cond => (i32) -> i32 { ... } else { ... }

Reference Instructions

WasmWax
ref.nullnull
ref.is_null!val
ref.as_non_nullval!
ref.i31val as &i31
i31.get_sval as i32_s
i31.get_uval as i32_u
ref.castval as &type
ref.testval is &type

Aggregate Instructions

WasmWax
struct.new $t{t| field: val, ... }
struct.new_default $t{t| .. }
struct.get $t $fval.field
struct.set $t $fval.field = new_val
array.new $t[t| val; len]
array.new_default $t[t| ..; len]
array.new_fixed $t[t| val, ...]
array.get $tarr[idx]
array.get_s $tarr[idx] as i32_s
array.get_u $tarr[idx] as i32_u
array.set $tarr[idx] = val
array.lenarr.length

Exception Instructions

WasmWax
throw $tagthrow tag(args)
throw_refthrow_ref
try_table ... catch $tag $l ...try { ... } catch [ tag -> 'l, ... ]
try_table ... catch_ref $tag $l ...try { ... } catch [ tag & -> 'l, ... ]
try_table ... catch_all $l ...try { ... } catch [ _ -> 'l, ... ]
try_table ... catch_all_ref $l ...try { ... } catch [ _ & -> 'l, ... ]
try ... catch $tag ...try { ... } catch { tag => { ... } ... }
try ... catch_all ...try { ... } catch { _ => { ... } }

The try { ... } catch [ ... ] syntax compiles to WebAssembly’s try_table instruction (the current standard). The try { ... } catch { tag => { ... } } syntax compiles to the older try/catch instructions (deprecated but still supported for compatibility).

Unsupported Features

The following WebAssembly features do not have dedicated Wax syntax. When converting from WAT/WASM to Wax, these instructions are preserved as-is or may be dropped:

  • Linear Memory: memory.size, memory.grow, load, store (all variants). Wax focuses on GC-based memory management using structs and arrays.
  • Tables: table.get, table.set, call_indirect (and related instructions). Use typed function references instead.
  • SIMD: All v128 vector instructions. The v128 type exists but operations are not exposed.
  • Indirect Calls via Tables: return_call_indirect, call_indirect. Use call_ref with function references instead.

Module Fields

Wax modules are defined by a sequence of top-level fields: types, functions, globals, and tags.

Types

Types are defined using type for single definitions or rec { ... } for mutually recursive types.

Simple Types

type point = { x: i32, y: i32 };
type bytes = [i8];
type callback = fn(_: i32) -> i32;

Recursive Types

Use rec when types reference each other:

rec {
    type node = { value: i32, next: &?list };
    type list = node;
}

Supertypes and Finality

Types are final (non-extensible) by default. Use open to allow subtypes:

type open shape = { x: i32, y: i32 };
type circle : shape = { radius: i32 };

Maps to:

(type $shape (sub (struct (field $x i32) (field $y i32))))
(type $circle (sub $shape (struct (field $x i32) (field $y i32) (field $radius i32))))

Functions

Definition

fn name(param: type, ...) -> return_type {
    body
}

Functions without a return type:

fn log_value(x: i32) {
    // no return
}

Function Signatures (Declarations)

Declare a function signature without a body (used with imports):

fn external_function(x: i32) -> i32;

Block Type Annotation

Functions can have a block type on the body:

fn example() -> i32 'label: {
    // body can use 'label
}

Globals

Immutable Globals

const PI: f64 = 3.14159;
const MAX_SIZE: i32 = 1024;

Mutable Globals

let mut counter: i32 = 0;
let mut state: &?any = null;

Imported Globals

#[import = ("env", "base")]
const base: i32;

#[import = ("env", "counter")]
let mut counter: i32;

Tags

Tags define exception types for structured exception handling.

Definition

tag my_error(code: i32);
tag empty_error();
tag multi_arg(a: i32, b: &string);

Maps to:

(tag $my_error (param i32))
(tag $empty_error)
(tag $multi_arg (param i32) (param (ref $string)))

Imported Tags

#[import = ("env", "js_error")]
tag js_error(&?extern);

Attributes

Attributes modify module fields. They use the syntax #[name = value] and appear before the field they modify.

Export Attribute

Export a field with a given name:

#[export = "add"]
fn add(x: i32, y: i32) -> i32 { x + y }

#[export = "PI"]
const PI: f64 = 3.14159;

#[export = "my_error"]
tag my_error(code: i32);

Multiple exports can share the same function:

#[export = "add"]
#[export = "plus"]
fn add(x: i32, y: i32) -> i32 { x + y }

Import Attribute

Import a field from a module. Takes a tuple of ("module", "name"):

#[import = ("env", "log")]
fn log(msg: i32);

#[import = ("env", "memory_base")]
const memory_base: i32;

#[import = ("env", "error")]
tag error(code: i32);

Combined Import and Export

A field can be both imported and re-exported:

#[import = ("env", "value")]
#[export = "value"]
const value: i32;

Tables and Memories

Wax focuses on GC-based memory management and does not provide dedicated syntax for linear memory or tables. When converting from WAT/WASM:

  • Memory definitions are dropped (Wax uses GC structs and arrays)
  • Table definitions are dropped (Wax uses typed function references)

If you need linear memory or tables, write that portion in WAT and link it with your Wax code.

Module Structure

A typical Wax module follows this structure:

// 1. Type definitions
type point = { x: i32, y: i32 };
type callback = fn(_: i32) -> i32;

// 2. Imported globals and functions
#[import = ("env", "log")]
fn log(value: i32);

#[import = ("env", "base")]
const base: i32;

// 3. Tags
tag my_error(code: i32);

// 4. Module globals
const FACTOR: i32 = 100;
let mut counter: i32 = 0;

// 5. Internal functions
fn helper(x: i32) -> i32 {
    x * FACTOR
}

// 6. Exported functions
#[export = "compute"]
fn compute(x: i32) -> i32 {
    counter = counter + 1;
    log(counter);
    helper(x + base)
}

This order is conventional but not required; Wax allows fields in any order.

CLI Reference

The wax binary is the primary interface for the Wax toolchain. It supports conversion between Wax, WebAssembly Text (WAT), and WebAssembly Binary (Wasm) formats.

Usage

wax [OPTIONS] [INPUT]

Positional Arguments

  • [INPUT]: Source file to convert/format. Supported extensions: .wax, .wat, .wasm.
    • If omitted, wax reads from stdin.

Options

  • -o, --output FILE

    • Output file. Writes to stdout if not specified.
  • -f, --format, --output-format FORMAT

    • Specify the output format.
    • Values: wax, wat, wasm.
    • Default: wasm (if not auto-detected from output filename).
  • -i, --input-format FORMAT

    • Specify the input format.
    • Values: wax, wat, wasm.
    • Default: Auto-detected from input filename, or wax if reading from stdin.
  • -v, --validate

    • Perform validation during conversion.
    • For Wax: Runs type checking.
    • For Wasm Text: Runs well-formedness checks.
    • Disabled by default.
  • -s, --strict-validate

    • Perform strict reference validation (for Wasm Text). Overrides default relaxed validation.
  • --color WHEN

    • Colorize output.
    • Values: always, never, auto.
    • Default: auto (colors enabled only if output is a TTY).
  • --fold

    • Fold instructions into nested S-expressions.
    • Applies typically to Wasm Text output.
  • --unfold

    • Unfold instructions into flat instruction lists.
    • Applies typically to Wasm Text output.
  • --source-map-file FILE

    • Generate a source map file.

Examples

Convert a Wax file to Wasm binary:

wax input.wax -o output.wasm

Convert a Wasm Text file to Wax (decompilation):

wax input.wat -o output.wax

Format a Wax file (round-trip):

wax input.wax -f wax

Read from stdin and write to stdout:

cat input.wax | wax -f wasm > output.wasm