Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 78 additions & 57 deletions plan/agent-context/v8x-js2wasm-deno-handover-2026-08-12.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# v8x + js2wasm Deno spike handover — 2026-08-12

Updated 2026-08-20 with standardized Wasm EH, the exact three-wrapper
Wasmtime bootstrap, and the public v8x `Script::Run` proof.

The initial spike merged in
[#4396](https://github.com/loopdive/js2wasm/pull/4396). Its compiler/runtime
follow-ups and primordials bootstrap merged in
Expand All @@ -11,7 +14,7 @@ The v8x backend itself is published as ready

## Exact stop point

- Branch: `codex/v8x-js2wasm-deno-spike`
- Active branch: `codex/4376-deno-core-bootstrap`
- Prior spike commit: `f26d0bf23a59e89a23979f27ddf744e762a6b61f`
- Compiler ABI fix: `35423bb9c1d4aa`
- Embedded runtime follow-up: `3917c3caa3a63e`
Expand All @@ -26,27 +29,34 @@ The v8x backend itself is published as ready
(Deno 2.9.2, `deno_core` 0.407.0)
- Strict consumer result: unchanged `deno_core` Rust source compiles; normal
linking refuses 276 unresolved ABI symbols.
- Diagnostic execution result: startup reaches
`ext:core/00_primordials.js`, then v8x reports the missing shared-instance
semantic boundary as an explicit failure.
- Diagnostic runtime result: a dynamic-lookup-only unchanged-Deno executable
run against the exact raw artifact eventually terminated with exit 139 and
no output. Treat this as a failed diagnostic, not as a bootstrap checkpoint.
- Exact wrapper result: unchanged pinned `00_primordials.js`, `00_infra.js`,
and `01_core.js` compile as one 2,946,532-byte state-sharing program.
Standardized `try_table` output precompiles under Wasmtime 47 and boots in
two isolated stores. Both instances return probe `42`; strict throwing stubs
prove that none of the seven deferred imports is called during bootstrap.
- Runtime prototype result: compiler-free Wasmtime 47.0.3 shares one Engine,
direct-Rust host Linker, and cached Module/InstancePre for the trusted
`.cwasm` artifact. Two private stores/instances call the typed Rust
`Deno.cwd()` bridge with exact fresh-instance call counts while the compiler
path is deliberately absent.
- Compiler bootstrap result: the pinned unchanged `00_primordials.js` now
compiles in the two-source graph. #4378 fixes its pristine
`Array.prototype` iterator capture; #4380 fixes the IIFE empty-object carrier
trap. Instrumented execution reaches the first JSON namespace copy.
- Public-API result: v8x validates the exact pinned source hashes/order through
`Script::Run`, keeps the prelinked transaction in one persistent context
runtime, and exposes the first Rust-visible `Deno.core.setUpAsyncStub`
callback effect. This remains a narrow bridge, not general Rust/Wasm object
identity and not a completed unchanged-`deno_core` boot.

The local `.tmp` v8x and Deno checkouts were disposable instruments. Do not
depend on them; the checked-in patch, issue, tests, and this handover are the
portable record.

## Shipped artifacts

- `examples/v8x-js2wasm-spike/v8x-js2wasm.patch` — the complete v8x backend
patch plus its `rusty_v8` integration test.
- `examples/v8x-js2wasm-spike/v8x-js2wasm.patch` — the baseline v8x backend
patch plus its `rusty_v8` integration test. The current standardized-EH
public-script follow-up is maintained in `loopdive/v8x#1`.
- `examples/v8x-js2wasm-spike/compile-graph.ts` — a temporary process-side
compiler adapter that accepts the canonical module manifest and invokes
`compileMulti()` with `target: "standalone", platform: "deno"`.
Expand Down Expand Up @@ -75,26 +85,33 @@ portable record.
6. A typed Rust op can implement the natural `Deno.cwd()` wrapper shape.
7. The trusted, target-specific `.cwasm` artifact executes with no compiler or
Node process available; the runtime dependency graph excludes Cranelift.
8. The exact pinned core wrapper graph can bootstrap to a value-level probe in
two isolated Wasm stores without invoking its deferred Promise/eval imports.
9. Wasmtime 47 can precompile and execute that exact wrapper graph, and v8x's
public `Script::Run` path validates the pinned source sequence and observes
the first Rust-visible callback effect.

## What is not proven

- A booting `deno_core` or portable Deno runtime.
- An unchanged Rust `deno_core` executable completing the exact wrapper
sequence through v8x. The public API proof calls the same `Script::Run`
surface but uses an integration harness and a prelinked transaction.
- General shared object/function identity between Rust-owned v8 handles and
the Wasm wrapper heap; only `setUpAsyncStub` is bridged explicitly so far.
- Deno ops beyond the narrow `cwd` proof, or Web/Node API providers.
- Module namespace objects or live binding updates returning through
`rusty_v8`.
- Shared promise and microtask semantics between Rust and compiled wrappers.
- Dynamic imports, top-level await, synthetic modules, or general URL/module
loading.
- Completion of Deno's real `00_primordials.js` bootstrap, compilation of
`00_infra.js`, extension wrappers, and the generated op manifest into the
v8x-owned AOT instance. The unchanged first source compiles, but stops during
builtin namespace reflection.
- Extension wrappers, an application, and the generated op manifest in the
same v8x-owned AOT instance as the now-booting three-file core wrapper graph.

Do not phrase “unchanged `deno_core` compiles” as “Deno runs.” The strict link
still rejects the incomplete ABI, and the diagnostic link exists only to
identify the next executed boundary.

## Why unchanged Deno still stops at `00_primordials.js`
## Exact wrapper bootstrap versus Rust `deno_core` boot

Before the first core wrapper runs, Rust creates and populates the initial
`Deno.core` object graph. `00_primordials.js` reads and mutates that graph.
Expand All @@ -104,25 +121,19 @@ such as `Object`, `Array`, `Promise`, and `Reflect`. Later wrappers rely on
those copies even if application code monkey-patches the globals. They are
JavaScript object identities and functions—not Rust ops or WASI calls.

The module prototype now shares compiler-free Wasmtime runtime/code/import
state while giving every evaluated module a persistent private store/instance,
and proves a typed `Deno.cwd()` host call. On the compiler side, the pinned
unchanged source is now included honestly with `allowJs` and compiles after two
focused fixes (#4378 and #4380). A temporary checkpoint instrument (not part of
the source or commit) proves execution advances through trusted Reflect/
Function helper capture and stops at:

```js
copyPropsRenamed(globalThis["JSON"], primordials, "JSON");
```
The compiler side now includes the three exact pinned sources honestly with
`allowJs`; no source checkpoint or transformation is used. A small seed
provides only the bootstrap prerequisites, and every unresolved import is a
throwing stub. The program reaches the end of `01_core.js` with zero import
calls and exposes the expected core/internals identities and functions. This
retires the old JSON namespace stop.

That is #3571's corrected boundary: standalone can name some builtin carriers,
but does not yet reify `globalThis` namespace entries such as JSON/Math/Reflect
as objects whose own keys and descriptors Deno can copy. The full compiled
artifact also has `env.Promise_new`, `env.Promise_then2`, and two
`js2wasm:runtime-eval` imports. Wasmtime 45 additionally rejects the emitted
legacy exception encoding (#2997), although Node's Wasm engine can execute the
diagnostic far enough to identify the semantic stop.
Target-gated standardized `try_table` lowering now retires the Wasmtime loader
boundary. The raw artifact precompiles under Wasmtime 47 and boots twice in
v8x-owned stores. The remaining integration boundary is semantic: the current
public `Script::Run` proof executes the prelinked transaction after validating
the exact sources and bridges only `setUpAsyncStub`; the Rust and Wasm heaps do
not yet share general object/function identity.

## Decisions already settled

Expand All @@ -146,29 +157,25 @@ diagnostic far enough to identify the semantic stop.

## Safest next implementation slice

Build the smallest path from the proven `Deno.cwd()` adapter into the first
real Deno core wrapper:

1. Implement #3571's bounded builtin-object value surface needed by the
namespace and intrinsic copy loops, starting with JSON/Math/Reflect and
proving real own-key/descriptor results rather than empty carriers.
2. Remove or replace the Promise and runtime-eval host imports retained by the
full primordials graph, and migrate standalone exception output to
standardized `try_table` through #2997 so embedded Wasmtime can load it.
3. Route `00_primordials.js` and the minimum prerequisite wrapper sources into
v8x's persistent instance as the same program as a tiny probe application.
4. Generate typed imports for only the Rust ops executed by that program. Keep
Advance the now-proven public-script transaction into an unchanged Deno run:

1. Replace the narrow `setUpAsyncStub` callback bridge with stable shared
object/function handles for the parts of `Deno.core` the wrappers mutate.
2. Run the unchanged `deno_core` diagnostic through the pinned `Script::Run`
sequence and record the next real semantic boundary.
3. Generate typed imports for only the Rust ops executed by that program. Keep
the op manifest explicit and generated from the same source of truth Deno
uses.
5. Bridge the initial `Deno.core` namespace into the instance with stable
4. Bridge the initial `Deno.core` namespace into the instance with stable
identity and observable property writes.
6. Prove one value-level effect after wrapper execution, not merely successful
5. Prove another value-level effect after wrapper execution, not merely successful
return—for example, a known primordial captured and read back through the
same context.
7. Add the promise/microtask behavior exercised by this graph within the same
6. Add the promise/microtask behavior exercised by this graph within the same
persistent store.
8. Advance to `00_infra.js` and extension wrappers only after that proof. Add
additional `rusty_v8` ABI functions when an executed call requires them.
7. Advance from the already included `00_infra.js`/`01_core.js` to extension
wrappers and an application only after that proof. Add additional
`rusty_v8` ABI functions when an executed call requires them.

Keep the first slice narrow. Module namespace exports, live bindings, dynamic
imports, top-level await, synthetic modules, inspector support, and full Web or
Expand Down Expand Up @@ -219,14 +226,23 @@ strict `cargo build` is expected to reach the normal linker and reject the
remaining ABI. Do not make the diagnostic `-undefined dynamic_lookup` option
part of a production build.

The exact compiler-side wrapper proof is:

```sh
pnpm exec vitest run \
tests/issue-4376-deno-primordials-runtime.test.ts \
tests/issue-4376-deno-core-bootstrap.test.ts \
--pool=forks --poolOptions.forks.singleFork=true --no-file-parallelism
```

## Last validation

- Focused compiler/v8x plus existing multi-file regression tests: 20/20
passed.
- Bootstrap regressions #4378/#4380: 5/5 passed; each artifact has zero host
imports.
- Pinned unchanged `00_primordials.js`: compiles as a two-module graph;
checkpointed diagnostic reaches the first JSON namespace copy.
- Exact pinned three-wrapper bootstrap plus focused primordials regressions:
16/16 passed. The 2,946,121-byte artifact boots twice, returns `42` twice,
and calls none of its seven deferred imports.
- Broader focused compiler/v8x/multi-file audit: 109/109 relevant tests passed.
The five `issue-1472.test.ts` failures reproduce identically on pristine
`origin/main` and are not regressions from this branch.
- v8x source-compile js2wasm integration: 1/1 passed.
- v8x compiler-free AOT integration with an invalid compiler path: 1/1
passed; the test observes one module load, one cached module, two isolated
Expand All @@ -236,8 +252,13 @@ part of a production build.
precompiled fixture: 1,434,192 bytes.
- Vendored simdutf suite: 14/14 passed.
- Unchanged `deno_core` Rust consumer check: passed with Wasmtime 45.
- Unchanged `deno_core` diagnostic runtime: terminated with exit 139 and no
output; no additional semantic boundary is claimed.
- Repository TypeScript typecheck: passed.
- Focused Prettier check: passed.
- v8x base-feature `js2wasm_spike` integration test target: compiles after
correctly gating the ignored raw-bootstrap diagnostic on
`js2wasm_runtime_compile`.
- Patch reverse-apply check against the pinned v8x checkout with
`git apply --unidiff-zero`: passed.

Expand Down
21 changes: 20 additions & 1 deletion plan/issues/2997-try-table-eh-migration.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
id: 2997
title: "Migrate legacy Wasm EH (try/catch 0x06/0x07) to try_table so binaries run under modern wasmtime/wasmer"
status: ready
status: in-review
sprint: Backlog
created: 2026-07-02
priority: medium
Expand Down Expand Up @@ -408,3 +408,22 @@ architect spec first" — the spec is above.
- Port the finally/break-continue-return depth regressions from #993 and #1858
to the gated path (nested try, `continue outer` inside finally).
- The #2962 exception-render harness stays green (host lane, unchanged bytes).

## Implementation status — 2026-08-20

The target-gated migration is implemented for standalone/WASI output while the
JavaScript-host lane retains the legacy encoding. The implementation adds the
`try_table` IR, binary/object/WAT encoders, structured branch-depth retargeting,
walker/fixup support, and matching IR-backend lowering.

Focused coverage now proves:

- simple catch, nested catch/rethrow/finally, break/continue, and return-through-
finally semantics;
- a real WASI exception module executing under current Wasmtime;
- the pinned `deno_core` 0.407.0 wrapper transaction precompiling under
Wasmtime 47 and booting in two isolated stores with probe value `42`; and
- the JavaScript-host lane continuing to use the legacy representation.

The broader host-lane conversion to `exnref`/`throw_ref` remains the separately
scoped slice 2 and is not required by the Deno/Wasmtime prototype.
Loading
Loading