WebAssembly
The same runtime that compiles native ELF and Mach-O binaries also compiles to a self-contained .wasm module. wlift.js is the JavaScript harness you import to run Wren code in a browser — used by the playground and ready for any embedder. A program can also be compiled ahead of time into its own module.
Add wlift.js to your page
Wrenlift hosts the harness at wrenlift.com/dist/:
<script type="module">
import { createWlift } from "https://wrenlift.com/dist/wlift.js";
const wlift = await createWlift();
</script>
The harness lazily loads the matching
wlift_wasm_bg.wasm module from the same origin.
It auto-detects support for the WebAssembly SIMD proposal
and loads the faster simd128 variant when
available; older browsers get the scalar fallback.
Hosting it yourself? Copy the four files into your
/dist/:
wlift.jswlift_wasm.jswlift_wasm_bg.wasmwlift_wasm_bg.simd128.wasm
Hello world
<script type="module">
import { createWlift } from "/dist/wlift.js";
const wlift = await createWlift();
const result = await wlift.run('System.print("hello, web")');
console.log(result.output); // "hello, web\n"
</script>
result is a
{ ok, output, errorKind, elapsedMs } shape.
output captures everything the script wrote to
System.print; errorKind is non-null
when the program threw.
runProject — source + hatchfile
For anything beyond a one-liner, you pass the program source and its hatchfile so dependencies install through the registry:
import { createWlift, runProject } from "/dist/wlift.js";
const wlift = await createWlift();
const source = `
import "@hatch:json" for JSON
System.print(JSON.encode({"hello": "world"}))
`;
const hatchfile = `
name = "demo"
version = "0.1.0"
entry = "main"
[dependencies]
"@hatch:json" = "0.1.4"
`;
const result = await runProject(wlift, source, hatchfile);
console.log(result.output);
runProject parses the manifest, fetches every
declared dep from the Hatch registry, installs them, then
runs the user source. It returns the same result shape as
wlift.run.
Bundle caching
Fetched .hatch bundles are cached via the
browser's Cache API by default. The next page load resolves
cached deps instantly, no network round-trip. Override the
cache name or opt out for tests:
// Default cache name:
await runProject(wlift, source, hatchfile);
// Per-tenant cache:
await runProject(wlift, source, hatchfile, { cache: "tenant-abc" });
// Disable persistence:
await runProject(wlift, source, hatchfile, { cache: false });
Wipe the stored bundles when a release goes wrong:
import { clearHatchCache } from "/dist/wlift.js";
await clearHatchCache();
Prefetching deps
Warm the cache while the user is still typing —
running the code itself stays snappy because the bundle
bytes are already on disk by the time
runProject needs them:
import { prefetchHatchDeps } from "/dist/wlift.js";
editor.on("change", debounce(() => {
prefetchHatchDeps(getHatchfileText())
.catch(err => console.warn("prefetch", err));
}, 600));
Worker vs main thread
Wrenlift runs on whatever thread createWlift
is called from. The runtime is cooperative — a
long-running script blocks the thread until it suspends
on a fiber yield or finishes. For UI-heavy pages, run the
VM in a Web Worker and post messages from the main thread:
// worker.js
import { createWlift, runProject } from "/dist/wlift.js";
const wlift = await createWlift();
self.onmessage = async (e) => {
const result = await runProject(wlift, e.data.source, e.data.hatchfile);
postMessage(result);
};
WebGPU canvas rendering needs an
OffscreenCanvas transferred to the worker
via postMessage. The playground does this
through the canvas key on the worker bridge;
see
wasm/web/worker.js
for a complete example.
SIMD detection
Skip the SIMD-fallback variant entirely on browsers that support the proposal:
import { simd128Supported, createWlift } from "/dist/wlift.js";
if (simd128Supported()) {
console.log("loading SIMD wasm");
}
const wlift = await createWlift(); // picks the right binary
createWlift already does the right thing;
simd128Supported() is exposed for embedders
who want to surface the choice (analytics, opt-in flags,
etc.).
Compile ahead of time
wlift.js runs Wren in a page with the whole
runtime. A program can also be compiled ahead of time into
its own .wasm. The module holds the compiled
program and the parts of the runtime it calls, with no
compiler inside. It is a WASI module that also asks its host
for fibers, sockets and native libraries, and
wlift is that host. This is another way to ship
a program; the harness stays the way to run Wren in a page.
The installed wlift does this on its own: the
runtime programs link against ships beside it.
Build and run
wlift --aot hello.wasm --aot-target wasm32-wasip1 hello.wren
wlift hello.wasm
Everything the program imports is compiled in with it: its
own modules and the path dependencies in its hatchfile. A
.hatch bundle works as the input too.
System.print writes to stdout, and files are
read and written in the working directory. An error the
program doesn't catch is printed, and the program exits
with code 70.
Code is generated for the target the command names.
--aot-cpu and --aot-features take
an LLVM CPU name and feature list. The default features are
sign extension, mutable globals, bulk memory, non-trapping
float conversion, multivalue and reference types. Add
+simd128 for a runtime with SIMD:
wlift --aot hello.wasm --aot-target wasm32-wasip1 \
--aot-features +sign-ext,+mutable-globals,+bulk-memory,+nontrapping-fptoint,+multivalue,+reference-types,+simd128 \
hello.wren
Fibers
Build with --aot-fibers and a fiber suspends
inside compiled code and resumes where it stopped, as it
does natively:
wlift --aot game.wasm --aot-target wasm32-wasip1 --aot-fibers game.wren
The build saves a suspended fiber's frames in memory of its own, which makes the module larger. Without the flag, fibers that run to completion work the same, and a yield inside compiled code raises an error.
Native libraries
A native library for a compiled module is a side module: a
.wasm that shares the program's memory and
function table, so it works on the program's heap the way
the native build does. Its package names it under the
target triple:
[native_libs.wlift_noise]
"wasm32-wasip1" = "libs/wasm32-wasip1/wlift_noise.wasm"
The wasm key stays the build the page harness
loads; the two sit side by side.
A package author builds it with hatch, from the
cargo workspace that holds the plugin crate:
hatch plugin build --target wasm32-wasip1 --source ../my-plugin
It builds [plugin_source] wasi_crate, or the
plugin crate, and writes the side module where the hatchfile
names it, exporting the functions the package's
#!symbol attributes bind. The build needs a
nightly Rust toolchain with rust-src. Without
--target, the same command builds the host's
native library.
An ahead-of-time build puts each library beside the output
as <name>.wasm and links the program to
host it. wlift loads the libraries beside the
program before starting it.