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