Manual
WasmTarget.jl collects the closed world of your entry points with the upstream trim machinery (Compiler.typeinf_ext_toplevel, the same collection behind juliac --trim), then translates each function's fully-inferred IR — and each concrete Julia type — to its WASM counterpart. This page covers type mappings, control flow, the supported math + collections surfaces, and JS interop.
Type Mappings
Primitive Types
| Julia Type | WASM Type | Notes |
|---|---|---|
Int32, UInt32 | i32 | 32-bit integer |
Int64, UInt64, Int | i64 | Int is Int64 on 64-bit |
Float32 | f32 | 32-bit float |
Float64 | f64 | 64-bit float |
Bool | i32 | 0 or 1 |
Reference Types
| Julia Type | WASM Type | Notes |
|---|---|---|
String | WasmGC packed (array (mut i8)) | UTF-8 bytes; array.get_u widens to i32 on the stack |
struct Foo … end | WasmGC struct | Fields map directly |
Tuple{A, B, …} | WasmGC struct | Immutable struct |
Vector{T} | WasmGC struct{array, length} | Mutable array with length |
Matrix{T} | WasmGC struct{array, sizes} | Data array + size tuple |
JSValue | externref | Opaque JS object reference |
Struct Mapping
Julia structs become WasmGC struct types with fields in declaration order:
struct Point
x::Float64
y::Float64
endBecomes a WasmGC struct type with two f64 fields. Mutable structs (mutable struct) work the same way but allow field mutation via struct.set.
Vector Mapping
Vector{T} is represented as a WasmGC struct containing a WasmGC array of the element type and a length field (i32). Mirrors Julia's internal representation; allows efficient element access and length queries.
Strings: packed i8, not i32
WASM has no top-level i8 type — stack values are always i32 / i64 / f32 / f64. The i8 form only exists (a) as a packed array element type and (b) as linear-memory load/store widths. WasmTarget stores String as (array (mut i8)) holding UTF-8 bytes; reads use array.get_u which zero-extends each byte to i32 on the stack, so arithmetic (e.g. inside str_char(s, i)::Int32) happens at i32 width with no truncation cost.
An (array (mut i16)) type also appears in compiled modules — it's purely the JS-boundary bridge. wasm:js-string.fromCharCodeArray (Chrome 131+ / Node 23+) takes UTF-16 char codes, so an i8 → i16 widen happens once at the println / format-output boundary. Internal strings stay UTF-8 i8 throughout.
JSValue + WasmGlobal
JSValue is a primitive type that maps to WASM externref — an opaque handle to a JavaScript value (DOM element, JS object, function reference). WasmGlobal{T, IDX} is a type-safe handle for WASM global variables; T sets the value type and IDX is the compile-time global index. See JS Interop below.
Math Functions
All 43 tested math functions compile and produce correct results, verified with both Float32 and Float64 (except exp(Float32) — one known codegen issue). Julia 1.12 implements math in pure Julia (no foreigncall to libm), so they compile directly to WASM without runtime dependencies.
| Category | Functions | Path |
|---|---|---|
| Trigonometric | sin, cos, tan, asin, acos, atan | Native |
| Hyperbolic | sinh, cosh, tanh | Native |
| Exponential | exp, exp2, expm1 | Native |
| Logarithmic | log, log2, log10, log1p | Native |
| Rounding | floor, ceil, round, trunc | Native |
| Roots/Powers | sqrt, cbrt, hypot, fourthroot | Native |
| Special | sincos, sinpi, cospi, tanpi, sinc, cosc, modf | Native |
| Utility | copysign, deg2rad, rad2deg, ldexp, mod2pi | Native |
| Power | Float64^Float64, Float64^Int | Native |
| Float mod/rem | mod(Float64), rem(Float64) | Overlay |
using WasmTarget
bytes = compile(sin, (Float64,))
write("sin.wasm", bytes)
# wasm-opt typically yields ~80-90% size reduction for math
opt = compile(sin, (Float64,); optimize=true)Collections
All 26 tested collection functions compile and produce correct results, verified with Vector{Int64} and Vector{Float64}.
| Function | Path | Notes |
|---|---|---|
sort, sort! | Overlay | Full kwarg support (rev=true) |
filter | Overlay | Predicate closures |
map | Native | Closures compile correctly |
reduce, foldl, foldr | Native | |
sum, prod | Native | |
minimum, maximum, extrema | Native | |
any, all | Native | Predicate closures |
count | Overlay | |
unique | Overlay | |
accumulate | Native | |
findmax, findmin | Native | |
argmax, argmin | Overlay | |
mapreduce | Native | |
foreach | Overlay | Ref mutation pattern |
reverse | Native | (Vector) |
using WasmTarget
f_sort(v::Vector{Int64}) = sort(v, rev=true)
f_filter(v::Vector{Int64}) = filter(iseven, v)
f_map(v::Vector{Int64}) = map(x -> x * 2, v)
bytes = compile_multi([
(f_sort, (Vector{Int64},)),
(f_filter, (Vector{Int64},)),
(f_map, (Vector{Int64},)),
])
# 8-deep chain — all verified E2E
f(v::Vector{Int64})::Int64 = sum(unique(sort(filter(x -> x > 0, map(abs, accumulate(+, reverse(v)))))))All 16 mutation functions work via overlays: push!, pop!, pushfirst!, popfirst!, insert!, deleteat!, append!, prepend!, splice!, resize!, empty!, fill!, copy, reverse, length, vec.
Structs & Tuples
User-defined structs and tuples compile to WasmGC struct types.
Structs
struct Point
x::Float64
y::Float64
end
function distance(p::Point)::Float64
return sqrt(p.x * p.x + p.y * p.y)
end
bytes = compile(distance, (Point,))The compiler automatically registers Point as a WasmGC struct type with two f64 fields and generates struct.new / struct.get instructions.
Mutable Structs
mutable struct Counter
value::Int32
end
function increment!(c::Counter)::Int32
c.value = c.value + Int32(1)
return c.value
end
bytes = compile(increment!, (Counter,))Mutable struct fields use struct.set for assignment.
Nested Structs
Nested struct types are registered recursively:
struct Color
r::Int32; g::Int32; b::Int32
end
struct Pixel
pos::Point
color::Color
end
pixel_x(p::Pixel)::Float64 = p.pos.x
bytes = compile(pixel_x, (Pixel,))Tuples & NamedTuples
Tuples compile as immutable WasmGC structs. Each element becomes a struct field; index access (t[1], t[2]) compiles to struct.get with the appropriate field index. Named tuples work like regular tuples — names are erased in the IR.
function swap(t::Tuple{Int32, Int32})::Tuple{Int32, Int32}
return (t[2], t[1])
end
function get_name(nt::NamedTuple{(:x, :y), Tuple{Float64, Float64}})::Float64
return nt.x + nt.y
endRecursive Structs
Self-referential types are supported — the compiler handles recursive type registration by creating forward references in the WasmGC type section.
mutable struct Node
value::Int32
next::Union{Node, Nothing}
endControl Flow
WasmTarget handles all Julia control flow patterns by translating the compiler's IR (GotoNode, GotoIfNot, PhiNode) into WASM structured control flow (block, loop, br, br_if).
If / Else, While, For
function clamp_positive(x::Int32)::Int32
if x > Int32(0); return x; else; return Int32(0); end
end
function sum_to(n::Int32)::Int32
total = Int32(0); i = Int32(1)
while i <= n
total += i; i += Int32(1)
end
return total
end
# For loops over ranges lower to while loops in the IR — compile identically.
function sum_range(n::Int32)::Int32
total = Int32(0)
for i in Int32(1):n
total += i
end
return total
endShort-Circuit Operators
&& and || compile correctly, including complex chains. They use WASM block / br_if patterns for short-circuit evaluation.
check(a::Int32, b::Int32, c::Int32)::Bool =
a > Int32(0) && b > Int32(0) && c > Int32(0)
any_positive(a::Int32, b::Int32)::Bool =
a > Int32(0) || b > Int32(0)Try / Catch / Throw
Exception handling uses WASM's try_table and throw instructions:
function safe_div(a::Int32, b::Int32)::Int32
try
if b == Int32(0)
throw(DivideError())
end
return div(a, b)
catch
return Int32(-1)
end
endRecursion + Stackifier
Self-recursive functions compile with the function calling itself by index. Functions with many conditional branches (e.g. Julia's sin implementation with 15+ GotoIfNot) use a stackifier algorithm that converts arbitrary CFG patterns to WASM structured control flow using nested block / loop / br instructions.
function factorial(n::Int32)::Int32
if n <= Int32(1); return Int32(1); end
return n * factorial(n - Int32(1))
endJS Interop
JSValue (externref)
JSValue is a primitive type that maps to WASM's externref. It represents an opaque handle to any JavaScript value:
using WasmTarget
# JSValue appears in function signatures
function process(el::JSValue, count::Int32)::Int32
# el is an opaque JS reference
return count + Int32(1)
endImporting JS Functions
Use add_import! on a WasmModule to declare functions the host (JavaScript) must provide. Two overloads exist — pure-numeric (NumType only) and the generalized WasmValType overload required when an ExternRef (a RefType) appears in the signature.
mod = WasmModule()
# ExternRef is a RefType — use WasmValType[…] to hit the right overload.
add_import!(mod, "dom", "set_text", WasmValType[ExternRef, I32], WasmValType[])
add_import!(mod, "dom", "get_value", WasmValType[ExternRef], WasmValType[I32])
# Pure numeric imports can use plain NumType vectors
add_import!(mod, "math", "add", [I32, I32], [I32])Provide the imports when instantiating in JS:
const imports = {
dom: {
set_text: (el, text) => { el.textContent = String(text); },
get_value: (el) => parseInt(el.value) || 0,
},
};
const { instance } = await WebAssembly.instantiate(bytes, imports);Exporting Functions
Compiled functions are automatically exported by name. compile_multi accepts an optional custom name:
increment(x::Int32)::Int32 = x + Int32(1)
bytes = compile(increment, (Int32,))
# instance.exports.increment(5) => 6
bytes = compile_multi([
(increment, (Int32,), "inc"),
])
# instance.exports.inc(5) => 6WasmGlobal{T, IDX}
WasmGlobal{T, IDX} provides type-safe mutable global variables. IDX is the compile-time WASM global index:
const Counter = WasmGlobal{Int32, 0}
const Threshold = WasmGlobal{Int32, 1}
function increment(g::Counter)::Int32
g[] = g[] + Int32(1)
return g[]
end
function check(g::Counter, t::Threshold)::Bool
return g[] >= t[]
end
bytes = compile_multi([
(increment, (Counter,)),
(check, (Counter, Threshold)),
])- Phantom parameters.
WasmGlobalarguments do not become WASM function parameters. - Auto-created. The compiler adds globals to the module.
- Julia-testable.
g[] = xandg[]work in Julia for testing. - Shared state. Multiple functions in the same
compile_multishare globals.
Manual Vector Bridge (not auto-generated)
When a function operates on Vector{T}, JavaScript cannot directly create WasmGC array references. You must manually compile bridge functions alongside your code using compile_multi. The compiler does not auto-export vec_new / vec_get / vec_set / vec_len.
# Your actual function
my_sum(v::Vector{Float64})::Float64 = sum(v)
# Bridge functions — you write these yourself
bv_new(n::Int64)::Vector{Float64} = Vector{Float64}(undef, n)
bv_set!(v::Vector{Float64}, i::Int64, val::Float64)::Int64 = (v[i] = val; Int64(0))
bv_get(v::Vector{Float64}, i::Int64)::Float64 = v[i]
bv_len(v::Vector{Float64})::Int64 = Int64(length(v))
# Compile everything together so they share the same WasmGC type space
bytes = compile_multi([
(my_sum, (Vector{Float64},)),
(bv_new, (Int64,)),
(bv_set!, (Vector{Float64}, Int64, Float64)),
(bv_get, (Vector{Float64}, Int64)),
(bv_len, (Vector{Float64},)),
])const e = instance.exports;
const v = e.bv_new(3n); // BigInt for i64
e["bv_set!"](v, 1n, 1.0); // 1-based indexing (Julia)
e["bv_set!"](v, 2n, 2.0);
e["bv_set!"](v, 3n, 3.0);
console.log(e.my_sum(v)); // 6.0Tables, Indirect Calls, Memory
WASM tables (funcref / externref) enable dynamic dispatch. call_indirect looks up a function in the table at runtime — the foundation for multiple dispatch in WASM.
mod = WasmModule()
add_table!(mod, FuncRef, 10) # Table of 10 function references
add_table!(mod, ExternRef, 5) # Table of 5 externref slots
add_table!(mod, FuncRef, 4, 16) # min=4, max=16For low-level control, linear memory + data segments are also available — but most use cases should prefer WasmGC types (structs, arrays):
add_memory!(mod, 1) # 1 page (64KB)
add_data_segment!(mod, 0, UInt8[0x48, 0x65, 0x6c, 0x6c, 0x6f]) # "Hello"