Skip to content
solenopsysPublic

About

JavaScript Nano Runtime

Resources

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

zRun

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.

Status

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.

JavaScript Support

The tables below summarize implemented source-language constructs and runtime APIs.

Values and expressions

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.

Statements and control flow

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.

Functions and classes

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.

Resident bytecode modules

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.

Objects, arrays, and collections

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.

Built-ins and host runtime

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.

Bytecode runtime

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.

Requirements

  • Zig 0.17.0 !

Build and run

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

zrun-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.zbc

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

Remote bytecode workers (pilot)

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-env

Run the local end-to-end pilot with two different bytecode images and URLs:

./pilot/remote_workers/run.sh

See pilot/remote_workers/README.md for limits and the pilot protocol.

Run the full verification sequence from the repository root:

./verification/run.sh

This runs Zig unit tests followed by the language fixtures in increasing levels of complexity, then checks the checked-in bytecode images.

Project layout

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

Code size

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

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.

License

zRun is distributed under the MIT license. The translated implementation retains the original MQuickJS copyright notices; see LICENSE.

Security and Artifact Trust

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.

Trusted Packages

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.

Bytecode Provenance

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.

Runtime Isolation

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.

Current Implementation

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.

About

JavaScript Nano Runtime

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages