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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ just conformance # official CM suite, Deno lane
just sched-seeds # seeded-shuffle reruns: DELTIC_SCHED_SEED=1, =4242 (FIFO when unset)
just test-ports # ports/webcrypto + ports/websocket unit suites
just test-webrtc # ports/webrtc unit suite
just shells # pinned engine shells, required gates (jsc-pinned is x64-only)
just shells # pinned engine/runtime lanes: sm + node everywhere, jsc on x64, bun findings-only
just browsers # chromium + firefox lanes (`just browsers-install` once)
just websocket-conformance # their suite under this host (spawns their echod)
just smoke-tls # polymorph-tls suite (issue #18)
Expand Down
16 changes: 12 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ the `Suspender` object was removed).
| Chrome/Chromium ≥ 137 | on by default | browser lane (exact Deno parity) |
| Firefox | flag: `javascript.options.wasm_js_promise_integration` | browser lane (pref flipped by the driver) |
| Safari / WebKit | works unflagged on WPE 26.5 (M3 finding); shipping-Safari status: [#11](https://github.com/lann/deltic/issues/11) | browser lane; pinned build capped by JSC's missing multi-memory — implemented and default-on in WebKit trunk (webkit-2342+ rolls reach 1248/0, effective parity; #11) |
| Node | on by default ≥ 26 | optional distribution target, not a lane — Deno substitutes across the consumer surface ([consumers.md](consumers.md)) |
| Node | on by default ≥ 26 | pinned runtime lane (`node-pinned`, v26.x: exact Deno parity, no flags; required gate). Node 24 LTS deliberately not laned: flag-gated JSPI (`--experimental-wasm-jspi`) whose older V8 13.6 vintage deviates on 2 corpus commands — see `harness/shell/expectations/node-pinned.ts` |
| Bun | on by default (1.3.x, vendored JSC) | pinned runtime lane (`bun-pinned`, findings-only until a track record): exact Deno parity under `BUN_JSC_useWasmMultiMemory=1` (driver-set; stock bun ships multi-memory off → 174 failures) — see `harness/shell/expectations/bun-pinned.ts` |

Notes:

Expand All @@ -129,6 +130,10 @@ Notes:
differences are normalized in the harness matcher, never in the runtime
(`TRAP_MESSAGE_EQUIVALENTS`, harness/src/runner.ts) — with them
reconciled, Firefox and trunk WebKit run at exact Deno-lane parity.
- The node/bun lanes add **embedding** coverage, not engine coverage (V8 and
JSC are already exercised above): module loading, event-loop integration,
and runtime I/O quirks — e.g. node's pooled `Buffer`, whose pool-backed
`.buffer` must never reach WebAssembly APIs (`tools/shell/host-node.mjs`).
- **Type reflection (js-types) is phase 3 and flagged everywhere** — function
signatures are not available from `WebAssembly.Module.imports()`. The
architecture below sidesteps this (the translator emits all type
Expand Down Expand Up @@ -561,11 +566,14 @@ cannot even validate component binaries). The TS harness executes the JSON
identically under `deno test` and in browsers (`tools/browser/run-lane.ts`:
static server + automated Chromium / Firefox-with-pref / WebKit, with
per-lane expectation overlays and stale-delta detection) — and directly
under engine *shells* (`tools/shell/run-lane.ts`: SpiderMonkey `js`, JSC
`jsc`; same classification machinery, no browser). Because the corpus is
under engine *shells* and server runtimes (`tools/shell/run-lane.ts`:
SpiderMonkey `js`, JSC `jsc`, and node/bun via a host preamble; same
classification machinery, no browser). Because the corpus is
engine-shaped, the per-push/PR engine gates are the **pinned shell lanes**
(`sm-pinned` = the Firefox-release shell matching the browser lane,
`jsc-pinned` = a sha256-mirrored trunk build; `tools/shell/pins.json`);
`jsc-pinned` = a sha256-mirrored trunk build, `node-pinned` = the node ≥ 26
runtime; `bun-pinned` rides along findings-only until it has a track
record; `tools/shell/pins.json`);
browser lanes run post-merge, verifying the embedding and shipped-channel
configs and gating the prerelease. Trunk/nightly shells and a Deno-canary
probe run weekly as findings-only canaries (`.github/workflows/canary.yml`)
Expand Down
60 changes: 60 additions & 0 deletions harness/shell/expectations/bun-pinned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Bun pinned lane expectation — findings-only (`required: false`) until it
// has a CI track record, the WebKit-lane precedent (issue #11: non-blocking
// until proven). Promote by flipping `required` after a few green weeks;
// deviations meanwhile print loudly but exit 0 (infrastructure failures
// still exit 2 and do gate).
//
// PIN: bun v1.3.14 (oven-sh/bun GitHub release zip, sha256-verified against
// the release's SHASUMS256.txt — tools/shell/pins.json). Both linux arches.
//
// TOTALS: seeded as EXACT Deno-lane parity from a local measurement
// (2026-08-11, linux-arm64 dev box, bun 1.3.14's vendored JSC): 1254 passed
// / 0 failed / 95 xfail, zero bun-specific deltas — but ONLY under
// `BUN_JSC_useWasmMultiMemory=1`, which the driver sets (run-lane.ts):
//
// * Stock bun 1.3.14 ships wasm multi-memory default-OFF and fails 174
// corpus commands with "there can at most be one Memory section for
// now" — the CABI routinely needs >1 memory per core module, the same
// gap that capped the pinned-WebKit browser lane (issue #11; JSC trunk
// and the jsc-pinned shell have it default-on since rev 318852@main).
// The implementation is vendored, just not enabled; the env flip is the
// firefox-lane precedent (that driver sets its JSPI pref itself).
// * JSPI, by contrast, is ON by default in stock bun (round trip
// verified) — bun curates its own JSC option defaults, hence the
// asymmetry with both Safari and the jsc shell.
// * memory64 and relaxedSimd probe false; nothing in the current corpus
// requires either (capability probes are informational). A future
// corpus bump touching them will surface here as deviations to triage.
//
// THE FLAG IS AN UNSTABLE SURFACE: bun prints "options change between
// releases of Bun and WebKit without notice" for unknown/renamed BUN_JSC_*
// options (and silently ignores them otherwise). The pin freezes that risk.
// A RE-PIN MUST RE-VERIFY the option name and re-measure: a bun that
// renames or drops `useWasmMultiMemory` regresses to the stock 174-failure
// shape, which this expectation catches loudly (totals mismatch), findings
// lane or not.

