Skip to content

runtime: spec-shaped WebAssembly global with graceful rejection (#6558 baseline)#6579

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:feat/6558-wasm-graceful-fail
Jul 18, 2026
Merged

runtime: spec-shaped WebAssembly global with graceful rejection (#6558 baseline)#6579
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:feat/6558-wasm-graceful-fail

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Baseline stage of #6558 (WebAssembly policy for the pi / kimi-code / opencode targets): make the WebAssembly global 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/instantiateStreaming returned undefined (crashing every .then/await consumer 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)

Member Behavior
compile / compileStreaming / instantiate / instantiateStreaming Promise rejected with a WebAssembly.CompileError naming the API and pointing at #6558 — never a crash/hang/sync-throw where the spec says reject
validate(bytes) false (spec-legal; the honest "can I run this here")
new Module(...) throws CompileError synchronously (spec shape for invalid/unsupported)
new Instance(...) throws LinkError
new Table(...) / new Global(...) throw RuntimeError
new Memory({initial}) genuinely works: real zero-filled ArrayBuffer backing (64KiB pages), descriptor validation (TypeError/RangeError arms), working grow(delta)
CompileError / LinkError / RuntimeError real error constructors — instanceof Error, .message, .stack, callable with or without new

All five non-error constructors reject plain calls with requires 'new' (new.target identity check). e instanceof WebAssembly.CompileError works via a new js_instanceof_dynamic hook: the ctor is identified by its dedicated thunk func_ptr (GC-move-safe), the instance by its error .name — needed because these ctors live on the namespace (not globalThis) and their instances are ErrorHeader-backed with no prototype chain to walk.

wasmi opt-in path (issue #76) aligned

Under the wasm-host feature the namespace routes validate/compile/instantiate/Module(+metadata statics) to the real host shims, and the shims now deliver spec-shaped failures:

  • js_webassembly_module_new throws CompileError (was: stderr + undefined)
  • js_webassembly_compile rejects with CompileError (was: plain string reason)
  • js_webassembly_instantiate rejects with TypeError/CompileError/LinkError per failure (was: undefined); the MVP sync-handle success shape is unchanged

Scope note: two spellings, two paths

Literal static spellings (WebAssembly.compile(bytes) written against the global) lower to the #76 HIR intrinsics and auto-link libperry_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-runtime or synthesizing zero-returning perry_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)

  • Unit: 7 new tests in 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). Full cargo test -p perry-runtime -- --test-threads=1 green.
  • Parity fixtures (invalid bytes on purpose so node degrades through the same spec shapes; aliased namespace access on purpose):
    • 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.
    • Existing webassembly-namespace.ts, webassembly-module-metadata.ts fixtures and test_wasm_add.ts (wasmi MVP): still identical / OK.
  • Perry-only e2e: tests/test_webassembly_graceful_fail.sh — a valid wasm module still answers validate → false and 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 resolves null, the program continues and exits 0.

Docs

docs/src/language/limitations.md gains 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

    • Added a spec-shaped WebAssembly global with graceful behavior when no engine is available.
    • Added functional WebAssembly.Memory, including validation, allocation, and grow().
    • Added branded CompileError, LinkError, and RuntimeError constructors with working instanceof checks.
    • WebAssembly compilation and instantiation now report typed synchronous errors or rejected Promises instead of crashing.
  • Documentation

    • Documented WebAssembly limitations, fallback behavior, and runtime requirements.
  • Tests

    • Added coverage for namespace APIs, graceful failures, error branding, and memory behavior.

…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
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee8ab345-6581-40a0-8492-f2b03c7fad32

📥 Commits

Reviewing files that changed from the base of the PR and between 8274cfe and e0289fe.

📒 Files selected for processing (8)
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this_webassembly.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/webassembly.rs
  • docs/src/language/limitations.md
  • test-files/test_gap_6558_webassembly_graceful_fail.ts
  • test-parity/node-suite/globals/webassembly-graceful-degradation.ts
  • tests/test_webassembly_graceful_fail.sh

📝 Walkthrough

Walkthrough

The runtime now exposes a graceful no-engine WebAssembly namespace, including branded errors, Promise rejection behavior, and functional Memory. Host-backed compilation and instantiation use JavaScript error values, with tests and documentation covering the new behavior.

Changes

WebAssembly graceful degradation

Layer / File(s) Summary
WebAssembly namespace and Memory baseline
crates/perry-runtime/src/object/global_this_webassembly.rs
Adds no-engine WebAssembly entry points, branded error constructors, namespace wiring, constructor metadata, and functional Memory allocation and grow() behavior.
Host compilation and Promise error delivery
crates/perry-runtime/src/webassembly.rs
Converts host failures into branded JavaScript errors and delivers synchronous throws or rejected Promises for module and instance operations.
WebAssembly error identity integration
crates/perry-runtime/src/object/global_this.rs, crates/perry-runtime/src/object/instanceof.rs
Connects WebAssembly error constructors to dynamic instanceof branding checks.
Graceful degradation tests and documentation
docs/src/language/limitations.md, test-files/*, test-parity/node-suite/globals/*, tests/*
Tests namespace shape, error branding, async rejection, Memory behavior, and continued execution after failed loading; documents default runtime behavior.

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
Loading

Possibly related PRs

  • PerryTS/perry#6205: Both changes modify dynamic instanceof handling in crates/perry-runtime/src/object/instanceof.rs.

Suggested reviewers: andrewtdiz

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug marked this pull request as ready for review July 18, 2026 10:18
@proggeramlug proggeramlug added the ready PR triaged: CodeRabbit feedback + conflicts addressed label Jul 18, 2026
@proggeramlug
proggeramlug merged commit 7c2f4de into PerryTS:main Jul 18, 2026
11 of 26 checks passed
proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Jul 18, 2026
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
@proggeramlug

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

proggeramlug added a commit that referenced this pull request Jul 18, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready PR triaged: CodeRabbit feedback + conflicts addressed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant