ReviewOS

zig-utils/zig-js

API: prefer clean zig-js contracts over compatibility shims

#22
Closed chris opened this 9 days ago ·
9 days ago

Parent: #1

Goal

Before the public API stabilizes, remove or redesign compatibility-first surfaces that make zig-js harder to reason about, test, optimize, or secure. Backward compatibility is not a constraint yet; correctness, clarity, and long-term quality are.

Current status

The initial compatibility-shim audit has been largely implemented. The documented C API still uses JavaScriptCore-shaped names where useful for embedders, but the old dangerous pattern of silently ignoring compatibility parameters has been removed or converted into explicit fail-fast behavior.

Implemented cleanup includes:

  • JSEvaluateScript honors thisObject, sourceURL, and startingLineNumber through source-aware syntax/runtime metadata.
  • JSGlobalContextRetain / JSGlobalContextRelease maintain a checked C-API refcount.
  • JSObjectSetProperty honors property attributes.
  • JSObjectMakeFunctionWithCallback honors function names and rejects null callbacks.
  • Non-null JSClassRef inputs are rejected rather than silently ignored.
  • C-boundary hardening rejects or guards null handles, wrong-context handles, invalid UTF-8, overflowed refcounts/protection counts, malformed argv vectors, ambiguous result-wrapper allocation failures, host callback null returns, and foreign-thread JSWorkerRef handles.
  • C-created objects/arrays/functions use ordinary runtime prototypes and real runtime conversion/call/construct/property semantics rather than compatibility-shaped stubs.
  • Docs describe zig-js semantics directly and call out zig-js extensions (ZJSGlobalContextCreateThreaded, JSWorker*) instead of implying broad JavaScriptCore compatibility.

Remaining policy

  • Keep taking breaking API changes before stabilization when they make the contract simpler, safer, or more internally coherent.
  • Do not expose test-only controls such as parallel_js, parallel_midscript_gc, or harness profile switches as stable embedder API.
  • If a future compatibility-shaped parameter is added, it must have real documented behavior, a focused test, or a documented fail-fast reason.
  • Close this tracker after the latest full CI gate is green, or split any newly discovered API/security cleanup into narrower follow-up issues.

Acceptance criteria

  • No stable API surface keeps an inert compatibility parameter unless there is a documented reason and a test for the promised behavior.
  • Docs describe zig-js semantics directly rather than implying JavaScriptCore compatibility where it is incomplete.
  • Any breaking rename/removal is done before stabilization and covered by focused compile/API tests.
  • The C and Zig entry points remain easy for embedders to use without relying on undocumented harness controls.
9 days ago

Landed the first cleanup slice in 79add18 (docs(api): prefer zig-js contracts over shims).

What changed:

  • Reworded README, guide, homepage, architecture, build comments, and C API module comments away from "drop-in" / "C-API-compatible" as a product promise.
  • docs/api.md now explicitly says JavaScriptCore-shaped entry points are an embedder convenience during pre-stabilization, not a commitment to keep inert arguments forever.
  • The current inert parameters are still documented (JSGlobalContextRetain, JSEvaluateScript source metadata/thisObject, JSObjectSetProperty attributes, JSObjectMakeFunctionWithCallback name), but framed as gaps to implement, redesign, or remove before the API is stable.

Validation:

  • bun run docs:build
  • zig build test -Dtest-filter='C-API:' --summary none
  • git diff --check

This does not remove symbols yet; it makes the intended API direction explicit before doing breaking cleanup.

9 days ago

Landed 683f4d6 (fix(c-api): honor host function names).

This removes one inert compatibility argument from the #22 audit list:

  • JSObjectMakeFunctionWithCallback(ctx, name, callback) now installs a real function name property from the provided JSStringRef.
  • The property has normal function-name attributes: non-writable, non-enumerable, configurable.
  • C-created host callback functions now inherit from Function.prototype and can be called from JavaScript code, not only through JSObjectCallAsFunction.
  • Function.prototype.toString.call(hostFn) now uses the named native-function form, e.g. function hostAnswer() { [native code] }.
  • The owning callback context is stored in a dedicated internal callback_context field rather than overloading embedder private_data.

Validation:

  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check

Remaining inert C API arguments from this tracker are now focused on JSGlobalContextRetain, JSEvaluateScript source/this metadata, and JSObjectSetProperty attributes.

9 days ago

Landed 9531d1f (fix(c-api): honor property attributes).

This removes another inert compatibility argument from the #22 audit list:

  • Added the JavaScriptCore-shaped property attribute constants in the C API layer: kJSPropertyAttributeNone, ReadOnly, DontEnum, and DontDelete.
  • JSObjectSetProperty now maps those bits to real JavaScript descriptor fields:
    • ReadOnly -> writable: false
    • DontEnum -> enumerable: false
    • DontDelete -> configurable: false
  • Added a C-API witness that reflects the descriptor from JS, verifies assignment does not overwrite a read-only host property, verifies delete fails for DontDelete, and verifies Object.keys omits DontEnum.
  • Docs now remove JSObjectSetProperty from the inert-argument caveat and describe the attribute mapping.

