zRun is a compact JavaScript bytecode runtime and source compiler written in Zig. It separates scriptable business logic from performance-critical host functions: business rules can run as JavaScript bytecode, while hot paths and host integrations can run as native Zig functions. Runtime capabilities are organized as plugins, so new APIs can be added without rewriting the VM core. It includes a command-line runner for source scripts and supported upstream bytecode images.
The complete engine—source compiler, bytecode runtime, VM, and built-in plugins—has a memory footprint of about 5 MB, not just the VM core. Actual process memory depends on the workload, allocator, and build configuration.
The source compiler and runtime focus on the language constructs and runtime APIs needed by their target workloads. Do not execute untrusted scripts in security-sensitive processes.
The tables below summarize implemented source-language constructs and runtime APIs.
| Area | Implemented features |
|---|---|
| Values | • undefined, null, booleans• Integer, hexadecimal, and BigInt literals; runtime floating-point values • Arbitrary-precision BigInt arithmetic • Strings and template interpolation |
| Bindings | • var, let, and const• Local assignment and closures • Captured mutable values |
| Expressions | • Arithmetic and loose/strict comparisons • Short-circuit &&/||; ternary and nullish expressions• Increments and compound assignments • typeof, void, in, and instanceof |
| Global object | • globalThis property reads and writes, including computed properties• globalThis.Object and access to the installed Object namespace |
| Bitwise operations | • &, |, and ^• <<, >>, and unsigned right shift >>> |
| Property access | • Dot and computed reads/writes • Optional dot/computed access • Array and string length |
| Strings | • Quoted literals and escapes • Concatenation and ${...} interpolation |
Coverage: values and arithmetic, operators and optional access.
| Area | Implemented features |
|---|---|
| Branching and loops | • if/else and switch• while, do/while, and C-style for• Array for/of and object-key for/in• Object binding patterns in for/of and for/in, including renamed bindings such as for (const { id: key } of rows)• Labeled break/continue |
| Exceptions | • throw and try/catch• Synchronous finally• Propagation through function calls |
Coverage: language control flow, runtime control flow.
| Area | Implemented features |
|---|---|
| Functions | • Declarations, expressions, and calls • Recursion and closures with captured mutable locals • Async declarations, function expressions, and parenthesized arrow functions suspend at await and resume through the embedding host• this and arguments |
| Async host tasks | • await on pending external plugin calls through __host• await Promise.all([...]) starts multiple external plugin tasks and returns results in input order |
| Arrow functions | • Parenthesized parameters • Expression and block bodies |
| Parameters | • Default values • Array and object destructuring • Object rest in bundled parameters |
| Spread | • Function calls • Array and object literals |
| Object methods | • Concise method syntax • Closure capture and method receiver behavior |
| Classes | • Class declarations and constructors • Error subclasses |
| Function invocation | • .call()• .bind() |
| Optional calls | • callback?.(value); arguments are skipped when the callee is null or undefined |
Coverage: function syntax, closures and call behavior, class syntax.
The generated g-* clients are the application API. A client method carries its generated service/method metadata and routes the request through the nrpc plugin; application code does not construct plugin payloads itself. Calls can be started at different points and gathered later:
import { createCompaniesServiceClient } from "g-companies";
import { createGeoServiceClient } from "g-geo";
const companies = createCompaniesServiceClient({ target: "services" });
const geo = createGeoServiceClient({ target: "services" });
async function loadDashboard(userId) {
var call1 = companies.listCityGroups();
var title = "Dashboard";
var call2 = geo.getCountryCities({ countryCode: "US", limit: 50 });
var call3 = companies.getCompany(userId);
var [cityGroups, cities, company] = await Promise.all([call1, call2, call3]);
return { title, cityGroups, cities, company };
}The intended route is generated g-* method → bundled nrpc client helper → plugin("nrpc").command("call", payload) → host NRPC plugin → Fujin. The G client packages target, service, method, args, and call context. The helper in libs/zrun/nrpc.ts already builds this plugin request for the next-arh bundle. The current native zRun runtime does not yet complete this route: it has no NRPC dispatcher wired to Fujin, and the synchronous helper JSON-parses the immediate __host response rather than handling a pending task result. The separate plugin_start/plugin_wait and Promise.all code is only the VM-side task prototype so far; it does not make generated G clients work yet.
Once the NRPC route is connected, the VM side can start each generated client request as an external task, then Promise.all can gather those handles in input order. This is limited host-task support, not a general JavaScript Promise implementation: nested task groups, user-created promises, .then()/.catch(), arbitrary thenables, and other Promise combinators are unsupported. Command-line runners remain synchronous.
Hosts can keep multiple separately compiled artifacts decoded in memory with
module_registry.ModuleRegistry. Loading assigns a host name to the artifact;
function calls resolve by module name and function name. The registry retains
each artifact until remove or deinit, so calls do not reread or decode the
bytecode. Removing a module invalidates pointers and execution sessions created
from its functions; release those first.
var modules = zrun.module_registry.ModuleRegistry.init(allocator);
defer modules.deinit();
try modules.load("users", users_artifact_bytes);
try modules.load("billing", billing_artifact_bytes);
var call = try zrun.execution.createModuleFunctionAsyncSession(
allocator,
&modules,
"users",
"lookupUser",
.{},
);
defer call.deinit();
// After all calls for this module are released:
_ = modules.remove("users");load rejects duplicate names; remove the old module before loading a
replacement under the same name. The registry and its modules are host-owned
and are not internally synchronized.
| Area | Implemented features |
|---|---|
| Arrays | • Literals, indexing, mutation, and length• Array(...), Array.from, Array.isArray, and spread• push, map, filter, find, flatMap, and reduce• includes, join, slice, some, and sort• concat, indexOf, and shift |
| Plain objects | • Object literals, shorthand fields, and concise methods • Computed keys and computed methods • Object spread • Property reads, writes, and delete on local or this object properties |
Map |
• Construction and iterable initialization • set, get, and has• keys iterator |
Set |
• Construction and iterable initialization • add, has, and delete• values iterator |
WeakMap |
• Construction • set, get, and has |
| Regular expressions | • Literal patterns with i/g flags• .test()• String replacement operations |
Coverage: arrays and objects, property deletion, collections, regular expressions.
| Area | Implemented features |
|---|---|
| String methods | • concat, indexOf, startsWith, slice, and split• replace and replaceAll• charCodeAt, localeCompare, and padStart• toLowerCase, toUpperCase, and trim |
Number |
• toString |
Object |
• assign, entries, and fromEntries• keys and values |
Object properties |
• create, defineProperty, and getOwnPropertyDescriptor• getPrototypeOf, getOwnPropertyNames, and hasOwnProperty• Shared ordinary-object prototype exposed as Object.prototype |
Array |
• isArray |
JSON |
• parse and stringify |
Math |
• imul, max, min, and random |
| Conversion and encoding | • Boolean, String, and Number conversion• encodeURIComponent |
| Host integration | • print• The __host bridge and --host-json input |
Coverage: runtime APIs, host primitives.
Object.defineProperty currently supports the runtime's own string properties;
property descriptors do not yet model non-enumerable or symbol properties.
| Area | Implemented features |
|---|---|
| Upstream bytecode | • Loads MQuickJS bytecode images • Relocates bytecode for runtime execution |
| Runtime values | • Object-store backed arrays, objects, and strings • Closures, collections, and regular-expression values |
Coverage: bytecode fixtures and checked-in images.
- Zig 0.17.0 !
zig build -Doptimize=fast
./zig-out/bin/zrun-compile path/to/bundle.js > path/to/program.zbc
./zig-out/bin/zrun-runtime path/to/program.zbc
./zig-out/bin/zrun --bytecode path/to/program.binzrun-compile turns JavaScript source into the versioned zRun artifact format.
zrun-runtime loads and executes that artifact without linking the source
compiler. The zrun executable remains a development runner for source fixtures
and upstream MQuickJS bytecode.
Artifacts use format version 2 for optional metadata. The header stores an absolute offset and byte length for a compact UTF-8 JSON section at the end of the artifact. The loader validates and parses the document once per module load, then indexes function records by artifact-local function ID. Version 1 artifacts remain readable. Metadata is not interpreted on each VM instruction or call.
Compile with a metadata sidecar:
zrun-compile path/to/bundle.js --metadata-json path/to/metadata.json > path/to/program.zbcThe JSON document starts with {"format":"zrun.metadata/1"}. Its optional
functions array contains records such as
{"id":3,"visibility":"public","nrpc":{"service":"users","method":"get"}}.
Function IDs are assigned in pre-order, starting at zero for the root function;
plugins can retrieve a function record from the loaded module by that ID. Other
plugin-specific fields are retained without interpretation by the VM.
The pilot starts one isolated zRun process per bytecode URL. Each process reads its own ZRUN_BYTECODE_URL, downloads a raw MQuickJS bytecode image, relocates it, and executes it:
ZRUN_WORKERS='["https://runtime.example/jobs/a.bin","https://runtime.example/jobs/b.bin"]' \
./zig-out/bin/zrun --workers-envRun the local end-to-end pilot with two different bytecode images and URLs:
./pilot/remote_workers/run.shSee pilot/remote_workers/README.md for limits and the pilot protocol.
Run the full verification sequence from the repository root:
./verification/run.shThis runs Zig unit tests followed by the language fixtures in increasing levels of complexity, then checks the checked-in bytecode images.
src/contains the runtime, compiler, VM, plugins, and checked-in bytecode fixtures.verification/contains the ordered correctness corpus and its single runner.performance/contains benchmark workloads and standalone runners;performance/knolage/contains the curated measurement record.
| Engine | Source lines | Counted files |
|---|---|---|
| V8 | ≈2,000,000 | Upstream engine estimate; the source is external to this repository. |
| QuickJS | 79,871 | Top-level .c/.h; excludes test runner, Unicode generator, and generated Unicode table. |
| MQuickJS | 30,950 | Top-level .c/.h; excludes the example and generated atom/opcode headers. |
| zRun | 11,375 | src/**/*.zig. |
performance/scripts/run.py compiles the same JavaScript independently with
MQuickJS and zRun, then executes each engine's own artifact. Compilation and
execution have separate timers and raw samples can be saved with --json.
Both phases include process startup and their respective file I/O; builds are
excluded. Use --suite all for the CLI and language-feature workloads. The
historical same-bytecode CLI results below used the earlier version of this
script; the optional in-process comparator still measures identical upstream
images.
Measurements captured on 2026-09-29 on Linux x86_64, AMD Ryzen 7 8845H, Zig 0.16.0. Ratios are reference time / zRun time; below 1.00x means zRun was slower. Suites have different workloads and timing scopes, so compare results only within each row.
Run the full benchmark set from the repository root. Set these paths to the installed MQuickJS executable, QuickJS executable, and MQuickJS source checkout. The command runs all suites represented below; it does not use V8.
set -euo pipefail
MQJS=/path/to/mquickjs/mqjs
QJS=/path/to/quickjs/qjs
MQUICKJS_DIR=/path/to/mquickjs
test -x "$MQJS"
test -x "$QJS"
test -f "$MQUICKJS_DIR/mquickjs.c"
if [ -e ../mquickjs ] || [ -L ../mquickjs ]; then
printf 'Refusing to replace existing ../mquickjs\n' >&2
exit 1
fi
tmpdir=$(mktemp -d)
ln -s "$MQUICKJS_DIR" ../mquickjs
cleanup() {
unlink ../mquickjs
for image in "$tmpdir"/*.bin; do
[ -e "$image" ] || continue
unlink "$image"
done
rmdir "$tmpdir"
}
trap cleanup EXIT
zig build bench -Doptimize=fast
for source in performance/workloads/inprocess/*.js; do
image="$tmpdir/$(basename "${source%.js}").bin"
"$MQJS" -o "$image" "$source"
zig build compare -Doptimize=fast -- "$image"
done
python3 performance/scripts/run.py "$MQJS" --suite all --json /tmp/zrun-pipeline.json
python3 performance/scripts/run_features.py "$MQJS"
python3 performance/scripts/run_quickjs.py --quickjs "$QJS"
zig build compile-bench -Doptimize=fast| Benchmark | Cases | Result | Scope |
|---|---|---|---|
| zRun VM microbenchmarks | 4 | stack push/pop 0.52 ns/pair; arithmetic 1.023 ms; calls 0.307 ms; native methods 2.318 ms |
Internal VM; medians of 5 samples |
| MQuickJS same-bytecode | 8 | geometric mean 0.92x (per-case results) |
Same compiled images; 2 warmups, 7 timed runs |
| MQuickJS CLI bytecode | 4 | geometric mean 0.87x |
Process startup and bytecode loading; 2 warmups, 9 timed runs |
| MQuickJS language features | 15 | geometric mean 0.56x |
Source compile and execution; 2 warmups, 7 timed runs |
| Official QuickJS CLI | 4 | geometric mean 0.91x |
Source compile and execution; 3 warmups, 11 timed runs |
| Compile-only comparison | 4 | geometric mean 0.60x |
MQuickJS C -O3 vs zRun Zig ReleaseFast; 15 timed runs |
A fresh run.py --suite all comparison on 2026-10-09 covered all 19 CLI and
feature workloads with 3 warmups and 21 measured runs. Both engines produced
matching output for every workload. Each engine compiled its own artifact;
medians include compiler startup in the compile phase and runtime startup plus
artifact loading in the execution phase. zRun/MQuickJS geometric-mean time was
1.068x for compilation, 1.561x for execution, and 1.505x combined. The
raw samples and per-workload results
show execution ahead on cli/arithmetic.js and features/json.js; the largest
gaps are object, string, and lowered-spread workloads.
The internal VM microbenchmarks run with zig build bench -Doptimize=fast. Additional workloads and Python runners are in performance/; detailed results and methodology include historical measurements. Cross-engine comparisons require separately installed MQuickJS or QuickJS executables; the zRun build and verification suite do not depend on them.
zRun is distributed under the MIT license. The translated implementation retains the original MQuickJS copyright notices; see LICENSE.
zRun is not an operating-system sandbox. A script or bytecode image runs with the filesystem, network, and process privileges of the zRun process. Running workers as separate processes does not by itself remove those privileges.
Distributed code must come from a repository the operator explicitly trusts. Package distribution must authenticate repository metadata with digital signatures and bind each package to a cryptographic digest listed by that signed metadata. The runtime or package manager must verify this chain before decompressing, parsing, caching, or executing an artifact, and must reject missing or invalid signatures. Repository keys should be scoped to their intended repositories and managed as trust roots.
Signatures establish artifact origin and integrity; they do not prove that code is harmless. Installing or running a trusted package grants its code the privileges of the process that installs or executes it. Operators must review and trust the repository and its build pipeline.
A bytecode package may be distributed without its source, including publicly, while source access is restricted to subscribers or other authorized recipients. For every distributed bytecode artifact, provide a signed provenance record binding at least the source digest, bytecode digest, compiler identity and version, bytecode ABI, and relevant build inputs. Authorized recipients must be able to obtain the corresponding source and build inputs, verify the provenance signature, reproduce the build with the identified compiler, and compare the locally generated bytecode digest with the bytecode digest in the signed record. A trusted build attestation may provide additional assurance, but does not replace this independent verification path. The source need not be bundled with the public bytecode package; a source file without a verifiable binding to the artifact is not proof of correspondence.
Caches should be keyed by artifact and compiler/ABI digests. Verify signatures and provenance before decoding or relocating bytecode. When compressed artifacts are accepted, enforce limits on both compressed and decompressed size and on decompression work.
Run zRun with the least privileges needed. Use operating-system controls to restrict filesystem and network access and to cap memory, CPU time, process count, and output. Use a separate restricted process or stronger OS sandbox when code may be untrusted or when containing runtime vulnerabilities is required. Process separation alone is not a security boundary if workers retain the same credentials, inherited secrets, filesystem access, and network access.
The repository-signature verification, package manager, source-to-bytecode provenance verification, and operating-system sandbox described above are security requirements for a trusted distribution system; they are not implemented by the current zRun CLI. The --bytecode path accepts a raw MQuickJS bytecode image, and the remote-worker pilot fetches and executes raw images. Treat these inputs as trusted build artifacts only. Do not use the current CLI or worker pilot to execute bytecode from arbitrary or untrusted sources.