ReviewOS

zig-utils/zig-js

WebAssembly reference values: add precise-GC externref slot substrate

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

Parent: #271 Depends on: #262 Roadmap: #263, #142, #134

Replace the scalar-only Wasm value assumption with a typed reference-slot substrate that remains precise under ordinary, mid-script, and no-GIL collection.

Acceptance:

  • Represent null funcref, function references, and arbitrary externref values without pointer/tag ambiguity.
  • Publish live operand-stack, local, global, argument, and result reference edges to the existing precise collector.
  • Make slot copy/drop/frame unwind/trap paths update liveness failure-atomically.
  • Preserve reference identity across Wasm-to-Wasm and Wasm-to-JavaScript calls without permanently rooting dead values.
  • Add forced-GC and no-GIL stress tests proving live retention and exact reclamation after overwrite/drop/unwind.
9 days ago

Initial audit: exec.State.stack, locals, and GlobalInst.value are scalar u64; TableOwner.refs is already an atomic GC-visible mirror traced through the wrapper, and active Interpreters are already explicit realm roots. The implementation should extend the active-execution publication model for typed Wasm reference slots, rather than permanently appending externrefs to InstanceDescriptor.roots. That preserves exact reclamation and remains compatible with parallel root handshakes.

9 days ago

Implementation design after collector audit:

  1. Add a typed Wasm slot (numeric, funcref, externref) so Value.bits, native function pointers, numeric bits, and null are never inferred from payload patterns.
  2. Register each live Wasm execution root set on the active Interpreter, under its existing gc_root_lock; extend gc.traceInterpreterRoots so ordinary/quiescent and parked-peer traces see externref slots exactly like VM stacks/frames.
  3. Extend the parallel self-publication path to snapshot Wasm slots, and add bounded checkpoints at backward branches/calls so long Wasm loops cannot starve/conceal roots from a peer collector. A running peer is never read concurrently.
  4. Keep persistent table/global roots in atomic GC-visible mirrors owned by their wrappers/instance, while execution stack/local/argument/result roots exist only for the invocation and are cleared on pop, overwrite, return, trap, and host-exception unwind.
  5. During Wasm-to-JS callbacks the Wasm state is frozen but remains registered, so the callback may allocate/collect without losing references held only by Wasm.

This can use the current zig-gc binding/visitor contract; no dependency API change is warranted unless implementation proves the existing root-publication hook insufficient.

9 days ago

Completed and pushed to main in six focused commits:

  • 506963ef registers invocation-scoped Wasm execution roots and bounded safepoints.
  • 9923f636 replaces scalar transient storage with explicit numeric / funcref / externref slots; numeric and pointer-identical funcref payloads are not traced.
  • 52bdc566 adds typed import/invocation argument and result slots, preserves externref/funcref identity, and rejects numeric aliases for reference signatures.
  • 9e651a54 adds tagged persistent globals, exact overwrite clearing, and the insertion barrier.
  • 639582f5 proves stack/local liveness transitions and forced-GC retention followed by WeakRef reclamation.
  • e2f01223 proves a real finished no-GIL parallel mid-script collection retains an externref held only by a frozen Wasm frame, then reclaims it exactly after unregistration.

Verification:

  • dedicated no-GIL filter: 3 passed, 0 skipped, 0 failed, 0 leaked
  • the new case also passed as test 396 in the broader engine run
  • execution-root balance covers normal return, backward-branch checkpoint, and trap unwind

The existing zig-gc visitor/barrier contract was sufficient; no dependency API change was needed.

Sign in to comment on this issue.