Validation:

  • zig build test -Dtest-filter='C-API: JSObjectSetProperty honors property attributes' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check

Remaining inert C API arguments from this tracker are now JSGlobalContextRetain reference counting and JSEvaluateScript thisObject / sourceURL / startingLineNumber metadata.

9 days ago

Landed 5e15b79 (fix(c-api): refcount global contexts).

This removes JSGlobalContextRetain from the inert compatibility list:

  • JSGlobalContextCreate and ZJSGlobalContextCreateThreaded now initialize a C-API context refcount at 1.
  • JSGlobalContextRetain increments that count and returns the same handle.
  • JSGlobalContextRelease decrements and only calls Context.destroy() when the final C-API reference is released.
  • Zig-owned contexts still keep their direct single-owner destroy() contract; the refcount is only initialized by the C API creation entry points.
  • Added a C-API witness that retains a context, releases the original reference, evaluates more JS through the retained handle, then final-releases it.

Validation:

  • zig build test -Dtest-filter='C-API: JSGlobalContextRetain keeps context alive until final release' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check

Remaining inert C API arguments from #22 are now concentrated in JSEvaluateScript: thisObject, sourceURL, and startingLineNumber.

9 days ago

Implemented another C-API cleanup chunk in 5c6ab60a:

  • JSEvaluateScript now honors a non-null thisObject by using it as the top-level this binding.
  • Null thisObject keeps the normal Script default (this === globalThis).
  • Added Context.evaluateWithThis so embedders do not need source wrapping for this behavior.
  • Added a focused C-API regression test and updated docs/api.md; remaining inert JSEvaluateScript args are now only sourceURL and startingLineNumber.

Validation run:

  • zig build test -Dtest-filter='C-API: JSEvaluateScript honors explicit thisObject' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Landed ff14b4f (fix(c-api): report date values).

C-API cleanup/correctness slice:

  • JSValueIsDate is no longer a hardcoded false stub.
  • It now reports the runtime Date internal slot (Object.is_date), including invalid Date objects (new Date(NaN)).
  • Added a C-API witness that checks new Date(0) and new Date(NaN) are dates, while callable Date() and a plain host object are not.
  • Updated docs/api.md to document the Date internal-slot behavior.

Validation run:

  • zig build test -Dtest-filter='C-API: JSValueIsDate reports Date internal slot' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check

I also inspected JSEvaluateScript sourceURL / startingLineNumber. The current error stack/location plumbing does not carry source locations yet, so those should not get a decorative/fake implementation; they either need real source-location design or a pre-stabilization API simplification.

9 days ago

Landed c87252d (fix(c-api): use real ToObject conversion).

C-API semantics cleanup:

  • JSValueToObject now routes through the interpreter’s JavaScript ToObject operation instead of allocating an anonymous empty object for every primitive.
  • Number / Boolean / String primitives now become real wrapper objects with the matching prototype, so methods like valueOf() and Object.prototype.toString.call(...) behave correctly.
  • Existing objects are returned unchanged.
  • null and undefined now report an exception instead of pretending conversion succeeded.
  • Updated docs/api.md to document the conversion and exception behavior.

Validation run:

  • zig build test -Dtest-filter='C-API: JSValueToObject uses JavaScript ToObject semantics' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Landed 4518db8 (feat(c-api): expose guarded private data).

C-API / security-boundary cleanup:

  • Added JSObjectGetPrivate(JSObjectRef) and JSObjectSetPrivate(JSObjectRef, void*).
  • JSObjectMake(..., data) now marks that pointer with a distinct host-owned private-data tag.
  • JSObjectGetPrivate only returns host-owned private data, so engine-owned native records are not exposed through the C API.
  • JSObjectSetPrivate updates host-owned data and can attach host data to plain objects without existing engine private data, but refuses to overwrite internal native records.
  • Updated the C API docs, README export list/count, and Home integration notes.

Validation run:

  • zig build test -Dtest-filter='C-API: JSObject private data is host-owned and guarded' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Landed fae1e96 (fix(c-api): recognize native constructors).

C-API predicate correctness slice:

  • JSObjectIsConstructor now uses the runtime constructability predicate instead of checking only JS/error constructors.
  • Native constructors such as Date and Array are now reported as constructors.
  • Added a C-API witness that checks Date is callable and constructable, constructs a Date through JSObjectCallAsConstructor, and verifies host callback functions/plain objects remain non-constructors.
  • Updated docs/api.md to document that JSObjectIsConstructor uses the runtime constructability check.

Validation run:

  • zig build test -Dtest-filter='C-API: JSObjectIsConstructor recognizes native constructors' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Landed b87f2eb (fix(c-api): use real ToNumber conversion).

C-API conversion semantics cleanup:

  • JSValueToNumber now routes through the interpreter’s JavaScript ToNumber operation instead of the lower-level value helper.
  • Object coercion now observes valueOf / toString / @@toPrimitive behavior.
  • Throwing coercions, Symbol values, and BigInt values now report through the C API exception out-param and return NaN.
  • The conversion registers an active interpreter while it may run JS coercion code, so GC sees the frame.
  • Updated docs/api.md with the real conversion semantics.

