Skip to content

zig-utils/zig-js

Repository files navigation

zig-js

A JavaScript engine written in pure Zig, with an implemented JavaScriptCore-shaped C API subset. No JSC, no V8, no external C libraries.

zig-js is a small embeddable engine for Zig applications, tools, experiments, and runtimes that want to own their JavaScript stack. Use it directly as a Zig module, or link libzig-js.a for hosts that only need the implemented public C API subset. The project is still pre-stabilization, so clean zig-js semantics win over preserving inert compatibility shims.

The configured conformance runner is green against the pinned tc39/test262 corpus it scores: 48,506 / 48,506 valid and 4,669 / 4,669 negative, with 0 parse, 0 runtime, 0 host, 0 skipped, and 0 excluded failures. That is a scoped result, not a claim of full ECMAScript completion.

const js = @import("js");

const ctx = try js.Context.create(allocator);
defer ctx.destroy();

const v = try ctx.evaluate("let x = 40; x + 2");
// v == .{ .number = 42 }

Contents

Status

Current public status is evidence-scoped:

The documentation guardrails live in docs/DOCS_ACCURACY_PLAN.md. Public claims should be tied to a run transcript, generated data file, source file, or current command output.

How It Works

The engine has two execution paths sharing one object model:

  • Tree-walking interpreter - the semantic baseline and fallback path.
  • Suspendable stack bytecode VM - the compiled path for supported top-level code, functions, generators, async functions, and async generators.

The compiler lowers the subset it knows. Unsupported constructs fall back to the tree-walker where that is semantically safe, so VM coverage can grow without sacrificing correctness. Object shapes and inline caches are implemented for property access, and function activations use frame slots/upvalues for compiled code.

Default contexts use arena lifetime: values and objects are released when the context is destroyed. Opt-in GC contexts (Context.createWith(.{ .enable_gc = true })) use the precise collector described in docs/architecture.md and docs/threads/P7-gc-design.md.

Conformance

Measured by zig build test262 against the pinned test262/ submodule. The runner scores two axes separately:

axis meaning passing
valid can the engine run the program? 48,506 / 48,506 (100.0%)
negative does the engine reject invalid input? 4,669 / 4,669 (100.0%)

Failure shape on the valid axis: 0 parse failures, 0 runtime failures, 0 host failures.

Skipped tests are excluded from both denominators. Current skipped count: 0. Current excluded count: 0.

Two non-normative SpiderMonkey staging files are removed from the configured corpus definition because their esid: pending expectations contradict the normative Annex B arguments tests in test/annexB. They are tracked in conformance/test262.zig as removed corpus inputs, not as engine failures, skips, or exclusions.

Representative green areas from the saved run:

area passing area passing
test/language saved-run subtrees 100% test/annexB 1,071 / 1,071
test/intl402 saved-run subtrees 100% test/staging 1,476 / 1,476
Array 3,081 / 3,081 Object 3,411 / 3,411
RegExp saved-run subtrees 100% String 1,223 / 1,223
Temporal 4,603 / 4,603 TypedArray 1,446 / 1,446
Atomics 390 / 390 SharedArrayBuffer 104 / 104
Map 204 / 204 Set 383 / 383
WeakMap 141 / 141 WeakSet 85 / 85
WeakRef 29 / 29 FinalizationRegistry 47 / 47

zig build conformance is a separate fast smoke suite; the July 4, 2026 verification run passed 33/33 cases. Use zig build test262 for the full configured corpus.

Performance

zig build bench currently times the bytecode VM against the tree-walker on a small set of microbenchmarks. The latest saved local run is docs/.data/bench-2026-07-04.txt:

case VM ns/op tree ns/op VM/tree
fib(27) recursion 172,360,029 166,933,604 0.97x
tight loop sum to 100k 8,614,833 8,547,850 0.99x
object property churn 7,563,963 7,356,334 0.97x
array push/sum 8,484,360 8,475,588 1.00x
deep recursion, depth 500 112,897 115,296 1.02x

Those numbers show current VM/tree-walk parity on these microbenchmarks, not a broad speedup claim. The same benchmark run also prints no-shared-state thread throughput scaling:

threads wall ns scaling
1 258,529,500 1.00x
2 297,057,916 1.74x
4 315,458,875 3.28x
8 362,099,959 5.71x

zig-js vs JavaScriptCore

zig build benchmark-comparison runs the same pure-JavaScript kernels through GC-enabled zig-js and the macOS system JavaScriptCore, checks deterministic results across engines, and reports seven-sample medians with dispersion. It directly compares warmed single contexts, warmed independent contexts on persistent OS workers, and cold thread/context lifecycles; zig-js shared-realm threads remain a separate capability panel. The latest full report preserves all 1,540 raw samples from commit db131e0a on an 11-core Apple M3 Pro (AC power, charging at 44% when captured). Every full-run row exceeds a 50 ms median timing floor.

Lower time is better. A throughput ratio above 1.00x favors zig-js; below 1.00x favors JSC.

workload zig-js single (ms) JSC single (ms) zig-js / JSC throughput
arithmetic 57.169 237.804 4.16x
properties 58.924 201.435 3.42x
polymorphic properties 71.133 180.003 2.53x
object churn 204.392 120.512 0.59x
arrays 65.839 128.433 1.95x
direct calls 64.457 100.548 1.56x
method calls 78.233 140.714 1.80x
closure calls 67.883 159.003 2.34x
arguments calls 60.091 213.843 3.56x
Fibonacci 68.897 373.520 5.42x

At eight warmed independent contexts, both engines use the same persistent-worker protocol and every lane performs the full job count:

workload zig-js (ms) JSC (ms) zig-js / JSC throughput zig-js scaling JSC scaling
arithmetic 72.838 370.596 5.09x 6.28x 5.10x
properties 86.367 320.280 3.71x 5.43x 5.00x
polymorphic properties 118.358 300.590 2.54x 4.76x 4.88x
object churn 786.543 196.866 0.25x 2.12x 4.88x
arrays 105.306 203.256 1.93x 4.98x 5.10x
direct calls 87.218 168.485 1.93x 5.94x 4.75x
method calls 104.680 225.601 2.16x 5.97x 5.00x
closure calls 100.858 278.503 2.76x 5.41x 4.55x
arguments calls 83.543 357.399 4.28x 5.71x 4.56x
Fibonacci 109.876 803.650 7.31x 5.13x 3.72x

zig-js's no-GIL shared-realm mode has no direct public-JSC equivalent because its threads share one object graph. Its latest eight-lane scaling is:

workload one lane (ms) eight lanes (ms) throughput scaling
arithmetic 57.403 74.216 6.19x
properties 87.853 132.873 5.29x
polymorphic properties 463.550 666.142 5.57x
object churn 986.746 28,321.516 0.28x
arrays 67.885 225.873 2.40x
direct calls 66.645 86.532 6.16x
method calls 121.762 186.462 5.22x
closure calls 69.264 127.141 4.36x
arguments calls 59.335 83.469 5.69x
Fibonacci 214.050 378.608 4.52x

zig-js wins 9 of 10 direct single-context rows. Across the complete 10-workload matrix, its geometric-mean throughput lead is 2.36x in direct single-context mode and 2.47x at eight warmed independent contexts. Mode-local eight-lane scaling is 4.99x for zig-js and 4.73x for JSC; the symmetric cold lifecycle scales 5.01x and 4.75x respectively. Shared-realm scaling is 3.67x by geometric mean, but object churn is the clear exception: JSC leads that direct row and allocation/GC contention drives shared scaling below 1x. See Performance Benchmarks for all lane counts, cold results, exact timed boundaries, allocator choice, dispersion, caveats, reproduction, and raw evidence.

Implemented performance machinery includes the bytecode VM, frame slots/upvalues, object shapes, inline caches, guarded loop and recurrence kernels, a baseline native tier, the engine-wide 8-byte NaN-boxed Value, GC slab backing, and an opt-in-GC one-cycle nursery that reclaims young garbage at quiescent boundaries and immediately tenures survivors. Future work includes broader native-tier coverage, nursery sizing and pause optimization, deeper generational policies, and a general optimizing tier.

