Skip to content

WASM Target

Devlish runs in WebAssembly through a bytecode-in-WASM approach: the Devlish VM itself is compiled to WASM, and it interprets Devlish bytecode at runtime. This gives browser and Node.js embedding without per-workflow code generation.

.dvl source -> Rust compiler -> .dvlc.json bytecode
|
v
Devlish VM (Rust) -> WASM binary -> JavaScript host

The WASM binary is a generic VM. It accepts any bytecode package and executes it. The reviewed artifact is always the .dvl source plus its inspectable bytecode, not generated JavaScript.

The runner exports these functions:

Export Purpose
devlish_init Initialize the VM with a bytecode package
devlish_run Execute the loaded program
devlish_resume Resume after a checkpoint
devlish_result_ptr Get pointer to result in linear memory
devlish_result_len Get length of result data
devlish_free Free allocated memory

The WASM module imports callbacks from the JavaScript host:

Import Signature Purpose
emit_event (ptr, len) Send execution events to the host
request_input (ptr, len) Request user input (for Ask)
write_file (ptr, len) Request file write from host

Values cross the WASM boundary as JSON strings in linear memory. This is not the final ABI, but it enables full integration without blocking on the WebAssembly Component Model.

The devlish-runtime package wraps the low-level WASM interface with an ergonomic JavaScript API:

import { loadTool, runTool } from "devlish-runtime";
// One-shot
const result = await runTool(bytecode, { amount: 1200 });
// Reusable (Web Worker)
const tool = await loadTool({ bytecode });
const r1 = await tool.run({ project: "A" });
const r2 = await tool.run({ project: "B" });
tool.dispose();

The WASM binary is inlined as base64 within the npm package. This ensures:

  • Zero-config bundler compatibility (webpack, vite, esbuild)
  • No separate .wasm file to serve or configure
  • Works with CDN imports (esm.sh, unpkg, skypack)
  • No async WASM loading path required in simple cases

The tradeoff is a larger package size, acceptable for the current VM binary.

When loadTool receives bytecode with a manifest, it checks permissions before creating the VM:

const tool = await loadTool({ bytecode: programWithHttpPermission });
// Throws: "Tool requires HTTP requests permission, which is not available in the WASM sandbox"

The WASM sandbox cannot perform HTTP requests or filesystem operations. Tools that declare these permissions are rejected immediately, preventing confusing runtime errors.

The same package works in all JavaScript environments:

Environment Worker Support Notes
Browser Web Worker Default async execution
Node.js worker_threads Same API
Deno Web Worker Standard imports
Cloudflare Workers Main thread only Use mainThread: true
Bun worker_threads Same as Node

From the Devlish repo:

Terminal window
./scripts/build_wasm_runner.sh

This compiles the devlish_wasm_runner crate to wasm32-unknown-unknown and produces the binary that gets base64-inlined into the npm package.

For advanced use cases, the low-level wrapper is available at crates/devlish_wasm_runner/js/index.mjs:

import { loadDevlishWorkflow } from "./index.mjs";
const workflow = await loadDevlishWorkflow({
wasmUrl: "/devlish_runner.wasm",
bytecodeUrl: "/program.dvlc.json",
host: {
writeFile: req => { files.push(req); return true; },
emitEvent: evt => events.push(evt)
}
});
const result = await workflow.run({ input_field: "value" });

Most users should use devlish-runtime instead.