Validation run:

  • zig build test -Dtest-filter='C-API: JSValueToNumber uses JavaScript ToNumber semantics' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Landed a964c17 (fix(c-api): use real ToString conversion).

C-API conversion semantics cleanup:

  • JSValueToStringCopy now routes through the interpreter’s JavaScript ToString operation instead of the lower-level value rendering helper.
  • Object coercion now observes toString / valueOf / @@toPrimitive behavior.
  • Throwing coercions and Symbol values now report through the C API exception out-param and return null.
  • Like the ToNumber fix, the conversion registers an active interpreter while it may run JS coercion code, so GC sees the frame.
  • Updated docs/api.md with the real conversion semantics.

Validation run:

  • zig build test -Dtest-filter='C-API: JSValueToStringCopy uses JavaScript ToString semantics' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Landed 14179d2 (fix(c-api): report primitive tagged types).

C-API primitive-boundary cleanup:

  • JSValueGetType now reports Symbol primitives as symbol and BigInt primitives as a zig-js bigint extension instead of leaking their internal object-tag representation.
  • JSValueIsObject now returns false for Symbol and BigInt primitives.
  • JSValueToObject no longer fast-returns those object-tagged primitives; it routes them through real ToObject, producing Symbol / BigInt wrapper objects.
  • Added focused C-API witnesses for primitive type reporting and Symbol/BigInt wrapper behavior through JSValueToObject.
  • Updated docs/api.md to document the primitive type reporting behavior.

Validation run:

  • zig build test -Dtest-filter='C-API: primitive object-tagged values report primitive types' --summary none
  • zig build test -Dtest-filter='C-API: JSValueToObject uses JavaScript ToObject semantics' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API cleanup chunk in 42eba48.

JSObjectGetProperty now routes through the interpreter’s JavaScript [[Get]] path instead of reading only own data slots. That means C callers now see prototype lookup and accessor/proxy behavior, and throwing getters report through the exception out pointer.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectGetProperty uses JavaScript get semantics" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API property semantics cleanup in bebf1ea.

JSObjectGetPropertyAtIndex now converts the index to the JavaScript property key and uses runtime [[Get]] rather than direct element storage. This covers inherited indexed properties, accessor side effects, and getter/proxy exceptions through the exception out pointer.

Validation:

  • zig build test -Dtest-filter="C-API: array construction and indexed get use JavaScript get semantics" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API semantic cleanup in 3f41b95.

JSValueIsEqual now goes through the interpreter’s JavaScript abstract equality (==) path instead of the pure value helper. This means object-to-primitive coercion runs in-order, BigInt/Symbol cases follow the runtime operator semantics, and throwing coercions report through the exception out pointer.

Validation:

  • zig build test -Dtest-filter="C-API: JSValueIsEqual uses JavaScript abstract equality semantics" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API correctness/API cleanup in bf05f80.

JSObjectMakeArray now allocates through the runtime array path instead of constructing a bare is_array object. C-created arrays now inherit from the current realm’s Array.prototype, so methods such as join / map work and the object has the same prototype behavior as arrays created by JavaScript.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectMakeArray inherits Array prototype" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API object-shape cleanup in e05ed97.

JSObjectMake now creates an ordinary object in the current realm through the interpreter object path, then attaches the host-owned private data tag. Host-created objects therefore inherit from the realm’s Object.prototype and expose ordinary inherited methods/prototype behavior instead of being accidental proto-less objects.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectMake creates ordinary realm objects" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API call-semantics cleanup in 709fa83.

JSObjectCallAsFunction now defaults a null thisObject to the context global object for both JS functions and direct C host callbacks. The JS-call path also registers an active interpreter while it may run JavaScript, so GC sees the executing frame. Non-null non-object thisObject handles now report a TypeError instead of silently becoming undefined.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectCallAsFunction defaults null thisObject to global object" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API runtime-safety cleanup in edbe893.

JSObjectCallAsConstructor now installs the active heap/arena and registers the interpreter while it runs the runtime [[Construct]] path. This keeps constructor execution visible to GC/runtime bookkeeping and preserves exception reporting for throwing JavaScript constructors.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectCallAsConstructor runs JavaScript constructors and reports throws" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented a small C-API GC-safety cleanup in 179ed03.

JSObjectMakeFunctionWithCallback now allocates C host callback function objects through the GC-aware object allocator instead of the arena-only fallback. The existing function name/prototype behavior is preserved, and the witness now also checks hostAnswer instanceof Function.

Validation:

  • zig build test -Dtest-filter="C-API: callback functions honor name and Function prototype" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API runtime-safety cleanup in 3b91739.

JSObjectMakeDeferredPromise now registers its temporary interpreter as active while creating the Promise and native resolve/reject functions, and it preserves thrown interpreter exceptions if any promise-capability allocation path throws. This aligns deferred promise creation with the other C entry points that can allocate runtime-managed JS cells through an interpreter.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectMakeDeferredPromise" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented a C-boundary hardening cleanup in cc7b50f.