import type { ShellLaneExpectation } from "./types.ts";

export const bunPinned: ShellLaneExpectation = {
lane: "bun-pinned",
required: false,
notes:
"Bun pinned (v1.3.14, GitHub release zip, sha256-verified, both arches; " +
"vendored JSC). Exact Deno-lane parity under BUN_JSC_useWasmMultiMemory=1 " +
"(driver-set; stock bun lacks multi-memory -> 174 failures). JSPI on by " +
"default. Findings-only until a CI track record, then promote.",
deltas: [],
totals: {
commands: 1395,
executed: 1349,
passed: 1254,
failed: 0,
xfail: 95,
pendingRuntime: 41,
pendingCapability: 0,
unsupportedDirective: 5,
},
};

export default bunPinned;
57 changes: 57 additions & 0 deletions harness/shell/expectations/node-pinned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Node.js pinned lane expectation — REQUIRED gate.
//
// PIN: node v26.7.0 (official nodejs.org dist tarball, sha256-verified
// against the release's SHASUMS256.txt — tools/shell/pins.json). Both linux
// arches are published, so unlike jsc-pinned this lane runs on both CI legs.
//
// TOTALS: seeded as EXACT Deno-lane parity from a local measurement
// (2026-08-11, linux-arm64 dev box, V8 14.6.202.34-node.28): 1254 passed /
// 0 failed / 95 xfail, zero node-specific deltas, full capability matrix
// true (JSPI round trip, multi-memory, wasm-GC, EH, memory64, tail-calls,
// relaxed-simd) — with NO runtime flags: wasm JSPI is on by default in
// node >= 26, exactly as docs/architecture.md §3's engine table recorded.
// Like jsc-pinned's seeding, determinism does the heavy lifting: pinned
// bytes + pinned corpus + pinned shim flags; confirm on the first CI run.
//
// WHY 26.x AND NOT 24 LTS (measured, same box, same corpus): node 24.18's
// V8 13.6 gates JSPI behind `--experimental-wasm-jspi`, and even with the
// flag its older-vintage JSPI deviates on 2 commands
// (async/dont-block-start.json:3 and :24, assert_uninstantiable — the
// runtime classifies pending-capability "instantiation-time task context"
// instead of delivering the expected instantiation trap). Both pass on
// node 26 / V8 14.6. Recorded so nobody re-lanes the LTS expecting clean
// parity: a node-24 lane needs flag plumbing AND a 2-delta overlay.
//
// WHAT THIS LANE ADDS over the Deno lane (same V8 family): the node
// EMBEDDING — ESM loading of the bundle, the node event loop under the
// scheduler, and node's pooled-Buffer I/O (tools/shell/host-node.mjs must
// copy out of the pool before bytes reach WebAssembly APIs; handing the
// pool-backed .buffer to wasm is a classic node-embedder defect this lane
// would catch). Raw-engine coverage was already carried by the shell and
// browser lanes; this pin is about the runtime consumers actually deploy.