Language And Runtime Coverage

The configured test262 coverage for these surfaces is green.

Syntax and operators - literals, strings, regex literals, template literals, objects, arrays, destructuring, spread/rest, optional chaining, nullish coalescing, logical assignment, exponentiation, bitwise/shift operators, in, instanceof, typeof, delete, void, and comma.

Bindings and scope - var/let/const, TDZ, block scope, closures, direct and indirect eval, with, destructuring in declarations/parameters/assignment, and mapped/unmapped arguments.

Functions and classes - declarations, expressions, arrows, default/rest parameters, this, new, new.target, getters/setters, class fields, private members, static members/blocks, super, derived constructors, and extends.

Control flow - if, loops, for-in, for-of, for await, switch, labels, break, continue, throw, try, catch, finally, and using/disposal syntax covered by the configured runner.

Generators and async - function*, yield, yield*, async functions, async generators, await, Promise jobs, microtask ordering, proper tail calls in strict return-position calls, and module+async/top-level-await tests in the configured surface.

Modules - imports, exports, default/named/namespace re-exports, export *, live bindings, namespace objects, import.meta, dynamic import(), dynamic-import catch-target behavior, import defer async-module behavior, and top-level-await graph ordering covered by the configured runner.

Built-ins - Object, Function, Array, String, RegExp via zig-regex, Number, Boolean, Math, JSON, Symbol, Map, Set, WeakMap, WeakSet, Promise, Date, errors, Proxy, Reflect, globalThis, typed arrays, ArrayBuffer, SharedArrayBuffer, DataView, Atomics, WeakRef, FinalizationRegistry, broad Temporal, and Intl coverage.

Using It

As A Zig Module

const js = @import("js");

const ctx = try js.Context.create(allocator);
defer ctx.destroy();

const v = try ctx.evaluate("let x = 40; x + 2");

As A C API Subset

Link libzig-js.a for hosts that use the implemented subset of Apple's public <JavaScriptCore/JSValueRef.h> / <JSObjectRef.h>-shaped surface:

JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
JSStringRef script = JSStringCreateWithUTF8CString("1 + 1");
JSValueRef result = JSEvaluateScript(ctx, script, NULL, NULL, 0, NULL);
double n = JSValueToNumber(ctx, result, NULL); // 2.0

The current exported C surface has 52 functions:

  • Context lifecycle - JSGlobalContextCreate, ZJSGlobalContextCreateThreaded(gil), JSGlobalContextRelease, JSGlobalContextRetain, JSContextGetGlobalObject, JSEvaluateScript, JSGarbageCollect.
  • Values - JSValueGetType, JSValueIs*, JSValueIsEqual, JSValueIsStrictEqual, JSValueMake*, JSValueTo*, JSValueProtect, JSValueUnprotect.
  • Objects - JSObjectMake, JSObjectGetPrivate, JSObjectSetPrivate, JSObjectMakeArray, JSObjectMakeDeferredPromise, JSObjectGetProperty, JSObjectSetProperty, JSObjectGetPropertyAtIndex, JSObjectCallAsFunction, JSObjectCallAsConstructor, JSObjectMakeFunctionWithCallback, JSObjectIsFunction, JSObjectIsConstructor.
  • Strings - JSStringCreateWithUTF8CString, JSStringRetain, JSStringRelease, JSStringGetLength, JSStringGetUTF8CString.
  • Worker extension - JSWorkerCreate, resource-bounded JSWorkerCreateWithLimits, JSWorkerPostMessage, JSWorkerReceive, JSWorkerTerminate, JSWorkerRelease.

ZJSGlobalContextCreateThreaded and JSWorker* are zig-js extensions, not public JSC symbols. JSObjectMakeDeferredPromise returns a pending native Promise plus paired resolving functions; callers observe settlement at the next microtask checkpoint (for example, after JSEvaluateScript returns).

See docs/api.md and docs/HOME_INTEGRATION.md for the fuller embedding story and the important warning that zig-js is not a drop-in replacement for Bun/Home's private JSC internals.

Architecture