The C API now rejects malformed argument vectors where argc > 0 but argv == null for:

  • JSObjectMakeArray
  • JSObjectCallAsFunction
  • JSObjectCallAsConstructor

Those cases now return failure and set the exception out pointer instead of indexing a null C pointer or passing a malformed vector through to a host callback.

Validation:

  • zig build test -Dtest-filter="C-API: argc rejects null argv" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-boundary hardening cleanup in 029e672.

String entry points now handle null C pointers safely:

  • JSStringCreateWithUTF8CString(null) returns null instead of spanning a null pointer.
  • JSStringGetUTF8CString returns 0 for a null string, null output buffer, or zero buffer size.
  • Existing non-null behavior still writes a null-terminated UTF-8 prefix and returns the bytes written including the terminator.

Validation:

  • zig build test -Dtest-filter="C-API: JSString null C pointers are rejected safely" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-boundary hardening cleanup in 5e9136f.

JSObjectMakeFunctionWithCallback now rejects a null callback pointer and returns null instead of constructing a callable object whose callback slot would fail later. The public callback type now models that nullability explicitly on the Zig side while preserving the existing C ABI shape.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectMakeFunctionWithCallback rejects null callback" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Split the remaining JSEvaluateScript source metadata work into #23.

The current parser/runtime path does not carry source-aware diagnostics yet, so sourceURL / startingLineNumber should be implemented as real source-location plumbing rather than a C-boundary-only placeholder. #23 tracks that larger design and acceptance tests while #22 can continue collecting smaller pre-stabilization C/API cleanup slices.

9 days ago

Implemented another C-API runtime-safety consistency cleanup in 0052f49.

C entry points that allocate runtime-managed cells through temporary interpreters now publish those interpreters while they allocate:

  • JSValueToObject
  • JSObjectMake
  • JSObjectMakeArray
  • JSObjectMakeFunctionWithCallback

This aligns those paths with the newer C-API conversion/call/worker fixes so GC/runtime root tracking can see the temporary interpreter frame during allocation.

Validation:

  • zig build test -Dtest-filter="C-API:" --summary none
  • git diff --check
9 days ago

Implemented a small C-API mutation safety cleanup in 0ebc58a.

JSObjectSetProperty now sets the active heap and active string arena while it mutates object properties/attributes. This keeps data-slot writes, accessor replacement, and descriptor metadata allocation under the same runtime context discipline as the other C object entry points.

Validation:

  • zig build test -Dtest-filter="C-API: JSObjectSetProperty honors property attributes" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • git diff --check
9 days ago

Implemented another pre-stabilization API cleanup in b36e552.

Non-null JSClassRef inputs are no longer silently ignored:

  • JSGlobalContextCreate(nonNullClass) returns null.
  • JSObjectMake(ctx, nonNullClass, data) returns null.

Full JavaScriptCore class definitions remain out of scope for the current subset, so failing fast is clearer and safer than pretending those class hooks were installed.

Validation:

  • zig build test -Dtest-filter="C-API: unsupported JSClassRef inputs fail fast" --summary none
  • zig build test -Dtest-filter="C-API:" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

C-API cleanup progress in 784fb60:

JSEvaluateScript now uses sourceURL and startingLineNumber for syntax/parse exception diagnostics instead of ignoring them. The parser records a best-effort source location from token byte offsets, Context.evaluateWithThis preserves that diagnostic for embedding surfaces, and docs/api.md no longer lists those parameters as inert.

Validated with focused parser location filters, the C-API test filter, and bun run docs:build. Broader source-note and stack metadata remains tracked under issue #23.

9 days ago

Implemented another C-API boundary cleanup in b7bc6e2.

JSEvaluateScript now rejects a null script source explicitly: with a valid context and exception out pointer it returns null and reports an exception containing script is null, instead of silently returning null. docs/api.md now documents that contract.

Validation:

  • zig build test -Dtest-filter="C-API: JSEvaluateScript rejects null script with exception" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another C-API hardening cleanup in 32cd490.

JSObjectCallAsFunction and JSObjectCallAsConstructor now report OutOfMemory through the exception out pointer when collecting JS argument refs into runtime Values fails. Previously those paths returned null silently after the null-argv guard, which made huge argc / allocation-failure cases indistinguishable from other null returns.

Validation:

  • zig build test -Dtest-filter="C-API: argv collection failures report exception" --summary none
  • git diff --check
9 days ago

Implemented another C-API contract cleanup in 1b60f19.

JSObjectGetProperty and JSObjectSetProperty now reject null property-name strings by reporting an exception through the out pointer instead of silently returning undefined/no-op. docs/api.md now records the null-name contract.

Validation:

  • zig build test -Dtest-filter="C-API: property accessors reject null names with exception" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented a C-API argument hardening follow-up in 4298534.

JSObjectMakeArray now preflights/collects the C argv vector with the shared argument collector before appending elements. That means impossible huge argc values fail with an OutOfMemory exception before the implementation walks argv[i], matching the call/construct hardening path.

Validation:

  • zig build test -Dtest-filter="C-API: argv collection failures report exception" --summary none
  • git diff --check