import type { ShellLaneExpectation } from "./types.ts";

export const nodePinned: ShellLaneExpectation = {
lane: "node-pinned",
required: true,
notes:
"Node.js pinned (v26.7.0, nodejs.org tarball, sha256-verified, both " +
"arches). Exact Deno-lane parity with no flags (JSPI default-on in " +
">= 26): zero deltas, all capabilities true. Required gate. Node 24 LTS " +
"is deliberately not laned — flag-gated JSPI with 2 real deviations " +
"(see this file's header).",
deltas: [],
totals: {
commands: 1395,
executed: 1349,
passed: 1254,
failed: 0,
xfail: 95,
pendingRuntime: 41,
pendingCapability: 0,
unsupportedDirective: 5,
},
};

export default nodePinned;
8 changes: 7 additions & 1 deletion harness/shell/expectations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ export type { LaneDelta, LaneTotals };
export { deltaKey } from "../../browser/expectations/types.ts";

export interface ShellLaneExpectation {
lane: "sm-pinned" | "sm-nightly" | "jsc-pinned" | "jsc-trunk";
lane:
| "sm-pinned"
| "sm-nightly"
| "jsc-pinned"
| "jsc-trunk"
| "node-pinned"
| "bun-pinned";
/** Human summary printed with the table. */
notes: string;
/** `true` = a red lane is a gate failure; `false` = findings-only lane.
Expand Down
11 changes: 9 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,18 @@ shell-lane lane *args: shim corpus
deno run -A tools/shell/run-lane.ts {{lane}} {{args}}

# JSC has no arm64 channel (jsc-built-products is x86_64-only), so its
# lane guards on the arch and skips cleanly elsewhere.
# The per-push pinned shell gates: sm-pinned everywhere; jsc-pinned on x64.
# lane guards on the arch and skips cleanly elsewhere. node/bun publish
# both linux arches, so those lanes run everywhere; bun-pinned is
# findings-only (expectation carries `required: false` — deviations print
# and exit 0, only infrastructure failures gate) until it has a track
# record, then promote.
# The per-push pinned shell gates: sm-pinned + node-pinned (+ bun-pinned,
# findings-only) everywhere; jsc-pinned on x64.
shells:
just shell-lane sm-pinned
@if [ "$(uname -m)" = "x86_64" ]; then just shell-lane jsc-pinned; else echo "jsc-pinned: skipped (no arm64 channel)"; fi
just shell-lane node-pinned
just shell-lane bun-pinned

# The Deno canary probe (V8-trailing-edge d8-lane substitute; findings-only).
deno-canary *args:
Expand Down
6 changes: 6 additions & 0 deletions tools/shell/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export async function bundle(): Promise<void> {
});
const { code } = await cmd.output();
if (code !== 0) throw new Error(`deno bundle failed with code ${code}`);
// Byte-identical .mjs copy for the node/bun lanes (host-node.mjs imports
// it): with no package.json anywhere above tools/shell/dist/, node parses
// a .js file as CommonJS and rejects the bundle's import/export syntax;
// the .mjs extension forces ESM. (The jsshells keep loading entry.js —
// one bundle, two names, so every lane runs the same bytes.)
await Deno.copyFile(out, join(dirname(out), "entry.mjs"));
return;
}

Expand Down
61 changes: 49 additions & 12 deletions tools/shell/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
//
// Deno-lane-shaped, not browser-lane-shaped: this file is bundled
// (`tools/shell/bundle.ts`) and executed directly by a JS shell (SpiderMonkey
// `js` nightly or JSC `jsc` trunk) as a module. There is no HTTP server and
// no page: the corpus is read straight off disk — at absolute paths derived
// from `import.meta.url` (the repo root is three directories above the
// bundle), because the shell's CWD is not trustworthy: JSC trunk bundles
// chdir into their own directory (see `readBinary` below) — and results are
// streamed as `@deltic:`-prefixed JSON lines on stdout via `print()`, which
// both target shells provide. `tools/shell/run-lane.ts` parses these lines
// and classifies with the exact same `harness/src/xfail.ts` + `Summary` +
// per-lane-overlay machinery the browser lanes use (`tools/browser/classify.ts`).
// `js` nightly or JSC `jsc` trunk) as a module — or by a JS *runtime* (node,
// bun) via the `tools/shell/host-node.mjs` preamble, which supplies the two
// host capabilities this entry needs (binary reads, `print`) without putting
// node: builtin imports into this browser-platform bundle. There is no HTTP
// server and no page: the corpus is read straight off disk — at absolute
// paths derived from `import.meta.url` (the repo root is three directories
// above the bundle), because the shell's CWD is not trustworthy: JSC trunk
// bundles chdir into their own directory (see `readBinary` below) — and
// results are streamed as `@deltic:`-prefixed JSON lines on stdout via
// `print()`, which both target shells provide. `tools/shell/run-lane.ts`
// parses these lines and classifies with the exact same
// `harness/src/xfail.ts` + `Summary` + per-lane-overlay machinery the
// browser lanes use (`tools/browser/classify.ts`).
//
// IMPORTANT: the polyfill import below MUST be first. ES module evaluation
// runs side-effect imports in declaration order; `runtime-executor.ts` pulls
Expand All @@ -31,9 +35,16 @@ import { runWastJson } from "../../harness/src/runner.ts";
// deno-lint-ignore no-explicit-any
const g = globalThis as any;

type EngineName = "spidermonkey" | "jsc" | "unknown";
type EngineName = "spidermonkey" | "jsc" | "node" | "bun" | "unknown";

function detectEngine(): EngineName {
// Order matters twice: bun defines process.versions.node too (check bun
// first), and Deno 2's node-compat layer defines globalThis.process as
// well — but this entry never runs under Deno (the Deno lane is
// harness/tests/conformance_test.ts), so process.versions.node here means
// a real node (or bun) driven via tools/shell/host-node.mjs.
if (typeof g.process?.versions?.bun === "string") return "bun";
if (typeof g.process?.versions?.node === "string") return "node";
if (typeof g.os?.file?.readFile === "function") return "spidermonkey";
if (typeof g.readFile === "function") return "jsc";
return "unknown";
Expand All @@ -42,7 +53,9 @@ function detectEngine(): EngineName {
const engine = detectEngine();

// Both target shells provide a global `print()` for stdout; `deno check`
// does not know it (it is not a Deno global), so declare it here.
// does not know it (it is not a Deno global), so declare it here. On the
// node/bun lanes the host preamble (tools/shell/host-node.mjs) installs it
// before importing this bundle.
declare function print(s: string): void;

function readBinary(path: string): Uint8Array {
Expand All @@ -52,13 +65,37 @@ function readBinary(path: string): Uint8Array {
return g.os.file.readFile(abs, "binary");
case "jsc":
return g.readFile(abs, "binary");
case "node":
case "bun":
// Installed by tools/shell/host-node.mjs (which keeps node: builtin
// imports out of this browser-platform bundle). It copies out of
// node's pooled Buffer — see the preamble for why that is load-bearing.
if (typeof g.__delticHostRead !== "function") {
throw new Error(
`readBinary: ${engine} detected but no __delticHostRead — run this ` +
`bundle via tools/shell/host-node.mjs, not directly`,
);
}
return g.__delticHostRead(abs);
default:
throw new Error(
`readBinary: unrecognized shell (no os.file.readFile, no readFile)`,
);
}
}

/** Best-effort engine identity for the header. The jsshells expose a
* `version()` global; node/bun carry theirs on `process.versions`. */
function engineVersionString(): string | null {
if (engine === "node") {
return `node ${g.process.version} (v8 ${g.process.versions.v8})`;
}
if (engine === "bun") {
return `bun ${g.process.versions.bun} (webkit ${g.process.versions.webkit ?? "?"})`;
}
return typeof g.version === "function" ? g.version() : null;
}

// Repo root, derived from this bundle's own location rather than the CWD.
// The CWD is NOT reliable: JSC trunk bundles are executed through their
// shipped wrapper (see the README inside the bundle), which chdir()s into
Expand Down Expand Up @@ -211,7 +248,7 @@ async function main() {
// Best-effort engine identity string; run-lane.ts overrides/augments
// this with fetched build metadata (nightly buildid / jsc revision) it
// already knows from the fetch step.
engineVersion: typeof g.version === "function" ? g.version() : null,
engineVersion: engineVersionString(),
capabilities,
shimBuildHash: sha256Hex(shimBytes),
fileCount: manifest.files.length,
Expand Down
Loading
Loading