file responsibility
src/value.zig Value, Object, coercions, equality, object slots/elements/accessors
src/lexer.zig tokenizer
src/ast.zig AST node model
src/parser.zig recursive-descent parser
src/interpreter.zig tree-walking evaluator and built-in library
src/compiler.zig AST to bytecode compiler
src/bytecode.zig bytecode instruction and template definitions
src/vm.zig suspendable bytecode VM
src/shape.zig hidden-class/shape transition tree
src/promise.zig Promise state and microtask queue
src/context.zig context lifecycle, module loader/cache, GC/thread options
src/gc.zig opt-in precise GC
src/jsthread.zig shared-realm Thread API
src/worker.zig isolated worker agents
src/jsstring.zig refcounted JSStringRef backing
src/c_api.zig exported C API
src/root.zig @import("js") entry point

Build And Test

Requires Zig 0.17.0-dev.

zig build                         # builds libzig-js.a
zig build test                    # unit + C-API tests
zig build conformance             # fast smoke suite
zig build test262                 # configured tc39/test262 corpus
zig build test262-bin             # build the test262 runner only
./zig-out/bin/test262 --list-skips > docs/.data/test262-skips.tsv
./zig-out/bin/test262 --list-excluded > docs/.data/test262-excluded.tsv

bun run docs:data
bun run docs:build

zig build bench                   # VM/tree-walk and thread-scaling benchmark
zig build benchmark-comparison    # zig-js direct/independent/shared vs system JSC (macOS)
zig build benchmark-comparison -Dbenchmark-comparison-quick=true
zig build benchmark-comparison-test
zig build threads-test            # WebKit PR-249 thread allowlist
zig build threads-reference-audit # classify non-promoted PR-249 files
python3 tools/threads-reference-audit.py --probe-candidates
python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60

zig build threadfuzz
zig build threadfuzz -Dfuzz-midgc=true
zig build threadfuzz -Dfuzz-lifecycle=true
zig build test -Dtsan=true
zig build threads-profile
zig build threads-profile -Dthreads-profile-case='global binding churn' -Dthreads-profile-max-workers=1
zig build threads-profile -Dthreads-profile-case='condition asyncWait'
zig build threads-profile -Dthreads-profile-case='condition asyncWait parked'
zig build threads-profile -Dthreads-profile-case='condition asyncWait multi-lock'
zig build threads-profile -Dthreads-profile-case='promise microtasks'
zig build threads-profile -Dthreads-profile-case='promise thenables'
zig build midgc-profile
zig build gc-profile
zig build gc-profile -Dgc-profile-case='nursery'

The test262 corpus is vendored as the test262/ git submodule. zig build test262 uses it by default and skips cleanly if it is absent.

Threads And GC

Context.createWith(.{ .enable_threads = true }) installs the shared-realm Thread, Lock, Condition, ThreadLocal, property-mode Atomics.*, proposal-aligned Atomics.Mutex / Atomics.Condition, and related surfaces. Shared-realm threads run true-parallel by default; .gil = true is available as a serialized compatibility mode.

const parallel = try js.Context.createWith(gpa, .{ .enable_threads = true });
const serialized = try js.Context.createWith(gpa, .{ .enable_threads = true, .gil = true });

The isolated Worker implementation lives in src/worker.zig and is exposed to C embedders through the JSWorker* extension functions.

Current thread status is tracked in:

The README intentionally avoids duplicating the detailed thread/GC implementation log; those docs and the commands above are the source of truth.

What Is Not Implemented

Do not read the green configured runner as "the whole JavaScript universe is finished." Known non-implemented or non-scored areas include:

  • full JavaScriptCore framework/private internals, Objective-C bridge, inspector/debugger APIs, and Bun/Home private JSC ABI;
  • WebAssembly and JIT shell hooks from the PR-249 reference corpus;
  • moving or multi-age generational GC, parallel mid-script minor collection, and any optimizing JIT.

Used By

License

MIT - see LICENSE.

About

A JavaScript engine in pure Zig.

Topics

Resources

License

Stars

5 stars

Watchers

0 watching

Forks

Contributors

Languages