9 days ago

Implemented another C-API property-boundary cleanup in 10e72f5.

JSObjectGetProperty, JSObjectGetPropertyAtIndex, and JSObjectSetProperty now reject null/non-object object refs through the exception out pointer instead of returning undefined or silently no-oping. docs/api.md now documents this alongside the null property-name contract.

Validation:

  • zig build test -Dtest-filter="C-API: property accessors reject null objects with exception" --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C API cleanup in ae9cba2.

What changed:

  • JSWorkerPostMessage now reports TypeError through the exception out pointer when the worker ref is null/invalid instead of silently returning false.
  • JSWorkerReceive now reports TypeError through the exception out pointer when the worker ref is null/invalid instead of silently returning null.
  • docs/api.md now documents null-worker exception behavior alongside structured-clone failures.
  • Added focused coverage: C-API: worker APIs reject null workers through exception.

Validation:

  • zig build test -Dtest-filter='C-API: worker APIs reject null workers through exception' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C API cleanup in 831a9f4.

What changed:

  • Added a value-ref boundary guard for exception-capable value APIs.
  • JSValueIsEqual, JSValueToNumber, JSValueToStringCopy, and JSValueToObject now reject null JSValueRef inputs with a TypeError through the exception out pointer instead of silently treating null handles as JavaScript undefined.
  • docs/api.md documents the new null-value-ref contract.
  • Added focused coverage: C-API: value coercion APIs reject null value refs with exception.

Validation:

  • zig build test -Dtest-filter='C-API: value coercion APIs reject null value refs with exception' --summary none
  • zig build test -Dtest-filter='C-API: JSValueToObject uses JavaScript ToObject semantics' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C API cleanup in 2ed15d2.

What changed:

  • collectArgs now rejects null JSValueRef elements inside non-null argv arrays instead of silently treating them as JavaScript undefined.
  • JSObjectMakeArray, JSObjectCallAsFunction, and JSObjectCallAsConstructor now report a TypeError through the exception out pointer for null argv elements.
  • Native host callback calls now preflight argv before crossing the callback boundary, so callbacks do not receive invalid value handles.
  • docs/api.md documents null argv-element rejection.
  • Added focused coverage: C-API: argv rejects null value refs.

Validation:

  • zig build test -Dtest-filter='C-API: argv rejects null value refs' --summary none
  • zig build test -Dtest-filter='C-API: argc rejects null argv' --summary none
  • zig build test -Dtest-filter='C-API: callback functions honor name and Function prototype' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C API cleanup in 0e00ad2.

What changed:

  • JSObjectSetProperty now rejects a null JSValueRef value through the exception out pointer instead of storing JavaScript undefined.
  • JSWorkerPostMessage now rejects a null JSValueRef payload through the exception out pointer instead of posting JavaScript undefined.
  • docs/api.md documents these write-boundary null-value rules.
  • Added focused coverage: C-API: value write APIs reject null value refs with exception.

Validation:

  • zig build test -Dtest-filter='C-API: value write APIs reject null value refs with exception' --summary none
  • zig build test -Dtest-filter='C-API: object property get/set' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C API cleanup in 61df91d.

What changed:

  • Added zig-js extension JSType.invalid for null JSValueRef handles.
  • JSValueGetType(null) now returns invalid instead of misreporting JavaScript undefined.
  • No-exception value inspection APIs now treat null handles as invalid input: value predicates, JSValueIsStrictEqual, and JSValueToBoolean return false.
  • Kept exception-bearing value APIs on the stricter TypeError path from prior commits.
  • docs/api.md documents the no-exception null-handle contract.
  • Added focused coverage: C-API: value inspection APIs report null handles as invalid.

Validation:

  • zig build test -Dtest-filter='C-API: value inspection APIs report null handles as invalid' --summary none
  • zig build test -Dtest-filter='C-API: primitive object-tagged values report primitive types' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 clean-contract cleanup in 2396a35.

What changed:

  • JSValueMakeString(ctx, null) now returns null instead of creating JavaScript undefined.
  • This removes another null-handle compatibility shim and keeps null input distinct from an actual JS undefined value.
  • docs/api.md documents the new constructor behavior.
  • Added focused coverage: C-API: JSValueMakeString rejects null string refs.

Validation:

  • zig build test -Dtest-filter='C-API: JSValueMakeString rejects null string refs' --summary none
  • zig build test -Dtest-filter='C-API: string value evaluation round-trips through JSValueToStringCopy' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C API cleanup in a94ab82.

What changed:

  • Native callbacks installed with JSObjectMakeFunctionWithCallback no longer implicitly convert a null return value into JavaScript undefined.
  • A callback must now return a non-null JSValueRef or set the exception out pointer; null without exception throws TypeError.
  • docs/api.md documents the callback contract.
  • Added focused coverage that calls a null-returning host callback through JS and verifies it throws.

Validation:

  • zig build test -Dtest-filter='C-API: host callback null result throws without implicit undefined' --summary none
  • zig build test -Dtest-filter='C-API: callback functions honor name and Function prototype' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 clean-contract cleanup in 01999bd.

