Skip to content

zig-utils/zig-js

Repository files navigation

zig-js

A JavaScript engine written in pure Zig, with a JavaScriptCore C-API-compatible 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 JavaScriptCore C API subset.

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

Implemented performance machinery includes the bytecode VM, frame slots/upvalues, object shapes, inline caches, the engine-wide 8-byte NaN-boxed Value, and GC slab backing. Future performance work includes tighter VM/codegen coverage, nursery/generational GC work, and possible baseline/JIT exploration. There is no optimizing JIT today.

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 JavaScriptCore C-API Subset

Link libzig-js.a for hosts that use the implemented subset of Apple's public <JavaScriptCore/JSValueRef.h> / <JSObjectRef.h> 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 50 functions:

  • Context lifecycle - JSGlobalContextCreate, ZJSGlobalContextCreateThreaded(gil), JSGlobalContextRelease, JSGlobalContextRetain, JSContextGetGlobalObject, JSEvaluateScript, JSGarbageCollect.
  • Values - JSValueGetType, JSValueIs*, JSValueIsEqual, JSValueIsStrictEqual, JSValueMake*, JSValueTo*, JSValueProtect, JSValueUnprotect.
  • Objects - JSObjectMake, JSObjectMakeArray, JSObjectMakeDeferredPromise, JSObjectGetProperty, JSObjectSetProperty, JSObjectGetPropertyAtIndex, JSObjectCallAsFunction, JSObjectCallAsConstructor, JSObjectMakeFunctionWithCallback, JSObjectIsFunction, JSObjectIsConstructor.
  • Strings - JSStringCreateWithUTF8CString, JSStringRetain, JSStringRelease, JSStringGetLength, JSStringGetUTF8CString.
  • Worker extension - JSWorkerCreate, 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 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 gc-profile

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;
  • nursery/generational GC and any optimizing JIT.

Used By

License

MIT - see LICENSE.

About

A JavaScript engine in pure Zig.

Topics

Resources

License

Stars

4 stars

Watchers

0 watching

Forks

Contributors

Languages