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 TypeWASM TypeNotes
Int32, UInt32i3232-bit integer
Int64, UInt64, Inti64Int is Int64 on 64-bit
Float32f3232-bit float
Float64f6464-bit float
Booli320 or 1

Reference Types

Julia TypeWASM TypeNotes
StringWasmGC packed (array (mut i8))UTF-8 bytes; array.get_u widens to i32 on the stack
struct Foo … endWasmGC structFields map directly
Tuple{A, B, …}WasmGC structImmutable struct
Vector{T}WasmGC struct{array, length}Mutable array with length
Matrix{T}WasmGC struct{array, sizes}Data array + size tuple
JSValueexternrefOpaque JS object reference

Struct Mapping

Julia structs become WasmGC struct types with fields in declaration order:

struct Point
    x::Float64
    y::Float64
end

Becomes 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.

CategoryFunctionsPath
Trigonometricsin, cos, tan, asin, acos, atanNative
Hyperbolicsinh, cosh, tanhNative
Exponentialexp, exp2, expm1Native
Logarithmiclog, log2, log10, log1pNative
Roundingfloor, ceil, round, truncNative
Roots/Powerssqrt, cbrt, hypot, fourthrootNative
Specialsincos, sinpi, cospi, tanpi, sinc, cosc, modfNative
Utilitycopysign, deg2rad, rad2deg, ldexp, mod2piNative
PowerFloat64^Float64, Float64^IntNative
Float mod/remmod(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}.

FunctionPathNotes
sort, sort!OverlayFull kwarg support (rev=true)
filterOverlayPredicate closures
mapNativeClosures compile correctly
reduce, foldl, foldrNative
sum, prodNative
minimum, maximum, extremaNative
any, allNativePredicate closures
countOverlay
uniqueOverlay
accumulateNative
findmax, findminNative
argmax, argminOverlay
mapreduceNative
foreachOverlayRef mutation pattern
reverseNative(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
end

Recursive 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}
end

Control 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
end

Short-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
end

Recursion + 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))
end

JS 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)
end

Importing 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) => 6

WasmGlobal{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. WasmGlobal arguments do not become WASM function parameters.
  • Auto-created. The compiler adds globals to the module.
  • Julia-testable. g[] = x and g[] work in Julia for testing.
  • Shared state. Multiple functions in the same compile_multi share 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.0

Tables, 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=16

For 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"