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 }- Status
- How It Works
- Conformance
- Performance
- Language And Runtime Coverage
- Using It
- Architecture
- Build And Test
- Threads And GC
- What Is Not Implemented
- License
Current public status is evidence-scoped:
- test262 totals come from docs/.data/test262.json, regenerated from docs/.data/test262-run-2026-07-05.txt.
- The skipped-test inventory is docs/.data/test262-skips.tsv, currently zero.
- The exact excluded-file inventory is docs/.data/test262-excluded.tsv, currently zero.
- VM/tree-walker numbers below come from docs/.data/bench-2026-07-04.txt; the JSC comparison comes from the July 15, 2026 report and its 1,540 raw timing samples.
- C API scope comes from the exported symbols in src/c_api.zig.
- Threading and GC status are documented under docs/threads and docs/architecture.md.
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.
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.
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.
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 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 timing samples from clean zig-js commit 0a74f7d1, zig-gc commit 9d4af0d, and zig-regex commit 50764b03 on an 11-core Apple M3 Pro. The harness recorded AC power at 100% charge. Every full-run median exceeds the 50 ms timing floor, equal work counts are identical for both engines, and the runners alternate execution order. Dispersion is shown in the full report; do not treat small cross-session differences as causal.
| mode | lanes | wins vs JSC | zig-js / JSC throughput | zig-js scaling | JSC scaling |
|---|---|---|---|---|---|
| direct warmed context | 1 | 10 / 10 | 2.75x | — | — |
| independent steady contexts | 8 | 10 / 10 | 2.95x | 5.38x | 5.14x |
| independent cold lifecycles | 8 | 10 / 10 | 2.90x | 5.27x | 5.08x |
| shared realm, no GIL | 8 | no public-JSC equivalent | — | 4.17x | — |
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 | 82.936 | 353.899 | 4.27x |
| properties | 87.534 | 293.427 | 3.35x |
| polymorphic properties | 82.027 | 204.107 | 2.49x |
| object churn | 80.380 | 117.343 | 1.46x |
| arrays | 79.124 | 150.564 | 1.90x |
| direct calls | 56.185 | 116.732 | 2.08x |
| method calls | 63.899 | 137.733 | 2.16x |
| closure calls | 77.444 | 187.510 | 2.42x |
| arguments calls | 67.331 | 296.124 | 4.40x |
| Fibonacci | 85.294 | 448.825 | 5.26x |
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 | 106.720 | 550.496 | 5.16x | 6.22x | 5.09x |
| properties | 119.083 | 462.222 | 3.88x | 5.88x | 5.06x |
| polymorphic properties | 129.555 | 336.130 | 2.59x | 5.07x | 4.86x |
| object churn | 170.179 | 187.303 | 1.10x | 3.77x | 5.05x |
| arrays | 128.029 | 233.891 | 1.83x | 5.05x | 5.16x |
| direct calls | 77.858 | 184.310 | 2.37x | 5.77x | 5.05x |
| method calls | 88.482 | 205.107 | 2.32x | 5.69x | 5.35x |
| closure calls | 85.020 | 295.237 | 3.47x | 5.84x | 5.04x |
| arguments calls | 102.239 | 459.981 | 4.50x | 5.29x | 5.15x |
| Fibonacci | 116.174 | 640.952 | 5.52x | 5.69x | 5.60x |
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 | 83.101 | 106.705 | 6.23x |
| properties | 126.288 | 175.031 | 5.77x |
| polymorphic properties | 503.704 | 738.795 | 5.45x |
| object churn | 123.180 | 1,922.828 | 0.51x |
| arrays | 82.337 | 214.583 | 3.07x |
| direct calls | 56.310 | 80.143 | 5.62x |
| method calls | 116.787 | 176.318 | 5.30x |
| closure calls | 62.684 | 91.244 | 5.50x |
| arguments calls | 67.804 | 97.508 | 5.56x |
| Fibonacci | 258.003 | 366.792 | 5.63x |
zig-js wins all 10 direct rows and all 10 eight-lane rows in both directly comparable multi-context modes. Its geometric-mean throughput lead is 2.75x direct, 2.95x at eight warmed independent contexts, and 2.90x in the symmetric eight-lane cold lifecycle. Mode-local eight-lane scaling is 5.38x for zig-js versus 5.14x for JSC when warmed and 5.27x versus 5.08x when cold; shared-realm scaling is 4.17x by geometric mean.
Object instances now occupy a 256-byte GC slab (208 bytes of payload and 240 raw bytes including allocator metadata), while cold error, regexp, constructor, and related state is allocated only when used. Current object buckets use 256 KiB chunks, avoiding repeated allocator and synchronized address-index growth without exceeding the fixed reuse bitmap. The resulting object-churn medians are 80.380 versus 117.343 ms direct, 170.179 versus 187.303 ms at eight warmed contexts, and 177.256 versus 189.788 ms across cold eight-context lifecycles: zig-js now leads JSC in every directly compared object-churn row. Shared object churn also falls to 1,922.828 ms at eight lanes, though its 0.51x scaling remains the clearest shared-GC bottleneck. See Performance Benchmarks for every lane count, exact boundaries, 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 with per-chunk reusable-slot bitmaps plus lock-amortized allocation and sweep-release batches, 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.
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.
const js = @import("js");
const ctx = try js.Context.create(allocator);
defer ctx.destroy();
const v = try ctx.evaluate("let x = 40; x + 2");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.0The 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-boundedJSWorkerCreateWithLimits,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.
| 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 |
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.
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.
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.
MIT - see LICENSE.