runtime: spec-shaped WebAssembly global with graceful rejection (#6558 baseline)#6579
Conversation
…yTS#6558 baseline) Perry ships no WebAssembly engine by default; before this change the namespace was shape-only — compile/compileStreaming/instantiateStreaming returned undefined (crashing every .then/await consumer at the first property read), constructors were shared no-op thunks returning {}, and the error classes were not Error-like at all. Real-world lazy wasm consumers (photon-node's wasm-bindgen loader, @jsquash/webp, undici's llhttp probe) crashed instead of reaching their own catch/fallback paths. The PerryTS#6558 baseline makes the namespace spec-shaped and graceful: - compile/compileStreaming/instantiate/instantiateStreaming return a Promise REJECTED with a WebAssembly.CompileError whose message names the API and points at issue PerryTS#6558 — never a crash, hang, or sync throw where the spec says reject. - validate(bytes) answers false (spec-legal, and the honest "can I run this here"). - new WebAssembly.Module(...) throws CompileError synchronously; Instance throws LinkError; Table/Global throw RuntimeError; all five reject plain calls with "requires 'new'" (new.target identity check). - new WebAssembly.Memory({initial}) genuinely works: real zero-filled ArrayBuffer backing (64KiB pages), descriptor validation (TypeError/RangeError arms), and a working grow(delta). - CompileError/LinkError/RuntimeError construct real ErrorHeader-backed errors (instanceof Error, .message, .stack) and instanceof against the namespace ctors brand-checks via a new js_instanceof_dynamic hook (ctor identified by thunk func_ptr — GC-move-safe; instance by error .name). The opt-in wasmi host path (issue PerryTS#76, --enable-wasm-runtime / auto-link) is aligned with the same shapes: the namespace routes validate/compile/instantiate/Module(+metadata statics) to the real host shims under the wasm-host feature, and the shims now deliver spec-shaped failures — js_webassembly_module_new throws CompileError (was: stderr + undefined), js_webassembly_compile rejects with CompileError (was: a plain string), js_webassembly_instantiate rejects with TypeError/CompileError/LinkError per failure (was: undefined; the MVP sync-handle success shape is unchanged). Tests (all verified locally): - 7 unit tests in global_this_webassembly.rs (shape, rejection reason + PerryTS#6558 message, error branding, instanceof hook, Memory descriptor + grow arms); cargo test -p perry-runtime -- --test-threads=1 fully green. - test-files/test_gap_6558_webassembly_graceful_fail.ts and test-parity/node-suite/globals/webassembly-graceful-degradation.ts: invalid-bytes fixtures (aliased namespace access) byte-identical to node v26 under BOTH the default and the wasm-host runtime; the existing webassembly-namespace.ts and webassembly-module-metadata.ts fixtures and test_wasm_add.ts stay identical/OK. - tests/test_webassembly_graceful_fail.sh: perry-only e2e — a VALID wasm module still validates false / rejects with the PerryTS#6558 CompileError, the photon-loader pattern degrades to null, the program continues and exits 0. Docs: docs/src/language/limitations.md gains a "WebAssembly (PerryTS#6558)" section stating the policy (WASM-dependent features degrade; full support tracked in PerryTS#6558) and the literal-spelling vs namespace-value path split. Issue: PerryTS#6558
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe runtime now exposes a graceful no-engine WebAssembly namespace, including branded errors, Promise rejection behavior, and functional ChangesWebAssembly graceful degradation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant JavaScript
participant WebAssemblyAPI
participant WasmHost
JavaScript->>WebAssemblyAPI: call compile or instantiate
WebAssemblyAPI->>WasmHost: compile or instantiate bytes
WasmHost-->>WebAssemblyAPI: module, instance, or host error
WebAssemblyAPI-->>JavaScript: resolved value or rejected Promise with branded error
Possibly related PRs
Suggested reviewers: ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Findings from the PR review, each fixed: 1. call.rs — replace the fixed 16-arg over-call trampoline (UB per Rust's ABI model, and it wrote surplus x86-64 stack slots into the callee's frame) with EXACT-ARITY shims: dispatch on the marshalled (n_int, n_float) and transmute to a signature with precisely that many usize + f64 params (9x9 macro-generated shims per return class). The zero-deps register-image design is unchanged; libffi noted as the eventual fully-blessed alternative. Wide-register int passing / f32 bit-image / narrow-return truncation documented as residual assumptions. 2. dlopen.rs — make symbol preparation transactional: validate + resolve the whole table into locals first (prepare_symbols, never throws); on any error dlclose the fresh handle and throw at one site, so repeated malformed dlopen calls can't grow LIBS/SYMS/leaked-name state. 3. dlopen.rs — verify the `args` value is genuinely an Array (js_array_is_array) before reading it as an ArrayHeader. 4. dlopen.rs — bound CString reads: keep the source buffer's span and clamp both the explicit-length slice and the NUL scan to its managed storage (raw numeric pointers stay caller-trusted, as in Bun). 5. types.rs — FFIType cache is now thread-local with per-thread rooting (perry's arena/GC is per-thread) instead of a process-global atomic. 6. tests — C fixture uses unsigned internal arithmetic (no signed-overflow UB at INT32_MAX + 1); wraparound expectation preserved. 7. tests — pty round-trip asserts on a shell-EVALUATED marker (echo FFI_$((40+2))_OK -> FFI_42_OK) the terminal echo can't contain, so input echo can't satisfy the check before the shell runs. 8. tests — duplicate the ABI, error-contract, and type-parse checks as cargo-visible perry-runtime unit tests (9 -> 22) so every-PR CI covers them without the compiled-binary e2e target. 9. docs — stage>=2 exports (toArrayBuffer/JSCallback/CFunction/linkSymbols/ viewSource/read/toBuffer) marked `.stub_note` at the manifest source so the generated .d.ts/reference.md say "not yet implemented, throws"; stub_inventory drift-guard updated (+PerryTS#6562 cluster, +bun:ffi allowlist). 10. docs — anchor() now matches mdbook's normalize_id (drops punctuation instead of turning it into `-`), so the `bun:ffi` TOC link targets `#bunffi` (also fixes slashed/util.types module anchors). Regenerated. Also merged origin/main (bun globals PerryTS#6560, node-pty PerryTS#6563, wasm PerryTS#6579): union-resolved the additive conflicts (bun + bun:ffi + node-pty buckets, NM_BUCKET_COUNT 40). Tests: perry-runtime bun_ffi 22/22; api-manifest incl. stub_inventory 4/4; e2e bun_ffi_stage1 3/3 (tier 1 exact-arity ABI, tier 1b errors, tier 2 real bun-pty shell round-trip); fmt clean; api docs regenerated. # Conflicts: # crates/perry-runtime/src/object/native_module_dispatch.rs # crates/perry-runtime/src/object/native_module_registry.rs # docs/api/perry.d.ts # docs/src/api/reference.md
|
@coderabbitai review |
✅ Action performedReview finished.
|
…ed buffers (#6562) (#6580) * runtime: C-ABI FFI stage 1 — dlopen, typed calls, ptr/CString on pinned buffers (#6562) bun:ffi-shaped module: dlopen(path, table) -> { symbols, close() } with typed per-symbol call stubs, FFIType (Bun-exact numeric values + aliases), ptr(view[, off]) on non-moving buffer storage, CString, suffix. i64/u64 returns are always BigInt; i64_fast/u64_fast number-within-safe-range; pointers are JS numbers. Stage>=2 exports (toArrayBuffer, JSCallback, CFunction, linkSymbols, viewSource, read, toBuffer, FFIType.function) are declared and throw descriptive stage-1 errors. Call stubs are register-image trampolines (no libffi, no link-line changes): scalar args pack densely per register class through one maximal extern "C" signature; correct on SysV x86-64 + AAPCS64 for <=8 int-class + <=8 float-class args, enforced at dlopen time. f32 args travel as bit images; narrow returns truncate to declared width. Pointer lifetime contract: buffer bytes never move (old-arena, TENURED, movable:false, no growth path), so ptr() is stable for the object's lifetime; views resolve through the view registry to the backing bytes; close() poisons symbols instead of leaving a dangling handle. Documented in bun_ffi/mod.rs. Wiring uses the existing native-module machinery: NATIVE_MODULES/ RUNTIME_ONLY_MODULES + manifest (docs regenerated), NmBucket::BunFfi + js_nm_install_bun_ffi (declared in runtime_decls), callable-export check/arity, constants path for FFIType/suffix (cached object is a registered GC root), module keys. Tests: e2e tier 1 (purpose-built C dylib, every FFIType incl. BigInt boundaries, mixed-register ABI, pinned-buffer round trips both ways), tier 1b (error surfaces), tier 2 (real bun-pty 0.4.10 dylib: spawn sh through the pty, echo round-trip, resize, kill; skips offline) + 9 trampoline/marshalling unit tests in perry-runtime. * bun:ffi stage 1: address CodeRabbit review (#6580) Findings from the PR review, each fixed: 1. call.rs — replace the fixed 16-arg over-call trampoline (UB per Rust's ABI model, and it wrote surplus x86-64 stack slots into the callee's frame) with EXACT-ARITY shims: dispatch on the marshalled (n_int, n_float) and transmute to a signature with precisely that many usize + f64 params (9x9 macro-generated shims per return class). The zero-deps register-image design is unchanged; libffi noted as the eventual fully-blessed alternative. Wide-register int passing / f32 bit-image / narrow-return truncation documented as residual assumptions. 2. dlopen.rs — make symbol preparation transactional: validate + resolve the whole table into locals first (prepare_symbols, never throws); on any error dlclose the fresh handle and throw at one site, so repeated malformed dlopen calls can't grow LIBS/SYMS/leaked-name state. 3. dlopen.rs — verify the `args` value is genuinely an Array (js_array_is_array) before reading it as an ArrayHeader. 4. dlopen.rs — bound CString reads: keep the source buffer's span and clamp both the explicit-length slice and the NUL scan to its managed storage (raw numeric pointers stay caller-trusted, as in Bun). 5. types.rs — FFIType cache is now thread-local with per-thread rooting (perry's arena/GC is per-thread) instead of a process-global atomic. 6. tests — C fixture uses unsigned internal arithmetic (no signed-overflow UB at INT32_MAX + 1); wraparound expectation preserved. 7. tests — pty round-trip asserts on a shell-EVALUATED marker (echo FFI_$((40+2))_OK -> FFI_42_OK) the terminal echo can't contain, so input echo can't satisfy the check before the shell runs. 8. tests — duplicate the ABI, error-contract, and type-parse checks as cargo-visible perry-runtime unit tests (9 -> 22) so every-PR CI covers them without the compiled-binary e2e target. 9. docs — stage>=2 exports (toArrayBuffer/JSCallback/CFunction/linkSymbols/ viewSource/read/toBuffer) marked `.stub_note` at the manifest source so the generated .d.ts/reference.md say "not yet implemented, throws"; stub_inventory drift-guard updated (+#6562 cluster, +bun:ffi allowlist). 10. docs — anchor() now matches mdbook's normalize_id (drops punctuation instead of turning it into `-`), so the `bun:ffi` TOC link targets `#bunffi` (also fixes slashed/util.types module anchors). Regenerated. Also merged origin/main (bun globals #6560, node-pty #6563, wasm #6579): union-resolved the additive conflicts (bun + bun:ffi + node-pty buckets, NM_BUCKET_COUNT 40). Tests: perry-runtime bun_ffi 22/22; api-manifest incl. stub_inventory 4/4; e2e bun_ffi_stage1 3/3 (tier 1 exact-arity ABI, tier 1b errors, tier 2 real bun-pty shell round-trip); fmt clean; api docs regenerated. # Conflicts: # crates/perry-runtime/src/object/native_module_dispatch.rs # crates/perry-runtime/src/object/native_module_registry.rs # docs/api/perry.d.ts # docs/src/api/reference.md --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Summary
Baseline stage of #6558 (WebAssembly policy for the pi / kimi-code / opencode targets): make the
WebAssemblyglobal spec-shaped and gracefully failing so lazy wasm consumers (photon-node's wasm-bindgen loader, @jsquash/webp, undici's llhttp probe) hit their own catch/fallback paths instead of crashing the process. This is deliberately not a WASM engine — option (3) from the issue, leaving (1)/(2) open.Before
The namespace was shape-only:
compile/compileStreaming/instantiateStreamingreturnedundefined(crashing every.then/awaitconsumer at the first property read), all constructors were shared no-op thunks returning{}, and the error classes were not Error-like at all.After (default build — no engine linked)
compile/compileStreaming/instantiate/instantiateStreamingWebAssembly.CompileErrornaming the API and pointing at #6558 — never a crash/hang/sync-throw where the spec says rejectvalidate(bytes)false(spec-legal; the honest "can I run this here")new Module(...)CompileErrorsynchronously (spec shape for invalid/unsupported)new Instance(...)LinkErrornew Table(...)/new Global(...)RuntimeErrornew Memory({initial})ArrayBufferbacking (64KiB pages), descriptor validation (TypeError/RangeError arms), workinggrow(delta)CompileError/LinkError/RuntimeErrorinstanceof Error,.message,.stack, callable with or withoutnewAll five non-error constructors reject plain calls with
requires 'new'(new.target identity check).e instanceof WebAssembly.CompileErrorworks via a newjs_instanceof_dynamichook: the ctor is identified by its dedicated thunkfunc_ptr(GC-move-safe), the instance by its error.name— needed because these ctors live on the namespace (notglobalThis) and their instances areErrorHeader-backed with no prototype chain to walk.wasmi opt-in path (issue #76) aligned
Under the
wasm-hostfeature the namespace routesvalidate/compile/instantiate/Module(+metadata statics) to the real host shims, and the shims now deliver spec-shaped failures:js_webassembly_module_newthrows CompileError (was: stderr +undefined)js_webassembly_compilerejects with CompileError (was: plain string reason)js_webassembly_instantiaterejects with TypeError/CompileError/LinkError per failure (was:undefined); the MVP sync-handle success shape is unchangedScope note: two spellings, two paths
Literal static spellings (
WebAssembly.compile(bytes)written against the global) lower to the #76 HIR intrinsics and auto-linklibperry_wasm_host.a— absent the archive that is still a hard build error with an actionable message (unchanged here; changing that is a pipeline policy call for #6558, e.g. gating the intrinsic lowering on--enable-wasm-runtimeor synthesizing zero-returningperry_wasm_host_*stubs, which with this PR's error paths would degrade cleanly). The namespace VALUE path — what minified bundles and the photon loader actually evaluate through — is fully graceful with nothing at link time.Tests (all verified locally, macOS arm64)
global_this_webassembly.rs(member shapes, rejection state + reason branding + runtime: WebAssembly story for target apps (photon, webp, llhttp) — real support or per-module replacement policy #6558 message, error ctor coercion, instanceof hook incl. cross-brand negatives, Memory descriptor + grow arms). Fullcargo test -p perry-runtime -- --test-threads=1green.test-files/test_gap_6558_webassembly_graceful_fail.ts— feature-detection pattern, member shapes, sync throw, clean rejections, photon-loader pattern. Byte-identical to node v26 under BOTH the default and the wasm-host runtime.test-parity/node-suite/globals/webassembly-graceful-degradation.ts— same plus streaming rejections, error-class matrix, Memory zero-fill + grow. Byte-identical to node v26 under both runtimes.webassembly-namespace.ts,webassembly-module-metadata.tsfixtures andtest_wasm_add.ts(wasmi MVP): still identical / OK.tests/test_webassembly_graceful_fail.sh— a valid wasm module still answersvalidate → falseand rejects with the runtime: WebAssembly story for target apps (photon, webp, llhttp) — real support or per-module replacement policy #6558 CompileError (message names the API and the issue), the lazy-loader pattern resolvesnull, the program continues and exits 0.Docs
docs/src/language/limitations.mdgains a "WebAssembly (#6558)" section: WASM-dependent features degrade gracefully; full support tracked in #6558; literal-vs-namespace path split documented.Closes nothing — #6558 stays open as the policy/parity tracker.
Issue: #6558
Summary by CodeRabbit
New Features
WebAssemblyglobal with graceful behavior when no engine is available.WebAssembly.Memory, including validation, allocation, andgrow().CompileError,LinkError, andRuntimeErrorconstructors with workinginstanceofchecks.Documentation
Tests