What changed:

  • JSObjectMakeDeferredPromise now requires both resolve and reject out pointers.
  • Passing a null resolve or reject slot reports a TypeError through the exception out pointer and returns null.
  • The implementation no longer creates a pending Promise that the host cannot settle because one of the output channels was omitted.
  • docs/api.md now documents the required-output contract.

Validation:

  • zig fmt src/c_api.zig
  • zig build test -Dtest-filter='C-API: JSObjectMakeDeferredPromise' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C-boundary hardening cleanup in f2721ce.

What changed:

  • JSStringCreateWithUTF8CString now rejects invalid UTF-8 by returning null.
  • The shared JsString.create constructor validates UTF-8 before storing bytes.
  • This keeps later string APIs such as JSStringGetLength safe to use validated UTF-8 backing instead of walking arbitrary C bytes with unchecked UTF-8 iteration.
  • docs/api.md now documents the valid-UTF-8 contract alongside the existing null-pointer behavior.

Validation:

  • zig fmt src/jsstring.zig src/c_api.zig
  • zig build test -Dtest-filter='JSString' --summary none
  • zig build test -Dtest-filter='C-API: JSString' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C-boundary hardening cleanup in db2bec8.

What changed:

  • Added JsString.tryRetain() with an atomic checked increment.
  • JSStringRetain now returns null instead of wrapping the string refcount if retaining would overflow.
  • The existing internal JsString.retain() convenience remains checked and panics rather than silently wrapping in impossible/buggy internal use.
  • docs/api.md documents the null-on-overflow retain contract.

Validation:

  • zig fmt src/jsstring.zig src/c_api.zig
  • zig build test -Dtest-filter='JSString' --summary none
  • zig build test -Dtest-filter='C-API: JSString' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C-boundary hardening cleanup in 3bcda97.

What changed:

  • Context.retainCApiRef() now uses a checked atomic increment instead of unchecked fetch-add.
  • JSGlobalContextRetain returns null if retaining would overflow the C-API context refcount.
  • Added a focused C-API witness that forces the refcount to max, verifies retain fails closed, then restores the count for normal release.
  • docs/api.md documents the null-on-overflow retain contract.

Validation:

  • zig fmt src/context.zig src/c_api.zig
  • zig build test -Dtest-filter='C-API: JSGlobalContextRetain' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Closed child tracker #23 as complete.

The JSEvaluateScript source metadata gap is now covered by focused C-API syntax/runtime tests, structured SyntaxError fields, runtime Error source metadata/stack behavior, internal eval/dynamic parser diagnostic fields, and docs/api.md language that describes the supported behavior instead of calling sourceURL / startingLineNumber inert.

Keeping #22 open for broader pre-stabilization API cleanup and future clean-contract/security hardening, but the source-location compatibility placeholder is resolved.

9 days ago

Implemented another #22 clean-contract cleanup in c414f19.

What changed:

  • JSValueProtect and JSValueUnprotect now return bool instead of silently swallowing all failure cases.
  • Invalid/null handles now return false.
  • GC-enabled contexts return false when JSValueUnprotect has no matching protected entry.
  • JSValueProtect rejects protection-count overflow instead of wrapping the counted root.
  • Allocation failure while growing the protected-handle table is now observable as false.
  • docs/api.md and the memory-model binding table document the new pre-stabilization contract.

This is intentionally not JavaScriptCore ABI-compatible; per the current project direction, explicit failure reporting is better than preserving a silent void API before anyone depends on it.

Validation:

  • zig fmt src/c_api.zig
  • zig build test -Dtest-filter='C-API: JSValueProtect' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C-boundary cleanup in feb68b0.

What changed:

  • Exception-capable APIs that successfully produce a JavaScript result now report OutOfMemory through the exception out pointer if they cannot allocate the returned JSValueRef / JSStringRef wrapper, instead of returning an ambiguous silent null.
  • Covered the successful-result boxing paths for evaluation, property gets, calls, construction, array creation, worker receive, JSValueToObject, and JSValueToStringCopy string wrapper creation.
  • Direct JSObjectCallAsFunction calls into C host callbacks now enforce the same null-result contract as the JavaScript-call path: returning null without setting an exception reports TypeError.
  • docs/api.md now documents the result-wrapper failure contract.

Validation:

  • zig fmt src/c_api.zig
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Implemented another #22 C-boundary/security cleanup in a144701.

What changed:

  • JSValueRef / JSObjectRef boxes now carry their owning Context.
  • Context-taking C APIs reject handles created by a different context instead of accidentally mixing arenas, object graphs, globals, or GC state.
  • Exception-capable paths report a TypeError; no-exception inspection/protection APIs return their invalid-handle result (JSType.invalid, false, etc.).
  • Host callback return and exception refs are also owner-checked, so callbacks cannot smuggle wrong-realm handles across the C boundary.
  • Kept Boxed.value as the first field so the GC root scanner can continue tracing protected handles as *Value roots.
  • Added a focused cross-context C-API witness and updated API docs.

