Wrenlift

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.js
  • wlift_wasm.js
  • wlift_wasm_bg.wasm
  • wlift_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.