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:
| Input | Output | Use Case |
|---|---|---|
.wax | .wasm | Compile to binary |
.wax | .wat | Compile to text |
.wax | .wax | Format / type-check |
.wat | .wasm | Assemble to binary |
.wat | .wax | Decompile to Wax |
.wat | .wat | Format |
.wasm | .wat | Disassemble |
.wasm | .wax | Decompile to Wax |
.wasm | .wasm | Round-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 — Variables, expressions, and control flow
- Correspondence — How Wax maps to WebAssembly
- CLI Reference — Complete command-line options
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
| Wasm | Wax | Notes |
|---|---|---|
i32 | i32 | 32-bit integer |
i64 | i64 | 64-bit integer |
f32 | f32 | 32-bit float |
f64 | f64 | 64-bit float |
v128 | v128 | 128-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.
| Wasm | Wax | Notes |
|---|---|---|
i8 | i8 | 8-bit integer (packed) |
i16 | i16 | 16-bit integer (packed) |
valtype | valtype | Any value type |
Heap Types
| Wasm | Wax |
|---|---|
func | func |
extern | extern |
any | any |
eq | eq |
struct | struct |
array | array |
i31 | i31 |
exn | exn |
noextern | noextern |
nofunc | nofunc |
noexn | noexn |
none | none |
<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.
| Wasm | Wax |
|---|---|
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
| Wasm | Wax |
|---|---|
f32.add / f64.add | + |
f32.sub / f64.sub | - |
f32.mul / f64.mul | * |
f32.div / f64.div | / |
f32.abs … f64.sqrt | val.abs, val.neg, val.ceil, val.floor, val.trunc, val.nearest, val.sqrt |
f32.min … f64.copysign | min(v1, v2), max(v1, v2), copysign(v1, v2) |
Advanced Integer Operations
| Wasm | Wax |
|---|---|
i32.clz … i64.popcnt | val.clz, val.ctz, val.popcnt |
i32.rotl … i64.rotr | rotl(v1, v2), rotr(v1, v2) |
Conversions
| Wasm | Wax |
|---|---|
i32.wrap_i64 | val as i32 |
i64.extend_i32_s | val as i64_s |
i64.extend_i32_u | val as i64_u |
i32.trunc_f32_s | val as i32_s |
f32.convert_i32_s | val as f32_s |
f32.demote_f64 | val as f32 |
f64.promote_f32 | val as f64 |
i32.reinterpret_f32 | val.to_bits |
f32.reinterpret_i32 | val.from_bits |
Comparison
| Wasm | Wax |
|---|---|
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
| Wasm | Wax |
|---|---|
local.get $x | x |
local.set $x | x = val |
local.tee $x | x := val |
global.get $g | g |
global.set $g | g = val |
(local $x t) | let x : t |
Control Instructions
| Wasm | Wax |
|---|---|
block | do { ... } or { ... } |
loop | loop { ... } |
if ... else ... | if cond { ... } else { ... } |
br $l | br 'l |
br_if $l | br_if 'l cond |
br_table $l* $ld | br_table ['l* else 'ld] val |
br_on_null $l | br_on_null 'l val |
br_on_non_null $l | br_on_non_null 'l val |
br_on_cast $l $t1 $t2 | br_on_cast 'l t2 val |
br_on_cast_fail $l $t1 $t2 | br_on_cast_fail 'l t2 val |
return | return val |
call $f | f(args) |
call_ref $t | (val as &?t)(args) |
return_call $f | become f(args) |
return_call_ref $t | become (val as &?t)(args) |
unreachable | unreachable |
nop | nop |
select | cond ? 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
| Wasm | Wax |
|---|---|
ref.null | null |
ref.is_null | !val |
ref.as_non_null | val! |
ref.i31 | val as &i31 |
i31.get_s | val as i32_s |
i31.get_u | val as i32_u |
ref.cast | val as &type |
ref.test | val is &type |
Aggregate Instructions
| Wasm | Wax |
|---|---|
struct.new $t | {t| field: val, ... } |
struct.new_default $t | {t| .. } |
struct.get $t $f | val.field |
struct.set $t $f | val.field = new_val |
array.new $t | [t| val; len] |
array.new_default $t | [t| ..; len] |
array.new_fixed $t | [t| val, ...] |
array.get $t | arr[idx] |
array.get_s $t | arr[idx] as i32_s |
array.get_u $t | arr[idx] as i32_u |
array.set $t | arr[idx] = val |
array.len | arr.length |
Exception Instructions
| Wasm | Wax |
|---|---|
throw $tag | throw tag(args) |
throw_ref | throw_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
v128vector instructions. Thev128type exists but operations are not exposed. - Indirect Calls via Tables:
return_call_indirect,call_indirect. Usecall_refwith 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,
waxreads fromstdin.
- If omitted,
Options
-
-o,--outputFILE- Output file. Writes to
stdoutif not specified.
- Output file. Writes to
-
-f,--format,--output-formatFORMAT- Specify the output format.
- Values:
wax,wat,wasm. - Default:
wasm(if not auto-detected from output filename).
-
-i,--input-formatFORMAT- Specify the input format.
- Values:
wax,wat,wasm. - Default: Auto-detected from input filename, or
waxif 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.
-
--colorWHEN- 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-fileFILE- 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