Validation:

  • zig fmt src/c_api.zig
  • zig build test -Dtest-filter='C-API: context-owned handles' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Follow-up to the wrong-context handle cleanup landed in a0efcbc.

What changed:

  • Added a dedicated C handle-inspection context helper for no-exception predicates (JSValueGetType, JSValueIs*, JSValueToBoolean, JSObjectIsFunction, etc.).
  • These APIs still enforce debug owner-thread affinity, but they no longer require holding the VM GIL just to inspect an already-returned handle from a serialized threaded C context.
  • This keeps the owner-context checks from a144701 without regressing ZJSGlobalContextCreateThreaded(gil=true) C API ergonomics.

Validation:

  • zig fmt src/c_api.zig
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Follow-up C-boundary hardening landed in 75892bf.

What changed:

  • Removed the remaining raw C handle unboxing helper from src/c_api.zig.
  • JSObjectGetPrivate / JSObjectSetPrivate now use owner-thread checked handle inspection even though their C signatures do not carry a JSContextRef.
  • JSObjectCallAsFunction now feeds its this handle through the same context-validated value path before dispatching to JavaScript functions.
  • This keeps the wrong-context / wrong-thread handle discipline from a144701 and a0efcbc from having a small context-free escape hatch.

Validation:

  • zig fmt src/c_api.zig
  • zig build test -Dtest-filter='C-API: JSObject private data' --summary none
  • zig build test -Dtest-filter='C-API: JSObjectCallAsFunction defaults null thisObject' --summary none
  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Documentation/audit follow-up landed in bbdb7bf.

What changed:

  • docs/api.md no longer says there are still inert compatibility-shaped parameters in the documented C API caveat.
  • The caveat now reflects the current contract: JavaScriptCore-shaped signatures may remain for embedder convenience, but documented parameters either have real behavior or fail fast when the underlying feature is out of scope.

Current acceptance audit against the original #22 checklist:

  • JSEvaluateScript now honors thisObject, sourceURL, and startingLineNumber with source-aware syntax/runtime metadata.
  • JSObjectSetProperty honors property attributes.
  • JSObjectMakeFunctionWithCallback honors function names and rejects null callbacks.
  • JSGlobalContextRetain / Release maintain a real checked C-API refcount.
  • Non-null JSClassRef inputs are rejected instead of silently ignored.
  • The C boundary now rejects/guards null handles, wrong-context handles, invalid UTF-8, overflowed refcounts/protection counts, malformed argv vectors, and ambiguous successful-result wrapper failures.
  • Docs describe zig-js semantics directly and call out zig-js extensions (ZJSGlobalContextCreateThreaded, JSWorker*) instead of implying broad JavaScriptCore compatibility.

Validation for this doc-only slice:

  • bun run docs:build
  • git diff --check

I’m leaving #22 open until the latest pushed CI run completes, then it should be reasonable to close this initial compatibility-shim cleanup tracker or split any future public-API polish into narrower follow-up issues.

9 days ago

CI follow-up for the #22 acceptance run: the latest run failed in tsan-suppression-witness, but the failure was not a C-API/API-contract regression.

Root cause: the old witness expected at least one timing-dependent program-byte case to reproduce a raw TSan race. Neither did, which made the suppression file no longer demonstrably load-bearing. Since the current engine keeps typed-array program-byte paths TSan-clean with locks/atomics, I pushed 762617d (ci(tsan): remove stale suppressions) to remove the stale suppression machinery rather than keep an inert suppression file.

Focused validation before the push:

  • Workflow YAML parse
  • bun run docs:build
  • git diff --check
  • Unsuppressed TSan focused cases:
    • cve/mc-prim-arraybuffer-transfer-vs-atomics.js
    • cve/mc-df-ta-detach-resize.js

I am leaving #22 open until the new full CI run for 762617d completes green.

9 days ago

Follow-up on the #22 acceptance CI gate: after the stale TSan suppression removal, CI progressed further but failed in the unrelated threadfuzz-lifecycle leg, not in the C-API/API-contract surface.

I pushed 194ab8b (test(threadfuzz): park abandoned waiter fuzzers) to address the lifecycle fuzzer failure. The failing mixed waiter race subprogram had abandoned waiter threads spin forever after registering outstanding waitAsync tickets; under CI load that could starve the normal waiter settlement oracle. They now park after publishing readiness, preserving the abandoned-ticket teardown/leak coverage without burning runner CPU.

Focused validation covered the exact CI-failing seeds: 5, 11, 21, 27, and 51. Leaving #22 open until the new full CI run for 194ab8b completes green.

9 days ago

Second lifecycle-fuzzer follow-up for the #22 gate: pushed dd74dbc (test(threadfuzz): avoid waiter-table pollution).

The fresh CI run after 194ab8b showed the mixed waiter lifecycle oracle failing across nearly all seeds. The parked abandoned threads were no longer burning CPU, but they were parking with Atomics.wait(gate, 'ready', ...), which polluted the same property waiter table under test. dd74dbc changes those abandoned threads to sleepMs(1000) after they register the intended outstanding waitAsync tickets and publish readiness.

