diff --git a/plan/agent-context/v8x-js2wasm-deno-handover-2026-08-12.md b/plan/agent-context/v8x-js2wasm-deno-handover-2026-08-12.md index 98d46a70b2..9f47639128 100644 --- a/plan/agent-context/v8x-js2wasm-deno-handover-2026-08-12.md +++ b/plan/agent-context/v8x-js2wasm-deno-handover-2026-08-12.md @@ -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 @@ -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` @@ -26,18 +29,24 @@ 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 @@ -45,8 +54,9 @@ 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"`. @@ -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. @@ -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 @@ -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 @@ -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 @@ -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. diff --git a/plan/issues/2997-try-table-eh-migration.md b/plan/issues/2997-try-table-eh-migration.md index da95b658fc..988a73ffd6 100644 --- a/plan/issues/2997-try-table-eh-migration.md +++ b/plan/issues/2997-try-table-eh-migration.md @@ -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 @@ -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. diff --git a/plan/issues/4376-v8x-js2wasm-deno-core-compatibility-spike.md b/plan/issues/4376-v8x-js2wasm-deno-core-compatibility-spike.md index e12959a9fd..c077e97986 100644 --- a/plan/issues/4376-v8x-js2wasm-deno-core-compatibility-spike.md +++ b/plan/issues/4376-v8x-js2wasm-deno-core-compatibility-spike.md @@ -3,7 +3,7 @@ id: 4376 title: "Spike v8x as a rusty_v8-compatible js2wasm backend for a compiler-free Deno runtime" status: in-review created: 2026-08-12 -updated: 2026-08-12 +updated: 2026-08-20 priority: high feasibility: hard reasoning_effort: max @@ -16,12 +16,67 @@ assignee: ttraenkler/codex-v8x-js2wasm horizon: xl related: [1584, 1662, 1772, 2525, 2658, 2928, 2997, 3571, 4377, 4378, 4380] origin: "Project-lead request to determine whether js2wasm can run behind v8x and preserve Deno APIs without V8, JSC, or QuickJS" +loc-budget-allow: + - src/codegen/array-object-proto.ts + - src/codegen/expressions/call-namespace-static.ts + - src/codegen/statements/loops.ts + - src/codegen/expressions/call-identifier.ts + - src/codegen/expressions/eval-inline.ts + - src/codegen/declarations/object-shape-widening.ts + - src/emit/binary.ts + - src/codegen/expressions/call-tail-dispatch.ts + - src/codegen/index.ts + - src/codegen/expressions/identifiers.ts + - src/codegen/literals.ts + - src/codegen/destructuring-params.ts + - src/codegen/expressions/calls.ts + - src/ir/lower.ts + - src/codegen/expressions.ts + - src/codegen/statements/nested-declarations.ts + - src/codegen/class-bodies.ts + - src/codegen/closures.ts + - src/codegen/property-access-dispatch.ts + - src/ir/integration.ts + - src/codegen/stack-balance.ts + - src/codegen/builtin-value-read.ts +func-budget-allow: + - src/codegen/builtin-value-read.ts::ensureStandaloneBuiltinStaticMethodClosure + - src/codegen/declarations/object-shape-widening.ts::collectGrowableObjectLiterals + - src/codegen/expressions/call-identifier.ts::compileIdentifierCall + - src/codegen/statements/exceptions.ts::compileTryStatement + - src/emit/object.ts::encodeInstrWithReloc + - src/codegen/statements/loops.ts::compileForOfIterator + - src/emit/binary.ts::encodeInstr + - src/codegen/statements/loops.ts::compileForOfDirectIterator + - src/codegen/literals.ts::compileArrayLiteral + - src/codegen/expressions/call-tail-dispatch.ts::compileTailDispatch + - src/codegen/expressions/calls.ts::tryEmitInlineDynamicCall + - src/codegen/statements/nested-declarations.ts::compileNestedFunctionDeclarationInScope + - src/codegen/property-access-dispatch.ts::tryBufferViewAttributeReads + - src/ir/integration.ts::makeResolver + - src/codegen/class-bodies.ts::compileClassBodiesInner + - src/codegen/closures.ts::compileLiftedClosureBody + - src/codegen/function-body.ts::compileFunctionBody + - src/codegen/literals.ts::compileObjectLiteralForStruct + - src/codegen/expressions/call-namespace-static.ts::compileNamespaceStaticCall + - src/codegen/declarations/object-shape-widening.ts::scanStatements#2 +oracle-ratchet-allow: + - src/codegen/builtin-static-globals.ts + - src/codegen/declarations/object-shape-widening.ts +coercion-sites-allow: + - src/codegen/builtin-value-read.ts files: - examples/v8x-js2wasm-spike/README.md - examples/v8x-js2wasm-spike/compile-graph.ts - examples/v8x-js2wasm-spike/deno.ts - examples/v8x-js2wasm-spike/v8x-js2wasm.patch - tests/v8x-js2wasm-spike.test.ts + - tests/fixtures/deno-core-0.407.0/00_primordials.js + - tests/fixtures/deno-core-0.407.0/00_infra.js + - tests/fixtures/deno-core-0.407.0/01_core.js + - tests/helpers/deno-core-bootstrap-probe.ts + - tests/issue-4376-deno-core-bootstrap.test.ts + - tests/issue-4376-deno-primordials-runtime.test.ts - plan/agent-context/v8x-js2wasm-deno-handover-2026-08-12.md --- # #4376 — v8x + js2wasm as an engine-free Deno substrate @@ -134,10 +189,12 @@ callbacks, creates templates and persistent handles, constructs the context, installs the initial `Deno.core` object graph, and reaches execution of `ext:core/00_primordials.js`. -`Script::Run` then refuses explicitly. The new module prototype establishes -the needed execution shape—an embedded, persistent Wasmtime store plus typed -host imports—but has not yet routed the real Deno bootstrap scripts through -it. It proves that shape with a narrow, typed `Deno.cwd()` adapter. +The latest diagnostic-only executable was linked with dynamic lookup and run +against the exact raw wrapper artifact. It eventually terminated with exit 139 +without producing output, so it does not establish a completed unchanged-Deno +bootstrap or a new semantic checkpoint. The separately controlled public +`Script::Run` integration test is the positive proof for the pinned source +sequence and narrow `setUpAsyncStub` bridge. ### Primordials boundary @@ -147,8 +204,7 @@ monkey-patch them. Deno's later wrappers use those private copies for stable internal behavior. Primordials are therefore JavaScript object identities and functions, not Rust ops and not WASI calls. -The pinned, unchanged file now compiles as part of the two-source virtual graph -(400,790-byte diagnostic artifact). The compiler adapter had previously +The compiler adapter had previously omitted side-effect JavaScript imports because it did not set `allowJs`; fixing that exposed and then fixed two honest compiler boundaries: @@ -160,15 +216,22 @@ that exposed and then fixed two honest compiler boundaries: bodies, preventing `primordials` from becoming a null carrier during the first property write. -An instrumented diagnostic source then reaches -`copyPropsRenamed(globalThis["JSON"], primordials, "JSON")`. Standalone's -`globalThis` is intentionally not yet a builtin-object emulator, and JSON/Math/ -Reflect carriers are not reified with inspectable own properties. That next -boundary is the corrected scope of #3571. The full artifact also imports -`env.Promise_new`, `env.Promise_then2`, and the two runtime-eval functions; and -Wasmtime 45 rejects its legacy exception opcodes pending #2997. Thus the real -source now compiles, but the primordials bootstrap is not yet complete or -host-free. +The exact pinned `00_primordials.js`, `00_infra.js`, and `01_core.js` now +compile unchanged as one 2,946,532-byte state-sharing standalone/`deno` +program. Target-gated standardized `try_table` lowering lets Wasmtime 47.0.3 +precompile that raw artifact and boot it in two isolated stores. Both stores +return probe value `42`; the captured bootstrap object, `Deno.core`, internals +object, microtask surface, and representative core functions have the expected +stable identities. Seven deferred imports remain, but strict throwing stubs +prove that none is called during bootstrap. + +The v8x follow-up also recognizes the exact three pinned source hashes and +their order through the public `rusty_v8` `Script::Run` API, retains one +Wasmtime runtime per context, and exposes the first Rust-visible +`Deno.core.setUpAsyncStub` effect. This is still a prelinked three-script +transaction with a narrow explicit bridge; it does not yet provide general +Rust/Wasm object identity or prove an unchanged `deno_core` process completing +bootstrap. ## What “306 ABI symbols” meant @@ -201,6 +264,15 @@ and a second invocation evaluates it while the configured compiler path is the application, real Deno wrappers, and generated op manifest as that one ahead-of-time linked program. +## Compile-time cost + +The compatibility analyses increase the deterministic #3437 harness traversal +count from 111,568 to 131,133 (+17.5%). This exceeds the prior 15% ceiling, so +the dedicated harness budget is intentionally rebanked with the repository's +provided update command. A follow-up should consolidate the added per-file +scans; this PR accepts the measured compile-time cost for the prototype rather +than hiding it behind a looser percentage margin. + ## Spike acceptance - [x] Preserve raw TypeScript source and use its types during js2wasm @@ -222,8 +294,11 @@ ahead-of-time linked program. - [x] Embed Wasmtime, share the Engine/Linker/precompiled Module/InstancePre, and keep one isolated store/instance alive per v8x module runtime. -- [ ] Compile `00_primordials.js`, `00_infra.js`, extension wrappers, and the - application as one state-sharing program. +- [x] Compile the exact pinned `00_primordials.js`, `00_infra.js`, and + `01_core.js` as one state-sharing program and prove value-level bootstrap + state in two isolated instances. +- [ ] Add extension wrappers and the application to that state-sharing + program. - [x] Bind a first Rust op (`Deno.cwd()`) through explicit typed imports. - [ ] Generate the broader Rust op table imports and preserve exceptions, promises, and microtask ordering across the bridge. @@ -234,6 +309,12 @@ ahead-of-time linked program. - [x] Include JavaScript side-effect modules in the virtual graph and compile the pinned unchanged `00_primordials.js` through its first two compiler boundaries (#4378, #4380). +- [x] Emit standardized `try_table` EH so the exact wrapper artifact loads in + v8x's embedded Wasmtime (#2997). +- [x] Route the pinned wrapper source sequence through v8x's public + `Script::Run` path and observe the first Rust-visible callback effect. +- [ ] Prove the same path from an unchanged Rust `deno_core` executable and + replace the narrow callback bridge with shared object/function identity. - [ ] Package the real Deno wrapper/application artifact for distribution. ## Verification @@ -242,6 +323,8 @@ Repository checks: ```sh pnpm exec vitest run \ + tests/issue-4376-deno-primordials-runtime.test.ts \ + tests/issue-4376-deno-core-bootstrap.test.ts \ tests/issue-4378-array-prototype-iterator-bootstrap.test.ts \ tests/issue-4380-empty-object-widening-iife-body.test.ts \ tests/issue-4377-multifile-exported-object-shorthand-callable.test.ts \ @@ -280,16 +363,17 @@ The pinned unchanged-Deno probe also passes `cargo check -p deno_core --example hello_world` after resolving the Wasmtime 45 dependency graph. -The bootstrap regressions pass 5/5. The pinned unchanged Deno primordials file -compiles as a two-module graph; temporary checkpoints (removed afterward) -prove execution advances through the two fixed boundaries to the first JSON -namespace copy. Prior results remain: focused repository and multi-file tests -20/20, simdutf 14/14, v8x source-compile integration 1/1, compiler-free AOT -integration 1/1 with one module load and two isolated instantiations, and -TypeScript project type-checking all pass. The compiler-free dependency graph -contains neither `wasmtime-cranelift` nor `cranelift-codegen`; on Apple arm64 -the stripped test runtime is 1,768,024 bytes and its `.cwasm` fixture is -1,434,192 bytes. The zero-context +The exact Deno bootstrap and focused primordials regressions pass 16/16. The +unchanged pinned three-file wrapper graph produces a 2,946,121-byte artifact, +boots in two isolated stores, returns probe `42` twice, and calls none of its +seven deferred imports. The broader focused audit passes 109/109 relevant +tests; five failures in `issue-1472.test.ts` reproduce unchanged on pristine +`origin/main`. Prior results remain: simdutf 14/14, v8x source-compile +integration 1/1, compiler-free AOT integration 1/1 with one module load and +two isolated instantiations, and TypeScript project type-checking all pass. +The compiler-free dependency graph contains neither `wasmtime-cranelift` nor +`cranelift-codegen`; on Apple arm64 the stripped test runtime is 1,768,024 +bytes and its `.cwasm` fixture is 1,434,192 bytes. The zero-context patch also reverse-applies cleanly with `git apply --unidiff-zero` to the pinned dirty v8x probe checkout. diff --git a/scripts/harness-compile-budget.json b/scripts/harness-compile-budget.json index c4a9891fba..6c92d0dadd 100644 --- a/scripts/harness-compile-budget.json +++ b/scripts/harness-compile-budget.json @@ -1,7 +1,7 @@ { - "forEachChildCalls": 111568, + "forEachChildCalls": 131133, "marginPct": 15, "fixtureCallSites": 120, - "updated": "2026-08-07", + "updated": "2026-08-20", "note": "Deterministic shared-forEachChild traversal count for the #3437 representative harness assembly. Reseed with `npx tsx scripts/check-harness-compile-budget.ts --update` when a slowdown is intentional and justified. See the script header." } diff --git a/src/codegen/array-object-proto.ts b/src/codegen/array-object-proto.ts index 00c941bcca..42ad2550e1 100644 --- a/src/codegen/array-object-proto.ts +++ b/src/codegen/array-object-proto.ts @@ -76,6 +76,7 @@ import { emitStringProtoToStringFlat, } from "./string-proto-tostring.js"; // (#3992) import { standaloneGlobalFunctionSeedInstrs } from "./standalone-global-functions.js"; +import { emitBuiltinNamespaceObject } from "./builtin-static-globals.js"; /** * `Array.prototype`'s own enumerable+non-enumerable method names (ES2024 @@ -2568,6 +2569,64 @@ export function emitGeneratorFunctionPrototypeSingleton(ctx: CodegenContext, fct return { kind: "externref" }; } +/** + * Native standalone `%AsyncGenerator%` (= `%AsyncGeneratorFunction.prototype%`) + * identity used by primordial capture. The outer object owns a stable + * `prototype` value representing `%AsyncGeneratorPrototype%`. + * + * Async-generator frame dispatch already lives in the async iterator runtime; + * exposing `next`/`return`/`throw` as first-class method closures is a separate + * semantic layer. Keeping the nested object distinct (rather than aliasing the + * synchronous generator prototype) preserves the intrinsic identities while + * allowing reflection/bootstrap to proceed without a JavaScript host. + */ +export function emitAsyncGeneratorFunctionPrototypeSingleton( + ctx: CodegenContext, + fctx: FunctionContext, +): ValType | null { + ensureObjectRuntime(ctx); + const newObjectIdx = ctx.funcMap.get("__new_plain_object"); + const setIdx = ctx.funcMap.get("__extern_set"); + if (newObjectIdx === undefined || setIdx === undefined) return null; + + const globalName = "__native_async_generator_function_prototype"; + let globalIdx = ctx.builtinObjectGlobals.get(globalName); + if (globalIdx === undefined) { + globalIdx = ctx.numImportGlobals + ctx.mod.globals.length; + ctx.mod.globals.push({ + name: globalName, + type: { kind: "externref" }, + mutable: true, + init: [{ op: "ref.null.extern" }], + }); + ctx.builtinObjectGlobals.set(globalName, globalIdx); + } + + const outerLocal = allocLocal(fctx, `__async_genfn_proto_${fctx.locals.length}`, { kind: "externref" }); + const innerLocal = allocLocal(fctx, `__async_gen_proto_${fctx.locals.length}`, { kind: "externref" }); + const initBody: Instr[] = [ + { op: "call", funcIdx: newObjectIdx }, + { op: "local.set", index: innerLocal }, + { op: "call", funcIdx: newObjectIdx }, + { op: "local.set", index: outerLocal }, + { op: "local.get", index: outerLocal }, + ]; + addStringConstantGlobal(ctx, "prototype"); + initBody.push(...stringConstantExternrefInstrs(ctx, "prototype")); + initBody.push( + { op: "local.get", index: innerLocal }, + { op: "call", funcIdx: setIdx }, + { op: "local.get", index: outerLocal }, + { op: "global.set", index: globalIdx }, + ); + + fctx.body.push({ op: "global.get", index: globalIdx }); + fctx.body.push({ op: "ref.is_null" }); + fctx.body.push({ op: "if", blockType: { kind: "empty" }, then: initBody, else: [] }); + fctx.body.push({ op: "global.get", index: globalIdx }); + return { kind: "externref" }; +} + /** * (#2996) Native standalone `globalThis` value. In host/gc mode a bare * `globalThis` identifier read leaks the `env::__get_globalThis` host import @@ -2619,11 +2678,42 @@ export function emitNativeGlobalThisObject(ctx: CodegenContext, fctx: FunctionCo ensureLateImport(ctx, "__box_number", [{ kind: "f64" }], [{ kind: "externref" }]); flushLateImportShifts(ctx, fctx); const objLocal = allocLocal(fctx, `__native_globalThis_obj_${fctx.locals.length}`, { kind: "externref" }); + + // Deno's primordials bootstrap deliberately discovers namespace objects via + // a computed realm-global read (`globalThis[name]`) before copying their own + // descriptors. The namespace carrier and the realm property must therefore + // be the same object; an empty or second carrier loses function identity. + // Build these demand-driven seeds through the canonical namespace emitter. + // Keep the detached body live while later seed construction can still add + // imports and shift defined-function indices. + const savedBody = fctx.body; + fctx.body = []; + ctx.liveBodies.add(savedBody); + for (const name of ["Array", "Object", "JSON", "Math", "Proxy", "Reflect"] as const) { + fctx.body.push({ op: "local.get", index: objLocal }); + addStringConstantGlobal(ctx, name); + fctx.body.push(...stringConstantExternrefInstrs(ctx, name)); + if (emitBuiltinNamespaceObject(ctx, fctx, name) === null) { + fctx.body.push({ op: "ref.null.extern" }); + } + const defineIdx = ctx.funcMap.get("__defineProperty_value"); + if (defineIdx === undefined) { + fctx.body.push({ op: "drop" }, { op: "drop" }, { op: "drop" }); + continue; + } + // Global builtin bindings: writable, non-enumerable, configurable. + fctx.body.push({ op: "f64.const", value: 0x05 }, { op: "call", funcIdx: defineIdx }, { op: "drop" }); + } + const namespaceSeeds = fctx.body; + fctx.body = savedBody; + ctx.liveBodies.delete(savedBody); + ctx.liveBodies.add(namespaceSeeds); const functionSeeds = standaloneGlobalFunctionSeedInstrs(ctx, objLocal); const newObjectIdx = ctx.funcMap.get("__new_plain_object"); const defineValueIdx = ctx.funcMap.get("__defineProperty_value"); const boxNumberIdx = ctx.funcMap.get("__box_number"); if (!functionSeeds || newObjectIdx === undefined || defineValueIdx === undefined || boxNumberIdx === undefined) { + ctx.liveBodies.delete(namespaceSeeds); return null; } @@ -2648,15 +2738,18 @@ export function emitNativeGlobalThisObject(ctx: CodegenContext, fctx: FunctionCo { op: "call", funcIdx: boxNumberIdx }, ]); seedValue("undefined", undefinedExternInstrs(ctx) ?? [{ op: "ref.null.extern" }]); + seedValue("globalThis", [{ op: "local.get", index: objLocal }]); const initBody: Instr[] = [ { op: "call", funcIdx: newObjectIdx }, { op: "local.set", index: objLocal }, ...functionSeeds, ...valueSeeds, + ...namespaceSeeds, { op: "local.get", index: objLocal }, { op: "global.set", index: globalIdx }, ]; + ctx.liveBodies.delete(namespaceSeeds); fctx.body.push({ op: "global.get", index: globalIdx }); fctx.body.push({ op: "ref.is_null" }); fctx.body.push({ op: "if", blockType: { kind: "empty" }, then: initBody, else: [] }); @@ -2692,12 +2785,26 @@ export function emitNativeGlobalThisObject(ctx: CodegenContext, fctx: FunctionCo * returns the externref ValType, or `null` if the `$Object` runtime is * unavailable (caller falls through to the host-import path). */ -export function emitArrayIteratorPrototypeSingleton(ctx: CodegenContext, fctx: FunctionContext): ValType | null { +export type NativeIteratorPrototypeKind = "Array" | "Map" | "Set" | "String"; + +/** + * Materialize one identity-stable intrinsic iterator prototype object. + * + * The iterator record carriers do not model [[Prototype]] yet. Keeping one + * singleton per iterator family nevertheless gives reflective bootstrap code + * a genuine object identity (rather than null) and prevents Map/Set/String + * iterator prototypes from collapsing onto %ArrayIteratorPrototype%. + */ +export function emitIteratorPrototypeSingleton( + ctx: CodegenContext, + fctx: FunctionContext, + kind: NativeIteratorPrototypeKind, +): ValType | null { ensureObjectRuntime(ctx); const newObjectIdx = ctx.funcMap.get("__new_plain_object"); if (newObjectIdx === undefined) return null; - const globalName = "__native_array_iterator_prototype"; + const globalName = `__native_${kind.toLowerCase()}_iterator_prototype`; let globalIdx = ctx.builtinObjectGlobals.get(globalName); if (globalIdx === undefined) { globalIdx = ctx.numImportGlobals + ctx.mod.globals.length; @@ -2720,3 +2827,7 @@ export function emitArrayIteratorPrototypeSingleton(ctx: CodegenContext, fctx: F fctx.body.push({ op: "global.get", index: globalIdx }); return { kind: "externref" }; } + +export function emitArrayIteratorPrototypeSingleton(ctx: CodegenContext, fctx: FunctionContext): ValType | null { + return emitIteratorPrototypeSingleton(ctx, fctx, "Array"); +} diff --git a/src/codegen/async-frame.ts b/src/codegen/async-frame.ts index f64c22cac9..c89b6ae504 100644 --- a/src/codegen/async-frame.ts +++ b/src/codegen/async-frame.ts @@ -98,6 +98,7 @@ import { ensureExnTag } from "./registry/imports.js"; import { addFuncType, getOrRegisterRefCellType, getOrRegisterVecType } from "./registry/types.js"; import { coerceType, compileExpression, compileStatement, ensureLateImport, flushLateImportShifts } from "./shared.js"; import { resolveSpillLocalValType } from "./statements/variables.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; /** * Is the host-free async **drive layer** (#2895 PATH B) active for this module? @@ -2460,15 +2461,7 @@ export function ensureAsyncResumeFunction( { op: "loop", blockType: { kind: "empty" }, - body: [ - { - op: "try", - blockType: { kind: "empty" }, - body: chain, - catches: [{ tagIdx: exnTag, body: route }], - ...(catchAllRoute !== undefined ? { catchAll: catchAllRoute } : {}), - }, - ], + body: [buildTargetTaggedTry(ctx, { kind: "empty" }, chain, [{ tagIdx: exnTag, body: route }], catchAllRoute)], }, ], }); @@ -2480,17 +2473,14 @@ export function ensureAsyncResumeFunction( body: [{ op: "loop", blockType: { kind: "empty" }, body: chain }], }, ]; - resumeFctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: dispatch, - catches: [ + resumeFctx.body.push( + buildTargetTaggedTry(ctx, { kind: "empty" }, dispatch, [ { tagIdx: exnTag, body: [{ op: "local.set", index: reasonLocal }, ...rejectTail], }, - ], - }); + ]), + ); } resumePlaceholder.locals = resumeFctx.locals; diff --git a/src/codegen/async-scheduler.ts b/src/codegen/async-scheduler.ts index e8b9306be5..6afe3d8964 100644 --- a/src/codegen/async-scheduler.ts +++ b/src/codegen/async-scheduler.ts @@ -49,6 +49,7 @@ import { getFuncRefWrapperRootTypeIdx } from "./closures/funcref-wrapper-types.j // module; the inline hooks in this file (settle-body note, Promise.reject mint) // call these two. `ensureUnhandledRejectionReporter` is imported by index.ts. import { ensureUnhandledRejectionTracking, buildNoteUnhandledRejection } from "./unhandled-rejection.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; /** * #1326 — Sentinel state values for `$Promise.state`. Match the JS spec @@ -1188,27 +1189,22 @@ function ensurePromiseThenableSubstrate( { op: "ref.cast", typeIdx: capsTypeIdx }, { op: "struct.get", typeIdx: capsTypeIdx, fieldIdx: 1 }, { op: "local.set", index: promiseLocal }, - { - op: "try", - blockType: { kind: "empty" }, - body: jobTryBody, - catches: [ - { - tagIdx: exnTag, - body: [ - // A throw from Get/then-call before settle rejects the promise - // (§27.2.2.2 step 2 / §27.2.1.3.2 step 15). Post-settle throws - // are no-ops via the one-shot settle guard. - { op: "local.set", index: reasonLocal }, - { op: "local.get", index: promiseLocal }, - { op: "ref.as_non_null" }, - { op: "local.get", index: reasonLocal }, - { op: "call", funcIdx: state.promiseRejectFuncIdx }, - { op: "drop" }, - ], - }, - ], - }, + buildTargetTaggedTry(ctx, { kind: "empty" }, jobTryBody, [ + { + tagIdx: exnTag, + body: [ + // A throw from Get/then-call before settle rejects the promise + // (§27.2.2.2 step 2 / §27.2.1.3.2 step 15). Post-settle throws + // are no-ops via the one-shot settle guard. + { op: "local.set", index: reasonLocal }, + { op: "local.get", index: promiseLocal }, + { op: "ref.as_non_null" }, + { op: "local.get", index: reasonLocal }, + { op: "call", funcIdx: state.promiseRejectFuncIdx }, + { op: "drop" }, + ], + }, + ]), { op: "ref.null.extern" }, ], exported: false, @@ -1421,15 +1417,15 @@ function buildPromiseResolveValueBody( // hasThen = __promise_has_callable_then(value) — the Get("then") runs // accessors, so a poisoned getter THROWS here (§27.2.1.3.2 step 9): // catch → reject(promise, thrown). - { - op: "try", - blockType: { kind: "empty" }, - body: [ + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [ { op: "local.get", index: valueLocal }, { op: "call", funcIdx: thenable.hasCallableThenFuncIdx }, { op: "local.set", index: hasThenLocal }, ], - catches: [ + [ { tagIdx: exnTagIdx, body: [ @@ -1439,7 +1435,7 @@ function buildPromiseResolveValueBody( ], }, ], - }, + ), { op: "local.get", index: poisonedLocal }, { op: "if", @@ -1807,11 +1803,8 @@ function emitThenWrapperFunction( { op: "drop" }, // settle returns the value; the drain ignores the wrapper result ); - body.push({ - op: "try", - blockType: { kind: "empty" }, - body: tryBody, - catches: [ + body.push( + buildTargetTaggedTry(ctx, { kind: "empty" }, tryBody, [ { tagIdx: exnTag, body: [ @@ -1823,8 +1816,8 @@ function emitThenWrapperFunction( { op: "drop" }, ], }, - ], - }); + ]), + ); // Wrapper result (externref) — dropped by the drain; always null now. body.push({ op: "ref.null.extern" }); @@ -1907,10 +1900,10 @@ function ensureDynamicThenWrapper(ctx: CodegenContext, kind: "fulfill" | "reject ]; const applyArm: Instr[] = [ - { - op: "try", - blockType: { kind: "empty" }, - body: [ + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [ { op: "call", funcIdx: objVecNewIdx }, { op: "local.set", index: vecLocal }, { op: "local.get", index: vecLocal }, @@ -1928,7 +1921,7 @@ function ensureDynamicThenWrapper(ctx: CodegenContext, kind: "fulfill" | "reject { op: "call", funcIdx: state.promiseResolveValueFuncIdx }, { op: "drop" }, ], - catches: [ + [ { tagIdx: exnTag, body: [ @@ -1942,7 +1935,7 @@ function ensureDynamicThenWrapper(ctx: CodegenContext, kind: "fulfill" | "reject ], }, ], - }, + ), ]; // IsCallable ≈ "is a closure-wrapper struct" — every compiled function value @@ -4501,11 +4494,8 @@ function emitFinallyWrapperFunction(ctx: CodegenContext, info: ClosureInfo, isRe { op: "call", funcIdx: state.finallyAfterFuncIdx }, ); - body.push({ - op: "try", - blockType: { kind: "empty" }, - body: tryBody, - catches: [ + body.push( + buildTargetTaggedTry(ctx, { kind: "empty" }, tryBody, [ { tagIdx: exnTag, body: [ @@ -4517,8 +4507,8 @@ function emitFinallyWrapperFunction(ctx: CodegenContext, info: ClosureInfo, isRe { op: "drop" }, ], }, - ], - }); + ]), + ); body.push({ op: "ref.null.extern" }); pushDefinedFunc(ctx, funcIdx, { diff --git a/src/codegen/builtin-fn-meta.ts b/src/codegen/builtin-fn-meta.ts index e36e4cd5d3..a1b9b2b042 100644 --- a/src/codegen/builtin-fn-meta.ts +++ b/src/codegen/builtin-fn-meta.ts @@ -60,6 +60,7 @@ export const BFN_ID_FIELD_IDX = 4; */ export const STANDALONE_STATIC_METHOD_META: Record = { "Array.isArray": { name: "isArray", length: 1 }, + "Object.assign": { name: "assign", length: 2 }, "Object.keys": { name: "keys", length: 1 }, "Object.getOwnPropertyDescriptor": { name: "getOwnPropertyDescriptor", length: 2 }, // (#2933) Fixed-arity Reflect.* namespace static-method value reads. Spec @@ -68,6 +69,8 @@ export const STANDALONE_STATIC_METHOD_META: Record = new Map([ // bare-identifier value read, which previously leaked `env.global_`. ["Math", []], ["JSON", []], + ["Proxy", []], ["Reflect", []], // #2907 — Error-family constructors as bare-value carriers. `expectedError = // TypeError`, `[TypeError, RangeError]`, `Object.isFrozen(TypeError)`. A `new @@ -228,6 +229,45 @@ export function resolveBuiltinNamespaceValueName(ctx: CodegenContext, expr: ts.E return undefined; } +/** + * Resolve a const object-binding alias of a builtin static method. + * + * Deno's primordials bootstrap snapshots intrinsics with declarations such as + * `const { ownKeys: ReflectOwnKeys } = Reflect`. The binding is stored through + * the ordinary externref object-destructuring path, while TypeScript describes + * its return using the lib declaration's structural array type. Call lowering + * needs the canonical builtin closure signature instead: for example the + * native Reflect.ownKeys closure returns the object runtime's externref key + * vector. Keeping that representation prevents generic callable dispatch from + * replacing the live result with a mismatched typed-array default. + */ +export function resolveBuiltinStaticBindingAlias( + ctx: CodegenContext, + expr: ts.Expression, +): { builtinName: string; propName: string } | undefined { + const unwrapped = unwrapExpression(expr); + if (!ts.isIdentifier(unwrapped)) return undefined; + const symbol = ctx.checker.getSymbolAtLocation(unwrapped); + const declaration = symbol?.valueDeclaration; + if (!declaration || !ts.isBindingElement(declaration) || declaration.dotDotDotToken) return undefined; + const pattern = declaration.parent; + if (!ts.isObjectBindingPattern(pattern)) return undefined; + const variable = pattern.parent; + if (!ts.isVariableDeclaration(variable) || variable.name !== pattern || !variable.initializer) return undefined; + const list = variable.parent; + if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return undefined; + + const builtinName = resolveBuiltinNamespaceValueName(ctx, variable.initializer); + if (!builtinName) return undefined; + const property = declaration.propertyName ?? declaration.name; + const propName = + ts.isIdentifier(property) || ts.isStringLiteral(property) || ts.isNumericLiteral(property) + ? property.text + : undefined; + if (!propName || BUILTIN_STATIC_METHOD_ARITY[builtinName]?.[propName] === undefined) return undefined; + return { builtinName, propName }; +} + function hiddenName(builtinName: string, propName: string): string { return `__builtin_static_${builtinName}_${propName}`; } diff --git a/src/codegen/builtin-value-read.ts b/src/codegen/builtin-value-read.ts index 4035142777..06116edeb2 100644 --- a/src/codegen/builtin-value-read.ts +++ b/src/codegen/builtin-value-read.ts @@ -74,6 +74,7 @@ import { ensureExtrasArgvGlobal } from "./statements/nested-declarations.js"; import { getArrTypeIdxFromVec } from "./registry/types.js"; import { ensureAnyFromExternHelper, ensureAnyHelpers, ensureExternStrictEqHelper } from "./any-helpers.js"; import { sameValueNumberOps } from "./same-value-number-ops.js"; +import { ensureObjectRuntime, ensureObjVecBuilders } from "./object-runtime.js"; export const BUILTIN_CTOR_NAMES = new Set([ "Object", @@ -882,6 +883,17 @@ export function ensureStandaloneBuiltinStaticMethodClosure( paramTypes = [{ kind: "externref" }]; returnType = BOOLEAN_PREDICATE_RESULT; break; + case "Object.assign": + // Deno snapshots Object.assign into its primordials object, then invokes + // that captured function while constructing `Deno.core`. Register the + // native object runtime before minting the closure so every funcidx used + // by the body is settled. The closure exposes the builtin's declared + // two-argument shape; its second argument is packed into the same + // `$ObjVec` consumed by the direct Object.assign lowering. + ensureObjVecBuilders(ctx); + paramTypes = [{ kind: "externref" }, { kind: "externref" }]; + returnType = { kind: "externref" }; + break; case "Object.keys": paramTypes = [{ kind: "externref" }]; // Standalone Object.keys returns the object-runtime `$ObjVec` as an @@ -901,6 +913,23 @@ export function ensureStandaloneBuiltinStaticMethodClosure( paramTypes = [{ kind: "externref" }, { kind: "externref" }]; returnType = BOOLEAN_PREDICATE_RESULT; break; + case "Object.defineProperty": + ensureObjectRuntime(ctx); + paramTypes = [{ kind: "externref" }, { kind: "externref" }, { kind: "externref" }]; + returnType = { kind: "externref" }; + break; + case "Object.setPrototypeOf": + ensureObjectRuntime(ctx); + paramTypes = [{ kind: "externref" }, { kind: "externref" }]; + returnType = { kind: "externref" }; + break; + case "Object.freeze": + case "Object.seal": + case "Object.preventExtensions": + ensureObjectRuntime(ctx); + paramTypes = [{ kind: "externref" }]; + returnType = { kind: "externref" }; + break; // (#2933) Namespace static-method VALUE reads for the fixed-arity `Reflect.*` // methods that the standalone CALL path already backs with a simple // externref/i32 native (calls.ts §"Reflect API"). The value closure calls @@ -927,6 +956,14 @@ export function ensureStandaloneBuiltinStaticMethodClosure( paramTypes = [{ kind: "externref" }]; returnType = { kind: "externref" }; break; + case "Reflect.getOwnPropertyDescriptor": + paramTypes = [{ kind: "externref" }, { kind: "externref" }]; + returnType = { kind: "externref" }; + break; + case "Reflect.defineProperty": + paramTypes = [{ kind: "externref" }, { kind: "externref" }, { kind: "externref" }]; + returnType = BOOLEAN_PREDICATE_RESULT; + break; // (#2933) JSON.stringify as a VALUE — fixed 1-arg compact form. Serialises // host-free via the native `__json_stringify_root` (the SAME entry the // direct `JSON.stringify(o)` call uses); returns the JSON `$AnyString` @@ -1071,6 +1108,21 @@ export function ensureStandaloneBuiltinStaticMethodClosure( if (key === "Array.isArray") { closureFctx.body.push({ op: "local.get", index: 1 }); emitArrayIsArrayExternrefPredicate(ctx, closureFctx); + } else if (key === "Object.assign") { + const { newIdx, pushIdx } = ensureObjVecBuilders(ctx); + const assignIdx = ctx.funcMap.get("__object_assign"); + if (assignIdx === undefined) return null; + const sourcesLocal = allocLocal(closureFctx, "assign_sources", { kind: "externref" }); + closureFctx.body.push( + { op: "call", funcIdx: newIdx }, + { op: "local.set", index: sourcesLocal }, + { op: "local.get", index: sourcesLocal }, + { op: "local.get", index: 2 }, + { op: "call", funcIdx: pushIdx }, + { op: "local.get", index: 1 }, + { op: "local.get", index: sourcesLocal }, + { op: "call", funcIdx: assignIdx }, + ); } else if (key === "Object.keys") { const keysIdx = ensureLateImport(ctx, "__object_keys", [{ kind: "externref" }], [{ kind: "externref" }]); if (keysIdx === undefined) return null; @@ -1106,6 +1158,33 @@ export function ensureStandaloneBuiltinStaticMethodClosure( closureFctx.body.push({ op: "local.get", index: 1 }); closureFctx.body.push({ op: "local.get", index: 2 }); closureFctx.body.push({ op: "call", funcIdx: hasOwnIdx }); + } else if (key === "Object.defineProperty") { + const defineIdx = ctx.funcMap.get("__obj_define_from_desc"); + if (defineIdx === undefined) return null; + closureFctx.body.push( + { op: "local.get", index: 1 }, + { op: "local.get", index: 2 }, + { op: "local.get", index: 3 }, + { op: "call", funcIdx: defineIdx }, + ); + } else if (key === "Object.setPrototypeOf") { + const setPrototypeIdx = ctx.funcMap.get("__object_setPrototypeOf"); + if (setPrototypeIdx === undefined) return null; + closureFctx.body.push( + { op: "local.get", index: 1 }, + { op: "local.get", index: 2 }, + { op: "call", funcIdx: setPrototypeIdx }, + ); + } else if (key === "Object.freeze" || key === "Object.seal" || key === "Object.preventExtensions") { + const helperName = + key === "Object.freeze" + ? "__object_freeze" + : key === "Object.seal" + ? "__object_seal" + : "__object_preventExtensions"; + const integrityIdx = ctx.funcMap.get(helperName); + if (integrityIdx === undefined) return null; + closureFctx.body.push({ op: "local.get", index: 1 }, { op: "call", funcIdx: integrityIdx }); } else if (key === "Reflect.get") { // (#2933) Same native the 2-arg standalone `Reflect.get(target, key)` call // path uses (calls.ts). The value closure is fixed 2-arg — the optional @@ -1144,13 +1223,74 @@ export function ensureStandaloneBuiltinStaticMethodClosure( closureFctx.body.push({ op: "local.get", index: 3 }); closureFctx.body.push({ op: "call", funcIdx: idx }); } else if (key === "Reflect.ownKeys") { - // Native __object_keys — string own keys of the $Object hash-map, per the - // standalone `Reflect.ownKeys(target)` call path (Symbol/non-enumerable - // keys are out of scope for the open-object runtime, consistent with it). - const idx = ensureLateImport(ctx, "__object_keys", [{ kind: "externref" }], [{ kind: "externref" }]); + // Reflect.ownKeys includes non-enumerable own properties. The native + // runtime does not yet retain symbol-keyed properties, so its strongest + // available approximation is __getOwnPropertyNames, not __object_keys + // (which deliberately implements Object.keys' enumerable-only view). + // This distinction is observable for builtin namespace carriers: Deno's + // primordials bootstrap discovers JSON.parse/stringify through this + // extracted function value. + const idx = ensureLateImport(ctx, "__getOwnPropertyNames", [{ kind: "externref" }], [{ kind: "externref" }]); if (idx === undefined) return null; closureFctx.body.push({ op: "local.get", index: 1 }); closureFctx.body.push({ op: "call", funcIdx: idx }); + } else if (key === "Reflect.getOwnPropertyDescriptor") { + // The first-class Reflect method must share the direct call path's + // native descriptor provider. Deno snapshots this method through object + // destructuring before using it to copy every primordial descriptor. + const runtime = ensureObjectRuntime(ctx); + const beforeThrow = closureFctx.body.length; + emitThrowTypeError(ctx, closureFctx, "Reflect.getOwnPropertyDescriptor called on non-object"); + const throwInstrs = closureFctx.body.splice(beforeThrow); + closureFctx.body.push( + { op: "local.get", index: 1 }, + { op: "any.convert_extern" }, + { op: "ref.test", typeIdx: runtime.objectTypeIdx }, + { op: "local.get", index: 1 }, + { op: "any.convert_extern" }, + { op: "ref.test", typeIdx: runtime.proxyTypeIdx }, + { op: "i32.or" }, + { op: "i32.eqz" }, + { op: "if", blockType: { kind: "empty" }, then: throwInstrs }, + ); + const idx = ensureLateImport( + ctx, + "__getOwnPropertyDescriptor", + [{ kind: "externref" }, { kind: "externref" }], + [{ kind: "externref" }], + ); + if (idx === undefined) return null; + closureFctx.body.push({ op: "local.get", index: 1 }, { op: "local.get", index: 2 }, { op: "call", funcIdx: idx }); + } else if (key === "Reflect.defineProperty") { + // Deno snapshots Reflect.defineProperty and invokes it with descriptor + // objects returned by Reflect.getOwnPropertyDescriptor. Route that + // first-class call through the same native dynamic-descriptor applier as + // the direct syntax and surface its boolean [[DefineOwnProperty]] result. + const runtime = ensureObjectRuntime(ctx); + const beforeThrow = closureFctx.body.length; + emitThrowTypeError(ctx, closureFctx, "Reflect.defineProperty called on non-object"); + const throwInstrs = closureFctx.body.splice(beforeThrow); + closureFctx.body.push( + { op: "local.get", index: 1 }, + { op: "any.convert_extern" }, + { op: "ref.test", typeIdx: runtime.objectTypeIdx }, + { op: "local.get", index: 1 }, + { op: "any.convert_extern" }, + { op: "ref.test", typeIdx: runtime.proxyTypeIdx }, + { op: "i32.or" }, + { op: "i32.eqz" }, + { op: "if", blockType: { kind: "empty" }, then: throwInstrs }, + ); + const defineIdx = ctx.funcMap.get("__obj_define_from_desc"); + const truthyIdx = ctx.funcMap.get("__is_truthy"); + if (defineIdx === undefined || truthyIdx === undefined) return null; + closureFctx.body.push( + { op: "local.get", index: 1 }, + { op: "local.get", index: 2 }, + { op: "local.get", index: 3 }, + { op: "call", funcIdx: defineIdx }, + { op: "call", funcIdx: truthyIdx }, + ); } else if (key === "JSON.stringify") { // Ensure the native codec (`__json_stringify_value` + its 1-arg entry // `__json_stringify_root`, `anyref -> ref $AnyString`) is registered; the diff --git a/src/codegen/call-arg-producers.ts b/src/codegen/call-arg-producers.ts index b2f9429b14..99905fa371 100644 --- a/src/codegen/call-arg-producers.ts +++ b/src/codegen/call-arg-producers.ts @@ -152,7 +152,7 @@ export function instrPopsPushes(instr: Instr, mod: WasmModule): { pops: number; // condition. The nested bodies are separate instruction lists and are walked // on their own, so from this list's point of view the block is one opaque // instruction with a known signature. - if (op === "block" || op === "loop" || op === "try" || op === "if") { + if (op === "block" || op === "loop" || op === "try" || op === "try_table" || op === "if") { const condPop = op === "if" ? 1 : 0; const bt = (instr as { blockType?: { kind: string; typeIdx?: number } }).blockType; if (!bt || bt.kind === "empty") return { pops: condPop, pushes: 0 }; diff --git a/src/codegen/call-dispatch-ic.ts b/src/codegen/call-dispatch-ic.ts index af98d35af9..dfb94b9923 100644 --- a/src/codegen/call-dispatch-ic.ts +++ b/src/codegen/call-dispatch-ic.ts @@ -144,6 +144,7 @@ function scanRegion(instrs: Instr[], outDepth: number, used: Set): strin case "return_call_ref": return "tail-call"; case "try": + case "try_table": // A try frame's catch-label bookkeeping is not modelled by the plain // depth recreation below; the fills never emit one in a dispatcher. return "try-frame"; diff --git a/src/codegen/class-bodies.ts b/src/codegen/class-bodies.ts index c0670f22e2..70458594b2 100644 --- a/src/codegen/class-bodies.ts +++ b/src/codegen/class-bodies.ts @@ -59,6 +59,7 @@ import { detectStringBuilders } from "./string-builder.js"; // (#2641/#1210) str import type { StringBuilderPresizeInfo } from "./string-builder.js"; import { emitUndefined } from "./expressions/late-imports.js"; import { addStringConstantGlobal, ensureExnTag, nextModuleGlobalIdx } from "./registry/imports.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; import { emitWasiErrorConstructor, getOrRegisterErrorStructType, isWasiErrorName } from "./registry/error-types.js"; import { emitStandaloneArrayConstructor, // (#2917) native `class Sub extends Array` @@ -2748,13 +2749,15 @@ function compileClassBodiesInner( { op: "local.set", index: pendingThrowLocal }, ] : []; - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], - catches: [{ tagIdx, body: catchBody }], - catchAll: catchAllBody.length > 0 ? catchAllBody : undefined, - }); + fctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], + [{ tagIdx, body: catchBody }], + catchAllBody.length > 0 ? catchAllBody : undefined, + ), + ); // Return __create_generator or __create_async_generator depending on async flag const createGenName = isAsyncMethod ? "__create_async_generator" : "__create_generator"; diff --git a/src/codegen/closures.ts b/src/codegen/closures.ts index 74cab8c4ff..46a1d44a10 100644 --- a/src/codegen/closures.ts +++ b/src/codegen/closures.ts @@ -50,6 +50,7 @@ import { resolveWasmTypeForClosureReturn, } from "./index.js"; import { refCellValueType } from "./registry/types.js"; // (#3328) boxed-capture valType fallback +import { buildTargetTaggedTry } from "../ir/try-table.js"; import { coerceType, compileExpression, @@ -2715,13 +2716,15 @@ export function compileLiftedClosureBody( { op: "local.set", index: pendingThrowLocal }, ] : []; - liftedFctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], - catches: [{ tagIdx, body: catchBody }], - catchAll: catchAllBody, - }); + liftedFctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], + [{ tagIdx, body: catchBody }], + catchAllBody, + ), + ); // Return __create_generator or __create_async_generator depending on async flag const createGenName = isAsync ? "__create_async_generator" : "__create_generator"; diff --git a/src/codegen/closures/capture-source-slot.ts b/src/codegen/closures/capture-source-slot.ts index a761d1d836..1ae9a887bf 100644 --- a/src/codegen/closures/capture-source-slot.ts +++ b/src/codegen/closures/capture-source-slot.ts @@ -71,6 +71,21 @@ export function captureSourceSlot(fctx: FunctionContext, cap: { name: string; ou if (fctx.asyncDriveReturn && inFrameIdx !== undefined) return inFrameIdx; if (fctx.liftedCaptureNames?.has(cap.name)) return inFrameIdx ?? cap.outerLocalIdx; + // A let/const pre-hoist can record the capture before block-shadow setup + // replaces that binding with its source-position local. Nested-function body + // compilation may allocate many temporaries in between, leaving the recorded + // index in range but now owned by an unrelated local. This is stronger + // evidence than the broad localMap preference reverted in #1177: if the slot + // no longer even names the captured binding, it cannot be the right source. + // Prefer the current lexical binding in that provably-stale case. Deno's + // `registerErrorClass` hits this with `errorConstructors` (recorded 485, + // live 561); reading 485 produced an unrelated externref and an illegal cast. + const recordedDef = + cap.outerLocalIdx < fctx.params.length + ? fctx.params[cap.outerLocalIdx] + : fctx.locals[cap.outerLocalIdx - fctx.params.length]; + if (inFrameIdx !== undefined && recordedDef?.name !== cap.name) return inFrameIdx; + const existsHere = cap.outerLocalIdx < fctx.params.length + fctx.locals.length; if (!existsHere && inFrameIdx !== undefined) return inFrameIdx; diff --git a/src/codegen/closures/funcref-as-closure.ts b/src/codegen/closures/funcref-as-closure.ts index 1f5f258ecb..a77cf9a602 100644 --- a/src/codegen/closures/funcref-as-closure.ts +++ b/src/codegen/closures/funcref-as-closure.ts @@ -333,6 +333,20 @@ export function emitFuncRefAsClosure( // mints an interned string global, and a path that ends up not carrying the // slot should not leave one behind. const metaDecl = ctx.funcMapOwnerDecl.get(funcName) ?? ctx.topLevelFunctionDeclarations.get(funcName); + // Constructibility belongs to the source function, not to whichever value + // read happened to materialize its cached capture struct first. A self-read + // can mint an ordinary function's artifact with `__constructible`, followed + // by another lowering path that historically passed the default `false`. + // Since artifacts are keyed by function identity, that disagreement omitted + // one operand from the later `struct.new`. Normalize ordinary declarations + // here so every materialization uses one stable layout. + constructible = + constructible || + ((noJsHost(ctx) || ctx.targetProfile.semanticProviders === "native-first") && + metaDecl !== undefined && + ts.isFunctionDeclaration(metaDecl) && + metaDecl.asteriskToken === undefined && + !(metaDecl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ?? false)); // (#4440) A STATIC class method reaches this helper (not the cached-singleton // one — `property-access-dispatch.ts` reads `C.m` through `emitFuncRefAsClosure`), // and its `funcName` is the physical `ClassName_m`, which neither map above diff --git a/src/codegen/context/types.ts b/src/codegen/context/types.ts index 668d61ef20..7252d930f7 100644 --- a/src/codegen/context/types.ts +++ b/src/codegen/context/types.ts @@ -1069,7 +1069,7 @@ export interface FunctionContext { * Stack of catch rethrow info. Each entry tracks a catch variable name and the * current depth (number of block-like structures) from the catch boundary. */ - catchRethrowStack?: { varName: string; depth: number }[]; + catchRethrowStack?: { varName: string; depth: number; exnLocalIdx?: number }[]; /** * Stack of pending finally blocks. When a return/break/continue exits a try * block that has a finally clause, the finally instructions must be inlined diff --git a/src/codegen/dead-elimination.ts b/src/codegen/dead-elimination.ts index 2ca46fd893..2f7e036ad1 100644 --- a/src/codegen/dead-elimination.ts +++ b/src/codegen/dead-elimination.ts @@ -76,6 +76,10 @@ function collectRefsFromBody(body: Instr[], usedFuncs: Set, usedTypes: S for (const c of instr.catches) collectRefsFromBody(c.body, usedFuncs, usedTypes); if (instr.catchAll) collectRefsFromBody(instr.catchAll, usedFuncs, usedTypes); break; + case "try_table": + collectBlockTypeRefs(instr.blockType, usedTypes); + collectRefsFromBody(instr.body, usedFuncs, usedTypes); + break; default: { // Catch-all for instructions whose op carries type/func indices we may // not have enumerated above (defensive: keeps DCE conservative). diff --git a/src/codegen/declarations/object-shape-widening.ts b/src/codegen/declarations/object-shape-widening.ts index 321f051dfc..47d73d8267 100644 --- a/src/codegen/declarations/object-shape-widening.ts +++ b/src/codegen/declarations/object-shape-widening.ts @@ -768,6 +768,36 @@ export function collectGrowableObjectLiterals( const shape = literalShapeNames(decl.initializer); if (!shape) continue; // not a pure data literal → skip (externref builder would decline) + // A value nested in an object literal passed to an `any`/`unknown` + // callable crosses a fully dynamic JavaScript boundary. Keeping the + // nested value as a closed struct makes its fields invisible after + // the outer literal is stored in an open object. Deno's bootstrap + // uses this exact shape when it publishes `infra` through its + // any-typed captured Object.assign primordial: + // + // ObjectAssign(globalThis, { __infra: infra }) + // + // Pin the declaration itself to the open-object representation so + // the published value keeps both identity and reflective fields. + const isNestedDynamicCallArgument = (id: ts.Identifier): boolean => { + if (!(ctx.standalone || ctx.wasi) || id.text !== varName) return false; + const property = id.parent; + if ( + !( + (ts.isShorthandPropertyAssignment(property) && property.name === id) || + (ts.isPropertyAssignment(property) && property.initializer === id) + ) + ) { + return false; + } + const literal = property.parent; + if (!ts.isObjectLiteralExpression(literal)) return false; + const call = literal.parent; + if (!ts.isCallExpression(call) || !call.arguments.includes(literal)) return false; + const calleeType = checker.getTypeAtLocation(call.expression); + return (calleeType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + }; + // (#671 W1) A direct bare-identifier DeleteBinding in `with (varName)` // makes the legacy static scope fall through to runtime HasBinding / // DeleteBinding. That runtime path and a later `varName.p` read must @@ -882,6 +912,9 @@ export function collectGrowableObjectLiterals( let poisoned = false; const visit = (node: ts.Node): void => { + if (ts.isIdentifier(node) && isNestedDynamicCallArgument(node)) { + grows = true; + } // Out-of-shape write rooted at varName. if ( ts.isBinaryExpression(node) && @@ -956,7 +989,8 @@ export function collectGrowableObjectLiterals( ts.isIdentifier(node) && node.text === varName && isValueUseOfIdentifier(node) && - !isObjectMopCallArg(node) + !isObjectMopCallArg(node) && + !isNestedDynamicCallArgument(node) ) { if (typeRequiresStruct(checker.getContextualType(node))) { poisoned = true; @@ -1048,6 +1082,18 @@ export function collectGrowableObjectLiterals( } if (ts.isFunctionDeclaration(stmt) && stmt.body) { scanStatements(stmt.body.statements); + } else { + // Bootstrap sources commonly keep all realm state inside an IIFE. + // Apply the same object-carrier analysis inside function/arrow + // expressions; otherwise their declarations never reach this pass. + const scanNestedFunctionExpressions = (node: ts.Node): void => { + if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isBlock(node.body)) { + scanStatements(node.body.statements); + return; + } + forEachChild(node, scanNestedFunctionExpressions); + }; + forEachChild(stmt, scanNestedFunctionExpressions); } if (ts.isTryStatement(stmt)) { scanStatements(stmt.tryBlock.statements); diff --git a/src/codegen/destructuring-params.ts b/src/codegen/destructuring-params.ts index 9b6be4630f..73fdc11c45 100644 --- a/src/codegen/destructuring-params.ts +++ b/src/codegen/destructuring-params.ts @@ -425,6 +425,13 @@ function boxToExternref( // undefined map (needs a scratch local). fctx?: FunctionContext, ): Instr[] { + // The registry key describes the logical producer, but the backing-array + // element is the value that is actually on the Wasm stack here. They can + // intentionally differ: Int32Array/Uint32Array use the dedicated + // `i32_elem` key while their storage still yields an i32. Always prefer the + // physical element kind for opcode selection; using `elemKey` for that case + // falls through to `extern.convert_any(i32)` and invalidates the module. + const storageKind = srcElemType?.kind ?? elemKey; // (#2669) When the backing array ALREADY stores externref elements, the value // produced by `array.get` is already an externref and needs no conversion. // The vec-type-map key alone is misleading here: a `ref_*` keyed vec (a vec of @@ -458,11 +465,11 @@ function boxToExternref( } return [{ op: "drop" }, { op: "ref.null.extern" }]; } - if (elemKey === "externref") { + if (storageKind === "externref" || storageKind === "ref_extern") { // Already externref, just pass through return []; } - if (elemKey === "f64") { + if (storageKind === "f64") { addUnionImports(ctx); const boxIdx = ctx.funcMap.get("__box_number"); if (boxIdx !== undefined) { @@ -509,7 +516,7 @@ function boxToExternref( // Fallback: drop and push null return [{ op: "drop" }, { op: "ref.null.extern" }]; } - if (elemKey === "i32") { + if (storageKind === "i32") { addUnionImports(ctx); const boxIdx = ctx.funcMap.get("__box_number"); if (boxIdx !== undefined) { @@ -517,7 +524,7 @@ function boxToExternref( } return [{ op: "drop" }, { op: "ref.null.extern" }]; } - if (elemKey === "i64") { + if (storageKind === "i64") { // (#3394) An i64-carrier vec element (a `bigint`, or a heterogeneous // `number | bigint` tuple element stored as i64) must be BOXED before it // reifies as an externref — the `array.get` yields a raw i64, and falling diff --git a/src/codegen/disposable-runtime.ts b/src/codegen/disposable-runtime.ts index fdd3ad1a54..7dbbfb43f2 100644 --- a/src/codegen/disposable-runtime.ts +++ b/src/codegen/disposable-runtime.ts @@ -49,6 +49,7 @@ import { addStringConstantGlobal, ensureExnTag } from "./registry/imports.js"; import { stringConstantExternrefInstrs } from "./native-strings.js"; import { emitUndefined } from "./expressions/late-imports.js"; import { BUILTIN_TYPE_TAGS } from "./builtin-tags.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; const EXTERNREF: ValType = { kind: "externref" }; const I32: ValType = { kind: "i32" }; @@ -608,13 +609,7 @@ export function fillDisposableStackDisposeDriver(ctx: CodegenContext): void { ], }, ]; - dispatch.push({ - op: "try", - blockType: { kind: "empty" }, - body: invokeSwitch, - catches: [{ tagIdx: exnTag, body: catchHandler }], - catchAll: undefined, - }); + dispatch.push(buildTargetTaggedTry(ctx, { kind: "empty" }, invokeSwitch, [{ tagIdx: exnTag, body: catchHandler }])); loopBody.push({ op: "if", blockType: { kind: "empty" }, then: dispatch, else: [] }); // i = i - 1; continue loopBody.push({ op: "local.get", index: I }); diff --git a/src/codegen/expressions.ts b/src/codegen/expressions.ts index f2cf972c70..1e8511eb6a 100644 --- a/src/codegen/expressions.ts +++ b/src/codegen/expressions.ts @@ -49,6 +49,7 @@ import { import { compileStringLiteral } from "./string-ops.js"; import { ensureImportMetaObject } from "./import-meta.js"; import { coerceType as coerceTypeImpl, pushDefaultValue } from "./type-coercion.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; // ── Sub-module imports ───────────────────────────────────────────────── @@ -615,13 +616,15 @@ function wrapAsyncCallInTryCatch(ctx: CodegenContext, fctx: FunctionContext, sta { op: "struct.new", typeIdx: promiseTypeIdx }, { op: "extern.convert_any" }, ]; - fctx.body.push({ - op: "try", - blockType: { kind: "val", type: { kind: "externref" } }, - body: inner, - catches: [{ tagIdx, body: catchExn }], - catchAll, - }); + fctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "val", type: { kind: "externref" } }, + inner, + [{ tagIdx, body: catchExn }], + catchAll, + ), + ); releaseTempLocal(fctx, reasonLocal); return; } @@ -642,13 +645,15 @@ function wrapAsyncCallInTryCatch(ctx: CodegenContext, fctx: FunctionContext, sta { op: "call", funcIdx: getCaughtIdx }, { op: "call", funcIdx: rejectIdx }, ]; - fctx.body.push({ - op: "try", - blockType: { kind: "val", type: { kind: "externref" } }, - body: inner, - catches: [{ tagIdx, body: catchExn }], - catchAll, - }); + fctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "val", type: { kind: "externref" } }, + inner, + [{ tagIdx, body: catchExn }], + catchAll, + ), + ); } /** diff --git a/src/codegen/expressions/call-identifier.ts b/src/codegen/expressions/call-identifier.ts index 0655ed3e94..2d65fafdf7 100644 --- a/src/codegen/expressions/call-identifier.ts +++ b/src/codegen/expressions/call-identifier.ts @@ -37,7 +37,12 @@ import { } from "../linear-uint8-signatures.js"; import { compileArrayConstructorCall, compileSymbolCall } from "../literals.js"; import { tryCompileNodeFsCall } from "../node-fs-api.js"; -import { boundFunctionTargetIsDefinitelyCompiled, calleeIsBoundFunctionVar } from "../object-builtin-effects.js"; +import { + boundFunctionTargetIsDefinitelyCompiled, + calleeIsBoundFunctionVar, + resolveApplyBindAlias, + resolveUncurryThisAlias, +} from "../object-builtin-effects.js"; import { ensureObjVecBuilders, reserveApplyClosure } from "../object-runtime.js"; import { emitNullCheckThrow, typeErrorThrowInstrs } from "../property-access.js"; import { emitRuntimeEvalInterpretedCallableAdapter } from "../runtime-eval-callable.js"; @@ -54,6 +59,8 @@ import { compileStringLiteral, emitBoolToString, emitNativeStringToHostExternref import { usesNativeNumberFormat } from "../number-format-native.js"; import { emitSymbolToString } from "../symbol-native.js"; import { resolveGlobalParseBuiltin } from "../global-builtin-resolution.js"; +import { resolveBuiltinStaticBindingAlias } from "../builtin-static-globals.js"; +import { ensureStandaloneBuiltinStaticMethodClosure } from "../builtin-value-read.js"; import { localBindingShadowsCapturingFunction } from "../function-declaration-observation.js"; import { isUnaliasedNodeFsImportBinding } from "../node-fs-binding-identity.js"; import { @@ -1254,6 +1261,37 @@ export function compileIdentifierCall( // `Function.prototype.call` VALUE, whose standalone body is the #2984 // degrade throw. The resolver only matches the immutable harness idiom. if (!isLocallyShadowed && (ctx.standalone || noJsHost(ctx))) { + // Deno's `uncurryThis = bind.bind(call)` has the exact native spelling + // `call.bind(...args)`. Construct that bound-function carrier directly; + // invoking the generic Function.prototype.bind method-value body would + // otherwise refuse dynamically discovered builtin method closures. + const callValue = resolveUncurryThisAlias(ctx.oracle, expr.expression); + if (callValue) { + const bindAccess = ts.factory.createPropertyAccessExpression(callValue, "bind"); + ts.setTextRange(bindAccess, expr.expression); + const bindCall = ts.factory.createCallExpression(bindAccess, undefined, expr.arguments); + ts.setTextRange(bindCall, expr); + (bindAccess as { parent: ts.Node }).parent = bindCall; + (bindCall as { parent: ts.Node }).parent = expr.parent; + const compiledUncurryThis = compileCallExpression(ctx, fctx, bindCall); + if (compiledUncurryThis !== null) return compiledUncurryThis; + } + // Deno's `applyBind = bind.bind(apply)` is a bound invocation of the + // Function.prototype.bind METHOD VALUE. The generic method-value body is + // intentionally a catchable refusal, but the immutable alias has an + // exact equivalent native spelling: `apply.bind(...args)`. Compile that + // spelling so the result is the ordinary `$__bound_fn` carrier. + const applyValue = resolveApplyBindAlias(ctx.oracle, expr.expression); + if (applyValue) { + const bindAccess = ts.factory.createPropertyAccessExpression(applyValue, "bind"); + ts.setTextRange(bindAccess, expr.expression); + const bindCall = ts.factory.createCallExpression(bindAccess, undefined, expr.arguments); + ts.setTextRange(bindCall, expr); + (bindAccess as { parent: ts.Node }).parent = bindCall; + (bindCall as { parent: ts.Node }).parent = expr.parent; + const compiledApplyBind = compileCallExpression(ctx, fctx, bindCall); + if (compiledApplyBind !== null) return compiledApplyBind; + } const uncurriedCall = tryCompileStoredObjectBuiltinCall(ctx, fctx, expr); if (uncurriedCall !== undefined) return uncurriedCall; } @@ -1369,11 +1407,20 @@ export function compileIdentifierCall( // the shorter (but JS-compatible) funcref signature out of the dispatch. ensureFuncValueWrappersRegistered(ctx, expr.getSourceFile()); const sig = callSigs[0]!; - const sigParamCount = sig.parameters.length; + const builtinAlias = + ctx.standalone || ctx.wasi ? resolveBuiltinStaticBindingAlias(ctx, expr.expression) : undefined; + const builtinAliasClosure = builtinAlias + ? ensureStandaloneBuiltinStaticMethodClosure(ctx, builtinAlias.builtinName, builtinAlias.propName) + : null; + const builtinAliasInfo = builtinAliasClosure + ? ctx.closureInfoByTypeIdx.get(builtinAliasClosure.type.typeIdx) + : undefined; + const sigParamCount = builtinAliasInfo?.paramTypes.length ?? sig.parameters.length; const sigRetType = ctx.checker.getReturnTypeOfSignature(sig); - const sigRetWasm = isVoidType(sigRetType) ? null : resolveWasmType(ctx, sigRetType); - const sigParamWasmTypes: ValType[] = []; - for (let i = 0; i < sigParamCount; i++) { + const sigRetWasm = + builtinAliasInfo?.returnType ?? (isVoidType(sigRetType) ? null : resolveWasmType(ctx, sigRetType)); + const sigParamWasmTypes: ValType[] = builtinAliasInfo ? [...builtinAliasInfo.paramTypes] : []; + for (let i = 0; !builtinAliasInfo && i < sigParamCount; i++) { // (#820d) Destructuring-pattern parameters (e.g. `method({ x = 5 } = {})`) // are compiled by the callee as a single `externref` slot — the binding // pattern is destructured inside the body from that externref, and the @@ -2330,13 +2377,18 @@ export function compileIdentifierCall( fctx.body.push({ op: "struct.new", typeIdx: refCellTypeIdx }); } else { // Create a ref cell, store the current value, keep ref on stack. - // (Note: #1177 originally proposed `localMap.get(cap.name) ?? cap.outerLocalIdx` - // but that caused 100+ test262 regressions where main's "wrong-slot" - // behavior was load-bearing for tests that relied on a null deref - // throwing inside an async fn body. Reverted; the canonical TDZ- - // through-closure case is fixed via the call-site TDZ check below - // and Stage 3 C.1 in compileArrowAsClosure.) - fctx.body.push({ op: "local.get", index: cap.outerLocalIdx }); + // `cap.outerLocalIdx` belongs to the callee's declaring frame. A + // lifted transitive caller carries the same binding in one of its + // own leading capture params, so reading the declaring-frame slot + // here can point far beyond the caller's frame (Deno 01_core's + // runImmediateCallbacks -> runImmediates hit slots 1240/1254 in a + // 47-slot function). Use the deliberately narrow resolver: it + // preserves the historical declaring-frame slot unless this frame + // explicitly recorded a lifted capture slot or can prove the old + // slot is stale. This is not #1177's reverted blanket localMap-first + // substitution. + const capSourceIdx = captureSourceSlot(fctx, cap); + fctx.body.push({ op: "local.get", index: capSourceIdx }); fctx.body.push({ op: "struct.new", typeIdx: refCellTypeIdx }); // Also box the outer local so subsequent reads/writes go through the ref cell const boxedLocalIdx = allocLocal(fctx, `__boxed_${cap.name}`, { diff --git a/src/codegen/expressions/call-namespace-static.ts b/src/codegen/expressions/call-namespace-static.ts index 2a19f85e67..09d5f150eb 100644 --- a/src/codegen/expressions/call-namespace-static.ts +++ b/src/codegen/expressions/call-namespace-static.ts @@ -14,7 +14,14 @@ import { ts } from "../../ts-api.js"; import { integrityVarKey } from "../widened-var-key.js"; import { isSymbolType } from "../../checker/type-mapper.js"; import type { Instr, ValType } from "../../ir/types.js"; -import { emitArrayIteratorPrototypeSingleton } from "../array-object-proto.js"; +import { + emitAsyncGeneratorFunctionPrototypeSingleton, + emitGeneratorFunctionPrototypeSingleton, + emitIteratorPrototypeSingleton, + emitTypedArrayIntrinsicCtorObject, + isWiredTypedArrayViewName, + type NativeIteratorPrototypeKind, +} from "../array-object-proto.js"; import { isPristineArrayPrototypeIteratorCall } from "../array-methods.js"; import { emitStandalonePromiseReject, @@ -106,28 +113,109 @@ function unwrapReflectConstructExpr(value: ts.Expression): ts.Expression { return current; } -/** - * Deno's first core script captures `%ArrayIteratorPrototype%` through an - * exact pristine-intrinsic expression. TypeScript names its result - * `IterableIterator` rather than `ArrayIterator`, so the type-based - * Object path cannot identify it. Compile and drop the iterator for normal - * evaluation, then return the genuine singleton shared by ordinary array - * iterators (#4378). - */ -function compilePristineArrayIteratorPrototypeCapture( +function pristineIteratorPrototypeKind( + fctx: FunctionContext, + argument: ts.Expression, +): NativeIteratorPrototypeKind | undefined { + if (isPristineArrayPrototypeIteratorCall(fctx, argument)) return "Array"; + if (!ts.isCallExpression(argument) || argument.arguments.length !== 0) return undefined; + const callee = argument.expression; + if (!ts.isElementAccessExpression(callee)) return undefined; + const key = callee.argumentExpression; + if ( + !key || + !ts.isPropertyAccessExpression(key) || + !ts.isIdentifier(key.expression) || + key.expression.text !== "Symbol" || + key.name.text !== "iterator" || + fctx.localMap.has("Symbol") || + (fctx.boxedCaptures?.has("Symbol") ?? false) + ) { + return undefined; + } + const receiver = callee.expression; + if ( + ts.isPropertyAccessExpression(receiver) && + receiver.name.text === "prototype" && + ts.isIdentifier(receiver.expression) && + receiver.expression.text === "String" && + !fctx.localMap.has("String") && + !(fctx.boxedCaptures?.has("String") ?? false) + ) { + return "String"; + } + if ( + ts.isNewExpression(receiver) && + receiver.arguments?.length === 0 && + ts.isIdentifier(receiver.expression) && + (receiver.expression.text === "Map" || receiver.expression.text === "Set") && + !fctx.localMap.has(receiver.expression.text) && + !(fctx.boxedCaptures?.has(receiver.expression.text) ?? false) + ) { + return receiver.expression.text; + } + return undefined; +} + +function compilePristineIteratorPrototypeCapture( ctx: CodegenContext, fctx: FunctionContext, argument: ts.Expression, ): ValType | undefined { - if (!noJsHost(ctx) || !isPristineArrayPrototypeIteratorCall(fctx, argument)) return undefined; - const argumentType = compileExpression(ctx, fctx, argument); - if (argumentType) fctx.body.push({ op: "drop" }); - const prototypeType = emitArrayIteratorPrototypeSingleton(ctx, fctx); + if (!noJsHost(ctx)) return undefined; + const kind = pristineIteratorPrototypeKind(fctx, argument); + if (!kind) return undefined; + // Every accepted form is an exact, zero-argument call on an unshadowed + // intrinsic receiver: + // + // Array.prototype[Symbol.iterator]() + // String.prototype[Symbol.iterator]() + // new Map()[Symbol.iterator]() + // new Set()[Symbol.iterator]() + // + // The temporary collection / iterator allocations are not observable, and + // the enclosing Reflect.getPrototypeOf immediately discards the iterator. + // Do not lower that dead intermediate call: apart from needless allocation, + // the generic dynamic-call path can require iterator instance machinery + // that is intentionally absent when only the intrinsic prototype is needed. + const prototypeType = emitIteratorPrototypeSingleton(ctx, fctx, kind); if (prototypeType) return prototypeType; fctx.body.push({ op: "ref.null.extern" }); return { kind: "externref" }; } +/** + * Resolve exact intrinsic constructor/function prototype queries without + * sending opaque compiled closures through the ordinary `$Object` prototype + * reader. `Object.getPrototypeOf` already exposes these canonical standalone + * objects; Reflect must observe the same identities. + */ +function compilePristineIntrinsicPrototypeCapture( + ctx: CodegenContext, + fctx: FunctionContext, + argument: ts.Expression, +): ValType | undefined { + if (!noJsHost(ctx)) return undefined; + const value = unwrapReflectConstructExpr(argument); + + if ( + ts.isIdentifier(value) && + !fctx.localMap.has(value.text) && + !(fctx.boxedCaptures?.has(value.text) ?? false) && + isWiredTypedArrayViewName(value.text) + ) { + return emitTypedArrayIntrinsicCtorObject(ctx, fctx) ?? undefined; + } + + if (ts.isFunctionExpression(value) && value.asteriskToken !== undefined) { + return value.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true + ? (emitAsyncGeneratorFunctionPrototypeSingleton(ctx, fctx) ?? undefined) + : (emitGeneratorFunctionPrototypeSingleton(ctx, fctx) ?? undefined); + } + + return undefined; +} + /** * Materialize a syntactic array literal as the existing standalone `$ObjVec` * carrier consumed by the native JSON codec. Ordinary array lowering chooses a @@ -628,12 +716,11 @@ export function compileNamespaceStaticCall( // operation used by dynamic property access. // - Reflect.set(target, key, value) → native __reflect_set, a boolean // wrapper around the supported __extern_set data-write subset. - // - Reflect.ownKeys(target) → native __object_keys (string own keys of - // the $Object hash-map, insertion order). The native runtime tracks - // only string keys; Symbol/non-enumerable keys are out of scope for the - // standalone object runtime (consistent approximation across #1472 - // Phase B). __object_keys is in OBJECT_RUNTIME_HELPER_NAMES, so - // ensureLateImport auto-routes it to the in-module native func. + // - Reflect.ownKeys(target) → native __getOwnPropertyNames (all own + // string keys, including non-enumerable ones, in insertion order). The + // native runtime does not retain symbol-keyed properties yet, but using + // __object_keys here would incorrectly give Reflect enumerable-only + // Object.keys semantics and hide builtin methods from reflection. // - Reflect.apply/construct require call/constructor machinery with no // native analog in this slice. Descriptor/prototype/integrity methods // stay refused until their native invariants are proven end-to-end. @@ -808,7 +895,7 @@ export function compileNamespaceStaticCall( fctx.body.push({ op: "local.set", index: targetLocal }); emitNativeReflectTargetGuard(targetLocal, "Reflect.ownKeys called on non-object"); - const funcIdx = ensureLateImport(ctx, "__object_keys", [externRef], [externRef]); + const funcIdx = ensureLateImport(ctx, "__getOwnPropertyNames", [externRef], [externRef]); flushLateImportShifts(ctx, fctx); const boundaryOwnKeysIdx = boundaryReflectInterop ? ctx.funcMap.get("__boundary_object_own_keys") : undefined; if (funcIdx !== undefined && boundaryOwnKeysIdx !== undefined) { @@ -988,7 +1075,9 @@ export function compileNamespaceStaticCall( fctx.body.push({ op: "ref.null.extern" }); // unreachable after throw return { kind: "externref" }; } - const pristineIteratorPrototype = compilePristineArrayIteratorPrototypeCapture(ctx, fctx, arg0); + const pristineIntrinsicPrototype = compilePristineIntrinsicPrototypeCapture(ctx, fctx, arg0); + if (pristineIntrinsicPrototype) return pristineIntrinsicPrototype; + const pristineIteratorPrototype = compilePristineIteratorPrototypeCapture(ctx, fctx, arg0); if (pristineIteratorPrototype) return pristineIteratorPrototype; const argType = compileExpression(ctx, fctx, arg0, externRef); if (!argType) { diff --git a/src/codegen/expressions/call-tail-dispatch.ts b/src/codegen/expressions/call-tail-dispatch.ts index c35ac7a49d..fcf0fb8430 100644 --- a/src/codegen/expressions/call-tail-dispatch.ts +++ b/src/codegen/expressions/call-tail-dispatch.ts @@ -39,7 +39,7 @@ import type { InnerResult } from "../shared.js"; import { brandExternMethodResult, coerceType, compileExpression, VOID_RESULT } from "../shared.js"; import { compileStatement, hoistFunctionDeclarations } from "../statements.js"; import { ensureExtrasArgvGlobal, maybeSetArgcForKnownCall } from "../statements/nested-declarations.js"; -import { isStaticUndefinedArg } from "../string-ops.js"; +import { compileStringLiteral, isStaticUndefinedArg } from "../string-ops.js"; import { isStrictFunction } from "../helpers/is-strict-function.js"; import { needsImplicitArgumentsObject } from "../helpers/body-uses-arguments.js"; import { @@ -90,6 +90,17 @@ import { } from "./calls.js"; import { enterInlineIifeBindingScope, argumentsEscapesIife } from "./inline-iife-scope.js"; // (#4555) +function isPristineStringPrototypeExpression(fctx: FunctionContext, expression: ts.Expression): boolean { + return ( + ts.isPropertyAccessExpression(expression) && + expression.name.text === "prototype" && + ts.isIdentifier(expression.expression) && + expression.expression.text === "String" && + !fctx.localMap.has("String") && + !(fctx.boxedCaptures?.has("String") ?? false) + ); +} + /** * (#742 slice 5) Tail dispatch of compileCallExpression — extracted verbatim. * Handles the remaining callee shapes after the property-access and identifier @@ -655,7 +666,18 @@ export function compileTailDispatch( // Fall through to the host bridge if the native path declined. } const importName = methodName === "@@iterator" ? "__iterator" : "__async_iterator"; - const recvType = compileExpression(ctx, fctx, elemAccess.expression); + // `%String.prototype%` is the empty String value (§22.1.3). Its + // general first-class representation is a `$NativeProto` metadata + // object, which the iterator provider cannot coerce to text. Preserve + // the intrinsic call semantics by feeding the provider the equivalent + // empty native string. This exact pristine-realm shape is used by + // Deno to discover `%StringIteratorPrototype%` during bootstrap. + const recvType = + methodName === "@@iterator" && + (ctx.standalone || ctx.wasi) && + isPristineStringPrototypeExpression(fctx, elemAccess.expression) + ? compileStringLiteral(ctx, fctx, "") + : compileExpression(ctx, fctx, elemAccess.expression); if (recvType) { if (recvType.kind === "ref" || recvType.kind === "ref_null") { fctx.body.push({ op: "extern.convert_any" }); diff --git a/src/codegen/expressions/calls.ts b/src/codegen/expressions/calls.ts index f603a43096..72282d1da6 100644 --- a/src/codegen/expressions/calls.ts +++ b/src/codegen/expressions/calls.ts @@ -128,6 +128,7 @@ import { resolveComputedKeyExpression, resolvePropertyNameText, } from "../literals.js"; +import { compileInternalCallArgument } from "./internal-call-argument.js"; import { jsonGapFromStaticSpace, staticSpaceValue, @@ -3798,7 +3799,12 @@ export function tryEmitInlineDynamicCall( // dispatch arm can marshal it independently without re-evaluating. const argLocals: number[] = []; for (let i = 0; i < arity; i++) { - compileExpression(ctx, fctx, expr.arguments[i]!, { kind: "externref" }); + // A dynamic native call is still an internal JavaScript-value boundary. + // In particular, Deno invokes the captured `Object.assign` primordial + // through this path. Materialize plain object literals as open `$Object` + // carriers so the selected callable can enumerate and mutate them rather + // than receiving an opaque boxed closed struct. + compileInternalCallArgument(ctx, fctx, expr.arguments[i]!, { kind: "externref" }); const argLocal = allocLocal(fctx, `__dyn_arg${i}_${fctx.locals.length}`, { kind: "externref" }); fctx.body.push({ op: "local.set", index: argLocal }); argLocals.push(argLocal); diff --git a/src/codegen/expressions/eval-inline.ts b/src/codegen/expressions/eval-inline.ts index f9f123495d..d17a771fec 100644 --- a/src/codegen/expressions/eval-inline.ts +++ b/src/codegen/expressions/eval-inline.ts @@ -1970,17 +1970,68 @@ export function emitStandaloneIndirectEvalRuntime( return emitRuntimeEvalResultUnwrap(ctx, fctx); } -/** Materialize the current realm's first-class `%eval%` value for standalone. - * - * The provider already owns the canonical intrinsic marker and installs it on - * the shared global object. Evaluating the tiny global Script `eval` through - * the existing indirect-eval entry returns that exact marker, so aliases are - * callable through `__runtime_apply_interpreted` and remain identity-equal to - * `globalThis.eval`. This deliberately reuses the published ABI instead of - * adding a second "get intrinsic" export. +/** Hoist the realm's first-class `%eval%` wrapper without executing eval. */ +function ensureStandaloneIntrinsicEvalWrapper( + ctx: CodegenContext, + fctx: FunctionContext, +): { fnName: string; funcIdx: number } | undefined { + const fnName = "__js2wasm_intrinsic_indirect_eval"; + const existing = ctx.funcMap.get(fnName); + if (existing !== undefined) return { fnName, funcIdx: existing }; + + // The sequence form is load-bearing: the wrapper is an ordinary function, + // so a bare `eval(source)` in its body would be direct eval in the wrapper's + // lexical environment. `(0, eval)(source)` preserves the intrinsic value's + // indirect/global semantics and is intercepted by the established runtime- + // eval call lowering when this synthetic declaration is compiled. + const sf = ts.createSourceFile( + EVAL_SOURCE_FILENAME, + `function ${fnName}(source) { return (0, eval)(source); }`, + ts.ScriptTarget.Latest, + /* setParentNodes */ true, + ts.ScriptKind.JS, + ); + if (sf.statements.length !== 1 || !ts.isFunctionDeclaration(sf.statements[0]!)) return undefined; + const fnDecl = sf.statements[0] as ts.FunctionDeclaration; + + const savedLocalMap = fctx.localMap; + const savedBoxed = fctx.boxedCaptures; + const savedFuncCount = ctx.mod.functions.length; + fctx.localMap = new Map(); + fctx.boxedCaptures = undefined; + try { + hoistFunctionDeclarations(ctx, fctx, [fnDecl]); + } catch { + if (ctx.mod.functions.length > savedFuncCount) { + ctx.mod.functions.length = savedFuncCount; + const cutoff = ctx.numImportFuncs + savedFuncCount; + for (const [name, idx] of ctx.funcMap) { + if (idx >= cutoff) ctx.funcMap.delete(name); + } + } + return undefined; + } finally { + fctx.localMap = savedLocalMap; + fctx.boxedCaptures = savedBoxed; + } + + const funcIdx = ctx.funcMap.get(fnName); + return funcIdx === undefined ? undefined : { fnName, funcIdx }; +} + +/** + * Materialize the current realm's first-class `%eval%` value for standalone. + * Construction is pure AOT: merely storing `eval` (Deno primordials does + * exactly this) must not execute the runtime compiler/interpreter. Calling the + * resulting closure later invokes the existing indirect-eval provider seam. */ export function emitStandaloneIntrinsicEvalValue(ctx: CodegenContext, fctx: FunctionContext): ValType | undefined { - return emitStandaloneIndirectEvalRuntime(ctx, fctx, [ts.factory.createStringLiteral("eval")]); + const wrapper = ensureStandaloneIntrinsicEvalWrapper(ctx, fctx); + if (!wrapper) return undefined; + const closureRef = emitFuncRefAsClosure(ctx, fctx, wrapper.fnName, wrapper.funcIdx); + if (!closureRef) return undefined; + if (closureRef.kind !== "externref") fctx.body.push({ op: "extern.convert_any" }); + return { kind: "externref" }; } /** Materialize the current realm's callable `%Function%` intrinsic through the diff --git a/src/codegen/expressions/identifiers.ts b/src/codegen/expressions/identifiers.ts index 79ea2cd16c..e6b26eca0c 100644 --- a/src/codegen/expressions/identifiers.ts +++ b/src/codegen/expressions/identifiers.ts @@ -214,6 +214,19 @@ export function emitLocalTdzCheck(ctx: CodegenContext, fctx: FunctionContext, na }); } +/** Resolve the lexical value read by an identifier, including `{ value }`. */ +function identifierValueSymbol(ctx: CodegenContext, id: ts.Identifier): ts.Symbol | undefined { + if (ts.isShorthandPropertyAssignment(id.parent) && id.parent.name === id) { + const shorthand = ( + ctx.checker as typeof ctx.checker & { + getShorthandAssignmentValueSymbol?: (node: ts.ShorthandPropertyAssignment) => ts.Symbol | undefined; + } + ).getShorthandAssignmentValueSymbol?.(id.parent); + if (shorthand !== undefined) return shorthand; + } + return ctx.checker.getSymbolAtLocation(id); +} + /** * Static TDZ analysis: determine at compile time whether a let/const variable * access is guaranteed to be after initialization (safe) or before (TDZ violation). @@ -224,7 +237,14 @@ export function emitLocalTdzCheck(ctx: CodegenContext, fctx: FunctionContext, na * - 'check': can't determine statically — keep runtime flag check */ function analyzeTdzAccess(ctx: CodegenContext, id: ts.Identifier): "skip" | "throw" | "check" { - const symbol = ctx.checker.getSymbolAtLocation(id); + // A shorthand property name (`{ value }`) has two symbols in TypeScript: + // the property being declared and the lexical binding whose value is read. + // `getSymbolAtLocation(id)` answers the former, whose declaration range is + // the shorthand itself. Treating that property declaration as the lexical + // declaration makes every tracked shorthand look like a self-read in its + // own TDZ and emits an unconditional ReferenceError. Ask the checker for the + // value symbol so ordering is measured against the real let/const binding. + const symbol = identifierValueSymbol(ctx, id); if (!symbol) return "check"; const decl = symbol.valueDeclaration; if (!decl) return "check"; @@ -359,7 +379,7 @@ function isDescendantOf(node: ts.Node, ancestor: ts.Node): boolean { } function getDeclaredNullablePrimitiveInfo(ctx: CodegenContext, id: ts.Identifier): NullablePrimitiveInfo | null { - const symbol = ctx.checker.getSymbolAtLocation(id); + const symbol = identifierValueSymbol(ctx, id); const decl = symbol?.valueDeclaration; if (!decl) return null; if (ts.isVariableDeclaration(decl) || ts.isParameter(decl)) { @@ -1305,8 +1325,7 @@ function compileIdentifierCore( // compiler-internal helper names do not resolve to any source declaration. const isInternalHelperName = (): boolean => { if (!name.startsWith("__")) return false; - const { checker } = ctx; - const valSym = checker.getSymbolAtLocation(id); + const valSym = identifierValueSymbol(ctx, id); const valDecl = valSym?.valueDeclaration; return !(valDecl !== undefined && ts.isFunctionDeclaration(valDecl)); }; @@ -1316,7 +1335,7 @@ function compileIdentifierCore( !isInternalHelperName() && !ctx.classSet.has(name) ) { - const valueDecl = ctx.checker.getSymbolAtLocation(id)?.valueDeclaration; + const valueDecl = identifierValueSymbol(ctx, id)?.valueDeclaration; const isOrdinaryFunctionDecl = (noJsHost(ctx) || ctx.targetProfile.semanticProviders === "native-first") && valueDecl !== undefined && @@ -1364,7 +1383,7 @@ function compileIdentifierCore( // (spec §13.10.1 / §13.11.4 — operand evaluation precedes ToPrimitive in `==`). // However, known globals (Symbol, Object, Reflect, etc.) have TS symbols from // lib.d.ts and should use the fallback default instead. - const sym = ctx.checker.getSymbolAtLocation(id); + const sym = identifierValueSymbol(ctx, id); if (!sym) { if ((ctx.standalone || ctx.wasi) && ctx.runtimeEvalGlobalFunctionBindings) { const dynamicGlobal = skipRuntimeEvalState @@ -1634,7 +1653,7 @@ function emitConstantInstanceOf( } function identifierHasSourceDeclaration(ctx: CodegenContext, id: ts.Identifier): boolean { - const symbol = ctx.checker.getSymbolAtLocation(id); + const symbol = identifierValueSymbol(ctx, id); const declarations = symbol?.declarations ?? []; return declarations.some((decl) => !decl.getSourceFile().isDeclarationFile); } diff --git a/src/codegen/expressions/internal-call-argument.ts b/src/codegen/expressions/internal-call-argument.ts index 63fe7cf142..936ed7bcc0 100644 --- a/src/codegen/expressions/internal-call-argument.ts +++ b/src/codegen/expressions/internal-call-argument.ts @@ -5,6 +5,7 @@ import type { ValType } from "../../ir/types.js"; import { getLocalType } from "../context/locals.js"; import type { CodegenContext, FunctionContext } from "../context/types.js"; import { getArrTypeIdxFromVec } from "../registry/types.js"; +import { compileObjectLiteralAsExternref, resolvePropertyNameText } from "../literals.js"; import { canCompilePropertyAccessForNullishObservation, compilePropertyAccessForNullishObservation, @@ -27,6 +28,29 @@ export function compileInternalCallArgument( expression: ts.Expression, expectedType: ValType | undefined, ): ValType | null { + // A native `externref` parameter is an open JavaScript-value boundary. A + // plain object literal must therefore use the runtime `$Object` carrier, + // even when TypeScript gives the literal a concrete contextual object type. + // Boxing a closed Wasm struct as externref makes dynamic operations such as + // a captured `Object.assign(target, source)` unable to enumerate the source. + if ( + expectedType?.kind === "externref" && + (ctx.targetProfile.semanticProviders === "native-first" || ctx.standalone || ctx.wasi) && + ts.isObjectLiteralExpression(expression) && + expression.properties.every( + (property) => + ts.isPropertyAssignment(property) || + ts.isShorthandPropertyAssignment(property) || + ts.isSpreadAssignment(property), + ) && + expression.properties.every( + (property) => ts.isSpreadAssignment(property) || resolvePropertyNameText(ctx, property) !== undefined, + ) + ) { + const objectValue = compileObjectLiteralAsExternref(ctx, fctx, expression); + if (objectValue !== null) return objectValue; + } + if (expectedType?.kind !== "externref" || ctx.standalone || ctx.wasi) { return compileExpression(ctx, fctx, expression, expectedType); } diff --git a/src/codegen/extern-get-inline-ic.ts b/src/codegen/extern-get-inline-ic.ts index 7517a5a156..f564a4db0f 100644 --- a/src/codegen/extern-get-inline-ic.ts +++ b/src/codegen/extern-get-inline-ic.ts @@ -140,7 +140,7 @@ function producesOneExternref(ctx: CodegenContext, fn: WasmFunction, instr: Inst if ((a.op === "local.get" || a.op === "local.tee") && a.index !== undefined) { return localTypeOf(ctx, fn, a.index)?.kind === "externref"; } - if ((a.op === "if" || a.op === "block") && a.blockType?.kind === "val") { + if ((a.op === "if" || a.op === "block" || a.op === "try_table") && a.blockType?.kind === "val") { return a.blockType.type?.kind === "externref"; } return false; diff --git a/src/codegen/fixups.ts b/src/codegen/fixups.ts index 79920cd917..2e7aed5c47 100644 --- a/src/codegen/fixups.ts +++ b/src/codegen/fixups.ts @@ -148,6 +148,7 @@ export function repairBody(body: Instr[], localTypes: ValType[], mod: WasmModule switch (instr.op) { case "block": case "loop": + case "try_table": if (instr.body) fixed += repairBody(instr.body, localTypes, mod); break; case "if": @@ -274,7 +275,7 @@ export function repairBody(body: Instr[], localTypes: ValType[], mod: WasmModule let crossedControlFlow = false; for (let j = i - 1; j >= 0; j--) { const op = body[j]!.op; - if (op === "if" || op === "block" || op === "loop" || op === "try") { + if (op === "if" || op === "block" || op === "loop" || op === "try" || op === "try_table") { crossedControlFlow = true; } depth += instrStackDelta(body[j]!, mod); @@ -300,6 +301,32 @@ export function repairBody(body: Instr[], localTypes: ValType[], mod: WasmModule const idx = (refProducer as { index: number }).index; const localType = localTypes[idx]; if (localType && localType.kind === "externref") { + // The receiver can already carry the exact coercion emitted by + // compileExpression(expected struct type): + // + // local.get $extern + // any.convert_extern + // ref.cast(_null) $Struct + // + // struct.set $Struct + // + // The backward walk deliberately skips net-zero conversions, so it + // still lands on the original externref producer. Inserting another + // pair there creates `ref.cast_null; any.convert_extern`, which is + // invalid because any.convert_extern only accepts externref. Deno's + // `class BadResource extends Error { ... this.name = ... }` exposes + // this exact shape. An immediate exact-type cast is authoritative + // evidence that this producer is already repaired; leave it alone. + const conversion = body[refIdx + 1]; + const cast = body[refIdx + 2]; + const alreadyExactStructRef = + conversion?.op === "any.convert_extern" && + (cast?.op === "ref.cast" || cast?.op === "ref.cast_null") && + cast.typeIdx === structTypeIdx; + if (alreadyExactStructRef) { + i++; + continue; + } body.splice(refIdx + 1, 0, { op: "any.convert_extern" }, { op: "ref.cast_null", typeIdx: structTypeIdx }); fixed++; i += 3; // shifted by 2 insertions + advance past struct.set @@ -499,6 +526,7 @@ export function instrStackDelta(instr: Instr, mod: WasmModule): number { case "loop": case "if": case "try": + case "try_table": return 0; // Terminal diff --git a/src/codegen/function-body.ts b/src/codegen/function-body.ts index 8a20400b13..7caa4ea6c5 100644 --- a/src/codegen/function-body.ts +++ b/src/codegen/function-body.ts @@ -28,6 +28,7 @@ import { resolveWasmType, } from "./index.js"; import { ensureExnTag } from "./registry/imports.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; import { getArrTypeIdxFromVec, getOrRegisterVecType } from "./registry/types.js"; import { coerceType, @@ -155,6 +156,7 @@ export const INLINE_DISALLOWED_OPS = new Set([ "br", "br_if", "try", + "try_table", "throw", "rethrow", "unreachable", @@ -787,13 +789,15 @@ export function compileFunctionBody(ctx: CodegenContext, decl: ts.FunctionDeclar { op: "local.set", index: pendingThrowLocal }, ] : []; - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], - catches: [{ tagIdx, body: catchBody }], - catchAll: catchAllBody.length > 0 ? catchAllBody : undefined, - }); + fctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], + [{ tagIdx, body: catchBody }], + catchAllBody.length > 0 ? catchAllBody : undefined, + ), + ); // Return __create_generator or __create_async_generator depending on async flag. // Note: ctx.asyncFunctions excludes async generators (by design), so we check diff --git a/src/codegen/generators-native.ts b/src/codegen/generators-native.ts index d4d349bec3..af30275ea7 100644 --- a/src/codegen/generators-native.ts +++ b/src/codegen/generators-native.ts @@ -57,6 +57,7 @@ import { bodyNeedsArgumentsObject } from "./helpers/body-uses-arguments.js"; import { resolveSpillLocalValType } from "./statements/variables.js"; import { resolveWasmType } from "./index.js"; import { ensureExnTag } from "./registry/imports.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; // (#2895 PR1) The frame ABI (state-struct field offsets + resume modes) and the // field-I/O / spill-store emit helpers now live in the shared resumable-frame // core, consumed unchanged here and by the host-free async path (PATH B). @@ -3152,18 +3153,13 @@ function compileState( // re-throw / a finally-thrown replacement error. Foreign JS // exceptions (host mode) recover via __get_caught_exception when // the resume emitter acquired it (#3050 wrap parity). - { - op: "try", - blockType: { kind: "empty" }, - body: [ - { op: "local.get", index: closeDelegLocal }, - { op: "call", funcIdx: closeResumeIdx }, - { op: "drop" }, - ], - catches: [{ tagIdx: ensureExnTag(ctx), body: closeCatch }], - catchAll: - getCaughtExnIdx !== undefined ? [{ op: "call", funcIdx: getCaughtExnIdx }, ...closeCatch] : undefined, - }, + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "local.get", index: closeDelegLocal }, { op: "call", funcIdx: closeResumeIdx }, { op: "drop" }], + [{ tagIdx: ensureExnTag(ctx), body: closeCatch }], + getCaughtExnIdx !== undefined ? [{ op: "call", funcIdx: getCaughtExnIdx }, ...closeCatch] : undefined, + ), // Close complete — clear the slot. { op: "local.get", index: selfLocal }, { op: "ref.null", typeIdx: closeInner.stateTypeIdx }, @@ -3764,13 +3760,13 @@ function compileState( const catchAll: Instr[] | undefined = getCaughtExnIdx !== undefined ? [{ op: "call", funcIdx: getCaughtExnIdx }, ...routeInstrs()] : undefined; return [ - { - op: "try", - blockType: { kind: "empty" }, + buildTargetTaggedTry( + ctx, + { kind: "empty" }, body, - catches: [{ tagIdx: ensureExnTag(ctx), body: routeInstrs() }], + [{ tagIdx: ensureExnTag(ctx), body: routeInstrs() }], catchAll, - }, + ), ]; } return body; diff --git a/src/codegen/ic-guard-reuse.ts b/src/codegen/ic-guard-reuse.ts index 6a5bf9df8a..07912b06d7 100644 --- a/src/codegen/ic-guard-reuse.ts +++ b/src/codegen/ic-guard-reuse.ts @@ -309,7 +309,7 @@ function walkForReuse( const a = instr as AnyInstr; const kids = childArrays(a); if (kids.length > 0) { - const reentrant = a.op === "loop" || a.op === "try"; + const reentrant = a.op === "loop" || a.op === "try" || a.op === "try_table"; for (const kid of kids) { const sub: WalkState = { live: new Map(state.live), vals: new ValueIds(state.vals) }; if (reentrant) { diff --git a/src/codegen/index.ts b/src/codegen/index.ts index 5966408b8d..9c512fa6d3 100644 --- a/src/codegen/index.ts +++ b/src/codegen/index.ts @@ -221,7 +221,12 @@ import { ensureMapRuntimeTypes } from "./map-runtime.js"; import { scanForNewTarget } from "./new-target.js"; // (#2023) import { scanForDynamicProto, fillDynamicProtoHelpers } from "./dynamic-proto.js"; // (#802) import { scanForArrayHoles, ensureHoleType } from "./array-holes.js"; // (#2001 S1) -import { hoistedVarRetypesToConcreteRef, inferArrayVecType, usageInferredLocalType } from "./statements/variables.js"; // (#2106 S1 PR-2) hoist undefined-init retype predicate; (#684) usage-based any-local f64 override +import { + hoistedVarRetypesToConcreteRef, + inferArrayVecType, + inferTaViewType, + usageInferredLocalType, +} from "./statements/variables.js"; // (#2106 S1 PR-2) hoist undefined-init retype predicate; (#684) usage-based any-local f64 override import { bindingHasMixedAssignmentCarrier } from "./analysis/mixed-assignment-carrier.js"; import { symbolBrand } from "./symbol-field-carrier.js"; import { ensureDynReadHelpers, ensureDynMemberGet } from "./dyn-read.js"; // (#2580 M0) / (#3053 U0) @@ -354,6 +359,7 @@ import { shiftAsyncSideChannelFuncIdxs, } from "./async-scheduler.js"; import { ensureUnhandledRejectionReporter } from "./unhandled-rejection.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; import { inLiveShiftRange } from "../emit/resolve-layout.js"; // (#1916 S3) stable handles never shift import { profileCount, profilePhase } from "../compile-profile.js"; import { frameSnapshotAtCompile } from "./function-body.js"; @@ -6224,24 +6230,19 @@ function addWasiStartExport(ctx: CodegenContext): void { const startBody: Instr[] = exnPrinterIdx !== undefined && ctx.wasiProcExitIdx >= 0 ? [ - { - op: "try", - blockType: { kind: "empty" }, - body, - catches: [ - { - tagIdx: ctx.exnTagIdx, - body: [ - // The catch pushes the thrown externref payload; render + write it. - { op: "call", funcIdx: exnPrinterIdx }, - // An uncaught exception is a failure — exit nonzero. - { op: "i32.const", value: 1 }, - { op: "call", funcIdx: ctx.wasiProcExitIdx }, - { op: "unreachable" }, - ], - }, - ], - }, + buildTargetTaggedTry(ctx, { kind: "empty" }, body, [ + { + tagIdx: ctx.exnTagIdx, + body: [ + // The catch pushes the thrown externref payload; render + write it. + { op: "call", funcIdx: exnPrinterIdx }, + // An uncaught exception is a failure — exit nonzero. + { op: "i32.const", value: 1 }, + { op: "call", funcIdx: ctx.wasiProcExitIdx }, + { op: "unreachable" }, + ], + }, + ]), ] : body; @@ -10905,6 +10906,14 @@ function inferLetConstInitializerWasmType( initializer: ts.Expression | undefined, ): ValType | null { if (!initializer) return null; + // (#4376) Keep the authoritative pre-hoisted slot type in lockstep with + // compileVariableStatement. A buffer-backed typed array is represented by a + // shared-backing `$__ta_view`, not the checker-inferred plain vector. Nested + // functions record their capture signatures before declaration lowering, so + // missing this override made reifying a closure cast the real view value to + // an unrelated vector type and trap during Deno core bootstrap. + const taViewType = inferTaViewType(ctx, initializer); + if (taViewType !== null) return taViewType; const standaloneRegExpMatchArrayType = inferStandaloneRegExpMatchArrayType(ctx, initializer); if (standaloneRegExpMatchArrayType !== null) return standaloneRegExpMatchArrayType; @@ -11124,10 +11133,23 @@ function walkStmtForLetConst(ctx: CodegenContext, fctx: FunctionContext, stmt: t decl.initializer !== undefined && ts.isObjectLiteralExpression(decl.initializer) && ctx.dynamicProtoLiteralNodes.has(decl.initializer); + // (#4376) Keep the authoritative pre-hoisted slot in lockstep with + // compileVariableStatement/compileObjectLiteral for a binding whose + // object later receives out-of-shape or runtime-keyed writes. Without + // this override a hoisted nested function records the capture as the + // literal's inferred closed struct, while the declaration correctly + // builds and stores an open `$Object` externref. Reifying that function + // then casts the externref capture back to the unrelated struct and + // traps (Deno's `registerErrorClass` capturing `errorConstructors`). + const initIsGrowableObjectLiteral = + decl.initializer !== undefined && + ts.isObjectLiteralExpression(decl.initializer) && + ctx.growableObjectLiteralVars.has(name); const initIsOrdinaryToPrimitiveObjectLiteral = ctx.ordinaryToPrimitiveObjectDeclarations.has(decl); const initForcesExternref = initIsAccessorLiteral || initIsHostSpreadLiteral || + initIsGrowableObjectLiteral || initIsOrdinaryToPrimitiveObjectLiteral || initIsProtoReceiverLiteral; if (initForcesExternref) { @@ -11290,7 +11312,7 @@ function collectStringCalls(instrs: Instr[], strFuncIdxSet: Set, found: found.add(instr.funcIdx); } // Recurse into nested blocks - if (instr.op === "block" || instr.op === "loop") { + if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { collectStringCalls(instr.body, strFuncIdxSet, found); } else if (instr.op === "if") { collectStringCalls(instr.then, strFuncIdxSet, found); @@ -11315,7 +11337,7 @@ function replaceStringCalls(instrs: Instr[], cacheMap: Map): voi (instrs as any)[i] = { op: "local.get", index: localIdx }; } // Recurse into nested blocks - if (instr.op === "block" || instr.op === "loop") { + if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { replaceStringCalls(instr.body, cacheMap); } else if (instr.op === "if") { replaceStringCalls(instr.then, cacheMap); diff --git a/src/codegen/ir-inline.ts b/src/codegen/ir-inline.ts index 5be30dd692..9bfcc7c466 100644 --- a/src/codegen/ir-inline.ts +++ b/src/codegen/ir-inline.ts @@ -412,6 +412,7 @@ function childBodies(instr: Instr): Instr[][] { switch (instr.op) { case "block": case "loop": + case "try_table": return [instr.body]; case "if": return instr.else ? [instr.then, instr.else] : [instr.then]; @@ -490,7 +491,7 @@ function calleeIsSafe(fn: WasmFunction, results: ValType[]): DeclineReason | nul forEachInstr(fn.body, (i) => { if (bad) return; if (i.op === "return_call" || i.op === "return_call_ref") bad = "unsafe:return-call"; - else if (i.op === "try" || i.op === "rethrow") bad = "unsafe:try"; + else if (i.op === "try" || i.op === "try_table" || i.op === "rethrow") bad = "unsafe:try"; }); return bad; } @@ -503,6 +504,7 @@ function cloneInstr(instr: Instr): Instr { switch (instr.op) { case "block": case "loop": + case "try_table": return { ...instr, body: instr.body.map(cloneInstr) }; case "if": return { diff --git a/src/codegen/ir-tail-call.ts b/src/codegen/ir-tail-call.ts index 633a2883fb..c1384b38ea 100644 --- a/src/codegen/ir-tail-call.ts +++ b/src/codegen/ir-tail-call.ts @@ -166,13 +166,24 @@ function rewriteArmTrailingTailCall(ctx: CodegenContext, arm: Instr[], caller: C * (possibly shortened) buffer. `try` is intentionally NOT descended into. */ function convertBuffer(ctx: CodegenContext, body: Instr[], caller: CallerSig): Instr[] { + const containsTryTable = (instrs: Instr[]): boolean => { + for (const instr of instrs) { + if (instr.op === "try_table") return true; + if ((instr.op === "block" || instr.op === "loop") && containsTryTable(instr.body)) return true; + if (instr.op === "if" && (containsTryTable(instr.then) || (instr.else && containsTryTable(instr.else)))) { + return true; + } + } + return false; + }; + // Recurse first into nested control-flow arms (their trailing return is a // tail of the function too). Skip `try` — see header. for (const instr of body) { if (instr.op === "if") { instr.then = convertBuffer(ctx, instr.then, caller); if (instr.else) instr.else = convertBuffer(ctx, instr.else, caller); - } else if (instr.op === "block" || instr.op === "loop") { + } else if ((instr.op === "block" || instr.op === "loop") && !containsTryTable(instr.body)) { instr.body = convertBuffer(ctx, instr.body, caller); } // `try`: left untouched (return_call inside a try-with-handler would let a diff --git a/src/codegen/literals.ts b/src/codegen/literals.ts index 1c064f3b14..830d23fc18 100644 --- a/src/codegen/literals.ts +++ b/src/codegen/literals.ts @@ -62,6 +62,7 @@ import { resolveWasmType, } from "./index.js"; import { ensureExnTag, nextModuleGlobalIdx } from "./registry/imports.js"; +import { buildTargetTaggedTry } from "../ir/try-table.js"; import { compileNativeGeneratorFunction, emitNativeGeneratorToVec, @@ -3510,13 +3511,15 @@ export function compileObjectLiteralForStruct( { op: "local.set", index: pendingThrowLocal }, ] : []; - methodFctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], - catches: [{ tagIdx, body: catchBody }], - catchAll: catchAllBody.length > 0 ? catchAllBody : undefined, - }); + methodFctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], + [{ tagIdx, body: catchBody }], + catchAllBody.length > 0 ? catchAllBody : undefined, + ), + ); // Return __create_generator or __create_async_generator depending on async flag const createGenName = isAsyncMethod ? "__create_async_generator" : "__create_generator"; @@ -4609,7 +4612,21 @@ export function compileArrayLiteral( // byte-identical (their elision keeps the element default / sNaN path). emitHoleSentinel(ctx, fctx); } else { - compileExpression(ctx, fctx, el, elemWasm); + // A heterogeneous object-literal array selects the universal + // externref carrier above because its closed struct shapes are not + // interchangeable. Keep construction in lockstep with that decision: + // a closed struct merely wrapped as externref is opaque to the dynamic + // property reads used by callback destructuring (`forEach(({x}) => …)`), + // so every field would read as undefined. Build data-only literals as + // open `$Object`s instead. The builder recursively applies the same + // representation to nested data literals, while accessors, methods, + // spreads, and computed keys retain their established lowering. + const objectElement = elemWasm.kind === "externref" ? unwrapObjectLiteralElement(el) : null; + const openObjectType = + objectElement !== null && staticObjectLiteralDataKeys(ctx, objectElement) !== null + ? compileObjectLiteralAsExternref(ctx, fctx, objectElement) + : null; + if (openObjectType === null) compileExpression(ctx, fctx, el, elemWasm); } } fctx.body.push({ op: "array.new_fixed", typeIdx: arrTypeIdx, length: expr.elements.length }); diff --git a/src/codegen/named-this-call.ts b/src/codegen/named-this-call.ts index 2d86f55d83..39a0783d6b 100644 --- a/src/codegen/named-this-call.ts +++ b/src/codegen/named-this-call.ts @@ -28,7 +28,9 @@ import { bodyReferencesOwnThis } from "./helpers/body-references-own-this.js"; import { isStrictContext } from "./helpers/is-strict-function.js"; import { thisReceiverIsGlobalObject } from "./helpers/sloppy-this-global.js"; import { addFuncType } from "./registry/types.js"; +import { ensureExnTag } from "./registry/imports.js"; import { ensureCurrentThisGlobal } from "./statements/nested-declarations.js"; +import { buildStandardTryTable } from "../ir/try-table.js"; interface NamedThisCallTarget { readonly trampolineFuncIdx: FuncHandle; @@ -209,31 +211,53 @@ function ensureNamedThisCallTrampoline( const prevThisLocal = trampolineParams.length; const resultType = results[0]; const resultLocal = resultType === undefined ? -1 : prevThisLocal + 1; - const exactCall = callTarget(targetFuncIdx, params.length); + const standardizedEh = ctx.wasi || ctx.standalone; + const unwindExnLocal = resultType === undefined ? prevThisLocal + 1 : resultLocal + 1; + const exactCall = (): Instr[] => callTarget(targetFuncIdx, params.length); // Save the ambient receiver, install `receiver`, run the exact call, restore // on both the normal and the unwinding exit. - const installAndCall = (receiver: readonly Instr[]): Instr[] => [ - { op: "global.get", index: currentThisGlobalIdx }, - { op: "local.set", index: prevThisLocal }, - ...receiver, - { op: "global.set", index: currentThisGlobalIdx }, - { - op: "try", - blockType: resultType === undefined ? { kind: "empty" } : { kind: "val", type: resultType }, - body: exactCall, - catches: [], - catchAll: [ - { op: "local.get", index: prevThisLocal }, - { op: "global.set", index: currentThisGlobalIdx }, - { op: "rethrow", depth: 0 }, - ], - }, - ...(resultLocal < 0 ? [] : ([{ op: "local.set", index: resultLocal }] satisfies Instr[])), - { op: "local.get", index: prevThisLocal }, - { op: "global.set", index: currentThisGlobalIdx }, - ...(resultLocal < 0 ? [] : ([{ op: "local.get", index: resultLocal }] satisfies Instr[])), - ]; + const installAndCall = (receiver: readonly Instr[]): Instr[] => { + const blockType = + resultType === undefined ? ({ kind: "empty" } as const) : ({ kind: "val", type: resultType } as const); + const protectedCall: Instr = standardizedEh + ? buildStandardTryTable(blockType, exactCall(), [ + { + kind: "catch", + tagIdx: ensureExnTag(ctx), + payloadType: { kind: "externref" }, + body: [ + { op: "local.set", index: unwindExnLocal }, + { op: "local.get", index: prevThisLocal }, + { op: "global.set", index: currentThisGlobalIdx }, + { op: "local.get", index: unwindExnLocal }, + { op: "throw", tagIdx: ensureExnTag(ctx) }, + ], + }, + ]) + : { + op: "try", + blockType, + body: exactCall(), + catches: [], + catchAll: [ + { op: "local.get", index: prevThisLocal }, + { op: "global.set", index: currentThisGlobalIdx }, + { op: "rethrow", depth: 0 }, + ], + }; + return [ + { op: "global.get", index: currentThisGlobalIdx }, + { op: "local.set", index: prevThisLocal }, + ...receiver, + { op: "global.set", index: currentThisGlobalIdx }, + protectedCall, + ...(resultLocal < 0 ? [] : ([{ op: "local.set", index: resultLocal }] satisfies Instr[])), + { op: "local.get", index: prevThisLocal }, + { op: "global.set", index: currentThisGlobalIdx }, + ...(resultLocal < 0 ? [] : ([{ op: "local.get", index: resultLocal }] satisfies Instr[])), + ]; + }; const liveCall: Instr[] = installAndCall([{ op: "local.get", index: 0 }]); @@ -263,6 +287,7 @@ function ensureNamedThisCallTrampoline( locals: [ { name: "__previous_this", type: { kind: "externref" } }, ...(resultType === undefined ? [] : [{ name: "__result", type: resultType }]), + ...(standardizedEh ? [{ name: "__unwind_exception", type: { kind: "externref" } as const }] : []), ], body, exported: false, diff --git a/src/codegen/native-strings-shared.ts b/src/codegen/native-strings-shared.ts index aabb0322dd..705e940ad1 100644 --- a/src/codegen/native-strings-shared.ts +++ b/src/codegen/native-strings-shared.ts @@ -121,7 +121,7 @@ export function makeNativeStrShared( processed.push(newIf); continue; } - if (instr.op === "block" || instr.op === "loop") { + if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { const blockInstr = instr as any; const newBlock: any = { ...blockInstr }; if (blockInstr.body) newBlock.body = wrapBodyWithFlatten(blockInstr.body, []).slice(0); diff --git a/src/codegen/object-builtin-effects.ts b/src/codegen/object-builtin-effects.ts index e74e1e1868..296624d497 100644 --- a/src/codegen/object-builtin-effects.ts +++ b/src/codegen/object-builtin-effects.ts @@ -84,6 +84,85 @@ export function boundFunctionTargetIsDefinitelyCompiled(oracle: TypeOracle, expr return visit(root); } +function resolvesToConstFunctionPrototypeMethod( + oracle: TypeOracle, + expr: ts.Expression, + method: "apply" | "bind" | "call", +): boolean { + const value = skipTransparentExpressions(expr); + if (!ts.isIdentifier(value)) return false; + const declaration = oracle.valueDeclarationOf(value); + if (!declaration || !ts.isBindingElement(declaration) || declaration.dotDotDotToken) return false; + const property = declaration.propertyName ?? declaration.name; + if (!ts.isIdentifier(property) || property.text !== method) return false; + const pattern = declaration.parent; + if (!ts.isObjectBindingPattern(pattern)) return false; + const variable = pattern.parent; + if (!ts.isVariableDeclaration(variable) || variable.name !== pattern || !variable.initializer) return false; + const list = variable.parent; + if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return false; + const initializer = skipTransparentExpressions(variable.initializer); + return ( + ts.isPropertyAccessExpression(initializer) && + initializer.name.text === "prototype" && + ts.isIdentifier(initializer.expression) && + initializer.expression.text === "Function" + ); +} + +/** + * Resolve Deno's immutable `applyBind = bind.bind(apply)` primordial helper. + * Calling `applyBind(target, receiver)` is exactly + * `Function.prototype.apply.bind(target, receiver)`. Returning the captured + * `apply` expression lets call lowering mint the ordinary native bound-function + * carrier without trying to invoke the still-generic Function.prototype.bind + * method-value closure. + */ +export function resolveApplyBindAlias(oracle: TypeOracle, expr: ts.Expression): ts.Expression | undefined { + const initializer = oracle.variableInitializerOf(expr); + if (!initializer) return undefined; + const init = skipTransparentExpressions(initializer); + if ( + !ts.isCallExpression(init) || + init.arguments.length !== 1 || + !ts.isPropertyAccessExpression(init.expression) || + init.expression.name.text !== "bind" + ) { + return undefined; + } + const bindValue = init.expression.expression; + const applyValue = init.arguments[0]!; + if (!resolvesToConstFunctionPrototypeMethod(oracle, bindValue, "bind")) return undefined; + if (!resolvesToConstFunctionPrototypeMethod(oracle, applyValue, "apply")) return undefined; + return applyValue; +} + +/** + * Resolve Deno's immutable `uncurryThis = bind.bind(call)` primordial helper. + * Calling `uncurryThis(target)` is exactly + * `Function.prototype.call.bind(target)`. As with `applyBind`, exposing the + * captured method lets call lowering construct the ordinary native bound + * function directly instead of invoking the generic `bind` method-value body. + */ +export function resolveUncurryThisAlias(oracle: TypeOracle, expr: ts.Expression): ts.Expression | undefined { + const initializer = oracle.variableInitializerOf(expr); + if (!initializer) return undefined; + const init = skipTransparentExpressions(initializer); + if ( + !ts.isCallExpression(init) || + init.arguments.length !== 1 || + !ts.isPropertyAccessExpression(init.expression) || + init.expression.name.text !== "bind" + ) { + return undefined; + } + const bindValue = init.expression.expression; + const callValue = init.arguments[0]!; + if (!resolvesToConstFunctionPrototypeMethod(oracle, bindValue, "bind")) return undefined; + if (!resolvesToConstFunctionPrototypeMethod(oracle, callValue, "call")) return undefined; + return callValue; +} + export type UncurriedBuiltinPrototypeMethod = | { builtin: "Array"; method: "join" | "push" } | { builtin: "Object"; method: "hasOwnProperty" | "propertyIsEnumerable" | "valueOf" }; diff --git a/src/codegen/promise-executor.ts b/src/codegen/promise-executor.ts index 32d72dfe26..173c8e9a79 100644 --- a/src/codegen/promise-executor.ts +++ b/src/codegen/promise-executor.ts @@ -41,6 +41,7 @@ import { coerceType, emitGuardedFuncRefCast, pushDefaultValue } from "./type-coe import { emitNullCheckThrow } from "./property-access.js"; import { ensureObjectRuntime, reserveApplyClosure } from "./object-runtime.js"; import { addUnionImportsViaRegistry } from "./shared.js"; +import { buildStandardTryTable } from "../ir/try-table.js"; import { PROMISE_STATE_PENDING, // (#3125) `ensurePromiseExecutorClosures` + its interface moved to @@ -186,13 +187,12 @@ export function emitStandalonePromiseFromExecutor( fctx.body = fctx.savedBodies.pop()!; } - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: tryBody, - catches: [ + fctx.body.push( + buildStandardTryTable({ kind: "empty" }, tryBody, [ { + kind: "catch", tagIdx: exnTag, + payloadType: { kind: "externref" }, body: [ { op: "local.set", index: reasonLocal }, { op: "local.get", index: pLocal }, @@ -201,8 +201,8 @@ export function emitStandalonePromiseFromExecutor( { op: "drop" }, ], }, - ], - }); + ]), + ); // 6. Result: the pending/settled $Promise as externref. fctx.body.push({ op: "local.get", index: pLocal }); @@ -302,13 +302,12 @@ export function emitStandalonePromiseFromExecutorValue( { op: "call", funcIdx: applyClosureIdx }, { op: "drop" }, // executor return value is ignored (§27.2.3.1) ]; - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: tryBody, - catches: [ + fctx.body.push( + buildStandardTryTable({ kind: "empty" }, tryBody, [ { + kind: "catch", tagIdx: exnTag, + payloadType: { kind: "externref" }, body: [ { op: "local.set", index: reasonLocal }, { op: "local.get", index: pLocal }, @@ -317,8 +316,8 @@ export function emitStandalonePromiseFromExecutorValue( { op: "drop" }, ], }, - ], - }); + ]), + ); // 5. Result: the pending/settled $Promise as externref. fctx.body.push({ op: "local.get", index: pLocal }); diff --git a/src/codegen/property-access-dispatch.ts b/src/codegen/property-access-dispatch.ts index b7d9640b56..5ca0fe78b4 100644 --- a/src/codegen/property-access-dispatch.ts +++ b/src/codegen/property-access-dispatch.ts @@ -1082,11 +1082,14 @@ export function tryBufferViewAttributeReads( }); return { kind: "externref" }; } else { - // TypedArray: backing is an f64 vec (or i8 for standalone Uint8Array); - // byteLen = element-count (field 0) × BYTES_PER_ELEMENT. - const elemKey = noJsHost(ctx) && bufRecvName === "Uint8Array" ? "i8_byte" : "f64"; - const elemType: ValType = elemKey === "i8_byte" ? { kind: "i8" } : { kind: "f64" }; - const viewVecTypeIdx = getOrRegisterVecType(ctx, elemKey, elemType); + // TypedArray: recover the receiver through the SAME storage mapping + // used by its constructor. The old Uint8Array-vs-f64 split predates + // packed Int8/Int16 and dedicated i32-element storage; after that + // migration it cast every such receiver to the wrong vec type and + // trapped on `.buffer` (Deno's Uint32Array→Uint8Array call-site + // scratch view is the bootstrap-critical instance). + const viewStorage = typedArrayVecStorage(ctx, bufRecvName!); + const viewVecTypeIdx = getOrRegisterVecType(ctx, viewStorage.key, viewStorage.type); const recvType = compileExpression(ctx, fctx, expr.expression); if (recvType?.kind === "externref") { fctx.body.push({ op: "any.convert_extern" }); diff --git a/src/codegen/stack-balance.ts b/src/codegen/stack-balance.ts index 8aa61f3940..be7ac28836 100644 --- a/src/codegen/stack-balance.ts +++ b/src/codegen/stack-balance.ts @@ -212,7 +212,7 @@ function eliminateDeadCode(body: Instr[]): number { const ifInstr = instr as { op: "if"; then: Instr[]; else?: Instr[] }; removed += eliminateDeadCode(ifInstr.then); if (ifInstr.else) removed += eliminateDeadCode(ifInstr.else); - } else if (instr.op === "block" || instr.op === "loop") { + } else if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { const blockInstr = instr as { op: string; body: Instr[] }; removed += eliminateDeadCode(blockInstr.body); } else if (instr.op === "try") { @@ -526,7 +526,7 @@ function instrDelta(instr: Instr, types: TypeDef[], funcSigs: FuncSigInfo): numb } // Structured blocks: their external stack effect is determined by blockType - if (op === "if" || op === "block" || op === "loop" || op === "try") { + if (op === "if" || op === "block" || op === "loop" || op === "try" || op === "try_table") { const bt = (instr as any).blockType as BlockType; if (!bt || bt.kind === "empty") { // if also pops the condition (1 value) @@ -815,7 +815,7 @@ function inferLastType(body: Instr[], types: TypeDef[], sigs: FuncSigInfo): stri } // Structured blocks: result is their blockType - if (op === "if" || op === "block" || op === "loop" || op === "try") { + if (op === "if" || op === "block" || op === "loop" || op === "try" || op === "try_table") { const bt = (instr as any).blockType as BlockType; if (bt?.kind === "val") { const t = bt.type; @@ -1127,7 +1127,7 @@ function fixBody( ifInstr.else = []; fixups += fixBranch(ifInstr.else, expected, types, sigs, ifInstr.blockType, boxNumberIdx, unboxNumberIdx); } - } else if (instr.op === "block" || instr.op === "loop") { + } else if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { const blockInstr = instr as { op: string; blockType: BlockType; body: Instr[] }; fixups += fixBody(blockInstr.body, types, sigs, tags, boxNumberIdx, unboxNumberIdx); @@ -1403,7 +1403,7 @@ export function inferInstrType( return { kind: "anyref" } as ValType; } // Compound instructions (if/block/loop/try) produce a value based on blockType - if (op === "if" || op === "block" || op === "loop" || op === "try") { + if (op === "if" || op === "block" || op === "loop" || op === "try" || op === "try_table") { const bt = (instr as any).blockType as BlockType | undefined; if (bt && bt.kind === "val") return bt.type; if (bt && bt.kind === "type") { @@ -1686,7 +1686,7 @@ function fixCallArgTypesInBody( boxNumberIdx, unboxNumberIdx, ); - } else if (instr.op === "block" || instr.op === "loop") { + } else if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { const blockInstr = instr as any; if (blockInstr.body) fixups += fixCallArgTypesInBody( @@ -1803,6 +1803,7 @@ function fixCallArgTypesInBody( op === "block" || op === "loop" || op === "try" || + op === "try_table" || op === "end" || op === "br" || op === "br_if" || @@ -1958,7 +1959,7 @@ function fixStructNewFieldCoercion( const ifInstr = instr as any; if (ifInstr.then) processBody(ifInstr.then); if (ifInstr.else) processBody(ifInstr.else); - } else if (instr.op === "block" || instr.op === "loop") { + } else if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { const blockInstr = instr as any; if (blockInstr.body) processBody(blockInstr.body); } else if (instr.op === "try") { @@ -2477,7 +2478,7 @@ function updateTypeStack( } // Structured blocks: external effect based on blockType - if (op === "if" || op === "block" || op === "loop" || op === "try") { + if (op === "if" || op === "block" || op === "loop" || op === "try" || op === "try_table") { const bt = (instr as any).blockType as BlockType | undefined; if (op === "if") stack.pop(); // condition @@ -2752,7 +2753,7 @@ function fixLocalSetCoercion( boxNumberIdx, unboxNumberIdx, ); - } else if (instr.op === "block" || instr.op === "loop") { + } else if (instr.op === "block" || instr.op === "loop" || instr.op === "try_table") { const blockInstr = instr as any; if (blockInstr.body) fixups += fixLocalSetCoercion( diff --git a/src/codegen/statements/exceptions.ts b/src/codegen/statements/exceptions.ts index 2d2de7581a..9e0285a372 100644 --- a/src/codegen/statements/exceptions.ts +++ b/src/codegen/statements/exceptions.ts @@ -19,6 +19,7 @@ import { import { emitExternrefDestructureGuard } from "../destructuring-params.js"; import { collectBindingNames } from "./loop-analysis.js"; import { adjustRethrowDepth, restoreBlockScopedShadows, saveBlockScopedShadows } from "./shared.js"; +import { buildStandardTryTable } from "../../ir/try-table.js"; type BoxedCapture = { refCellTypeIdx: number; valType: ValType }; @@ -131,7 +132,7 @@ function bumpOuterBranchDepths(instrs: Instr[], outerDepths: Set, delta: // with the canonical traversal. Each of these container ops introduces // exactly one Wasm label, so any branch found inside a child array is one // level deeper than `instr` itself — hence `localDepth + 1`. - const isLabelOp = op === "block" || op === "loop" || op === "if" || op === "try"; + const isLabelOp = op === "block" || op === "loop" || op === "if" || op === "try" || op === "try_table"; const childLocalDepth = isLabelOp ? localDepth + 1 : localDepth; walkChildren(instr, (children) => bumpOuterBranchDepths(children, outerDepths, delta, childLocalDepth)); } @@ -294,6 +295,12 @@ export function compileThrowStatement(ctx: CodegenContext, fctx: FunctionContext for (let i = fctx.catchRethrowStack.length - 1; i >= 0; i--) { const entry = fctx.catchRethrowStack[i]!; if (entry.varName === thrownName) { + if ((ctx.wasi || ctx.standalone) && entry.exnLocalIdx !== undefined) { + const tagIdx = ensureExnTag(ctx); + fctx.body.push({ op: "local.get", index: entry.exnLocalIdx }); + fctx.body.push({ op: "throw", tagIdx }); + return; + } fctx.body.push({ op: "rethrow", depth: entry.depth } as any); return; } @@ -324,6 +331,7 @@ export function compileThrowStatement(ctx: CodegenContext, fctx: FunctionContext export function compileTryStatement(ctx: CodegenContext, fctx: FunctionContext, stmt: ts.TryStatement): void { const tagIdx = ensureExnTag(ctx); + const standardizedEh = ctx.wasi || ctx.standalone; // Pre-compile the finally body once so we can clone it into each // control-flow path instead of re-compiling the TS statements 2-5 times. @@ -462,9 +470,18 @@ export function compileTryStatement(ctx: CodegenContext, fctx: FunctionContext, // that runs the finally block and then rethrows the exception. if (finallyInstrs && !stmt.catchClause) { fctx.body = []; - fctx.body.push(...cloneFinally()); - fctx.body.push({ op: "rethrow", depth: 0 } as any); - catchAllBody = fctx.body; + if (standardizedEh) { + const finallyExnLocal = allocLocal(fctx, `__finally_exn_${fctx.locals.length}`, { kind: "externref" }); + fctx.body.push({ op: "local.set", index: finallyExnLocal }); + fctx.body.push(...cloneFinally()); + fctx.body.push({ op: "local.get", index: finallyExnLocal }); + fctx.body.push({ op: "throw", tagIdx }); + catches = [{ tagIdx, body: fctx.body }]; + } else { + fctx.body.push(...cloneFinally()); + fctx.body.push({ op: "rethrow", depth: 0 } as any); + catchAllBody = fctx.body; + } } if (stmt.catchClause) { @@ -517,7 +534,11 @@ export function compileTryStatement(ctx: CodegenContext, fctx: FunctionContext, const rethrowEligible = catchVarName !== undefined && !catchVarIsReassigned(stmt.catchClause.block, catchVarName); if (catchVarName && rethrowEligible) { if (!fctx.catchRethrowStack) fctx.catchRethrowStack = []; - fctx.catchRethrowStack.push({ varName: catchVarName, depth: 0 }); + fctx.catchRethrowStack.push({ + varName: catchVarName, + depth: 0, + ...(standardizedEh && exnLocalIdx !== null ? { exnLocalIdx } : {}), + }); } if (finallyInstrs) { @@ -615,15 +636,29 @@ export function compileTryStatement(ctx: CodegenContext, fctx: FunctionContext, // The cloned finally inside the inner catch_all is at +2 depth relative // to the original outer context (outer try +1, inner try +1), but the // pre-compiled finallyInstrs targets +1. Bump outer branch depths by +1. - const innerCatchAllBody: Instr[] = [...cloneFinallyAtDepth(1), { op: "rethrow", depth: 0 } as any]; - - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: catchBodyInstrs, - catches: [], - catchAll: innerCatchAllBody, - } as any); + if (standardizedEh) { + const innerExnLocal = allocLocal(fctx, `__finally_exn_${fctx.locals.length}`, { kind: "externref" }); + const innerHandler: Instr[] = [ + { op: "local.set", index: innerExnLocal }, + ...cloneFinallyAtDepth(1), + { op: "local.get", index: innerExnLocal }, + { op: "throw", tagIdx }, + ]; + fctx.body.push( + buildStandardTryTable({ kind: "empty" }, catchBodyInstrs, [ + { kind: "catch", tagIdx, payloadType: { kind: "externref" }, body: innerHandler }, + ]), + ); + } else { + const innerCatchAllBody: Instr[] = [...cloneFinallyAtDepth(1), { op: "rethrow", depth: 0 } as any]; + fctx.body.push({ + op: "try", + blockType: { kind: "empty" }, + body: catchBodyInstrs, + catches: [], + catchAll: innerCatchAllBody, + } as any); + } // Finally on normal exit path (no exception in catch body) — at +1 depth // (the outer try frame), so use the as-compiled clone. @@ -722,14 +757,31 @@ export function compileTryStatement(ctx: CodegenContext, fctx: FunctionContext, if (fctx.generatorReturnDepth !== undefined) fctx.generatorReturnDepth--; adjustRethrowDepth(fctx, -1); - // Emit the try instruction with catch $tag + catch_all - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: tryBody, - catches, - catchAll: catchAllBody, - }); + // Embedded standalone runtimes implement the standardized EH proposal. + // Keep the legacy structured encoding byte-inert for JS-host output. + if (standardizedEh) { + if (catchAllBody) throw new Error("standalone try_table lowering cannot retain a legacy catch_all body"); + fctx.body.push( + buildStandardTryTable( + { kind: "empty" }, + tryBody, + catches.map((c) => ({ + kind: "catch" as const, + tagIdx: c.tagIdx, + payloadType: { kind: "externref" as const }, + body: c.body, + })), + ), + ); + } else { + fctx.body.push({ + op: "try", + blockType: { kind: "empty" }, + body: tryBody, + catches, + catchAll: catchAllBody, + }); + } } /** Compile a function declaration nested inside another function. diff --git a/src/codegen/statements/loops.ts b/src/codegen/statements/loops.ts index 6f4fa3c19b..c0a4c68f78 100644 --- a/src/codegen/statements/loops.ts +++ b/src/codegen/statements/loops.ts @@ -69,6 +69,7 @@ import { varCounterRedeclarationBlocksI32, } from "./loop-analysis.js"; import { emitForAwaitElementUnwrap, emitForAwaitStepCapCheck } from "./for-await-helpers.js"; +import { buildStandardTryTable } from "../../ir/try-table.js"; import { compileForOfAssignDestructuring, compileForOfDestructuring, @@ -2518,31 +2519,64 @@ function compileForOfDirectIterator( // rejection reaches the user catch. Per §7.4.6 step 6, an error thrown by // `return()` itself is suppressed (the original throw wins) — hence the // empty inner catch_all. Mirrors the __iterator path's #1347 wrapper. - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [blockLoopInstr], - catches: [], - catchAll: [ - { op: "local.get", index: doneFlagDirect }, - { op: "i32.eqz" }, - { - op: "if", - blockType: { kind: "empty" }, - then: [ + const catchBodyPrefix: Instr[] = [ + { op: "local.get", index: doneFlagDirect }, + { op: "i32.eqz" }, + { + op: "if", + blockType: { kind: "empty" }, + then: [ + ctx.wasi || ctx.standalone + ? buildStandardTryTable({ kind: "empty" }, closeCallInstrs(), [ + { + kind: "catch", + tagIdx: ensureExnTag(ctx), + payloadType: { kind: "externref" }, + body: [{ op: "drop" }], + }, + ]) + : { + op: "try", + blockType: { kind: "empty" }, + body: closeCallInstrs(), + catches: [], + catchAll: [], // suppress return() errors per §7.4.6 step 6 + }, + ], + else: [], + }, + ]; + if (ctx.wasi || ctx.standalone) { + const tagIdx = ensureExnTag(ctx); + const exnLocal = allocLocal(fctx, `__forawait_close_exn_${fctx.locals.length}`, { kind: "externref" }); + fctx.body.push( + buildStandardTryTable( + { kind: "empty" }, + [blockLoopInstr], + [ { - op: "try", - blockType: { kind: "empty" }, - body: closeCallInstrs(), - catches: [], - catchAll: [], // suppress return() errors per §7.4.6 step 6 + kind: "catch", + tagIdx, + payloadType: { kind: "externref" }, + body: [ + { op: "local.set", index: exnLocal }, + ...catchBodyPrefix, + { op: "local.get", index: exnLocal }, + { op: "throw", tagIdx }, + ], }, ], - else: [], - }, - { op: "rethrow", depth: 0 }, - ], - }); + ), + ); + } else { + fctx.body.push({ + op: "try", + blockType: { kind: "empty" }, + body: [blockLoopInstr], + catches: [], + catchAll: [...catchBodyPrefix, { op: "rethrow", depth: 0 }], + }); + } } else { fctx.body.push(blockLoopInstr); } @@ -2938,17 +2972,28 @@ function compileForOfIterator(ctx: CodegenContext, fctx: FunctionContext, stmt: // wrapping the inner __iterator_return call in a nested try/catch_all // whose catchAll is empty (drops any exception). The outer catch_all // then `rethrow 0` re-raises the ORIGINAL exception. (#1347) - const innerCloseTry: Instr = { - op: "try", - blockType: { kind: "empty" }, - body: [ - { op: "local.get", index: iterLocal }, - { op: "call", funcIdx: returnIdx }, - ], - catches: [], - catchAll: [], // suppress any error from GetMethod / return() per spec step 6 - }; - const catchAllBody: Instr[] = [ + const closeBody: Instr[] = [ + { op: "local.get", index: iterLocal }, + { op: "call", funcIdx: returnIdx }, + ]; + const innerCloseTry: Instr = + ctx.wasi || ctx.standalone + ? buildStandardTryTable({ kind: "empty" }, closeBody, [ + { + kind: "catch", + tagIdx: ensureExnTag(ctx), + payloadType: { kind: "externref" }, + body: [{ op: "drop" }], + }, + ]) + : { + op: "try", + blockType: { kind: "empty" }, + body: closeBody, + catches: [], + catchAll: [], // suppress any error from GetMethod / return() per spec step 6 + }; + const closeOnThrowBody: Instr[] = [ { op: "local.get", index: doneFlag }, { op: "i32.eqz" }, { @@ -2957,15 +3002,38 @@ function compileForOfIterator(ctx: CodegenContext, fctx: FunctionContext, stmt: then: [innerCloseTry], else: [], }, - { op: "rethrow", depth: 0 }, ]; - fctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [blockLoopInstr], - catches: [], - catchAll: catchAllBody, - }); + if (ctx.wasi || ctx.standalone) { + const tagIdx = ensureExnTag(ctx); + const exnLocal = allocLocal(fctx, `__iterator_close_exn_${fctx.locals.length}`, { kind: "externref" }); + fctx.body.push( + buildStandardTryTable( + { kind: "empty" }, + [blockLoopInstr], + [ + { + kind: "catch", + tagIdx, + payloadType: { kind: "externref" }, + body: [ + { op: "local.set", index: exnLocal }, + ...closeOnThrowBody, + { op: "local.get", index: exnLocal }, + { op: "throw", tagIdx }, + ], + }, + ], + ), + ); + } else { + fctx.body.push({ + op: "try", + blockType: { kind: "empty" }, + body: [blockLoopInstr], + catches: [], + catchAll: [...closeOnThrowBody, { op: "rethrow", depth: 0 }], + }); + } } else { fctx.body.push(blockLoopInstr); } diff --git a/src/codegen/statements/nested-declarations.ts b/src/codegen/statements/nested-declarations.ts index 7a543addab..81f9091220 100644 --- a/src/codegen/statements/nested-declarations.ts +++ b/src/codegen/statements/nested-declarations.ts @@ -54,6 +54,7 @@ import { } from "../index.js"; import { emitAsyncGenerator, isAsyncGenDriveCandidate } from "../async-frame.js"; // (#2865) nested async-gen producer import { ensureExnTag, nextModuleGlobalIdx } from "../registry/imports.js"; +import { buildTargetTaggedTry } from "../../ir/try-table.js"; import { addFuncType, getArrTypeIdxFromVec, @@ -1098,13 +1099,15 @@ function compileNestedFunctionDeclarationInScope( { op: "local.set", index: pendingThrowLocal }, ] : []; - liftedFctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], - catches: [{ tagIdx, body: catchBody }], - catchAll: catchAllBody.length > 0 ? catchAllBody : undefined, - }); + liftedFctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], + [{ tagIdx, body: catchBody }], + catchAllBody.length > 0 ? catchAllBody : undefined, + ), + ); // Return __create_generator or __create_async_generator depending on async flag const createGenName = isAsync ? "__create_async_generator" : "__create_generator"; @@ -1570,13 +1573,15 @@ function compileNestedFunctionDeclarationInScope( { op: "local.set", index: pendingThrowLocal }, ] : []; - liftedFctx.body.push({ - op: "try", - blockType: { kind: "empty" }, - body: [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], - catches: [{ tagIdx, body: catchBody }], - catchAll: catchAllBody.length > 0 ? catchAllBody : undefined, - }); + liftedFctx.body.push( + buildTargetTaggedTry( + ctx, + { kind: "empty" }, + [{ op: "block", blockType: { kind: "empty" }, body: bodyInstrs }], + [{ tagIdx, body: catchBody }], + catchAllBody.length > 0 ? catchAllBody : undefined, + ), + ); // Return __create_generator or __create_async_generator depending on async flag const createGenName = isAsync ? "__create_async_generator" : "__create_generator"; diff --git a/src/codegen/statements/variables.ts b/src/codegen/statements/variables.ts index a2d7b78f1a..8980e5afd3 100644 --- a/src/codegen/statements/variables.ts +++ b/src/codegen/statements/variables.ts @@ -968,7 +968,7 @@ const TA_VIEW_CTOR_NAMES = new Set([ * `new TA(buf, byteOffset[, length])` also resolves to a `$__ta_view` (with the * byteOffset field populated), so 1..3 args are accepted here. */ -function inferTaViewType(ctx: CodegenContext, initializer: ts.Expression | undefined): ValType | null { +export function inferTaViewType(ctx: CodegenContext, initializer: ts.Expression | undefined): ValType | null { if (!initializer) return null; const unwrapped = stripInferenceWrapper(initializer); if (!ts.isNewExpression(unwrapped) || !ts.isIdentifier(unwrapped.expression)) return null; diff --git a/src/codegen/stdlib-selfhost.ts b/src/codegen/stdlib-selfhost.ts index a8bfc8445c..cb1ffc0424 100644 --- a/src/codegen/stdlib-selfhost.ts +++ b/src/codegen/stdlib-selfhost.ts @@ -82,6 +82,7 @@ import type { StdlibMathBuiltin } from "../stdlib/math.js"; import type { CodegenContext } from "./context/types.js"; import { addFuncType } from "./registry/types.js"; import { mintDefinedFunc, nativeStrHelperHandle, pushDefinedFunc } from "./func-space.js"; +import { ensureExnTag } from "./registry/imports.js"; const F64: IrType = irVal({ kind: "f64" }); @@ -638,6 +639,12 @@ function lowerAndRegister(ctx: CodegenContext, name: string, ir: IrFunction): nu nativeStrings(): boolean { return ctx.nativeStrings; }, + ensureExnTag(): number { + return ensureExnTag(ctx); + }, + standardizedExceptions(): boolean { + return ctx.standalone || ctx.wasi; + }, resolveString(): ValType { if (ctx.nativeStrings && ctx.anyStrTypeIdx >= 0) { return { kind: "ref", typeIdx: ctx.anyStrTypeIdx }; diff --git a/src/emit/binary.ts b/src/emit/binary.ts index 4a336e7868..0983db1e86 100644 --- a/src/emit/binary.ts +++ b/src/emit/binary.ts @@ -1573,6 +1573,40 @@ export function encodeInstr(instr: Instr, enc: WasmEncoder): void { enc.byte(OP.rethrow); enc.u32(instr.depth); break; + case "try_table": { + enc.byte(OP.try_table); + encodeBlockType(instr.blockType, enc); + enc.u32(instr.catches.length); + for (const clause of instr.catches) { + switch (clause.kind) { + case "catch": + enc.byte(0x00); + if (clause.tagIdx === undefined) throw new Error("try_table catch is missing a tag index"); + if (valCtx) vIdx("exception tag", clause.tagIdx, valCtx.numTags); + enc.u32(clause.tagIdx); + enc.u32(clause.depth); + break; + case "catch_ref": + enc.byte(0x01); + if (clause.tagIdx === undefined) throw new Error("try_table catch_ref is missing a tag index"); + if (valCtx) vIdx("exception tag", clause.tagIdx, valCtx.numTags); + enc.u32(clause.tagIdx); + enc.u32(clause.depth); + break; + case "catch_all": + enc.byte(0x02); + enc.u32(clause.depth); + break; + case "catch_all_ref": + enc.byte(0x03); + enc.u32(clause.depth); + break; + } + } + for (const i of instr.body) encodeInstr(i, enc); + enc.byte(OP.end); + break; + } case "try": { enc.byte(OP.try); encodeBlockType(instr.blockType, enc); diff --git a/src/emit/object.ts b/src/emit/object.ts index 3bf46fc823..2ca28836ca 100644 --- a/src/emit/object.ts +++ b/src/emit/object.ts @@ -924,6 +924,43 @@ function encodeInstrWithReloc( enc.byte(OP.rethrow); enc.u32(instr.depth); break; + case "try_table": { + enc.byte(OP.try_table); + encodeBlockType(instr.blockType, enc); + enc.u32(instr.catches.length); + for (const clause of instr.catches) { + switch (clause.kind) { + case "catch": + case "catch_ref": { + enc.byte(clause.kind === "catch" ? 0x00 : 0x01); + if (clause.tagIdx === undefined) throw new Error(`try_table ${clause.kind} is missing a tag index`); + const symIdx = tagIdxToSymIdx.get(clause.tagIdx); + if (symIdx !== undefined) { + relocs.push({ + type: RELOC.R_WASM_TAG_INDEX_LEB, + offset: enc.position, + symbolIndex: symIdx, + }); + } + enc.u32(clause.tagIdx); + enc.u32(clause.depth); + break; + } + case "catch_all": + enc.byte(0x02); + enc.u32(clause.depth); + break; + case "catch_all_ref": + enc.byte(0x03); + enc.u32(clause.depth); + break; + } + } + for (const i of instr.body) + encodeInstrWithReloc(i, enc, relocs, 0, funcIdxToSymIdx, globalIdxToSymIdx, tagIdxToSymIdx); + enc.byte(OP.end); + break; + } case "try": { enc.byte(OP.try); encodeBlockType(instr.blockType, enc); diff --git a/src/emit/opcodes.ts b/src/emit/opcodes.ts index 47981e6e2f..d229800b91 100644 --- a/src/emit/opcodes.ts +++ b/src/emit/opcodes.ts @@ -23,6 +23,7 @@ export const OP = { catch_all: 0x19, drop: 0x1a, select: 0x1b, + try_table: 0x1f, local_get: 0x20, local_set: 0x21, local_tee: 0x22, diff --git a/src/emit/wat.ts b/src/emit/wat.ts index f1c397d507..f9d5631bfe 100644 --- a/src/emit/wat.ts +++ b/src/emit/wat.ts @@ -380,6 +380,22 @@ function formatInstrIndented(instr: Instr, depth: number): string { result += `\n${pad})`; return result; } + case "try_table": { + const bt = formatBlockType(instr.blockType); + let result = `${pad}(try_table${bt}`; + for (const clause of instr.catches) { + if (clause.kind === "catch" || clause.kind === "catch_ref") { + result += ` (${clause.kind} ${clause.tagIdx} ${clause.depth})`; + } else { + result += ` (${clause.kind} ${clause.depth})`; + } + } + if (instr.body.length > 0) { + result += `\n${instr.body.map((i) => formatInstrIndented(i, depth + 1)).join("\n")}\n${pad}`; + } + result += ")"; + return result; + } default: return `${pad}${formatInstr(instr, depth)}`; } diff --git a/src/ir/backend/wasmgc-emitter.ts b/src/ir/backend/wasmgc-emitter.ts index dcee02429b..d5cf2f85da 100644 --- a/src/ir/backend/wasmgc-emitter.ts +++ b/src/ir/backend/wasmgc-emitter.ts @@ -27,6 +27,7 @@ import { } from "../nodes.js"; import type { IrStringConcatMode, IrStringEncoding } from "../string-runtime.js"; import type { BlockType, Instr, ValType } from "../types.js"; +import { buildStandardTryTable } from "../try-table.js"; import type { BackendEmitter, BackendI32BitwiseOp, @@ -431,6 +432,46 @@ export class WasmGcEmitter implements BackendEmitter { catchAll: Instr[] | undefined, out: Instr[], ): void { + if (this.stringRuntime?.standardizedExceptions?.()) { + if (catches.length > 0) { + out.push( + buildStandardTryTable( + blockType, + body, + catches.map((clause) => ({ + kind: "catch", + tagIdx: clause.tagIdx, + payloadType: { kind: "externref" }, + body: clause.body, + })), + ), + ); + return; + } + + if (!catchAll) { + throw new Error("WasmGcEmitter: standardized try requires a handler"); + } + const tagIdx = this.stringRuntime.ensureExnTag?.(); + if (tagIdx === undefined) { + throw new Error("WasmGcEmitter: standardized try requires the shared exception tag"); + } + const handler = [...catchAll]; + const terminal = handler[handler.length - 1]; + if (!terminal || terminal.op !== "rethrow" || terminal.depth !== 0) { + throw new Error("WasmGcEmitter: standardized finally handler must end in rethrow 0"); + } + // The handler block result leaves the caught externref on the operand + // stack beneath the balanced finally body. Replace legacy rethrow with + // a throw of that same payload through the module's shared tag. + handler[handler.length - 1] = { op: "throw", tagIdx }; + out.push( + buildStandardTryTable(blockType, body, [ + { kind: "catch", tagIdx, payloadType: { kind: "externref" }, body: handler }, + ]), + ); + return; + } out.push({ op: "try", blockType, diff --git a/src/ir/integration.ts b/src/ir/integration.ts index 3900a94433..478b65be9f 100644 --- a/src/ir/integration.ts +++ b/src/ir/integration.ts @@ -5342,6 +5342,9 @@ function makeResolver( } return ctx.exnTagIdx; }, + standardizedExceptions(): boolean { + return ctx.standalone || ctx.wasi; + }, // ------------------------------------------------------------------- // Async / Promise dispatch (#1373b Slice 1). // diff --git a/src/ir/lower.ts b/src/ir/lower.ts index 9af169d996..bec9de7f9b 100644 --- a/src/ir/lower.ts +++ b/src/ir/lower.ts @@ -291,6 +291,12 @@ export interface IrLowerResolver { * through the same single tag. */ ensureExnTag?(): number; + /** + * True for no-JavaScript-host targets that use the standardized + * `try_table` exception proposal. Host `gc` output keeps the legacy + * `try`/`catch` encoding for compatibility with JavaScript engines. + */ + standardizedExceptions?(): boolean; /** * #1373b Phase C scaffolding — resolve (and lazily register) the * standalone `$Promise` WasmGC struct type. The struct's layout is diff --git a/src/ir/try-table.ts b/src/ir/try-table.ts new file mode 100644 index 0000000000..b90ff3dd6d --- /dev/null +++ b/src/ir/try-table.ts @@ -0,0 +1,148 @@ +// Copyright (c) 2026 Loopdive GmbH. Licensed under Apache-2.0 WITH LLVM-exception. + +import type { BlockType, Instr, TryTableCatch, ValType } from "./types.js"; + +function walkChildren(instr: Instr, visit: (children: Instr[]) => void): void { + const nested = instr as Instr & { + body?: Instr[]; + then?: Instr[]; + else?: Instr[]; + catches?: { body: Instr[] }[]; + catchAll?: Instr[]; + }; + if (Array.isArray(nested.body)) visit(nested.body); + if (Array.isArray(nested.then)) visit(nested.then); + if (Array.isArray(nested.else)) visit(nested.else); + for (const clause of nested.catches ?? []) { + if (Array.isArray(clause.body)) visit(clause.body); + } + if (Array.isArray(nested.catchAll)) visit(nested.catchAll); +} + +/** One handler in source/legacy selection order. */ +export interface StandardEhHandler { + kind: "catch" | "catch_all"; + tagIdx?: number; + /** Values delivered to the handler block by the catch branch. */ + payloadType?: ValType; + body: Instr[]; +} + +function isLabelOp(op: string): boolean { + return op === "block" || op === "loop" || op === "if" || op === "try" || op === "try_table"; +} + +/** + * Retarget branches after standardized-EH handler blocks are inserted. + * + * Legacy `try` contributes one label. In a `try_table` body that label remains + * the `try_table` label, while the synthesized handler blocks plus join block + * sit between it and every outer target. In a handler body the legacy try + * label itself becomes the synthesized join block. + */ +function bumpBranches(instrs: Instr[], delta: number, includeLegacyTryLabel: boolean, localDepth = 0): void { + if (delta === 0) return; + for (const instr of instrs) { + const op = instr.op; + const targetsLegacyTryOrOuter = (depth: number): boolean => + includeLegacyTryLabel ? depth >= localDepth : depth > localDepth; + + if (op === "br" || op === "br_if") { + if (targetsLegacyTryOrOuter(instr.depth)) instr.depth += delta; + } else if (op === "br_table") { + for (let i = 0; i < instr.targets.length; i++) { + if (targetsLegacyTryOrOuter(instr.targets[i]!)) instr.targets[i] = instr.targets[i]! + delta; + } + if (targetsLegacyTryOrOuter(instr.defaultDepth)) instr.defaultDepth += delta; + } + + const childDepth = isLabelOp(op) ? localDepth + 1 : localDepth; + walkChildren(instr, (children) => bumpBranches(children, delta, includeLegacyTryLabel, childDepth)); + } +} + +/** + * Build the standardized `try_table` control-flow scaffold from handler bodies + * that were compiled for one legacy `try` label. + * + * The result is ordinary block IR around a raw `try_table` instruction: + * + * block $join + * block $handlerN + * ... block $handler0 + * try_table (catch ... $handler0) ... + * br $join + * end + * handler0; br $join + * end + * handlerN; br $join + * end + * + * Keeping the synthesized blocks in IR (rather than hiding them in the binary + * encoder) lets all existing control-flow, stack, liveness, and fixup passes + * observe the real label depths. + */ +export function buildStandardTryTable(blockType: BlockType, body: Instr[], handlers: StandardEhHandler[]): Instr { + if (handlers.length === 0) { + throw new Error("try_table requires at least one catch handler"); + } + + const handlerCount = handlers.length; + // The try_table label replaces the legacy try label. Only branches escaping + // that label cross the N handler blocks plus the join block. + bumpBranches(body, handlerCount + 1, false); + + const catches: TryTableCatch[] = handlers.map((handler, depth) => ({ + kind: handler.kind, + ...(handler.tagIdx === undefined ? {} : { tagIdx: handler.tagIdx }), + depth, + })); + + let nested: Instr[] = [ + { op: "try_table", blockType, body, catches }, + { op: "br", depth: handlerCount }, + ]; + + // Handler 0 is the innermost block and therefore catch depth 0. Each handler + // runs just after its own target block closes, still enclosed by the + // remaining handler blocks and the join block. + for (let i = 0; i < handlerCount; i++) { + const handler = handlers[i]!; + const remainingHandlerBlocks = handlerCount - i - 1; + bumpBranches(handler.body, remainingHandlerBlocks, true); + nested = [ + { + op: "block", + blockType: handler.payloadType ? { kind: "val", type: handler.payloadType } : { kind: "empty" }, + body: nested, + }, + ...handler.body, + { op: "br", depth: remainingHandlerBlocks }, + ]; + } + + return { op: "block", blockType, body: nested }; +} + +/** Preserve legacy host output while selecting standardized EH for no-JS-host targets. */ +export function buildTargetTaggedTry( + target: { wasi: boolean; standalone: boolean }, + blockType: BlockType, + body: Instr[], + catches: { tagIdx: number; body: Instr[] }[], + catchAll?: Instr[], +): Instr { + if (!target.wasi && !target.standalone) { + return { op: "try", blockType, body, catches, ...(catchAll === undefined ? {} : { catchAll }) }; + } + return buildStandardTryTable( + blockType, + body, + catches.map((clause) => ({ + kind: "catch", + tagIdx: clause.tagIdx, + payloadType: { kind: "externref" }, + body: clause.body, + })), + ); +} diff --git a/src/ir/types.ts b/src/ir/types.ts index 448e163b63..3da02cefdd 100644 --- a/src/ir/types.ts +++ b/src/ir/types.ts @@ -424,6 +424,7 @@ type InstrBase = | { op: "memory.copy" } | { op: "memory.fill" } | { op: "try"; blockType: BlockType; body: Instr[]; catches: CatchClause[]; catchAll?: Instr[] } + | { op: "try_table"; blockType: BlockType; body: Instr[]; catches: TryTableCatch[] } | { op: "throw"; tagIdx: number } | { op: "rethrow"; depth: number } | { op: "any.convert_extern" } @@ -560,6 +561,15 @@ export interface CatchClause { body: Instr[]; } +/** A standardized exception-handling clause attached to `try_table`. */ +export interface TryTableCatch { + kind: "catch" | "catch_ref" | "catch_all" | "catch_all_ref"; + /** Present for the tagged `catch` / `catch_ref` forms. */ + tagIdx?: number; + /** Relative label depth of the enclosing handler block. */ + depth: number; +} + export interface TagDef { name: string; /** Type index of the tag's function signature (params = exception values) */ diff --git a/tests/fixtures/deno-core-0.407.0/00_infra.js b/tests/fixtures/deno-core-0.407.0/00_infra.js new file mode 100644 index 0000000000..244d09ddf3 --- /dev/null +++ b/tests/fixtures/deno-core-0.407.0/00_infra.js @@ -0,0 +1,548 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +"use strict"; + +((window) => { + const { + Array, + ArrayPrototypeFill, + Error, + ErrorCaptureStackTrace, + MapPrototypeDelete, + MapPrototypeGet, + MapPrototypeHas, + MapPrototypeSet, + ObjectAssign, + ObjectDefineProperty, + ObjectFreeze, + Promise, + PromiseReject, + PromiseResolve, + PromisePrototypeCatch, + RangeError, + ReferenceError, + SafeArrayIterator, + SafeMap, + StringPrototypeSplit, + SymbolFor, + SyntaxError, + TypeError, + URIError, + } = window.__bootstrap.primordials; + + // Promise ids must be non-negative and fit in an i32 (Rust's `PromiseId`); + // the increment in the async op stubs wraps back to 0 past 2^31 - 1. + let nextPromiseId = 0; + const promiseMap = new SafeMap(); + const RING_SIZE = 4 * 1024; + const NO_PROMISE = null; // Alias to null is faster than plain nulls + const promiseRing = ArrayPrototypeFill(new Array(RING_SIZE), NO_PROMISE); + // TODO(bartlomieju): in the future use `v8::Private` so it's not visible + // to users. Currently missing bindings. + const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); + + let isLeakTracingEnabled = false; + let submitLeakTrace; + + // Exposed for testing promise id wraparound behavior. + function __setNextPromiseId(promiseId) { + nextPromiseId = promiseId; + } + + function __setLeakTracingEnabled(enabled) { + isLeakTracingEnabled = enabled; + } + + function __isLeakTracingEnabled() { + return isLeakTracingEnabled; + } + + function __initializeCoreMethods(submitLeakTrace_) { + submitLeakTrace = submitLeakTrace_; + } + + const build = { + target: "unknown", + arch: "unknown", + os: "unknown", + vendor: "unknown", + env: undefined, + }; + + function setBuildInfo(target) { + const { 0: arch, 1: vendor, 2: os, 3: env } = StringPrototypeSplit( + target, + "-", + 4, + ); + build.target = target; + build.arch = arch; + build.vendor = vendor; + build.os = os; + build.env = env; + ObjectFreeze(build); + } + + const errorMap = {}; + // Maps a registered error class name to its constructor. Unlike `errorMap` + // (which stores builder closures), this keeps a reference to the class itself + // so the Rust side can restore the exact prototype when building an exception + // natively (e.g. from inside a V8 fast call, where re-entering JS to call + // `buildCustomError` is forbidden). Null prototype so class names can't + // collide with `Object.prototype` members. + const errorConstructors = { __proto__: null }; + // Builtin v8 / JS errors + registerErrorClass("Error", Error); + registerErrorClass("RangeError", RangeError); + registerErrorClass("ReferenceError", ReferenceError); + registerErrorClass("SyntaxError", SyntaxError); + registerErrorClass("TypeError", TypeError); + registerErrorClass("URIError", URIError); + + function buildCustomError(className, message, additionalProperties) { + let error; + try { + error = errorMap[className]?.(message); + } catch (e) { + throw new Error( + `Unable to build custom error for "${className}"\n ${e.message}`, + ); + } + // Strip buildCustomError() calls from stack trace + if (typeof error == "object") { + ErrorCaptureStackTrace(error, buildCustomError); + if (additionalProperties) { + const keys = []; + for (const property of new SafeArrayIterator(additionalProperties)) { + const key = property[0]; + if (!(key in error)) { + keys.push(key); + error[key] = property[1]; + } + } + Object.defineProperty(error, SymbolFor("errorAdditionalPropertyKeys"), { + value: keys, + writable: false, + enumerable: false, + configurable: false, + }); + } + } + return error; + } + + function registerErrorClass(className, errorClass) { + registerErrorBuilder(className, (msg) => new errorClass(msg)); + errorConstructors[className] = errorClass; + } + + function registerErrorBuilder(className, errorBuilder) { + if (typeof errorMap[className] !== "undefined") { + throw new TypeError(`Error class for "${className}" already registered`); + } + errorMap[className] = errorBuilder; + } + + function setPromise(promiseId) { + const idx = promiseId % RING_SIZE; + // Move old promise from ring to map + const oldPromise = promiseRing[idx]; + if (oldPromise !== NO_PROMISE) { + // Keyed on the entry's own id rather than `promiseId - RING_SIZE` so it + // stays correct across the id counter wrapping back to 0. The one + // unavoidable limit: if a single promise stays pending while 2^31 more + // ids are dispatched its id is reused, and this set silently overwrites + // (loses) the older map entry. Not a regression: the old code broke far + // sooner, and no real workload keeps an op pending across 2^31 dispatches. + MapPrototypeSet(promiseMap, oldPromise[2], oldPromise); + } + + const promise = new Promise((resolve, reject) => { + promiseRing[idx] = [resolve, reject, promiseId]; + }); + const wrappedPromise = PromisePrototypeCatch( + promise, + function __opRejectHandler(res) { + // recreate the stacktrace and strip internal event loop frames + ErrorCaptureStackTrace(res, __opRejectHandler); + throw res; + }, + ); + wrappedPromise[promiseIdSymbol] = promiseId; + return wrappedPromise; + } + + function __resolvePromise(promiseId, res, isOk) { + // A promise stays in the ring until its slot is reclaimed by a newer + // promise, at which point it moves to the map. The entry stores its own + // promise id, so a slot reused after the id counter wrapped around is + // never mistaken for an older promise that is now in the map. + const idx = promiseId % RING_SIZE; + let promise = promiseRing[idx]; + if (promise !== NO_PROMISE && promise[2] === promiseId) { + promiseRing[idx] = NO_PROMISE; + } else { + promise = MapPrototypeGet(promiseMap, promiseId); + if (!promise) { + throw "Missing promise @ " + promiseId; + } + MapPrototypeDelete(promiseMap, promiseId); + } + if (isOk) { + promise[0](res); + } else { + promise[1](res); + } + } + + function hasPromise(promiseId) { + const idx = promiseId % RING_SIZE; + // Loose comparison on purpose: `promiseId` can be `undefined` (e.g. + // `unrefOpPromise()` with a promise from an op that completed eagerly and + // has no promise id attached), making `idx` NaN and the ring lookup + // return `undefined` instead of the `NO_PROMISE` (null) sentinel. + const promise = promiseRing[idx]; + if (promise != NO_PROMISE && promise[2] === promiseId) { + return true; + } + return MapPrototypeHas(promiseMap, promiseId); + } + + function setUpAsyncStub(opName, originalOp, maybeProto) { + let fn; + + // The body of this switch statement can be generated using the script above. + switch (originalOp.length - 1) { + /* BEGIN TEMPLATE setUpAsyncStub */ + /* DO NOT MODIFY: use rebuild_async_stubs.js to regenerate */ + case 0: + fn = function async_op_0() { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_0); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 1: + fn = function async_op_1(a) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_1); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 2: + fn = function async_op_2(a, b) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_2); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 3: + fn = function async_op_3(a, b, c) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_3); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 4: + fn = function async_op_4(a, b, c, d) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c, d); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_4); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 5: + fn = function async_op_5(a, b, c, d, e) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c, d, e); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_5); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 6: + fn = function async_op_6(a, b, c, d, e, f) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c, d, e, f); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_6); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 7: + fn = function async_op_7(a, b, c, d, e, f, g) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c, d, e, f, g); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_7); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 8: + fn = function async_op_8(a, b, c, d, e, f, g, h) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c, d, e, f, g, h); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_8); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + case 9: + fn = function async_op_9(a, b, c, d, e, f, g, h, i) { + const id = nextPromiseId; + try { + // deno-fmt-ignore + const maybeResult = originalOp.call(this, id, a, b, c, d, e, f, g, h, i); + if (maybeResult !== undefined) { + return PromiseResolve(maybeResult); + } + } catch (err) { + ErrorCaptureStackTrace(err, async_op_9); + return PromiseReject(err); + } + if (isLeakTracingEnabled) { + submitLeakTrace(id); + } + nextPromiseId = (id + 1) & 0x7fffffff; + return setPromise(id); + }; + break; + /* END TEMPLATE */ + + default: + throw new Error( + `Too many arguments for async op codegen (length of ${opName} was ${ + originalOp.length - 1 + })`, + ); + } + ObjectDefineProperty(fn, "name", { + value: opName, + configurable: false, + writable: false, + }); + + if (maybeProto) { + ObjectDefineProperty(fn, "prototype", { + value: maybeProto.prototype, + configurable: false, + writable: false, + }); + maybeProto.prototype[opName] = fn; + } + + return fn; + } + + // --------------------------------------------------------------------------- + // FixedQueue: a singly-linked list of fixed-size circular buffers. + // Used by the nextTick queue (and available to other core subsystems). + // + // Closely mirrors Node.js lib/internal/fixed_queue.js. + // + // Copyright Joyent, Inc. and other Node contributors. + // + // Permission is hereby granted, free of charge, to any person obtaining a + // copy of this software and associated documentation files (the + // "Software"), to deal in the Software without restriction, including + // without limitation the rights to use, copy, modify, merge, publish, + // distribute, sublicense, and/or sell copies of the Software, and to permit + // persons to whom the Software is furnished to do so, subject to the + // following conditions: + // + // The above copyright notice and this permission notice shall be included + // in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + // USE OR OTHER DEALINGS IN THE SOFTWARE. + // --------------------------------------------------------------------------- + const kQueueSize = 2048; + const kQueueMask = kQueueSize - 1; + + class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kQueueSize); + this.next = null; + } + isEmpty() { + return this.top === this.bottom; + } + isFull() { + return ((this.top + 1) & kQueueMask) === this.bottom; + } + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kQueueMask; + } + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) return null; + this.list[this.bottom] = undefined; + this.bottom = (this.bottom + 1) & kQueueMask; + return nextItem; + } + } + + class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + isEmpty() { + return this.head.isEmpty(); + } + push(data) { + if (this.head.isFull()) { + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + this.tail = tail.next; + } + return next; + } + } + + // Extra Deno.core.* exports + const core = ObjectAssign(globalThis.Deno.core, { + build, + setBuildInfo, + registerErrorBuilder, + buildCustomError, + registerErrorClass, + errorConstructors, + setUpAsyncStub, + hasPromise, + promiseIdSymbol, + __setNextPromiseId, + }); + + const infra = { + __resolvePromise, + __setLeakTracingEnabled, + __isLeakTracingEnabled, + __initializeCoreMethods, + FixedQueue, + }; + + ObjectAssign(globalThis, { __infra: infra }); + ObjectAssign(globalThis.__bootstrap, { core }); + ObjectAssign(globalThis.Deno, { core }); +})(globalThis); diff --git a/tests/fixtures/deno-core-0.407.0/00_primordials.js b/tests/fixtures/deno-core-0.407.0/00_primordials.js new file mode 100644 index 0000000000..a1d838b22a --- /dev/null +++ b/tests/fixtures/deno-core-0.407.0/00_primordials.js @@ -0,0 +1,614 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +// Based on https://github.com/nodejs/node/blob/889ad35d3d41e376870f785b0c1b669cb732013d/lib/internal/per_context/primordials.js +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. +// This file subclasses and stores the JS builtins that come from the VM +// so that Node.js's builtin modules do not need to later look these up from +// the global proxy, which can be mutated by users. + +// Use of primordials have sometimes a dramatic impact on performance, please +// benchmark all changes made in performance-sensitive areas of the codebase. +// See: https://github.com/nodejs/node/pull/38248 + +// deno-lint-ignore-file prefer-primordials + +"use strict"; + +(() => { + const primordials = {}; + + const { + defineProperty: ReflectDefineProperty, + getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor, + ownKeys: ReflectOwnKeys, + } = Reflect; + + // `uncurryThis` is equivalent to `func => Function.prototype.call.bind(func)`. + // It is using `bind.bind(call)` to avoid using `Function.prototype.bind` + // and `Function.prototype.call` after it may have been mutated by users. + const { apply, bind, call } = Function.prototype; + const uncurryThis = bind.bind(call); + primordials.uncurryThis = uncurryThis; + + // `applyBind` is equivalent to `func => Function.prototype.apply.bind(func)`. + // It is using `bind.bind(apply)` to avoid using `Function.prototype.bind` + // and `Function.prototype.apply` after it may have been mutated by users. + const applyBind = bind.bind(apply); + primordials.applyBind = applyBind; + + // Methods that accept a variable number of arguments, and thus it's useful to + // also create `${prefix}${key}Apply`, which uses `Function.prototype.apply`, + // instead of `Function.prototype.call`, and thus doesn't require iterator + // destructuring. + const varargsMethods = [ + // 'ArrayPrototypeConcat' is omitted, because it performs the spread + // on its own for arrays and array-likes with a truthy + // @@isConcatSpreadable symbol property. + "ArrayOf", + "ArrayPrototypePush", + "ArrayPrototypeUnshift", + // 'FunctionPrototypeCall' is omitted, since there's 'ReflectApply' + // and 'FunctionPrototypeApply'. + "MathHypot", + "MathMax", + "MathMin", + "StringPrototypeConcat", + "TypedArrayOf", + ]; + + function getNewKey(key) { + return typeof key === "symbol" + ? `Symbol${key.description[7].toUpperCase()}${key.description.slice(8)}` + : `${key[0].toUpperCase()}${key.slice(1)}`; + } + + function copyAccessor(dest, prefix, key, { enumerable, get, set }) { + ReflectDefineProperty(dest, `${prefix}Get${key}`, { + value: uncurryThis(get), + enumerable, + }); + if (set !== undefined) { + ReflectDefineProperty(dest, `${prefix}Set${key}`, { + value: uncurryThis(set), + enumerable, + }); + } + } + + function copyPropsRenamed(src, dest, prefix) { + for (const key of ReflectOwnKeys(src)) { + const newKey = getNewKey(key); + const desc = ReflectGetOwnPropertyDescriptor(src, key); + if ("get" in desc) { + copyAccessor(dest, prefix, newKey, desc); + } else { + const name = `${prefix}${newKey}`; + ReflectDefineProperty(dest, name, desc); + if (varargsMethods.includes(name)) { + ReflectDefineProperty(dest, `${name}Apply`, { + // `src` is bound as the `this` so that the static `this` points + // to the object it was defined on, + // e.g.: `ArrayOfApply` gets a `this` of `Array`: + value: applyBind(desc.value, src), + }); + } + } + } + } + + function copyPropsRenamedBound(src, dest, prefix) { + for (const key of ReflectOwnKeys(src)) { + const newKey = getNewKey(key); + const desc = ReflectGetOwnPropertyDescriptor(src, key); + if ("get" in desc) { + copyAccessor(dest, prefix, newKey, desc); + } else { + const { value } = desc; + if (typeof value === "function") { + desc.value = value.bind(src); + } + + const name = `${prefix}${newKey}`; + ReflectDefineProperty(dest, name, desc); + if (varargsMethods.includes(name)) { + ReflectDefineProperty(dest, `${name}Apply`, { + value: applyBind(value, src), + }); + } + } + } + } + + function copyPrototype(src, dest, prefix) { + for (const key of ReflectOwnKeys(src)) { + const newKey = getNewKey(key); + const desc = ReflectGetOwnPropertyDescriptor(src, key); + if ("get" in desc) { + copyAccessor(dest, prefix, newKey, desc); + } else { + const { value } = desc; + if (typeof value === "function") { + desc.value = uncurryThis(value); + } + + const name = `${prefix}${newKey}`; + ReflectDefineProperty(dest, name, desc); + if (varargsMethods.includes(name)) { + ReflectDefineProperty(dest, `${name}Apply`, { + value: applyBind(value), + }); + } + } + } + } + + // Create copies of configurable value properties of the global object + [ + "Proxy", + "globalThis", + ].forEach((name) => { + primordials[name] = globalThis[name]; + }); + + // Create copy of isNaN + primordials[isNaN.name] = isNaN; + + // Create copies of URI handling functions + [ + decodeURI, + decodeURIComponent, + encodeURI, + encodeURIComponent, + ].forEach((fn) => { + primordials[fn.name] = fn; + }); + + // Create copies of the namespace objects + [ + "JSON", + "Math", + "Proxy", + "Reflect", + ].forEach((name) => { + copyPropsRenamed(globalThis[name], primordials, name); + }); + + // Create copies of intrinsic objects + [ + "AggregateError", + "Array", + "ArrayBuffer", + "BigInt", + "BigInt64Array", + "BigUint64Array", + "Boolean", + "DataView", + "Date", + "Error", + "EvalError", + "FinalizationRegistry", + "Float32Array", + "Float64Array", + "Function", + "Int16Array", + "Int32Array", + "Int8Array", + "Map", + "Number", + "Object", + "RangeError", + "ReferenceError", + "RegExp", + "Set", + "String", + "Symbol", + "SyntaxError", + "TypeError", + "URIError", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "WeakMap", + "WeakRef", + "WeakSet", + ].forEach((name) => { + const original = globalThis[name]; + primordials[name] = original; + copyPropsRenamed(original, primordials, name); + copyPrototype(original.prototype, primordials, `${name}Prototype`); + }); + + // Create copies of intrinsic objects that require a valid `this` to call + // static methods. + // Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all + [ + "Promise", + ].forEach((name) => { + const original = globalThis[name]; + primordials[name] = original; + copyPropsRenamedBound(original, primordials, name); + copyPrototype(original.prototype, primordials, `${name}Prototype`); + }); + + // Create copies of abstract intrinsic objects that are not directly exposed + // on the global object. + // Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object + [ + { name: "TypedArray", original: Reflect.getPrototypeOf(Uint8Array) }, + { + name: "ArrayIterator", + original: { + prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()), + }, + }, + { + name: "SetIterator", + original: { + prototype: Reflect.getPrototypeOf(new Set()[Symbol.iterator]()), + }, + }, + { + name: "MapIterator", + original: { + prototype: Reflect.getPrototypeOf(new Map()[Symbol.iterator]()), + }, + }, + { + name: "StringIterator", + original: { + prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()), + }, + }, + { name: "Generator", original: Reflect.getPrototypeOf(function* () {}) }, + { + name: "AsyncGenerator", + original: Reflect.getPrototypeOf(async function* () {}), + }, + ].forEach(({ name, original }) => { + primordials[name] = original; + // The static %TypedArray% methods require a valid `this`, but can't be bound, + // as they need a subclass constructor as the receiver: + copyPrototype(original, primordials, name); + copyPrototype(original.prototype, primordials, `${name}Prototype`); + }); + + const { + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeMap, + FunctionPrototypeCall, + ObjectDefineProperty, + ObjectFreeze, + ObjectPrototypeIsPrototypeOf, + ObjectSetPrototypeOf, + Promise, + PromisePrototype, + PromisePrototypeThen, + SymbolIterator, + TypedArrayPrototypeJoin, + } = primordials; + + // Because these functions are used by `makeSafe`, which is exposed + // on the `primordials` object, it's important to use const references + // to the primordials that they use: + const createSafeIterator = (factory, next) => { + class SafeIterator { + constructor(iterable) { + this._iterator = factory(iterable); + } + next() { + return next(this._iterator); + } + [SymbolIterator]() { + return this; + } + } + ObjectSetPrototypeOf(SafeIterator.prototype, null); + ObjectFreeze(SafeIterator.prototype); + ObjectFreeze(SafeIterator); + return SafeIterator; + }; + + const SafeArrayIterator = createSafeIterator( + primordials.ArrayPrototypeSymbolIterator, + primordials.ArrayIteratorPrototypeNext, + ); + primordials.SafeArrayIterator = SafeArrayIterator; + primordials.SafeSetIterator = createSafeIterator( + primordials.SetPrototypeSymbolIterator, + primordials.SetIteratorPrototypeNext, + ); + primordials.SafeMapIterator = createSafeIterator( + primordials.MapPrototypeSymbolIterator, + primordials.MapIteratorPrototypeNext, + ); + primordials.SafeStringIterator = createSafeIterator( + primordials.StringPrototypeSymbolIterator, + primordials.StringIteratorPrototypeNext, + ); + + const copyProps = (src, dest) => { + ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => { + if (!ReflectGetOwnPropertyDescriptor(dest, key)) { + ReflectDefineProperty( + dest, + key, + ReflectGetOwnPropertyDescriptor(src, key), + ); + } + }); + }; + + /** + * @type {typeof primordials.makeSafe} + */ + const makeSafe = (unsafe, safe) => { + if (SymbolIterator in unsafe.prototype) { + const dummy = new unsafe(); + let next; // We can reuse the same `next` method. + + ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => { + if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) { + const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key); + if ( + typeof desc.value === "function" && + desc.value.length === 0 && + SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {}) + ) { + const createIterator = uncurryThis(desc.value); + next ??= uncurryThis(createIterator(dummy).next); + const SafeIterator = createSafeIterator(createIterator, next); + desc.value = function () { + return new SafeIterator(this); + }; + } + ReflectDefineProperty(safe.prototype, key, desc); + } + }); + } else { + copyProps(unsafe.prototype, safe.prototype); + } + copyProps(unsafe, safe); + + ObjectSetPrototypeOf(safe.prototype, null); + ObjectFreeze(safe.prototype); + ObjectFreeze(safe); + return safe; + }; + primordials.makeSafe = makeSafe; + + // Subclass the constructors because we need to use their prototype + // methods later. + // Defining the `constructor` is necessary here to avoid the default + // constructor which uses the user-mutable `%ArrayIteratorPrototype%.next`. + primordials.SafeMap = makeSafe( + Map, + class SafeMap extends Map { + constructor(i) { + if (i == null) { + super(); + return; + } + super(new SafeArrayIterator(i)); + } + }, + ); + primordials.SafeWeakMap = makeSafe( + WeakMap, + class SafeWeakMap extends WeakMap { + constructor(i) { + if (i == null) { + super(); + return; + } + super(new SafeArrayIterator(i)); + } + }, + ); + + primordials.SafeSet = makeSafe( + Set, + class SafeSet extends Set { + constructor(i) { + if (i == null) { + super(); + return; + } + super(new SafeArrayIterator(i)); + } + }, + ); + primordials.SafeWeakSet = makeSafe( + WeakSet, + class SafeWeakSet extends WeakSet { + constructor(i) { + if (i == null) { + super(); + return; + } + super(new SafeArrayIterator(i)); + } + }, + ); + + primordials.SafeRegExp = makeSafe( + RegExp, + class SafeRegExp extends RegExp { + constructor(pattern, flags) { + super(pattern, flags); + } + }, + ); + + primordials.SafeFinalizationRegistry = makeSafe( + FinalizationRegistry, + class SafeFinalizationRegistry extends FinalizationRegistry { + constructor(cleanupCallback) { + super(cleanupCallback); + } + }, + ); + + primordials.SafeWeakRef = makeSafe( + WeakRef, + class SafeWeakRef extends WeakRef { + constructor(target) { + super(target); + } + }, + ); + + const SafePromise = makeSafe( + Promise, + class SafePromise extends Promise { + constructor(executor) { + super(executor); + } + }, + ); + + primordials.ArrayPrototypeToString = (thisArray) => + ArrayPrototypeJoin(thisArray); + + primordials.TypedArrayPrototypeToString = (thisArray) => + TypedArrayPrototypeJoin(thisArray); + + primordials.PromisePrototypeCatch = (thisPromise, onRejected) => + PromisePrototypeThen(thisPromise, undefined, onRejected); + + const arrayToSafePromiseIterable = (array) => + new SafeArrayIterator( + ArrayPrototypeMap( + array, + (p) => { + if (ObjectPrototypeIsPrototypeOf(PromisePrototype, p)) { + return new SafePromise((c, d) => PromisePrototypeThen(p, c, d)); + } + return p; + }, + ), + ); + + /** + * Creates a Promise that is resolved with an array of results when all of the + * provided Promises resolve, or rejected when any Promise is rejected. + * @template T + * @param {Array>} values + * @returns {Promise[]>} + */ + primordials.SafePromiseAll = (values) => + // Wrapping on a new Promise is necessary to not expose the SafePromise + // prototype to user-land. + new Promise((a, b) => + SafePromise.all(arrayToSafePromiseIterable(values)).then(a, b) + ); + + // /** + // * Creates a Promise that is resolved with an array of results when all + // * of the provided Promises resolve or reject. + // * @template T + // * @param {Array>} values + // * @returns {Promise[]>} + // */ + primordials.SafePromiseAllSettled = (values) => + // Wrapping on a new Promise is necessary to not expose the SafePromise + // prototype to user-land. + new Promise((a, b) => + SafePromise.allSettled(arrayToSafePromiseIterable(values)).then(a, b) + ); + + // /** + // * The any function returns a promise that is fulfilled by the first given + // * promise to be fulfilled, or rejected with an AggregateError containing + // * an array of rejection reasons if all of the given promises are rejected. + // * It resolves all elements of the passed iterable to promises as it runs + // * this algorithm. + // * @template T + // * @param {T} values + // * @returns {Promise>} + // */ + primordials.SafePromiseAny = (values) => + // Wrapping on a new Promise is necessary to not expose the SafePromise + // prototype to user-land. + new Promise((a, b) => + SafePromise.any(arrayToSafePromiseIterable(values)).then(a, b) + ); + + // /** + // * Creates a Promise that is resolved or rejected when any of the provided + // * Promises are resolved or rejected. + // * @template T + // * @param {T} values + // * @returns {Promise>} + // */ + primordials.SafePromiseRace = (values) => + // Wrapping on a new Promise is necessary to not expose the SafePromise + // prototype to user-land. + new Promise((a, b) => + SafePromise.race(arrayToSafePromiseIterable(values)).then(a, b) + ); + + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or + * rejected). The resolved value cannot be modified from the callback. + * Prefer using async functions when possible. + * @param {Promise} thisPromise + * @param {() => void) | undefined | null} onFinally The callback to execute + * when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + primordials.SafePromisePrototypeFinally = (thisPromise, onFinally) => + // Wrapping on a new Promise is necessary to not expose the SafePromise + // prototype to user-land. + new Promise((a, b) => + new SafePromise((a, b) => PromisePrototypeThen(thisPromise, a, b)) + .finally(onFinally) + .then(a, b) + ); + + // Create getter and setter for `queueMicrotask`, it hasn't been bound yet. + let queueMicrotask = undefined; + ObjectDefineProperty(primordials, "queueMicrotask", { + get() { + return queueMicrotask; + }, + }); + primordials.setQueueMicrotask = (value) => { + if (queueMicrotask !== undefined) { + throw new Error("queueMicrotask is already defined"); + } + queueMicrotask = value; + }; + + // Renaming from `eval` is necessary because otherwise it would perform direct + // evaluation, allowing user-land access to local variables. + // This is because the identifier `eval` is somewhat treated as a keyword + primordials.indirectEval = eval; + + ObjectSetPrototypeOf(primordials, null); + ObjectFreeze(primordials); + + // Provide bootstrap namespace + globalThis.__bootstrap ??= {}; + globalThis.__bootstrap.primordials = primordials; +})(); diff --git a/tests/fixtures/deno-core-0.407.0/01_core.js b/tests/fixtures/deno-core-0.407.0/01_core.js new file mode 100644 index 0000000000..44894af8bb --- /dev/null +++ b/tests/fixtures/deno-core-0.407.0/01_core.js @@ -0,0 +1,1302 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +"use strict"; + +((window) => { + const { + ArrayPrototypeMap, + ArrayPrototypePush, + Error, + ErrorCaptureStackTrace, + FunctionPrototypeBind, + ObjectAssign, + ObjectDefineProperties, + ObjectDefineProperty, + ObjectFreeze, + ObjectFromEntries, + ObjectKeys, + ObjectHasOwn, + setQueueMicrotask, + SafeMap, + SafeWeakMap, + StringPrototypeSlice, + Symbol, + SymbolFor, + TypedArrayPrototypeGetLength, + TypedArrayPrototypeJoin, + TypedArrayPrototypeSlice, + TypedArrayPrototypeGetSymbolToStringTag, + TypeError, + } = window.__bootstrap.primordials; + const { + ops, + hasPromise, + promiseIdSymbol, + registerErrorClass, + } = window.Deno.core; + const { + __setLeakTracingEnabled, + __isLeakTracingEnabled, + __initializeCoreMethods, + __resolvePromise, + FixedQueue, + } = window.__infra; + const __timers = window.__timers; + delete window.__timers; + const { + op_abort_wasm_streaming, + op_current_user_call_site, + op_compile_function, + op_decode, + op_deserialize, + op_destructure_error, + op_dispatch_exception, + op_encode, + op_encode_binary_string, + op_eval_context, + op_structured_clone, + op_event_loop_has_more_work, + op_get_extras_binding_object, + op_get_promise_details, + op_get_proxy_details, + op_get_ext_import_meta_proto, + op_drain_pending_rejections, + op_lazy_load_esm, + op_load_ext_script, + op_set_captured_bootstrap, + op_memory_usage, + op_op_names, + op_print, + op_ref_op, + op_resources, + op_run_microtasks, + op_serialize, + op_add_main_module_handler, + op_set_handled_promise_rejection_handler, + op_set_promise_hooks, + op_set_wasm_streaming_callback, + op_str_byte_length, + op_unref_op, + op_cancel_handle, + op_leak_tracing_enable, + op_leak_tracing_submit, + op_leak_tracing_get_all, + op_leak_tracing_get, + op_immediate_check, + + op_is_any_array_buffer, + op_is_arguments_object, + op_is_array_buffer, + op_is_array_buffer_view, + op_is_async_function, + op_is_big_int_object, + op_is_boolean_object, + op_is_boxed_primitive, + op_is_data_view, + op_is_date, + op_is_generator_function, + op_is_generator_object, + op_is_map, + op_is_map_iterator, + op_is_module_namespace_object, + op_is_native_error, + op_is_number_object, + op_is_promise, + op_is_proxy, + op_is_reg_exp, + op_is_set, + op_is_set_iterator, + op_is_shared_array_buffer, + op_is_string_object, + op_is_symbol_object, + op_is_typed_array, + op_is_weak_map, + op_is_weak_set, + } = ops; + + const { + getContinuationPreservedEmbedderData, + setContinuationPreservedEmbedderData, + } = op_get_extras_binding_object(); + + // core/infra collaborative code + delete window.__infra; + + __initializeCoreMethods( + submitLeakTrace, + ); + + function submitLeakTrace(id) { + const error = new Error(); + ErrorCaptureStackTrace(error, submitLeakTrace); + // "Error\n".length == 6 + op_leak_tracing_submit(0, id, StringPrototypeSlice(error.stack, 6)); + } + + let unhandledPromiseRejectionHandler = () => false; + + // --------------------------------------------------------------------------- + // Immediate queue (ImmediateList linked list + drain loop) + // --------------------------------------------------------------------------- + class ImmediateList { + constructor() { + this.head = null; + this.tail = null; + } + append(item) { + if (this.tail !== null) { + this.tail._idleNext = item; + item._idlePrev = this.tail; + } else { + this.head = item; + } + this.tail = item; + } + remove(item) { + if (item._idleNext) { + item._idleNext._idlePrev = item._idlePrev; + } + if (item._idlePrev) { + item._idlePrev._idleNext = item._idleNext; + } + if (item === this.head) { + this.head = item._idleNext; + } + if (item === this.tail) { + this.tail = item._idlePrev; + } + item._idleNext = null; + item._idlePrev = null; + } + } + + const immediateQueue = new ImmediateList(); + const outstandingQueue = new ImmediateList(); + + // Shared buffer with Rust - avoids JS-to-Rust op calls for immediate info. + // Indices: 0 = count, 1 = ref_count, 2 = has_outstanding + const kImmCount = 0; + const kImmRefCount = 1; + const kImmHasOutstanding = 2; + const kRefed = Symbol("refed"); + let immediateInfo; + + function queueImmediate(immediate) { + immediateInfo[kImmCount]++; + immediateQueue.append(immediate); + } + + function clearImmediate(immediate) { + if (!immediate || immediate._destroyed) { + return; + } + immediateInfo[kImmCount]--; + immediate._destroyed = true; + if (immediate[kRefed]) { + immediateInfo[kImmRefCount]--; + if (immediateInfo[kImmRefCount] === 0) { + op_immediate_check(false); + } + } + immediate[kRefed] = null; + emitDestroy(immediate.asyncId); + immediate._onImmediate = null; + immediateQueue.remove(immediate); + } + + function runImmediates() { + const queue = outstandingQueue.head !== null + ? outstandingQueue + : immediateQueue; + let immediate = queue.head; + if (queue !== outstandingQueue) { + queue.head = queue.tail = null; + immediateInfo[kImmHasOutstanding] = 1; + } + + let prevImmediate; + let ranAtLeastOneImmediate = false; + while (immediate !== null) { + if (ranAtLeastOneImmediate) { + runNextTicks(); + } else { + ranAtLeastOneImmediate = true; + } + + if (immediate._destroyed) { + outstandingQueue.head = immediate = prevImmediate._idleNext; + continue; + } + + immediate._destroyed = true; + + immediateInfo[kImmCount]--; + if (immediate[kRefed]) { + immediateInfo[kImmRefCount]--; + if (immediateInfo[kImmRefCount] === 0) { + op_immediate_check(false); + } + } + immediate[kRefed] = null; + + prevImmediate = immediate; + + const asyncId = immediate.asyncId; + emitBefore(asyncId, immediate.triggerAsyncId, immediate); + + try { + const argv = immediate._argv; + if (!argv) { + immediate._onImmediate(); + } else { + immediate._onImmediate(...argv); + } + } finally { + immediate._onImmediate = null; + emitDestroy(asyncId); + outstandingQueue.head = immediate = immediate._idleNext; + } + + emitAfter(asyncId); + } + + if (queue === outstandingQueue) { + outstandingQueue.head = null; + } + + immediateInfo[kImmHasOutstanding] = 0; + } + + // --------------------------------------------------------------------------- + // NextTick queue + // + // Closely mirrors Node.js lib/internal/process/task_queues.js. + // The queue and drain loop live here in core; Node-specific concerns + // (validation, async hooks init, exit check) stay in ext/node/. + // --------------------------------------------------------------------------- + const queue = new FixedQueue(); + + // Async hook emit functions. Default to no-ops; ext/node/ replaces them + // at bootstrap via setAsyncHooksEmit() with the real implementations + // from async_hooks.ts (emitBefore, emitAfter, emitDestroy). + let emitBefore = (_asyncId, _triggerAsyncId, _resource) => {}; + let emitAfter = (_asyncId) => {}; + let emitDestroy = (_asyncId) => {}; + + function setAsyncHooksEmit(before, after, destroy) { + emitBefore = before; + emitAfter = after; + emitDestroy = destroy; + } + + // Shared buffer with Rust - avoids JS-to-Rust op calls for tick scheduling. + // Index 0: hasTickScheduled + // Index 1: hasRejectionToWarn (set by Rust in promise_reject_callback) + // Set by Rust during store_js_callbacks via Deno.core.__tickInfo. + const kHasTickScheduled = 0; + const kHasRejectionToWarn = 1; + let tickInfo; + + function hasTickScheduled() { + return tickInfo[kHasTickScheduled] === 1; + } + + function hasRejectionToWarn() { + return tickInfo[kHasRejectionToWarn] === 1; + } + + function setHasRejectionToWarn(value) { + tickInfo[kHasRejectionToWarn] = value ? 1 : 0; + } + + function setHasTickScheduled(value) { + tickInfo[kHasTickScheduled] = value ? 1 : 0; + } + + // Enqueue a tick object. The object must have { callback, args } and + // an async context snapshot. It may contain additional fields for + // async hooks (asyncId, triggerAsyncId). + function queueNextTick(tickObject) { + if (queue.isEmpty()) { + setHasTickScheduled(true); + } + queue.push(tickObject); + } + + // Drain pending promise rejections from the Rust-side queue and process + // them through the unhandledPromiseRejectionHandler. Returns true if any + // rejections were processed (matching Node.js processPromiseRejections). + function processPromiseRejections() { + tickInfo[kHasRejectionToWarn] = 0; + const rejections = op_drain_pending_rejections(); + if (rejections === undefined) { + return false; + } + for (let i = 0; i < rejections.length; i += 3) { + const prevContext = getAsyncContext(); + setAsyncContext(rejections[i + 2]); + try { + const handled = unhandledPromiseRejectionHandler( + rejections[i], + rejections[i + 1], + ); + if (!handled) { + const err = rejections[i + 1]; + op_dispatch_exception(err, true); + } + } finally { + setAsyncContext(prevContext); + } + } + return true; + } + + // Matches Node.js processTicksAndRejections() from + // lib/internal/process/task_queues.js + function processTicksAndRejections() { + let tock; + do { + // deno-lint-ignore no-cond-assign + while ((tock = queue.shift()) !== null) { + const oldContext = getAsyncContext(); + setAsyncContext(tock.snapshot); + + const asyncId = tock.asyncId; + emitBefore(asyncId, tock.triggerAsyncId, tock); + + try { + const callback = tock.callback; + if (tock.args === undefined) { + callback(); + } else { + const args = tock.args; + switch (args.length) { + case 1: + callback(args[0]); + break; + case 2: + callback(args[0], args[1]); + break; + case 3: + callback(args[0], args[1], args[2]); + break; + case 4: + callback(args[0], args[1], args[2], args[3]); + break; + default: + callback(...args); + } + } + emitAfter(asyncId); + } catch (e) { + // In Node.js, errors propagate from JS to C++ (TryCatch in + // node_task_queue.cc InternalCallbackScope::Close), which calls + // TriggerUncaughtException and then re-enters the drain loop. + // We approximate this by catching here and routing through + // reportExceptionCallback (which triggers uncaughtException), + // then continuing the drain loop. + reportExceptionCallback(e); + } finally { + emitDestroy(asyncId); + } + + setAsyncContext(oldContext); + } + op_run_microtasks(); + } while (!queue.isEmpty() || processPromiseRejections()); + setHasTickScheduled(false); + setHasRejectionToWarn(false); + } + + // Flush microtasks and drain the nextTick queue if work is pending. + // Under Explicit microtask policy, microtasks (promise continuations) + // don't run automatically. We flush them here so that any ticks they + // schedule are discovered and drained in the same iteration. + // + // IMPORTANT: When ticks are already scheduled, we skip the microtask + // flush and go straight to processTicksAndRejections, which drains + // ticks BEFORE running microtasks. This preserves the Node.js + // invariant that nextTick callbacks fire before Promise.then + // continuations in the same event loop phase. + // + // This is the single drain function used by: __eventLoopTick (from + // Rust), __drainNextTickAndMacrotasks (I/O tight loop), runNextTicks + // (interleaved between timer/immediate callbacks), and runImmediates. + function drainTicks() { + if (!hasTickScheduled() && !hasRejectionToWarn()) { + op_run_microtasks(); + if (!hasTickScheduled() && !hasRejectionToWarn()) { + return; + } + } + processTicksAndRejections(); + } + + // Alias for timer/immediate interleaving (matches Node.js name). + const runNextTicks = drainTicks; + + // Wire runNextTicks into the timer module so processTimers can + // interleave nextTick drains between timer callbacks. + __timers.setRunNextTicks(runNextTicks); + // Wire reportException so timer callback errors are dispatched + // via the uncaught exception handler rather than propagating. + // Use a wrapper since reportExceptionCallback is defined later. + __timers.setReportException((e) => reportExceptionCallback(e)); + + // Called from Rust when the native user timer fires. + function __processTimers(now) { + return __timers.processTimers(now); + } + + // Resolve completed async ops. + // Called from Rust with args: (promiseId, isOk, res, ...) + // + // NOTE: This does NOT drain ticks. Under Explicit microtask policy, + // microtasks from op resolution are deferred. Rust calls + // __drainNextTickAndMacrotasks separately after this returns, + // with the correct microtask checkpoint ordering to preserve + // the nextTick-before-then invariant. + function __eventLoopTick() { + for (let i = 0; i < arguments.length; i += 3) { + const promiseId = arguments[i]; + const isOk = arguments[i + 1]; + const res = arguments[i + 2]; + __resolvePromise(promiseId, res, isOk); + } + } + + // Drain nextTick/microtask queues only (no timers or ops). + // Used in the I/O tight loop and when ticks are pending without ops. + function __drainNextTickAndMacrotasks() { + drainTicks(); + } + + // Phase 2: Handle unhandled promise rejections. + // Called from Rust with a flat array: [promise, reason, context, promise, reason, context, ...] + function __handleRejections() { + for (let i = 0; i < arguments.length; i += 3) { + // Restore the async context that was active when the promise was + // rejected, so that AsyncLocalStorage.getStore() works correctly + // inside unhandledrejection handlers (matching Node.js behavior). + const prevContext = getAsyncContext(); + setAsyncContext(arguments[i + 2]); + try { + const handled = unhandledPromiseRejectionHandler( + arguments[i], + arguments[i + 1], + ); + if (!handled) { + const err = arguments[i + 1]; + op_dispatch_exception(err, true); + } + } finally { + setAsyncContext(prevContext); + } + } + } + + // Report an exception (called from Rust for timer callback errors). + function __reportException(e) { + reportExceptionCallback(e); + } + + function runImmediateCallbacks() { + try { + runImmediates(); + } catch (e) { + reportExceptionCallback(e); + } + } + + function refOp(promiseId) { + if (!hasPromise(promiseId)) { + return; + } + op_ref_op(promiseId); + } + + function unrefOp(promiseId) { + if (!hasPromise(promiseId)) { + return; + } + op_unref_op(promiseId); + } + + function refOpPromise(promise) { + refOp(promise[promiseIdSymbol]); + } + + function unrefOpPromise(promise) { + unrefOp(promise[promiseIdSymbol]); + } + + function resources() { + return ObjectFromEntries(op_resources()); + } + + let reportExceptionCallback = (error) => { + op_dispatch_exception(error, false); + }; + + // Used to report errors thrown from functions passed to `queueMicrotask()`. + // The callback will be passed the thrown error. For example, you can use this + // to dispatch an error event to the global scope. + // In other words, set the implementation for + // https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception + function setReportExceptionCallback(cb) { + if (cb === null || cb === undefined) { + reportExceptionCallback = (error) => { + op_dispatch_exception(error, false); + }; + } else { + if (typeof cb != "function") { + throw new TypeError("expected a function"); + } + reportExceptionCallback = cb; + } + } + + // V8 installs a native `queueMicrotask` on the global object (enabled via the + // `--enable-queue-microtask` flag). We wrap it below to route uncaught + // exceptions through `reportExceptionCallback`, so we grab a reference to the + // native implementation here - it lets us enqueue microtasks without crossing + // the op boundary. + // + // V8 skips experimental globals while snapshotting, so the native function is + // only installed at runtime. In a from-scratch runtime it's already on the + // global when this module runs (before our wrapper replaces it below), so we + // can grab it directly. When restoring from a snapshot it isn't there yet, so + // fall back to a lazy capture on first use: our wrapper shadows the native one + // as an own property of the global, so momentarily remove our property to + // reveal the native one underneath, then restore our wrapper. + let nativeQueueMicrotask = window.queueMicrotask; + function captureNativeQueueMicrotask() { + const wrapper = window.queueMicrotask; + delete window.queueMicrotask; + nativeQueueMicrotask = window.queueMicrotask; + window.queueMicrotask = wrapper; + return nativeQueueMicrotask; + } + + function queueMicrotask(cb) { + if (typeof cb != "function") { + throw new TypeError("expected a function"); + } + const enqueue = nativeQueueMicrotask ?? captureNativeQueueMicrotask(); + return enqueue(() => { + try { + cb(); + } catch (error) { + reportExceptionCallback(error); + } + }); + } + + // Some "extensions" rely on "BadResource", "Interrupted", "NotCapable" + // errors in the JS code (eg. "deno_net") so they are provided in "Deno.core" + // but later reexported on "Deno.errors" + class BadResource extends Error { + constructor(msg, options) { + super(msg, options); + this.name = "BadResource"; + } + } + const BadResourcePrototype = BadResource.prototype; + + class Interrupted extends Error { + constructor(msg, options) { + super(msg, options); + this.name = "Interrupted"; + } + } + const InterruptedPrototype = Interrupted.prototype; + + class NotCapable extends Error { + constructor(msg, options) { + super(msg, options); + this.name = "NotCapable"; + } + } + const NotCapablePrototype = NotCapable.prototype; + + registerErrorClass("BadResource", BadResource); + registerErrorClass("Interrupted", Interrupted); + registerErrorClass("NotCapable", NotCapable); + + const promiseHooks = [ + [], // init + [], // before + [], // after + [], // resolve + ]; + + function setPromiseHooks(init, before, after, resolve) { + const hooks = [init, before, after, resolve]; + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + // Skip if no callback was provided for this hook type. + if (hook == null) { + continue; + } + // Verify that the type of `hook` is a function. + if (typeof hook !== "function") { + throw new TypeError(`Expected function at position ${i}`); + } + // Add the hook to the list. + ArrayPrototypePush(promiseHooks[i], hook); + } + + const wrappedHooks = ArrayPrototypeMap(promiseHooks, (hooks) => { + switch (hooks.length) { + case 0: + return undefined; + case 1: + return hooks[0]; + case 2: + return create2xHookWrapper(hooks[0], hooks[1]); + case 3: + return create3xHookWrapper(hooks[0], hooks[1], hooks[2]); + default: + return createHookListWrapper(hooks); + } + + // The following functions are used to create wrapper functions that call + // all the hooks in a list of a certain length. The reason to use a + // function that creates a wrapper is to minimize the number of objects + // captured in the closure. + function create2xHookWrapper(hook1, hook2) { + return function (promise, parent) { + hook1(promise, parent); + hook2(promise, parent); + }; + } + function create3xHookWrapper(hook1, hook2, hook3) { + return function (promise, parent) { + hook1(promise, parent); + hook2(promise, parent); + hook3(promise, parent); + }; + } + function createHookListWrapper(hooks) { + return function (promise, parent) { + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + hook(promise, parent); + } + }; + } + }); + + op_set_promise_hooks( + wrappedHooks[0], + wrappedHooks[1], + wrappedHooks[2], + wrappedHooks[3], + ); + } + + const { + op_close: close, + op_try_close: tryClose, + op_cancel_read: cancelRead, + op_read: read, + op_read_all: readAll, + op_write: write, + op_write_all: writeAll, + op_write_type_error: writeTypeError, + op_read_sync: readSync, + op_write_sync: writeSync, + op_shutdown: shutdown, + op_is_terminal: isTerminal, + } = ops; + + const callSiteRetBuf = new Uint32Array(2); + const callSiteRetBufU8 = new Uint8Array(callSiteRetBuf.buffer); + + function currentUserCallSite() { + const fileName = op_current_user_call_site(callSiteRetBufU8); + const lineNumber = callSiteRetBuf[0]; + const columnNumber = callSiteRetBuf[1]; + return { fileName, lineNumber, columnNumber }; + } + + const hostObjectBrand = SymbolFor("Deno.core.hostObject"); + const transferableResources = {}; + const registerTransferableResource = (name, send, receive) => { + if (transferableResources[name]) { + throw new Error(`${name} is already registered`); + } + transferableResources[name] = { send, receive }; + }; + const getTransferableResource = (name) => transferableResources[name]; + const cloneableDeserializers = { __proto__: null }; + const registerCloneableResource = (name, deserialize) => { + if (cloneableDeserializers[name]) { + throw new Error(`${name} is already registered`); + } + cloneableDeserializers[name] = deserialize; + }; + const getCloneableDeserializers = () => cloneableDeserializers; + + // A helper function that will bind our own console implementation + // with default implementation of Console from V8. This will cause + // console messages to be piped to inspector console. + // + // We are using `Deno.core.callConsole` binding to preserve proper stack + // frames in inspector console. This has to be done because V8 considers + // the last JS stack frame as gospel for the inspector. In our case we + // specifically want the latest user stack frame to be the one that matters + // though. + // + // Inspired by: + // https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/internal/util/inspector.js#L39-L61 + function wrapConsole(customConsole, consoleFromV8) { + const callConsole = window.Deno.core.callConsole; + + const keys = ObjectKeys(consoleFromV8); + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (ObjectHasOwn(customConsole, key)) { + customConsole[key] = FunctionPrototypeBind( + callConsole, + customConsole, + consoleFromV8[key], + customConsole[key], + ); + } else { + // Add additional console APIs from the inspector + customConsole[key] = consoleFromV8[key]; + } + } + } + + // Minimal console implementation, that uses `Deno.core.print` under the hood. + // It's not fully fledged and is meant to make debugging slightly easier when working with + // only `deno_core` crate. + class CoreConsole { + log = (...args) => { + op_print(`${consoleStringify(...args)}\n`, false); + }; + + debug = (...args) => { + op_print(`${consoleStringify(...args)}\n`, false); + }; + + warn = (...args) => { + op_print(`${consoleStringify(...args)}\n`, false); + }; + + error = (...args) => { + op_print(`${consoleStringify(...args)}\n`, false); + }; + } + + // Default impl of contextual logging + op_get_ext_import_meta_proto().log = function internalLog(level, ...args) { + console.error(`[${level.toUpperCase()}]`, ...args); + }; + + const consoleStringify = (...args) => args.map(consoleStringifyArg).join(" "); + + const consoleStringifyArg = (arg) => { + if ( + typeof arg === "string" || typeof arg === "boolean" || + typeof arg === "number" || arg === null || arg === undefined + ) { + return arg; + } + const tag = TypedArrayPrototypeGetSymbolToStringTag(arg); + if (op_is_typed_array(arg)) { + return `${tag}(${TypedArrayPrototypeGetLength(arg)}) [${ + TypedArrayPrototypeJoin(TypedArrayPrototypeSlice(arg, 0, 10), ", ") + }]`; + } + if (tag !== undefined) { + tag + " " + JSON.stringify(arg, undefined, 2); + } else { + return JSON.stringify(arg, undefined, 2); + } + }; + + const v8Console = globalThis.console; + const coreConsole = new CoreConsole(); + globalThis.console = coreConsole; + wrapConsole(coreConsole, v8Console); + + function propWritable(value) { + return { + value, + writable: true, + enumerable: true, + configurable: true, + }; + } + + function propNonEnumerable(value) { + return { + value, + writable: true, + enumerable: false, + configurable: true, + }; + } + + function propReadOnly(value) { + return { + value, + enumerable: true, + writable: false, + configurable: true, + }; + } + + function propGetterOnly(getter) { + return { + get: getter, + set() {}, + enumerable: true, + configurable: true, + }; + } + + // Marker symbol identifying a lazy-loaded property descriptor. The property + // name is filled in by `defineGlobalProperties` at install time so the + // setter can mimic data-property [[Set]] semantics (define an own data + // property on the receiver) instead of mutating a closure shared across all + // receivers - see denoland/deno#34403. + const lazyNameSym = Symbol("lazyName"); + + function propWritableLazyLoaded(getter, loadFn) { + const desc = { + get() { + return getter(loadFn()); + }, + set(v) { + ObjectDefineProperty(this, desc[lazyNameSym], { + value: v, + writable: true, + enumerable: true, + configurable: true, + }); + }, + enumerable: true, + configurable: true, + [lazyNameSym]: undefined, + }; + return desc; + } + + function propNonEnumerableLazyLoaded(getter, loadFn) { + const desc = { + get() { + return getter(loadFn()); + }, + set(v) { + const name = desc[lazyNameSym]; + // Match OrdinarySetWithOwnDescriptor semantics for an inherited + // writable data property: a direct set on the holder updates the + // value in place (preserving enumerable: false), while an inherited + // set creates a new enumerable own data property on the receiver. + if (ObjectHasOwn(this, name)) { + ObjectDefineProperty(this, name, { + value: v, + writable: true, + enumerable: false, + configurable: true, + }); + } else { + ObjectDefineProperty(this, name, { + value: v, + writable: true, + enumerable: true, + configurable: true, + }); + } + }, + enumerable: false, + configurable: true, + [lazyNameSym]: undefined, + }; + return desc; + } + + // Like `Object.defineProperties`, but also stamps the property name into any + // lazy-loaded descriptors so their setters can target the correct receiver. + function defineGlobalProperties(target, props) { + const keys = ObjectKeys(props); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const desc = props[key]; + if (desc !== null && typeof desc === "object" && lazyNameSym in desc) { + desc[lazyNameSym] = key; + } + } + ObjectDefineProperties(target, props); + } + + function createLazyLoader(specifier) { + let value; + + return function lazyLoad() { + if (!value) { + value = op_lazy_load_esm(specifier); + } + return value; + }; + } + + const loadedScripts = { __proto__: null }; + + // Note: the Rust side of `op_load_ext_script` (in `libs/core/modules/map.rs`) + // temporarily reinstalls a captured snapshot-time view of `__bootstrap` + // on `globalThis` for the duration of each script evaluation if + // `__bootstrap` isn't already on the global. Every `lazy_loaded_js` + // polyfill's IIFE preamble destructures `globalThis.__bootstrap` and + // `__bootstrap.core.ops`, but at runtime (`runtime/js/99_main.js`) the + // harness deletes `globalThis.__bootstrap` and `removeImportedOps()` + // strips most entries out of `Deno.core.ops`. The captured view (a + // shallow clone of `core.ops` immune to `removeImportedOps`) is + // registered via `op_set_captured_bootstrap` at the end of this IIFE. + // Doing the install in Rust means the `synthetic_esm` dispatch path + // (which calls `load_ext_script` directly without going through this + // wrapper) gets the same treatment without leaving `__bootstrap` + // permanently on the global (where user code can observe it via + // `Object.keys`). + function loadExtScript(specifier) { + if (specifier in loadedScripts) { + return loadedScripts[specifier]; + } + const result = op_load_ext_script(specifier); + loadedScripts[specifier] = result; + return result; + } + + const getAsyncContext = getContinuationPreservedEmbedderData; + const setAsyncContext = setContinuationPreservedEmbedderData; + const kNoAsyncContextRestore = Symbol("Deno.core.noAsyncContextRestore"); + + function scopeAsyncContext(ctx) { + const old = getAsyncContext(); + setAsyncContext(ctx); + return { + __proto__: null, + [Symbol.dispose]() { + setAsyncContext(old); + }, + }; + } + + let asyncVariableCounter = 0; + class AsyncVariable { + #id = asyncVariableCounter++; + #data = new SafeWeakMap(); + + enter(value) { + const previousContextMapping = getAsyncContext(); + this.#enterWithPreviousContext(value, previousContextMapping); + return previousContextMapping; + } + + enterIfActive(value) { + const previousContextMapping = getAsyncContext(); + if ( + previousContextMapping === null || + previousContextMapping === undefined || + ObjectKeys(previousContextMapping).length === 0 + ) { + return kNoAsyncContextRestore; + } + this.#enterWithPreviousContext(value, previousContextMapping); + return previousContextMapping; + } + + #enterWithPreviousContext(value, previousContextMapping) { + const entry = { id: this.#id }; + const asyncContextMapping = { + __proto__: null, + ...previousContextMapping, + [this.#id]: entry, + }; + this.#data.set(entry, value); + setAsyncContext(asyncContextMapping); + } + + get() { + const current = getAsyncContext(); + const entry = current?.[this.#id]; + if (entry) { + return this.#data.get(entry); + } + return undefined; + } + } + + // Extra Deno.core.* exports + const core = ObjectAssign(globalThis.Deno.core, { + internalRidSymbol: Symbol("Deno.internal.rid"), + internalFdSymbol: Symbol("Deno.internal.fd"), + resources, + __eventLoopTick, + __processTimers, + __setTickInfo(buf) { + tickInfo = buf; + }, + __setImmediateInfo(buf) { + immediateInfo = buf; + }, + __drainNextTickAndMacrotasks, + __handleRejections, + __reportException, + __setTimerInfo: __timers.__setTimerInfo, + immediateRefCount(increase) { + if (increase) { + if (immediateInfo[kImmRefCount] === 0) { + op_immediate_check(true); + } + immediateInfo[kImmRefCount]++; + } else { + immediateInfo[kImmRefCount]--; + if (immediateInfo[kImmRefCount] === 0) { + op_immediate_check(false); + } + } + }, + runImmediateCallbacks, + BadResource, + BadResourcePrototype, + Interrupted, + InterruptedPrototype, + NotCapable, + NotCapablePrototype, + refOpPromise, + unrefOpPromise, + setReportExceptionCallback, + setPromiseHooks, + consoleStringify, + close, + tryClose, + cancelRead, + read, + readAll, + write, + writeAll, + writeTypeError, + readSync, + writeSync, + shutdown, + isTerminal, + print: (msg, isErr) => op_print(msg, isErr), + setLeakTracingEnabled: (enabled) => { + __setLeakTracingEnabled(enabled); + op_leak_tracing_enable(enabled); + }, + isLeakTracingEnabled: () => __isLeakTracingEnabled(), + getAllLeakTraces: () => { + const traces = op_leak_tracing_get_all(); + return new SafeMap(traces); + }, + getLeakTraceForPromise: (promise) => + op_leak_tracing_get(0, promise[promiseIdSymbol]), + queueNextTick, + processTicksAndRejections, + runNextTicks, + setAsyncHooksEmit, + queueImmediate, + clearImmediate, + runImmediates, + immediateQueue, + getActiveImmediateCount() { + return immediateInfo[kImmRefCount]; + }, + kRefed, + runMicrotasks: () => op_run_microtasks(), + hasTickScheduled, + setHasTickScheduled, + compileFunction: ( + source, + specifier, + hostDefinedOptions, + params, + ) => { + const [result, error] = op_compile_function( + source, + specifier, + hostDefinedOptions, + params, + ); + if (error) { + const { 0: thrown, 1: isNativeError, 2: isCompileError } = error; + return [ + result, + { + thrown, + isNativeError, + isCompileError, + }, + ]; + } + return [result, null]; + }, + evalContext: ( + source, + specifier, + hostDefinedOptions, + ) => { + const [result, error] = op_eval_context( + source, + specifier, + hostDefinedOptions, + ); + if (error) { + const { 0: thrown, 1: isNativeError, 2: isCompileError } = error; + return [ + result, + { + thrown, + isNativeError, + isCompileError, + }, + ]; + } + return [result, null]; + }, + hostObjectBrand, + registerTransferableResource, + getTransferableResource, + registerCloneableResource, + getCloneableDeserializers, + encode: (text) => op_encode(text), + encodeBinaryString: (buffer) => op_encode_binary_string(buffer), + decode: (buffer) => op_decode(buffer), + structuredClone: (value, deserializers) => + op_structured_clone(value, deserializers ?? cloneableDeserializers), + serialize: ( + value, + options, + errorCallback, + ) => { + return op_serialize( + value, + options?.hostObjects, + options?.transferredArrayBuffers, + options?.forStorage ?? false, + errorCallback, + ); + }, + deserialize: (buffer, options) => { + return op_deserialize( + buffer, + options?.hostObjects, + options?.transferredArrayBuffers, + options?.deserializers, + options?.forStorage ?? false, + ); + }, + getPromiseDetails: (promise) => op_get_promise_details(promise), + getProxyDetails: (proxy) => op_get_proxy_details(proxy), + isAnyArrayBuffer: (value) => op_is_any_array_buffer(value), + isArgumentsObject: (value) => op_is_arguments_object(value), + isArrayBuffer: (value) => op_is_array_buffer(value), + isArrayBufferView: (value) => op_is_array_buffer_view(value), + isAsyncFunction: (value) => op_is_async_function(value), + isBigIntObject: (value) => op_is_big_int_object(value), + isBooleanObject: (value) => op_is_boolean_object(value), + isBoxedPrimitive: (value) => op_is_boxed_primitive(value), + isDataView: (value) => op_is_data_view(value), + isDate: (value) => op_is_date(value), + isGeneratorFunction: (value) => op_is_generator_function(value), + isGeneratorObject: (value) => op_is_generator_object(value), + isMap: (value) => op_is_map(value), + isMapIterator: (value) => op_is_map_iterator(value), + isModuleNamespaceObject: (value) => op_is_module_namespace_object(value), + isNativeError: (value) => op_is_native_error(value), + isNumberObject: (value) => op_is_number_object(value), + isPromise: (value) => op_is_promise(value), + isProxy: (value) => op_is_proxy(value), + isRegExp: (value) => op_is_reg_exp(value), + isSet: (value) => op_is_set(value), + isSetIterator: (value) => op_is_set_iterator(value), + isSharedArrayBuffer: (value) => op_is_shared_array_buffer(value), + isStringObject: (value) => op_is_string_object(value), + isSymbolObject: (value) => op_is_symbol_object(value), + isTypedArray: (value) => op_is_typed_array(value), + isWeakMap: (value) => op_is_weak_map(value), + isWeakSet: (value) => op_is_weak_set(value), + memoryUsage: () => op_memory_usage(), + setWasmStreamingCallback: (fn) => op_set_wasm_streaming_callback(fn), + abortWasmStreaming: ( + rid, + error, + ) => op_abort_wasm_streaming(rid, error), + destructureError: (error) => op_destructure_error(error), + opNames: () => op_op_names(), + eventLoopHasMoreWork: () => op_event_loop_has_more_work(), + byteLength: (str) => op_str_byte_length(str), + addMainModuleHandler: (handler) => op_add_main_module_handler(handler), + setHandledPromiseRejectionHandler: (handler) => + op_set_handled_promise_rejection_handler(handler), + setUnhandledPromiseRejectionHandler: (handler) => + unhandledPromiseRejectionHandler = handler, + reportUnhandledException: (e) => op_dispatch_exception(e, false), + reportUnhandledPromiseRejection: (e) => op_dispatch_exception(e, true), + createTimer: __timers.createTimer, + cancelTimer: __timers.cancelTimer, + refreshTimer: __timers.refreshTimer, + refTimer: __timers.refTimer, + unrefTimer: __timers.unrefTimer, + // System timers bypass Deno's test sanitizer resource tracking. + createSystemTimer: (callback, after, isRefed) => + __timers.createTimer(callback, after, undefined, false, !!isRefed, true), + createSystemInterval: (callback, interval, isRefed) => + __timers.createTimer( + callback, + interval, + undefined, + true, + !!isRefed, + true, + ), + currentUserCallSite, + wrapConsole, + v8Console, + propReadOnly, + propWritable, + propNonEnumerable, + propGetterOnly, + propWritableLazyLoaded, + propNonEnumerableLazyLoaded, + defineGlobalProperties, + createLazyLoader, + loadExtScript, + createCancelHandle: () => op_cancel_handle(), + getAsyncContext, + setAsyncContext, + scopeAsyncContext, + AsyncVariable, + kNoAsyncContextRestore, + }); + + const internals = {}; + ObjectAssign(globalThis.__bootstrap, { core, internals }); + ObjectAssign(globalThis.Deno, { core }); + ObjectFreeze(globalThis.__bootstrap.core); + + // Build the snapshot-time view of __bootstrap and hand it off to Rust. + // The Rust `load_ext_script` (libs/core/modules/map.rs) temporarily + // installs this on `globalThis.__bootstrap` for each script evaluation + // when the live `__bootstrap` has already been deleted (i.e. after + // runtime bootstrap). See the comment above `loadExtScript` earlier in + // this file for context. + const capturedCore = ObjectAssign({ __proto__: null }, core); + capturedCore.ops = ObjectAssign({ __proto__: null }, core.ops); + const capturedBootstrap = { + __proto__: null, + primordials: globalThis.__bootstrap.primordials, + core: capturedCore, + internals, + }; + op_set_captured_bootstrap(capturedBootstrap); + + // Direct bindings on `globalThis` + ObjectAssign(globalThis, { queueMicrotask }); + setQueueMicrotask(queueMicrotask); +})(globalThis); diff --git a/tests/fixtures/deno-core-0.407.0/README.md b/tests/fixtures/deno-core-0.407.0/README.md new file mode 100644 index 0000000000..2eea7ecb9d --- /dev/null +++ b/tests/fixtures/deno-core-0.407.0/README.md @@ -0,0 +1,16 @@ +# Deno core bootstrap fixture + +These files are unchanged copies of Deno's core bootstrap sources at commit +`1d4e6c1cb855b62a7fb572c6c138e4e8b4e7fa44` (Deno 2.9.2, +`deno_core` 0.407.0): + +| Upstream path | SHA-256 | +| --- | --- | +| `libs/core/00_primordials.js` | `5a2dfbdc4bb81412575d035901a11788001c7e0110e3f736d16289891af44a52` | +| `libs/core/00_infra.js` | `33984000be930f3b02a2d1149ac0319724e8d95891623c8cc74699da4ce97287` | +| `libs/core/01_core.js` | `6e67972322cc5385a2b642a4f7e941fccb6f992c9de662a5111d11fd0aaf1a3a` | + +They are retained verbatim so the integration test cannot silently replace +Deno's real wrappers with a reduced reproduction. Deno distributes these +sources under the MIT license; see the copyright and license notice in each +file and Deno's repository-level `LICENSE.md`. diff --git a/tests/helpers/deno-core-bootstrap-probe.ts b/tests/helpers/deno-core-bootstrap-probe.ts new file mode 100644 index 0000000000..da299233ff --- /dev/null +++ b/tests/helpers/deno-core-bootstrap-probe.ts @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Loopdive GmbH. Licensed under Apache-2.0 WITH LLVM-exception. + +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { compileMulti } from "../../src/index.ts"; + +const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), "../fixtures/deno-core-0.407.0"); + +const upstreamFiles = [ + ["00_primordials.js", "5a2dfbdc4bb81412575d035901a11788001c7e0110e3f736d16289891af44a52"], + ["00_infra.js", "33984000be930f3b02a2d1149ac0319724e8d95891623c8cc74699da4ce97287"], + ["01_core.js", "6e67972322cc5385a2b642a4f7e941fccb6f992c9de662a5111d11fd0aaf1a3a"], +] as const; + +const runtimeSeed = ` + const extrasBinding = { + getContinuationPreservedEmbedderData() { return undefined; }, + setContinuationPreservedEmbedderData(_value: any) {}, + }; + const importMetaPrototype: any = {}; + const ops: any = { + op_get_extras_binding_object() { + return extrasBinding; + }, + op_get_ext_import_meta_proto() { + return importMetaPrototype; + }, + op_set_captured_bootstrap(bootstrap: any) { + (globalThis as any).__capturedBootstrap = bootstrap; + }, + }; + const core: any = { + ops, + callConsole(_v8Method: any, denoMethod: any, ...args: any[]) { + return denoMethod(...args); + }, + }; + const noTimer = () => {}; + (globalThis as any).Deno = { core }; + (globalThis as any).__timers = { + createTimer: noTimer, + cancelTimer: noTimer, + refreshTimer: noTimer, + refTimer: noTimer, + unrefTimer: noTimer, + setRunNextTicks: noTimer, + setReportException: noTimer, + processTimers: noTimer, + }; +`; + +const entrySource = ` + import "./runtime-seed.ts"; + import "./00_primordials.js"; + import "./00_infra.js"; + import "./01_core.js"; + + export function __v8x_probe_deno_core_bootstrap(): number { + const global: any = globalThis; + const bootstrap: any = global.__bootstrap; + if (bootstrap == null) return 0; + const core: any = bootstrap.core; + const captured: any = global.__capturedBootstrap; + return core != null && + global.Deno.core === core && + typeof core.queueNextTick === "function" && + typeof core.setAsyncHooksEmit === "function" && + typeof core.registerErrorClass === "function" && + typeof global.queueMicrotask === "function" && + bootstrap.internals != null && + captured != null && + typeof captured.core.queueNextTick === "function" && + captured.internals === bootstrap.internals ? 42 : 0; + } +`; + +function sha256(source: string): string { + return createHash("sha256").update(source).digest("hex"); +} + +const files: Record = { + "/deno-bootstrap/entry.ts": entrySource, + "/deno-bootstrap/runtime-seed.ts": runtimeSeed, +}; +const hashes: Record = {}; +for (const [fileName, expectedHash] of upstreamFiles) { + const source = readFileSync(join(fixtureDir, fileName), "utf8"); + const actualHash = sha256(source); + if (actualHash !== expectedHash) { + throw new Error(`${fileName} changed: expected ${expectedHash}, received ${actualHash}`); + } + hashes[fileName] = actualHash; + files[`/deno-bootstrap/${fileName}`] = source; +} + +const result = await compileMulti(files, "/deno-bootstrap/entry.ts", { + target: "standalone", + platform: "deno", + allowJs: true, + skipSemanticDiagnostics: true, + deferTopLevelInit: true, +}); +if (!result.success) { + throw new Error(result.errors.map((error) => error.message).join("\n")); +} + +const module = new WebAssembly.Module(result.binary); +const artifactOutput = process.env.DENO_CORE_BOOTSTRAP_WASM_OUTPUT; +if (artifactOutput) writeFileSync(artifactOutput, result.binary); +const moduleImports = WebAssembly.Module.imports(module); +const imports = moduleImports.map(({ module, name }) => `${module}::${name}`).sort(); +const calls: string[] = []; +const probes: number[] = []; +for (let instanceIndex = 0; instanceIndex < 2; instanceIndex++) { + const importObject: Record> = {}; + for (const descriptor of moduleImports) { + importObject[descriptor.module] ??= {}; + importObject[descriptor.module]![descriptor.name] = () => { + calls.push(`${descriptor.module}::${descriptor.name}`); + throw new Error(`bootstrap called unresolved import ${descriptor.module}::${descriptor.name}`); + }; + } + const instance = await WebAssembly.instantiate(module, importObject); + (instance.exports.__module_init as () => void)(); + probes.push((instance.exports.__v8x_probe_deno_core_bootstrap as () => number)()); +} + +process.stdout.write(`${JSON.stringify({ bytes: result.binary.byteLength, calls, hashes, imports, probes })}\n`); diff --git a/tests/issue-1725.test.ts b/tests/issue-1725.test.ts index e14396008c..f25ac7872d 100644 --- a/tests/issue-1725.test.ts +++ b/tests/issue-1725.test.ts @@ -154,4 +154,20 @@ describe("#1725 — functor-constructor struct.set repair must not cast a value- // so the body is unchanged (the receiver is already a struct ref). expect(JSON.stringify(fixedBody)).toBe(before); }); + + it("does not duplicate an exact receiver cast already emitted before struct.set", () => { + const mod = buildModule(); + mod.functions[0]!.body = [ + { op: "local.get", index: 0 }, + { op: "any.convert_extern" }, + { op: "ref.cast_null", typeIdx: 0 }, + { op: "f64.const", value: 1 }, + { op: "struct.set", typeIdx: 0, fieldIdx: 0 }, + ]; + const before = JSON.stringify(mod.functions[0]!.body); + + expect(repairStructTypeMismatches(mod)).toBe(0); + expect(JSON.stringify(mod.functions[0]!.body)).toBe(before); + expect(hasBadAdjacency(mod.functions[0]!.body)).toBe(false); + }); }); diff --git a/tests/issue-2933-reflect-static-method-value.test.ts b/tests/issue-2933-reflect-static-method-value.test.ts index d21e542544..dbf461fe16 100644 --- a/tests/issue-2933-reflect-static-method-value.test.ts +++ b/tests/issue-2933-reflect-static-method-value.test.ts @@ -9,7 +9,8 @@ import { compile } from "../src/index.js"; // `#1907`/`#1888 S6-b` "built-in static property value read is not supported" // compile error. The standalone CALL path already backs the fixed-arity // `Reflect.get`/`has`/`set`/`ownKeys` with a simple externref/i32 native -// (`__extern_get` / `__extern_has` / `__reflect_set` / `__object_keys`); this +// (`__extern_get` / `__extern_has` / `__reflect_set` / +// `__getOwnPropertyNames`); this // slice wires those SAME natives into `ensureStandaloneBuiltinStaticMethodClosure` // so the reified value calls identically. `JSON.stringify` as a value now works // too (host-free via `__json_stringify_root` — see @@ -59,10 +60,14 @@ describe("#2933 — standalone Reflect.* static-method value reads", () => { ); }); - it("Reflect.ownKeys as a value returns the own keys", async () => { + it("Reflect.ownKeys as a value includes non-enumerable own names", async () => { expect( await runStandalone( - `const o: any = { a: 1, b: 2 }; const f: any = Reflect.ownKeys; const ks: any = f(o); return ks.length;`, + `const o: any = { visible: 1 }; + Object.defineProperty(o, "hidden", { value: 2, enumerable: false }); + const f: any = Reflect.ownKeys; + const ks: any = f(o); + return ks.length;`, ), ).toBe(2); }); diff --git a/tests/issue-2968.test.ts b/tests/issue-2968.test.ts index e188462241..2670a9f15f 100644 --- a/tests/issue-2968.test.ts +++ b/tests/issue-2968.test.ts @@ -1,57 +1,35 @@ import { test, expect, describe } from "vitest"; import { compile } from "../src/index.ts"; -import { WASI } from "node:wasi"; -import { openSync, readFileSync, closeSync, mkdirSync, existsSync, rmSync } from "node:fs"; +import { spawnSync } from "node:child_process"; // #2968 — WASI `_start` uncaught-exception printer. // // When an uncaught exception reaches `_start` in a `--target wasi` binary, the // `_start` wrapper renders it to stderr (fd 2) via the #2962 native // `__error_to_string` / `__any_to_string` path and `proc_exit(1)`s, instead of -// the pre-fix silent exit 0. Validated under a real WASI runtime (`node:wasi`, -// which — like the V8/Node #2962 harness — supports the compiler's exception -// encoding). Modern wasmtime rejects the compiler's legacy exception-handling -// opcodes for EVERY try/catch program (a separate, pre-existing, compiler-wide -// gap), so these end-to-end runs use `node:wasi`. - -const WORK_DIR = "/tmp/wasi-test-2968"; +// the pre-fix silent exit 0. The compiler now emits standardized `try_table` +// EH for host-free targets. These assertions use node:wasi in an isolated +// child with exnref enabled; #2997 separately executes the output in Wasmtime. /** Compile `src` for wasi, run `_start` under node:wasi, return {code, stderr}. */ async function runWasi(src: string): Promise<{ code: number; stdout: string; stderr: string }> { const r = await compile(src, { fileName: "test.ts", target: "wasi" }); expect(r.success).toBe(true); - if (!existsSync(WORK_DIR)) mkdirSync(WORK_DIR, { recursive: true }); - const outPath = `${WORK_DIR}/out-${Math.random().toString(36).slice(2)}.txt`; - const errPath = `${WORK_DIR}/err-${Math.random().toString(36).slice(2)}.txt`; - const outFd = openSync(outPath, "w+"); - const errFd = openSync(errPath, "w+"); - try { - const wasi = new WASI({ - version: "preview1", - args: ["prog"], - returnOnExit: true, - stdout: outFd, - stderr: errFd, - }); - const mod = await WebAssembly.compile(r.binary); - const instance = await WebAssembly.instantiate(mod, wasi.getImportObject()); + const runner = ` + import { WASI } from "node:wasi"; + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + const wasi = new WASI({ version: "preview1", args: ["prog"], returnOnExit: true }); + const { instance } = await WebAssembly.instantiate(Buffer.concat(chunks), wasi.getImportObject()); const code = wasi.start(instance); - return { - code: typeof code === "number" ? code : 0, - stdout: readFileSync(outPath, "utf-8"), - stderr: readFileSync(errPath, "utf-8"), - }; - } finally { - closeSync(outFd); - closeSync(errFd); - try { - rmSync(outPath); - rmSync(errPath); - } catch { - /* ignore */ - } - } + process.exitCode = typeof code === "number" ? code : 0; + `; + const child = spawnSync(process.execPath, ["--experimental-wasm-exnref", "--input-type=module", "-e", runner], { + input: r.binary, + encoding: "utf8", + }); + return { code: child.status ?? 1, stdout: child.stdout, stderr: child.stderr }; } describe("#2968 — WASI _start uncaught-exception printer", () => { @@ -95,12 +73,9 @@ describe("#2968 — WASI _start uncaught-exception printer", () => { test("a throwing wasi module gains the exception-tag + proc_exit + fd_write it needs", async () => { const r = await compile(`throw new TypeError("x");`, { fileName: "test.ts", target: "wasi" }); expect(r.success).toBe(true); - const mod = new WebAssembly.Module(r.binary); - const importNames = WebAssembly.Module.imports(mod).map((i) => i.name); - expect(importNames).toContain("fd_write"); - expect(importNames).toContain("proc_exit"); - const exportNames = WebAssembly.Module.exports(mod).map((e) => e.name); - expect(exportNames).toContain("_start"); + expect(r.wat).toContain('(import "wasi_snapshot_preview1" "fd_write"'); + expect(r.wat).toContain('(import "wasi_snapshot_preview1" "proc_exit"'); + expect(r.wat).toContain('(export "_start"'); }); test("a NON-throwing wasi module is unaffected: no proc_exit import, still runs", async () => { diff --git a/tests/issue-2980-carrier-fallback.test.ts b/tests/issue-2980-carrier-fallback.test.ts index f5ecb8730b..f685f8cd79 100644 --- a/tests/issue-2980-carrier-fallback.test.ts +++ b/tests/issue-2980-carrier-fallback.test.ts @@ -24,8 +24,9 @@ * * So the CURRENT invariants, asserted from the binary's import section: * 1. all-drivable async-gen module → native carrier, ZERO imports; - * 2. non-drivable async-gen module → host lane fires (legacy `__gen_*` / - * `__get_caught_exception` imports present — no native/legacy mixing); + * 2. non-drivable async-gen module → the generator host lane fires + * (legacy `__gen_*` imports present), while standalone exception handling + * remains native (`__get_caught_exception` absent; #2997); * 3. non-async-gen module → native carrier, no host Promise imports. */ import { describe, expect, it } from "vitest"; @@ -77,7 +78,9 @@ describe("#2980/#3132 Promise-lane carrier gates (standalone, post-widen)", () = } `); expect(imports).toContain("env.__gen_next"); - expect(imports).toContain("env.__get_caught_exception"); + // #2997 moves standalone exception handling to standardized Wasm EH even + // when an unrelated generator shape still requires the legacy host buffer. + expect(imports).not.toContain("env.__get_caught_exception"); }); it("NON-async-gen module: Promise.reject stays NATIVE (widen wins, unchanged since #2980)", async () => { diff --git a/tests/issue-2997-try-table-eh.test.ts b/tests/issue-2997-try-table-eh.test.ts new file mode 100644 index 0000000000..ee8de0ffc2 --- /dev/null +++ b/tests/issue-2997-try-table-eh.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) 2026 Loopdive GmbH. Licensed under Apache-2.0 WITH LLVM-exception. + +import { describe, expect, test } from "vitest"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { compile } from "../src/index.ts"; + +const wasmtime = process.env.WASMTIME ?? "wasmtime"; +const hasWasmtime = spawnSync(wasmtime, ["--version"], { stdio: "ignore" }).status === 0; + +async function runStandalone(source: string): Promise<{ value: number; wat: string; binary: Uint8Array }> { + const result = await compile(source, { fileName: "issue-2997.ts", target: "standalone" }); + expect(result.success, result.errors.map((error) => error.message).join("\n")).toBe(true); + const output = process.env.JS2WASM_TRY_TABLE_WASM_OUTPUT; + if (output) writeFileSync(output, result.binary); + const child = spawnSync( + process.execPath, + [ + "--experimental-wasm-exnref", + "--input-type=module", + "-e", + "const chunks=[]; for await (const chunk of process.stdin) chunks.push(chunk); " + + "const {instance}=await WebAssembly.instantiate(Buffer.concat(chunks),{}); " + + "process.stdout.write(String(instance.exports.run()));", + ], + { input: result.binary, encoding: "utf8" }, + ); + expect(child.status, child.stderr).toBe(0); + return { value: Number(child.stdout), wat: result.wat ?? "", binary: result.binary }; +} + +describe("#2997 — standardized try_table exception handling", () => { + test("catches the module exception tag without legacy try/catch opcodes", async () => { + const result = await runStandalone(` + export function run(): number { + try { + throw 42; + } catch (error) { + return error as number; + } + } + `); + + expect(result.value).toBe(42); + expect(result.wat).toContain("(try_table"); + expect(result.wat).not.toMatch(/\(try(?:\s|$)/); + }); + + test("runs finally on normal, caught, and rethrown completion", async () => { + const result = await runStandalone(` + export function run(): number { + let total = 0; + try { + try { + total += 1; + throw 4; + } catch (error) { + total += error as number; + throw error; + } finally { + total += 10; + } + } catch (error) { + total += error as number; + } finally { + total += 100; + } + return total; + } + `); + + expect(result.value).toBe(119); + expect(result.wat).not.toContain("rethrow"); + }); + + test("retargets break and continue from both try and catch handlers", async () => { + const result = await runStandalone(` + export function run(): number { + let total = 0; + outer: for (let i = 0; i < 5; i += 1) { + try { + if (i === 1) continue outer; + if (i === 3) break outer; + total += i; + } finally { + total += 10; + } + } + outer: for (let i = 0; i < 5; i += 1) { + try { + throw i; + } catch (error) { + if ((error as number) === 1) continue outer; + if ((error as number) === 3) break outer; + total += error as number; + } finally { + total += 10; + } + } + return total; + } + `); + + expect(result.value).toBe(84); + }); + + test("preserves return values while nested finally blocks execute", async () => { + const result = await runStandalone(` + function inner(shouldReturn: boolean): number { + let total = 1; + try { + try { + if (shouldReturn) return 7; + throw 3; + } finally { + total += 10; + } + } catch (error) { + return total + (error as number); + } + } + export function run(): number { + return inner(true) * 100 + inner(false); + } + `); + + expect(result.value).toBe(714); + }); + + test.runIf(hasWasmtime)("executes a WASI try/catch module in Wasmtime", async () => { + const result = await compile( + ` + try { + throw 40; + } catch (error) { + console.log((error as number) + 2); + } + `, + { fileName: "issue-2997-wasi.ts", target: "wasi" }, + ); + expect(result.success, result.errors.map((error) => error.message).join("\n")).toBe(true); + expect(result.wat).toContain("(try_table"); + expect(result.wat).not.toMatch(/\(try(?:\s|$)/); + + const workDir = mkdtempSync(join(tmpdir(), "js2wasm-2997-")); + const wasmPath = join(workDir, "try-table.wasm"); + try { + writeFileSync(wasmPath, result.binary); + const run = spawnSync(wasmtime, ["run", "-W", "gc=y,function-references=y,tail-call=y,exceptions=y", wasmPath], { + encoding: "utf8", + }); + expect(run.status, run.stderr).toBe(0); + expect(run.stdout.trim()).toBe("42"); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/issue-4376-deno-core-bootstrap.test.ts b/tests/issue-4376-deno-core-bootstrap.test.ts new file mode 100644 index 0000000000..a4b4c4ef9c --- /dev/null +++ b/tests/issue-4376-deno-core-bootstrap.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2026 Loopdive GmbH. Licensed under Apache-2.0 WITH LLVM-exception. + +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +describe("#4376 — unchanged deno_core bootstrap graph", () => { + // The exact graph needs more than Vitest's deliberately small 512 MiB fork + // heap. Compile it in a bounded child so an infrastructure OOM cannot be + // mistaken for a compiler verdict and the parent releases all compiler state. + it("boots primordials, infra, and core in one isolated Wasm program", () => { + const probe = fileURLToPath(new URL("./helpers/deno-core-bootstrap-probe.ts", import.meta.url)); + const result = spawnSync( + process.execPath, + ["--max-old-space-size=2048", "--experimental-wasm-exnref", "--import", "tsx", probe], + { + cwd: fileURLToPath(new URL("..", import.meta.url)), + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + }, + ); + expect(result.status, result.stderr || result.stdout).toBe(0); + const report = JSON.parse(result.stdout) as { + bytes: number; + calls: string[]; + hashes: Record; + imports: string[]; + probes: number[]; + }; + expect(report.hashes).toEqual({ + "00_infra.js": "33984000be930f3b02a2d1149ac0319724e8d95891623c8cc74699da4ce97287", + "00_primordials.js": "5a2dfbdc4bb81412575d035901a11788001c7e0110e3f736d16289891af44a52", + "01_core.js": "6e67972322cc5385a2b642a4f7e941fccb6f992c9de662a5111d11fd0aaf1a3a", + }); + expect(report.bytes).toBeGreaterThan(2_000_000); + expect(report.imports).toEqual([ + "env::Promise_all", + "env::Promise_allSettled", + "env::Promise_any", + "env::Promise_new", + "env::Promise_race", + "js2wasm:runtime-eval::__runtime_apply_interpreted", + "js2wasm:runtime-eval::__runtime_indirect_eval", + ]); + expect(report.probes).toEqual([42, 42]); + expect(report.calls).toEqual([]); + }, 30_000); +}); diff --git a/tests/issue-4376-deno-primordials-runtime.test.ts b/tests/issue-4376-deno-primordials-runtime.test.ts new file mode 100644 index 0000000000..abb26e4023 --- /dev/null +++ b/tests/issue-4376-deno-primordials-runtime.test.ts @@ -0,0 +1,343 @@ +// Copyright (c) 2026 Loopdive GmbH. Licensed under Apache-2.0 WITH LLVM-exception. +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { compile } from "../src/index.js"; + +const EXNREF_RUNNER = String.raw` + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + const module = new WebAssembly.Module(Buffer.concat(chunks)); + const imports = WebAssembly.Module.imports(module); + const calls = []; + const importObject = {}; + for (const descriptor of imports) { + importObject[descriptor.module] ??= {}; + importObject[descriptor.module][descriptor.name] = (...args) => { + calls.push(descriptor.module + "::" + descriptor.name + "(" + args.length + ")"); + return null; + }; + } + const instance = await WebAssembly.instantiate(module, importObject); + instance.exports.__module_init(); + process.stdout.write(JSON.stringify({ value: instance.exports.test(), imports, calls })); +`; + +async function runStandaloneJs(source: string): Promise<{ + value: number; + imports: WebAssembly.ModuleImportDescriptor[]; + calls: string[]; +}> { + const result = await compile(source, { + target: "standalone", + platform: "deno", + fileName: "bootstrap.js", + allowJs: true, + skipSemanticDiagnostics: true, + deferTopLevelInit: true, + }); + expect(result.success, result.errors.map((error) => error.message).join("\n")).toBe(true); + + const child = spawnSync( + process.execPath, + ["--experimental-wasm-exnref", "--input-type=module", "--eval", EXNREF_RUNNER], + { input: result.binary, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, + ); + expect(child.status, child.stderr || child.stdout).toBe(0); + return JSON.parse(child.stdout) as { + value: number; + imports: WebAssembly.ModuleImportDescriptor[]; + calls: string[]; + }; +} + +describe("#4376 — Deno primordials standalone runtime substrate", () => { + it("materializes a function that captures a buffer-backed typed-array view", async () => { + const result = await runStandaloneJs(` + let observed = 0; + (() => { + const words = new Uint32Array(2); + const bytes = new Uint8Array(words.buffer); + function readLengths() { + return bytes.length * 5 + words.length; + } + const published = Object.assign({}, { readLengths }); + observed = typeof published.readLengths === "function" ? 42 : 0; + })(); + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("boxes Int32Array storage when a dynamic closure destructures its parameter", async () => { + const result = await runStandaloneJs(` + let observed = 0; + const values = new Int32Array(1); + values[0] = 42; + const consume = function ([value]) { + observed = value; + }; + /** @type {any} */ + const dynamicConsume = consume; + dynamicConsume(values); + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("preserves heterogeneous object entries through callback destructuring", async () => { + const result = await runStandaloneJs(` + let observed = 0; + [ + { name: "TypedArray", original: Reflect.getPrototypeOf(Uint8Array) }, + { + name: "ArrayIterator", + original: { + prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()), + }, + }, + ].forEach(({ name, original }) => { + if (name === "TypedArray" && original.prototype) observed = 42; + }); + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("invokes Object.assign after capture through a primordials carrier", async () => { + const result = await runStandaloneJs(` + const primordials = { ObjectAssign: Object.assign }; + const { ObjectAssign } = primordials; + /** @type {any} */ + const assigned = ObjectAssign({}, { value: 42 }); + /** @returns {number} */ + export function test() { return assigned.value; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("invokes Object.assign after loading it from an any-typed bootstrap carrier", async () => { + const result = await runStandaloneJs(` + /** @type {any} */ + const bootstrap = { primordials: { ObjectAssign: Object.assign } }; + /** @type {any} */ + const global = globalThis; + global.__bootstrap = bootstrap; + const { ObjectAssign } = global.__bootstrap.primordials; + /** @type {any} */ + const target = {}; + const assigned = ObjectAssign(target, { value: 42 }); + /** @returns {number} */ + export function test() { return assigned.value; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("keeps a nested object open when an any-typed primordial publishes it", async () => { + const result = await runStandaloneJs(` + /** @type {any} */ + const ObjectAssign = Object.assign; + const infra = { value: 42 }; + /** @type {any} */ + const global = globalThis; + ObjectAssign(global, { __infra: infra }); + /** @returns {number} */ + export function test() { + return global.__infra === infra && global.__infra.value === 42 ? 42 : 0; + } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("discovers and invokes Object statics through the computed realm-global carrier", async () => { + const result = await runStandaloneJs(` + /** @type {any} */ + const global = globalThis; + /** @type {any} */ + let ObjectAssign; + /** @type {any} */ + let ObjectDefineProperty; + /** @type {any} */ + let ObjectFreeze; + /** @type {any} */ + let ObjectSetPrototypeOf; + ["Object"].forEach((name) => { + const original = global[name]; + for (const key of Reflect.ownKeys(original)) { + if (key === "assign") ObjectAssign = original[key]; + if (key === "defineProperty") ObjectDefineProperty = original[key]; + if (key === "freeze") ObjectFreeze = original[key]; + if (key === "setPrototypeOf") ObjectSetPrototypeOf = original[key]; + } + }); + /** @type {any} */ + const value = {}; + ObjectAssign(value, { first: 1 }); + ObjectDefineProperty(value, "answer", { + value: 42, + writable: true, + enumerable: true, + configurable: true, + }); + ObjectSetPrototypeOf(value, null); + ObjectFreeze(value); + /** @returns {number} */ + export function test() { return value.first === 1 && value.answer === 42 ? 42 : 0; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("resolves shorthand values through the lexical binding instead of the property symbol", async () => { + const result = await runStandaloneJs(` + let observed = 0; + (() => { + const build = { value: 42 }; + function readBuild() { return build.value; } + const carrier = { build }; + observed = carrier.build.value === readBuild() ? 42 : 0; + })(); + /** @returns {number} */ + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("uses one constructible closure layout for self-referential function shorthand", async () => { + const result = await runStandaloneJs(` + let observed = 0; + (() => { + const state = { value: 42 }; + function read() { return read === read ? state.value : 0; } + const carrier = { read }; + observed = carrier.read(); + })(); + /** @returns {number} */ + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("reifies a nested function that captures a growable object", async () => { + const result = await runStandaloneJs(` + let observed = 0; + (() => { + const state = { __proto__: null }; + function write(name, value) { state[name] = value; } + const retained = write; + observed = typeof retained === "function" ? 42 : 0; + })(); + /** @returns {number} */ + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("captures distinct sync and async generator intrinsic prototype objects", async () => { + const result = await runStandaloneJs(` + const Generator = Reflect.getPrototypeOf(function* () {}); + const AsyncGenerator = Reflect.getPrototypeOf(async function* () {}); + export function test() { + return Generator && Generator.prototype && AsyncGenerator && + AsyncGenerator.prototype && Generator !== AsyncGenerator && + Generator.prototype !== AsyncGenerator.prototype ? 42 : 0; + } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("constructs Deno's uncurryThis alias for reflected native methods", async () => { + const result = await runStandaloneJs(` + const { bind, call } = Function.prototype; + const uncurryThis = bind.bind(call); + const Generator = Reflect.getPrototypeOf(function* () {}); + const desc = Reflect.getOwnPropertyDescriptor(Generator.prototype, "next"); + const next = uncurryThis(desc.value); + export function test() { return typeof next === "function" ? 42 : 0; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("stores the eval intrinsic without executing the runtime-eval provider", async () => { + const result = await runStandaloneJs(` + const indirectEval = eval; + export function test() { return typeof indirectEval === "function" ? 42 : 0; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + expect(result.imports.map(({ module, name }) => `${module}::${name}`)).toContain( + "js2wasm:runtime-eval::__runtime_indirect_eval", + ); + }); + + it("forwards TDZ-flagged transitive captures from the lifted caller frame", async () => { + const result = await runStandaloneJs(` + let observed = 0; + (() => { + const p0 = 0, p1 = 1, p2 = 2, p3 = 3, p4 = 4; + const p5 = 5, p6 = 6, p7 = 7, p8 = 8, p9 = 9; + const p10 = 10, p11 = 11, p12 = 12, p13 = 13, p14 = 14; + const p15 = 15, p16 = 16, p17 = 17, p18 = 18, p19 = 19; + + function drainTicks() { queue.value = 42; } + function runImmediates() { + runNextTicks(); + return localQueue.value; + } + const queue = { value: 1 }; + const localQueue = { value: 42 }; + const runNextTicks = drainTicks; + function dispatch() { return runImmediates(); } + + const padding = p0 + p1 + p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9 + + p10 + p11 + p12 + p13 + p14 + p15 + p16 + p17 + p18 + p19; + observed = dispatch() + queue.value * 0 + padding * 0; + })(); + export function test() { return observed; } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); + + it("constructs Deno-style Error subclasses that assign this.name", async () => { + const result = await runStandaloneJs(` + class BadResource extends Error { + constructor(msg, options) { + super(msg, options); + this.name = "BadResource"; + } + } + const error = new BadResource("closed"); + export function test() { + return error.name === "BadResource" && error.message === "closed" ? 42 : 0; + } + `); + + expect(result.value).toBe(42); + expect(result.calls).toEqual([]); + }); +}); diff --git a/tests/issue-4378-array-prototype-iterator-bootstrap.test.ts b/tests/issue-4378-array-prototype-iterator-bootstrap.test.ts index 981877c271..487e81a69b 100644 --- a/tests/issue-4378-array-prototype-iterator-bootstrap.test.ts +++ b/tests/issue-4378-array-prototype-iterator-bootstrap.test.ts @@ -1,18 +1,33 @@ // Copyright (c) 2026 Loopdive GmbH. Licensed under Apache-2.0 WITH LLVM-exception. import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; import { compile } from "../src/index.js"; -async function compileStandalone(source: string) { +const EXNREF_RUNNER = String.raw` + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + const module = new WebAssembly.Module(Buffer.concat(chunks)); + const imports = WebAssembly.Module.imports(module); + if (imports.length !== 0) throw new Error("expected no imports, received " + JSON.stringify(imports)); + const instance = await WebAssembly.instantiate(module, {}); + process.stdout.write(String(instance.exports.probe())); +`; + +async function compileStandalone(source: string): Promise { const result = await compile(source, { target: "standalone", nativeStrings: true }); expect(result.success, result.errors.map((error) => error.message).join("\n")).toBe(true); - expect(WebAssembly.Module.imports(new WebAssembly.Module(result.binary))).toEqual([]); - const { instance } = await WebAssembly.instantiate(result.binary, {}); - return instance.exports as Record; + const child = spawnSync( + process.execPath, + ["--experimental-wasm-exnref", "--input-type=module", "--eval", EXNREF_RUNNER], + { input: result.binary, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, + ); + expect(child.status, child.stderr || child.stdout).toBe(0); + return Number(child.stdout); } describe("#4378 — Deno primordials Array.prototype iterator capture", () => { it("captures the genuine shared array-iterator prototype from the pristine Array.prototype", async () => { - const exports = await compileStandalone(` + const value = await compileStandalone(` export function probe(): number { const captured: any = Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()); const control: any = Object.getPrototypeOf([][Symbol.iterator]()); @@ -21,11 +36,11 @@ describe("#4378 — Deno primordials Array.prototype iterator capture", () => { } `); - expect(exports.probe!()).toBe(42); + expect(value).toBe(42); }); it("does not reinterpret a user binding that shadows Array", async () => { - const exports = await compileStandalone(` + const value = await compileStandalone(` export function probe(): number { const Array = { prototype: [7] }; let result = 0; @@ -34,6 +49,6 @@ describe("#4378 — Deno primordials Array.prototype iterator capture", () => { } `); - expect(exports.probe!()).toBe(7); + expect(value).toBe(7); }); });