Focused validation covered representative failing/shifted seeds 1, 5, and 50 locally. Leaving #22 open until the new full CI run for dd74dbc completes green.

9 days ago

Follow-up for the latest TSan lifecycle failure: pushed f9e5e60 (test(threadfuzz): settle worker async-hold joins). The failing job from run https://github.com/zig-utils/zig-js/actions/runs/29142720376 was worker-terminate/ThreadLocal asyncHold cleanup at seed 2, reporting checked got 8/9. Root cause was in the fuzzer oracle rather than the API contract work: release asyncJoin callbacks could settle just after the pre-teardown script threw under TSan, and that script treated any JS throw as the intended teardown throw.\n\nThe patch drains/asserts release join callbacks in the post-teardown check and marks the intended teardown throw so early JS assertion failures are no longer swallowed. Local CI-equivalent validation now passes with:\n\nzig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=2\n\nThis keeps #22 open until the fresh full GitHub Actions run for f9e5e60 is green.

9 days ago

API-contract follow-up pushed in fdc0562 (fix(c-api): enforce worker handle affinity).

The C API docs already said JSWorkerRef is owner-thread-affine, but the implementation only cast the opaque pointer. The worker now records its creator thread and C handle lookup fails closed off-owner, so JSWorkerPostMessage / JSWorkerReceive reject foreign-thread handles instead of racing lifecycle state; JSWorkerTerminate / JSWorkerRelease become no-ops off-owner rather than racing destroy/close.

Focused validation:

  • zig fmt src/c_api.zig src/worker.zig
  • zig build test -Dtest-filter='C-API: worker' --summary none
  • git diff --check

The previous full run had already passed tsan-threadfuzz-lifecycle; this push starts a fresh full CI gate that includes that lifecycle fix plus this C-API worker safety hardening. Keeping #22 open until the latest full run is green.

9 days ago

Docs follow-up for the worker-affinity C-API hardening landed in 3778c8f (docs(api): document worker handle affinity). The API guide now matches the implementation from fdc0562: JSWorkerRef handles are owner-thread-affine, while structured-cloned worker messages are the intended cross-thread boundary.

Validation:

  • bun run docs:build
  • git diff --check

Keeping #22 open until the latest full CI run for 3778c8f is green.

9 days ago

CI follow-up for the API-contract chunk: db57aa3 fixes the threadfuzz-midgc failure from run 29143593643. Root cause was the ThreadLocal asyncHold fuzzer oracle being too strict for the mid-GC variant, where TLS-owner threads are intentionally released before the top-level teardown throw. The C API affinity change itself still looked fine; this keeps the latest direct-to-main chunk moving toward a clean full CI run.

9 days ago

Project-direction note from Chris: backward compatibility does not matter yet because nobody is using this publicly. We should prefer the clean/perfect zig-js API and runtime contract over compatibility shims or preserving early internal shapes.

Practical implication for this issue: if a breaking change makes the API simpler, safer, or more internally coherent, take it and document the new contract rather than carrying transitional behavior.

9 days ago

Implemented another #22 C-boundary hardening slice in ef07c0b (fix(c-api): guard evaluation context access).

What changed:

  • JSEvaluateScript no longer fetches the raw context directly.
  • Added an evaluation-specific C-boundary helper that preserves the documented debug affinity check for non-threaded and true-parallel C contexts.
  • Serialized threaded contexts still work through the public C API because Context.evaluateWithThis acquires the GIL internally before its normal assertion.
  • This keeps the previous compatibility cleanup direction: the C API should fail loudly in debug when a host crosses an unsupported context/thread boundary, instead of accidentally bypassing the access funnel.

Focused validation:

  • zig build test -Dtest-filter="C-API: JSEvaluateScript" --summary none
  • git diff --check

Fresh full CI gate for this commit is pending/running at https://github.com/zig-utils/zig-js/actions/runs/29162789671.

9 days ago

Implemented another #22 lifecycle-boundary hardening slice in b89b1bd (fix(c-api): guard context lifecycle access).

What changed:

  • JSGlobalContextRetain and JSGlobalContextRelease now use a lifecycle-specific C-boundary helper instead of raw context lookup.
  • The helper preserves the documented debug owner-thread affinity check for context handles.
  • It intentionally does not require holding the serialized GIL, because retain/release are host lifecycle operations rather than VM execution; Context.destroy() still handles serialized threaded teardown.
  • JSStringRef retain/release remain thread-safe and unchanged.

Focused validation:

  • zig build test -Dtest-filter="C-API: JSGlobalContextRetain" --summary none
  • zig build test -Dtest-filter="C-API: JSEvaluateScript" --summary none
  • git diff --check

Fresh full CI gate for this commit is pending/running at https://github.com/zig-utils/zig-js/actions/runs/29162858049.

9 days ago

Completed. The compatibility-shim cleanup has landed, and the fresh full gate is green for b2e7dbe5e9bc2a27a87ff1bad8be8b0be777e46e:

https://github.com/zig-utils/zig-js/actions/runs/29170980761

Future API/security cleanup should be split into narrower issues rather than keeping this broad tracker open.

Sign in to comment on this issue.