diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90cc94f048..cd09630ec1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -649,6 +649,16 @@ jobs: python3 scripts/check_gc_env_knobs.py --self-test python3 scripts/check_gc_env_knobs.py + # `this`-as-a-parameter lane, stage 0: every call of a JS function body + # goes through one funnel per side (runtime `closure/body_call.rs`, + # codegen `expr/body_call.rs`), so a body-ABI change is made in one place + # and the compiler finds every caller. + - name: JS body-call funnel + if: ${{ !cancelled() }} + run: | + python3 scripts/check_js_body_call_funnel.py --self-test + python3 scripts/check_js_body_call_funnel.py + # #7982. The in-process LLVM reader's unit gate builds three tracked `.ll` # corpora and asserts they RAN — which proves the tests ran, not that they # test today's IR. All three froze on 2026-08-03 and carried zero diff --git a/Cargo.lock b/Cargo.lock index ab30bfef1b..dff3cb81b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5226,9 +5226,10 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1654" +version = "0.6.0" dependencies = [ "dashmap 6.2.1", + "perry-abi", "perry-native-registration", "perry-runtime", ] diff --git a/Cargo.toml b/Cargo.toml index c2feb89f08..4fb945d012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -529,7 +529,7 @@ perry-hir = { path = "crates/perry-hir" } perry-transform = { path = "crates/perry-transform" } perry-codegen = { path = "crates/perry-codegen" } perry-dispatch = { path = "crates/perry-dispatch" } -perry-abi = { path = "crates/perry-abi" } +perry-abi = { path = "crates/perry-abi", version = "0.5.1654" } # #1112: `perry-ffi` is published for external native wrappers. Its optional # `runtime-link` edge supplies runtime symbols to in-tree adapter tests. # The dependency-free registration core sits below both crates so future @@ -545,7 +545,7 @@ perry-abi = { path = "crates/perry-abi" } # applies — tests and the shipped prebuilt keep every engine. This mirrors why # `wasm-host` must stay out of `default`, generalized to all heavy features. perry-runtime = { path = "crates/perry-runtime", version = "0.5.1011", default-features = false } -perry-ffi = { path = "crates/perry-ffi", version = "0.5.1011" } +perry-ffi = { path = "crates/perry-ffi", version = "0.6.0" } # turnloop P7 (docs/turnloop/p7-report.md): the loop-driven transport the four # database bindings share. rlib only — it is linked *into* each binding's # staticlib, never alongside it, so it exports no symbol of its own. diff --git a/changelog.d/11637-perry-ffi-0.6-typed-js-bodies.md b/changelog.d/11637-perry-ffi-0.6-typed-js-bodies.md new file mode 100644 index 0000000000..4941f32a1c --- /dev/null +++ b/changelog.d/11637-perry-ffi-0.6-typed-js-bodies.md @@ -0,0 +1,29 @@ +**perry-ffi 0.6 (breaking): one JS body ABI, typed.** Every native body a +function object runs is `body(callee, this, a0, ...)`; its Rust type is +defined once, in perry-abi (`js_body_fn_ty!`, `JsBody0`..`JsBody16`, +`JsThis`), and shared by the runtime and perry-ffi. + +- `perry_ffi::alloc_closure`, `register_closure_arity` and the new + `register_closure_rest` take a typed body (`F: JsBody`) + instead of a `*const u8`. A body with the wrong signature — no receiver, a + wrong parameter type, a bare pointer — is a compile error on every + platform. Migrate with a cast to the body's type: + `alloc_closure(my_body as perry_ffi::JsBody1, captures)`, where `my_body` + is `extern "C" fn(*const RawClosureHeader, JsThis, f64) -> f64`. +- Calls into JS take the receiver after the function, `JsThis::UNDEFINED` + for a plain call: `JsClosure::call0..4(this, ...)`, the new + `JsClosure::call_slice(this, &args)` and `perry_ffi::call_value(func, + this, &args)`. The runtime's entries match: `js_closure_call{N}(closure, + this, ...)`, `js_native_call_value(func, this, args, len)`, + `js_closure_call_array(closure, this, args, len)`. No native code reads or + writes an ambient `this`. +- perry-ffi now carries its own version (0.6.0) and depends on perry-abi, + which is published before it (`scripts/publish_perry_ffi.sh`). +- `scripts/check_js_body_call_funnel.py` refuses an `extern` declaration of a + call entry without the receiver (a stale declaration compiles and passes + garbage as `this`), a closure registrar declared outside the runtime, + stdlib and perry-ffi, and a perry-ffi registration function taking a + `*const u8`. It found stale entry declarations in perry-stdlib, the UI + crates and the runtime's own geisterhand registry. +- Every in-tree perry-ffi user (the `perry-ext-*` crates, the UI crates, + perry-audio-miniaudio) is updated. diff --git a/changelog.d/11637-this-as-a-parameter-stage-1.md b/changelog.d/11637-this-as-a-parameter-stage-1.md new file mode 100644 index 0000000000..ccb27dd9e6 --- /dev/null +++ b/changelog.d/11637-this-as-a-parameter-stage-1.md @@ -0,0 +1,37 @@ +Every JS body now takes its receiver as a parameter: a compiled closure body, +a function's value wrapper, a class method's value wrapper and every native +builtin installed as a function object are `double body(i64 callee, i64 this, +double a0, ...)` (`perry_abi::JS_BODY_*`; the receiver is NaN-boxed bits in an +integer register, so every floating-point argument register stays free for +JS arguments). This is stage 1 of passing `this` as a parameter instead of +through the per-agent implicit-`this` cell, and it changes no behavior: +every caller passes exactly the receiver the cell holds for the call — the +method-call site its receiver, a call that bound the cell to `undefined` +`undefined`, the runtime's dispatch paths the cell's current value — and +bodies still read the cell. + +- The runtime's native bodies declare the receiver (`_this: JsThis`, + `closure::JsThis`, `repr(transparent)` over `u64`); perry-ffi exports an + ABI-identical `JsThis` for wrapper crates. The body-call funnel + (`closure/body_call.rs`) passes it on every route: exact arity, padded + arity, rest bundling, the wide ladder, hoisted `DirectCallN` sites, + microtask steps and worker threads. +- Stage 0 (the preceding commit) made one funnel per side for every call of + a body's code pointer: `closure/body_call.rs` in the runtime, + `expr::body_call` in codegen, and deleted the dead, wrong + `js_closure_unbind_this`. +- Codegen spells the ABI once (`expr::body_call::js_body_params` / + `emit_js_body_call`); a stage-0 miss is closed — the inline + `Array.prototype.some` loop called its captureless callback body directly. +- `PERRY_THIS_WITNESS=1` (compile-time, in the object-cache key) makes the + entry of every compiled body that reads the cell compare its `this` + parameter with the cell and report `PERRY_THIS_WITNESS checks=N + mismatches=M` at exit. The gap corpus, tsc and the call-route fixture run + with zero mismatches. +- `scripts/check_js_body_call_funnel.py` now also refuses a body-shaped + `extern "C" fn` that does not declare the receiver, and a function handed + straight to a closure allocator that does not. + +Cost (instructions per call, LTO-off fast build, same host): a closure value +call through the runtime +9 (the dispatcher reads the cell to pass it), a +method-site hit +3. Stage 3 removes the cell and these reads with it. diff --git a/changelog.d/11637-this-as-a-parameter-stage-2.md b/changelog.d/11637-this-as-a-parameter-stage-2.md new file mode 100644 index 0000000000..2e1faa2ef3 --- /dev/null +++ b/changelog.d/11637-this-as-a-parameter-stage-2.md @@ -0,0 +1,22 @@ +A body that reads its dynamic `this` now reads its receiver PARAMETER +(stage 2 of passing `this` as a parameter). The thread-local implicit-`this` +cell is still written by every caller in this commit, so stage 2 can be +measured on its own; stage 3 deletes it. + +- A function expression or object-literal method that reads `this` stores + `%js_this` into its rooted entry `this` slot before the body's first + safepoint. A sloppy body applies OrdinaryCallBindThis once, in place: an + object receiver is tested inline, anything else goes through + `js_this_coerce_sloppy` (nullish -> globalThis, primitive -> wrapper). +- A top-level function that reads `this` is compiled as + `perry_fn_X$this(i64 %js_this, ...)`. The public `perry_fn_X` symbol that + direct calls and other modules name is a forwarder passing `undefined`; the + function's value wrapper passes the receiver it was given. Such functions + are not arena-threaded. +- `__perry_wrap_` forwards its `this` parameter as the method's + receiver, and the runtime's native bodies read their `this` parameter + instead of the cell. + +Instructions per call (LTO-off build, same host) against stage 1: an +object-literal method reading `this` 227 -> 143, an ES5 prototype method +217 -> 134, `forEach` with a `thisArg` 828 -> 744. diff --git a/changelog.d/11637-this-as-a-parameter-stage-3.md b/changelog.d/11637-this-as-a-parameter-stage-3.md new file mode 100644 index 0000000000..12bdcc815e --- /dev/null +++ b/changelog.d/11637-this-as-a-parameter-stage-3.md @@ -0,0 +1,38 @@ +The implicit-`this` cell is gone (stage 3 of passing `this` as a parameter): +a body's `this` parameter is the only way it learns its receiver, so no +caller saves, sets or restores anything around a call. + +- Deleted: the per-agent `IMPLICIT_THIS` cell, `js_implicit_this_get`, + `_get_sloppy` and `_set`, `ImplicitThisScope`, its exception savepoint, + its `HotTls` field and agent-pointer slot, the codegen save/restore + funnel (`rooting::implicit_this_*`), the stage-1 witness + (`PERRY_THIS_WITNESS`), and `js_closure_call1_receiverless` (identical to + `js_closure_call1` once no cell exists). +- A plain call passes `undefined`: `js_closure_call0..16`, + `js_native_call_value` and `js_closure_call_array`. A call with a + receiver uses the new `js_closure_call_this0..16`, + `js_native_call_value_this` and `js_closure_call_array_this`. perry-ffi + gains `JsClosure::call_this0..4` and `JsThis::{UNDEFINED, as_f64, + from_f64}` (additive). +- Runtime routes that used to carry a receiver through the cell now pass it: + `util.promisify`/`callbackify`/`deprecate` wrappers call the original with + their own receiver (Node's `ReflectApply(original, this, args)`), the + legacy `Intl.NumberFormat.call(obj)` / `DateTimeFormat` chain, event + listeners (the emitter), stream and socket methods, N-API + `napi_call_function`. One route never had it and is fixed: an accessor + defined on an `arguments` object now runs with that object as `this` + (`test-files/test_gap_this_param_receiver_routes.ts`). +- Async and generator function expressions bind their receiver at entry like + any other body. They run once per call; the step closures that run across + resumptions capture it lexically. +- `this` in module top-level code is `undefined` in strict code and + globalThis in sloppy code, the value the cell held when nothing set it. +- A method's entry-resolved callback target + (`js_closure_resolve_plain_direct_call`, formerly `_arrow_`) now admits + ordinary functions too: the call passes the plain-call `undefined` + receiver itself, which is all `js_closure_callN` did for them. + +Instructions per call (LTO-off build, same host), main -> this change: a +closure value call 145 -> 46, a two-argument closure call 160 -> 59, a +method-site hit on an object literal 99 -> 79, an object-literal method +reading `this` 223 -> 120, an ES5 prototype method 214 -> 111. diff --git a/crates/perry-abi/src/lib.rs b/crates/perry-abi/src/lib.rs index 61ffa65514..076bf95638 100644 --- a/crates/perry-abi/src/lib.rs +++ b/crates/perry-abi/src/lib.rs @@ -18,18 +18,17 @@ pub const STATIC_SHAPE_ID_COUNT: u32 = 1 << 20; pub const ARRAY_HEADER_SIZE: usize = 8; /// `agent_ptrs::PERRY_AGENT_PTRS`: the number of per-agent pointer slots. -/// Slot 0 is reserved (the megamorphic follow-up's shape-record directory). +/// Slot 0 is reserved (the megamorphic follow-up's shape-record directory); +/// slot 1 held the implicit-`this` cell's address until this-as-a-parameter +/// deleted the cell, and is free; slot 2 is the stack limit. pub const AGENT_PTR_SLOTS: usize = 4; -/// Slot 1: the address of this agent's implicit-`this` cell -/// (`tls_hot::HotTls::implicit_this`), which a direct method call binds. -pub const AGENT_PTR_IMPLICIT_THIS: usize = 1; /// Slot 2: this agent's stack limit (#10812) — not a pointer to anything, the /// lowest frame address a compiled prologue accepts before it throws /// `RangeError: Maximum call stack size exceeded`. Null means unchecked. pub const AGENT_PTR_STACK_LIMIT: usize = 2; -/// `tls_hot::HotTls::agent_ptrs` (Apple aarch64 TSD path; LP64): directly -/// after `implicit_this` (128), behind fixed-size fields only. -pub const HOT_TLS_AGENT_PTRS_OFFSET: usize = 136; +/// `tls_hot::HotTls::agent_ptrs` (Apple aarch64 TSD path; LP64): the first +/// inline value, behind fixed-size fields only. +pub const HOT_TLS_AGENT_PTRS_OFFSET: usize = 128; /// `closure::ClosureHeader` (LP64): the u32 capture count at 0, the ShapeId /// at 4 (the same word as `ObjectHeader`), the code pointer at 8, the shaped @@ -49,6 +48,189 @@ pub const GC_FLAG_FORWARDED: u8 = 0x80; /// `gc::GC_HEADER_SIZE`. pub const GC_HEADER_SIZE: usize = 8; +/// The JS BODY calling convention. Every native body a function object runs — +/// a compiled closure body, a value wrapper, a native builtin installed as a +/// function object, a body an addon registers through perry-ffi — is +/// +/// ```text +/// double body(i64 callee, i64 this, double a0, double a1, ...) +/// ``` +/// +/// where `callee` is the function object (its captures follow the header) and +/// `this` is the NaN-boxed receiver bits ([`JsThis`]), passed in an INTEGER +/// register so every floating-point argument register stays free for JS +/// arguments (SysV x86-64 / AAPCS64; Win64 assigns positionally, which is +/// equally correct). Passing more JS arguments than a body declares is safe +/// (the caller owns the stack argument area); fewer is padded with +/// `undefined` by the caller. Its Rust type is [`js_body_fn_ty!`], defined +/// here and nowhere else. The runtime calls bodies only through +/// `closure/body_call.rs`; emitted code only through +/// `expr::body_call::emit_js_body_call`. +/// +/// The `this` parameter is the only way a body learns its receiver: a +/// method-style caller passes the receiver, a plain call `undefined`. +pub const JS_BODY_CALLEE_PARAM: usize = 0; +/// Native parameter index of the receiver (`this`) bits. +pub const JS_BODY_THIS_PARAM: usize = 1; +/// Native parameter index of the first JS argument. +pub const JS_BODY_FIRST_ARG_PARAM: usize = 2; +/// Native parameters every JS body declares before its JS arguments. +pub const JS_BODY_FIXED_PARAMS: usize = 2; + +/// NaN-boxed `undefined` (`value::TAG_UNDEFINED` in the runtime, which +/// asserts it equals this). +pub const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + +/// The receiver a JS body takes as its second native parameter +/// ([`JS_BODY_THIS_PARAM`]): the NaN-boxed `this` bits, in an integer +/// register (`repr(transparent)` over `u64`, so its ABI is exactly a `u64`'s). +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct JsThis(pub u64); + +impl JsThis { + /// `undefined`: the receiver of a plain (non-method) call. + pub const UNDEFINED: JsThis = JsThis(TAG_UNDEFINED); + + /// The receiver bits. + #[inline(always)] + pub const fn bits(self) -> u64 { + self.0 + } + + /// The receiver as a NaN-boxed value. + #[inline(always)] + pub fn as_f64(self) -> f64 { + f64::from_bits(self.0) + } + + /// A NaN-boxed value as a receiver. + #[inline(always)] + pub fn from_f64(value: f64) -> Self { + JsThis(value.to_bits()) + } +} + +/// THE Rust type of a JS body: `js_body_fn_ty!(Callee; a, b)` is +/// `unsafe extern "C" fn(*const Callee, JsThis, f64, f64) -> f64` — the +/// callee header type, then one `f64` per token. A safe `extern "C" fn` body +/// coerces to it. +#[macro_export] +macro_rules! js_body_fn_ty { + (@f64 $x:tt) => { f64 }; + ($callee:ty; $($x:tt),* $(,)?) => { + unsafe extern "C" fn( + *const $callee, + $crate::JsThis + $(, $crate::js_body_fn_ty!(@f64 $x))* + ) -> f64 + }; +} + +/// A JS body with a statically known JS arity: implemented for exactly the +/// [`js_body_fn_ty!`] pointer types (`JsBody0` .. `JsBody16`), so an API +/// taking `impl JsBody` refuses any other signature — a bare `*const u8`, +/// a body without the receiver, a wrong argument type — at compile time. +/// +/// # Safety +/// Implemented only here, for the body pointer types; `code` is the body's +/// entry address. +pub unsafe trait JsBody: Copy { + /// The JS parameters the body declares. + const ARITY: u32; + /// The body's code address, for the runtime's registries. + fn code(self) -> *const u8; +} + +macro_rules! js_body_types { + ($($alias:ident = $n:literal [$($x:tt),*];)*) => {$( + #[doc = concat!("A JS body declaring ", stringify!($n), " JS parameters.")] + pub type $alias = js_body_fn_ty!(C; $($x),*); + // SAFETY: the pointer type is a JS body type by construction. + unsafe impl JsBody for $alias { + const ARITY: u32 = $n; + #[inline(always)] + fn code(self) -> *const u8 { + self as *const u8 + } + } + )*}; +} + +js_body_types! { + JsBody0 = 0 []; + JsBody1 = 1 [a]; + JsBody2 = 2 [a, a]; + JsBody3 = 3 [a, a, a]; + JsBody4 = 4 [a, a, a, a]; + JsBody5 = 5 [a, a, a, a, a]; + JsBody6 = 6 [a, a, a, a, a, a]; + JsBody7 = 7 [a, a, a, a, a, a, a]; + JsBody8 = 8 [a, a, a, a, a, a, a, a]; + JsBody9 = 9 [a, a, a, a, a, a, a, a, a]; + JsBody10 = 10 [a, a, a, a, a, a, a, a, a, a]; + JsBody11 = 11 [a, a, a, a, a, a, a, a, a, a, a]; + JsBody12 = 12 [a, a, a, a, a, a, a, a, a, a, a, a]; + JsBody13 = 13 [a, a, a, a, a, a, a, a, a, a, a, a, a]; + JsBody14 = 14 [a, a, a, a, a, a, a, a, a, a, a, a, a, a]; + JsBody15 = 15 [a, a, a, a, a, a, a, a, a, a, a, a, a, a, a]; + JsBody16 = 16 [a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a]; +} + +/// `js_closure_call{N}(callee, this, a0..aN-1)` calls a function object with +/// receiver `this` ([`JsThis::UNDEFINED`] for a plain call); it exists for +/// `N <= JS_CLOSURE_CALL_MAX_ARGS`, and wider calls use +/// `js_closure_call_array(callee, this, args, len)`. +pub const JS_CLOSURE_CALL_MAX_ARGS: usize = 16; +/// The fixed-arity entries, indexed by JS argument count. +pub const JS_CLOSURE_CALL_ENTRIES: [&str; JS_CLOSURE_CALL_MAX_ARGS + 1] = [ + "js_closure_call0", + "js_closure_call1", + "js_closure_call2", + "js_closure_call3", + "js_closure_call4", + "js_closure_call5", + "js_closure_call6", + "js_closure_call7", + "js_closure_call8", + "js_closure_call9", + "js_closure_call10", + "js_closure_call11", + "js_closure_call12", + "js_closure_call13", + "js_closure_call14", + "js_closure_call15", + "js_closure_call16", +]; +/// Every runtime entry point native code (emitted or Rust) calls to run a JS +/// function. Each takes the receiver after the function, can run arbitrary JS +/// and therefore collect: `scripts/gc_root_dominance_check.py` reads its +/// poll-capable set from THIS list. +pub const JS_CALL_ENTRIES: [&str; JS_CLOSURE_CALL_MAX_ARGS + 1 + 4] = [ + "js_closure_call0", + "js_closure_call1", + "js_closure_call2", + "js_closure_call3", + "js_closure_call4", + "js_closure_call5", + "js_closure_call6", + "js_closure_call7", + "js_closure_call8", + "js_closure_call9", + "js_closure_call10", + "js_closure_call11", + "js_closure_call12", + "js_closure_call13", + "js_closure_call14", + "js_closure_call15", + "js_closure_call16", + "js_closure_call_array", + "js_closure_call_apply_with_spread", + "js_native_call_value", + // V8's callback trampoline contract (`func(env, args, len)`, no + // receiver): a plain call. + "js_closure_v8_callback", +]; /// `object::method_site::MethodEntry` — the words the emitted method-call site /// reads (`perry-codegen/src/expr/method_site.rs`). pub const METHOD_SITE_WORD_OFFSET: usize = 0; diff --git a/crates/perry-audio-miniaudio/src/lib.rs b/crates/perry-audio-miniaudio/src/lib.rs index 711c1dd834..ca30798ad5 100644 --- a/crates/perry-audio-miniaudio/src/lib.rs +++ b/crates/perry-audio-miniaudio/src/lib.rs @@ -107,7 +107,7 @@ extern "C" { // ============================================================================= extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_promise_run_microtasks() -> i32; fn js_run_stdlib_pump(); @@ -367,7 +367,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -396,7 +396,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } if should_clean { diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index e6561fe798..0976ede7ab 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -7,7 +7,7 @@ use std::time::Instant; use anyhow::{Context, Result}; -use crate::types::{LlvmType, DOUBLE, I64, VOID}; +use crate::types::{LlvmType, DOUBLE, VOID}; use super::artifact_context::{ModuleArtifactsCtx, OptsView}; use super::class_artifacts::{emit_class_artifacts, ClassArtifactsCtx}; @@ -116,7 +116,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { crate::collectors::spec_abi_sites::single_binding_closure_locals(hir) .into_iter() // A closure with a trusted-box clone is better served by the - // ENTRY-RESOLVED path: `js_closure_resolve_arrow_direct_call` + // ENTRY-RESOLVED path: `js_closure_resolve_plain_direct_call` // hands back the trusted clone with its entry-cached box-capture // pointers, which beats the known arm's public/typed call for // capturing bodies (measured: 2.5 vs 5.1 ns). Seeding such an id @@ -304,19 +304,19 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { // so `Expr::SuperPropertyGet` (value-form `super.`) can // materialize them via `js_closure_alloc_singleton(@__perry_wrap_)`. // Methods have signature `perry_method_<...>(this_box, args...)`; - // the closure-call ABI is `(i64 closure, double a0, ...)` and - // doesn't carry a separate `this`. The receiver therefore comes from - // IMPLICIT_THIS, set by the dispatcher right before the call: + // the JS body ABI is `(i64 closure, i64 this, double a0, ...)`, so the + // wrapper forwards its receiver parameter as the method's `this`. The + // receiver is the call site's: // * A method-style invocation of a stored super-method value // (`this._complete = super._complete; obj._complete()` — rxjs's // `OperatorSubscriber` forwarding `complete`/`error`/`next` to the - // base `Subscriber` when no override callback was supplied) sets - // IMPLICIT_THIS to the receiver, so the base method runs with the + // base `Subscriber` when no override callback was supplied) passes + // the receiver, so the base method runs with the // right `this`. Pre-fix this hardcoded `this=undefined`, so the // forwarded `complete` never reached `this.destination.complete()` // and the pipeline stalled (top-level await never settled, #5138). - // * A bare call (`const fn = super.greet; fn(x)`) leaves - // IMPLICIT_THIS undefined, matching strict-mode `this`. + // * A bare call (`const fn = super.greet; fn(x)`) passes `undefined`, + // matching strict-mode `this`. let mut emitted_wrappers: std::collections::HashSet = std::collections::HashSet::new(); for class in &hir.classes { for method in &class.methods { @@ -331,16 +331,15 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { continue; } let arity = method.params.len(); - let mut wrap_params: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; - for i in 0..arity { - wrap_params.push((DOUBLE, format!("%a{}", i))); - } + let wrap_params = + crate::expr::body_call::js_body_params((0..arity).map(|i| format!("%a{}", i))); let wf = llmod.define_function(&wrap_name, DOUBLE, wrap_params); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); - // Forward the call-site receiver (IMPLICIT_THIS) as `this`, - // then the args. See the block comment above (#5138). - let this_box = blk.call(DOUBLE, "js_implicit_this_get", &[]); + // Forward the call-site receiver (the `this` parameter) as the + // method's `this`, then the args. See the block comment above + // (#5138). + let this_box = blk.bitcast_i64_to_double(crate::expr::body_call::JS_BODY_THIS); let mut call_args: Vec<(LlvmType, String)> = Vec::with_capacity(arity + 1); call_args.push((DOUBLE, this_box)); for i in 0..arity { @@ -380,14 +379,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { let wf = llmod.define_function( &wrap_name, DOUBLE, - vec![ - (I64, "%this_closure".to_string()), - (DOUBLE, "%a0".to_string()), - (DOUBLE, "%a1".to_string()), - (DOUBLE, "%a2".to_string()), - (DOUBLE, "%a3".to_string()), - (DOUBLE, "%a4".to_string()), - ], + crate::expr::body_call::js_body_params((0..5).map(|i| format!("%a{i}"))), ); // Fix #420 (v0.5.576): internal linkage keeps unsplit modules' copies // dead-code-eliminable. Split units promote the definition so calls @@ -448,7 +440,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { let wrapper = llmod.define_function( &wrapper_name, DOUBLE, - vec![(I64, "%this_closure".to_string())], + crate::expr::body_call::js_body_params(std::iter::empty::()), ); let _ = wrapper.create_block("entry"); let blk = wrapper.block_mut(0).unwrap(); @@ -465,7 +457,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { let wrapper = llmod.define_function( &wrapper_name, DOUBLE, - vec![(I64, "%this_closure".to_string())], + crate::expr::body_call::js_body_params(std::iter::empty::()), ); let _ = wrapper.create_block("entry"); let blk = wrapper.block_mut(0).unwrap(); @@ -489,7 +481,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { let wrapper = llmod.define_function( &wrapper_name, DOUBLE, - vec![(I64, "%this_closure".to_string())], + crate::expr::body_call::js_body_params(std::iter::empty::()), ); let _ = wrapper.create_block("entry"); let blk = wrapper.block_mut(0).unwrap(); diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index a26563ebb1..0740234241 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -140,12 +140,8 @@ fn emit_public_typed_closure_trampoline( TypedFunctionTrampolineKind::StringRef => typed_param_reps_for_params(params) .unwrap_or_else(|| vec![TypedParamRep::StringRef; params.len()]), }; - let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1); - llvm_params.push((I64, "%this_closure".to_string())); - for p in params { - llvm_params.push((DOUBLE, format!("%arg{}", p.id))); - } let arg_names: Vec = params.iter().map(|p| format!("%arg{}", p.id)).collect(); + let llvm_params = crate::expr::body_call::js_body_params(arg_names.iter().cloned()); let wf = llmod.define_function(&public_name, DOUBLE, llvm_params); let _ = wf.create_block("entry"); @@ -169,12 +165,15 @@ fn emit_public_typed_closure_trampoline( emit_typed_closure_trampoline_fast_value(blk, kind, &typed_name, values, &arg_reps) }, &mut |blk| { - let mut call_args: Vec<(LlvmType, &str)> = Vec::with_capacity(arg_names.len() + 1); - call_args.push((I64, "%this_closure")); - for arg in &arg_names { - call_args.push((DOUBLE, arg.as_str())); - } - blk.call(DOUBLE, generic_body_name, &call_args) + // The generic body is this entry's own continuation: it gets the + // receiver this entry was given. + crate::expr::body_call::emit_js_body_call( + blk, + crate::expr::body_call::JsBody::Symbol(generic_body_name), + crate::expr::body_call::JS_BODY_CALLEE, + crate::expr::body_call::JS_BODY_THIS, + &arg_names, + ) }, ); Ok(()) @@ -407,10 +406,12 @@ pub(super) fn compile_typed_i32_closure( /// Compile a closure body as a top-level LLVM function. /// -/// Signature: `double perry_closure___(i64 this_closure, -/// double arg0, double arg1, …)`. The first parameter is the closure -/// pointer (raw i64); the remaining params are the closure's own -/// declared parameters. +/// Signature: the JS body ABI (`expr::body_call::js_body_params`), +/// `double perry_closure___(i64 this_closure, i64 +/// js_this, double arg0, double arg1, …)`. The first parameter is the +/// closure pointer (raw i64), the second the receiver bits (still read from +/// the implicit-`this` cell, stage 1); the remaining params are the +/// closure's own declared parameters. /// /// Inside the body, captured variables (`closure.captures`) are mapped /// to capture indices and accessed via the runtime @@ -544,15 +545,13 @@ pub(super) fn compile_closure( ordinary_body_name }; - // Param list: i64 this_closure, then each param as double. The private - // versioned-loop clone reuses its proven-unused first callback parameter - // for the caller's stack context, so its ABI and register footprint stay - // identical to the ordinary trusted clone. - let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1); - llvm_params.push((I64, "%this_closure".to_string())); - for p in params { - llvm_params.push((DOUBLE, format!("%arg{}", p.id))); - } + // Param list: the JS body ABI (`i64 %this_closure, i64 %js_this`), then + // each param as double. The private versioned-loop clone reuses its + // proven-unused first callback parameter for the caller's stack context, + // so its ABI and register footprint stay identical to the ordinary + // trusted clone. + let llvm_params = + crate::expr::body_call::js_body_params(params.iter().map(|p| format!("%arg{}", p.id))); let ic_base = llmod.ic_counter; let buffer_alias_base = llmod.buffer_alias_counter; @@ -570,23 +569,21 @@ pub(super) fn compile_closure( // #10906: a non-arrow closure with its OWN `this` binding (a function // expression, or an object-literal method — the closed-shape literal path // lowers every method to exactly this) used to call - // `js_implicit_this_get{,_sloppy}` at EVERY `this` in its body. Bind the + // a thread-local receiver-cell read at EVERY `this` in its body. Bind the // receiver once instead, at entry, into a rooted `this` slot that every // `Expr::This` loads — what OrdinaryCallBindThis does, and what a class // method gets from `%this_arg`. The sloppy conversion (nullish -> // globalThis, primitive -> wrapper) then also runs once, so `this === this` // holds for a primitive receiver instead of boxing a fresh wrapper per read. // - // Async and generator bodies are excluded: their statements run across - // resumptions, and the entry read has not been audited against re-entry. - // Arrows have lexical `this`, which is `captures_this`'s job. + // Async and generator bodies bind it the same way: such a body runs ONCE + // per call (it returns a promise or a generator object), and the step + // closures that do run across resumptions capture the receiver lexically + // (the generator transform sets their `captures_this`). Arrows have + // lexical `this`, which is `captures_this`'s job. let entry_bound_this = !captures_this && enclosing_class.is_none() && !is_arrow - && !is_async - && !is_generator - && !cross_module.local_generator_funcs.contains(&func_id) - && !cross_module.async_step_closures.contains(&func_id) && perry_hir::analysis::body_reads_dynamic_this(body); // gh #6206 / #6081: closures/arrows compiled WITHOUT a shadow frame left @@ -786,10 +783,11 @@ pub(super) fn compile_closure( let class_ref = crate::expr::emit_class_value(blk, class_id); blk.store(DOUBLE, &class_ref, &slot); } else if entry_bound_this { - // A valid non-pointer until the prologue's receiver read below - // fills it, so the slot is safe to bind here with the others. - let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - blk.store(DOUBLE, &undef, &slot); + // The receiver parameter, stored before the body's first + // safepoint so the bind below makes it a rewritten root; a + // sloppy body coerces it in place in the prologue. + let receiver = blk.bitcast_i64_to_double(crate::expr::body_call::JS_BODY_THIS); + blk.store(DOUBLE, &receiver, &slot); } else { blk.store(DOUBLE, "0.0", &slot); } @@ -1133,7 +1131,7 @@ pub(super) fn compile_closure( local_closure_func_ids: HashMap::new(), guard_free_closure_bindings: std::collections::HashSet::new(), local_closure_param_counts: HashMap::new(), - resolved_arrow_callback_targets: HashMap::new(), + resolved_plain_callback_targets: HashMap::new(), resolved_versioned_loop_callback_targets: HashMap::new(), trusted_box_captures, versioned_loop_deopt_context, @@ -1327,23 +1325,18 @@ pub(super) fn compile_closure( super::arguments::ArgumentsCallee::CurrentClosure, ); - // #10906: read the dynamic receiver into the entry `this` slot. It runs + // #10906: the entry `this` slot holds the receiver parameter (stored + // at entry, above). A sloppy body applies OrdinaryCallBindThis to it here, // after every parameter, capture and `arguments` root is bound, because - // the sloppy read can allocate a primitive wrapper — and ahead of the - // first statement, so no user code can have rebound IMPLICIT_THIS yet. - if entry_bound_this { - let helper = if is_strict { - "js_implicit_this_get" - } else { - "js_implicit_this_get_sloppy" - }; + // the coercion can allocate a primitive wrapper — and ahead of the first + // statement. A strict body uses the receiver as passed. + if entry_bound_this && !is_strict { let slot = ctx .this_stack .last() .cloned() .expect("entry-bound `this` has a slot"); - let receiver = ctx.block().call(DOUBLE, helper, &[]); - ctx.block().store(DOUBLE, &receiver, &slot); + crate::expr::body_call::emit_sloppy_receiver_coercion(&mut ctx, &slot); } // #9060 follow-up: resolve loop-called immutable callee bindings once at diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index b2fc7eb7b7..d802639ff0 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -724,7 +724,7 @@ pub(super) fn compile_module_entry( local_closure_func_ids: HashMap::new(), guard_free_closure_bindings: std::collections::HashSet::new(), local_closure_param_counts: HashMap::new(), - resolved_arrow_callback_targets: HashMap::new(), + resolved_plain_callback_targets: HashMap::new(), resolved_versioned_loop_callback_targets: HashMap::new(), trusted_box_captures: false, versioned_loop_deopt_context: None, @@ -1587,7 +1587,7 @@ pub(super) fn compile_module_entry( local_closure_func_ids: HashMap::new(), guard_free_closure_bindings: std::collections::HashSet::new(), local_closure_param_counts: HashMap::new(), - resolved_arrow_callback_targets: HashMap::new(), + resolved_plain_callback_targets: HashMap::new(), resolved_versioned_loop_callback_targets: HashMap::new(), trusted_box_captures: false, versioned_loop_deopt_context: None, diff --git a/crates/perry-codegen/src/codegen/export_value_wrappers.rs b/crates/perry-codegen/src/codegen/export_value_wrappers.rs index fbe768d3c8..ea3c44f725 100644 --- a/crates/perry-codegen/src/codegen/export_value_wrappers.rs +++ b/crates/perry-codegen/src/codegen/export_value_wrappers.rs @@ -87,11 +87,12 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { } // Emit FuncRef-as-value wrappers. For each user function, generate - // a thin wrapper `__perry_wrap_` whose signature matches the - // closure-call ABI: `double(i64 this_closure, double arg0, double - // arg1, ...)`. Most wrappers discard the closure pointer and forward the - // args to the underlying function; generator wrappers reuse it to link the - // returned iterator to the closure-cached `prototype`. + // a thin wrapper `__perry_wrap_` whose signature is the JS body + // ABI: `double(i64 this_closure, i64 js_this, double arg0, double arg1, + // ...)`. Most wrappers discard the closure pointer and forward the args + // to the underlying function; generator wrappers reuse it to link the + // returned iterator to the closure-cached `prototype`. The underlying + // function still reads its receiver from the implicit-`this` cell. // // The wrapper exists so that `apply(add, 3, 4)` can pass `add` as // a value and have `apply` call it via `js_closure_call2`. Without @@ -114,18 +115,14 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { // `js_closure_call_array`. let arity = f.params.len(); let arg_names: Vec = (0..arity).map(|i| format!("%a{}", i)).collect(); - let mut wrap_params: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; - for name in &arg_names { - wrap_params.push((DOUBLE, name.clone())); - } + let wrap_params = crate::expr::body_call::js_body_params(arg_names.iter().cloned()); let wrap_name = format!("__perry_wrap_{}", original_name); let wf = llmod.define_function(&wrap_name, DOUBLE, wrap_params); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); - // Call the underlying function with just the arg doubles. - let call_args: Vec<(LlvmType, &str)> = - arg_names.iter().map(|n| (DOUBLE, n.as_str())).collect(); - let mut result = blk.call(DOUBLE, &original_name, &call_args); + // Call the underlying function with the arg doubles — a this-reading + // function's receiver-taking body with the receiver first. + let mut result = call_function_body(blk, f, &original_name, &arg_names); if function_body_returns_generator_object(&f.body) { result = blk.call( DOUBLE, @@ -166,14 +163,13 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { } let arity = f.params.len(); - let mut params: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; - params.extend((0..arity).map(|i| (DOUBLE, format!("%a{}", i)))); - let alias = llmod.define_function(&alias_wrap, DOUBLE, params); + let params = + crate::expr::body_call::js_body_params((0..arity).map(|i| format!("%a{}", i))); + let alias = llmod.define_function(&alias_wrap, DOUBLE, params.clone()); let _ = alias.create_block("entry"); let blk = alias.block_mut(0).unwrap(); - let mut args: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; - args.extend((0..arity).map(|i| (DOUBLE, format!("%a{}", i)))); - let arg_refs: Vec<(LlvmType, &str)> = args + // Forward every parameter, the receiver included. + let arg_refs: Vec<(LlvmType, &str)> = params .iter() .map(|(ty, value)| (*ty, value.as_str())) .collect(); @@ -245,11 +241,8 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { // fall back to a no-op (variable/class/type rename). if let Some(f) = func_by_local_name.get(local.as_str()) { let arity = f.params.len().min(32); - let mut wrap_params: Vec<(LlvmType, String)> = - vec![(I64, "%this_closure".to_string())]; - for i in 0..arity { - wrap_params.push((DOUBLE, format!("%a{}", i))); - } + let wrap_params = + crate::expr::body_call::js_body_params((0..arity).map(|i| format!("%a{}", i))); let wf = llmod.define_function(&exported_wrap, DOUBLE, wrap_params); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); @@ -257,11 +250,8 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { .get(&f.id) .cloned() .unwrap_or_else(|| scoped_fn_name(module_prefix, &f.name)); - let call_args: Vec<(LlvmType, String)> = - (0..arity).map(|i| (DOUBLE, format!("%a{}", i))).collect(); - let call_args_ref: Vec<(LlvmType, &str)> = - call_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); - let result = blk.call(DOUBLE, &target, &call_args_ref); + let arg_names: Vec = (0..arity).map(|i| format!("%a{}", i)).collect(); + let result = call_function_body(blk, f, &target, &arg_names); blk.ret(DOUBLE, &result); } else { // Variable / class / type rename — no callable function @@ -279,14 +269,7 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { let wf = llmod.define_function( &exported_wrap, DOUBLE, - vec![ - (I64, "%this_closure".to_string()), - (DOUBLE, "%a0".to_string()), - (DOUBLE, "%a1".to_string()), - (DOUBLE, "%a2".to_string()), - (DOUBLE, "%a3".to_string()), - (DOUBLE, "%a4".to_string()), - ], + crate::expr::body_call::js_body_params((0..5).map(|i| format!("%a{i}"))), ); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); @@ -390,11 +373,13 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { let target_wrap = format!("__perry_wrap_{}", target); if !llmod.has_function(&raw_wrap) && emitted_aliases.insert(raw_wrap.clone()) { if llmod.has_function(&target_wrap) { - // Match the canonical wrapper's closure-call ABI (one - // double per declared param), including renamed exports. + // Match the canonical wrapper's JS body ABI (callee, + // receiver, one double per declared param), including + // renamed exports. let arity = function.map(|f| f.params.len()).unwrap_or(5); - let mut params = vec![(I64, "%this_closure".to_string())]; - params.extend((0..arity).map(|i| (DOUBLE, format!("%a{}", i)))); + let params = crate::expr::body_call::js_body_params( + (0..arity).map(|i| format!("%a{}", i)), + ); let wf = llmod.define_function(&raw_wrap, DOUBLE, params.clone()); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); @@ -411,14 +396,9 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { let wf = llmod.define_function( &raw_wrap, DOUBLE, - vec![ - (I64, "%this_closure".to_string()), - (DOUBLE, "%a0".to_string()), - (DOUBLE, "%a1".to_string()), - (DOUBLE, "%a2".to_string()), - (DOUBLE, "%a3".to_string()), - (DOUBLE, "%a4".to_string()), - ], + crate::expr::body_call::js_body_params( + (0..5).map(|i| format!("%a{i}")), + ), ); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); @@ -488,11 +468,9 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { .and_then(|(_, fid)| hir.functions.iter().find(|f| f.id == *fid)); if let Some(f) = aliased_func { let arity = f.params.len().min(32); - let mut wrap_params: Vec<(LlvmType, String)> = - vec![(I64, "%this_closure".to_string())]; - for i in 0..arity { - wrap_params.push((DOUBLE, format!("%a{}", i))); - } + let wrap_params = crate::expr::body_call::js_body_params( + (0..arity).map(|i| format!("%a{}", i)), + ); let wf = llmod.define_function(&exported_wrap, DOUBLE, wrap_params); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); @@ -500,24 +478,17 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { .get(&f.id) .cloned() .unwrap_or_else(|| scoped_fn_name(module_prefix, &f.name)); - let call_args: Vec<(LlvmType, String)> = - (0..arity).map(|i| (DOUBLE, format!("%a{}", i))).collect(); - let call_args_ref: Vec<(LlvmType, &str)> = - call_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); - let result = blk.call(DOUBLE, &target, &call_args_ref); + let arg_names: Vec = + (0..arity).map(|i| format!("%a{}", i)).collect(); + let result = call_function_body(blk, f, &target, &arg_names); blk.ret(DOUBLE, &result); } else { let wf = llmod.define_function( &exported_wrap, DOUBLE, - vec![ - (I64, "%this_closure".to_string()), - (DOUBLE, "%a0".to_string()), - (DOUBLE, "%a1".to_string()), - (DOUBLE, "%a2".to_string()), - (DOUBLE, "%a3".to_string()), - (DOUBLE, "%a4".to_string()), - ], + crate::expr::body_call::js_body_params( + (0..5).map(|i| format!("%a{i}")), + ), ); let _ = wf.create_block("entry"); let blk = wf.block_mut(0).unwrap(); @@ -528,3 +499,27 @@ pub(super) fn emit_export_value_wrappers(c: ExportValueWrapperCtx<'_>) { } } } + +/// Call top-level function `f` (public symbol `target`) from one of its +/// value wrappers with the wrapper's JS arguments. A function whose body +/// reads its dynamic `this` is entered through its receiver-taking body +/// (`codegen/function.rs`) with the wrapper's receiver parameter; any other +/// through its public symbol. +fn call_function_body( + blk: &mut crate::block::LlBlock, + f: &perry_hir::Function, + target: &str, + arg_names: &[String], +) -> String { + let mut call_args: Vec<(LlvmType, &str)> = Vec::with_capacity(arg_names.len() + 1); + let receiver_body; + let callee = if perry_hir::analysis::body_reads_dynamic_this(&f.body) { + call_args.push((I64, crate::expr::body_call::JS_BODY_THIS)); + receiver_body = crate::expr::body_call::receiver_body_name(target); + receiver_body.as_str() + } else { + target + }; + call_args.extend(arg_names.iter().map(|n| (DOUBLE, n.as_str()))); + blk.call(DOUBLE, callee, &call_args) +} diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 4e13d1a5a9..a34fa2d160 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -663,6 +663,28 @@ pub(super) fn compile_function( } else { public_llvm_name.clone() }; + // This-as-a-parameter: a body that reads its dynamic `this` binds it at + // entry into a rooted `this` slot. The ordinary public body becomes a + // receiver-taking body (`perry_fn_*$this`, leading `i64 %js_this`) behind + // a public forwarder that passes `undefined` — what every direct call + // binds — and the value wrapper passes the receiver it was given. A + // specialized entry is only ever called directly, so it binds + // `undefined`. (This-reading functions get no typed clone, spec plan or + // arena threading, so the other variants never meet this.) + let reads_this = cross_module.funcs_reading_dynamic_this.contains(&f.id); + let receiver_body = reads_this && spec_entry.is_none(); + debug_assert!( + !receiver_body + || (typed_public_trampoline.is_none() + && guarded_public_plan.is_none() + && !arena_threaded), + "a this-reading function has no public variant but the plain body" + ); + let llvm_name = if receiver_body { + crate::expr::body_call::receiver_body_name(&public_llvm_name) + } else { + llvm_name + }; // Phase A assumes all user-function params are `double`. Parameter // registers are named `%arg{LocalId}` so the body can store them into @@ -685,6 +707,9 @@ pub(super) fn compile_function( if arena_threaded { params.push((PTR, "%perry_arena_state".to_string())); } + if receiver_body { + params.insert(0, (I64, crate::expr::body_call::JS_BODY_THIS.to_string())); + } let ic_base = llmod.ic_counter; let buffer_alias_base = llmod.buffer_alias_counter; @@ -698,6 +723,7 @@ pub(super) fn compile_function( || guarded_public_plan.is_some() || spec_entry.is_some() || arena_threaded + || receiver_body { lf.linkage = "internal".to_string(); } @@ -717,13 +743,16 @@ pub(super) fn compile_function( cross_module.flat_const_arrays.keys().copied().collect(); let m = crate::collectors::collect_pointer_typed_locals(&f.params, &f.body, &flat_const_ids); + // One more slot roots the entry `this` slot of a this-reading body, + // exactly as `codegen/method.rs` reserves one for `%this_arg`. + let this_root_slots = usize::from(reads_this); crate::codegen::helpers::maybe_spill_roots_to_shadow_frame( lf, &llvm_name, - m.len(), + m.len() + this_root_slots, &f.body, ); - lf.enable_shadow_frame(m.len() as u32); + lf.enable_shadow_frame((m.len() + this_root_slots) as u32); m } else { std::collections::HashMap::new() @@ -867,6 +896,31 @@ pub(super) fn compile_function( }; super::arguments::release_boxed_param_slots_at_exit(lf, &f.params, &boxed_vars, &locals); + // The entry `this` slot: the receiver parameter (or `undefined` for a + // directly-called specialized entry), stored before the body's first + // safepoint and bound so a moving collection rewrites it. A sloppy body + // coerces it in place once the parameters are bound (below). + let this_stack: Vec = if reads_this { + let this_shadow_slot_idx = shadow_slot_map.len() as u32; + let blk = lf.block_mut(0).unwrap(); + let this_slot = blk.alloca(DOUBLE); + let receiver = if receiver_body { + blk.bitcast_i64_to_double(crate::expr::body_call::JS_BODY_THIS) + } else { + crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }; + blk.store(DOUBLE, &receiver, &this_slot); + if precise_root_analysis_enabled() { + blk.call_void( + "js_shadow_slot_bind", + &[(I32, &this_shadow_slot_idx.to_string()), (PTR, &this_slot)], + ); + } + vec![this_slot] + } else { + Vec::new() + }; + // Param types feed local_types so type-aware dispatch (e.g. string // concat detection on a `: string` parameter) works inside the body. // Also seed with module global types so functions that access module @@ -1161,7 +1215,7 @@ pub(super) fn compile_function( label_targets: HashMap::new(), pending_labels: Vec::new(), classes, - this_stack: Vec::new(), + this_stack, super_called_stack: Vec::new(), shared_super_scope_active: false, lexical_this_uses_derived_binding: false, @@ -1205,7 +1259,7 @@ pub(super) fn compile_function( local_closure_func_ids: HashMap::new(), guard_free_closure_bindings: std::collections::HashSet::new(), local_closure_param_counts: HashMap::new(), - resolved_arrow_callback_targets: HashMap::new(), + resolved_plain_callback_targets: HashMap::new(), resolved_versioned_loop_callback_targets: HashMap::new(), trusted_box_captures: false, versioned_loop_deopt_context: None, @@ -1395,6 +1449,10 @@ pub(super) fn compile_function( Some(&f.body), super::arguments::ArgumentsCallee::FunctionWrapper(&wrapper_name), ); + if reads_this && !f.is_strict { + let slot = ctx.this_stack[0].clone(); + crate::expr::body_call::emit_sloppy_receiver_coercion(&mut ctx, &slot); + } // Issue #92 follow-up: pre-register `buffer_data_slots` entries for // `Buffer`-typed function parameters so that the readInt32BE/etc. @@ -1597,6 +1655,33 @@ pub(super) fn compile_function( } } else if arena_threaded { emit_public_arena_threaded_wrapper(llmod, f, &public_llvm_name, &llvm_name); + } else if receiver_body { + emit_public_receiverless_forwarder(llmod, f, &public_llvm_name, &llvm_name); } Ok(()) } + +/// The public `perry_fn_*` symbol of a this-reading function: every direct +/// call binds no receiver, so it forwards to the receiver-taking body with +/// `undefined`. +fn emit_public_receiverless_forwarder( + llmod: &mut LlModule, + f: &Function, + public_name: &str, + receiver_body_name: &str, +) { + let params: Vec<(LlvmType, String)> = f + .params + .iter() + .map(|p| (DOUBLE, format!("%arg{}", p.id))) + .collect(); + let wf = llmod.define_function(public_name, DOUBLE, params.clone()); + wf.force_inline = true; + let _ = wf.create_block("entry"); + let mut call_args: Vec<(LlvmType, &str)> = + vec![(I64, crate::expr::body_call::JS_THIS_UNDEFINED)]; + call_args.extend(params.iter().map(|(ty, a)| (*ty, a.as_str()))); + let blk = wf.block_mut(0).unwrap(); + let value = blk.call(DOUBLE, receiver_body_name, &call_args); + blk.ret(DOUBLE, &value); +} diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index d25c208976..9c9aa5e30c 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -1572,8 +1572,7 @@ pub(super) fn emit_namespace_populator( sanitize_member(source_local) ); let arity = *param_count; - let mut wrapper_params: Vec = vec![I64]; - wrapper_params.extend(std::iter::repeat_n(DOUBLE, arity)); + let wrapper_params = crate::expr::body_call::js_body_param_types(arity); ctx.pending_declares .push((wrapper_name.clone(), DOUBLE, wrapper_params)); let blk = ctx.block(); @@ -1828,7 +1827,7 @@ pub(super) fn callee_binding_resolution_enabled() -> bool { }) } -/// Populate `resolved_arrow_callback_targets` for loop-called immutable callee +/// Populate `resolved_plain_callback_targets` for loop-called immutable callee /// bindings — the generalization of `codegen/method.rs`'s callback-parameter /// resolution to plain function and closure bodies, and to captured bindings /// and module globals. @@ -1877,7 +1876,7 @@ pub(super) fn emit_callee_binding_resolutions( ); for (id, arity) in candidates { if ctx - .resolved_arrow_callback_targets + .resolved_plain_callback_targets .contains_key(&(id, arity)) { continue; @@ -1937,10 +1936,10 @@ pub(super) fn emit_callee_binding_resolutions( let handle = crate::expr::unbox_to_i64(ctx.block(), &value_box); let fn_ptr = ctx.block().call( PTR, - "js_closure_resolve_arrow_direct_call", + "js_closure_resolve_plain_direct_call", &[(I64, &handle), (I32, &arity.to_string())], ); - ctx.resolved_arrow_callback_targets + ctx.resolved_plain_callback_targets .insert((id, arity), fn_ptr); } } diff --git a/crates/perry-codegen/src/codegen/hoisted_callback_method_tests.rs b/crates/perry-codegen/src/codegen/hoisted_callback_method_tests.rs index 80c129fc5d..54352a61e2 100644 --- a/crates/perry-codegen/src/codegen/hoisted_callback_method_tests.rs +++ b/crates/perry-codegen/src/codegen/hoisted_callback_method_tests.rs @@ -137,7 +137,7 @@ fn immutable_alias_resolves_once_without_an_identity_guard_and_keeps_the_fallbac "@perry_method_hoisted_callback_method_ts__Iterator__visit(", ); assert_eq!( - body.matches("@js_closure_resolve_arrow_direct_call(") + body.matches("@js_closure_resolve_plain_direct_call(") .count(), 1, "the method must resolve one target before all callback calls:\n{body}" @@ -168,7 +168,7 @@ fn direct_parameter_call_needs_no_alias_identity_guard() { "@perry_method_hoisted_callback_method_ts__Iterator__visit(", ); assert_eq!( - body.matches("@js_closure_resolve_arrow_direct_call(") + body.matches("@js_closure_resolve_plain_direct_call(") .count(), 1 ); @@ -195,7 +195,7 @@ fn reassigned_callback_parameter_is_not_hoisted() { &ir, "@perry_method_hoisted_callback_method_ts__Iterator__visit(", ); - assert!(!body.contains("@js_closure_resolve_arrow_direct_call(")); + assert!(!body.contains("@js_closure_resolve_plain_direct_call(")); assert!(body.contains("@js_closure_call3(")); } @@ -207,7 +207,7 @@ fn more_than_four_call_arities_declines_the_parameter() { &ir, "@perry_method_hoisted_callback_method_ts__Iterator__visit(", ); - assert!(!body.contains("@js_closure_resolve_arrow_direct_call(")); + assert!(!body.contains("@js_closure_resolve_plain_direct_call(")); for arity in 0..=4 { assert!(body.contains(&format!("@js_closure_call{arity}("))); } @@ -244,5 +244,5 @@ fn nested_closure_calls_are_not_assigned_the_outer_methods_ssa_target() { &ir, "@perry_method_hoisted_callback_method_ts__Iterator__visit(", ); - assert!(!body.contains("@js_closure_resolve_arrow_direct_call(")); + assert!(!body.contains("@js_closure_resolve_plain_direct_call(")); } diff --git a/crates/perry-codegen/src/codegen/literal_method_this_tests.rs b/crates/perry-codegen/src/codegen/literal_method_this_tests.rs index ef1c263a63..b913ec19ec 100644 --- a/crates/perry-codegen/src/codegen/literal_method_this_tests.rs +++ b/crates/perry-codegen/src/codegen/literal_method_this_tests.rs @@ -3,8 +3,8 @@ //! HIR lowers `{ a: 1, m() { … this.a … } }` to //! `new __AnonShape_(1, )`. These pin the two //! halves of the fix: the closure binds its receiver ONCE at entry into a -//! rooted `this` slot (it used to call `js_implicit_this_get_sloppy` at every -//! `this`), and `this.field` nominates the literal's shape class for the +//! rooted `this` slot (it used to read a thread-local receiver cell at every +//! `this`; it now stores its `%js_this` parameter), and `this.field` nominates the literal's shape class for the //! guarded class-field path instead of the generic per-site IC. use crate::{compile_module, CompileOptions}; @@ -176,7 +176,44 @@ fn closure_body(ir: &str, func_id: u32) -> &str { } fn calls_to(body: &str, callee: &str) -> usize { - body.matches(&format!("@{callee}()")).count() + body.matches(&format!("@{callee}(")).count() +} + +/// The register holding the receiver parameter as a double, and the slot it +/// is stored into at entry. +fn entry_receiver_slot(body: &str) -> (String, String) { + let conv = body + .lines() + .find(|l| l.contains("bitcast i64 %js_this to double")) + .unwrap_or_else(|| panic!("the receiver parameter is never read:\n{body}")); + let value = conv.split('=').next().unwrap().trim().to_string(); + // The store is either `store double %v, ptr %slot` or, once the + // root-lowering pass rewrote the slot, the same value through its + // `bitcast` / `inttoptr` pair into `store ptr addrspace(1) %s, ptr %slot`. + let mut names = vec![value.clone()]; + for l in body.lines() { + let l = l.trim(); + let Some((lhs, rhs)) = l.split_once(" = ") else { + continue; + }; + if (rhs.starts_with("bitcast ") || rhs.starts_with("inttoptr ")) + && names.iter().any(|n| rhs.contains(&format!(" {n} to "))) + { + names.push(lhs.trim().to_string()); + } + } + let slot = body + .lines() + .find_map(|l| { + let l = l.trim(); + names.iter().find_map(|n| { + l.strip_prefix(&format!("store double {n}, ptr ")) + .or_else(|| l.strip_prefix(&format!("store ptr addrspace(1) {n}, ptr "))) + .map(|s| s.trim().to_string()) + }) + }) + .unwrap_or_else(|| panic!("receiver {value} is never stored:\n{body}")); + (value, slot) } #[test] @@ -186,27 +223,19 @@ fn literal_method_reads_its_receiver_once_at_entry_into_a_rooted_slot() { let body = closure_body(&ir, METHOD); assert_eq!( - calls_to(body, "js_implicit_this_get_sloppy"), + body.matches("%js_this").count(), + 2, + "three `this` uses must share ONE entry read of the parameter:\n{body}" + ); + assert_eq!( + calls_to(body, "js_this_coerce_sloppy"), 1, - "three `this` uses must share ONE entry read:\n{body}" + "the sloppy coercion runs once, in the prologue:\n{body}" ); - assert_eq!(calls_to(body, "js_implicit_this_get"), 0, "{body}"); // The receiver lands in a slot the shadow frame roots, so an evacuating // minor inside the body rewrites it along with every other root. - let read = body - .lines() - .find(|l| l.contains("@js_implicit_this_get_sloppy()")) - .unwrap(); - let value = read.split('=').next().unwrap().trim(); - let slot = body - .lines() - .find_map(|l| { - l.trim() - .strip_prefix(&format!("store double {value}, ptr ")) - .map(str::trim) - }) - .unwrap_or_else(|| panic!("entry read {value} is never stored:\n{body}")); + let (_, slot) = entry_receiver_slot(body); assert!( body.lines().any(|l| l.contains("@js_shadow_slot_bind(") && l.trim_end().ends_with(&format!("ptr {slot})"))), @@ -218,8 +247,8 @@ fn literal_method_reads_its_receiver_once_at_entry_into_a_rooted_slot() { fn strict_literal_method_binds_the_raw_receiver() { let ir = compile_ir(&literal_module(method(METHOD, false, true))); let body = closure_body(&ir, METHOD); - assert_eq!(calls_to(body, "js_implicit_this_get"), 1, "{body}"); - assert_eq!(calls_to(body, "js_implicit_this_get_sloppy"), 0, "{body}"); + let _ = entry_receiver_slot(body); + assert_eq!(calls_to(body, "js_this_coerce_sloppy"), 0, "{body}"); } #[test] @@ -244,10 +273,13 @@ fn literal_method_this_field_takes_the_guarded_class_field_path() { #[test] fn arrow_keeps_its_per_use_receiver_reads() { // An arrow with no captured `this` is outside the change: lexical `this` - // is `captures_this`'s job, and this body must lower exactly as before. + // is `captures_this`'s job. It never reads the receiver parameter; with + // no enclosing receiver its `this` is module code's (sloppy: globalThis), + // per use. let ir = compile_ir(&literal_module(method(METHOD, true, false))); let body = closure_body(&ir, METHOD); - assert_eq!(calls_to(body, "js_implicit_this_get_sloppy"), 3, "{body}"); + assert!(!body.contains("bitcast i64 %js_this"), "{body}"); + assert_eq!(calls_to(body, "js_this_coerce_sloppy"), 3, "{body}"); assert!(!body.contains("@js_class_field_get_ic("), "{body}"); } diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index a6fddf9a52..86747de879 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -612,7 +612,7 @@ pub(super) fn compile_method( local_closure_func_ids: HashMap::new(), guard_free_closure_bindings: std::collections::HashSet::new(), local_closure_param_counts: HashMap::new(), - resolved_arrow_callback_targets: HashMap::new(), + resolved_plain_callback_targets: HashMap::new(), resolved_versioned_loop_callback_targets: HashMap::new(), trusted_box_captures: false, versioned_loop_deopt_context: None, @@ -804,7 +804,7 @@ pub(super) fn compile_method( }; // Resolve each immutable callback parameter/arity once, before the method - // body. The runtime answers null unless the actual value is a plain arrow + // body. The runtime answers null unless the actual value is a closure // whose declared/rest shape can use this exact call ABI. Exact immutable // aliases (`const cb = callback`) reuse the same answer: every successful // read has that identity by construction, while a pre-initialisation read @@ -824,7 +824,7 @@ pub(super) fn compile_method( let source_handle = crate::expr::unbox_to_i64(ctx.block(), &source_box); let fn_ptr = ctx.block().call( PTR, - "js_closure_resolve_arrow_direct_call", + "js_closure_resolve_plain_direct_call", &[(I64, &source_handle), (I32, &arity.to_string())], ); resolved_callback_ptrs.insert((source_param, arity), fn_ptr); @@ -842,7 +842,7 @@ pub(super) fn compile_method( else { continue; }; - ctx.resolved_arrow_callback_targets + ctx.resolved_plain_callback_targets .insert((call.callee_local, call.arity), fn_ptr); if let Some(versioned_fn_ptr) = resolved_versioned_callback_ptrs .get(&(call.source_param, call.arity)) diff --git a/crates/perry-codegen/src/codegen/method_static.rs b/crates/perry-codegen/src/codegen/method_static.rs index b85c52dd29..e7117f776a 100644 --- a/crates/perry-codegen/src/codegen/method_static.rs +++ b/crates/perry-codegen/src/codegen/method_static.rs @@ -97,8 +97,8 @@ pub(in crate::codegen) fn compile_static_method( // INT32-tagged class-id value `Expr::ClassRef` lowers to) stored in a // `this` slot so `this.x` / `this.#x()` / `this[k]` inside the body // resolve against the class object via the normal dynamic-dispatch - // path. (Previously `this` fell through to `js_implicit_this_get` and - // read back `undefined`.) + // path. (Previously `this` fell through to a receiver-less read and came + // back `undefined`.) let class_ref_cid = class_ids.get(&class.name).copied().unwrap_or(class.id); let (this_slot, locals): (String, HashMap) = { let blk = lf.block_mut(0).unwrap(); @@ -287,7 +287,7 @@ pub(in crate::codegen) fn compile_static_method( local_closure_func_ids: HashMap::new(), guard_free_closure_bindings: std::collections::HashSet::new(), local_closure_param_counts: HashMap::new(), - resolved_arrow_callback_targets: HashMap::new(), + resolved_plain_callback_targets: HashMap::new(), resolved_versioned_loop_callback_targets: HashMap::new(), trusted_box_captures: false, versioned_loop_deopt_context: None, diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 6e795a4f82..29198a2ed3 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -2524,6 +2524,14 @@ fn compile_module_impl( llmod.add_external_module_state_global(shape, I32); } + // A function whose body reads its dynamic `this` gets a receiver-taking + // body (`codegen/function.rs`); the arena-threaded wrapper scheme does + // not compose with it, so those stay ordinary. + let arena_threaded_functions: std::collections::HashSet = + crate::collectors::collect_self_recursive_allocators(hir) + .into_iter() + .filter(|id| !funcs_reading_dynamic_this.contains(id)) + .collect(); let mut cross_module = CrossModuleCtx { namespace_imports: opts.namespace_imports.iter().cloned().collect(), namespace_member_nested: opts.namespace_member_nested.iter().cloned().collect(), @@ -2626,7 +2634,7 @@ fn compile_module_impl( // no call-site cap (the cap prices `inlinehint`'s duplication, which // the inline bump allocator does not incur), plus direct recursion. alloc_hot_functions: crate::collectors::collect_alloc_hot_functions(hir), - arena_threaded_functions: crate::collectors::collect_self_recursive_allocators(hir), + arena_threaded_functions, clamp3_functions: hir .functions .iter() diff --git a/crates/perry-codegen/src/codegen/namespace_value_getters.rs b/crates/perry-codegen/src/codegen/namespace_value_getters.rs index 4b5d14dd0d..6ba5602c04 100644 --- a/crates/perry-codegen/src/codegen/namespace_value_getters.rs +++ b/crates/perry-codegen/src/codegen/namespace_value_getters.rs @@ -3,7 +3,7 @@ use super::helpers::sanitize_member; use super::opts::CrossModuleCtx; use crate::module::LlModule; -use crate::types::{DOUBLE, I64}; +use crate::types::DOUBLE; pub(crate) fn symbol(module_prefix: &str, namespace: &str, member: &str) -> String { format!( @@ -31,8 +31,11 @@ pub(super) fn emit(llmod: &mut LlModule, module_prefix: &str, imports: &CrossMod ); let getter_name = format!("perry_fn_{source_prefix}__{origin}"); llmod.declare_function(&getter_name, DOUBLE, &[]); - let wrapper = - llmod.define_function(wrapper_name, DOUBLE, vec![(I64, "%closure".to_string())]); + let wrapper = llmod.define_function( + wrapper_name, + DOUBLE, + crate::expr::body_call::js_body_params(std::iter::empty::()), + ); wrapper.create_block("entry"); let block = wrapper.block_mut(0).unwrap(); let value = block.call(DOUBLE, &getter_name, &[]); diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index 09f944f6dc..027a9aafcf 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -752,7 +752,7 @@ pub struct ShortSpreadMethodCandidate { pub struct ImportedObjectLiteralMethod { pub name: String, pub func_id: u32, - /// Exact closure-call ABI function stored in the live own slot. This is a + /// Exact JS body (`perry_abi::JS_BODY_*`) stored in the live own slot. This is a /// `perry_closure_*` body for a `this`-capturing method and a /// `__perry_wrap_perry_fn_*` wrapper for HIR's lifted no-`this` method. pub target: String, diff --git a/crates/perry-codegen/src/collectors/hoisted_callback_calls.rs b/crates/perry-codegen/src/collectors/hoisted_callback_calls.rs index c4396e8288..a2405ebb8f 100644 --- a/crates/perry-codegen/src/collectors/hoisted_callback_calls.rs +++ b/crates/perry-codegen/src/collectors/hoisted_callback_calls.rs @@ -1,8 +1,8 @@ //! Immutable method callbacks whose dispatch can be resolved once at entry. //! //! A TypeScript function annotation only nominates a possible callback. The -//! runtime still validates the actual value and admits only plain arrow -//! closures whose arity needs no padding or rest bundling. This collector's +//! runtime still validates the actual value and admits only closures whose +//! arity needs no padding or rest bundling (and no bound-function routing). This collector's //! job is narrower: find direct call arities of immutable callback parameters //! and their immutable local aliases, without entering nested closure bodies. @@ -194,8 +194,8 @@ fn immutable_aliases(root: u32, edges: &[(u32, u32)]) -> HashSet { } /// Find callback parameter/alias call sites that may reuse one entry-resolved -/// arrow target. Runtime validation remains mandatory; annotations never prove -/// the incoming value's class, arity, rest shape, or arrow semantics. +/// target. Runtime validation remains mandatory; annotations never prove the +/// incoming value's class, arity or rest shape. pub(crate) fn collect_hoisted_callback_calls(method: &Function) -> Vec { if method.is_async || method.is_generator @@ -260,7 +260,7 @@ pub(crate) fn collect_hoisted_callback_calls(method: &Function) -> Vec` is emitted by `compile_module` for - // every user function and has the closure-call ABI: it takes - // `(closure_ptr, arg0, arg1, ...)` and forwards to the - // underlying function. + // every user function and has the JS body ABI: it takes + // `(closure_ptr, this, arg0, arg1, ...)` and forwards the + // arguments to the underlying function. Expr::FuncRef(id) => { let wrap_name = ctx.func_names.get(id).map_or_else( || crate::codegen::helpers::unknown_func_wrapper_name(ctx.strings.module_prefix()), diff --git a/crates/perry-codegen/src/expr/body_call.rs b/crates/perry-codegen/src/expr/body_call.rs new file mode 100644 index 0000000000..be64271585 --- /dev/null +++ b/crates/perry-codegen/src/expr/body_call.rs @@ -0,0 +1,169 @@ +//! The only builder of calls to a JS BODY (`perry_abi::JS_BODY_*`): +//! `double body(i64 callee, i64 this, double a0, ...)` — a compiled closure +//! body (`perry_closure_*`, its `$generic` / `$trusted_boxes` / +//! versioned-loop clones), a value wrapper (`__perry_wrap_*`), or any body +//! reached through a function object's code pointer. +//! +//! Every emitted call of such a body goes through [`emit_js_body_call`], and +//! every definition of one declares its parameters through +//! [`js_body_params`], so the body ABI is spelled here and nowhere else. +//! Private typed clones (`$typed_*`, raw-rep parameters) are not JS bodies +//! and are called by their own emitters. `scripts/check_js_body_call_funnel.py` +//! refuses an indirect call built anywhere else. +//! +//! The receiver (`this`) is passed as NaN-boxed bits in an INTEGER register +//! (owner decision D1), so every floating-point argument register stays free +//! for JS arguments. The parameter is the ONLY way a body learns its +//! receiver: there is no thread-local `this` cell for a caller to set or a +//! body to read. + +use crate::block::LlBlock; +use crate::types::{LlvmType, DOUBLE, I64}; + +/// The callee parameter of every JS body definition (`perry_abi::JS_BODY_CALLEE_PARAM`). +pub(crate) const JS_BODY_CALLEE: &str = "%this_closure"; +/// The receiver parameter of every JS body definition +/// (`perry_abi::JS_BODY_THIS_PARAM`): NaN-boxed `this` bits, `i64`. +pub(crate) const JS_BODY_THIS: &str = "%js_this"; + +const _: () = assert!(crate::runtime_abi::JS_BODY_CALLEE_PARAM == 0); +const _: () = assert!(crate::runtime_abi::JS_BODY_THIS_PARAM == 1); +const _: () = assert!(crate::runtime_abi::JS_BODY_FIRST_ARG_PARAM == 2); +const _: () = assert!(crate::runtime_abi::JS_BODY_FIXED_PARAMS == 2); + +/// The native parameter list of a JS body DEFINITION: the callee, the +/// receiver, then one `double` per JS parameter name given. +pub(crate) fn js_body_params(js_params: I) -> Vec<(LlvmType, String)> +where + I: IntoIterator, + S: Into, +{ + let mut out = vec![ + (I64, JS_BODY_CALLEE.to_string()), + (I64, JS_BODY_THIS.to_string()), + ]; + out.extend(js_params.into_iter().map(|name| (DOUBLE, name.into()))); + out +} + +/// The native parameter TYPES of a JS body taking `js_arity` JS arguments, +/// for a declaration of a body defined elsewhere. +pub(crate) fn js_body_param_types(js_arity: usize) -> Vec { + let mut out = vec![I64, I64]; + out.extend(std::iter::repeat_n(DOUBLE, js_arity)); + out +} + +/// What is being called. +#[derive(Clone, Copy)] +pub(crate) enum JsBody<'a> { + /// A body symbol, without the leading `@`. + Symbol(&'a str), + /// A code pointer value (e.g. loaded from a function object or a site). + Pointer(&'a str), +} + +/// The native argument list of a JS body call: the callee, the receiver +/// bits, then the JS arguments, in `perry_abi::JS_BODY_*` order. +fn js_body_args<'a>( + callee: &'a str, + this_bits: &'a str, + args: &'a [String], +) -> Vec<(LlvmType, &'a str)> { + let mut out: Vec<(LlvmType, &str)> = Vec::with_capacity(args.len() + 2); + out.push((I64, callee)); + out.push((I64, this_bits)); + out.extend(args.iter().map(|a| (DOUBLE, a.as_str()))); + out +} + +/// Emit `body(callee, this, args...)` and return the result value. +/// `this_bits` is an `i64` value (NaN-boxed receiver bits). +pub(crate) fn emit_js_body_call( + blk: &mut LlBlock, + body: JsBody<'_>, + callee: &str, + this_bits: &str, + args: &[String], +) -> String { + let native = js_body_args(callee, this_bits, args); + match body { + JsBody::Symbol(name) => blk.call(DOUBLE, name, &native), + JsBody::Pointer(ptr) => blk.call_indirect(DOUBLE, ptr, &native), + } +} + +/// [`emit_js_body_call`] through a code pointer whose caller-side native GC +/// values need not be relocated across the call (`LlBlock::call_indirect_gc_leaf` +/// states when that is sound). +pub(crate) fn emit_js_body_call_gc_leaf( + blk: &mut LlBlock, + code_ptr: &str, + callee: &str, + this_bits: &str, + args: &[String], +) -> String { + let native = js_body_args(callee, this_bits, args); + blk.call_indirect_gc_leaf(DOUBLE, code_ptr, &native) +} + +/// The receiver bits for a call whose callee binds `this` to `undefined` +/// (or whose callee is an arrow and never reads it). +pub(crate) const JS_THIS_UNDEFINED: &str = crate::nanbox::TAG_UNDEFINED_I64; + +/// The value of `this` in code that has no receiver binding of its own and no +/// `this` slot: module top-level code. `undefined` in strict code; in sloppy +/// code OrdinaryCallBindThis's `undefined -> globalThis`, through the same +/// `js_this_coerce_sloppy` a sloppy body's receiver prologue calls (a +/// safepoint). A function, method or closure body that reads `this` must bind +/// it from its receiver parameter (or a lexical capture) at entry instead: a +/// body reaching this has lost the receiver its caller passed. +pub(crate) fn unbound_this_value(ctx: &mut crate::expr::FnCtx<'_>) -> String { + let undefined = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + if ctx.is_strict_fn { + return undefined; + } + ctx.block() + .call(DOUBLE, "js_this_coerce_sloppy", &[(DOUBLE, &undefined)]) +} + +/// The receiver prologue of a SLOPPY body that reads `this` (stage 2 of +/// this-as-a-parameter): `slot` is the body's rooted entry `this` slot, which +/// already holds the receiver parameter's bits (stored before the body's +/// first safepoint, so a moving collection rewrites it). Applies +/// OrdinaryCallBindThis once, in place: an object receiver is kept inline; +/// anything else (`undefined`, `null`, a primitive, a class ref) goes through +/// `js_this_coerce_sloppy`, the only safepoint here. A strict body needs no +/// prologue: its receiver is the parameter as passed. +pub(crate) fn emit_sloppy_receiver_coercion(ctx: &mut crate::expr::FnCtx<'_>, slot: &str) { + let (value, is_object) = { + let blk = ctx.block(); + let value = blk.load(DOUBLE, slot); + let bits = blk.bitcast_double_to_i64(&value); + let top16 = blk.lshr(I64, &bits, "48"); + let is_object = blk.icmp_eq(I64, &top16, crate::nanbox::POINTER_TAG_TOP16_I64); + (value, is_object) + }; + let slow_idx = ctx.new_block("this_prologue.coerce"); + let done_idx = ctx.new_block("this_prologue.done"); + let slow_label = ctx.block_label(slow_idx); + let done_label = ctx.block_label(done_idx); + ctx.block().cond_br(&is_object, &done_label, &slow_label); + ctx.current_block = slow_idx; + { + let blk = ctx.block(); + let coerced = blk.call(DOUBLE, "js_this_coerce_sloppy", &[(DOUBLE, &value)]); + blk.store(DOUBLE, &coerced, slot); + blk.br(&done_label); + } + ctx.current_block = done_idx; +} + +/// The body of a top-level function that reads its dynamic `this`: the +/// function compiled with the receiver as a leading `i64 %js_this` +/// parameter. Its value wrapper passes the receiver it was given; the public +/// `perry_fn_*` symbol every direct call (and every other module) names is a +/// forwarder passing `undefined` — a direct call binds no receiver. +pub(crate) fn receiver_body_name(public_name: &str) -> String { + format!("{public_name}$this") +} diff --git a/crates/perry-codegen/src/expr/call_spread.rs b/crates/perry-codegen/src/expr/call_spread.rs index ac9c85fac1..41fb582883 100644 --- a/crates/perry-codegen/src/expr/call_spread.rs +++ b/crates/perry-codegen/src/expr/call_spread.rs @@ -595,6 +595,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "js_closure_call_apply_with_spread", &[ (DOUBLE, &cb_box), + // `f(...xs)`: a plain call. + (I64, crate::expr::body_call::JS_THIS_UNDEFINED), (crate::types::PTR, ®s_ptr), (I64, ®s_len), (I64, &spread_handle), diff --git a/crates/perry-codegen/src/expr/closure.rs b/crates/perry-codegen/src/expr/closure.rs index d48d56df2e..b82be07315 100644 --- a/crates/perry-codegen/src/expr/closure.rs +++ b/crates/perry-codegen/src/expr/closure.rs @@ -232,8 +232,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // size (real_capture_count) but preserves it in the stored // `capture_count` field. Used by `clone_closure_rebind_this` at // `Object.defineProperty(obj, k, { get(){}, set(){} })` time so - // accessor invocation sees `this === obj` per spec, and by - // `js_closure_unbind_this` for detached method references. + // accessor invocation sees `this === obj` per spec. let cap_count_val = if *captures_this { (total_caps as u32) | 0x8000_0000u32 } else { @@ -359,12 +358,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Some(if let Some(slot) = this_slot { ctx.block().load(DOUBLE, &slot) } else { - // Issue #1845: empty `this_stack` => dynamically-bound - // `this` (computed-key method / function-expression - // receiver). Capture the runtime's `IMPLICIT_THIS`, not - // a 0.0 sentinel. See the matching comment on the - // post-create patch path below. - ctx.block().call(DOUBLE, "js_implicit_this_get", &[]) + // Issue #1845: never a 0.0 sentinel. See the matching + // comment on the post-create patch path below. + crate::expr::body_call::unbound_this_value(ctx) }) } else { None @@ -504,32 +500,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // `Expr::This` reads back 0.0 and any `this.field` access in // the body crashes. // - // Issue #1845: when `this_stack` is empty the enclosing - // function still has a dynamically-bound `this` — this is the - // case for a computed-key method (`[expr](){…}`) which lowers - // to a function-expression closure field (see - // `lower_computed_key_method_as_field`): its body reads `this` - // via the runtime's `IMPLICIT_THIS` (set by `recv[k]()` - // dispatch), NOT via `this_stack`. A direct `Expr::This` in - // such a body already falls back to `js_implicit_this_get` - // (see `this_super_call.rs`); a *nested* arrow that captures - // `this` must capture that same dynamic receiver, otherwise it - // snapshots a bogus 0.0 sentinel. effect's fiber-runtime - // op-dispatch hit this: `[OP_WITH_RUNTIME](op){ internalCall(() - // => op.i0(this, …)) }` passed `this = 0.0` (a raw number) into - // the WithRuntime handler, so `fiber.currentContext` read - // `undefined` and `Effect.all`/`Effect.forEach` died with a - // `{}` FiberFailure. Falling back to `js_implicit_this_get` - // here matches the direct-read path exactly: top-level arrows - // that legitimately have no `this` see `undefined` (the - // `IMPLICIT_THIS` default), not 0.0 — both are non-crashing. + // Issue #1845: an arrow captures its enclosing body's `this`, + // never a 0.0 sentinel. effect's fiber-runtime op-dispatch hit + // the sentinel: `[OP_WITH_RUNTIME](op){ internalCall(() => + // op.i0(this, …)) }` passed `this = 0.0` (a raw number) into the + // WithRuntime handler, so `fiber.currentContext` read `undefined` + // and `Effect.all`/`Effect.forEach` died with a `{}` + // FiberFailure. A body that reads `this` (a computed-key method + // lowered to a function-expression field included) binds it from + // its receiver parameter at entry, so the slot is the enclosing + // `this`; code with no receiver (module top level) takes exactly + // what a direct `Expr::This` there reads. if *captures_this { let this_idx = this_capture_idx.to_string(); let this_slot = ctx.this_stack.last().cloned(); let this_value = if let Some(slot) = this_slot { ctx.block().load(DOUBLE, &slot) } else { - ctx.block().call(DOUBLE, "js_implicit_this_get", &[]) + crate::expr::body_call::unbound_this_value(ctx) }; let blk = ctx.block(); let this_bits = blk.bitcast_double_to_i64(&this_value); diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index 6bee5f3c97..4c0a8608d3 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -477,8 +477,11 @@ fn materialize_compiled_namespace(ctx: &mut FnCtx<'_>, name: &str) -> Result, expr: &Expr) -> Result { .copied() .unwrap_or(0) .min(5); - let mut wrap_param_types: Vec = vec![I64]; - for _ in 0..param_count { - wrap_param_types.push(DOUBLE); - } + let wrap_param_types = crate::expr::body_call::js_body_param_types(param_count); ctx.pending_declares .push((wrap_name.clone(), DOUBLE, wrap_param_types)); let blk = ctx.block(); diff --git a/crates/perry-codegen/src/expr/hot_tls.rs b/crates/perry-codegen/src/expr/hot_tls.rs index cce6559c8c..2b96d0e429 100644 --- a/crates/perry-codegen/src/expr/hot_tls.rs +++ b/crates/perry-codegen/src/expr/hot_tls.rs @@ -3,11 +3,11 @@ //! The runtime keeps its hottest per-thread state in one `HotTls` block whose //! address every thread publishes under a pthread key (`tls_hot.rs`); on this //! platform the pthread TSD array is addressable straight from -//! `TPIDRRO_EL0`, which is how `pthread_getspecific` itself works. Two runtime -//! entries generated code calls on every dynamically-dispatched method call — -//! `js_implicit_this_set` (the save and the restore) — and the once-per- -//! allocating-activation `js_inline_arena_state` do nothing but that lookup -//! plus one load or store, so the lookup is emitted here instead: +//! `TPIDRRO_EL0`, which is how `pthread_getspecific` itself works. Runtime +//! entries that do nothing but that lookup plus one load or store — the +//! once-per-allocating-activation `js_inline_arena_state`, and the agent +//! pointer block read (`agent_ptr.rs`) — have the lookup emitted here +//! instead: //! //! ```text //! key = load @PERRY_HOT_TSD_KEY ; -1 = no direct path @@ -33,8 +33,6 @@ use crate::types::{I64, I8, PTR}; /// `HotTls::inline_state` — see `tls_hot::HOT_TLS_INLINE_STATE_OFFSET`. pub(crate) const HOT_TLS_INLINE_STATE_OFFSET: &str = "8"; -/// `HotTls::implicit_this` — see `tls_hot::HOT_TLS_IMPLICIT_THIS_OFFSET`. -pub(crate) const HOT_TLS_IMPLICIT_THIS_OFFSET: &str = "128"; /// Is the inline hot-cache access available for this compile's target? pub(crate) fn inline_hot_tls_enabled(ctx: &FnCtx<'_>) -> bool { @@ -120,12 +118,5 @@ mod layout_tie_tests { runtime_offset("HOT_TLS_INLINE_STATE_OFFSET"), "codegen emits a stale HotTls::inline_state offset", ); - assert_eq!( - super::HOT_TLS_IMPLICIT_THIS_OFFSET - .parse::() - .unwrap(), - runtime_offset("HOT_TLS_IMPLICIT_THIS_OFFSET"), - "codegen emits a stale HotTls::implicit_this offset", - ); } } diff --git a/crates/perry-codegen/src/expr/js_runtime.rs b/crates/perry-codegen/src/expr/js_runtime.rs index fa6edfea2b..4e2282e11f 100644 --- a/crates/perry-codegen/src/expr/js_runtime.rs +++ b/crates/perry-codegen/src/expr/js_runtime.rs @@ -248,12 +248,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // a JS function whose trampoline (perry-jsruntime/src/interop.rs:993, // `native_callback_trampoline`) calls // `func_ptr(closure_env, args_ptr, args_len)` — but Perry closure - // bodies expect `(closure_ptr, arg0, arg1, ...)` per arity. Bridge + // bodies expect `(callee, this, arg0, arg1, ...)` per arity. Bridge // is the `js_closure_call_array` runtime helper added alongside // (`crates/perry-runtime/src/closure.rs`) which takes the i64 // closure pointer and dispatches to the right `js_closure_callN` // based on `args_len`. Codegen passes: - // func_ptr = ptrtoint @js_closure_call_array to i64 + // func_ptr = ptrtoint @js_closure_v8_callback to i64 // closure_env = unbox(closure) — raw *ClosureHeader as i64 // param_count = static usize from HIR // Result is a NaN-boxed JS handle (V8-handle tag 0x7FFB) that JS @@ -268,11 +268,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let closure_i64 = unbox_to_i64(blk, &closure_dbl); // ptrtoint of a function symbol: assigns a fresh SSA register // and emits the conversion. The resulting i64 is the address - // of `js_closure_call_array`, which we hand to js_create_callback + // of `js_closure_v8_callback`, which we hand to js_create_callback // as its trampoline target. let func_addr = blk.next_reg(); blk.emit_raw(format!( - "{} = ptrtoint ptr @js_closure_call_array to i64", + "{} = ptrtoint ptr @js_closure_v8_callback to i64", func_addr )); let pcount = (*param_count as i64).to_string(); diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index e1f981621b..02ec353a50 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -315,15 +315,22 @@ fn lower_captureless_some_inline( let elem = blk.bitcast_i64_to_double(&elem_bits); let i_double = blk.uitofp(I32, &i, DOUBLE); let recv = nanbox_pointer_inline(blk, &raw); - let mut args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, "0"), (DOUBLE, elem.as_str())]; + let mut args: Vec = vec![elem.clone()]; if param_count >= 2 { - args.push((DOUBLE, i_double.as_str())); + args.push(i_double.clone()); } if param_count >= 3 { - args.push((DOUBLE, recv.as_str())); + args.push(recv.clone()); } - let result = blk.call(DOUBLE, callback_func.trim_start_matches('@'), &args); + // A captureless arrow: no environment (callee 0) and lexical + // `this`, so the receiver parameter is unread. + let result = crate::expr::body_call::emit_js_body_call( + blk, + crate::expr::body_call::JsBody::Symbol(callback_func.trim_start_matches('@')), + "0", + crate::expr::body_call::JS_THIS_UNDEFINED, + &args, + ); let bits = blk.bitcast_double_to_i64(&result); let is_true = blk.icmp_eq(I64, &bits, TAG_TRUE_I64); blk.cond_br(&is_true, &found_l, &slow_l); diff --git a/crates/perry-codegen/src/expr/method_site.rs b/crates/perry-codegen/src/expr/method_site.rs index 7bdf07f658..c904238cb6 100644 --- a/crates/perry-codegen/src/expr/method_site.rs +++ b/crates/perry-codegen/src/expr/method_site.rs @@ -397,7 +397,7 @@ pub(crate) fn emit_method_site( blk.cond_br(&valid, &call_l, &miss_l); (h, f, end) }; - // call: bind `this`, call the body directly, restore. + // call: the body directly, with the receiver as its `this` parameter. ctx.current_block = call_idx; let handle = ctx .block() @@ -406,29 +406,23 @@ pub(crate) fn emit_method_site( .block() .phi(I64, &[(&own_func, &own_end), (&inh_func, &inh_end)]); let fptr = ctx.block().inttoptr(I64, &func); - let cell = crate::rooting::implicit_this_cell_ptr(ctx); - let saved = match &cell { - Some(cell) => crate::rooting::implicit_this_save_at(ctx, cell, recv_box), - None => crate::rooting::implicit_this_save(ctx, recv_box), - }; - let mut call_args: Vec<(crate::types::LlvmType, &str)> = - Vec::with_capacity(lowered_args.len() + 1); - call_args.push((I64, &handle)); - call_args.extend(lowered_args.iter().map(|a| (DOUBLE, a.as_str()))); + let mut call_args: Vec = lowered_args.to_vec(); // Pad with `undefined` up to the arity the prime admits, so a body that // declares a few more parameters than this call passes is entered // directly (`dispatch_with_arity` pads the same way). A body declaring // fewer ignores the extra registers. let undefined = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); let pad = crate::runtime_abi::method_site_padded_argc(lowered_args.len()) - lowered_args.len(); - for _ in 0..pad { - call_args.push((DOUBLE, undefined.as_str())); - } - let hit_value = ctx.block().call_indirect(DOUBLE, &fptr, &call_args); - match &cell { - Some(cell) => crate::rooting::implicit_this_restore_at(ctx, cell, saved), - None => crate::rooting::implicit_this_restore(ctx, saved), - } + call_args.extend(std::iter::repeat_n(undefined, pad)); + // The body gets the receiver as its `this` parameter. + let recv_bits = ctx.block().bitcast_double_to_i64(recv_box); + let hit_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Pointer(&fptr), + &handle, + &recv_bits, + &call_args, + ); let hit_end = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_l); diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 0c8042cd60..19728c0587 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -76,11 +76,16 @@ use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR}; // `lower_expr` and the foundational types (`FnCtx`, `FlatConstInfo`) // remain here. `pub(crate) use` keeps the public surface stable so // existing `crate::expr::X` paths resolve unchanged. +// No emitted reader since the implicit-`this` cell (slot 1) was deleted; kept +// for slot 0, reserved for the megamorphic follow-up's shape-record directory +// (`perry_abi::AGENT_PTR_SLOTS`). +#[allow(dead_code)] pub(crate) mod agent_ptr; pub(crate) mod array_length; mod array_literal; pub(crate) mod array_proto_guard; mod bitset_test; +pub(crate) mod body_call; pub(crate) mod folded_builtin_override; pub(crate) mod hot_tls; mod literal_descriptor; @@ -722,7 +727,7 @@ pub(crate) struct FnCtx<'a> { /// Nullable code pointers resolved once from immutable method callback /// parameters, indexed by callback local (including exact const aliases) /// and call arity. - pub resolved_arrow_callback_targets: std::collections::HashMap<(u32, usize), String>, + pub resolved_plain_callback_targets: std::collections::HashMap<(u32, usize), String>, /// Nullable compiler-private callback targets whose guarded cold arms /// poison a versioned loop before they can run user code. pub resolved_versioned_loop_callback_targets: std::collections::HashMap<(u32, usize), String>, diff --git a/crates/perry-codegen/src/expr/object_literal.rs b/crates/perry-codegen/src/expr/object_literal.rs index 1df756c909..37c0a38459 100644 --- a/crates/perry-codegen/src/expr/object_literal.rs +++ b/crates/perry-codegen/src/expr/object_literal.rs @@ -756,11 +756,20 @@ mod by_name_method_closure_tests { /// The `build` function's body, and only it — the closure bodies are /// separate `define`s and contain none of what is asserted below. pub(super) fn build_fn(ir: &str) -> String { + // `build` reads its dynamic `this` (the analysis counts the methods' + // `captures_this`), so it is compiled as a receiver-taking + // `__build$this` body behind a forwarding `__build`; the lowering + // under test is in the body. + let marker = if ir.contains("__build$this(") { + "__build$this(" + } else { + "__build(" + }; let mut body = Vec::new(); let mut inside = false; for line in ir.lines() { if line.starts_with("define ") { - inside = line.contains("__build("); + inside = line.contains(marker); continue; } if inside { @@ -889,9 +898,9 @@ mod by_name_method_closure_tests { /// the first place. /// /// Counting patch calls over the WHOLE function would be vacuous: closure - /// construction emits `js_closure_set_capture_bits` too (that is how the - /// reserved `this` slot is seeded from `js_implicit_this_get`), so the - /// count only means something in the tail. + /// construction emits `js_closure_set_capture_bits` too (that is how a + /// capturing closure's reserved `this` slot is seeded), so the count only + /// means something in the tail. #[test] fn the_this_patches_run_below_every_property_store() { let _pin = pin_routing("0"); diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index fcd35cf34d..6a5c762d65 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -1245,10 +1245,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .copied() .unwrap_or(0) .min(5); - let mut wrap_param_types: Vec = vec![I64]; - for _ in 0..param_count { - wrap_param_types.push(DOUBLE); - } + let wrap_param_types = + crate::expr::body_call::js_body_param_types(param_count); ctx.pending_declares .push((wrap_name.clone(), DOUBLE, wrap_param_types)); let blk = ctx.block(); diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index e59b29af56..e2643c5489 100644 --- a/crates/perry-codegen/src/expr/super_method.rs +++ b/crates/perry-codegen/src/expr/super_method.rs @@ -307,12 +307,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let recv_v = if let Some(this_slot) = ctx.this_stack.last().cloned() { ctx.block().load(DOUBLE, &this_slot) } else { - let helper = if ctx.is_strict_fn { - "js_implicit_this_get" - } else { - "js_implicit_this_get_sloppy" - }; - ctx.block().call(DOUBLE, helper, &[]) + crate::expr::body_call::unbound_this_value(ctx) }; let key_idx = ctx.strings.intern(property); let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); @@ -340,12 +335,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // dedupes against any later same-TU `define` (`module.rs:67-69` // comment) so this is safe for the same-module case too. The // signature is informational only (runtime dispatches via - // ClosureHeader's func_ptr); use the same `(i64)` + 0 doubles - // shape the imported-function-ref site at `expr/mod.rs:12331` - // uses for unknown-arity imports. + // ClosureHeader's func_ptr); use the JS body ABI with 0 doubles, + // the shape the imported-function-ref sites use for + // unknown-arity imports. let wrap_name = format!("__perry_wrap_{}", fn_name); - ctx.pending_declares - .push((wrap_name.clone(), DOUBLE, vec![I64])); + ctx.pending_declares.push(( + wrap_name.clone(), + DOUBLE, + crate::expr::body_call::js_body_param_types(0), + )); let blk = ctx.block(); let wrap_ptr = format!("@{}", wrap_name); let closure_handle = blk.call(I64, "js_closure_alloc_singleton", &[(PTR, &wrap_ptr)]); @@ -377,12 +375,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let recv_v = if let Some(this_slot) = ctx.this_stack.last().cloned() { ctx.block().load(DOUBLE, &this_slot) } else { - let helper = if ctx.is_strict_fn { - "js_implicit_this_get" - } else { - "js_implicit_this_get_sloppy" - }; - ctx.block().call(DOUBLE, helper, &[]) + crate::expr::body_call::unbound_this_value(ctx) }; let key_v = lower_expr(ctx, key)?; let value_v = lower_expr(ctx, value)?; diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 4e144085f2..18c24033f3 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -178,12 +178,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if let Some(slot) = ctx.this_stack.last().cloned() { Ok(ctx.block().load(DOUBLE, &slot)) } else { - let helper = if ctx.is_strict_fn { - "js_implicit_this_get" - } else { - "js_implicit_this_get_sloppy" - }; - Ok(ctx.block().call(DOUBLE, helper, &[])) + Ok(crate::expr::body_call::unbound_this_value(ctx)) } } Expr::NewTarget => { @@ -718,12 +713,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { (ptr_reg, user_arg_count.to_string()) }; - // Bind IMPLICIT_THIS to the current `this` so the parent - // function body's `this.x = ...` writes land on the - // subclass instance (non-arrow functions read `this` via - // `js_implicit_this_get` when their this_stack is empty). - // Save the prior IMPLICIT_THIS and restore it after — see - // the #519 pattern in console_promise.rs / method_override.rs. + // The parent function is called with the current `this` + // as its receiver, so the parent body's `this.x = ...` + // writes land on the subclass instance. let this_box = match ctx.this_stack.last().cloned() { Some(slot) => ctx.block().load(DOUBLE, &slot), None => { @@ -737,9 +729,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // `GlobalRequest = global.Request`), it allocates the // native fetch handle and stashes it on `this` so // inherited body methods resolve; otherwise it falls - // back to the ordinary implicit-`this`-bound - // `js_native_call_value` (unchanged behavior for - // every other runtime-value parent). + // back to an ordinary call of the parent with + // `this_box` as its receiver (unchanged behavior + // for every other runtime-value parent). let nt_save = crate::rooting::new_target_save_for_super(ctx); let parent_result = ctx.block().call( DOUBLE, diff --git a/crates/perry-codegen/src/gc_call_effects.rs b/crates/perry-codegen/src/gc_call_effects.rs index a73f514850..8e6a3a060c 100644 --- a/crates/perry-codegen/src/gc_call_effects.rs +++ b/crates/perry-codegen/src/gc_call_effects.rs @@ -487,7 +487,6 @@ mod tests { "js_i32_box_get", "js_box_release", "js_box_scope_release", - "js_implicit_this_get", "js_tdz_suppress_begin", "js_tdz_suppress_end", ]; @@ -788,7 +787,7 @@ mod tests { "js_object_get_field_ic_miss_packed", "js_object_get_field_ic_slow", "js_object_get_field_ic_nonptr", - "js_implicit_this_get_sloppy", + "js_this_coerce_sloppy", "js_box_get_bits", "js_box_get_bits_trusted", "js_box_get_bits_named", diff --git a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv index 9d6ab0aeef..b8169c8d43 100644 --- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv @@ -622,7 +622,6 @@ js_closure_call13 Reenters js_closure_call14 Reenters js_closure_call15 Reenters js_closure_call16 Reenters -js_closure_call1_receiverless Reenters js_closure_call2 Reenters js_closure_call3 Reenters js_closure_call4 Reenters @@ -638,15 +637,15 @@ js_closure_get_capture_bits Leaf js_closure_get_capture_f64 Leaf js_closure_get_capture_ptr Leaf js_closure_get_func Leaf -js_closure_resolve_arrow_direct_call Leaf +js_closure_resolve_plain_direct_call Leaf js_closure_resolve_versioned_loop_direct_call Leaf js_closure_set_box_capture_ptr Leaf js_closure_set_capture_bits Leaf js_closure_set_capture_f64 Leaf js_closure_set_capture_ptr Leaf -js_closure_unbind_this AllocOnly js_closure_unbox_callee_checked Reenters js_closure_unbox_callee_checked_rebind Reenters +js_closure_v8_callback Reenters js_cluster_disconnect Reenters js_cluster_emit Reenters js_cluster_event_names Reenters @@ -1454,9 +1453,6 @@ js_i32_box_scope_release Leaf js_i32_box_set Leaf js_ic_diag_note_hit Leaf js_immediate_has_pending Leaf -js_implicit_this_get Leaf -js_implicit_this_get_sloppy Reenters -js_implicit_this_set Leaf js_import_meta_resolve Reenters js_import_meta_resolve_value Reenters js_in_operator Reenters @@ -3240,6 +3236,7 @@ js_text_encoder_encode_llvm Reenters js_text_encoder_new Reenters js_text_encoder_stream_new Reenters js_text_encoding_stream_new Reenters +js_this_coerce_sloppy Reenters js_thread_has_pending Leaf js_thread_parallel_filter Reenters js_thread_parallel_map Reenters @@ -3930,7 +3927,6 @@ perry_i18n_plural_category Reenters perry_i18n_set_currencies Leaf perry_i18n_set_locale_index Leaf perry_i18n_set_plural_locales Leaf -perry_implicit_this_cell Leaf perry_init_guard_check_and_set Leaf perry_key_content_hash Leaf perry_macos_bundle_chdir Leaf diff --git a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv index 416ac68806..2910e150eb 100644 --- a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv +++ b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv @@ -622,7 +622,6 @@ js_closure_call13 Reenters js_closure_call14 Reenters js_closure_call15 Reenters js_closure_call16 Reenters -js_closure_call1_receiverless Reenters js_closure_call2 Reenters js_closure_call3 Reenters js_closure_call4 Reenters @@ -638,15 +637,15 @@ js_closure_get_capture_bits Leaf js_closure_get_capture_f64 Leaf js_closure_get_capture_ptr Leaf js_closure_get_func Leaf -js_closure_resolve_arrow_direct_call Leaf +js_closure_resolve_plain_direct_call Leaf js_closure_resolve_versioned_loop_direct_call Leaf js_closure_set_box_capture_ptr Leaf js_closure_set_capture_bits Leaf js_closure_set_capture_f64 Leaf js_closure_set_capture_ptr Leaf -js_closure_unbind_this Reenters js_closure_unbox_callee_checked ThrowOnly js_closure_unbox_callee_checked_rebind Reenters +js_closure_v8_callback Reenters js_cluster_disconnect Reenters js_cluster_emit Reenters js_cluster_event_names Reenters @@ -1454,9 +1453,6 @@ js_i32_box_scope_release Leaf js_i32_box_set Leaf js_ic_diag_note_hit Leaf js_immediate_has_pending Leaf -js_implicit_this_get Leaf -js_implicit_this_get_sloppy Reenters -js_implicit_this_set Leaf js_import_meta_resolve Reenters js_import_meta_resolve_value Reenters js_in_operator Reenters @@ -3233,6 +3229,7 @@ js_text_encoder_encode_llvm Reenters js_text_encoder_new Reenters js_text_encoder_stream_new Reenters js_text_encoding_stream_new Reenters +js_this_coerce_sloppy Reenters js_thread_has_pending Leaf js_thread_parallel_filter Reenters js_thread_parallel_map Reenters @@ -3925,7 +3922,6 @@ perry_i18n_plural_category Reenters perry_i18n_set_currencies Leaf perry_i18n_set_locale_index Leaf perry_i18n_set_plural_locales Leaf -perry_implicit_this_cell Leaf perry_init_guard_check_and_set Leaf perry_key_content_hash Leaf perry_macos_bundle_chdir Reenters diff --git a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv index 784bc9a415..6db7fc1fa6 100644 --- a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv @@ -622,7 +622,6 @@ js_closure_call13 Reenters js_closure_call14 Reenters js_closure_call15 Reenters js_closure_call16 Reenters -js_closure_call1_receiverless Reenters js_closure_call2 Reenters js_closure_call3 Reenters js_closure_call4 Reenters @@ -638,15 +637,15 @@ js_closure_get_capture_bits Leaf js_closure_get_capture_f64 Leaf js_closure_get_capture_ptr Leaf js_closure_get_func Leaf -js_closure_resolve_arrow_direct_call Leaf +js_closure_resolve_plain_direct_call Leaf js_closure_resolve_versioned_loop_direct_call Leaf js_closure_set_box_capture_ptr Leaf js_closure_set_capture_bits Leaf js_closure_set_capture_f64 Leaf js_closure_set_capture_ptr Leaf -js_closure_unbind_this Reenters js_closure_unbox_callee_checked Reenters js_closure_unbox_callee_checked_rebind Reenters +js_closure_v8_callback Reenters js_cluster_disconnect Reenters js_cluster_emit Reenters js_cluster_event_names Reenters @@ -1454,9 +1453,6 @@ js_i32_box_scope_release Leaf js_i32_box_set Leaf js_ic_diag_note_hit Leaf js_immediate_has_pending Leaf -js_implicit_this_get Leaf -js_implicit_this_get_sloppy Reenters -js_implicit_this_set Leaf js_import_meta_resolve Reenters js_import_meta_resolve_value Reenters js_in_operator Reenters @@ -3233,6 +3229,7 @@ js_text_encoder_encode_llvm Reenters js_text_encoder_new Reenters js_text_encoder_stream_new Reenters js_text_encoding_stream_new Reenters +js_this_coerce_sloppy Reenters js_thread_has_pending Leaf js_thread_parallel_filter Reenters js_thread_parallel_map Reenters @@ -3919,7 +3916,6 @@ perry_i18n_plural_category Reenters perry_i18n_set_currencies Leaf perry_i18n_set_locale_index Leaf perry_i18n_set_plural_locales Leaf -perry_implicit_this_cell Leaf perry_init_guard_check_and_set Leaf perry_key_content_hash Leaf perry_macos_bundle_chdir Leaf diff --git a/crates/perry-codegen/src/lower_call/atomics.rs b/crates/perry-codegen/src/lower_call/atomics.rs index 84a5654cee..586168d325 100644 --- a/crates/perry-codegen/src/lower_call/atomics.rs +++ b/crates/perry-codegen/src/lower_call/atomics.rs @@ -5,7 +5,6 @@ use perry_hir::Expr; use crate::expr::{lower_expr, FnCtx}; use crate::nanbox::double_literal; -use crate::types::{DOUBLE, PTR}; fn is_global_this_atomics_expr(e: &Expr) -> bool { matches!( @@ -60,9 +59,15 @@ pub fn try_lower_atomics_static_call( let _ = lower_expr(ctx, arg)?; } - let mut call_args: Vec<(crate::types::LlvmType, &str)> = vec![(PTR, "null")]; - for value in &lowered { - call_args.push((DOUBLE, value.as_str())); - } - Ok(Some(ctx.block().call(DOUBLE, runtime_fn, &call_args))) + // The `js_atomics_*` natives are JS bodies (the function objects + // `Atomics.load` &c. run): no environment, and they never read their + // receiver. + let this_bits = crate::expr::body_call::JS_THIS_UNDEFINED; + Ok(Some(crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(runtime_fn), + "0", + this_bits, + &lowered, + ))) } diff --git a/crates/perry-codegen/src/lower_call/console_promise.rs b/crates/perry-codegen/src/lower_call/console_promise.rs index deab6ac180..2c14ec7c2f 100644 --- a/crates/perry-codegen/src/lower_call/console_promise.rs +++ b/crates/perry-codegen/src/lower_call/console_promise.rs @@ -14,8 +14,7 @@ use perry_hir::types::Type as HirType; use perry_hir::Expr; use crate::rooting::{ - any_operand_may_collect, implicit_this_restore, implicit_this_save, operand_may_collect, - with_operands_rooted, with_rooted_group, + any_operand_may_collect, operand_may_collect, with_operands_rooted, with_rooted_group, }; use crate::expr::{ @@ -1364,21 +1363,17 @@ pub fn try_lower_closure_call_fallthrough( // The runtime checks the closure header on its own — if the value // isn't actually a closure, js_closure_call handles the error. // Issue #519: when the callee shape is `recv.method(args)` (a - // PropertyGet) — i.e. a method-style invocation — bind the - // receiver as the implicit `this` for the duration of the call. - // Non-arrow function bodies (including FuncRef wrappers) read - // `this` via `js_implicit_this_get` when their lexical - // this_stack is empty (codegen Expr::This fallback). Without - // this save/set/restore, hono's `RegExpRouter.match = match` - // (where `match` is an imported function declaration whose - // body does `this.buildAllMatchers()`) sees `this = undefined` - // and TypeErrors out at the first chained method call. + // PropertyGet) — i.e. a method-style invocation — pass the receiver as + // the body's `this` (`js_closure_call{N}(closure, this, ..)`). Without it, hono's + // `RegExpRouter.match = match` (where `match` is an imported function + // declaration whose body does `this.buildAllMatchers()`) sees + // `this = undefined` and TypeErrors out at the first chained method call. // // We evaluate `object` once into a fresh slot so that // (a) it's only side-effect-evaluated once, and // (b) the lowered `callee` (which re-reads `object` to get the - // property) and the IMPLICIT_THIS save/set both see the - // same receiver value. + // property) and the receiver passed as `this` are the same + // value. // // The `this`-binding / closure-unbox setup below is arity-independent; // only the final dispatch differs. Arities 0..=16 use the per-arity @@ -1578,67 +1573,6 @@ fn lower_closure_call_rooted<'a>( None => None, }; let recv_box = group.reread(ctx, callee_slot)?; - // One-argument receiverless calls have a runtime arrow-aware dispatcher. - // It folds arrow-ness into the existing closure-strategy cache and only - // performs OrdinaryCallBindThis for non-arrows. Other arities retain the - // generated save/restore path below. - let receiverless_one_arg = - method_recv.is_none() && !matches!(callee, Expr::PropertyGet { .. }) && args.len() == 1; - - // #7211: the value `js_implicit_this_set` hands back is the PREVIOUS - // implicit `this`, read straight out of the `IMPLICIT_THIS` cell — which - // `object/this_binding.rs:176` registers as a scanned MUTABLE root the - // collector rewrites in place (`scan_implicit_this_roots_mut`). The swap - // has already overwritten the cell by the time we hold it, so this - // register is now the only copy this frame has, and it stays live across - // the allocating rebind unbox below AND the entire user-code dispatch. - // - // Two ways that hurts, and the second is the one that makes this worse - // than an ordinary stale read: - // - // * the enclosing frame still roots the same object (its own operand - // group, one temp-root frame down), so an evacuating minor inside the - // callee MOVES it and rewrites that root — leaving this register - // naming from-space. The restore then publishes a pre-move address - // back INTO a root the collector scans, so the corruption outlives the - // call that caused it and surfaces in whatever reads `this` next. - // * where no other root holds it, the object is simply collected. - // - // It WAS invisible to `scripts/gc_root_dominance_check.py` at both ends, - // which is how it survived #7206 and #7214: `js_implicit_this_set` was - // NONCOLLECTING but not in `ROOT_READ_CALLS`, so the register had no - // recognised heap-value source, and the restore is not a `RECEIVER_SINKS` - // fatal sink, so it would not have ranked even if it had. Being - // non-collecting is precisely what makes a call a root READ — the same - // rule `js_closure_get_capture_bits` is listed under — and it is now - // classified that way, so the checker reports any lowering that - // reintroduces this. - // - // Unconditional, unlike the operand groups above: the window is the user - // call itself, so `operand_protection`'s "can this window collect?" test - // has exactly one answer here and there is nothing to gate on. - // - // Six sibling lowerings emit the same pair; `implicit_this_save` / - // `implicit_this_restore` is the shared form, so a seventh cannot - // reintroduce this by copy-paste. - let prev_this_root = if let Some(ref this_val) = method_recv { - Some(implicit_this_save(ctx, this_val)) - } else if !matches!(callee, Expr::PropertyGet { .. }) && !receiverless_one_arg { - // Receiverless closure-value call (`fn()`, IIFE, `curry(1)(2)`): - // OrdinaryCallBindThis binds `this` to undefined — without the - // reset the enclosing method dispatch's IMPLICIT_THIS leaks into - // the callee (#3576). Member-shaped callees keep their existing - // receiver/skip behavior above. - // - // The value pushed here is the ENCLOSING method's receiver, not - // `undefined`: this arm is the one that runs for `helper()` called - // from inside `o.m()`, and dropping that object is #3576's leak with - // the sign flipped. - let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - Some(implicit_this_save(ctx, &undef)) - } else { - None - }; // #5247: record the source location right before the throw-capable // dispatch — after the receiver/args are lowered, so a nested-call @@ -1654,7 +1588,7 @@ fn lower_closure_call_rooted<'a>( // a function` for any non-`POINTER_TAG` value. // #6475: a member-shaped call (`o.m(args)`) must rebind an // object-literal method's baked `this` capture slot to the receiver — - // the slot wins over the IMPLICIT_THIS cell set above, so a method + // the slot wins over the `this` parameter passed below, so a method // inherited via `Object.setPrototypeOf(obj, proto)` otherwise runs // with `this` bound to the proto literal (effect's Pipeable // `TagClass.pipe(...)` composed against the wrong `this` and @@ -1689,13 +1623,16 @@ fn lower_closure_call_rooted<'a>( lowered_args.push(group.reread(ctx, arg_base + i)?); } - let result = if receiverless_one_arg { - ctx.block().call( - DOUBLE, - "js_closure_call1_receiverless", - &[(I64, &closure_handle), (DOUBLE, &lowered_args[0])], - ) + let result = if recv_slot.is_some() { + // A member-shaped call binds its receiver: re-read it from its root + // below the (allocating) rebind unbox and pass it as the body's + // `this`. + let this_val = group.reread(ctx, 0)?; + let this_bits = ctx.block().bitcast_double_to_i64(&this_val); + super::emit_closure_handle_call_this(ctx, &closure_handle, &this_bits, &lowered_args) } else { + // A plain call: `js_closure_call{N}` hands the body `undefined` + // (OrdinaryCallBindThis; a sloppy body coerces it itself). // #3527: > 16 args marshal into an entry-block `[N x double]` buffer and // dispatch through the variadic `js_closure_call_array`. // @@ -1707,24 +1644,6 @@ fn lower_closure_call_rooted<'a>( super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args) }; - // #7211: re-read the saved implicit `this` from its slot. Mandatory, not - // defensive, and for the same reason the operand re-reads above are: the - // temp-root slot is a MUTABLE root, so an evacuating cycle anywhere inside - // the dispatch rewrote the slot and left the register that was pushed - // naming from-space. Restoring the register instead of the slot is the - // whole bug. - // - // Ordered inner-to-outer: this slot was pushed above `roots`' first slot, - // so it is dropped first and `roots.release` then drops the group below - // it. `js_gc_temp_root_truncate` drops everything at or above its - // argument, so `roots.release` alone would in fact take this slot with it - // — but only when `roots` actually pushed one, and a receiverless call on - // inert arguments pushes nothing at all. Releasing this one explicitly is - // what makes the order correct in both shapes rather than in the common - // one. - if let Some(prev) = prev_this_root { - implicit_this_restore(ctx, prev); - } // The group is released by `with_rooted_group` AFTER this returns, which is // after the dispatch — the dispatcher allocates while it reads these values. Ok(Some(result)) diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index 51c85cc201..7e4c72875d 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -389,7 +389,7 @@ pub fn try_lower_closure_typed_local_call( // enters it. if matches!(ctx.local_type_hint(id), Some(HirType::Function(_))) || ctx - .resolved_arrow_callback_targets + .resolved_plain_callback_targets .contains_key(&(*id, args.len())) { // #7803: the callee outlives the arguments here too, and this is @@ -448,13 +448,10 @@ pub fn try_lower_closure_typed_local_call( let blk = ctx.block(); unbox_to_i64(blk, &recv_box) }; - let undef_this = - crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); // A method callback parameter can be resolved once at entry when - // its actual value is a directly callable arrow. Keep the full - // dispatcher as a nullable-target fallback: TypeScript's function - // annotation is not a runtime proof, and ordinary functions must - // still receive receiverless `this === undefined` semantics. + // its actual value is directly callable. Keep the full dispatcher + // as a nullable-target fallback: TypeScript's function annotation + // is not a runtime proof. // // Exact immutable aliases (`const cb = callback`) have the same // identity whenever their read succeeds. A TDZ read throws while @@ -473,57 +470,60 @@ pub fn try_lower_closure_typed_local_call( if callback_local_id == *id && callback_arity == lowered_args.len() { let context_bits = ctx.block().ptrtoint(&context, I64); let context_box = ctx.block().bitcast_i64_to_double(&context_bits); - let mut direct_args: Vec<(crate::types::LlvmType, &str)> = - Vec::with_capacity(lowered_args.len() + 1); - direct_args.push((I64, &closure_handle)); - direct_args.extend(lowered_args.iter().enumerate().map(|(index, value)| { - if index == 0 { - (DOUBLE, context_box.as_str()) - } else { - (DOUBLE, value.as_str()) - } - })); + // The versioned-loop clone repurposes JS argument 0 for + // the caller's deopt context (see `codegen/closure.rs`). + let mut direct_args: Vec = lowered_args.to_vec(); + direct_args[0] = context_box; // The exact clone marks the caller's counter for an // immediate side exit before any cold arm that can run // user code or collect. Hot returns cannot collect; cold // returns never observe caller-side cached heap handles. - let value = ctx - .block() - .call_indirect_gc_leaf(DOUBLE, &target, &direct_args); + // An arrow (the versioned-loop clone admits only arrows): + // lexical `this`, so the receiver parameter is unread. + let value = crate::expr::body_call::emit_js_body_call_gc_leaf( + ctx.block(), + &target, + &closure_handle, + crate::expr::body_call::JS_THIS_UNDEFINED, + &direct_args, + ); callee_group.release(ctx); return Ok(Some(value)); } } if let Some(target) = ctx - .resolved_arrow_callback_targets + .resolved_plain_callback_targets .get(&(*id, lowered_args.len())) .cloned() { let fast_ok = ctx.block().icmp_ne(PTR, &target, "null"); - let fast_idx = ctx.new_block("callback_arrow_direct.fast"); - let fallback_idx = ctx.new_block("callback_arrow_direct.fallback"); - let merge_idx = ctx.new_block("callback_arrow_direct.merge"); + let fast_idx = ctx.new_block("callback_plain_direct.fast"); + let fallback_idx = ctx.new_block("callback_plain_direct.fallback"); + let merge_idx = ctx.new_block("callback_plain_direct.merge"); let fast_label = ctx.block_label(fast_idx); let fallback_label = ctx.block_label(fallback_idx); let merge_label = ctx.block_label(merge_idx); ctx.block().cond_br(&fast_ok, &fast_label, &fallback_label); ctx.current_block = fast_idx; - let mut direct_args: Vec<(crate::types::LlvmType, &str)> = - Vec::with_capacity(lowered_args.len() + 1); - direct_args.push((I64, &closure_handle)); - direct_args.extend(lowered_args.iter().map(|value| (DOUBLE, value.as_str()))); - let fast_value = ctx.block().call_indirect(DOUBLE, &target, &direct_args); + // A plain call: the receiver is `undefined`, as + // `js_closure_callN` would pass it. + let fast_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Pointer(&target), + &closure_handle, + crate::expr::body_call::JS_THIS_UNDEFINED, + &lowered_args, + ); let after_fast = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); } ctx.current_block = fallback_idx; - let prev_this = crate::rooting::implicit_this_save(ctx, &undef_this); + // A plain call: the body receives `undefined`. let fallback_value = super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args); - crate::rooting::implicit_this_restore(ctx, prev_this); let after_fallback = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); @@ -540,19 +540,10 @@ pub fn try_lower_closure_typed_local_call( callee_group.release(ctx); return Ok(Some(merged)); } - // Receiverless call of a closure-typed local: bind `this` to - // undefined for the duration of the call (OrdinaryCallBindThis, - // #3576) so an enclosing method dispatch's IMPLICIT_THIS does - // not leak into the callee body. Like the FuncRef path, the - // reset is gated on the statically-known callee actually reading - // dynamic `this`, so a hot-loop call of a plain helper closure - // pays nothing (#5030). When the typed-feedback guard falls back - // (the receiver is NOT the statically-mapped closure), the - // fallback block does its own reset — that callee is unknown. + // Receiverless call of a closure-typed local: a plain call, the + // body receives `undefined` as `this` (OrdinaryCallBindThis, + // #3576) — no enclosing receiver can leak into it. let known_func_id = ctx.local_closure_func_ids.get(id).copied(); - let callee_reads_this = known_func_id - .map(|fid| ctx.funcs_reading_dynamic_this.contains(&fid)) - .unwrap_or(true); if let Some(func_id) = known_func_id { let declared_count = ctx .local_closure_param_counts @@ -569,16 +560,6 @@ pub fn try_lower_closure_typed_local_call( &format!("closure:{}", func_id), TypedFeedbackContract::closure_direct_call(), ); - // #7211: rooted save/restore. The displaced value is the - // enclosing method's receiver and it is live across the - // callee body; the restore below sits in the merge block, - // so the slot index crosses the diamond exactly as the - // bare register used to. - let prev_this = if callee_reads_this { - Some(crate::rooting::implicit_this_save(ctx, &undef_this)) - } else { - None - }; let expected_arity = declared_count.to_string(); let call_arity = lowered_args.len().to_string(); let fast_idx = ctx.new_block("closure_direct.fast"); @@ -690,6 +671,8 @@ pub fn try_lower_closure_typed_local_call( } ctx.current_block = fast_idx; + // A plain call: the body receives `undefined`. + let direct_this_bits = crate::expr::body_call::JS_THIS_UNDEFINED; let typed_f64_param_reps = if ctx.typed_f64_closures.contains(&func_id) { ctx.typed_i1_closure_param_reps .get(&func_id) @@ -800,13 +783,13 @@ pub fn try_lower_closure_typed_local_call( } ctx.current_block = generic_idx; - let mut generic_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in &lowered_args { - generic_args.push((DOUBLE, v.as_str())); - } - let generic_value = - ctx.block().call(DOUBLE, &generic_closure_fn, &generic_args); + let generic_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&generic_closure_fn), + &closure_handle, + direct_this_bits, + &lowered_args, + ); let after_generic = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&typed_merge_label); @@ -901,13 +884,13 @@ pub fn try_lower_closure_typed_local_call( } ctx.current_block = generic_idx; - let mut generic_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in &lowered_args { - generic_args.push((DOUBLE, v.as_str())); - } - let generic_value = - ctx.block().call(DOUBLE, &generic_closure_fn, &generic_args); + let generic_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&generic_closure_fn), + &closure_handle, + direct_this_bits, + &lowered_args, + ); let after_generic = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&typed_merge_label); @@ -1005,13 +988,13 @@ pub fn try_lower_closure_typed_local_call( } ctx.current_block = generic_idx; - let mut generic_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in &lowered_args { - generic_args.push((DOUBLE, v.as_str())); - } - let generic_value = - ctx.block().call(DOUBLE, &generic_closure_fn, &generic_args); + let generic_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&generic_closure_fn), + &closure_handle, + direct_this_bits, + &lowered_args, + ); let after_generic = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&typed_merge_label); @@ -1112,13 +1095,13 @@ pub fn try_lower_closure_typed_local_call( } ctx.current_block = generic_idx; - let mut generic_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in &lowered_args { - generic_args.push((DOUBLE, v.as_str())); - } - let generic_value = - ctx.block().call(DOUBLE, &generic_closure_fn, &generic_args); + let generic_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&generic_closure_fn), + &closure_handle, + direct_this_bits, + &lowered_args, + ); let after_generic = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&typed_merge_label); @@ -1152,12 +1135,13 @@ pub fn try_lower_closure_typed_local_call( ); result } else { - let mut direct_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in &lowered_args { - direct_args.push((DOUBLE, v.as_str())); - } - ctx.block().call(DOUBLE, &closure_fn, &direct_args) + crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&closure_fn), + &closure_handle, + direct_this_bits, + &lowered_args, + ) }; let after_fast = ctx.block().label.clone(); if !ctx.block().is_terminated() { @@ -1170,22 +1154,9 @@ pub fn try_lower_closure_typed_local_call( "js_typed_feedback_record_fallback_call", &[(I64, &site_id)], ); - // Guard failed: the receiver is some OTHER closure whose - // body codegen never saw — reset `this` here (and only - // here) when the static gating skipped the outer reset. - let fallback_prev_this = if prev_this.is_none() { - Some(crate::rooting::implicit_this_save(ctx, &undef_this)) - } else { - None - }; + // Guard failed: some OTHER closure, a plain call. let fallback_value = super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args); - // Inner save, released inside its own arm — so the outer - // slot (restored in the merge block) is still live and the - // temp-root depth matches on both paths into the merge. - if let Some(prev) = fallback_prev_this { - crate::rooting::implicit_this_restore(ctx, prev); - } let after_fallback = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); @@ -1199,9 +1170,6 @@ pub fn try_lower_closure_typed_local_call( (fallback_value.as_str(), after_fallback.as_str()), ], ); - if let Some(prev) = prev_this { - crate::rooting::implicit_this_restore(ctx, prev); - } // Below both arms' calls, in the merge that post-dominates // them — which is why this group is `open_rooted_group`. callee_group.release(ctx); @@ -1209,12 +1177,9 @@ pub fn try_lower_closure_typed_local_call( } } // Generic js_closure_callN dispatch (unknown func id, rest - // params, or arity mismatch): the runtime-resolved callee may - // read `this`, so the reset is unconditional here. - // #7211: rooted save/restore across the runtime-resolved callee. - let prev_this = crate::rooting::implicit_this_save(ctx, &undef_this); + // params, or arity mismatch): a plain call, the body receives + // `undefined`. let result = super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args); - crate::rooting::implicit_this_restore(ctx, prev_this); callee_group.release(ctx); return Ok(Some(result)); } diff --git a/crates/perry-codegen/src/lower_call/func_ref.rs b/crates/perry-codegen/src/lower_call/func_ref.rs index f111b5157d..d896321a32 100644 --- a/crates/perry-codegen/src/lower_call/func_ref.rs +++ b/crates/perry-codegen/src/lower_call/func_ref.rs @@ -1015,30 +1015,20 @@ pub fn try_lower_func_ref_call( lowered.iter().map(|s| (DOUBLE, s.as_str())).collect(); // OrdinaryCallBindThis for a receiverless call: `f()` binds `this` to - // undefined (sloppy bodies then substitute globalThis at the read). - // Without the reset, a bare call inside a method body leaks the - // enclosing dispatch's IMPLICIT_THIS into the callee — a nested - // `function inner(){ return this; }` called as `inner()` inside - // `o.m()` must NOT see `o` (#3576). Gated on the callee actually - // reading dynamic `this` so ordinary helper calls pay nothing. Args - // are lowered BEFORE the reset: `this` inside an argument expression - // still sees the enclosing binding. - let resets_this = ctx.funcs_reading_dynamic_this.contains(fid); - // #7211: rooted save/restore. The value displaced here is the ENCLOSING - // method's receiver, held across the callee body — arbitrary user code. - let prev_this = if resets_this { - let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - Some(crate::rooting::implicit_this_save(ctx, &undef)) - } else { - None - }; + // undefined — a this-reading function's public symbol is a forwarder that + // passes `undefined` to its receiver-taking body (`codegen/function.rs`), + // so a nested `function inner(){ return this; }` called as `inner()` + // inside `o.m()` does not see `o` (#3576). Such a callee keeps to that + // public entry: the specialized and typed-clone arms below are not taken + // for it (unchanged from when this site reset a receiver cell). + let callee_reads_this = ctx.funcs_reading_dynamic_this.contains(fid); // Representation-selection Phase 2: specialized-ABI dispatch. Tier A // (static, guard-free) fires only when every slot's proof holds AT THIS // SITE; Tier B keeps the guarded-diamond shape. Any mismatch falls // through to the existing typed/generic chain — the public boxed entry is // the permanent fallback ABI. let spec_result: Option = if crate::codegen::spec_abi_enabled() - && !resets_this + && !callee_reads_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && declared_count == args.len() @@ -1068,7 +1058,7 @@ pub fn try_lower_func_ref_call( // No `spec_result.is_none()` gate on the typed-clone candidate arms: plan // selection makes the spec and typed-clone sets mutually exclusive, and // the `if let` chain below consumes `spec_result` first anyway. - let typed_f64_call_param_reps = if !resets_this + let typed_f64_call_param_reps = if !callee_reads_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_f64_functions.contains(fid) @@ -1081,7 +1071,7 @@ pub fn try_lower_func_ref_call( } else { None }; - let typed_i32_call_param_reps = if !resets_this + let typed_i32_call_param_reps = if !callee_reads_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_i32_functions.contains(fid) @@ -1094,7 +1084,7 @@ pub fn try_lower_func_ref_call( } else { None }; - let typed_string_call_param_reps = if !resets_this + let typed_string_call_param_reps = if !callee_reads_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_string_functions.contains(fid) @@ -1107,7 +1097,7 @@ pub fn try_lower_func_ref_call( } else { None }; - let typed_i1_call_param_reps = if !resets_this + let typed_i1_call_param_reps = if !callee_reads_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_i1_functions.contains(fid) @@ -1465,18 +1455,6 @@ pub fn try_lower_func_ref_call( // because releasing on one side of it would leave the other side's call // reading dropped slots. // - // AFTER `implicit_this_restore`, and that order is load-bearing rather than - // stylistic. `implicit_this_save` runs BELOW the argument lowering, so its - // slot sits ABOVE this group, and `js_gc_temp_root_truncate` drops `base` - // and everything above it. Releasing first therefore drops the saved - // receiver, and `js_gc_temp_root_get` answers an out-of-range read with - // `0` — so the restore would rebind the enclosing method's `this` to the - // NUMBER 0. `implicit_this_restore` truncates at its own (higher) slot, and - // its doc calls out that a caller holding a lower group may release - // afterwards and drop the slot a second time harmlessly. - if let Some(prev) = prev_this { - crate::rooting::implicit_this_restore(ctx, prev); - } arg_group.release(ctx); if ctx.local_generator_funcs.contains(fid) { let wrap_ptr = format!("@__perry_wrap_{}", fname); diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index da5c7fb3f7..8699fb7f8e 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -782,9 +782,9 @@ pub(super) fn emit_own_method_override_check( // assigned via `this.method = fn` or `class X { method = fn; }` // (hono's RegExpRouter uses this exact shape — `match = match;` // assigns the imported standalone `match` function as an instance - // own-property; its body reads `this.buildAllMatchers()`). Bind - // `IMPLICIT_THIS` to the receiver around the call so non-arrow - // function bodies see the right `this` (issue #632 / #519 pattern). + // own-property; its body reads `this.buildAllMatchers()`). Pass the + // receiver as `this` so non-arrow function bodies see the right `this` + // (issue #632 / #519 pattern). ctx.current_block = override_idx; let user_arg_count = override_user_args.len(); let (args_ptr, args_len) = if user_arg_count == 0 { @@ -809,19 +809,17 @@ pub(super) fn emit_own_method_override_check( } else { this_box.to_string() }; - // #7211: rooted save/restore — the displaced implicit `this` is live - // across `js_native_call_value`, which runs arbitrary user code. - let prev_this = crate::rooting::implicit_this_save(ctx, &recv_for_this); + let this_bits = ctx.block().bitcast_double_to_i64(&recv_for_this); let v_override = ctx.block().call( DOUBLE, "js_native_call_value", &[ (DOUBLE, &own_method), + (I64, &this_bits), (crate::types::PTR, &args_ptr), (I64, &args_len), ], ); - crate::rooting::implicit_this_restore(ctx, prev_this); let after_override = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index d3e3513408..6a5b660a57 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -276,31 +276,49 @@ pub(crate) fn emit_rooted_call( pub(crate) const MAX_FIXED_CLOSURE_CALL_ARGS: usize = 16; /// Dispatch an unboxed closure handle over already-lowered arguments through -/// the closure-call ABI. +/// the closure-call ABI as a PLAIN call (receiver `undefined`). +pub(crate) fn emit_closure_handle_call( + ctx: &mut FnCtx<'_>, + closure_handle: &str, + args: &[String], +) -> String { + emit_closure_handle_call_this( + ctx, + closure_handle, + crate::expr::body_call::JS_THIS_UNDEFINED, + args, + ) +} + +/// Dispatch an unboxed closure handle with receiver `this_bits` (the +/// receiver's NaN-boxed bits, `i64`, already re-read from its root by the +/// caller — nothing between here and the dispatch collects). /// /// Up to [`MAX_FIXED_CLOSURE_CALL_ARGS`] arguments use the per-arity -/// `js_closure_call{N}` register entry points — the fast path, unchanged. -/// Wider calls marshal the arguments into an entry-block `[N x double]` buffer -/// and dispatch through the variadic `js_closure_call_array(closure, args_ptr, -/// argc)`, which owns arbitrary-arity dispatch including rest bundling (#3527). -/// #10420: every closure-value call site used to either reject a 17th argument -/// at compile time or truncate the list to 16; they all route here now. +/// `js_closure_call{N}(closure, this, ...)` register entry points. Wider +/// calls marshal the arguments into an entry-block `[N x double]` buffer and +/// dispatch through `js_closure_call_array(closure, this, args_ptr, argc)`, +/// which owns arbitrary-arity dispatch including rest bundling (#3527). +/// #10420: every closure-value call site used to either reject a 17th +/// argument at compile time or truncate the list to 16; they all route here. /// /// The buffer is NOT a GC root, so callers pass values that are already valid /// below their last collection point; nothing emitted between the stores and /// the call can collect. -pub(crate) fn emit_closure_handle_call( +pub(crate) fn emit_closure_handle_call_this( ctx: &mut FnCtx<'_>, closure_handle: &str, + this_bits: &str, args: &[String], ) -> String { use crate::types::{DOUBLE, I64, PTR}; if args.len() <= MAX_FIXED_CLOSURE_CALL_ARGS { - let runtime_fn = format!("js_closure_call{}", args.len()); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = Vec::with_capacity(args.len() + 1); + let runtime_fn = crate::runtime_abi::JS_CLOSURE_CALL_ENTRIES[args.len()]; + let mut call_args: Vec<(crate::types::LlvmType, &str)> = Vec::with_capacity(args.len() + 2); call_args.push((I64, closure_handle)); + call_args.push((I64, this_bits)); call_args.extend(args.iter().map(|value| (DOUBLE, value.as_str()))); - return ctx.block().call(DOUBLE, &runtime_fn, &call_args); + return ctx.block().call(DOUBLE, runtime_fn, &call_args); } let n = args.len(); let buf = ctx.func.alloca_entry_array(DOUBLE, n); @@ -313,7 +331,12 @@ pub(crate) fn emit_closure_handle_call( blk.call( DOUBLE, "js_closure_call_array", - &[(I64, closure_handle), (PTR, &buf), (I64, &argc)], + &[ + (I64, closure_handle), + (I64, this_bits), + (PTR, &buf), + (I64, &argc), + ], ) } diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index d13eb48cc5..25a6273a4e 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -692,9 +692,10 @@ fn lower_new_impl_inner<'a>( // observes the same cell — `new Child()` where only `Base` reads // `new.target` would otherwise see `undefined` instead of `Child`. // A runtime parent is an unknown reader, so it also needs the cell. - // ponytail: a throw inside the ctor skips the restore, leaving the cell - // set — same edge case the runtime construct paths already have; fix - // holistically if it bites. + // A throw inside the ctor skips this restore; the `new.target` catch + // savepoint (`exception/savepoints.rs`) replays the cell at the + // nearest enclosing JS `try`, so only a throw that no `try` catches + // leaves it set. // #7664: `prev` is saved across the WHOLE constructor body, and the // cell it comes out of is a registered mutable root that evacuation // rewrites — so it goes in a temp root, not a bare register. diff --git a/crates/perry-codegen/src/lower_call/pipeline_call_tests.rs b/crates/perry-codegen/src/lower_call/pipeline_call_tests.rs index c93237181a..4a66e22e33 100644 --- a/crates/perry-codegen/src/lower_call/pipeline_call_tests.rs +++ b/crates/perry-codegen/src/lower_call/pipeline_call_tests.rs @@ -80,20 +80,16 @@ fn immutable_function_alias_calls_the_known_symbol_directly() { "an immutable FuncRef alias must use direct function lowering:\n{ir}" ); assert!( - !ir.contains("call double @js_closure_call1_receiverless"), + !ir.contains("call double @js_closure_call1("), "the immutable alias must not retain dynamic closure dispatch:\n{ir}" ); } #[test] -fn mutable_function_alias_keeps_guarded_receiverless_dispatch() { +fn mutable_function_alias_keeps_guarded_plain_dispatch() { let ir = alias_call_ir(true); assert!( - ir.contains("call double @js_closure_call1_receiverless"), + ir.contains("call double @js_closure_call1("), "a mutable alias has no stable target and must retain dynamic dispatch:\n{ir}" ); - assert!( - !ir.contains("call double @js_implicit_this_set"), - "the arrow-aware runtime dispatcher owns receiverless this binding:\n{ir}" - ); } diff --git a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs index 12828d9c4d..e90278182a 100644 --- a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs +++ b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs @@ -508,27 +508,23 @@ pub(crate) fn try_lower_instance_method_call( )); (ptr_reg, user_arg_count_probe.to_string()) }; - // Issue #632: bind IMPLICIT_THIS to the receiver around - // the override call. The stored function may be a class - // field assigning a non-arrow function (`class X { match - // = match; }` — hono RegExpRouter — where the imported - // `match` body reads `this.buildAllMatchers()`). Without - // the bind, the body sees stale IMPLICIT_THIS and reads - // garbage. Mirrors `lower_call.rs:2607` for the closure- - // call fallthrough pattern (#519). - let recv_for_this_probe = recv_box.clone(); - // #7211: rooted save/restore across the user-code dispatch. - let prev_this_probe = crate::rooting::implicit_this_save(ctx, &recv_for_this_probe); + // Issue #632: pass the receiver as `this` to the override + // call. The stored function may be a class field assigning a + // non-arrow function (`class X { match = match; }` — hono + // RegExpRouter — where the imported `match` body reads + // `this.buildAllMatchers()`). Mirrors the closure-call + // fallthrough pattern (#519). + let this_bits_probe = ctx.block().bitcast_double_to_i64(&recv_box); let v_override_probe = ctx.block().call( DOUBLE, "js_native_call_value", &[ (DOUBLE, &own_method_probe), + (I64, &this_bits_probe), (crate::types::PTR, &probe_args_ptr), (I64, &probe_args_len_str), ], ); - crate::rooting::implicit_this_restore(ctx, prev_this_probe); let after_override_probe = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&probe_outer_merge_label); @@ -1926,21 +1922,20 @@ fn emit_collapsed_instance_dispatch( ctx.block() .cond_br(&is_undef, &dispatch_label, &override_label); - // Override arm: bind IMPLICIT_THIS to the receiver and call the stored - // function value (#632 — a class-field non-arrow function reads `this`). + // Override arm: call the stored function value with the receiver as + // `this` (#632 — a class-field non-arrow function reads `this`). ctx.current_block = override_idx; - // #7211: rooted save/restore across the user-code dispatch. - let prev_this = crate::rooting::implicit_this_save(ctx, recv_box); + let this_bits = ctx.block().bitcast_double_to_i64(recv_box); let v_override = ctx.block().call( DOUBLE, "js_native_call_value", &[ (DOUBLE, &own_method), + (I64, &this_bits), (crate::types::PTR, &args_ptr), (I64, &args_len), ], ); - crate::rooting::implicit_this_restore(ctx, prev_this); let after_override = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); diff --git a/crates/perry-codegen/src/lower_call/property_get/imported_object.rs b/crates/perry-codegen/src/lower_call/property_get/imported_object.rs index 925e5e5076..dc10cb8b13 100644 --- a/crates/perry-codegen/src/lower_call/property_get/imported_object.rs +++ b/crates/perry-codegen/src/lower_call/property_get/imported_object.rs @@ -207,9 +207,7 @@ pub(super) fn try_lower_imported_object_method_call( let key_index = ctx.strings.intern(property); let dispatch_global = ctx.strings.static_dispatch_global(key_index); let closure_symbol = method.target.clone(); - let mut closure_params = Vec::with_capacity(method.param_count + 1); - closure_params.push(I64); - closure_params.extend(std::iter::repeat_n(DOUBLE, method.param_count)); + let closure_params = crate::expr::body_call::js_body_param_types(method.param_count); ctx.pending_declares .push((closure_symbol.clone(), DOUBLE, closure_params)); @@ -244,11 +242,15 @@ pub(super) fn try_lower_imported_object_method_call( &closure_symbol, &fallback_label, ); - let mut direct_args: Vec<(crate::types::LlvmType, &str)> = - Vec::with_capacity(lowered_args.len() + 1); - direct_args.push((I64, &closure_handle)); - direct_args.extend(lowered_args.iter().map(|arg| (DOUBLE, arg.as_str()))); - let direct_value = ctx.block().call(DOUBLE, &closure_symbol, &direct_args); + // `recv.method(args)`: the method's receiver is `recv`. + let this_bits = recv_bits.clone(); + let direct_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&closure_symbol), + &closure_handle, + &this_bits, + &lowered_args, + ); let direct_end = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); @@ -380,9 +382,8 @@ pub(super) fn try_lower_dynamic_object_method_call( .cond_br(&receiver_matches, &cache_label, &miss_label); let closure_symbol = candidate.method.target.clone(); - let mut closure_params = Vec::with_capacity(candidate.method.param_count + 1); - closure_params.push(I64); - closure_params.extend(std::iter::repeat_n(DOUBLE, candidate.method.param_count)); + let closure_params = + crate::expr::body_call::js_body_param_types(candidate.method.param_count); ctx.pending_declares .push((closure_symbol.clone(), DOUBLE, closure_params)); let closure_handle = emit_cached_own_method_guard( @@ -396,11 +397,15 @@ pub(super) fn try_lower_dynamic_object_method_call( &closure_symbol, &miss_label, ); - let mut direct_args: Vec<(crate::types::LlvmType, &str)> = - Vec::with_capacity(lowered_args.len() + 1); - direct_args.push((I64, &closure_handle)); - direct_args.extend(lowered_args.iter().map(|arg| (DOUBLE, arg.as_str()))); - let direct_value = ctx.block().call(DOUBLE, &closure_symbol, &direct_args); + // As above: the method's receiver is `recv`. + let this_bits = recv_bits.clone(); + let direct_value = crate::expr::body_call::emit_js_body_call( + ctx.block(), + crate::expr::body_call::JsBody::Symbol(&closure_symbol), + &closure_handle, + &this_bits, + &lowered_args, + ); let direct_end = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); diff --git a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs index 0adb171ff5..6ec927deec 100644 --- a/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs +++ b/crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs @@ -252,18 +252,15 @@ pub(crate) fn try_lower_static_dispatch( // register captured before it — see the rationale on this // closure's opening comment. let recv_box = group.reread(ctx, recv_idx)?; - // #7211: rooted save/restore — the displaced implicit `this` is - // live across the static method body below, which is user code. - let prev_this = crate::rooting::implicit_this_save(ctx, &recv_box); // Receiver-sensitive static `this`: arm the one-shot override with // the ACTUAL receiver box so the callee prologue's // `js_static_this_resolve` binds `this` to it (spec // OrdinaryCallBindThis). This must cover the dynamic-value receiver // shapes too (ClassExprFresh / factory `Call` / `LocalGet`), not // just plain class-refs: the prologue consumes the armed override - // or falls back to the LEXICAL class-ref — it never reads implicit - // `this` — so the previous implicit-this-only treatment of these - // shapes silently bound `this` to the shared template. A class + // or falls back to the LEXICAL class-ref, so binding the + // receiver any other way silently bound `this` to the shared + // template. A class // EXPRESSION's per-evaluation statics are OWN properties of the // fresh heap class object (never written to the template's // static-field globals), so `this.` inside the static body @@ -279,7 +276,6 @@ pub(crate) fn try_lower_static_dispatch( let arg_slices: Vec<(crate::types::LlvmType, &str)> = lowered.iter().map(|s| (DOUBLE, s.as_str())).collect(); let result = ctx.block().call(DOUBLE, &fn_name, &arg_slices); - crate::rooting::implicit_this_restore(ctx, prev_this); Ok(Some(result)) }); } @@ -346,6 +342,8 @@ pub(crate) fn try_lower_static_dispatch( "js_native_call_value", &[ (DOUBLE, &callee_val), + // A plain call, as it has always been lowered here. + (I64, crate::expr::body_call::JS_THIS_UNDEFINED), (I64, &args_ptr_i64), (I64, &args_len), ], diff --git a/crates/perry-codegen/src/rooting/group.rs b/crates/perry-codegen/src/rooting/group.rs index 9a656e0c71..8416f55a7c 100644 --- a/crates/perry-codegen/src/rooting/group.rs +++ b/crates/perry-codegen/src/rooting/group.rs @@ -9,7 +9,7 @@ use anyhow::Result; use perry_hir::Expr; use crate::expr::FnCtx; -use crate::types::{LlvmType, DOUBLE, I64}; +use crate::types::{LlvmType, DOUBLE}; use super::temp_root; use super::{call_void_with_roots, call_with_roots, read_slot, Arg, Repr, RootedSlot}; @@ -401,183 +401,8 @@ pub(crate) fn open_rooted_group<'a>(capacity: usize) -> RootedGroup<'a> { RootedGroup::new(capacity) } -/// A saved implicit `this`, held in a rooted slot for the duration of a -/// dispatch (#7211). -/// -/// **Moved here in slice 6 rather than re-exported.** It was already a paired -/// combinator with this file's contract — root before the window, re-read -/// after it, never hand out the register — but it lived in the raw API, so six -/// `lower_call/` modules had to name `expr::temp_root` while making no ordering -/// decision at all. Leaving a copy behind would have given the pair two -/// spellings, which is the drift that produced #7114; there is one. -/// -/// `js_implicit_this_set` swaps the `IMPLICIT_THIS` cell and returns what was -/// there, read straight out of a cell `scan_implicit_this_roots_mut` -/// (`object/this_binding.rs:176`) registers as a scanned MUTABLE root. The swap -/// has already overwritten the cell, so the returned value is now held ONLY in -/// an SSA register, across the whole call the bind exists to scope. -/// -/// Two ways that hurts, and the second is what makes it worse than an ordinary -/// stale read: -/// -/// * the enclosing frame still roots the same object, so an evacuating minor -/// inside the callee MOVES it and rewrites that root — leaving this register -/// naming from-space. The restore then publishes a pre-move address back -/// INTO a root the collector scans, so the corruption outlives the call that -/// caused it and surfaces in whatever reads `this` next; -/// * where no other root holds it, the object is simply collected. -/// -/// Seven lowerings emit this pair. They had seven copies of the same three -/// lines and therefore seven copies of the same bug, which is why it is a -/// combinator rather than seven edits. -pub(crate) struct ImplicitThisSave { - slot: RootedSlot, -} - -/// Bind `new_this` as the implicit `this` and root the value it displaced. -/// -/// Unconditional, unlike an operand group: the window is a user or native call, -/// so `operand_protection`'s "can this window collect?" test has exactly one -/// answer here and there is nothing to gate on. -pub(crate) fn implicit_this_save(ctx: &mut FnCtx<'_>, new_this: &str) -> ImplicitThisSave { - let prev = implicit_this_swap(ctx, new_this, "implicit_this.save"); - let idx = temp_root::temp_root_push_double(ctx, &prev); - ImplicitThisSave { - slot: RootedSlot { - idx, - repr: Repr::Boxed, - }, - } -} - -/// The implicit-`this` cell's address when this target reads it without a -/// call (ELF executables, `expr::agent_ptr`), for a save/restore pair that -/// wants to find the cell once: the address is per-thread and stable, so a -/// restore after the call may reuse it. -pub(crate) fn implicit_this_cell_ptr(ctx: &mut FnCtx<'_>) -> Option { - if crate::expr::agent_ptr::agent_ptr_access(ctx) - != crate::expr::agent_ptr::AgentPtrAccess::InitialExec - { - return None; - } - Some(crate::expr::agent_ptr::emit_agent_ptr( - ctx, - crate::runtime_abi::AGENT_PTR_IMPLICIT_THIS, - "perry_implicit_this_cell", - )) -} - -/// [`implicit_this_save`] through a cell address from -/// [`implicit_this_cell_ptr`]. -pub(crate) fn implicit_this_save_at( - ctx: &mut FnCtx<'_>, - cell: &str, - new_this: &str, -) -> ImplicitThisSave { - let prev = { - let blk = ctx.block(); - let prev_bits = blk.load(I64, cell); - let value_bits = blk.bitcast_double_to_i64(new_this); - // GC_STORE_AUDIT(ROOT_CELL): the implicit-`this` cell is a registered root. - blk.store(I64, &value_bits, cell); - blk.bitcast_i64_to_double(&prev_bits) - }; - let idx = temp_root::temp_root_push_double(ctx, &prev); - ImplicitThisSave { - slot: RootedSlot { - idx, - repr: Repr::Boxed, - }, - } -} - -/// [`implicit_this_restore`] through the same cell address. -pub(crate) fn implicit_this_restore_at(ctx: &mut FnCtx<'_>, cell: &str, save: ImplicitThisSave) { - let prev = read_slot(ctx, &save.slot); - save.slot.release(ctx); - let blk = ctx.block(); - let bits = blk.bitcast_double_to_i64(&prev); - // GC_STORE_AUDIT(ROOT_CELL): the implicit-`this` cell is a registered root. - blk.store(I64, &bits, cell); -} - -/// Restore the saved implicit `this`, re-read from its root. -/// -/// Reading the slot rather than the register is the fix, not a precaution: the -/// slot is a mutable root, so an evacuating cycle inside the dispatch rewrote -/// it and the register pushed beforehand names from-space. -/// -/// The release is emitted BEFORE the restore call so that nested saves — an -/// override arm inside an outer bind — release inner to outer. A release is a -/// stack cut, so a caller holding a LOWER group may release it afterwards and -/// drop this slot a second time harmlessly. -pub(crate) fn implicit_this_restore(ctx: &mut FnCtx<'_>, save: ImplicitThisSave) { - let prev = read_slot(ctx, &save.slot); - save.slot.release(ctx); - implicit_this_swap(ctx, &prev, "implicit_this.restore"); -} - -/// `js_implicit_this_set(value)`: bind `value` as the implicit `this` and -/// return the previous binding. On Apple aarch64 the cell is read and -/// written inline through the hot-cache lookup (`expr::hot_tls`), with the -/// runtime call as the fallback for every miss — the pair around a -/// dynamically-dispatched call was two runtime calls whose whole body was -/// that lookup plus a `replace`. -fn implicit_this_swap(ctx: &mut FnCtx<'_>, value: &str, stem: &str) -> String { - // ELF executables: the cell's address is a per-agent pointer read - // thread-pointer-relative (`expr::agent_ptr`), so the swap is a load and a - // store with no call. - if let Some(cell) = implicit_this_cell_ptr(ctx) { - let blk = ctx.block(); - let prev_bits = blk.load(I64, &cell); - let value_bits = blk.bitcast_double_to_i64(value); - // GC_STORE_AUDIT(ROOT_CELL): the implicit-`this` cell is a registered root. - blk.store(I64, &value_bits, &cell); - return blk.bitcast_i64_to_double(&prev_bits); - } - if !crate::expr::hot_tls::inline_hot_tls_enabled(ctx) { - return ctx - .block() - .call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, value)]); - } - let lookup = crate::expr::hot_tls::emit_hot_tls_lookup(ctx, stem); - let merge_idx = ctx.new_block(&format!("{stem}.hot_tls.merge")); - let merge_label = ctx.block_label(merge_idx); - let cell = crate::expr::hot_tls::hot_tls_field( - ctx, - &lookup.hot, - crate::expr::hot_tls::HOT_TLS_IMPLICIT_THIS_OFFSET, - ); - let (fast_prev, fast_pred) = { - let blk = ctx.block(); - let prev_bits = blk.load(I64, &cell); - let value_bits = blk.bitcast_double_to_i64(value); - blk.store(I64, &value_bits, &cell); - let prev = blk.bitcast_i64_to_double(&prev_bits); - let pred = blk.label.clone(); - blk.br(&merge_label); - (prev, pred) - }; - ctx.current_block = lookup.slow_idx; - let (slow_prev, slow_pred) = { - let blk = ctx.block(); - let prev = blk.call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, value)]); - let pred = blk.label.clone(); - blk.br(&merge_label); - (prev, pred) - }; - ctx.current_block = merge_idx; - ctx.block().phi( - DOUBLE, - &[(&fast_prev, &fast_pred), (&slow_prev, &slow_pred)], - ) -} - /// The `new.target` cell's saved previous value (#7664). /// -/// Structurally [`ImplicitThisSave`] for a different cell, and it is a separate -/// type rather than a parameter so the two cannot be crossed at a restore. -/// /// The cell is a registered mutable root — `scan_current_new_target_root_mut`, /// `gc/mod.rs` — so an evacuating cycle inside the constructor rewrites it and /// a register saved beforehand names from-space. The RUNTIME's own construct @@ -620,8 +445,8 @@ pub(crate) fn new_target_save_for_super(ctx: &mut FnCtx<'_>) -> Option, expr: &Expr) -> bool { mod group; mod ledger; -// `ImplicitThisSave` and `NewTargetSave` are deliberately absent: no caller -// names either type — both are only ever held as an inferred local between the -// `*_save` and `*_restore` pair — so re-exporting them would be an unused -// import. They stay `pub(crate)` in `group.rs`, where the functions that -// produce them live. +// `NewTargetSave` is deliberately absent: no caller names the type — it is +// only ever held as an inferred local between the `new_target_save` and +// `new_target_restore` pair — so re-exporting it would be an unused import. It +// stays `pub(crate)` in `group.rs`, where the functions that produce it live. pub(crate) use group::{ - implicit_this_cell_ptr, implicit_this_restore, implicit_this_restore_at, implicit_this_save, - implicit_this_save_at, new_target_restore, new_target_save, new_target_save_for_super, - open_rooted_group, with_rooted_accumulator, with_rooted_group, AccArray, EmittedValue, - RootedAcc, RootedGroup, + new_target_restore, new_target_save, new_target_save_for_super, open_rooted_group, + with_rooted_accumulator, with_rooted_group, AccArray, EmittedValue, RootedAcc, RootedGroup, }; // --------------------------------------------------------------------------- diff --git a/crates/perry-codegen/src/rooting/temp_root.rs b/crates/perry-codegen/src/rooting/temp_root.rs index ab878ba22f..0edebb591b 100644 --- a/crates/perry-codegen/src/rooting/temp_root.rs +++ b/crates/perry-codegen/src/rooting/temp_root.rs @@ -224,10 +224,9 @@ pub(in crate::rooting) fn temp_root_truncate(ctx: &mut FnCtx<'_>, idx: &str) { // above it. Each released slot is zeroed (dropping its retention) and // its frame mirror cleared; the pool entry becomes reusable. // - // A repeated release of an already-released handle (the documented - // `implicit_this_restore` → outer-group interleaving) finds no - // watermark position and is the same harmless no-op the FFI's - // `base < len` guard made it. + // A repeated release of an already-released handle (an inner release + // interleaved with an outer group's) finds no watermark position and + // is the same harmless no-op the FFI's `base < len` guard made it. let Some(pos) = ctx.temp_roots.position(idx) else { return; }; diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 5228f32a4e..0fa7b3c24f 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -629,7 +629,6 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { &format!("[{} x ptr]", crate::expr::agent_ptr::AGENT_PTR_SLOTS), "initialexec", ); - module.declare_function("perry_implicit_this_cell", PTR, &[]); // #10812: the prologue stack check (`expr/stack_guard.rs`). module.declare_function("js_stack_overflow", VOID, &[]); module.declare_function( diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs index 5f6e56a826..5f2d720156 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs @@ -45,34 +45,58 @@ pub(crate) fn declare_core(module: &mut LlModule) { module.declare_function("js_math_tan", DOUBLE, &[DOUBLE]); // ========== Atomics ========== - module.declare_function("js_atomics_load", DOUBLE, &[PTR, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_is_lock_free", DOUBLE, &[PTR, DOUBLE]); - module.declare_function("js_atomics_store", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_add", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_sub", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_and", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_or", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_atomics_xor", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_load", DOUBLE, &[I64, I64, DOUBLE, DOUBLE]); + module.declare_function("js_atomics_is_lock_free", DOUBLE, &[I64, I64, DOUBLE]); + module.declare_function( + "js_atomics_store", + DOUBLE, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_atomics_add", + DOUBLE, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_atomics_sub", + DOUBLE, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_atomics_and", + DOUBLE, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_atomics_or", DOUBLE, &[I64, I64, DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_atomics_xor", + DOUBLE, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], + ); module.declare_function( "js_atomics_exchange", DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE], + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], ); module.declare_function( "js_atomics_compare_exchange", DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + &[I64, I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function( + "js_atomics_notify", + DOUBLE, + &[I64, I64, DOUBLE, DOUBLE, DOUBLE], ); - module.declare_function("js_atomics_notify", DOUBLE, &[PTR, DOUBLE, DOUBLE, DOUBLE]); module.declare_function( "js_atomics_wait", DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + &[I64, I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], ); module.declare_function( "js_atomics_wait_async", DOUBLE, - &[PTR, DOUBLE, DOUBLE, DOUBLE, DOUBLE], + &[I64, I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], ); // ========== Number ========== @@ -109,11 +133,13 @@ pub(crate) fn declare_core(module: &mut LlModule) { // pointer to a `[N x double]` stack buffer; declare it PTR (ABI-identical // to I64 in the integer register class) so call sites can pass an alloca // directly. See `try_lower_closure_call_fallthrough` (#3527). - module.declare_function("js_closure_call_array", DOUBLE, &[I64, PTR, I64]); + module.declare_function("js_closure_call_array", DOUBLE, &[I64, I64, PTR, I64]); + // V8's callback trampoline (`func(env, args, len)`): its address only. + module.declare_function("js_closure_v8_callback", DOUBLE, &[I64, PTR, I64]); module.declare_function( "js_closure_call_apply_with_spread", DOUBLE, - &[DOUBLE, PTR, I64, I64], + &[DOUBLE, I64, PTR, I64, I64], ); module.declare_function("js_create_callback", DOUBLE, &[I64, I64, I64]); @@ -391,9 +417,6 @@ pub(crate) fn declare_core(module: &mut LlModule) { // `js_native_call_method`'s field-scan dispatch when invoking a // closure-typed class field method-style. `Expr::This` codegen reads // this when the lexical this_stack is empty. - module.declare_function("js_implicit_this_get", DOUBLE, &[]); - module.declare_function("js_implicit_this_get_sloppy", DOUBLE, &[]); - module.declare_function("js_implicit_this_set", DOUBLE, &[DOUBLE]); // Static-method prologue `this`: takes the one-shot receiver override // armed by dynamic static dispatch / call/apply, else returns the // lexical class-ref argument. @@ -424,7 +447,7 @@ pub(crate) fn declare_core(module: &mut LlModule) { DOUBLE, &[DOUBLE, I64, I64, I64, I64], ); - module.declare_function("js_native_call_value", DOUBLE, &[DOUBLE, I64, I64]); + module.declare_function("js_native_call_value", DOUBLE, &[DOUBLE, I64, I64, I64]); module.declare_function("js_new_from_handle", DOUBLE, &[DOUBLE, I64, I64]); module.declare_function("js_new_instance", DOUBLE, &[I64, I64, I64, I64, I64]); module.declare_function("js_runtime_init", VOID, &[]); diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index d7e7002d79..3a5a1f70a4 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -283,101 +283,17 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { I64, &[DOUBLE, DOUBLE], ); - module.declare_function("js_closure_call0", DOUBLE, &[I64]); - module.declare_function("js_closure_call1", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_closure_call1_receiverless", DOUBLE, &[I64, DOUBLE]); - module.declare_function("js_closure_call2", DOUBLE, &[I64, DOUBLE, DOUBLE]); - module.declare_function("js_closure_call3", DOUBLE, &[I64, DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_closure_resolve_arrow_direct_call", PTR, &[I64, I32]); - module.declare_function( - "js_closure_call4", - DOUBLE, - &[I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_closure_call5", - DOUBLE, - &[I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_closure_call6", - DOUBLE, - &[I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_closure_call7", - DOUBLE, - &[I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_closure_call8", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call9", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call10", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call11", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - DOUBLE, - ], - ); - module.declare_function( - "js_closure_call12", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call13", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call14", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call15", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); - module.declare_function( - "js_closure_call16", - DOUBLE, - &[ - I64, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, - ], - ); + // `js_closure_call{N}(callee, this, a0..aN-1)`, generated from the ABI + // table so the declarations cannot drift from `perry_abi`. + for (argc, name) in crate::runtime_abi::JS_CLOSURE_CALL_ENTRIES + .iter() + .enumerate() + { + let mut params = vec![I64, I64]; + params.extend(std::iter::repeat_n(DOUBLE, argc)); + module.declare_function(name, DOUBLE, ¶ms); + } + module.declare_function("js_closure_resolve_plain_direct_call", PTR, &[I64, I32]); // Phase B.16 / D follow-ups: more runtime functions discovered // by the test-files sweep histogram. diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index 2e4bd2d86a..04cab5c459 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -11,6 +11,9 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { // by a `PERRY_RECV_ROUTE_COUNT=1` build; declared unconditionally so the // declaration set does not depend on a diagnostic knob. module.declare_function("js_recv_route_note", VOID, &[I32]); + // OrdinaryCallBindThis for a sloppy body's receiver parameter + // (`expr::body_call::emit_receiver_prologue`). Can allocate: a safepoint. + module.declare_function("js_this_coerce_sloppy", DOUBLE, &[DOUBLE]); // RegExp exec module.declare_function("js_regexp_exec", I64, &[I64, I64]); module.declare_function("js_number_to_precision", I64, &[DOUBLE, DOUBLE]); diff --git a/crates/perry-codegen/src/stubs.rs b/crates/perry-codegen/src/stubs.rs index 6ca2c452c0..ceb2e73d84 100644 --- a/crates/perry-codegen/src/stubs.rs +++ b/crates/perry-codegen/src/stubs.rs @@ -27,10 +27,11 @@ use crate::nanbox::TAG_UNDEFINED; /// 3. **Identity functions** — `double(double)` functions that pass their /// argument through unchanged. Used for `js_await_any_promise` and /// similar pass-through points where the V8 runtime is not present. -/// 4. **Closure-wrapper symbols** — `double(i64, double, double, double, +/// 4. **Closure-wrapper symbols** — `double(i64, i64, double, double, /// double, double, double)` functions returning NaN-boxed `TAG_UNDEFINED`. -/// Match the closure-call ABI emitted by `expr.rs:~11783` for cross- -/// module function-VALUE references (`__perry_wrap_perry_fn___`). +/// The JS body ABI (`perry_abi::JS_BODY_*`: callee, receiver, arguments) +/// of the wrappers cross-module function-VALUE references name +/// (`__perry_wrap_perry_fn___`). /// Used when a non-entry module fails to compile under `--allow-unimplemented` /// so the link still succeeds; consumers that read the import as a closure /// handle observe a singleton wrapping the undefined-return stub. @@ -105,13 +106,12 @@ pub fn generate_stub_object_full( )); } - // 4. Closure-wrapper stubs — match the closure-call ABI emitted at - // `crates/perry-codegen/src/expr.rs:~11783` for cross-module - // function-VALUE references: `double name(i64 closure, double a0, - // double a1, double a2, double a3, double a4)`. Return NaN-boxed - // TAG_UNDEFINED. The 5-double arg list covers the max-arity 5 - // that `js_closure_call0..5` dispatches through; signature shape - // matches the no-op variable/class branch at codegen.rs:~2750. + // 4. Closure-wrapper stubs — the JS body ABI of the wrappers + // cross-module function-VALUE references name: `double name(i64 + // callee, i64 this, double a0, double a1, double a2, double a3, + // double a4)`. Return NaN-boxed TAG_UNDEFINED. The stub reads no + // argument, so its declared width is informational; it matches the + // no-op variable/class wrappers in `codegen/export_value_wrappers.rs`. // // Used by the failed-module stub block at // `crates/perry/src/commands/compile.rs:~5518` so consumers that @@ -126,7 +126,7 @@ pub fn generate_stub_object_full( // Refs #903 / uuid `__perry_wrap_perry_fn_...sha1_js__default`. for name in wrapper_func_symbols { ll.push_str(&format!( - "define double @{}(i64 %0, double %1, double %2, double %3, double %4, double %5) {{\n ret double {}\n}}\n\n", + "define double @{}(i64 %0, i64 %1, double %2, double %3, double %4, double %5, double %6) {{\n ret double {}\n}}\n\n", name, undef_hex )); } diff --git a/crates/perry-codegen/src/wasm32/closure_abi.rs b/crates/perry-codegen/src/wasm32/closure_abi.rs index f5276f510d..d9d99a163f 100644 --- a/crates/perry-codegen/src/wasm32/closure_abi.rs +++ b/crates/perry-codegen/src/wasm32/closure_abi.rs @@ -1,10 +1,11 @@ //! Give every closure body the runtime's view of its first parameter. //! -//! Codegen defines a closure body as `(i64 %this_closure, double …)`: the -//! closure pointer travels as an `i64` like every other address it carries. -//! The runtime invokes those bodies through `extern "C" fn(*const -//! ClosureHeader, f64 …)` (and defines its own native-closure bodies that -//! way), so on wasm32 the first slot is an `i32` pointer. A native C ABI +//! Codegen defines a closure body as `(i64 %this_closure, i64 %js_this, +//! double …)` (`perry_abi::JS_BODY_*`): the closure pointer travels as an +//! `i64` like every other address it carries. The runtime invokes those +//! bodies through `extern "C" fn(*const ClosureHeader, JsThis, f64 …)` (and +//! defines its own native-closure bodies that way), so on wasm32 the first +//! slot is an `i32` pointer; the receiver is a `u64` on both sides. A native C ABI //! does not care; a wasm `call_indirect` must match exactly, and traps. //! //! This pass retypes each such definition to take `ptr %this_closure.ptr` diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index 4a061bf583..8e0316c211 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -5,7 +5,7 @@ clearImmediate void i64 clearInterval void i64 clearTimeout void i64 gc_check_trigger_export void -global_this_builtin_noop_thunk f64 ptr,f64 +global_this_builtin_noop_thunk f64 ptr,i64,f64 js_abort_controller_abort void ptr js_abort_controller_abort_reason void ptr,f64 js_abort_controller_new ptr @@ -289,19 +289,19 @@ js_async_resource_trigger_async_id f64 i64 js_async_step_chain ptr f64,ptr js_async_step_done ptr f64,ptr js_atob ptr f64 -js_atomics_add f64 ptr,f64,f64,f64 -js_atomics_and f64 ptr,f64,f64,f64 -js_atomics_compare_exchange f64 ptr,f64,f64,f64,f64 -js_atomics_exchange f64 ptr,f64,f64,f64 -js_atomics_is_lock_free f64 ptr,f64 -js_atomics_load f64 ptr,f64,f64 -js_atomics_notify f64 ptr,f64,f64,f64 -js_atomics_or f64 ptr,f64,f64,f64 -js_atomics_store f64 ptr,f64,f64,f64 -js_atomics_sub f64 ptr,f64,f64,f64 -js_atomics_wait f64 ptr,f64,f64,f64,f64 -js_atomics_wait_async f64 ptr,f64,f64,f64,f64 -js_atomics_xor f64 ptr,f64,f64,f64 +js_atomics_add f64 ptr,i64,f64,f64,f64 +js_atomics_and f64 ptr,i64,f64,f64,f64 +js_atomics_compare_exchange f64 ptr,i64,f64,f64,f64,f64 +js_atomics_exchange f64 ptr,i64,f64,f64,f64 +js_atomics_is_lock_free f64 ptr,i64,f64 +js_atomics_load f64 ptr,i64,f64,f64 +js_atomics_notify f64 ptr,i64,f64,f64,f64 +js_atomics_or f64 ptr,i64,f64,f64,f64 +js_atomics_store f64 ptr,i64,f64,f64,f64 +js_atomics_sub f64 ptr,i64,f64,f64,f64 +js_atomics_wait f64 ptr,i64,f64,f64,f64,f64 +js_atomics_wait_async f64 ptr,i64,f64,f64,f64,f64 +js_atomics_xor f64 ptr,i64,f64,f64,f64 js_aux_has_active i32s js_await_any_promise f64 f64 js_await_js_promise f64 f64 @@ -581,7 +581,7 @@ js_class_capture_value f64 i32u,i32u js_class_capture_value_for_receiver f64 f64,i32u,i32u js_class_capture_value_or f64 i32u,i32u,f64 js_class_computed_field_key f64 f64,i32u,ptr,usize -js_class_constructor_called f64 ptr +js_class_constructor_called f64 ptr,i64 js_class_env_current f64 f64,i32u js_class_env_evaluate void i32u,f64,f64 js_class_env_get f64 f64,i32u,i32u @@ -621,40 +621,39 @@ js_closure_alloc ptr ptr,i32u js_closure_alloc_init ptr ptr,i32u,ptr js_closure_alloc_singleton ptr ptr js_closure_alloc_with_captures_singleton ptr ptr,i32u,ptr -js_closure_call0 f64 ptr -js_closure_call1 f64 ptr,f64 -js_closure_call10 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call11 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call12 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call13 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call14 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call15 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call16 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call1_receiverless f64 ptr,f64 -js_closure_call2 f64 ptr,f64,f64 -js_closure_call3 f64 ptr,f64,f64,f64 -js_closure_call4 f64 ptr,f64,f64,f64,f64 -js_closure_call5 f64 ptr,f64,f64,f64,f64,f64 -js_closure_call6 f64 ptr,f64,f64,f64,f64,f64,f64 -js_closure_call7 f64 ptr,f64,f64,f64,f64,f64,f64,f64 -js_closure_call8 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call9 f64 ptr,f64,f64,f64,f64,f64,f64,f64,f64,f64 -js_closure_call_apply_with_spread f64 f64,ptr,i64,i64 -js_closure_call_array f64 i64,ptr,i64 +js_closure_call0 f64 ptr,i64 +js_closure_call1 f64 ptr,i64,f64 +js_closure_call10 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call11 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call12 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call13 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call14 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call15 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call16 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call2 f64 ptr,i64,f64,f64 +js_closure_call3 f64 ptr,i64,f64,f64,f64 +js_closure_call4 f64 ptr,i64,f64,f64,f64,f64 +js_closure_call5 f64 ptr,i64,f64,f64,f64,f64,f64 +js_closure_call6 f64 ptr,i64,f64,f64,f64,f64,f64,f64 +js_closure_call7 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call8 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call9 f64 ptr,i64,f64,f64,f64,f64,f64,f64,f64,f64,f64 +js_closure_call_apply_with_spread f64 f64,i64,ptr,i64,i64 +js_closure_call_array f64 i64,i64,ptr,i64 js_closure_exact_func_guard i64 f64,ptr js_closure_get_capture_bits i64 ptr,i32u js_closure_get_capture_f64 f64 ptr,i32u js_closure_get_capture_ptr i64 ptr,i32u js_closure_get_func ptr ptr -js_closure_resolve_arrow_direct_call ptr ptr,i32u +js_closure_resolve_plain_direct_call ptr ptr,i32u js_closure_resolve_versioned_loop_direct_call ptr ptr,i32u js_closure_set_box_capture_ptr void ptr,i32u,i64 js_closure_set_capture_bits void ptr,i32u,i64 js_closure_set_capture_f64 void ptr,i32u,f64 js_closure_set_capture_ptr void ptr,i32u,i64 -js_closure_unbind_this f64 f64 js_closure_unbox_callee_checked i64 f64 js_closure_unbox_callee_checked_rebind i64 f64,f64 +js_closure_v8_callback f64 i64,ptr,i64 js_cluster_disconnect f64 f64 js_cluster_emit f64 f64,ptr js_cluster_event_names f64 @@ -1675,9 +1674,6 @@ js_i32_box_scope_release void ptr js_i32_box_set void ptr,i32s js_ic_diag_note_hit void i64,i32s js_immediate_has_pending i32s -js_implicit_this_get f64 -js_implicit_this_get_sloppy f64 -js_implicit_this_set f64 f64 js_import_meta_resolve f64 f64,f64,f64 js_import_meta_resolve_value f64 f64 js_in_operator f64 f64,f64 @@ -1991,7 +1987,7 @@ js_native_call_method_patched_proto_apply f64 f64,ptr,usize,i64 js_native_call_method_str_key f64 f64,i64,ptr,usize js_native_call_method_value f64 f64,f64,ptr,usize js_native_call_method_value_apply f64 f64,f64,i64 -js_native_call_value f64 f64,ptr,usize +js_native_call_value f64 f64,i64,ptr,usize js_native_handle_dispose i32s f64 js_native_handle_mark_main_thread void js_native_handle_new_borrowed f64 i64,i64,i32s,i32s,ptr,i64 @@ -2765,7 +2761,7 @@ js_perf_measure f64 f64,f64,f64 js_perf_monitor_event_loop_delay f64 f64 js_perf_node_timing f64 js_perf_observer_disconnect f64 f64 -js_perf_observer_flush_all f64 ptr +js_perf_observer_flush_all f64 ptr,i64 js_perf_observer_new f64 f64 js_perf_observer_observe f64 f64,f64 js_perf_observer_take_records f64 f64 @@ -3728,6 +3724,7 @@ js_text_encoder_encode_llvm i64 f64 js_text_encoder_new i64 js_text_encoder_stream_new f64 js_text_encoding_stream_new f64 +js_this_coerce_sloppy f64 f64 js_thread_has_pending i32s js_thread_parallel_filter f64 f64,f64 js_thread_parallel_map f64 f64,f64 @@ -3837,20 +3834,20 @@ js_try_end void js_try_push ptr js_tty_isatty f64 f64 js_tty_read_stream_new f64 f64 -js_tty_read_stream_set_raw_mode f64 ptr,f64 +js_tty_read_stream_set_raw_mode f64 ptr,i64,f64 js_tty_resize_drain i32s -js_tty_write_stream_clear_line f64 ptr,f64,f64 -js_tty_write_stream_clear_screen_down f64 ptr,f64 -js_tty_write_stream_cursor_to f64 ptr,f64,f64,f64 -js_tty_write_stream_get_color_depth f64 ptr,f64 -js_tty_write_stream_get_window_size f64 ptr -js_tty_write_stream_has_colors f64 ptr,f64,f64 -js_tty_write_stream_move_cursor f64 ptr,f64,f64,f64 +js_tty_write_stream_clear_line f64 ptr,i64,f64,f64 +js_tty_write_stream_clear_screen_down f64 ptr,i64,f64 +js_tty_write_stream_cursor_to f64 ptr,i64,f64,f64,f64 +js_tty_write_stream_get_color_depth f64 ptr,i64,f64 +js_tty_write_stream_get_window_size f64 ptr,i64 +js_tty_write_stream_has_colors f64 ptr,i64,f64,f64 +js_tty_write_stream_move_cursor f64 ptr,i64,f64,f64,f64 js_tty_write_stream_new f64 f64 -js_tty_write_stream_on f64 ptr,f64,f64 -js_tty_write_stream_refresh_size f64 ptr -js_tty_write_stream_remove_all_listeners f64 ptr,f64 -js_tty_write_stream_remove_listener f64 ptr,f64,f64 +js_tty_write_stream_on f64 ptr,i64,f64,f64 +js_tty_write_stream_refresh_size f64 ptr,i64 +js_tty_write_stream_remove_all_listeners f64 ptr,i64,f64 +js_tty_write_stream_remove_listener f64 ptr,i64,f64,f64 js_typed_array_at f64 ptr,f64 js_typed_array_copy_within ptr ptr,f64,f64,f64 js_typed_array_every f64 ptr,ptr @@ -4182,12 +4179,12 @@ js_vm_synthetic_module_set_export f64 i64,f64,f64 js_wait_for_event void js_wasi_constructor_call f64 f64 js_wasi_emit_warning f64 -js_wasi_finalize_bindings f64 ptr,f64,f64 -js_wasi_get_import_object f64 ptr -js_wasi_import_stub f64 ptr,f64,f64,f64,f64 -js_wasi_initialize f64 ptr,f64 +js_wasi_finalize_bindings f64 ptr,i64,f64,f64 +js_wasi_get_import_object f64 ptr,i64 +js_wasi_import_stub f64 ptr,i64,f64,f64,f64,f64 +js_wasi_initialize f64 ptr,i64,f64 js_wasi_new f64 f64 -js_wasi_start f64 ptr,f64 +js_wasi_start f64 ptr,i64,f64 js_weak_collection_subclass_init f64 f64,i32s,f64 js_weak_throw_primitive f64 js_weakmap_delete f64 f64,f64 @@ -4641,7 +4638,6 @@ perry_i18n_plural_category i32s i32s,f64 perry_i18n_set_currencies void ptr,i32u perry_i18n_set_locale_index void i32s perry_i18n_set_plural_locales void ptr,ptr,i32u -perry_implicit_this_cell ptr perry_init_guard_check_and_set i32s i64 perry_ios_classes_registered void perry_ios_get_connected_scene i64 @@ -4702,7 +4698,7 @@ perry_string_header_abi_revision i32u perry_stub_warn_ffi void ptr,ptr,ptr perry_thread_exit_probe_object_prototype_recorded bool usize perry_transition_cache_base ptr -perry_tui_state_setter_trampoline f64 ptr,f64 +perry_tui_state_setter_trampoline f64 ptr,i64,f64 perry_update_notify_startup void ptr,i32s perry_updater_check_headers ptr f64 perry_updater_check_url ptr f64 diff --git a/crates/perry-codegen/tests/export_function_alias_identity.rs b/crates/perry-codegen/tests/export_function_alias_identity.rs index 81eb5caa19..043a82604a 100644 --- a/crates/perry-codegen/tests/export_function_alias_identity.rs +++ b/crates/perry-codegen/tests/export_function_alias_identity.rs @@ -79,7 +79,7 @@ fn dollar_export_aliases_resolve_function_identity_not_sanitized_spelling() { .next() .unwrap(); let mut args = if wrapper { - vec!["i64 %this_closure".to_string()] + vec!["i64 %this_closure".to_string(), "i64 %js_this".to_string()] } else { Vec::new() }; diff --git a/crates/perry-ext-events/src/dispatch_ext.rs b/crates/perry-ext-events/src/dispatch_ext.rs index 482a6330be..267839b2dc 100644 --- a/crates/perry-ext-events/src/dispatch_ext.rs +++ b/crates/perry-ext-events/src/dispatch_ext.rs @@ -193,12 +193,12 @@ mod tests { use super::*; use perry_ffi::alloc_string; - extern "C" fn noop_listener(_c: *const RawClosureHeader) -> f64 { + extern "C" fn noop_listener(_c: *const RawClosureHeader, _this: perry_ffi::JsThis) -> f64 { undefined_value() } fn listener_value() -> f64 { - let closure = unsafe { js_closure_alloc(noop_listener as *const u8, 0) }; + let closure = perry_ffi::alloc_closure(noop_listener as perry_ffi::JsBody0, 0); nanbox_pointer_bits(closure as i64) } diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index 5b6f443e53..bb2898355a 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -117,17 +117,16 @@ extern "C" { on_fulfilled: *mut RawClosureHeader, on_rejected: *mut RawClosureHeader, ) -> *mut Promise; - // #1557: closure allocation hooks needed by events.on's queue-listener. + // #1557: closure capture hooks needed by events.on's queue-listener. // Mirrors perry-runtime::closure exports; declared here because perry-ffi - // doesn't yet expose them and perry-ext-events deliberately avoids a - // direct perry-runtime dep. - fn js_closure_alloc(fn_ptr: *const u8, capture_count: u32) -> *mut RawClosureHeader; + // doesn't expose them and perry-ext-events deliberately avoids a direct + // perry-runtime dep. Bodies are allocated through perry-ffi's typed + // `alloc_closure`. fn js_closure_set_capture_ptr(closure: *mut RawClosureHeader, slot: u32, ptr: i64); fn js_closure_get_capture_ptr(closure: *const RawClosureHeader, slot: u32) -> i64; fn js_array_push_f64(arr: *mut ArrayHeader, value: f64) -> *mut ArrayHeader; fn js_array_shift_f64(arr: *mut ArrayHeader) -> f64; fn js_promise_new() -> *mut Promise; - fn js_register_closure_arity(func_ptr: *const u8, arity: u32); fn js_closure_get_capture_f64(closure: *const RawClosureHeader, slot: u32) -> f64; fn js_closure_set_capture_f64(closure: *mut RawClosureHeader, slot: u32, value: f64); // #1557: AbortSignal listener attachment for events.addAbortListener. @@ -178,9 +177,13 @@ extern "C" { fn js_abort_error_value() -> f64; fn js_throw(value: f64) -> !; fn js_domain_emit_error(handle: Handle, error: f64, emitter: f64, domain_thrown: bool) -> bool; - fn js_implicit_this_set(value: f64) -> f64; fn js_jsvalue_to_string(value: f64) -> *mut StringHeader; - fn js_native_call_value(func_value: f64, args_ptr: *const f64, args_len: usize) -> f64; + fn js_native_call_value( + func_value: f64, + this: perry_ffi::JsThis, + args_ptr: *const f64, + args_len: usize, + ) -> f64; fn js_value_is_promise(value: f64) -> i32; fn js_register_event_emitter_handle_probe(f: unsafe extern "C" fn(i64) -> bool); fn js_register_event_emitter_async_resource_handle_probe(f: unsafe extern "C" fn(i64) -> bool); @@ -204,7 +207,6 @@ extern "C" { // TypeError [ERR_INVALID_ARG_TYPE]. Centralized in perry-runtime so the // stdlib and ext-events EventEmitter implementations stay byte-identical. fn js_validate_event_listener(listener_bits: i64, name_ptr: *const u8, name_len: u32) -> i64; - fn js_register_closure_rest(fn_ptr: *const u8, fixed_arity: u32); fn js_async_resource_new(type_value: f64, options: f64) -> i64; fn js_async_resource_async_id(handle: i64) -> f64; fn js_async_resource_trigger_async_id(handle: i64) -> f64; @@ -857,8 +859,8 @@ fn remove_listener_by_callback(emitter: &mut EventEmitterHandle, callback: i64) static RAW_ONCE_WRAPPER_REST_REGISTERED: Once = Once::new(); fn ensure_raw_once_wrapper_rest_registered() { - RAW_ONCE_WRAPPER_REST_REGISTERED.call_once(|| unsafe { - js_register_closure_rest(event_emitter_once_wrapper as *const u8, 0); + RAW_ONCE_WRAPPER_REST_REGISTERED.call_once(|| { + perry_ffi::register_closure_rest(event_emitter_once_wrapper as perry_ffi::JsBody1, 0); }); } @@ -873,7 +875,7 @@ unsafe fn create_once_raw_wrapper(handle: Handle, event_name: &str, callback: i6 } ensure_raw_once_wrapper_rest_registered(); - let wrapper = js_closure_alloc(event_emitter_once_wrapper as *const u8, 4); + let wrapper = perry_ffi::alloc_closure(event_emitter_once_wrapper as perry_ffi::JsBody1, 4); let event_ptr = js_string_from_bytes(event_name.as_ptr(), event_name.len() as u32); js_closure_set_capture_ptr(wrapper, 0, handle); js_closure_set_capture_ptr(wrapper, 1, event_ptr as i64); @@ -891,7 +893,11 @@ unsafe fn create_once_raw_wrapper(handle: Handle, event_name: &str, callback: i6 wrapper as i64 } -extern "C" fn event_emitter_once_wrapper(closure: *const RawClosureHeader, rest: f64) -> f64 { +extern "C" fn event_emitter_once_wrapper( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, + rest: f64, +) -> f64 { unsafe { let handle = js_closure_get_capture_ptr(closure, 0) as Handle; let event_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const StringHeader; @@ -1146,18 +1152,18 @@ unsafe fn collect_emit_args(args_ptr: *const ArrayHeader) -> Vec { unsafe fn call_emitter_listener(handle: Handle, callback: i64, args: &[f64]) -> f64 { let receiver = nanbox_pointer_bits(handle); let callback_value = nanbox_pointer_bits(callback); - let previous_this = js_implicit_this_set(receiver); - let result = if args.is_empty() { - js_native_call_value(callback_value, std::ptr::null(), 0) + // A listener runs with the emitter as `this`. + let this = perry_ffi::JsThis::from_f64(receiver); + if args.is_empty() { + js_native_call_value(callback_value, this, std::ptr::null(), 0) } else { - js_native_call_value(callback_value, args.as_ptr(), args.len()) - }; - js_implicit_this_set(previous_this); - result + js_native_call_value(callback_value, this, args.as_ptr(), args.len()) + } } extern "C" fn events_capture_rejection_handler( closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, reason: f64, ) -> f64 { unsafe { @@ -1181,7 +1187,8 @@ unsafe fn capture_listener_rejection(handle: Handle, result: f64) { if promise.is_null() { return; } - let on_rejected = js_closure_alloc(events_capture_rejection_handler as *const u8, 1); + let on_rejected = + perry_ffi::alloc_closure(events_capture_rejection_handler as perry_ffi::JsBody1, 1); js_closure_set_capture_ptr(on_rejected, 0, handle); js_promise_then(promise, std::ptr::null_mut(), on_rejected); } @@ -1806,7 +1813,8 @@ pub unsafe extern "C" fn js_events_once( }; if let Some(signal) = signal { if let Some(signal_ptr) = object_ptr_from_value(signal) { - let abort_listener = js_closure_alloc(events_once_abort_listener as *const u8, 2); + let abort_listener = + perry_ffi::alloc_closure(events_once_abort_listener as perry_ffi::JsBody0, 2); js_closure_set_capture_ptr(abort_listener, 0, handle); js_closure_set_capture_ptr(abort_listener, 1, raw as i64); js_abort_signal_add_listener( @@ -1830,7 +1838,8 @@ pub unsafe extern "C" fn js_events_once( if event_name_ptr.is_null() { return raw; } - let listener = js_closure_alloc(events_once_event_target_listener as *const u8, 3); + let listener = + perry_ffi::alloc_closure(events_once_event_target_listener as perry_ffi::JsBody1, 3); js_closure_set_capture_ptr(listener, 0, raw as i64); js_closure_set_capture_ptr(listener, 1, target as i64); js_closure_set_capture_ptr(listener, 2, event_name_ptr as i64); @@ -1846,21 +1855,30 @@ pub unsafe extern "C" fn js_events_once( let event = scope.root_nanbox(f64::from_bits(nanbox_string_bits( event_name_ptr as *mut StringHeader, ))); - js_register_closure_rest(events_once_stream_resolve_listener as *const u8, 0); - let listener = js_closure_alloc(events_once_stream_resolve_listener as *const u8, 4); + perry_ffi::register_closure_rest( + events_once_stream_resolve_listener as perry_ffi::JsBody1, + 0, + ); + let listener = + perry_ffi::alloc_closure(events_once_stream_resolve_listener as perry_ffi::JsBody1, 4); js_closure_set_capture_ptr(listener, 0, raw as i64); js_closure_set_capture_ptr(listener, 1, handle); js_closure_set_capture_ptr(listener, 2, 0); js_closure_set_capture_ptr(listener, 3, 0); let listener = scope.root_addr(listener as i64); if event_name != "error" { - js_register_closure_rest(events_once_stream_reject_listener as *const u8, 0); + perry_ffi::register_closure_rest( + events_once_stream_reject_listener as perry_ffi::JsBody1, + 0, + ); let error_event_ptr = js_string_from_bytes(b"error".as_ptr(), 5); let error_event = scope.root_nanbox(f64::from_bits(nanbox_string_bits( error_event_ptr as *mut StringHeader, ))); - let reject_listener = - js_closure_alloc(events_once_stream_reject_listener as *const u8, 4); + let reject_listener = perry_ffi::alloc_closure( + events_once_stream_reject_listener as perry_ffi::JsBody1, + 4, + ); let reject_listener = scope.root_addr(reject_listener as i64); let event_name_ptr = (event.get().to_bits() & POINTER_MASK) as i64; js_closure_set_capture_ptr( @@ -1908,9 +1926,16 @@ pub unsafe extern "C" fn js_events_once( let event = scope.root_nanbox(f64::from_bits(nanbox_string_bits( event_name_ptr as *mut StringHeader, ))); - js_register_closure_rest(events_once_stream_resolve_listener as *const u8, 0); - js_register_closure_rest(events_once_stream_reject_listener as *const u8, 0); - let listener = js_closure_alloc(events_once_stream_resolve_listener as *const u8, 4); + perry_ffi::register_closure_rest( + events_once_stream_resolve_listener as perry_ffi::JsBody1, + 0, + ); + perry_ffi::register_closure_rest( + events_once_stream_reject_listener as perry_ffi::JsBody1, + 0, + ); + let listener = + perry_ffi::alloc_closure(events_once_stream_resolve_listener as perry_ffi::JsBody1, 4); js_closure_set_capture_ptr(listener, 0, raw as i64); js_closure_set_capture_ptr(listener, 1, handle); js_closure_set_capture_ptr(listener, 2, 0); @@ -1921,8 +1946,10 @@ pub unsafe extern "C" fn js_events_once( let error_event = scope.root_nanbox(f64::from_bits(nanbox_string_bits( error_event_ptr as *mut StringHeader, ))); - let reject_listener = - js_closure_alloc(events_once_stream_reject_listener as *const u8, 4); + let reject_listener = perry_ffi::alloc_closure( + events_once_stream_reject_listener as perry_ffi::JsBody1, + 4, + ); let reject_listener = scope.root_addr(reject_listener as i64); js_closure_set_capture_ptr( reject_listener.get() as *mut RawClosureHeader, diff --git a/crates/perry-ext-events/src/max_listeners.rs b/crates/perry-ext-events/src/max_listeners.rs index 08ef6f75be..3509507021 100644 --- a/crates/perry-ext-events/src/max_listeners.rs +++ b/crates/perry-ext-events/src/max_listeners.rs @@ -114,10 +114,14 @@ unsafe fn call_process_emit_warning(warning: f64) { let process_value = JsValue::from_bits(process.to_bits()); let process_obj = process_value.as_pointer::(); let emit_warning = get_object_field_by_str(process_obj, "emitWarning"); - let previous_this = js_implicit_this_set(process); + // `process.emitWarning(warning)`: a method call of `process`. let args = [warning]; - js_native_call_value(emit_warning, args.as_ptr(), args.len()); - js_implicit_this_set(previous_this); + js_native_call_value( + emit_warning, + perry_ffi::JsThis::from_f64(process), + args.as_ptr(), + args.len(), + ); } pub(super) unsafe fn emit_max_listeners_warning( diff --git a/crates/perry-ext-events/src/module_iterators.rs b/crates/perry-ext-events/src/module_iterators.rs index e7584299df..2f38ea6b51 100644 --- a/crates/perry-ext-events/src/module_iterators.rs +++ b/crates/perry-ext-events/src/module_iterators.rs @@ -2,6 +2,7 @@ use super::*; pub(super) extern "C" fn events_once_event_target_listener( closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, arg0: f64, ) -> f64 { unsafe { @@ -21,7 +22,10 @@ pub(super) extern "C" fn events_once_event_target_listener( undefined_value() } -pub(super) extern "C" fn events_once_abort_listener(closure: *const RawClosureHeader) -> f64 { +pub(super) extern "C" fn events_once_abort_listener( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { unsafe { let handle = js_closure_get_capture_ptr(closure, 0) as Handle; let promise = js_closure_get_capture_ptr(closure, 1) as *mut Promise; @@ -40,6 +44,7 @@ pub(super) extern "C" fn events_once_abort_listener(closure: *const RawClosureHe pub(super) extern "C" fn events_once_stream_resolve_listener( closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, rest: f64, ) -> f64 { unsafe { @@ -79,6 +84,7 @@ pub(super) extern "C" fn events_once_stream_resolve_listener( pub(super) extern "C" fn events_once_stream_reject_listener( closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, rest: f64, ) -> f64 { unsafe { @@ -251,6 +257,7 @@ fn events_on_finish_pending(state: *mut ArrayHeader, reason: Option) { /// immediately, otherwise retain the argument tuple in FIFO order. pub(super) extern "C" fn events_on_queue_listener( closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, arg0: f64, ) -> f64 { unsafe { @@ -289,7 +296,7 @@ pub(super) extern "C" fn events_on_queue_listener( f64::from_bits(TAG_UNDEFINED_F64_BITS) } -extern "C" fn events_on_next(closure: *const RawClosureHeader) -> f64 { +extern "C" fn events_on_next(closure: *const RawClosureHeader, _this: perry_ffi::JsThis) -> f64 { unsafe { let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; if state.is_null() { @@ -318,7 +325,7 @@ extern "C" fn events_on_next(closure: *const RawClosureHeader) -> f64 { } } -extern "C" fn events_on_return(closure: *const RawClosureHeader) -> f64 { +extern "C" fn events_on_return(closure: *const RawClosureHeader, _this: perry_ffi::JsThis) -> f64 { unsafe { let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; if state.is_null() { @@ -366,11 +373,17 @@ extern "C" fn events_on_return(closure: *const RawClosureHeader) -> f64 { } } -extern "C" fn events_on_iterator_self(closure: *const RawClosureHeader) -> f64 { +extern "C" fn events_on_iterator_self( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { unsafe { js_closure_get_capture_f64(closure, 0) } } -extern "C" fn events_on_async_iterator(closure: *const RawClosureHeader) -> f64 { +extern "C" fn events_on_async_iterator( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { unsafe { let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; let scope = TransientRootScope::enter(); @@ -383,7 +396,7 @@ extern "C" fn events_on_async_iterator(closure: *const RawClosureHeader) -> f64 packed.len() as u32, ); let object_root = scope.root_nanbox(nanbox_pointer_bits(object as i64)); - let next = js_closure_alloc(events_on_next as *const u8, 1); + let next = perry_ffi::alloc_closure(events_on_next as perry_ffi::JsBody0, 1); js_closure_set_capture_ptr(next, 0, (state_root.get().to_bits() & POINTER_MASK) as i64); let next_root = scope.root_addr(next as i64); js_object_set_field( @@ -391,7 +404,7 @@ extern "C" fn events_on_async_iterator(closure: *const RawClosureHeader) -> f64 0, JsValue::from_object_ptr(next_root.get() as *mut u8), ); - let return_fn = js_closure_alloc(events_on_return as *const u8, 1); + let return_fn = perry_ffi::alloc_closure(events_on_return as perry_ffi::JsBody0, 1); js_closure_set_capture_ptr( return_fn, 0, @@ -407,7 +420,7 @@ extern "C" fn events_on_async_iterator(closure: *const RawClosureHeader) -> f64 let iterator = object_root.get(); let iterator_root = scope.root_nanbox(iterator); let symbol = js_symbol_well_known_async_iterator(); - let self_fn = js_closure_alloc(events_on_iterator_self as *const u8, 1); + let self_fn = perry_ffi::alloc_closure(events_on_iterator_self as perry_ffi::JsBody0, 1); js_closure_set_capture_f64(self_fn, 0, iterator_root.get()); js_object_set_symbol_property( iterator_root.get(), @@ -425,11 +438,11 @@ pub(super) unsafe fn events_on_install_async_iterator( let scope = TransientRootScope::enter(); let queue_root = scope.root_nanbox(nanbox_pointer_bits(queue as i64)); let state_root = scope.root_nanbox(nanbox_pointer_bits(state as i64)); - js_register_closure_arity(events_on_next as *const u8, 0); - js_register_closure_arity(events_on_return as *const u8, 0); - js_register_closure_arity(events_on_iterator_self as *const u8, 0); - js_register_closure_arity(events_on_async_iterator as *const u8, 0); - let closure = js_closure_alloc(events_on_async_iterator as *const u8, 1); + perry_ffi::register_closure_arity(events_on_next as perry_ffi::JsBody0, 0); + perry_ffi::register_closure_arity(events_on_return as perry_ffi::JsBody0, 0); + perry_ffi::register_closure_arity(events_on_iterator_self as perry_ffi::JsBody0, 0); + perry_ffi::register_closure_arity(events_on_async_iterator as perry_ffi::JsBody0, 0); + let closure = perry_ffi::alloc_closure(events_on_async_iterator as perry_ffi::JsBody0, 1); js_closure_set_capture_ptr( closure, 0, @@ -443,7 +456,10 @@ pub(super) unsafe fn events_on_install_async_iterator( } /// A configured close event ends the iterator after already-buffered events. -pub(super) extern "C" fn events_on_close_listener(closure: *const RawClosureHeader) -> f64 { +pub(super) extern "C" fn events_on_close_listener( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { unsafe { let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; if !state.is_null() { @@ -454,7 +470,10 @@ pub(super) extern "C" fn events_on_close_listener(closure: *const RawClosureHead undefined_value() } -pub(super) extern "C" fn events_on_abort_listener(closure: *const RawClosureHeader) -> f64 { +pub(super) extern "C" fn events_on_abort_listener( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { unsafe { let handle = js_closure_get_capture_ptr(closure, 0) as Handle; let data_listener = js_closure_get_capture_ptr(closure, 1); diff --git a/crates/perry-ext-events/src/module_on.rs b/crates/perry-ext-events/src/module_on.rs index a583696c80..4140a6998a 100644 --- a/crates/perry-ext-events/src/module_on.rs +++ b/crates/perry-ext-events/src/module_on.rs @@ -30,7 +30,7 @@ pub unsafe extern "C" fn js_events_on( if signal.is_some_and(signal_is_aborted) { js_throw(js_abort_error_value()); } - let listener = js_closure_alloc(events_on_queue_listener as *const u8, 1); + let listener = perry_ffi::alloc_closure(events_on_queue_listener as perry_ffi::JsBody1, 1); js_closure_set_capture_ptr( listener, 0, @@ -92,7 +92,8 @@ pub unsafe extern "C" fn js_events_on( }; let close_ptr = js_string_from_bytes(close_name.as_ptr(), close_name.len() as u32); - let close_listener = js_closure_alloc(events_on_close_listener as *const u8, 1); + let close_listener = + perry_ffi::alloc_closure(events_on_close_listener as perry_ffi::JsBody0, 1); js_closure_set_capture_ptr( close_listener, 0, @@ -136,7 +137,8 @@ pub unsafe extern "C" fn js_events_on( } if let Some(signal) = signal { if let Some(signal_ptr) = object_ptr_from_value(signal) { - let abort_listener = js_closure_alloc(events_on_abort_listener as *const u8, 5); + let abort_listener = + perry_ffi::alloc_closure(events_on_abort_listener as perry_ffi::JsBody0, 5); js_closure_set_capture_ptr(abort_listener, 0, handle); js_closure_set_capture_ptr(abort_listener, 1, listener_root.get()); js_closure_set_capture_ptr(abort_listener, 2, signal_ptr as i64); @@ -156,7 +158,10 @@ pub unsafe extern "C" fn js_events_on( (queue_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader } -extern "C" fn events_abort_listener_dispose(closure: *const RawClosureHeader) -> f64 { +extern "C" fn events_abort_listener_dispose( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { unsafe { let signal_ptr = js_closure_get_capture_ptr(closure, 0); let callback_ptr = js_closure_get_capture_ptr(closure, 1); @@ -190,7 +195,8 @@ pub unsafe extern "C" fn js_events_add_abort_listener(signal: f64, listener: f64 abort_event_value(), nanbox_pointer_bits(callback_ptr), ); - let dispose_closure = js_closure_alloc(events_abort_listener_dispose as *const u8, 2); + let dispose_closure = + perry_ffi::alloc_closure(events_abort_listener_dispose as perry_ffi::JsBody0, 2); let dispose_closure_root = root_scope.root_addr(dispose_closure as i64); let signal_ptr = object_ptr_from_value(signal_root.get()) .expect("validated AbortSignal remained a rooted object"); diff --git a/crates/perry-ext-events/src/tests.rs b/crates/perry-ext-events/src/tests.rs index d65797ac45..db53a00c2d 100644 --- a/crates/perry-ext-events/src/tests.rs +++ b/crates/perry-ext-events/src/tests.rs @@ -58,12 +58,12 @@ fn young_promise_root() -> *mut Promise { /// The listener-registration FFIs take raw NaN-box bits and validate that the /// value is a closure. Allocate a real closure and return its NaN-boxed bits /// as i64; the sentinel function pointer is never invoked in these tests. -extern "C" fn noop_listener(_c: *const RawClosureHeader) -> f64 { +extern "C" fn noop_listener(_c: *const RawClosureHeader, _this: perry_ffi::JsThis) -> f64 { f64::from_bits(TAG_UNDEFINED_F64_BITS) } fn fake_listener() -> i64 { - let closure = unsafe { js_closure_alloc(noop_listener as *const u8, 0) }; + let closure = perry_ffi::alloc_closure(noop_listener as perry_ffi::JsBody0, 0); nanbox_pointer_bits(closure as i64).to_bits() as i64 } diff --git a/crates/perry-ext-http/src/agent.rs b/crates/perry-ext-http/src/agent.rs index ff308b8124..3370516358 100644 --- a/crates/perry-ext-http/src/agent.rs +++ b/crates/perry-ext-http/src/agent.rs @@ -313,7 +313,10 @@ unsafe fn read_string_field(obj_f64: f64, field: &str) -> Option { JsValue::from_bits(bits).to_owned_string() } -extern "C" fn agent_connection_abort_listener(closure: *const RawClosureHeader) -> f64 { +extern "C" fn agent_connection_abort_listener( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { let socket = unsafe { perry_ffi::closure_capture_f64(closure, 0) } as i64; let signal_value = unsafe { perry_ffi::closure_capture_f64(closure, 1) }; unsafe { @@ -361,7 +364,8 @@ unsafe fn install_connection_abort_signal(options: f64, socket: Handle) { perry_ext_net::js_ext_net_socket_emit_abort_error(socket); return; } - let listener = perry_ffi::alloc_closure(agent_connection_abort_listener as *const u8, 2); + let listener = + perry_ffi::alloc_closure(agent_connection_abort_listener as perry_ffi::JsBody0, 2); if listener.is_null() { return; } @@ -1644,7 +1648,7 @@ unsafe fn invoke_create_connection_closure( let cc = scope.root_addr(closure_ptr); let options = scope.root_nanbox(build_connect_options(agent_handle, host, port, path)); let closure = JsClosure::from_raw(cc.get() as *const RawClosureHeader); - let ret = closure.call1(options.get()); + let ret = closure.call1(perry_ffi::JsThis::UNDEFINED, options.get()); // `net.connect` / `net.createConnection` return the socket id NaN-boxed // with POINTER_TAG; some codegen paths hand back a bare raw pointer. diff --git a/crates/perry-ext-http/src/client_abort.rs b/crates/perry-ext-http/src/client_abort.rs index e9f08a177e..a67a5a16b7 100644 --- a/crates/perry-ext-http/src/client_abort.rs +++ b/crates/perry-ext-http/src/client_abort.rs @@ -14,7 +14,10 @@ fn abort_event_value() -> f64 { f64::from_bits(JsValue::from_string_ptr(alloc_string("abort").as_raw()).bits()) } -extern "C" fn request_signal_listener(closure: *const RawClosureHeader) -> f64 { +extern "C" fn request_signal_listener( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { let request_handle = unsafe { perry_ffi::closure_capture_f64(closure, 0) } as i64; push_event(PendingHttpEvent::SignalAbort { request_handle }); f64::from_bits(TAG_UNDEFINED) @@ -37,7 +40,7 @@ pub(crate) unsafe fn attach_request_signal(request_handle: Handle, options: f64) return; } - let listener = perry_ffi::alloc_closure(request_signal_listener as *const u8, 1); + let listener = perry_ffi::alloc_closure(request_signal_listener as perry_ffi::JsBody0, 1); if listener.is_null() { return; } diff --git a/crates/perry-ext-http/src/client_events.rs b/crates/perry-ext-http/src/client_events.rs index 6d5ba6270c..2c4bd57426 100644 --- a/crates/perry-ext-http/src/client_events.rs +++ b/crates/perry-ext-http/src/client_events.rs @@ -113,7 +113,7 @@ pub(crate) unsafe fn fire_request_event_listeners(request_handle: Handle, event: for cb in listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } } @@ -136,7 +136,7 @@ pub(crate) unsafe fn fire_request_socket_event(request_handle: Handle) { for callback in listeners { if callback.get() != 0 { let closure = JsClosure::from_raw(callback.get() as *const RawClosureHeader); - let _ = closure.call1(value); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, value); } } } @@ -151,9 +151,9 @@ unsafe fn fire_incoming_event(incoming: Handle, event: &str, arg: Option) { } let closure = JsClosure::from_raw(callback as *const RawClosureHeader); if let Some(value) = arg { - let _ = closure.call1(value); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, value); } else { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } } @@ -189,7 +189,7 @@ pub(crate) unsafe fn fire_request_error_listeners(request_handle: Handle, arg: f for cb in listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call1(arg.get()); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg.get()); } } } @@ -314,14 +314,14 @@ pub(crate) unsafe fn handle_response_event( let response_listeners = scope.root_addrs(&response_listeners); if response_callback.get() != 0 { let closure = JsClosure::from_raw(response_callback.get() as *const RawClosureHeader); - let _ = closure.call1(arg); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg); } // #4909 — `.on('response', cb)` listeners fire too (the factory // callback is just Node's pre-registered once-listener). for cb in response_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call1(arg); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg); } } @@ -354,7 +354,7 @@ pub(crate) unsafe fn handle_response_event( for cb in &data_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call1(arg.get()); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg.get()); } } for dest in &pipes { @@ -372,7 +372,7 @@ pub(crate) unsafe fn handle_response_event( for cb in end_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } @@ -478,7 +478,12 @@ pub(crate) unsafe fn handle_upgrade_event( for cb in listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call3(res_arg.get(), socket_arg.get(), head_arg.get()); + let _ = closure.call3( + perry_ffi::JsThis::UNDEFINED, + res_arg.get(), + socket_arg.get(), + head_arg.get(), + ); } } @@ -544,12 +549,12 @@ pub(crate) unsafe fn handle_response_head_event( let response_listeners = scope.root_addrs(&response_listeners); if response_callback.get() != 0 { let closure = JsClosure::from_raw(response_callback.get() as *const RawClosureHeader); - let _ = closure.call1(arg); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg); } for cb in response_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call1(arg); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg); } } } @@ -608,7 +613,7 @@ pub(crate) unsafe fn handle_response_chunk_event(request_handle: Handle, chunk: for cb in data_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call1(arg.get()); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg.get()); } } for dest in pipes { @@ -674,7 +679,7 @@ pub(crate) unsafe fn handle_response_end_event(request_handle: Handle) { for cb in data_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call1(arg.get()); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg.get()); } } for dest in pipes { @@ -697,7 +702,7 @@ pub(crate) unsafe fn handle_response_end_event(request_handle: Handle) { for cb in end_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } @@ -863,7 +868,7 @@ pub(crate) unsafe fn handle_timeout_event(request_handle: Handle) { for cb in timeout_listeners { if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } // The transport deadline killed an in-flight exchange; if the handler @@ -894,12 +899,12 @@ pub(crate) unsafe fn handle_flushed_event(request_handle: Handle) { for cb in write_cbs { if cb != 0 { let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } fire_request_event_listeners(request_handle, "finish"); if end_cb != 0 { let closure = JsClosure::from_raw(end_cb as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } diff --git a/crates/perry-ext-http/src/client_request_surface.rs b/crates/perry-ext-http/src/client_request_surface.rs index 74a66625be..3d39699fbd 100644 --- a/crates/perry-ext-http/src/client_request_surface.rs +++ b/crates/perry-ext-http/src/client_request_surface.rs @@ -17,8 +17,12 @@ extern "C" { method_name_ptr: *const u8, method_name_len: usize, ) -> f64; - fn js_register_closure_rest(func_ptr: *const u8, fixed_arity: u32); - fn js_closure_call_array(closure: i64, args: *const f64, args_len: i64) -> f64; + fn js_closure_call_array( + closure: i64, + this: perry_ffi::JsThis, + args: *const f64, + args_len: i64, + ) -> f64; fn js_object_set_field_by_name(object: *mut ObjectHeader, key: *const StringHeader, value: f64); } @@ -33,15 +37,15 @@ pub(crate) fn create_client_once_wrapper( if callback == 0 { return 0; } - CLIENT_ONCE_WRAPPER_REGISTERED.call_once(|| unsafe { - js_register_closure_rest(client_once_wrapper as *const u8, 0); + CLIENT_ONCE_WRAPPER_REGISTERED.call_once(|| { + perry_ffi::register_closure_rest(client_once_wrapper as perry_ffi::JsBody1, 0); }); let scope = perry_ffi::TransientRootScope::enter(); let callback = scope.root_addr(callback); let event = scope.root_nanbox(f64::from_bits( JsValue::from_string_ptr(alloc_string(event).as_raw()).bits(), )); - let wrapper = perry_ffi::alloc_closure(client_once_wrapper as *const u8, 5); + let wrapper = perry_ffi::alloc_closure(client_once_wrapper as perry_ffi::JsBody1, 5); let wrapper = scope.root_addr(wrapper as i64); let wrapper_ptr = wrapper.get() as *mut RawClosureHeader; unsafe { @@ -72,7 +76,11 @@ pub(crate) fn create_client_once_wrapper( wrapper.get() } -extern "C" fn client_once_wrapper(closure: *const RawClosureHeader, rest: f64) -> f64 { +extern "C" fn client_once_wrapper( + closure: *const RawClosureHeader, + this: perry_ffi::JsThis, + rest: f64, +) -> f64 { unsafe { let handle = perry_ffi::closure_capture_f64(closure, 0) as Handle; let event_value = perry_ffi::closure_capture_f64(closure, 1); @@ -139,16 +147,16 @@ extern "C" fn client_once_wrapper(closure: *const RawClosureHeader, rest: f64) - } let value = JsValue::from_bits(rest.to_bits()); if !value.is_pointer() { - return js_closure_call_array(callback, std::ptr::null(), 0); + return js_closure_call_array(callback, this, std::ptr::null(), 0); } let array = value.as_pointer::(); if array.is_null() { - return js_closure_call_array(callback, std::ptr::null(), 0); + return js_closure_call_array(callback, this, std::ptr::null(), 0); } // The rest ABI creates this fresh, unshifted argument array for this // invocation; no JS callback has run since it was packed. let args = (array as *const u8).add(8) as *const f64; - js_closure_call_array(callback, args, (*array).length as i64) + js_closure_call_array(callback, this, args, (*array).length as i64) } } diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index 1e4f4ebdf3..8f5f06e6ee 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -924,10 +924,10 @@ unsafe fn invoke_create_socket( // of reading an uninitialized register for the second parameter. static REGISTER_ARITY: Once = Once::new(); REGISTER_ARITY.call_once(|| { - perry_ffi::register_closure_arity(http_create_socket_cb as *const u8, 2); + perry_ffi::register_closure_arity(http_create_socket_cb as perry_ffi::JsBody2, 2); }); - let cb = perry_ffi::alloc_closure(http_create_socket_cb as *const u8, 1); + let cb = perry_ffi::alloc_closure(http_create_socket_cb as perry_ffi::JsBody2, 1); if cb.is_null() { return; } @@ -949,7 +949,12 @@ unsafe fn invoke_create_socket( )); let closure = JsClosure::from_raw(cs.get() as *const RawClosureHeader); - closure.call3(req_val, options.get(), cb_val.get()); + closure.call3( + perry_ffi::JsThis::UNDEFINED, + req_val, + options.get(), + cb_val.get(), + ); } /// Continuation for a `createSocket` override's `cb(err, socket)` callback. @@ -960,6 +965,7 @@ unsafe fn invoke_create_socket( /// small handle on some codegen paths). unsafe extern "C" fn http_create_socket_cb( closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, err: f64, socket: f64, ) -> f64 { diff --git a/crates/perry-ext-http/src/server/bun_server.rs b/crates/perry-ext-http/src/server/bun_server.rs index a9f964e264..c58456649e 100644 --- a/crates/perry-ext-http/src/server/bun_server.rs +++ b/crates/perry-ext-http/src/server/bun_server.rs @@ -9,7 +9,7 @@ use perry_ffi::{ GcRootVisitor, JsClosure, JsValue, RawClosureHeader, StringHeader, TransientRootScope, }; -use crate::server::request::{handle_to_pointer_f64, with_implicit_this, IncomingMessage}; +use crate::server::request::{handle_to_pointer_f64, IncomingMessage}; use crate::server::response::ServerResponse; use crate::server::server::{ finalize_or_park_request, synthesize_default_response_if_needed, HttpPendingRequest, HttpServer, @@ -382,7 +382,7 @@ fn invoke_fetch(server_handle: i64, request: f64) -> ClosureCallResult { } let closure = unsafe { JsClosure::from_raw(handler.get() as *const RawClosureHeader) }; let server = handle_to_pointer_f64(server_handle); - unsafe { call_catching(|| with_implicit_this(server, || closure.call2(request, server))) } + unsafe { call_catching(|| closure.call2(perry_ffi::JsThis::from_f64(server), request, server)) } } fn invoke_error(server_handle: i64, reason: f64) -> Option { @@ -394,7 +394,9 @@ fn invoke_error(server_handle: i64, reason: f64) -> Option { } let closure = unsafe { JsClosure::from_raw(handler.get() as *const RawClosureHeader) }; let server = handle_to_pointer_f64(server_handle); - Some(unsafe { call_catching(|| with_implicit_this(server, || closure.call1(reason.get()))) }) + Some(unsafe { + call_catching(|| closure.call1(perry_ffi::JsThis::from_f64(server), reason.get())) + }) } fn apply_response(response_handle: i64, value: f64) -> bool { diff --git a/crates/perry-ext-http/src/server/http2_server.rs b/crates/perry-ext-http/src/server/http2_server.rs index 5deff28fc4..1daaf62ace 100644 --- a/crates/perry-ext-http/src/server/http2_server.rs +++ b/crates/perry-ext-http/src/server/http2_server.rs @@ -394,7 +394,7 @@ pub(crate) fn call0(callback: i64) { let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } } @@ -407,7 +407,7 @@ pub(crate) fn call1(callback: i64, arg: f64) { let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call1(arg); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, arg); } } } @@ -420,7 +420,7 @@ pub(crate) fn call2(callback: i64, arg0: f64, arg1: f64) { let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call2(arg0, arg1); + let _ = closure.call2(perry_ffi::JsThis::UNDEFINED, arg0, arg1); } } } @@ -433,7 +433,7 @@ pub(crate) fn call3(callback: i64, arg0: f64, arg1: f64, arg2: f64) { let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call3(arg0, arg1, arg2); + let _ = closure.call3(perry_ffi::JsThis::UNDEFINED, arg0, arg1, arg2); } } } diff --git a/crates/perry-ext-http/src/server/http2_server/pump.rs b/crates/perry-ext-http/src/server/http2_server/pump.rs index 09ce2fc367..b1fa9d8472 100644 --- a/crates/perry-ext-http/src/server/http2_server/pump.rs +++ b/crates/perry-ext-http/src/server/http2_server/pump.rs @@ -7,7 +7,7 @@ use perry_ffi::{ alloc_buffer, get_handle, get_handle_mut, iter_handles_of, JsClosure, RawClosureHeader, }; -use crate::server::request::{handle_to_pointer_f64, with_implicit_this}; +use crate::server::request::handle_to_pointer_f64; use crate::server::response::ResponseShape; use crate::server::server::{finalize_or_park_request, HttpPendingRequest}; use crate::server::types::{js_promise_run_microtasks, POINTER_TAG, PTR_MASK, TAG_UNDEFINED}; @@ -64,9 +64,7 @@ pub(crate) fn process_pending_h2(pending: HttpPendingRequest) { let raw = handler.get() as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } @@ -80,9 +78,7 @@ pub(crate) fn process_pending_h2(pending: HttpPendingRequest) { let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } @@ -106,7 +102,8 @@ pub(crate) fn process_pending_h2(pending: HttpPendingRequest) { let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call2(stream_f64, headers_f64.get()); + let _ = + closure.call2(perry_ffi::JsThis::UNDEFINED, stream_f64, headers_f64.get()); } js_promise_run_microtasks(); } diff --git a/crates/perry-ext-http/src/server/https_server.rs b/crates/perry-ext-http/src/server/https_server.rs index 13142d461c..195b3d7e75 100644 --- a/crates/perry-ext-http/src/server/https_server.rs +++ b/crates/perry-ext-http/src/server/https_server.rs @@ -12,7 +12,7 @@ use perry_ffi::{ }; use crate::server::ensure_gc_scanner_registered; -use crate::server::request::{handle_to_pointer_f64, with_implicit_this}; +use crate::server::request::handle_to_pointer_f64; use crate::server::server::{ sanitize_request_timeout, signal_connections_close, HttpPendingRequest, HttpServer, }; @@ -470,9 +470,8 @@ pub(crate) fn process_pending_https(pending: HttpPendingRequest) { let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = + closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } @@ -485,9 +484,7 @@ pub(crate) fn process_pending_https(pending: HttpPendingRequest) { let raw = handler_rooted.get() as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } @@ -501,9 +498,7 @@ pub(crate) fn process_pending_https(pending: HttpPendingRequest) { let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } diff --git a/crates/perry-ext-http/src/server/request.rs b/crates/perry-ext-http/src/server/request.rs index ce12bc6b2f..746dffe7f8 100644 --- a/crates/perry-ext-http/src/server/request.rs +++ b/crates/perry-ext-http/src/server/request.rs @@ -828,7 +828,7 @@ pub(crate) fn emit_data_to_listeners(listeners: &[i64], body: &[u8], encoding: O let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call1(chunk.get()); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, chunk.get()); } } } @@ -852,17 +852,23 @@ pub(crate) fn emit_no_arg_to_listeners(listeners: &[i64]) { let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } } } /// Fire a one-arg event (`server.on('connection', socket)`). -pub(crate) fn emit_one_arg_to_listeners(listeners: &[i64], arg: f64) { - // #8082: the snapshot AND the arg cross every callback — root both. +/// Call every listener with `this` bound to `this_val` (the emitting object: +/// Node invokes server, socket and request callbacks that way, so +/// `server.listen(0, function() { this.address().port })` resolves `this` to +/// the server, #2132) and one argument. +pub(crate) fn emit_one_arg_to_listeners(listeners: &[i64], this_val: f64, arg: f64) { + // #8082: the snapshot, the receiver AND the arg cross every callback — + // root all of them. let scope = perry_ffi::TransientRootScope::enter(); let rooted = scope.root_addrs(listeners); + let this_val = scope.root_nanbox(this_val); let arg = scope.root_nanbox(arg); for cb in &rooted { let addr = cb.get(); @@ -873,7 +879,7 @@ pub(crate) fn emit_one_arg_to_listeners(listeners: &[i64], arg: f64) { let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call1(arg.get()); + let _ = closure.call1(perry_ffi::JsThis::from_f64(this_val.get()), arg.get()); } } } @@ -957,26 +963,6 @@ pub(crate) fn handle_to_pointer_f64(handle: i64) -> f64 { f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)) } -extern "C" { - /// `perry-runtime`'s implicit-`this` cell setter (resolved at final - /// link; declared here because `perry-runtime` is only a - /// dev-dependency of this crate). Returns the previous value. - fn js_implicit_this_set(value: f64) -> f64; -} - -/// Run `f` with the runtime's implicit-`this` cell bound to `this_val`, -/// restoring the previous binding afterward. Node invokes server, -/// socket, and request callbacks with `this` bound to the emitting -/// object, so the canonical -/// `server.listen(0, function() { this.address().port })` idiom -/// resolves `this` to the server (#2132). -pub(crate) fn with_implicit_this(this_val: f64, f: impl FnOnce() -> R) -> R { - let prev = unsafe { js_implicit_this_set(this_val) }; - let r = f(); - unsafe { js_implicit_this_set(prev) }; - r -} - // ============================================================================ // #4904: standalone `new http.IncomingMessage(socket)` support // ============================================================================ diff --git a/crates/perry-ext-http/src/server/response.rs b/crates/perry-ext-http/src/server/response.rs index f70c76d249..36045c2017 100644 --- a/crates/perry-ext-http/src/server/response.rs +++ b/crates/perry-ext-http/src/server/response.rs @@ -1334,7 +1334,7 @@ pub unsafe extern "C" fn js_node_http_res_on( let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } handle_to_pointer_f64(handle) @@ -1376,7 +1376,7 @@ pub unsafe extern "C" fn js_node_http_res_once( let raw = callback as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } handle_to_pointer_f64(handle) diff --git a/crates/perry-ext-http/src/server/response_end.rs b/crates/perry-ext-http/src/server/response_end.rs index 5332c5c85c..ccc7bf3804 100644 --- a/crates/perry-ext-http/src/server/response_end.rs +++ b/crates/perry-ext-http/src/server/response_end.rs @@ -301,7 +301,7 @@ pub(crate) fn call_closure0(callback: i64) { unsafe { let closure = JsClosure::from_raw(callback as *const RawClosureHeader); if !closure.is_null() { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ext-http/src/server/server.rs b/crates/perry-ext-http/src/server/server.rs index 11880b1c21..7d52552384 100644 --- a/crates/perry-ext-http/src/server/server.rs +++ b/crates/perry-ext-http/src/server/server.rs @@ -14,7 +14,7 @@ use perry_ffi::{ }; use crate::server::ensure_gc_scanner_registered; -use crate::server::request::{handle_to_pointer_f64, with_implicit_this}; +use crate::server::request::handle_to_pointer_f64; use crate::server::response::{ResponseShape, ServerResponse}; use crate::server::types::{ extract_host, extract_port, js_handle_clear_side_tables, js_promise_run_microtasks, @@ -1156,9 +1156,7 @@ pub extern "C" fn js_node_http_server_process_pending() -> i32 { } let this_val = handle_to_pointer_f64(server_handle); let socket_val = handle_to_pointer_f64(socket_handle); - with_implicit_this(this_val, || { - crate::server::request::emit_one_arg_to_listeners(&listeners, socket_val) - }); + crate::server::request::emit_one_arg_to_listeners(&listeners, this_val, socket_val); count += 1; } @@ -1428,9 +1426,8 @@ fn process_pending(pending: HttpPendingRequest) { let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = + closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } @@ -1462,9 +1459,7 @@ fn process_pending(pending: HttpPendingRequest) { if !closure.is_null() { // `createServer(handler)` registers `handler` as a // `'request'` listener — same `this` = server binding. - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } @@ -1478,9 +1473,7 @@ fn process_pending(pending: HttpPendingRequest) { let raw = addr as *const RawClosureHeader; let closure = JsClosure::from_raw(raw); if !closure.is_null() { - with_implicit_this(server_this, || { - let _ = closure.call2(req_f64, res_f64); - }); + let _ = closure.call2(perry_ffi::JsThis::from_f64(server_this), req_f64, res_f64); } js_promise_run_microtasks(); } diff --git a/crates/perry-ext-http/src/server/server/deferred_events.rs b/crates/perry-ext-http/src/server/server/deferred_events.rs index e639d40231..2214f6cce4 100644 --- a/crates/perry-ext-http/src/server/server/deferred_events.rs +++ b/crates/perry-ext-http/src/server/server/deferred_events.rs @@ -2,9 +2,13 @@ use super::*; +/// The callbacks run with `this` bound to the emitting server (Node's +/// `EventEmitter` contract); `this_value` is the rooted receiver, re-read for +/// every callback. struct DeferredCallbacksCall { callbacks: *const perry_ffi::TransientRootedAddr, len: usize, + this_value: *const perry_ffi::TransientRootedNanbox, } unsafe extern "C" fn call_deferred_callbacks(data: *mut std::ffi::c_void) -> f64 { @@ -18,7 +22,7 @@ unsafe extern "C" fn call_deferred_callbacks(data: *mut std::ffi::c_void) -> f64 } let closure = JsClosure::from_raw(callback as *const RawClosureHeader); if !closure.is_null() { - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::from_f64((*call.this_value).get())); fired += 1; } } @@ -134,10 +138,12 @@ where // #8082: the drained snapshot crosses each callback — root it. let scope = perry_ffi::TransientRootScope::enter(); let rooted = scope.root_addrs(&cbs); + let this_rooted = scope.root_nanbox(this_val); perry_ext_ws::attached_server_listening(server_handle); let mut call = DeferredCallbacksCall { callbacks: rooted.as_ptr(), len: rooted.len(), + this_value: &this_rooted, }; unsafe { crate::js_async_hooks_provider_run_catching_with_this( @@ -178,9 +184,11 @@ where let this_value = handle_to_pointer_f64(server_handle); let scope = perry_ffi::TransientRootScope::enter(); let callbacks = scope.root_addrs(&callbacks); + let this_rooted = scope.root_nanbox(this_value); let mut call = DeferredCallbacksCall { callbacks: callbacks.as_ptr(), len: callbacks.len(), + this_value: &this_rooted, }; unsafe { crate::js_async_hooks_provider_run_catching_with_this( @@ -351,6 +359,8 @@ struct DeferredErrorCall { callbacks: *const perry_ffi::TransientRootedAddr, len: usize, error: *const ListenError, + /// The rooted receiver: the emitting server (see `DeferredCallbacksCall`). + this_value: *const perry_ffi::TransientRootedNanbox, } unsafe extern "C" fn call_deferred_error_callbacks(data: *mut std::ffi::c_void) -> f64 { @@ -369,7 +379,8 @@ unsafe extern "C" fn call_deferred_error_callbacks(data: *mut std::ffi::c_void) } let closure = JsClosure::from_raw(callback as *const RawClosureHeader); if !closure.is_null() { - let _ = closure.call1(error.get()); + let this = perry_ffi::JsThis::from_f64((*call.this_value).get()); + let _ = closure.call1(this, error.get()); fired += 1; } } @@ -451,10 +462,12 @@ where let this_val = handle_to_pointer_f64(server_handle); let scope = perry_ffi::TransientRootScope::enter(); let rooted = scope.root_addrs(&cbs); + let this_rooted = scope.root_nanbox(this_val); let mut call = DeferredErrorCall { callbacks: rooted.as_ptr(), len: rooted.len(), error: &error as *const ListenError, + this_value: &this_rooted, }; unsafe { crate::js_async_hooks_provider_run_catching_with_this( diff --git a/crates/perry-ext-http/src/server/server/upgrade_tests.rs b/crates/perry-ext-http/src/server/server/upgrade_tests.rs index 3db7633719..5275beeefd 100644 --- a/crates/perry-ext-http/src/server/server/upgrade_tests.rs +++ b/crates/perry-ext-http/src/server/server/upgrade_tests.rs @@ -2,7 +2,13 @@ use super::*; use perry_ffi::{alloc_closure, closure_capture_f64, set_closure_capture_f64, RawClosureHeader}; -extern "C" fn observe(closure: *const RawClosureHeader, req: f64, socket: f64, head: f64) -> f64 { +extern "C" fn observe( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, + req: f64, + socket: f64, + head: f64, +) -> f64 { unsafe { let count = closure_capture_f64(closure, 0); set_closure_capture_f64(closure as *mut _, 0, count + 1.0); @@ -45,8 +51,8 @@ fn queue(server_handle: i64, request_handle: i64, raw_socket_id: i64) { fn delivery(https: bool) { let scope = perry_ffi::TransientRootScope::enter(); - perry_ffi::register_closure_arity(observe as *const u8, 3); - let callback = scope.root_addr(alloc_closure(observe as *const u8, 3) as i64); + perry_ffi::register_closure_arity(observe as perry_ffi::JsBody3, 3); + let callback = scope.root_addr(alloc_closure(observe as perry_ffi::JsBody3, 3) as i64); unsafe { set_closure_capture_f64(callback.get() as *mut _, 0, 0.0); } diff --git a/crates/perry-ext-http/src/server/types.rs b/crates/perry-ext-http/src/server/types.rs index ab2c06a66c..4831094cad 100644 --- a/crates/perry-ext-http/src/server/types.rs +++ b/crates/perry-ext-http/src/server/types.rs @@ -414,7 +414,10 @@ mod tests { ); } - extern "C" fn listen_test_callback() -> f64 { + extern "C" fn listen_test_callback( + _closure: *const perry_ffi::RawClosureHeader, + _this: perry_ffi::JsThis, + ) -> f64 { f64::from_bits(TAG_UNDEFINED) } diff --git a/crates/perry-ext-http/src/server/upgrade.rs b/crates/perry-ext-http/src/server/upgrade.rs index 1ac813ceb6..00729546c2 100644 --- a/crates/perry-ext-http/src/server/upgrade.rs +++ b/crates/perry-ext-http/src/server/upgrade.rs @@ -65,7 +65,12 @@ pub(crate) fn fire_upgrade_listeners( let closure = JsClosure::from_raw(raw); if !closure.is_null() { delivered = true; - let _ = closure.call3(req_f64, ws_id_f64, head_arg.get()); + let _ = closure.call3( + perry_ffi::JsThis::UNDEFINED, + req_f64, + ws_id_f64, + head_arg.get(), + ); } js_promise_run_microtasks(); } diff --git a/crates/perry-ext-http/src/tls_client.rs b/crates/perry-ext-http/src/tls_client.rs index 1dabc8139d..48a9124363 100644 --- a/crates/perry-ext-http/src/tls_client.rs +++ b/crates/perry-ext-http/src/tls_client.rs @@ -927,7 +927,7 @@ pub(crate) unsafe fn check_server_identity_error( let cert = scope.root_nanbox(f64::from_bits(perry_ffi::alloc_object().bits())); let closure = perry_ffi::JsClosure::from_raw(callback.get() as *const perry_ffi::RawClosureHeader); - let result = closure.call2(host.get(), cert.get()); + let result = closure.call2(perry_ffi::JsThis::UNDEFINED, host.get(), cert.get()); let is_error = perry_ffi::JsValue::from_bits(js_error_is_error(result).to_bits()); if !is_error.is_bool() || !is_error.to_bool() { return None; diff --git a/crates/perry-ext-net/src/bun_tcp.rs b/crates/perry-ext-net/src/bun_tcp.rs index 20ad454900..3b6cbc8b85 100644 --- a/crates/perry-ext-net/src/bun_tcp.rs +++ b/crates/perry-ext-net/src/bun_tcp.rs @@ -205,8 +205,8 @@ fn dispatch_one(callback: i64, socket: i64) { } let frame = crate::dispatch_custody::DispatchFrame::park(vec![callback]); unsafe { - let _ = - JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader).call1(handle_value(socket)); + let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader) + .call1(perry_ffi::JsThis::UNDEFINED, handle_value(socket)); } } @@ -217,8 +217,11 @@ fn dispatch_two(callback: i64, socket: i64, payload: f64) { let mut frame = crate::dispatch_custody::DispatchFrame::park(vec![callback]); frame.set_payload(payload.to_bits()); unsafe { - let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader) - .call2(handle_value(socket), f64::from_bits(frame.payload_bits())); + let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader).call2( + perry_ffi::JsThis::UNDEFINED, + handle_value(socket), + f64::from_bits(frame.payload_bits()), + ); } } @@ -229,8 +232,11 @@ fn dispatch_error(callback: i64, socket: i64, message: &str) { let mut frame = crate::dispatch_custody::DispatchFrame::park(vec![callback]); frame.set_payload(unsafe { build_error_object(message) }.to_bits()); unsafe { - let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader) - .call2(handle_value(socket), f64::from_bits(frame.payload_bits())); + let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader).call2( + perry_ffi::JsThis::UNDEFINED, + handle_value(socket), + f64::from_bits(frame.payload_bits()), + ); } } @@ -483,8 +489,11 @@ fn dispatch_data(handle: i64, bytes: &Bytes) { let mut frame = frame; frame.set_payload(POINTER_TAG | (buffer as u64 & POINTER_MASK)); unsafe { - let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader) - .call2(handle_value(handle), f64::from_bits(frame.payload_bits())); + let _ = JsClosure::from_raw(frame.cb(0) as *const RawClosureHeader).call2( + perry_ffi::JsThis::UNDEFINED, + handle_value(handle), + f64::from_bits(frame.payload_bits()), + ); } } diff --git a/crates/perry-ext-net/src/dispatch.rs b/crates/perry-ext-net/src/dispatch.rs index f01a070a8e..75ba770c1d 100644 --- a/crates/perry-ext-net/src/dispatch.rs +++ b/crates/perry-ext-net/src/dispatch.rs @@ -494,7 +494,11 @@ unsafe fn server_method(handle: i64, method: &str, args: &[f64]) -> Option if let Some(callback) = args.first().copied().map(unbox_to_i64) { if callback >= 0x1000 { let cb = JsClosure::from_raw(callback as *const RawClosureHeader); - let _ = cb.call2(null(), crate::js_net_server_get_connections(handle)); + let _ = cb.call2( + perry_ffi::JsThis::UNDEFINED, + null(), + crate::js_net_server_get_connections(handle), + ); } } undefined() diff --git a/crates/perry-ext-net/src/lifecycle.rs b/crates/perry-ext-net/src/lifecycle.rs index 482d4e5565..82b2c05d7e 100644 --- a/crates/perry-ext-net/src/lifecycle.rs +++ b/crates/perry-ext-net/src/lifecycle.rs @@ -95,10 +95,13 @@ pub(crate) unsafe fn dispatch_socket_completion(completion: u64, error: Option f64; - fn js_implicit_this_set(value: f64) -> f64; - fn js_native_call_value(func_value: f64, args_ptr: *const f64, args_len: usize) -> f64; + fn js_native_call_value( + func_value: f64, + this: perry_ffi::JsThis, + args_ptr: *const f64, + args_len: usize, + ) -> f64; } fn is_nullish(v: f64) -> bool { @@ -80,32 +84,50 @@ fn is_callable(v: f64) -> bool { /// `Get(dest, "write")(chunk)` with `this` bound to `dest`. fn generic_write(dest: f64, chunk: f64) { + // The `Get` can run a getter (user code): root the receiver and the + // argument across it and re-read both. + let scope = perry_ffi::TransientRootScope::enter(); + let dest = scope.root_nanbox(dest); + let chunk = scope.root_nanbox(chunk); unsafe { - let write_fn = js_dynamic_object_get_property(dest, c"write".as_ptr(), 5); + let write_fn = js_dynamic_object_get_property(dest.get(), c"write".as_ptr(), 5); if !is_callable(write_fn) { return; } - let prev = js_implicit_this_set(dest); - let args = [chunk]; - let _ = js_native_call_value(write_fn, args.as_ptr(), args.len()); - js_implicit_this_set(prev); + let args = [chunk.get()]; + let _ = js_native_call_value( + write_fn, + perry_ffi::JsThis::from_f64(dest.get()), + args.as_ptr(), + args.len(), + ); } } /// `Get(dest, "end")()` with `this` bound to `dest`. fn generic_end(dest: f64) { + // The `Get` can run a getter (user code): root the receiver across it. + let scope = perry_ffi::TransientRootScope::enter(); + let dest = scope.root_nanbox(dest); unsafe { - let end_fn = js_dynamic_object_get_property(dest, c"end".as_ptr(), 3); + let end_fn = js_dynamic_object_get_property(dest.get(), c"end".as_ptr(), 3); if !is_callable(end_fn) { return; } - let prev = js_implicit_this_set(dest); - let _ = js_native_call_value(end_fn, std::ptr::null(), 0); - js_implicit_this_set(prev); + let _ = js_native_call_value( + end_fn, + perry_ffi::JsThis::from_f64(dest.get()), + std::ptr::null(), + 0, + ); } } -extern "C" fn pipe_data_forward(closure: *const RawClosureHeader, chunk: f64) -> f64 { +extern "C" fn pipe_data_forward( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, + chunk: f64, +) -> f64 { if !closure.is_null() { let dest = unsafe { closure_capture_f64(closure, 0) }; generic_write(dest, chunk); @@ -113,7 +135,7 @@ extern "C" fn pipe_data_forward(closure: *const RawClosureHeader, chunk: f64) -> f64::from_bits(TAG_UNDEFINED_BITS) } -extern "C" fn pipe_end_forward(closure: *const RawClosureHeader) -> f64 { +extern "C" fn pipe_end_forward(closure: *const RawClosureHeader, _this: perry_ffi::JsThis) -> f64 { if !closure.is_null() { let dest = unsafe { closure_capture_f64(closure, 0) }; let end_on_finish = unsafe { closure_capture_f64(closure, 1) }; @@ -128,8 +150,8 @@ static ARITY_REGISTERED: std::sync::Once = std::sync::Once::new(); fn ensure_pipe_closure_arities_registered() { ARITY_REGISTERED.call_once(|| { - register_closure_arity(pipe_data_forward as *const u8, 1); - register_closure_arity(pipe_end_forward as *const u8, 0); + register_closure_arity(pipe_data_forward as perry_ffi::JsBody1, 1); + register_closure_arity(pipe_end_forward as perry_ffi::JsBody0, 0); }); } @@ -213,8 +235,8 @@ pub(crate) fn socket_pipe(handle: i64, dest: f64, options: f64) -> f64 { } }; - let data_closure = alloc_closure(pipe_data_forward as *const u8, 1); - let end_closure = alloc_closure(pipe_end_forward as *const u8, 2); + let data_closure = alloc_closure(pipe_data_forward as perry_ffi::JsBody1, 1); + let end_closure = alloc_closure(pipe_end_forward as perry_ffi::JsBody0, 2); if data_closure.is_null() || end_closure.is_null() { return f64::from_bits(TAG_UNDEFINED_BITS); } diff --git a/crates/perry-ext-net/src/socket_emit.rs b/crates/perry-ext-net/src/socket_emit.rs index e5353e68dc..09a3a5ef8b 100644 --- a/crates/perry-ext-net/src/socket_emit.rs +++ b/crates/perry-ext-net/src/socket_emit.rs @@ -3,7 +3,12 @@ use super::*; extern "C" { - fn js_closure_call_array(closure: i64, args: *const f64, args_len: i64) -> f64; + fn js_closure_call_array( + closure: i64, + this: perry_ffi::JsThis, + args: *const f64, + args_len: i64, + ) -> f64; } type HttpAgentSocketEventHook = extern "C" fn(i64, *const u8, usize); @@ -56,23 +61,23 @@ pub unsafe extern "C" fn js_ext_net_socket_emit( let mut frame = dispatch_custody::DispatchFrame::park(listeners_for(handle, &event)); frame.set_payloads(&arg_bits); // #11227: `emit()` binds `this` to the socket, like every Node emitter. - let this = crate::socket_events::ListenerThis::bind(handle); + let this = crate::socket_events::ListenerThis::of(handle); for index in 0..frame.len() { let callback = frame.cb(index); if callback == 0 { continue; } - this.rebind(); let closure = JsClosure::from_raw(callback as *const RawClosureHeader); match arg_bits.len() { 0 => { - let _ = closure.call0(); + let _ = closure.call0(this.receiver()); } 1 => { - let _ = closure.call1(f64::from_bits(frame.payload_bits())); + let _ = closure.call1(this.receiver(), f64::from_bits(frame.payload_bits())); } 2 => { let _ = closure.call2( + this.receiver(), f64::from_bits(frame.payload_bits_at(0)), f64::from_bits(frame.payload_bits_at(1)), ); @@ -81,12 +86,16 @@ pub unsafe extern "C" fn js_ext_net_socket_emit( let args = (0..arg_bits.len()) .map(|index| f64::from_bits(frame.payload_bits_at(index))) .collect::>(); - let _ = js_closure_call_array(callback, args.as_ptr(), args.len() as i64); + let _ = js_closure_call_array( + callback, + this.receiver(), + args.as_ptr(), + args.len() as i64, + ); } } } let emitted = frame.len() != 0; - drop(this); drop(frame); lifecycle::drain_once_listeners(handle, &event); if statics::http_agent_phases() diff --git a/crates/perry-ext-net/src/socket_events.rs b/crates/perry-ext-net/src/socket_events.rs index cca9333670..aea69f21f5 100644 --- a/crates/perry-ext-net/src/socket_events.rs +++ b/crates/perry-ext-net/src/socket_events.rs @@ -7,67 +7,40 @@ fn socket_receiver(handle: i64) -> f64 { f64::from_bits(0x7FFD_0000_0000_0000 | (handle as u64 & 0x0000_FFFF_FFFF_FFFF)) } -extern "C" { - fn js_implicit_this_set(value: f64) -> f64; -} - /// Node calls every EventEmitter listener with `this` bound to the emitter /// (#11227). Only `'connect'`/`'drain'`/`'readable'` did; every other socket /// and server event ran its `function` listeners with `this === undefined`, /// so undici's `onHttpSocketClose` (`this[kParser]`) threw on `close()`. /// -/// Binds the socket/server handle as the implicit receiver for the life of -/// the guard and restores the caller's on drop — including when a listener -/// unwinds. The displaced receiver can be a heap object that a collection -/// inside the listeners moves, so it is held in a transient root and re-read -/// at the restore (the #9445 shape `js_implicit_this_set`'s docs describe). +/// The receiver every listener of one dispatch is called with: the +/// socket/server handle. A handle id is not a heap address, so it needs no +/// root across the listeners. +#[derive(Clone, Copy)] pub(crate) struct ListenerThis { - receiver: f64, - previous: perry_ffi::TransientRootedNanbox, - _scope: perry_ffi::TransientRootScope, + receiver: perry_ffi::JsThis, } impl ListenerThis { - pub(crate) fn bind(handle: i64) -> Self { - let scope = perry_ffi::TransientRootScope::enter(); - let receiver = socket_receiver(handle); - let previous = scope.root_nanbox(unsafe { js_implicit_this_set(receiver) }); + pub(crate) fn of(handle: i64) -> Self { Self { - receiver, - previous, - _scope: scope, + receiver: perry_ffi::JsThis::from_f64(socket_receiver(handle)), } } - /// Re-install before each listener: the previous one may have left a - /// different binding behind (a handle id is not a heap address, so the - /// receiver itself never moves). - pub(crate) fn rebind(&self) { - unsafe { - js_implicit_this_set(self.receiver); - } - } -} - -impl Drop for ListenerThis { - fn drop(&mut self) { - unsafe { - js_implicit_this_set(self.previous.get()); - } + pub(crate) fn receiver(self) -> perry_ffi::JsThis { + self.receiver } } unsafe fn emit_socket_no_arg(handle: i64, event: &str) { let frame = dispatch_custody::DispatchFrame::park(listeners_for(handle, event)); - let this = ListenerThis::bind(handle); + let this = ListenerThis::of(handle); for index in 0..frame.len() { let callback = frame.cb(index); if callback != 0 { - this.rebind(); - let _ = JsClosure::from_raw(callback as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(callback as *const RawClosureHeader).call0(this.receiver()); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(handle, event); } @@ -80,16 +53,14 @@ unsafe fn emit_tls_secure_connect(handle: i64) { if !JsValue::from_bits(identity_error.to_bits()).is_undefined() { let mut frame = dispatch_custody::DispatchFrame::park(listeners_for(handle, "error")); frame.set_payload(identity_error.to_bits()); - let this = ListenerThis::bind(handle); + let this = ListenerThis::of(handle); for index in 0..frame.len() { let callback = frame.cb(index); if callback != 0 { - this.rebind(); let _ = JsClosure::from_raw(callback as *const RawClosureHeader) - .call1(f64::from_bits(frame.payload_bits())); + .call1(this.receiver(), f64::from_bits(frame.payload_bits())); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(handle, "error"); if let Some(socket) = statics::sockets().lock().unwrap().get_mut(&handle) { @@ -222,16 +193,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { f64::from_bits(0x7FFD_0000_0000_0000 | (buf as u64 & 0x0000_FFFF_FFFF_FFFF)) }; frame.set_payload(payload_f64.to_bits()); - let this = ListenerThis::bind(id); + let this = ListenerThis::of(id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); let _ = JsClosure::from_raw(cb as *const RawClosureHeader) - .call1(f64::from_bits(frame.payload_bits())); + .call1(this.receiver(), f64::from_bits(frame.payload_bits())); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(id, "data"); } @@ -256,16 +225,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { // raw NaN-boxed string and `err.message` was `undefined`. let err_f64 = build_error_object(&msg); frame.set_payload(err_f64.to_bits()); - let this = ListenerThis::bind(id); + let this = ListenerThis::of(id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); let _ = JsClosure::from_raw(cb as *const RawClosureHeader) - .call1(f64::from_bits(frame.payload_bits())); + .call1(this.receiver(), f64::from_bits(frame.payload_bits())); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(id, "error"); } @@ -279,16 +246,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { fn js_abort_error_value() -> f64; } frame.set_payload(js_abort_error_value().to_bits()); - let this = ListenerThis::bind(id); + let this = ListenerThis::of(id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); let _ = JsClosure::from_raw(cb as *const RawClosureHeader) - .call1(f64::from_bits(frame.payload_bits())); + .call1(this.receiver(), f64::from_bits(frame.payload_bits())); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(id, "error"); } @@ -304,15 +269,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { // listener-map / socket-map teardown, so don't remove // anything here. let frame = dispatch_custody::DispatchFrame::park(listeners_for(id, "end")); - let this = ListenerThis::bind(id); + let this = ListenerThis::of(id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(cb as *const RawClosureHeader) + .call0(this.receiver()); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(id, "end"); // P1: a turnloop socket has no task to run the post-EOF @@ -350,15 +314,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { } let had_error = f64::from_bits(JsValue::from_bool(false).bits()); let frame = dispatch_custody::DispatchFrame::park(listeners_for(id, "close")); - let this = ListenerThis::bind(id); + let this = ListenerThis::of(id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call1(had_error); + let _ = JsClosure::from_raw(cb as *const RawClosureHeader) + .call1(this.receiver(), had_error); } } - drop(this); drop(frame); statics::listeners().lock().unwrap().remove(&id); statics::sockets().lock().unwrap().remove(&id); @@ -409,15 +372,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { // #8259: sock_f64 is a handle id (not a heap address), so // only the callbacks need custody. let frame = dispatch_custody::DispatchFrame::park(cbs); - let this = ListenerThis::bind(server_id); + let this = ListenerThis::of(server_id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call1(sock_f64); + let _ = JsClosure::from_raw(cb as *const RawClosureHeader) + .call1(this.receiver(), sock_f64); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(server_id, "connection"); server_state::release_pending_server_data(socket_id); @@ -443,15 +405,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { // so this frame is their ONLY root during dispatch — without // it a collection here can free, not just move, them. let frame = dispatch_custody::DispatchFrame::park(cbs); - let this = ListenerThis::bind(server_id); + let this = ListenerThis::of(server_id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(cb as *const RawClosureHeader) + .call0(this.receiver()); } } - drop(this); drop(frame); } PendingNetEvent::ServerClose(server_id) => { @@ -467,15 +428,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { // #8259: removed from the table above — custody is the only // root; see the ServerListening arm. let frame = dispatch_custody::DispatchFrame::park(cbs); - let this = ListenerThis::bind(server_id); + let this = ListenerThis::of(server_id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(cb as *const RawClosureHeader) + .call0(this.receiver()); } } - drop(this); drop(frame); // Tear down the server entry so the keepalive gate // (`js_ext_net_has_active_handles`) lets the runtime @@ -499,16 +459,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { let mut frame = dispatch_custody::DispatchFrame::park(cbs); let err_f64 = build_error_object(&msg); frame.set_payload(err_f64.to_bits()); - let this = ListenerThis::bind(server_id); + let this = ListenerThis::of(server_id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); let _ = JsClosure::from_raw(cb as *const RawClosureHeader) - .call1(f64::from_bits(frame.payload_bits())); + .call1(this.receiver(), f64::from_bits(frame.payload_bits())); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(server_id, "error"); } @@ -522,16 +480,14 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { let mut frame = dispatch_custody::DispatchFrame::park(cbs); let info = server_state::build_drop_object(&info); frame.set_payload(info.to_bits()); - let this = ListenerThis::bind(server_id); + let this = ListenerThis::of(server_id); for i in 0..frame.len() { let cb = frame.cb(i); if cb != 0 { - this.rebind(); let _ = JsClosure::from_raw(cb as *const RawClosureHeader) - .call1(f64::from_bits(frame.payload_bits())); + .call1(this.receiver(), f64::from_bits(frame.payload_bits())); } } - drop(this); drop(frame); lifecycle::drain_once_listeners(server_id, "drop"); } @@ -562,37 +518,59 @@ mod listener_this_tests { use super::*; const UNDEFINED: u64 = 0x7FFC_0000_0000_0001; - const NULL: u64 = 0x7FFC_0000_0000_0002; + /// Not a NaN-boxed value any call passes: "the listener never ran". + const NOT_CALLED: f64 = 0.5; + + /// Records the receiver it was called with in its own capture slot 0 (a + /// slot the collector sees), so no static holds a heap value. + extern "C" fn record_this(closure: *const RawClosureHeader, this: perry_ffi::JsThis) -> f64 { + unsafe { + perry_ffi::set_closure_capture_f64(closure as *mut RawClosureHeader, 0, this.as_f64()) + }; + f64::from_bits(UNDEFINED) + } + + fn recording_listener() -> i64 { + let body = record_this as perry_ffi::JsBody0; + perry_ffi::register_closure_arity(body, 0); + let closure = perry_ffi::alloc_closure(body, 1); + unsafe { perry_ffi::set_closure_capture_f64(closure, 0, NOT_CALLED) }; + closure as i64 + } + + fn seen_this(listener: i64) -> u64 { + unsafe { perry_ffi::closure_capture_f64(listener as *const RawClosureHeader, 0) }.to_bits() + } - /// #11227: every listener dispatch binds the emitter as `this`, rebinding - /// clobbers a leftover receiver, and the caller's receiver comes back. + /// #11227: every listener dispatch passes the emitter as `this`. The + /// listener reads `this` only from its receiver parameter, so this fails + /// if the dispatch makes a plain call (the listener would see + /// `undefined`). #[test] - fn binds_the_handle_and_restores_the_callers_receiver() { - let caller = f64::from_bits(UNDEFINED); - unsafe { js_implicit_this_set(caller) }; - { - let this = ListenerThis::bind(42); - let seen = unsafe { js_implicit_this_set(f64::from_bits(NULL)) }; - assert_eq!(seen.to_bits(), socket_receiver(42).to_bits()); - this.rebind(); - let seen = unsafe { js_implicit_this_set(socket_receiver(42)) }; - assert_eq!(seen.to_bits(), socket_receiver(42).to_bits()); - } - let restored = unsafe { js_implicit_this_set(caller) }; - assert_eq!(restored.to_bits(), UNDEFINED); + fn listeners_receive_the_emitting_handle_as_this() { + const HANDLE: i64 = 0x7A11_0042; + let listener = recording_listener(); + statics::listeners() + .lock() + .unwrap() + .entry(HANDLE) + .or_default() + .insert("probe".to_string(), vec![listener]); + unsafe { emit_socket_no_arg(HANDLE, "probe") }; + statics::listeners().lock().unwrap().remove(&HANDLE); + assert_eq!(seen_this(listener), socket_receiver(HANDLE).to_bits()); } + /// The negative control: the same listener called plainly sees + /// `undefined`, so the test above observes the receiver the dispatch + /// passed and nothing left over from an earlier call. #[test] - fn nested_dispatch_restores_the_outer_emitter() { - unsafe { js_implicit_this_set(f64::from_bits(UNDEFINED)) }; - let outer = ListenerThis::bind(7); - { - let _inner = ListenerThis::bind(9); - } - let seen = unsafe { js_implicit_this_set(socket_receiver(7)) }; - assert_eq!(seen.to_bits(), socket_receiver(7).to_bits()); - drop(outer); - let restored = unsafe { js_implicit_this_set(f64::from_bits(UNDEFINED)) }; - assert_eq!(restored.to_bits(), UNDEFINED); + fn a_plain_call_of_the_same_listener_sees_undefined() { + let listener = recording_listener(); + let _ = unsafe { + JsClosure::from_raw(listener as *const RawClosureHeader) + .call0(perry_ffi::JsThis::UNDEFINED) + }; + assert_eq!(seen_this(listener), UNDEFINED); } } diff --git a/crates/perry-ext-parcel-watcher/src/lib.rs b/crates/perry-ext-parcel-watcher/src/lib.rs index 181369b9fa..c4232fdaa5 100644 --- a/crates/perry-ext-parcel-watcher/src/lib.rs +++ b/crates/perry-ext-parcel-watcher/src/lib.rs @@ -553,7 +553,11 @@ fn fire_events(id: u64, events: Vec) -> bool { let callback = scope.root_addr(callback); let closure = unsafe { JsClosure::from_raw(callback.get() as *const RawClosureHeader) }; unsafe { - closure.call2(f64::from_bits(JsValue::NULL.bits()), events_value.get()); + closure.call2( + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(JsValue::NULL.bits()), + events_value.get(), + ); } true } @@ -575,7 +579,7 @@ fn fire_error(id: u64, message: &str) -> bool { let callback = scope.root_addr(callback); let closure = unsafe { JsClosure::from_raw(callback.get() as *const RawClosureHeader) }; unsafe { - closure.call2(error.get(), events.get()); + closure.call2(perry_ffi::JsThis::UNDEFINED, error.get(), events.get()); } true } diff --git a/crates/perry-ext-streams/src/lib.rs b/crates/perry-ext-streams/src/lib.rs index 3a4b95d143..2b55bd3abc 100644 --- a/crates/perry-ext-streams/src/lib.rs +++ b/crates/perry-ext-streams/src/lib.rs @@ -428,7 +428,7 @@ unsafe fn invoke_start(stream_id: usize) { }; if cb != 0 { let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call1(controller); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, controller); } } @@ -452,7 +452,7 @@ unsafe fn maybe_pull(stream_id: usize) { return; } let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call1(controller); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, controller); if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&stream_id) { s.pulling = false; } @@ -597,7 +597,7 @@ pub unsafe extern "C" fn js_readable_stream_cancel( }; if cb != 0 { let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call1(reason); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, reason); } close_pending(id); promise_resolve_bits(promise, TAG_UNDEFINED); @@ -949,7 +949,7 @@ pub unsafe extern "C" fn js_readable_stream_pipe_to( .unwrap_or(0); if write_cb != 0 { let closure = JsClosure::from_raw(write_cb as *const RawClosureHeader); - let _ = closure.call1(f64::from_bits(chunk)); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, f64::from_bits(chunk)); } } } @@ -970,7 +970,7 @@ pub unsafe extern "C" fn js_readable_stream_pipe_to( .unwrap_or(0); if close_cb != 0 { let closure = JsClosure::from_raw(close_cb as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } if let Some(w) = WRITABLE_STREAMS.lock().unwrap().get_mut(&w_id) { w.state = WritableState::Closed; @@ -1073,7 +1073,7 @@ pub unsafe extern "C" fn js_writable_stream_close(stream_handle: f64) -> *mut Pr }; if cb != 0 { let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call0(); + let _ = closure.call0(perry_ffi::JsThis::UNDEFINED); } if !closed_p.is_null() { promise_resolve_bits(closed_p, TAG_UNDEFINED); @@ -1100,7 +1100,7 @@ pub unsafe extern "C" fn js_writable_stream_abort(stream_handle: f64, reason: f6 }; if cb != 0 { let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call1(reason); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, reason); } if !closed_p.is_null() { promise_reject_bits(closed_p, reason_bits); @@ -1145,7 +1145,7 @@ pub unsafe extern "C" fn js_writer_write(writer_handle: f64, chunk: f64) -> *mut }; if cb != 0 { let closure = JsClosure::from_raw(cb as *const RawClosureHeader); - let _ = closure.call1(chunk); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, chunk); } promise_resolve_bits(promise, TAG_UNDEFINED); promise @@ -1349,7 +1349,7 @@ unsafe fn transform_write(writable_id: usize, chunk: f64) -> *mut Promise { }; if transform_cb != 0 && readable_id != 0 { let closure = JsClosure::from_raw(transform_cb as *const RawClosureHeader); - let _ = closure.call2(chunk, readable_id as f64); + let _ = closure.call2(perry_ffi::JsThis::UNDEFINED, chunk, readable_id as f64); } else { // Identity transform — pass-through. js_readable_stream_controller_enqueue(readable_id as f64, chunk); @@ -1375,7 +1375,7 @@ unsafe fn transform_close(writable_id: usize) -> *mut Promise { }; if flush_cb != 0 && readable_id != 0 { let closure = JsClosure::from_raw(flush_cb as *const RawClosureHeader); - let _ = closure.call1(readable_id as f64); + let _ = closure.call1(perry_ffi::JsThis::UNDEFINED, readable_id as f64); } if readable_id != 0 { js_readable_stream_controller_close(readable_id as f64); diff --git a/crates/perry-ext-typescript/src/bun.rs b/crates/perry-ext-typescript/src/bun.rs index 9a47cd891e..6017de4459 100644 --- a/crates/perry-ext-typescript/src/bun.rs +++ b/crates/perry-ext-typescript/src/bun.rs @@ -694,9 +694,9 @@ fn ensure_build_runtime_registered() { ); }); NATIVE_CLOSURE_ARITIES_REGISTERED.call_once(|| { - register_closure_arity(bun_on_resolve as *const u8, 2); - register_closure_arity(bun_on_load as *const u8, 2); - register_closure_arity(bun_build_output_text as *const u8, 0); + register_closure_arity(bun_on_resolve as perry_ffi::JsBody2, 2); + register_closure_arity(bun_on_load as perry_ffi::JsBody2, 2); + register_closure_arity(bun_build_output_text as perry_ffi::JsBody0, 0); }); } @@ -779,15 +779,25 @@ fn register_plugin_hook( f64::from_bits(JsValue::UNDEFINED.bits()) } -extern "C" fn bun_on_resolve(closure: *const RawClosureHeader, options: f64, callback: f64) -> f64 { +extern "C" fn bun_on_resolve( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, + options: f64, + callback: f64, +) -> f64 { register_plugin_hook(closure, options, callback, true) } -extern "C" fn bun_on_load(closure: *const RawClosureHeader, options: f64, callback: f64) -> f64 { +extern "C" fn bun_on_load( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, + options: f64, + callback: f64, +) -> f64 { register_plugin_hook(closure, options, callback, false) } -fn native_closure_value(function: *const u8, session: Handle) -> JsValue { +fn native_closure_value(function: perry_ffi::JsBody2, session: Handle) -> JsValue { let closure = alloc_closure(function, 1); if closure.is_null() { return JsValue::UNDEFINED; @@ -822,10 +832,10 @@ fn configure_plugins(options: f64) -> Handle { return session; } let on_resolve = scope.root_nanbox(f64::from_bits( - native_closure_value(bun_on_resolve as *const u8, session).bits(), + native_closure_value(bun_on_resolve as perry_ffi::JsBody2, session).bits(), )); let on_load = scope.root_nanbox(f64::from_bits( - native_closure_value(bun_on_load as *const u8, session).bits(), + native_closure_value(bun_on_load as perry_ffi::JsBody2, session).bits(), )); let builder = scope.root_nanbox(f64::from_bits( alloc_null_proto_object(&[ @@ -836,7 +846,7 @@ fn configure_plugins(options: f64) -> Handle { )); for callback in callbacks { let closure = unsafe { JsClosure::from_raw(callback.get() as *const RawClosureHeader) }; - unsafe { closure.call1(builder.get()) }; + unsafe { closure.call1(perry_ffi::JsThis::UNDEFINED, builder.get()) }; } session } @@ -884,7 +894,7 @@ fn invoke_hook(hook: &PluginHook, args: f64) -> JsValue { let callback = scope.root_addr(hook.callback); let args = scope.root_nanbox(args); let closure = unsafe { JsClosure::from_raw(callback.get() as *const RawClosureHeader) }; - JsValue::from_bits(unsafe { closure.call1(args.get()) }.to_bits()) + JsValue::from_bits(unsafe { closure.call1(perry_ffi::JsThis::UNDEFINED, args.get()) }.to_bits()) } fn file_parts(file: &FileName) -> (String, String) { @@ -1284,7 +1294,10 @@ fn array_from_values(values: impl IntoIterator) -> JsValue { JsValue::from_object_ptr(array.get() as *mut ArrayHeader) } -extern "C" fn bun_build_output_text(closure: *const RawClosureHeader) -> f64 { +extern "C" fn bun_build_output_text( + closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, +) -> f64 { let scope = TransientRootScope::enter(); let contents = scope.root_nanbox(unsafe { closure_capture_f64(closure, 0) }); let promise = JsPromise::new(); @@ -1299,7 +1312,7 @@ fn build_output_value(output: BuildOutput) -> JsValue { let contents = scope.root_nanbox(f64::from_bits( JsValue::from_string_ptr(alloc_string(&output.contents).as_raw()).bits(), )); - let text = alloc_closure(bun_build_output_text as *const u8, 1); + let text = perry_ffi::alloc_closure(bun_build_output_text as perry_ffi::JsBody0, 1); unsafe { set_closure_capture_f64(text, 0, contents.get()) }; let text = scope.root_nanbox(f64::from_bits(JsValue::from_object_ptr(text).bits())); let path = JsValue::from_string_ptr(alloc_string(&output.path).as_raw()); diff --git a/crates/perry-ext-ws/src/lib.rs b/crates/perry-ext-ws/src/lib.rs index 55ee0ece1b..6a913116bf 100644 --- a/crates/perry-ext-ws/src/lib.rs +++ b/crates/perry-ext-ws/src/lib.rs @@ -1325,7 +1325,11 @@ pub unsafe extern "C" fn js_ws_handle_upgrade( // duplicates the usual `wss.emit("connection", ws, req)` idiom. if cb.get() != 0 { let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); - let _ = closure.call2(f64::from_bits(client_js_value(ws_id).bits()), req.get()); + let _ = closure.call2( + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(client_js_value(ws_id).bits()), + req.get(), + ); } } @@ -1378,7 +1382,10 @@ pub extern "C" fn js_ws_process_pending() -> i32 { // Use the same handle value as the clients Set and // manual-upgrade callback, including dynamic dispatch. let _ = unsafe { - closure.call1(f64::from_bits(client_js_value(client_id).bits())) + closure.call1( + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(client_js_value(client_id).bits()), + ) }; fired += 1; } @@ -1400,7 +1407,9 @@ pub extern "C" fn js_ws_process_pending() -> i32 { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call2(msg_f64, binary_f64) }; + let _ = unsafe { + closure.call2(perry_ffi::JsThis::UNDEFINED, msg_f64, binary_f64) + }; fired += 1; } } @@ -1415,6 +1424,7 @@ pub extern "C" fn js_ws_process_pending() -> i32 { unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; let _ = unsafe { closure.call2( + perry_ffi::JsThis::UNDEFINED, f64::from_bits(client_js_value(ws_id).bits()), msg_f64, ) @@ -1433,7 +1443,7 @@ pub extern "C" fn js_ws_process_pending() -> i32 { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call1(value) }; + let _ = unsafe { closure.call1(perry_ffi::JsThis::UNDEFINED, value) }; fired += 1; } } @@ -1447,7 +1457,7 @@ pub extern "C" fn js_ws_process_pending() -> i32 { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call1(value) }; + let _ = unsafe { closure.call1(perry_ffi::JsThis::UNDEFINED, value) }; fired += 1; } } @@ -1472,7 +1482,9 @@ pub extern "C" fn js_ws_process_pending() -> i32 { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call2(code_f64, reason_f64) }; + let _ = unsafe { + closure.call2(perry_ffi::JsThis::UNDEFINED, code_f64, reason_f64) + }; fired += 1; } } @@ -1485,7 +1497,10 @@ pub extern "C" fn js_ws_process_pending() -> i32 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; let _ = unsafe { - closure.call1(f64::from_bits(client_js_value(ws_id).bits())) + closure.call1( + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(client_js_value(ws_id).bits()), + ) }; fired += 1; } @@ -1502,8 +1517,10 @@ pub extern "C" fn js_ws_process_pending() -> i32 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; let s = alloc_string(&err); let _ = unsafe { - closure - .call1(f64::from_bits(JsValue::from_string_ptr(s.as_raw()).bits())) + closure.call1( + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(JsValue::from_string_ptr(s.as_raw()).bits()), + ) }; fired += 1; } @@ -1516,8 +1533,10 @@ pub extern "C" fn js_ws_process_pending() -> i32 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; let s = alloc_string(&err); let _ = unsafe { - closure - .call1(f64::from_bits(JsValue::from_string_ptr(s.as_raw()).bits())) + closure.call1( + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(JsValue::from_string_ptr(s.as_raw()).bits()), + ) }; fired += 1; } @@ -1528,7 +1547,7 @@ pub extern "C" fn js_ws_process_pending() -> i32 { for cb in listeners { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call0() }; + let _ = unsafe { closure.call0(perry_ffi::JsThis::UNDEFINED) }; fired += 1; } } @@ -1537,7 +1556,7 @@ pub extern "C" fn js_ws_process_pending() -> i32 { for cb in listeners_on_server(server_handle, "close") { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call0() }; + let _ = unsafe { closure.call0(perry_ffi::JsThis::UNDEFINED) }; fired += 1; } } @@ -1549,7 +1568,7 @@ pub extern "C" fn js_ws_process_pending() -> i32 { for cb in listeners { if cb != 0 { let closure = unsafe { JsClosure::from_raw(cb as *const RawClosureHeader) }; - let _ = unsafe { closure.call0() }; + let _ = unsafe { closure.call0(perry_ffi::JsThis::UNDEFINED) }; fired += 1; } } diff --git a/crates/perry-ext-ws/src/server.rs b/crates/perry-ext-ws/src/server.rs index 98cc1a5841..1e2a54674c 100644 --- a/crates/perry-ext-ws/src/server.rs +++ b/crates/perry-ext-ws/src/server.rs @@ -335,13 +335,13 @@ fn emit_server_event(handle: Handle, event: &str, first: f64, second: f64, argc: let closure = JsClosure::from_raw(cb.get() as *const RawClosureHeader); match argc { 0 => { - closure.call0(); + closure.call0(perry_ffi::JsThis::UNDEFINED); } 1 => { - closure.call1(first.get()); + closure.call1(perry_ffi::JsThis::UNDEFINED, first.get()); } _ => { - closure.call2(first.get(), second.get()); + closure.call2(perry_ffi::JsThis::UNDEFINED, first.get(), second.get()); } } } diff --git a/crates/perry-ext-ws/src/tests.rs b/crates/perry-ext-ws/src/tests.rs index fd93e45173..36ed5750d3 100644 --- a/crates/perry-ext-ws/src/tests.rs +++ b/crates/perry-ext-ws/src/tests.rs @@ -198,19 +198,28 @@ fn server_clients_is_a_stable_set_that_tracks_connections() { static FIRST_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); static SECOND_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); -extern "C" fn count_second(_c: *const perry_runtime::closure::ClosureHeader) -> f64 { +extern "C" fn count_second( + _c: *const perry_runtime::closure::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { SECOND_CALLS.fetch_add(1, Ordering::SeqCst); f64::from_bits(JsValue::UNDEFINED.bits()) } -extern "C" fn count_first(_c: *const perry_runtime::closure::ClosureHeader) -> f64 { +extern "C" fn count_first( + _c: *const perry_runtime::closure::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { FIRST_CALLS.fetch_add(1, Ordering::SeqCst); f64::from_bits(JsValue::UNDEFINED.bits()) } /// Counts its calls and registers a `count_second` listener, from inside /// this callback, on the server handle in its capture slot 0. -extern "C" fn count_first_and_register(c: *const perry_runtime::closure::ClosureHeader) -> f64 { +extern "C" fn count_first_and_register( + c: *const perry_runtime::closure::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { FIRST_CALLS.fetch_add(1, Ordering::SeqCst); let server = perry_runtime::closure::js_closure_get_capture_f64(c, 0) as Handle; on_listening(server, closure_of(count_second)); @@ -230,7 +239,12 @@ fn on_listening(server: Handle, listener: i64) { unsafe { js_ws_on(server, event.as_raw(), listener.get()) }; } -fn closure_of(f: extern "C" fn(*const perry_runtime::closure::ClosureHeader) -> f64) -> i64 { +fn closure_of( + f: extern "C" fn( + *const perry_runtime::closure::ClosureHeader, + perry_runtime::closure::JsThis, + ) -> f64, +) -> i64 { perry_runtime::closure::js_closure_alloc(f as *const u8, 0) as i64 } diff --git a/crates/perry-ext-zlib/src/lib.rs b/crates/perry-ext-zlib/src/lib.rs index 1f7308b970..dc3a901b37 100644 --- a/crates/perry-ext-zlib/src/lib.rs +++ b/crates/perry-ext-zlib/src/lib.rs @@ -492,6 +492,7 @@ mod tests { extern "C" fn record_dispatch_callback( _closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, err: f64, value: f64, ) -> f64 { @@ -569,8 +570,8 @@ mod tests { DISPATCH_CALLBACK_FIRED.with(|fired| fired.set(false)); DISPATCH_CALLBACK_OK.with(|ok| ok.set(false)); - register_closure_arity(record_dispatch_callback as *const u8, 2); - let callback = alloc_closure(record_dispatch_callback as *const u8, 0); + register_closure_arity(record_dispatch_callback as perry_ffi::JsBody2, 2); + let callback = alloc_closure(record_dispatch_callback as perry_ffi::JsBody2, 0); assert!(!callback.is_null()); let input = alloc_buffer(b"captured zlib export"); @@ -593,7 +594,12 @@ mod tests { #[test] fn external_dispatch_accepts_options_and_honors_level_zero() { - extern "C" fn callback(_closure: *const RawClosureHeader, err: f64, output: f64) -> f64 { + extern "C" fn callback( + _closure: *const RawClosureHeader, + _this: perry_ffi::JsThis, + err: f64, + output: f64, + ) -> f64 { assert_eq!(err.to_bits(), JsValue::NULL.bits()); let bytes = read_buffer_bytes( JsValue::from_bits(output.to_bits()).as_pointer::(), @@ -608,9 +614,9 @@ mod tests { } DISPATCH_CALLBACK_FIRED.with(|fired| fired.set(false)); let scope = perry_ffi::TransientRootScope::enter(); - register_closure_arity(callback as *const u8, 2); + register_closure_arity(callback as perry_ffi::JsBody2, 2); let callback = scope.root_nanbox(f64::from_bits( - JsValue::from_object_ptr(alloc_closure(callback as *const u8, 0)).bits(), + JsValue::from_object_ptr(alloc_closure(callback as perry_ffi::JsBody2, 0)).bits(), )); let data = scope.root_nanbox(f64::from_bits( JsValue::from_object_ptr(alloc_buffer(&vec![b'A'; 4096])).bits(), diff --git a/crates/perry-ext-zlib/src/stream.rs b/crates/perry-ext-zlib/src/stream.rs index 91f7236bc3..21e14d8fb3 100644 --- a/crates/perry-ext-zlib/src/stream.rs +++ b/crates/perry-ext-zlib/src/stream.rs @@ -733,13 +733,19 @@ unsafe fn call_one_shot_callback(callback: i64, result: Result, String>) make_buffer_f64(&bytes) .unwrap_or_else(|| f64::from_bits(JsValue::UNDEFINED.bits())), ); - let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) - .call2(err, out.get()); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call2( + perry_ffi::JsThis::UNDEFINED, + err, + out.get(), + ); } Err(msg) => { let err = roots.root_nanbox(build_error_object(&msg)); - let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) - .call2(err.get(), f64::from_bits(JsValue::UNDEFINED.bits())); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call2( + perry_ffi::JsThis::UNDEFINED, + err.get(), + f64::from_bits(JsValue::UNDEFINED.bits()), + ); } } } @@ -1357,7 +1363,7 @@ unsafe extern "C" fn zlib_event_dispatch_thunk(data: *mut c_void) -> f64 { if callback.get() != 0 { let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) - .call1(buffer.get()); + .call1(perry_ffi::JsThis::UNDEFINED, buffer.get()); } } } @@ -1371,7 +1377,8 @@ unsafe extern "C" fn zlib_event_dispatch_thunk(data: *mut c_void) -> f64 { let roots = TransientRootScope::enter(); for callback in roots.root_addrs(&listeners_for(id, "finish")) { if callback.get() != 0 { - let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call0(perry_ffi::JsThis::UNDEFINED); } } } @@ -1387,7 +1394,8 @@ unsafe extern "C" fn zlib_event_dispatch_thunk(data: *mut c_void) -> f64 { drop_buffered_stream(&mut statics().lock().unwrap(), id); for callback in end_callbacks { if callback.get() != 0 { - let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call0(perry_ffi::JsThis::UNDEFINED); } } for destination in destinations { @@ -1395,7 +1403,8 @@ unsafe extern "C" fn zlib_event_dispatch_thunk(data: *mut c_void) -> f64 { } for callback in close_callbacks { if callback.get() != 0 { - let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call0(perry_ffi::JsThis::UNDEFINED); } } } @@ -1407,13 +1416,14 @@ unsafe extern "C" fn zlib_event_dispatch_thunk(data: *mut c_void) -> f64 { for callback in callbacks { if callback.get() != 0 { let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) - .call1(error.get()); + .call1(perry_ffi::JsThis::UNDEFINED, error.get()); } } } ZlibEvent::Callback(callback) => { if callback != 0 { - let _ = JsClosure::from_raw(callback as *const RawClosureHeader).call0(); + let _ = JsClosure::from_raw(callback as *const RawClosureHeader) + .call0(perry_ffi::JsThis::UNDEFINED); } } ZlibEvent::OneShotCallback(_, _, _) => { diff --git a/crates/perry-ffi/Cargo.toml b/crates/perry-ffi/Cargo.toml index 5092fd3d7b..30c8ad65e3 100644 --- a/crates/perry-ffi/Cargo.toml +++ b/crates/perry-ffi/Cargo.toml @@ -1,6 +1,9 @@ [package] name = "perry-ffi" -version.workspace = true +# Its own version, not the workspace train: 0.6 is the JS body ABI with the +# receiver as a parameter (typed `JsBody` registration, receiver-taking calls), +# incompatible with every 0.5 addon. +version = "0.6.0" edition.workspace = true license.workspace = true description = "Stable FFI surface for native bindings packages that wrap Rust crates for Perry. Wrapper authors depend on this; perry-runtime internals stay free to evolve." @@ -17,6 +20,8 @@ workspace = true [dependencies] perry-native-registration.workspace = true +# The JS body ABI (`JsThis`, `js_body_fn_ty!`, `JsBody*`), shared with the runtime. +perry-abi.workspace = true # #1112: `perry-runtime` is restored as an OPTIONAL regular dep (not # a dev-dep) so the `runtime-link` feature actually propagates the diff --git a/crates/perry-ffi/src/closure.rs b/crates/perry-ffi/src/closure.rs index 06e4b450f8..dd5a6ac40c 100644 --- a/crates/perry-ffi/src/closure.rs +++ b/crates/perry-ffi/src/closure.rs @@ -1,6 +1,4 @@ -//! JavaScript closure invocation across the FFI boundary (added -//! in v0.5.x of the perry-ffi v0.5 surface — non-breaking; pure -//! additions). +//! JavaScript closure invocation across the FFI boundary. //! //! Many wrappers need to call back into TypeScript-side //! functions: @@ -18,9 +16,9 @@ //! //! All of these consume a `*const ClosureHeader` (the runtime's //! closure layout) and call it via `js_closure_call0` / -//! `js_closure_call1` / etc. — perry-runtime exports those as -//! `extern "C"` already, so perry-ffi just declares them and -//! exposes a typed [`JsClosure`] wrapper. +//! `js_closure_call1` / etc., passing the receiver (`this`) after the +//! closure — perry-runtime exports those as `extern "C"`, so perry-ffi +//! declares them and exposes a typed [`JsClosure`] wrapper. //! //! # Argument / return ABI //! @@ -41,32 +39,145 @@ use crate::ClosureHeader; pub use crate::ClosureHeader as RawClosureHeader; +/// The receiver every native closure body takes as its SECOND parameter, +/// and every call into a JS function passes: the NaN-boxed `this` bits, in +/// an integer register ([`JsThis::UNDEFINED`] for a plain call). Defined +/// once in perry-abi and shared with the runtime. +pub use perry_abi::JsThis; + +/// The JS body types (`perry_abi::js_body_fn_ty!`) over perry-ffi's closure +/// header: `JsBody1` is +/// `unsafe extern "C" fn(*const RawClosureHeader, JsThis, f64) -> f64`. +/// [`alloc_closure`] and [`register_closure_arity`] take one of these, so a +/// body with any other signature — including a bare `*const u8` — does not +/// compile: +/// +/// ```ignore +/// extern "C" fn body(c: *const RawClosureHeader, this: JsThis, a0: f64) -> f64 { .. } +/// let closure = perry_ffi::alloc_closure(body as perry_ffi::JsBody1, 0); +/// ``` +/// +/// A JS body declaring 0 JS parameters. +pub type JsBody0 = perry_abi::JsBody0; +/// A JS body declaring 1 JS parameters. +pub type JsBody1 = perry_abi::JsBody1; +/// A JS body declaring 2 JS parameters. +pub type JsBody2 = perry_abi::JsBody2; +/// A JS body declaring 3 JS parameters. +pub type JsBody3 = perry_abi::JsBody3; +/// A JS body declaring 4 JS parameters. +pub type JsBody4 = perry_abi::JsBody4; +/// A JS body declaring 5 JS parameters. +pub type JsBody5 = perry_abi::JsBody5; +/// A JS body declaring 6 JS parameters. +pub type JsBody6 = perry_abi::JsBody6; +/// A JS body declaring 7 JS parameters. +pub type JsBody7 = perry_abi::JsBody7; +/// A JS body declaring 8 JS parameters. +pub type JsBody8 = perry_abi::JsBody8; +/// A JS body declaring 9 JS parameters. +pub type JsBody9 = perry_abi::JsBody9; +/// A JS body declaring 10 JS parameters. +pub type JsBody10 = perry_abi::JsBody10; +/// A JS body declaring 11 JS parameters. +pub type JsBody11 = perry_abi::JsBody11; +/// A JS body declaring 12 JS parameters. +pub type JsBody12 = perry_abi::JsBody12; +/// A JS body declaring 13 JS parameters. +pub type JsBody13 = perry_abi::JsBody13; +/// A JS body declaring 14 JS parameters. +pub type JsBody14 = perry_abi::JsBody14; +/// A JS body declaring 15 JS parameters. +pub type JsBody15 = perry_abi::JsBody15; +/// A JS body declaring 16 JS parameters. +pub type JsBody16 = perry_abi::JsBody16; +/// A JS body of any arity over perry-ffi's closure header. +pub use perry_abi::JsBody; + extern "C" { - fn js_closure_call0(closure: *const ClosureHeader) -> f64; - fn js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64; - fn js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64; - fn js_closure_call3(closure: *const ClosureHeader, arg0: f64, arg1: f64, arg2: f64) -> f64; + fn js_closure_call0(closure: *const ClosureHeader, this: JsThis) -> f64; + fn js_closure_call1(closure: *const ClosureHeader, this: JsThis, arg0: f64) -> f64; + fn js_closure_call2(closure: *const ClosureHeader, this: JsThis, arg0: f64, arg1: f64) -> f64; + fn js_closure_call3( + closure: *const ClosureHeader, + this: JsThis, + arg0: f64, + arg1: f64, + arg2: f64, + ) -> f64; fn js_closure_call4( closure: *const ClosureHeader, + this: JsThis, arg0: f64, arg1: f64, arg2: f64, arg3: f64, ) -> f64; + fn js_closure_call_array( + closure: *const ClosureHeader, + this: JsThis, + args: *const f64, + args_len: i64, + ) -> f64; + fn js_native_call_value( + func_value: f64, + this: JsThis, + args: *const f64, + args_len: usize, + ) -> f64; fn js_closure_alloc(func_ptr: *const u8, capture_count: u32) -> *mut ClosureHeader; fn js_register_closure_arity(func_ptr: *const u8, arity: u32); + fn js_register_closure_rest(func_ptr: *const u8, fixed_arity: u32); fn js_closure_get_capture_f64(closure: *const ClosureHeader, index: u32) -> f64; fn js_closure_set_capture_f64(closure: *mut ClosureHeader, index: u32, value: f64); } -/// Register the arity the runtime uses when dispatching a native closure. -pub fn register_closure_arity(func: *const u8, arity: u32) { - unsafe { js_register_closure_arity(func, arity) } +/// Register the arity the runtime uses when dispatching a native closure +/// body (a rest-bundling body registers its fixed arity). +pub fn register_closure_arity>(func: F, arity: u32) { + unsafe { js_register_closure_arity(func.code(), arity) } +} + +/// Register `func` as a rest-parameter body: the runtime bundles every JS +/// argument from index `fixed_arity` on into one array, passed as the body's +/// last parameter (so a body with `fixed_arity` fixed parameters is a +/// `JsBody{fixed_arity + 1}`). +pub fn register_closure_rest>(func: F, fixed_arity: u32) { + debug_assert_eq!( + F::ARITY, + fixed_arity + 1, + "a rest body takes its fixed parameters and the rest array" + ); + unsafe { js_register_closure_rest(func.code(), fixed_arity) } } -/// Allocate a native closure with `capture_count` f64 capture slots. -pub fn alloc_closure(func: *const u8, capture_count: u32) -> *mut ClosureHeader { - unsafe { js_closure_alloc(func, capture_count) } +/// Allocate a native closure running `func` with `capture_count` f64 +/// capture slots. +/// +/// `func` must be a JS body pointer type; anything else does not compile. +/// A body without the receiver: +/// +/// ```compile_fail,E0605 +/// // NOT-A-JS-BODY: the example of a body missing its receiver. +/// extern "C" fn no_this(_: *const perry_ffi::RawClosureHeader, a: f64) -> f64 { a } +/// let _ = perry_ffi::alloc_closure(no_this as perry_ffi::JsBody1, 0); +/// ``` +/// +/// an erased pointer: +/// +/// ```compile_fail,E0277 +/// extern "C" fn body(_: *const perry_ffi::RawClosureHeader, _: perry_ffi::JsThis) -> f64 { 0.0 } +/// let _ = perry_ffi::alloc_closure(body as *const u8, 0); +/// ``` +/// +/// a JS argument of the wrong type: +/// +/// ```compile_fail,E0605 +/// extern "C" fn body(_: *const perry_ffi::RawClosureHeader, _: perry_ffi::JsThis, a: i64) -> f64 { 0.0 } +/// let _ = perry_ffi::alloc_closure(body as perry_ffi::JsBody1, 0); +/// ``` +pub fn alloc_closure>(func: F, capture_count: u32) -> *mut ClosureHeader { + unsafe { js_closure_alloc(func.code(), capture_count) } } /// Read an f64 capture slot from a native closure. @@ -85,14 +196,22 @@ pub unsafe fn set_closure_capture_f64(closure: *mut ClosureHeader, index: u32, v js_closure_set_capture_f64(closure, index, value) } +/// Call any JS value as a function with receiver `this` +/// ([`JsThis::UNDEFINED`] for a plain call): what `func.call(this, ...args)` +/// does. A value that is not callable throws a JS `TypeError`. +/// +/// # Safety +/// As [`JsClosure::call0`]; `args` must stay valid for the call. +pub unsafe fn call_value(func: f64, this: JsThis, args: &[f64]) -> f64 { + js_native_call_value(func, this, args.as_ptr(), args.len()) +} + /// Opaque handle to a JS closure (a `*const ClosureHeader`). /// /// Wrapper authors receive a `*const ClosureHeader` from their /// FFI parameter list, convert it via [`JsClosure::from_raw`], -/// then call zero through four args via the `call_*` methods. -/// Beyond 4 args, drop down to the raw extern fns — -/// perry-runtime exports `js_closure_call5..8` for completeness; -/// they're not yet wrapped here because no wrapper needs them. +/// then call it through the `call*` methods, each taking the receiver +/// first ([`JsThis::UNDEFINED`] for a plain call). #[repr(transparent)] #[derive(Copy, Clone)] pub struct JsClosure(*const ClosureHeader); @@ -126,9 +245,10 @@ impl JsClosure { self.0 } - /// Invoke the closure with no arguments. Returns the result - /// as a NaN-boxed f64 (the runtime's standard return ABI for - /// dynamic JS calls). + /// Invoke the closure with no arguments and receiver `this` + /// ([`JsThis::UNDEFINED`] for a plain call; a sloppy body sees + /// globalThis). Returns the result as a NaN-boxed f64 (the runtime's + /// standard return ABI for dynamic JS calls). /// /// # Safety /// @@ -137,28 +257,33 @@ impl JsClosure { /// the runtime / arena, so callers must not hold any /// references that would alias with allocations the closure /// may make. - pub unsafe fn call0(self) -> f64 { - js_closure_call0(self.0) + pub unsafe fn call0(self, this: JsThis) -> f64 { + js_closure_call0(self.0, this) + } + + /// Invoke with one argument. See [`Self::call0`]. + pub unsafe fn call1(self, this: JsThis, arg0: f64) -> f64 { + js_closure_call1(self.0, this, arg0) } - /// Invoke with one argument. See [`Self::call0`] safety. - pub unsafe fn call1(self, arg0: f64) -> f64 { - js_closure_call1(self.0, arg0) + /// Invoke with two arguments. See [`Self::call0`]. + pub unsafe fn call2(self, this: JsThis, arg0: f64, arg1: f64) -> f64 { + js_closure_call2(self.0, this, arg0, arg1) } - /// Invoke with two arguments. See [`Self::call0`] safety. - pub unsafe fn call2(self, arg0: f64, arg1: f64) -> f64 { - js_closure_call2(self.0, arg0, arg1) + /// Invoke with three arguments. See [`Self::call0`]. + pub unsafe fn call3(self, this: JsThis, arg0: f64, arg1: f64, arg2: f64) -> f64 { + js_closure_call3(self.0, this, arg0, arg1, arg2) } - /// Invoke with three arguments. See [`Self::call0`] safety. - pub unsafe fn call3(self, arg0: f64, arg1: f64, arg2: f64) -> f64 { - js_closure_call3(self.0, arg0, arg1, arg2) + /// Invoke with four arguments. See [`Self::call0`]. + pub unsafe fn call4(self, this: JsThis, arg0: f64, arg1: f64, arg2: f64, arg3: f64) -> f64 { + js_closure_call4(self.0, this, arg0, arg1, arg2, arg3) } - /// Invoke with four arguments. See [`Self::call0`] safety. - pub unsafe fn call4(self, arg0: f64, arg1: f64, arg2: f64, arg3: f64) -> f64 { - js_closure_call4(self.0, arg0, arg1, arg2, arg3) + /// Invoke with any number of arguments. See [`Self::call0`]. + pub unsafe fn call_slice(self, this: JsThis, args: &[f64]) -> f64 { + js_closure_call_array(self.0, this, args.as_ptr(), args.len() as i64) } } @@ -176,11 +301,11 @@ mod tests { #[cfg(feature = "runtime-link")] #[test] fn native_closure_retains_capture() { - unsafe extern "C" fn callback(_: *const ClosureHeader) -> f64 { + unsafe extern "C" fn callback(_: *const ClosureHeader, _this: crate::JsThis) -> f64 { 0.0 } - register_closure_arity(callback as *const u8, 0); - let closure = alloc_closure(callback as *const u8, 1); + register_closure_arity(callback as JsBody0, 0); + let closure = alloc_closure(callback as JsBody0, 1); unsafe { set_closure_capture_f64(closure, 0, 42.0) }; assert_eq!(unsafe { closure_capture_f64(closure, 0) }, 42.0); } diff --git a/crates/perry-ffi/src/lib.rs b/crates/perry-ffi/src/lib.rs index 1f5ed3cd3b..3048222539 100644 --- a/crates/perry-ffi/src/lib.rs +++ b/crates/perry-ffi/src/lib.rs @@ -106,8 +106,10 @@ pub use jsvalue::{ mod closure; mod transient_roots; pub use closure::{ - alloc_closure, closure_capture_f64, register_closure_arity, set_closure_capture_f64, JsClosure, - RawClosureHeader, + alloc_closure, call_value, closure_capture_f64, register_closure_arity, register_closure_rest, + set_closure_capture_f64, JsBody, JsBody0, JsBody1, JsBody10, JsBody11, JsBody12, JsBody13, + JsBody14, JsBody15, JsBody16, JsBody2, JsBody3, JsBody4, JsBody5, JsBody6, JsBody7, JsBody8, + JsBody9, JsClosure, JsThis, RawClosureHeader, }; pub use transient_roots::{TransientRootScope, TransientRootedAddr, TransientRootedNanbox}; diff --git a/crates/perry-runtime/src/agent_ptrs.rs b/crates/perry-runtime/src/agent_ptrs.rs index 0bf760fdcb..34f4b4a39c 100644 --- a/crates/perry-runtime/src/agent_ptrs.rs +++ b/crates/perry-runtime/src/agent_ptrs.rs @@ -1,10 +1,10 @@ //! Per-agent runtime pointers that generated code reads WITHOUT a call. //! -//! Some runtime state is per agent (per thread) and read on hot emitted paths: -//! the first is the implicit-`this` cell a direct method call binds around -//! the call (`perry-codegen/src/expr/method_site.rs`), which otherwise costs -//! two runtime calls per method call. This block is the one place such -//! pointers live, at fixed slots, so emitted code can load one directly: +//! Some runtime state is per agent (per thread) and read on hot emitted paths. +//! This block is the one place such pointers live, at fixed slots, so emitted +//! code can load one directly. No slot is published today: slot 1 held the +//! implicit-`this` cell until this-as-a-parameter deleted it, and slot 0 is +//! reserved for the megamorphic follow-up's shape-record directory. //! //! * ELF executables (Linux, Android; x86-64 and aarch64): an `initialexec` //! thread-local access to [`PERRY_AGENT_PTRS`] — `mov %fs:off` (or @@ -24,8 +24,6 @@ use std::cell::Cell; -/// Slot 1: this agent's implicit-`this` cell. (Slot 0 is reserved.) -pub use crate::codegen_abi::AGENT_PTR_IMPLICIT_THIS; /// Slots in the block. **Must equal `AGENT_PTR_SLOTS` in /// `perry-codegen/src/expr/agent_ptr.rs`** (the emitted global's type). pub use crate::codegen_abi::AGENT_PTR_SLOTS; @@ -51,6 +49,7 @@ pub static PERRY_AGENT_PTRS: AgentPtrs = AgentPtrs([const { Cell::new(std::ptr::null()) }; AGENT_PTR_SLOTS]); /// Publish (or clear, with null) one of this agent's pointers. +#[allow(dead_code)] // no slot is published today (see the module doc) #[inline] pub(crate) fn publish(slot: usize, ptr: *const u8) { PERRY_AGENT_PTRS.0[slot].set(ptr); @@ -67,13 +66,3 @@ pub(crate) fn read(slot: usize) -> *const u8 { pub(crate) fn hot_addr() -> *mut u8 { &PERRY_AGENT_PTRS as *const AgentPtrs as *mut u8 } - -/// The address of this agent's implicit-`this` cell, published into slot -/// [`AGENT_PTR_IMPLICIT_THIS`] for emitted code that binds `this` around a -/// direct method call. A leaf: one TLS read and one store. -#[no_mangle] -pub extern "C" fn perry_implicit_this_cell() -> *const u8 { - let cell = &crate::tls_hot::hot().implicit_this as *const std::cell::Cell as *const u8; - publish(AGENT_PTR_IMPLICIT_THIS, cell); - cell -} diff --git a/crates/perry-runtime/src/arkts_callbacks.rs b/crates/perry-runtime/src/arkts_callbacks.rs index ca7d524b57..bf292ded0a 100644 --- a/crates/perry-runtime/src/arkts_callbacks.rs +++ b/crates/perry-runtime/src/arkts_callbacks.rs @@ -172,7 +172,7 @@ pub extern "C" fn perry_arkts_invoke_callback(idx: i64) -> f64 { return f64::from_bits(TAG_UNDEFINED); } arkts_log(&format!("invoke calling closure idx={}", idx)); - let result = js_closure_call0(raw); + let result = js_closure_call0(raw, crate::closure::plain_call_receiver()); arkts_log(&format!("invoke RETURN idx={}", idx)); result } @@ -211,7 +211,7 @@ pub extern "C" fn perry_arkts_invoke_callback1(idx: i64, arg_d: f64) -> f64 { return f64::from_bits(TAG_UNDEFINED); } arkts_log(&format!("invoke1 calling closure idx={}", idx)); - let result = js_closure_call1(raw, arg_d); + let result = js_closure_call1(raw, crate::closure::plain_call_receiver(), arg_d); arkts_log(&format!("invoke1 RETURN idx={}", idx)); result } diff --git a/crates/perry-runtime/src/array/collection_tag_tests.rs b/crates/perry-runtime/src/array/collection_tag_tests.rs index a403e34e04..33fb2e6ba1 100644 --- a/crates/perry-runtime/src/array/collection_tag_tests.rs +++ b/crates/perry-runtime/src/array/collection_tag_tests.rs @@ -355,6 +355,7 @@ thread_local! { extern "C" fn record_first_arg( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, _index: f64, _receiver: f64, diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index f012a970e0..b1cd2c1c56 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -526,7 +526,8 @@ pub extern "C" fn js_array_clone(src: *const ArrayHeader) -> *mut ArrayHeader { && crate::closure::is_closure_ptr(fn_raw) { let fn_ptr = fn_raw as *const crate::closure::ClosureHeader; - let iter = crate::closure::js_closure_call0(fn_ptr); + let iter = + crate::closure::js_closure_call0(fn_ptr, crate::closure::plain_call_receiver()); if js_array_is_array(iter).to_bits() == crate::value::TAG_TRUE { let ptr = crate::value::js_nanbox_get_pointer(iter) as *mut ArrayHeader; if !ptr.is_null() { diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 372a6d0088..1580b91f44 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -34,7 +34,7 @@ use super::generic_object::{ object_splice, object_unshift, }; use super::*; -use crate::closure::{js_closure_call3, ClosureHeader}; +use crate::closure::ClosureHeader; use crate::value::{JSValue, TAG_HOLE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED}; use std::ptr; @@ -580,7 +580,7 @@ pub extern "C" fn js_arraylike_forEach(recv: f64, cb: f64, this_arg: f64) -> f64 return undef(); } let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -593,8 +593,9 @@ pub extern "C" fn js_arraylike_forEach(recv: f64, cb: f64, this_arg: f64) -> f64 continue; } let v = al_get(recv_h.get_nanbox_f64(), k); - js_closure_call3( + crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v, k as f64, recv_h.get_nanbox_f64(), @@ -606,7 +607,7 @@ pub extern "C" fn js_arraylike_forEach(recv: f64, cb: f64, this_arg: f64) -> f64 #[no_mangle] pub extern "C" fn js_arraylike_map(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -631,8 +632,9 @@ pub extern "C" fn js_arraylike_map(recv: f64, cb: f64, this_arg: f64) -> f64 { // element pointer was derived before it. The pre-call address is never // bound, so there is nothing stale to reach for. let (mapped, result) = result_h.across_mut::(|| { - js_closure_call3( + crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v, k as f64, recv_h.get_nanbox_f64(), @@ -654,7 +656,7 @@ pub extern "C" fn js_arraylike_map(recv: f64, cb: f64, this_arg: f64) -> f64 { #[no_mangle] pub extern "C" fn js_arraylike_filter(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -672,8 +674,9 @@ pub extern "C" fn js_arraylike_filter(recv: f64, cb: f64, this_arg: f64) -> f64 continue; } v_h.set_nanbox_f64(al_get(recv_h.get_nanbox_f64(), k)); - let keep = js_closure_call3( + let keep = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v_h.get_nanbox_f64(), k as f64, recv_h.get_nanbox_f64(), @@ -696,7 +699,7 @@ pub extern "C" fn js_arraylike_filter(recv: f64, cb: f64, this_arg: f64) -> f64 #[no_mangle] pub extern "C" fn js_arraylike_some(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -709,8 +712,9 @@ pub extern "C" fn js_arraylike_some(recv: f64, cb: f64, this_arg: f64) -> f64 { continue; } let v = al_get(recv_h.get_nanbox_f64(), k); - let hit = js_closure_call3( + let hit = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v, k as f64, recv_h.get_nanbox_f64(), @@ -725,7 +729,7 @@ pub extern "C" fn js_arraylike_some(recv: f64, cb: f64, this_arg: f64) -> f64 { #[no_mangle] pub extern "C" fn js_arraylike_every(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -738,8 +742,9 @@ pub extern "C" fn js_arraylike_every(recv: f64, cb: f64, this_arg: f64) -> f64 { continue; } let v = al_get(recv_h.get_nanbox_f64(), k); - let hit = js_closure_call3( + let hit = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v, k as f64, recv_h.get_nanbox_f64(), @@ -757,7 +762,7 @@ pub extern "C" fn js_arraylike_every(recv: f64, cb: f64, this_arg: f64) -> f64 { #[no_mangle] pub extern "C" fn js_arraylike_find(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -769,8 +774,9 @@ pub extern "C" fn js_arraylike_find(recv: f64, cb: f64, this_arg: f64) -> f64 { let v_h = scope.root_nanbox_f64(undef()); for k in 0..len { v_h.set_nanbox_f64(al_get(recv_h.get_nanbox_f64(), k)); - let hit = js_closure_call3( + let hit = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v_h.get_nanbox_f64(), k as f64, recv_h.get_nanbox_f64(), @@ -785,7 +791,7 @@ pub extern "C" fn js_arraylike_find(recv: f64, cb: f64, this_arg: f64) -> f64 { #[no_mangle] pub extern "C" fn js_arraylike_findIndex(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -795,8 +801,9 @@ pub extern "C" fn js_arraylike_findIndex(recv: f64, cb: f64, this_arg: f64) -> f callable(cb_h.get_nanbox_f64()); for k in 0..len { let v = al_get(recv_h.get_nanbox_f64(), k); - let hit = js_closure_call3( + let hit = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v, k as f64, recv_h.get_nanbox_f64(), @@ -811,7 +818,7 @@ pub extern "C" fn js_arraylike_findIndex(recv: f64, cb: f64, this_arg: f64) -> f #[no_mangle] pub extern "C" fn js_arraylike_findLast(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -824,8 +831,9 @@ pub extern "C" fn js_arraylike_findLast(recv: f64, cb: f64, this_arg: f64) -> f6 let mut k = len - 1; while k >= 0 { v_h.set_nanbox_f64(al_get(recv_h.get_nanbox_f64(), k)); - let hit = js_closure_call3( + let hit = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v_h.get_nanbox_f64(), k as f64, recv_h.get_nanbox_f64(), @@ -841,7 +849,7 @@ pub extern "C" fn js_arraylike_findLast(recv: f64, cb: f64, this_arg: f64) -> f6 #[no_mangle] pub extern "C" fn js_arraylike_findLastIndex(recv: f64, cb: f64, this_arg: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let _g = crate::object::ImplicitThisScope::bind(&scope, this_arg); + let this_h = scope.root_nanbox_f64(this_arg); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -852,8 +860,9 @@ pub extern "C" fn js_arraylike_findLastIndex(recv: f64, cb: f64, this_arg: f64) let mut k = len - 1; while k >= 0 { let v = al_get(recv_h.get_nanbox_f64(), k); - let hit = js_closure_call3( + let hit = crate::closure::js_closure_call3( callable(cb_h.get_nanbox_f64()), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), v, k as f64, recv_h.get_nanbox_f64(), @@ -873,8 +882,6 @@ pub extern "C" fn js_arraylike_findLastIndex(recv: f64, cb: f64, this_arg: f64) #[no_mangle] pub extern "C" fn js_arraylike_reduce(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - // #11419: `this` is undefined in the callback (no thisArg parameter). - let _this = crate::object::ImplicitThisScope::bind_undefined(&scope); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -904,6 +911,7 @@ pub extern "C" fn js_arraylike_reduce(recv: f64, cb: f64, has_init: i32, init: f let v = al_get(recv_h.get_nanbox_f64(), k); acc_h.set_nanbox_f64(crate::closure::js_closure_call4( callable(cb_h.get_nanbox_f64()), + crate::closure::plain_call_receiver(), acc_h.get_nanbox_f64(), v, k as f64, @@ -918,8 +926,6 @@ pub extern "C" fn js_arraylike_reduce(recv: f64, cb: f64, has_init: i32, init: f #[no_mangle] pub extern "C" fn js_arraylike_reduceRight(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - // #11419: `this` is undefined in the callback (no thisArg parameter). - let _this = crate::object::ImplicitThisScope::bind_undefined(&scope); let cb_h = scope.root_nanbox_f64(cb); let recv_h = scope.root_nanbox_f64(to_object(recv)); // Spec order: LengthOfArrayLike(O) is read *before* the IsCallable(cb) @@ -948,6 +954,7 @@ pub extern "C" fn js_arraylike_reduceRight(recv: f64, cb: f64, has_init: i32, in let v = al_get(recv_h.get_nanbox_f64(), k); acc_h.set_nanbox_f64(crate::closure::js_closure_call4( callable(cb_h.get_nanbox_f64()), + crate::closure::plain_call_receiver(), acc_h.get_nanbox_f64(), v, k as f64, @@ -1387,12 +1394,11 @@ pub fn dispatch_arraylike_read_method( /// method thunks (`Array.prototype.pop`, etc.). A real-array receiver routes to /// the dense helpers; a plain array-like object routes to the spec-generic /// engine; any other receiver yields `undefined`. `recv` is the call-site -/// `this` (IMPLICIT_THIS) the thunk read. +/// `this` (the receiver parameter) the thunk read. pub fn array_proto_mutator(recv: f64, method: &str, args_ptr: *const f64, args_len: usize) -> f64 { // #6908: every mutator's step 1 is `ToObject(this value)`, which throws // for a nullish receiver. This is the receiver-less thunk invocation - // (`const f = arr.push; f(3)` — IMPLICIT_THIS holds its `undefined` - // default), which previously fell through every normalization below and + // (`const f = arr.push; f(3)` — a plain call passes `undefined`), which previously fell through every normalization below and // silently no-opped; node throws. let bits = recv.to_bits(); if bits == TAG_UNDEFINED || bits == TAG_NULL { diff --git a/crates/perry-runtime/src/array/generic_object.rs b/crates/perry-runtime/src/array/generic_object.rs index 534c676ddf..d8ebd5f943 100644 --- a/crates/perry-runtime/src/array/generic_object.rs +++ b/crates/perry-runtime/src/array/generic_object.rs @@ -382,8 +382,7 @@ pub(crate) fn object_sort(recv: f64, cmp_validated: *const ClosureHeader) -> f64 // Length and indexed getters may collect before comparison begins. let scope = crate::gc::RuntimeHandleScope::new(); let recv_handle = scope.root_nanbox_f64(recv); - // #11419: the comparator runs with `this` undefined. - let _this = crate::object::ImplicitThisScope::bind_undefined(&scope); + // #11419: the comparator runs with `this` undefined (a plain call). let cmp_handle = scope.root_raw_const_ptr(cmp_validated); let cmp = if cmp_validated.is_null() { None diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 0e704d1f5c..a5de77ff62 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -151,24 +151,6 @@ mod rooted_iter_array_tests { } } -/// Bind the callback's `this` to `undefined` for the duration of a dense -/// iteration (spec: absent `thisArg` means the callback's `this` is -/// `undefined` — NOT whatever ambient receiver the enclosing call left in -/// IMPLICIT_THIS; test262 some/15.4.4.17-5-25, filter/15.4.4.20-5-30). -/// Explicit-`thisArg` call sites route through the `js_arraylike_*` engine -/// instead of these helpers. Arrow callbacks capture `this` lexically and -/// are unaffected. -/// -/// The displaced receiver is the caller's `this`, held across every callback -/// in the loop, so it is restored from a root in the iteration's `scope` -/// (#10490 — this guard used to keep it in a plain field). -#[inline] -fn bind_undefined_this( - scope: &crate::gc::RuntimeHandleScope, -) -> crate::object::ImplicitThisScope<'_> { - crate::object::ImplicitThisScope::bind_undefined(scope) -} - /// #5989/#8117: `.forEach` on a receiver codegen could not prove is a /// collection is statically fused to the ARRAY entry point below, so a native /// `Set`/`Map` arrives there. Run the collection's own `forEach` and report @@ -282,7 +264,6 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo Some(h) => h.get_nanbox_f64(), None => rooted.receiver(), }; - let _tg = bind_undefined_this(&scope); if crate::array::array_iteration_is_exotic(arr) { for i in 0..length as usize { let arr = rooted.arr(); @@ -290,7 +271,13 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo continue; } let element = crate::array::array_spec_get(arr, i as u32); - cb_site.call(current_callback(), element, i as f64, self_value(&rooted)); + cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + self_value(&rooted), + ); } return; } @@ -302,7 +289,13 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo // dispatch path supports call3 safely, so bound native // methods like `array.forEach(console.log)` can observe the // source array just like Node. - cb_site.call(current_callback(), element, i as f64, self_value(&rooted)); + cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + self_value(&rooted), + ); } } } @@ -358,7 +351,6 @@ pub extern "C" fn js_array_map( // conservative scan; that knob was deleted in #7611, so there is no // longer a configuration in which this rooting is optional. See gh #6206. let cb_handle = scope.root_raw_const_ptr(callback); - let _tg = bind_undefined_this(&scope); // ECMA-262 §23.1.3.20 step 5: ArraySpeciesCreate(O, len) runs BEFORE // the iteration — it reads `O.constructor` / `@@species` (firing any @@ -393,7 +385,13 @@ pub extern "C" fn js_array_map( }; // JS .map() callback receives (element, index, array). let callback = cb_handle.get_raw_const_ptr::(); - let mapped = cb_site.call(callback, element, i as f64, rooted.receiver()); + let mapped = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); if is_plain { let result = result_arr(&result_rooted); // The head was just re-derived from `result_rooted` (a GC @@ -477,7 +475,6 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); if crate::array::array_iteration_is_exotic(arr) { for i in 0..length as usize { let arr = rooted.arr(); @@ -485,7 +482,13 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const continue; } let element = crate::array::array_spec_get(arr, i as u32); - let _ = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let _ = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); } return; } @@ -493,7 +496,13 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const let Some(element) = rooted.present(i) else { continue; }; - let _ = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let _ = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); } } } @@ -540,7 +549,6 @@ pub extern "C" fn js_array_filter( let cb_site = crate::closure::DirectCall3::resolve(callback); // Root the callback across the loop — see js_array_map / gh #6206. let cb_handle = scope.root_raw_const_ptr(callback); - let _tg = bind_undefined_this(&scope); // ECMA-262 §23.1.3.7 step 5: ArraySpeciesCreate(O, 0) runs before the // iteration (validates `O.constructor` / `@@species`, throwing on a @@ -568,7 +576,13 @@ pub extern "C" fn js_array_filter( } }; let callback = cb_handle.get_raw_const_ptr::(); - let keep = cb_site.call(callback, element, i as f64, rooted.receiver()); + let keep = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); // Proper truthy check: handles NaN-boxed booleans (TAG_FALSE != 0.0 but is falsy) if crate::value::js_is_truthy(keep) != 0 { if is_plain { @@ -636,7 +650,6 @@ pub extern "C" fn js_array_find(arr: *const ArrayHeader, callback: *const Closur let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -645,7 +658,13 @@ pub extern "C" fn js_array_find(arr: *const ArrayHeader, callback: *const Closur } else { rooted.get_or_undefined(i) }; - let result = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let result = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); // Proper truthy check: handles NaN-boxed booleans if crate::value::js_is_truthy(result) != 0 { return element; @@ -706,7 +725,6 @@ pub extern "C" fn js_array_findIndex( let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -715,7 +733,13 @@ pub extern "C" fn js_array_findIndex( } else { rooted.get_or_undefined(i) }; - let result = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let result = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); // Proper truthy check: handles NaN-boxed booleans if crate::value::js_is_truthy(result) != 0 { return i as i32; @@ -761,7 +785,6 @@ pub extern "C" fn js_array_find_last( let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); let exotic = crate::array::array_iteration_is_exotic(arr); for i in (0..length).rev() { let element = if exotic { @@ -769,7 +792,13 @@ pub extern "C" fn js_array_find_last( } else { rooted.get_or_undefined(i) }; - let result = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let result = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); if crate::value::js_is_truthy(result) != 0 { return element; } @@ -813,7 +842,6 @@ pub extern "C" fn js_array_find_last_index( let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); let exotic = crate::array::array_iteration_is_exotic(arr); for i in (0..length).rev() { let element = if exotic { @@ -821,7 +849,13 @@ pub extern "C" fn js_array_find_last_index( } else { rooted.get_or_undefined(i) }; - let result = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let result = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); if crate::value::js_is_truthy(result) != 0 { return i as i32; } @@ -922,7 +956,6 @@ pub extern "C" fn js_array_some(arr: *const ArrayHeader, callback: *const Closur let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -938,7 +971,13 @@ pub extern "C" fn js_array_some(arr: *const ArrayHeader, callback: *const Closur None => continue, } }; - let result = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let result = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); if crate::value::js_is_truthy(result) != 0 { return f64::from_bits(TAG_TRUE); } @@ -977,8 +1016,9 @@ pub extern "C" fn js_array_some_captureless( return js_array_some(original_arr, callback); } - let callback: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(callback_func) }; + // `callback_func` is a captureless compiled closure body. + let callback: crate::closure::body_call::js_body_fn_ty!(value, index, array) = + unsafe { crate::closure::body_call::js_body_fn!(callback_func; value, index, array) }; unsafe { let length = (*arr).length; // SAFETY: `normalize_array_receiver` returned this live plain-array @@ -1013,6 +1053,7 @@ pub extern "C" fn js_array_some_captureless( }; let result = callback( std::ptr::null(), + crate::closure::plain_call_receiver(), element, i as f64, array_receiver_value(arr), @@ -1083,7 +1124,6 @@ pub extern "C" fn js_array_every(arr: *const ArrayHeader, callback: *const Closu let current_callback = || { crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader }; - let _tg = bind_undefined_this(&scope); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -1099,7 +1139,13 @@ pub extern "C" fn js_array_every(arr: *const ArrayHeader, callback: *const Closu None => continue, } }; - let result = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let result = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); if crate::value::js_is_truthy(result) == 0 { return f64::from_bits(TAG_FALSE); } @@ -1152,13 +1198,18 @@ pub extern "C" fn js_array_flatMap( crate::value::JSValue::pointer(result as *const u8).bits(), )); }; - let _tg = bind_undefined_this(&scope); for i in 0..length as usize { let Some(element) = rooted.present(i) else { continue; }; - let mapped = cb_site.call(current_callback(), element, i as f64, rooted.receiver()); + let mapped = cb_site.call( + current_callback(), + crate::closure::plain_call_receiver(), + element, + i as f64, + rooted.receiver(), + ); // Root first: detecting a lazy array may materialize it, and a // push in the inner loop can move the callback result's target. sub_rooted.set_nanbox_f64(mapped); @@ -1199,10 +1250,8 @@ pub extern "C" fn js_array_reduce( has_initial: i32, initial: f64, ) -> f64 { - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback runs with `this` undefined (a plain call), not the + // receiver of whatever method dispatch encloses this call. // #8137: a Buffer-backed `Uint8Array` receiver. Perry's // `new Uint8Array([…])` is a `BufferHeader`, absent from the typed-array // registry, so the `lookup_typed_array_kind` re-dispatch below never @@ -1304,6 +1353,7 @@ pub extern "C" fn js_array_reduce( // Spec callback is `(accumulator, currentValue, currentIndex, array)`. let next = cb_site.call( current_callback(), + crate::closure::plain_call_receiver(), acc_rooted.get_nanbox_f64(), element, i as f64, diff --git a/crates/perry-runtime/src/array/iter_object.rs b/crates/perry-runtime/src/array/iter_object.rs index d5c08b37e1..12637d5e52 100644 --- a/crates/perry-runtime/src/array/iter_object.rs +++ b/crates/perry-runtime/src/array/iter_object.rs @@ -222,7 +222,10 @@ unsafe fn fs_dir_entries_iter_obj(arr: *const ArrayHeader, kind: i32) -> Option< if method_ptr == 0 || !crate::closure::is_closure_ptr(method_ptr as usize) { return None; } - let result = crate::closure::js_closure_call0(method_ptr as *const _); + let result = crate::closure::js_closure_call0( + method_ptr as *const _, + crate::closure::plain_call_receiver(), + ); let result_ptr = crate::value::js_nanbox_get_pointer(result); (result_ptr != 0).then_some(result_ptr) } diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 9a48155987..095dce8c81 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -307,6 +307,7 @@ fn async_from_sync_iter_result(value: f64, done: bool) -> f64 { extern "C" fn async_from_sync_fulfilled( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { // `async_from_sync_iter_result` allocates and can move the nursery `outer` @@ -328,6 +329,7 @@ extern "C" fn async_from_sync_fulfilled( extern "C" fn async_from_sync_rejected_value( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { // `async_from_sync_close` calls back into JS and allocates, which can move @@ -488,11 +490,6 @@ fn async_from_sync_call_raw(iter: f64, method: &[u8], args: &[f64]) -> Result Result Result Ok(Some(value)), @@ -573,7 +573,6 @@ fn async_from_sync_call_cached_raw( Err(exc) } }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::exception::js_try_end(); result } @@ -598,6 +597,7 @@ fn async_from_sync_call(iter: f64, method: &[u8], args: &[f64], close_on_rejecti extern "C" fn async_from_sync_next( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { // Root the captured sync iterator + its `[[NextMethod]]` across the sync @@ -633,6 +633,7 @@ extern "C" fn async_from_sync_next( extern "C" fn async_from_sync_return( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); @@ -659,6 +660,7 @@ extern "C" fn async_from_sync_return( extern "C" fn async_from_sync_throw( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); @@ -677,7 +679,10 @@ extern "C" fn async_from_sync_throw( } } -extern "C" fn async_from_sync_async_iterator(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn async_from_sync_async_iterator( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::closure::js_closure_get_capture_f64(closure, 0) } @@ -700,7 +705,7 @@ fn register_async_from_sync_thunks_once() { fn install_async_from_sync_method( obj: *mut crate::object::ObjectHeader, name: &[u8], - func: extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(a), iter: f64, ) -> f64 { // `obj`, `iter` and the freshly-allocated closure are live young objects @@ -856,11 +861,14 @@ pub extern "C" fn js_get_async_iterator(value: f64) -> f64 { if !is_callable_value(method) { throw_iterator_method_not_callable(); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); - let iterator = - unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let iterator = unsafe { + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(value), + std::ptr::null(), + 0, + ) + }; // GetIterator step 5: the result must be an Object. if !is_async_iterator_object(iterator) { throw_iterator_result_not_object(); @@ -947,8 +955,8 @@ pub(crate) fn array_from_spread_value(value: f64) -> *mut ArrayHeader { // * `clone_closure_rebind_this(method, value)` would bind a from-space // `this`, so a user `[Symbol.iterator]()` factory reads a dead // receiver and yields nothing; - // * `js_implicit_this_set(value)` publishes the same dead receiver to - // the canonical bound-method path; + // * the `[Symbol.iterator]()` call's receiver would be the same dead + // object for the canonical bound-method path; // * the `js_array_is_array(value)` fallback reads a recycled GcHeader // and reports a live array as "not iterable". // @@ -1179,15 +1187,10 @@ pub(crate) fn array_from_spread_value(value: f64) -> *mut ArrayHeader { } // Spec `GetIterator(obj)` → `Call(method, obj)`: the // `[Symbol.iterator]()` factory runs with `this === obj`. A canonical - // bound class method (#5128's `@@iterator` wrapper) reads its receiver - // from IMPLICIT_THIS, so set it here too — mirroring `js_get_iterator`. - // Without this the wrapper saw a stale `this` and the generator + // bound class method (#5128's `@@iterator` wrapper) takes its + // receiver from the call, so pass it — mirroring `js_get_iterator`. + // Without it the wrapper saw a stale `this` and the generator // yielded nothing (empty spread). - let prev_this = crate::object::js_implicit_this_set(value()); - // The DISPLACED receiver rides through arbitrary user code before - // being republished, so it is rooted too — republishing a from-space - // `this` is the same defect one frame out. - let prev_this_h = scope.root_nanbox_f64(prev_this); let trap_buf = crate::exception::js_try_push(); // `js_try_push` captured the handle-stack depth AFTER these roots // were pushed, so the `longjmp` restore leaves them intact and @@ -1195,25 +1198,25 @@ pub(crate) fn array_from_spread_value(value: f64) -> *mut ArrayHeader { // (#9305); the rethrow below runs after `js_try_end` pops this // trap, so it targets the enclosing handler. let outcome = crate::exception::arm_trap_and_run(trap_buf, || { - crate::closure::js_closure_call0(js_nanbox_get_pointer(rebound_h.get_nanbox_f64()) - as *const crate::closure::ClosureHeader) + crate::closure::js_closure_call0( + js_nanbox_get_pointer(rebound_h.get_nanbox_f64()) + as *const crate::closure::ClosureHeader, + crate::closure::JsThis::from_f64(value()), + ) }); let iter = match outcome { Some(iter) => iter, None => { - // Factory threw: restore the receiver and unwind the trap - // frame before re-propagating, so IMPLICIT_THIS can't leak - // into later calls (mirrors `async_from_sync_call_cached_raw` + // Factory threw: unwind the trap frame before + // re-propagating (mirrors `async_from_sync_call_cached_raw` // above). let exc = crate::exception::js_get_exception(); crate::exception::js_clear_exception(); - crate::object::js_implicit_this_set(prev_this_h.get_nanbox_f64()); crate::exception::js_try_end(); crate::exception::js_throw(exc) } }; let iter_h = scope.root_nanbox_f64(iter); - crate::object::js_implicit_this_set(prev_this_h.get_nanbox_f64()); crate::exception::js_try_end(); if crate::array::js_array_is_array(iter_h.get_nanbox_f64()).to_bits() == crate::value::TAG_TRUE @@ -1359,11 +1362,14 @@ fn throw_iterator_result_not_object() -> ! { pub extern "C" fn js_iterator_next_result(iter_f64: f64) -> f64 { let next = named_field(iter_f64, b"next"); let result = if is_callable_value(next) { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); - let result = unsafe { crate::closure::js_native_call_value(next, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + next, + crate::closure::JsThis::from_f64(iter_f64), + std::ptr::null(), + 0, + ) + } } else if next.to_bits() == crate::value::TAG_UNDEFINED && is_builtin_iterator_class_id(crate::value::js_nanbox_get_pointer(iter_f64) as usize) { @@ -1422,10 +1428,14 @@ pub extern "C" fn js_iterator_close_if_not_done(iter_f64: f64, done_f64: f64) -> crate::closure::throw_not_callable(); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); - let result = unsafe { crate::closure::js_native_call_value(ret, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = unsafe { + crate::closure::native_call_value_this( + ret, + crate::closure::JsThis::from_f64(iter_f64), + std::ptr::null(), + 0, + ) + }; if !is_object_like_value(result) { throw_iterator_result_not_object(); } @@ -1499,12 +1509,11 @@ pub(crate) fn sync_iterator_to_array_if_not_async(iter_f64: f64) -> Option<*mut } else { // Call(next, iterator) — bind `this` like `js_iterator_to_array` // does for its stored-closure path (#9019). - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); - let r = closure::js_closure_call1(next_ptr, f64::from_bits(TAG_UNDEFINED)); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - r + closure::js_closure_call1( + next_ptr, + closure::JsThis::from_f64(iter_f64), + f64::from_bits(TAG_UNDEFINED), + ) }; if crate::promise::js_value_is_promise(step) != 0 { return None; @@ -1539,10 +1548,14 @@ pub(crate) fn call_symbol_async_iterator(value: f64) -> Option { if !is_callable_value(method) { return None; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); - let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let iterator = unsafe { + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(value), + std::ptr::null(), + 0, + ) + }; if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { @@ -1710,14 +1723,11 @@ fn js_iterator_to_array_impl(iter_f64: f64) -> *mut ArrayHeader { // Call(next, iterator): bind `this` for the stored-closure path // exactly like `js_iterator_next_result` — a user-assigned // `it.next = function () { … }` may read `this` (#9019). - let prev_this = - scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); - let r = closure::js_closure_call1( + closure::js_closure_call1( js_nanbox_get_pointer(next_h.get_nanbox_f64()) as *const closure::ClosureHeader, + closure::JsThis::from_f64(iter_h.get_nanbox_f64()), f64::from_bits(TAG_UNDEFINED), - ); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - r + ) }; // IteratorNext (ECMA-262 §7.4.2 step 3): if Type(result) is not // Object, throw a TypeError. `is_pointer()` is true only for @@ -1833,12 +1843,11 @@ fn js_async_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { } else { // Call(next, iterator) — bind `this` for the stored-closure path // (#9019), mirroring `js_iterator_to_array`. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); - let r = closure::js_closure_call1(next_ptr, f64::from_bits(TAG_UNDEFINED)); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - r + closure::js_closure_call1( + next_ptr, + closure::JsThis::from_f64(iter_f64), + f64::from_bits(TAG_UNDEFINED), + ) }; let Some(step_result) = settled_promise_value(step) else { break; diff --git a/crates/perry-runtime/src/array/reduce_right.rs b/crates/perry-runtime/src/array/reduce_right.rs index a2653d4d5c..072344a604 100644 --- a/crates/perry-runtime/src/array/reduce_right.rs +++ b/crates/perry-runtime/src/array/reduce_right.rs @@ -22,10 +22,8 @@ pub extern "C" fn js_array_reduce_right( has_initial: i32, initial: f64, ) -> f64 { - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback runs with `this` undefined (a plain call), not + // the receiver of whatever method dispatch encloses this call. // #8137: a Buffer-backed `Uint8Array` receiver reads as an `ArrayHeader` // below — correct `length`, GARBAGE elements. `reduceRight` is the widest // case in the family: it is wrong even for a STATICALLY typed @@ -118,6 +116,7 @@ pub extern "C" fn js_array_reduce_right( // Spec callback `(accumulator, currentValue, currentIndex, array)`. let next = cb_site.call( callback, + crate::closure::plain_call_receiver(), acc_rooted.get_nanbox_f64(), element, i as f64, diff --git a/crates/perry-runtime/src/array/sort.rs b/crates/perry-runtime/src/array/sort.rs index afefd91184..a217758088 100644 --- a/crates/perry-runtime/src/array/sort.rs +++ b/crates/perry-runtime/src/array/sort.rs @@ -42,7 +42,9 @@ impl ComparatorCall { /// `direct` is a static code address and remains valid across relocation. #[inline(always)] pub(crate) fn less_equal_at(&self, comparator: *const ClosureHeader, a: f64, b: f64) -> bool { - let r = self.direct.call(comparator, a, b); + let r = self + .direct + .call(comparator, crate::closure::plain_call_receiver(), a, b); if r <= 0.0 { return true; } @@ -767,10 +769,8 @@ pub extern "C" fn js_array_sort_with_comparator( if comparator.is_null() { return js_array_sort_default(arr); } - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback runs with `this` undefined (a plain call), not + // the receiver of whatever method dispatch encloses this call. unsafe { // Runtime plain-object receiver behind a statically-Array variable — // probe the RAW pointer before the array-plausibility clean. diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index d42c0924b6..82258508bc 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -10,6 +10,7 @@ mod strict_dense; extern "C" fn test_map_to_string( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _element: f64, _index: f64, ) -> f64 { @@ -21,6 +22,7 @@ static DIRECT_SOME_CALLS: AtomicUsize = AtomicUsize::new(0); extern "C" fn direct_some_is_two( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, element: f64, index: f64, _array: f64, @@ -390,6 +392,7 @@ fn test_array_exotic_descriptors_and_global_prototype_identity() { let args = [0.0, 1.0, 0.0, 1.0]; crate::closure::js_native_call_value( f64::from_bits(array_ctor.bits()), + crate::closure::plain_call_receiver(), args.as_ptr(), args.len(), ) diff --git a/crates/perry-runtime/src/array/typed_array_receiver_tests.rs b/crates/perry-runtime/src/array/typed_array_receiver_tests.rs index ca09b9a58a..3ef32f96ae 100644 --- a/crates/perry-runtime/src/array/typed_array_receiver_tests.rs +++ b/crates/perry-runtime/src/array/typed_array_receiver_tests.rs @@ -242,6 +242,7 @@ fn typed_read_back(ta: *mut ArrayHeader, len: usize) -> Vec { /// `ClosureHeader`, which is what `DirectCall2::resolve` expects. extern "C" fn descending_cmp( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a: f64, b: f64, ) -> f64 { @@ -773,6 +774,7 @@ fn record(value: f64, index: f64, receiver: f64) { /// `(element, index, receiver) -> element * 2` — records, then doubles. extern "C" fn cb_double( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, index: f64, receiver: f64, @@ -785,6 +787,7 @@ extern "C" fn cb_double( /// answer "true" for the garbage reads, so it must not be used. extern "C" fn cb_is_three_or_two( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, index: f64, receiver: f64, @@ -796,6 +799,7 @@ extern "C" fn cb_is_three_or_two( /// Truthy only for the literal `1`. extern "C" fn cb_is_one( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, index: f64, receiver: f64, @@ -808,6 +812,7 @@ extern "C" fn cb_is_one( /// predicate. A `x > 0` predicate here is VACUOUS (see the header comment). extern "C" fn cb_is_a_source_byte( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, index: f64, receiver: f64, @@ -819,6 +824,7 @@ extern "C" fn cb_is_a_source_byte( /// `(accumulator, element, index, receiver) -> accumulator + element`. extern "C" fn cb_sum( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, accumulator: f64, value: f64, index: f64, diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index c32b5860ea..ea67e2cfc1 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -11,9 +11,9 @@ use std::sync::{LazyLock, Mutex}; use crate::array::{js_array_length, ArrayHeader}; use crate::closure::{ - js_closure_alloc, js_closure_call1, js_closure_call4, js_closure_call_array, - js_closure_get_capture_f64, js_closure_get_capture_ptr, js_closure_set_capture_f64, - js_closure_set_capture_ptr, js_register_closure_rest, ClosureHeader, + js_closure_alloc, js_closure_call1, js_closure_call4, js_closure_get_capture_f64, + js_closure_get_capture_ptr, js_closure_set_capture_f64, js_closure_set_capture_ptr, + js_register_closure_rest, ClosureHeader, }; use crate::object::{js_object_get_field_by_name, ObjectHeader}; use crate::string::{js_string_from_bytes, StringHeader}; @@ -993,6 +993,7 @@ fn emit_init(async_id: u64, type_name: &str, trigger_async_id: u64, resource: f6 with_hook_callbacks(HookPhase::Init, type_name == "PROMISE", |callback| { js_closure_call4( callback, + crate::closure::plain_call_receiver(), async_id as f64, type_value_handle.get_nanbox_f64(), async_id_to_js_number(trigger_async_id), @@ -1013,7 +1014,11 @@ fn before_with_kind(async_id: u64, trigger_async_id: u64, is_promise: bool) { CURRENT_EXECUTION_ID.with(|c| c.set(async_id)); CURRENT_TRIGGER_ID.with(|c| c.set(trigger_async_id)); with_hook_callbacks(HookPhase::Before, is_promise, |callback| { - js_closure_call1(callback, async_id as f64); + js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + async_id as f64, + ); }); } @@ -1030,7 +1035,11 @@ fn after_with_kind(async_id: u64, is_promise: bool) { return; } with_hook_callbacks(HookPhase::After, is_promise, |callback| { - js_closure_call1(callback, async_id as f64); + js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + async_id as f64, + ); }); let prev = EXECUTION_STACK .with(|stack| stack.borrow_mut().pop()) @@ -1064,7 +1073,11 @@ pub fn promise_resolve(async_id: u64) { return; } with_hook_callbacks(HookPhase::PromiseResolve, true, |callback| { - js_closure_call1(callback, async_id as f64); + js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + async_id as f64, + ); }); } @@ -1086,7 +1099,11 @@ fn destroy_with_kind(async_id: u64, is_promise: bool) { return; } with_hook_callbacks(HookPhase::Destroy, is_promise, |callback| { - js_closure_call1(callback, async_id as f64); + js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + async_id as f64, + ); }); RESOURCES.lock().unwrap().remove(&async_id); } @@ -1104,7 +1121,11 @@ fn emit_explicit_destroy(async_id: u64) { return; } with_hook_callbacks(HookPhase::Destroy, false, |callback| { - js_closure_call1(callback, async_id as f64); + js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + async_id as f64, + ); }); RESOURCES.lock().unwrap().remove(&async_id); } @@ -1339,6 +1360,7 @@ pub extern "C" fn js_async_resource_set_event_emitter(handle: i64, event_emitter /// the call-site `this` for a detached function. extern "C" fn async_resource_bind_method_trampoline( closure: *const ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { if closure.is_null() { @@ -1594,23 +1616,33 @@ pub extern "C" fn js_async_resource_run_in_async_scope( } let outcome = try_run_resource_scope(ids, || { let callback = crate::fs::extract_closure_ptr(rebound_handle.get_nanbox_f64()); - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - this_arg_handle.get_nanbox_f64(), - )); let callback_outcome = crate::exception::js_call_catching(|| { args_array_handle.with_const_ptr::(|arr| { if arr.is_null() { - unsafe { js_closure_call_array(callback as i64, ptr::null(), 0) } + unsafe { + crate::closure::js_closure_call_array( + callback as i64, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + ptr::null(), + 0, + ) + } } else { let len = js_array_length(arr) as i64; let data = unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 }; - unsafe { js_closure_call_array(callback as i64, data, len) } + unsafe { + crate::closure::js_closure_call_array( + callback as i64, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + data, + len, + ) + } } }) }); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match callback_outcome { Ok(value) => value, Err(error) => crate::exception::js_throw(error), @@ -1626,7 +1658,11 @@ pub extern "C" fn js_async_resource_run_in_async_scope( /// synthesized closure; receives the rest array of forwarded args and replays /// the call through `runInAsyncScope` so init/before/after/destroy fire with /// the bound resource's async id active. -extern "C" fn async_resource_bind_trampoline(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn async_resource_bind_trampoline( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } @@ -1637,7 +1673,7 @@ extern "C" fn async_resource_bind_trampoline(closure: *const ClosureHeader, rest return TAG_UNDEFINED_F64; } if JSValue::from_bits(this_arg.to_bits()).is_undefined() { - this_arg = crate::object::js_implicit_this_get(); + this_arg = this.as_f64(); } let args_array_ptr = ptr_from_nanboxed(rest) as i64; js_async_resource_run_in_async_scope(handle, callback, this_arg, args_array_ptr) @@ -1782,6 +1818,7 @@ pub extern "C" fn js_async_resource_static_bind_direct( pub extern "C" fn js_async_resource_static_bind_method( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, callback_value: f64, type_value: f64, this_arg: f64, @@ -1792,6 +1829,7 @@ pub extern "C" fn js_async_resource_static_bind_method( pub extern "C" fn js_async_local_storage_static_bind_method( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, callback_value: f64, _rest: f64, ) -> f64 { @@ -1806,124 +1844,28 @@ pub extern "C" fn js_async_local_storage_static_bind_direct( js_async_resource_static_bind_value(callback_value, TAG_UNDEFINED_F64, TAG_UNDEFINED_F64) } -fn register_context_snapshot(snapshot: crate::async_context::AsyncContextSnapshot) -> usize { - let id = NEXT_CONTEXT_SNAPSHOT_ID.fetch_add(1, Ordering::Relaxed); - CONTEXT_SNAPSHOTS.lock().unwrap().insert(id, snapshot); - id -} - -fn run_with_context_snapshot(snapshot_id: usize, f: impl FnOnce() -> f64) -> f64 { - let snapshot = CONTEXT_SNAPSHOTS - .lock() - .unwrap() - .get(&snapshot_id) - .cloned() - .unwrap_or_default(); - let scope = crate::gc::RuntimeHandleScope::new(); - let mut snapshot = snapshot; - let snapshot_roots = crate::async_context::root_snapshot(&scope, &snapshot); - let previous = crate::async_context::enter_context(&snapshot); - // Guard-held (GC-scanned, throw-safe) — see runInAsyncScope (#788). - crate::async_context::push_context_guard( - crate::async_context::ContextGuardAction::RestoreSnapshot(previous), - ); - let result = f(); - let result_handle = scope.root_nanbox_f64(result); - crate::async_context::refresh_snapshot_from_roots(&mut snapshot, &snapshot_roots); - if let Some(action) = crate::async_context::pop_context_guard() { - crate::async_context::apply_context_guard(action); - } - result_handle.get_nanbox_f64() -} - -fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 { - if !is_callable_value(callback_value) { - throw_apply_not_function(callback_value); - } - let scope = crate::gc::RuntimeHandleScope::new(); - let callback_handle = scope.root_nanbox_f64(callback_value); - let this_arg_handle = scope.root_nanbox_f64(this_arg); - let rebound_bits = crate::closure::clone_closure_rebind_this( - callback_handle.get_nanbox_f64().to_bits(), - this_arg_handle.get_nanbox_f64(), - ); - let rebound_handle = scope.root_nanbox_f64(f64::from_bits(rebound_bits)); - let callback = crate::fs::extract_closure_ptr(rebound_handle.get_nanbox_f64()); - if callback.is_null() { - throw_apply_not_function(callback_handle.get_nanbox_f64()); - } - let args_array = ptr_from_nanboxed(rest) as *const ArrayHeader; - let args_array_handle = scope.root_raw_const_ptr(args_array); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - this_arg_handle.get_nanbox_f64(), - )); - let result = if args_array.is_null() { - unsafe { js_closure_call_array(callback as i64, ptr::null(), 0) } - } else { - let arr = args_array_handle.get_raw_const_ptr::(); - let len = js_array_length(arr) as i64; - let data = if arr.is_null() || len == 0 { - ptr::null() - } else { - unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } - }; - unsafe { js_closure_call_array(callback as i64, data, len) } - }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result -} - -extern "C" fn async_local_storage_snapshot_trampoline( - closure: *const ClosureHeader, - callback_value: f64, - rest: f64, -) -> f64 { - let snapshot_id = js_closure_get_capture_ptr(closure, 0) as usize; - run_with_context_snapshot(snapshot_id, || { - // AsyncLocalStorage.snapshot() intentionally invokes the supplied - // callback as a plain function. The receiver used to call the snapshot - // wrapper itself is not forwarded. - call_callback_with_rest(callback_value, TAG_UNDEFINED_F64, rest) - }) -} +mod context_snapshots; +pub use context_snapshots::{ + js_async_local_storage_static_snapshot_direct, js_async_local_storage_static_snapshot_method, +}; +// Stays beside the module's root scanner: its registration flag is +// per-thread state (`scripts/gc_runtime_root_holders.py`). fn register_snapshot_trampoline_once() { thread_local! { static REGISTERED: Cell = const { Cell::new(false) }; } REGISTERED.with(|flag| { if !flag.get() { - js_register_closure_rest(async_local_storage_snapshot_trampoline as *const u8, 1); + js_register_closure_rest( + context_snapshots::async_local_storage_snapshot_trampoline as *const u8, + 1, + ); flag.set(true); } }); } -fn async_local_storage_static_snapshot_value() -> f64 { - register_snapshot_trampoline_once(); - let snapshot_id = register_context_snapshot(crate::async_context::capture_context()); - let closure = js_closure_alloc(async_local_storage_snapshot_trampoline as *const u8, 1); - if closure.is_null() { - return TAG_UNDEFINED_F64; - } - js_closure_set_capture_ptr(closure, 0, snapshot_id as i64); - crate::object::set_builtin_closure_length(closure as usize, 1); - crate::object::set_bound_native_closure_name(closure, "bound"); - crate::value::js_nanbox_pointer(closure as i64) -} - -pub extern "C" fn js_async_local_storage_static_snapshot_method( - _closure: *const ClosureHeader, - _rest: f64, -) -> f64 { - async_local_storage_static_snapshot_value() -} - -#[no_mangle] -pub extern "C" fn js_async_local_storage_static_snapshot_direct(_rest: i64) -> f64 { - async_local_storage_static_snapshot_value() -} - pub fn scan_async_hooks_roots(mark: &mut dyn FnMut(f64)) { let mut visitor = crate::gc::RuntimeRootVisitor::for_copy(mark); scan_async_hooks_roots_mut(&mut visitor); diff --git a/crates/perry-runtime/src/async_hooks/context_snapshots.rs b/crates/perry-runtime/src/async_hooks/context_snapshots.rs new file mode 100644 index 0000000000..aaf81ecf63 --- /dev/null +++ b/crates/perry-runtime/src/async_hooks/context_snapshots.rs @@ -0,0 +1,125 @@ +//! `AsyncLocalStorage.snapshot()` and the context snapshots it and the +//! static bind helpers run callbacks under. Split out of `async_hooks.rs` +//! (file-size gate); items move unchanged. + +use super::*; + +pub(super) fn register_context_snapshot( + snapshot: crate::async_context::AsyncContextSnapshot, +) -> usize { + let id = NEXT_CONTEXT_SNAPSHOT_ID.fetch_add(1, Ordering::Relaxed); + CONTEXT_SNAPSHOTS.lock().unwrap().insert(id, snapshot); + id +} + +pub(super) fn run_with_context_snapshot(snapshot_id: usize, f: impl FnOnce() -> f64) -> f64 { + let snapshot = CONTEXT_SNAPSHOTS + .lock() + .unwrap() + .get(&snapshot_id) + .cloned() + .unwrap_or_default(); + let scope = crate::gc::RuntimeHandleScope::new(); + let mut snapshot = snapshot; + let snapshot_roots = crate::async_context::root_snapshot(&scope, &snapshot); + let previous = crate::async_context::enter_context(&snapshot); + // Guard-held (GC-scanned, throw-safe) — see runInAsyncScope (#788). + crate::async_context::push_context_guard( + crate::async_context::ContextGuardAction::RestoreSnapshot(previous), + ); + let result = f(); + let result_handle = scope.root_nanbox_f64(result); + crate::async_context::refresh_snapshot_from_roots(&mut snapshot, &snapshot_roots); + if let Some(action) = crate::async_context::pop_context_guard() { + crate::async_context::apply_context_guard(action); + } + result_handle.get_nanbox_f64() +} + +pub(super) fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 { + if !is_callable_value(callback_value) { + throw_apply_not_function(callback_value); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let callback_handle = scope.root_nanbox_f64(callback_value); + let this_arg_handle = scope.root_nanbox_f64(this_arg); + let rebound_bits = crate::closure::clone_closure_rebind_this( + callback_handle.get_nanbox_f64().to_bits(), + this_arg_handle.get_nanbox_f64(), + ); + let rebound_handle = scope.root_nanbox_f64(f64::from_bits(rebound_bits)); + let callback = crate::fs::extract_closure_ptr(rebound_handle.get_nanbox_f64()); + if callback.is_null() { + throw_apply_not_function(callback_handle.get_nanbox_f64()); + } + let args_array = ptr_from_nanboxed(rest) as *const ArrayHeader; + let args_array_handle = scope.root_raw_const_ptr(args_array); + let result = if args_array.is_null() { + unsafe { + crate::closure::js_closure_call_array( + callback as i64, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + ptr::null(), + 0, + ) + } + } else { + let arr = args_array_handle.get_raw_const_ptr::(); + let len = js_array_length(arr) as i64; + let data = if arr.is_null() || len == 0 { + ptr::null() + } else { + unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } + }; + unsafe { + crate::closure::js_closure_call_array( + callback as i64, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + data, + len, + ) + } + }; + result +} + +pub(super) extern "C" fn async_local_storage_snapshot_trampoline( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + callback_value: f64, + rest: f64, +) -> f64 { + let snapshot_id = js_closure_get_capture_ptr(closure, 0) as usize; + run_with_context_snapshot(snapshot_id, || { + // AsyncLocalStorage.snapshot() intentionally invokes the supplied + // callback as a plain function. The receiver used to call the snapshot + // wrapper itself is not forwarded. + call_callback_with_rest(callback_value, TAG_UNDEFINED_F64, rest) + }) +} + +pub(super) fn async_local_storage_static_snapshot_value() -> f64 { + super::register_snapshot_trampoline_once(); + let snapshot_id = register_context_snapshot(crate::async_context::capture_context()); + let closure = js_closure_alloc(async_local_storage_snapshot_trampoline as *const u8, 1); + if closure.is_null() { + return TAG_UNDEFINED_F64; + } + js_closure_set_capture_ptr(closure, 0, snapshot_id as i64); + crate::object::set_builtin_closure_length(closure as usize, 1); + crate::object::set_bound_native_closure_name(closure, "bound"); + crate::value::js_nanbox_pointer(closure as i64) +} + +pub extern "C" fn js_async_local_storage_static_snapshot_method( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _rest: f64, +) -> f64 { + async_local_storage_static_snapshot_value() +} + +#[no_mangle] +pub extern "C" fn js_async_local_storage_static_snapshot_direct(_rest: i64) -> f64 { + async_local_storage_static_snapshot_value() +} diff --git a/crates/perry-runtime/src/async_hooks/provider_ffi.rs b/crates/perry-runtime/src/async_hooks/provider_ffi.rs index 7570dc316a..cf5f524a12 100644 --- a/crates/perry-runtime/src/async_hooks/provider_ffi.rs +++ b/crates/perry-runtime/src/async_hooks/provider_ffi.rs @@ -5,7 +5,10 @@ use super::{ try_leave_resource_scope, AsyncResourceIds, RESOURCES, }; -extern "C" fn deferred_destroy_step(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn deferred_destroy_step( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let async_id = crate::closure::js_closure_get_capture_f64(closure, 0) as u64; let remaining = crate::closure::js_closure_get_capture_f64(closure, 1) as u32; if remaining == 0 { @@ -213,8 +216,10 @@ unsafe fn provider_run_catching( } /// Provider callback wrapper for external EventEmitter-style dispatch. It -/// additionally restores implicit `this` and can retire a one-shot provider -/// before propagating a JavaScript exception. +/// additionally roots the listener receiver for the duration of the callback +/// and can retire a one-shot provider before propagating a JavaScript +/// exception. `callback` makes its own JS calls and passes them the receiver +/// itself: nothing here binds `this` for it. #[no_mangle] pub unsafe extern "C" fn js_async_hooks_provider_run_catching_with_this( async_id: u64, @@ -224,7 +229,7 @@ pub unsafe extern "C" fn js_async_hooks_provider_run_catching_with_this( data: *mut std::ffi::c_void, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let this_value = scope.root_nanbox_f64(this_value); + let _receiver_root = scope.root_nanbox_f64(this_value); if let Err(error) = try_enter_resource_scope(provider_ids(async_id)) { let error = scope.root_nanbox_f64(error); if destroy_after != 0 { @@ -235,15 +240,11 @@ pub unsafe extern "C" fn js_async_hooks_provider_run_catching_with_this( } crate::exception::js_throw(error.get_nanbox_f64()); } - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - this_value.get_nanbox_f64(), - )); let outcome = crate::exception::js_call_catching(|| callback(data)); let (threw, result) = match outcome { Ok(value) => (false, scope.root_nanbox_f64(value)), Err(error) => (true, scope.root_nanbox_f64(error)), }; - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); let leave = try_leave_resource_scope(async_id); let (leave_threw, leave_result) = match leave { Ok(()) => ( diff --git a/crates/perry-runtime/src/async_hooks/test_support.rs b/crates/perry-runtime/src/async_hooks/test_support.rs index 93fc9330bb..a264f1b335 100644 --- a/crates/perry-runtime/src/async_hooks/test_support.rs +++ b/crates/perry-runtime/src/async_hooks/test_support.rs @@ -63,7 +63,11 @@ pub(crate) fn test_async_hooks_scanner_snapshot() -> (usize, u64) { mod tests { use super::*; - extern "C" fn throwing_lifecycle_hook(_closure: *const ClosureHeader, _async_id: f64) -> f64 { + extern "C" fn throwing_lifecycle_hook( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _async_id: f64, + ) -> f64 { crate::exception::js_throw(73.0) } diff --git a/crates/perry-runtime/src/atomics.rs b/crates/perry-runtime/src/atomics.rs index d2abd66ba1..b2f47eeebd 100644 --- a/crates/perry-runtime/src/atomics.rs +++ b/crates/perry-runtime/src/atomics.rs @@ -465,7 +465,12 @@ fn atomics_bitwise(view: f64, index: f64, value: f64, op: impl FnOnce(u64, u64) } #[no_mangle] -pub extern "C" fn js_atomics_load(_closure: *const ClosureHeader, view: f64, index: f64) -> f64 { +pub extern "C" fn js_atomics_load( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + view: f64, + index: f64, +) -> f64 { let (view, idx) = slot(view, index); if view.is_bigint() { return bigint_result_for_kind(view.kind(), view.get_bigint_bits(idx)); @@ -474,7 +479,11 @@ pub extern "C" fn js_atomics_load(_closure: *const ClosureHeader, view: f64, ind } #[no_mangle] -pub extern "C" fn js_atomics_is_lock_free(_closure: *const ClosureHeader, size: f64) -> f64 { +pub extern "C" fn js_atomics_is_lock_free( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + size: f64, +) -> f64 { // ToNumber(size) (runs `valueOf`/`toString`, so `'4'`→4 behaves like Node), // then an EXACT membership test over the lock-free element widths. Node/V8 // compares the raw number, so `4.9` is NOT floored to 4 — it is simply not a @@ -486,6 +495,7 @@ pub extern "C" fn js_atomics_is_lock_free(_closure: *const ClosureHeader, size: #[no_mangle] pub extern "C" fn js_atomics_store( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -509,6 +519,7 @@ pub extern "C" fn js_atomics_store( #[no_mangle] pub extern "C" fn js_atomics_add( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -531,6 +542,7 @@ pub extern "C" fn js_atomics_add( #[no_mangle] pub extern "C" fn js_atomics_sub( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -553,6 +565,7 @@ pub extern "C" fn js_atomics_sub( #[no_mangle] pub extern "C" fn js_atomics_and( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -563,6 +576,7 @@ pub extern "C" fn js_atomics_and( #[no_mangle] pub extern "C" fn js_atomics_or( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -573,6 +587,7 @@ pub extern "C" fn js_atomics_or( #[no_mangle] pub extern "C" fn js_atomics_xor( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -583,6 +598,7 @@ pub extern "C" fn js_atomics_xor( #[no_mangle] pub extern "C" fn js_atomics_exchange( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, value: f64, @@ -602,6 +618,7 @@ pub extern "C" fn js_atomics_exchange( #[no_mangle] pub extern "C" fn js_atomics_compare_exchange( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, expected: f64, @@ -630,6 +647,7 @@ pub extern "C" fn js_atomics_compare_exchange( #[no_mangle] pub extern "C" fn js_atomics_notify( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, count: f64, @@ -652,6 +670,7 @@ pub extern "C" fn js_atomics_notify( #[no_mangle] pub extern "C" fn js_atomics_wait( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, expected: f64, @@ -706,6 +725,7 @@ pub extern "C" fn js_atomics_wait( #[no_mangle] pub extern "C" fn js_atomics_wait_async( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, view: f64, index: f64, expected: f64, diff --git a/crates/perry-runtime/src/buffer/mod.rs b/crates/perry-runtime/src/buffer/mod.rs index 184172401e..b896430e12 100644 --- a/crates/perry-runtime/src/buffer/mod.rs +++ b/crates/perry-runtime/src/buffer/mod.rs @@ -345,7 +345,14 @@ mod tests { unsafe { crate::symbol::js_object_get_symbol_property(buf_value, iter_sym_value) }; assert_ne!(method.to_bits(), crate::value::TAG_UNDEFINED); - let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; + let iterator = unsafe { + crate::closure::js_native_call_value( + method, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; let result = unsafe { crate::object::js_native_call_method( iterator, diff --git a/crates/perry-runtime/src/builtins/console.rs b/crates/perry-runtime/src/builtins/console.rs index 4aa7183396..4267a81d69 100644 --- a/crates/perry-runtime/src/builtins/console.rs +++ b/crates/perry-runtime/src/builtins/console.rs @@ -117,11 +117,11 @@ pub extern "C" fn js_console_log_dynamic(value: f64) { /// Thunk for `console.log` exposed as a real callable closure value /// (#236). Lets `Promise.resolve(x).then(console.log)` actually call into -/// `js_console_log_dynamic` instead of being a no-op sentinel; the call -/// signature `extern "C" fn(*const ClosureHeader, f64) -> f64` matches -/// what `js_closure_call1` invokes through. +/// `js_console_log_dynamic` instead of being a no-op sentinel; it is a JS +/// body (`perry_abi::JS_BODY_*`: callee, receiver, one argument). extern "C" fn console_log_callable_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { js_console_log_dynamic(value); @@ -552,18 +552,29 @@ fn console_set_field(obj: *mut crate::object::ObjectHeader, name: &str, value: f crate::object::js_object_set_field_by_name(obj, key, value); } -extern "C" fn console_context_method_noop(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn console_context_method_noop( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { console_undefined() } extern "C" fn console_task_run( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, callback: f64, ) -> f64 { if !console_is_callable(callback) { throw_plain_console_error("First argument must be a function."); } - unsafe { crate::closure::js_native_call_value(callback, std::ptr::null(), 0) } + unsafe { + crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + } } /// `console.context([name])` returns an inspector-scoped console object in diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index 0e2a9db0b3..c784f30e4f 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -1043,6 +1043,7 @@ unsafe fn format_object_as_json( let undef_arg = f64::from_bits(crate::value::TAG_UNDEFINED); let ret = crate::closure::js_closure_call3( closure_ptr, + crate::closure::plain_call_receiver(), remaining, options_arg, undef_arg, @@ -1364,7 +1365,8 @@ fn format_accessor_property(acc: crate::object::AccessorDescriptor, depth: usize let closure = (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !closure.is_null() { - let value = crate::closure::js_closure_call0(closure); + let value = + crate::closure::js_closure_call0(closure, crate::closure::plain_call_receiver()); return format!("[{}: {}]", label, format_jsvalue_for_json(value, depth + 1)); } } diff --git a/crates/perry-runtime/src/builtins/globals.rs b/crates/perry-runtime/src/builtins/globals.rs index 4e42606df3..ea779f5455 100644 --- a/crates/perry-runtime/src/builtins/globals.rs +++ b/crates/perry-runtime/src/builtins/globals.rs @@ -1205,35 +1205,91 @@ pub(crate) fn drain_queued_microtasks_count() -> i32 { let cb_ptr = callback_handle.get_raw_const_ptr::(); match a.len() { 0 => { - js_closure_call0(cb_ptr); + js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } 1 => { - js_closure_call1(cb_ptr, a[0]); + js_closure_call1(cb_ptr, crate::closure::plain_call_receiver(), a[0]); } 2 => { - js_closure_call2(cb_ptr, a[0], a[1]); + js_closure_call2(cb_ptr, crate::closure::plain_call_receiver(), a[0], a[1]); } 3 => { - js_closure_call3(cb_ptr, a[0], a[1], a[2]); + js_closure_call3( + cb_ptr, + crate::closure::plain_call_receiver(), + a[0], + a[1], + a[2], + ); } 4 => { - js_closure_call4(cb_ptr, a[0], a[1], a[2], a[3]); + js_closure_call4( + cb_ptr, + crate::closure::plain_call_receiver(), + a[0], + a[1], + a[2], + a[3], + ); } 5 => { - js_closure_call5(cb_ptr, a[0], a[1], a[2], a[3], a[4]); + js_closure_call5( + cb_ptr, + crate::closure::plain_call_receiver(), + a[0], + a[1], + a[2], + a[3], + a[4], + ); } 6 => { - js_closure_call6(cb_ptr, a[0], a[1], a[2], a[3], a[4], a[5]); + js_closure_call6( + cb_ptr, + crate::closure::plain_call_receiver(), + a[0], + a[1], + a[2], + a[3], + a[4], + a[5], + ); } 7 => { - js_closure_call7(cb_ptr, a[0], a[1], a[2], a[3], a[4], a[5], a[6]); + js_closure_call7( + cb_ptr, + crate::closure::plain_call_receiver(), + a[0], + a[1], + a[2], + a[3], + a[4], + a[5], + a[6], + ); } 8 => { - js_closure_call8(cb_ptr, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); + js_closure_call8( + cb_ptr, + crate::closure::plain_call_receiver(), + a[0], + a[1], + a[2], + a[3], + a[4], + a[5], + a[6], + a[7], + ); } // #10420: more than 8 trailing args used to clamp to 9. n => unsafe { - crate::closure::js_closure_call_array(cb_ptr as i64, a.as_ptr(), n as i64); + crate::closure::js_closure_call_array( + cb_ptr as i64, + crate::closure::plain_call_receiver(), + a.as_ptr(), + n as i64, + ); }, } crate::async_hooks::after(async_id); diff --git a/crates/perry-runtime/src/bun_compat/cli_utils.rs b/crates/perry-runtime/src/bun_compat/cli_utils.rs index 1a4f1c9a48..3bbd1a6036 100644 --- a/crates/perry-runtime/src/bun_compat/cli_utils.rs +++ b/crates/perry-runtime/src/bun_compat/cli_utils.rs @@ -53,7 +53,7 @@ fn number_arg(value: f64) -> Option { } } -fn closure1(name: &str, func: extern "C" fn(*const ClosureHeader, f64) -> f64) -> f64 { +fn closure1(name: &str, func: crate::closure::body_call::js_body_fn_ty!(a)) -> f64 { js_register_closure_arity(func as *const u8, 1); let closure = js_closure_alloc(func as *const u8, 0); crate::object::set_bound_native_closure_name(closure, name); @@ -61,11 +61,7 @@ fn closure1(name: &str, func: extern "C" fn(*const ClosureHeader, f64) -> f64) - f64::from_bits(JSValue::pointer(closure as *const u8).bits()) } -fn closure2( - name: &str, - func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - length: u32, -) -> f64 { +fn closure2(name: &str, func: crate::closure::body_call::js_body_fn_ty!(a, a), length: u32) -> f64 { js_register_closure_arity(func as *const u8, 2); let closure = js_closure_alloc(func as *const u8, 0); crate::object::set_bound_native_closure_name(closure, name); @@ -75,7 +71,7 @@ fn closure2( fn closure3( name: &str, - func: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(a, a, a), length: u32, ) -> f64 { js_register_closure_arity(func as *const u8, 3); @@ -118,12 +114,17 @@ fn namespace_object(fields: &[(&[u8], f64)]) -> f64 { // Object-valued exports // --------------------------------------------------------------------------- -extern "C" fn yaml_parse_closure(_closure: *const ClosureHeader, input: f64) -> f64 { +extern "C" fn yaml_parse_closure( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + input: f64, +) -> f64 { yaml_parse(input) } extern "C" fn yaml_stringify_closure( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, input: f64, replacer: f64, space: f64, @@ -141,7 +142,11 @@ pub fn js_bun_yaml() -> f64 { ]) } -extern "C" fn toml_parse_closure(_closure: *const ClosureHeader, input: f64) -> f64 { +extern "C" fn toml_parse_closure( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + input: f64, +) -> f64 { let source = value_to_string(input); match toml_parse_result(&source) { Ok(value) => value, @@ -189,7 +194,12 @@ fn normalize_semver_version(input: &str) -> &str { .unwrap_or_else(|| input.trim()) } -extern "C" fn semver_order_closure(_closure: *const ClosureHeader, left: f64, right: f64) -> f64 { +extern "C" fn semver_order_closure( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + left: f64, + right: f64, +) -> f64 { let left_source = value_to_string(left); let right_source = value_to_string(right); let left = match node_semver::Version::parse(normalize_semver_version(&left_source)) { @@ -215,6 +225,7 @@ extern "C" fn semver_order_closure(_closure: *const ClosureHeader, left: f64, ri extern "C" fn semver_satisfies_closure( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, version: f64, range: f64, ) -> f64 { @@ -245,6 +256,7 @@ pub fn js_bun_semver() -> f64 { extern "C" fn jsonl_parse_chunk_closure( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, input: f64, start: f64, end: f64, @@ -567,7 +579,12 @@ pub extern "C" fn js_bun_zstd_decompress(input: f64) -> f64 { } } -extern "C" fn xxhash64_closure(_closure: *const ClosureHeader, input: f64, seed: f64) -> f64 { +extern "C" fn xxhash64_closure( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + input: f64, + seed: f64, +) -> f64 { let bytes = payload_bytes(input).unwrap_or_default(); let hash = xxhash_rust::xxh64::xxh64(&bytes, super::hash_seed(seed)); let bigint = crate::bigint::js_bigint_from_u64(hash); diff --git a/crates/perry-runtime/src/bun_compat/glob.rs b/crates/perry-runtime/src/bun_compat/glob.rs index 9c903779a1..f37c3bda30 100644 --- a/crates/perry-runtime/src/bun_compat/glob.rs +++ b/crates/perry-runtime/src/bun_compat/glob.rs @@ -2,15 +2,27 @@ use super::*; -extern "C" fn bun_glob_scan(closure: *const ClosureHeader, options: f64) -> f64 { +extern "C" fn bun_glob_scan( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + options: f64, +) -> f64 { crate::fs::js_bun_glob_async_iterator(captured(closure), options) } -extern "C" fn bun_glob_scan_sync(closure: *const ClosureHeader, options: f64) -> f64 { +extern "C" fn bun_glob_scan_sync( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + options: f64, +) -> f64 { crate::fs::js_bun_glob_sync_iterator(captured(closure), options) } -extern "C" fn bun_glob_match(closure: *const ClosureHeader, path: f64) -> f64 { +extern "C" fn bun_glob_match( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + path: f64, +) -> f64 { match crate::fs::bun_glob_matches(captured(closure), path) { Ok(matches) => bool_value(matches), Err(error) => crate::exception::js_throw(error), diff --git a/crates/perry-runtime/src/bun_compat/mod.rs b/crates/perry-runtime/src/bun_compat/mod.rs index 1e21a017f2..f1ccb6f760 100644 --- a/crates/perry-runtime/src/bun_compat/mod.rs +++ b/crates/perry-runtime/src/bun_compat/mod.rs @@ -159,14 +159,14 @@ fn set_field(obj: *mut crate::object::ObjectHeader, key: &[u8], value: f64) { js_object_set_field_by_name(obj, key_ptr(key), value); } -fn bound_method0(func: extern "C" fn(*const ClosureHeader) -> f64, capture: f64) -> f64 { +fn bound_method0(func: crate::closure::body_call::js_body_fn_ty!(), capture: f64) -> f64 { js_register_closure_arity(func as *const u8, 0); let closure = js_closure_alloc(func as *const u8, 1); js_closure_set_capture_f64(closure, 0, capture); f64::from_bits(JSValue::pointer(closure as *const u8).bits()) } -fn bound_method1(func: extern "C" fn(*const ClosureHeader, f64) -> f64, capture: f64) -> f64 { +fn bound_method1(func: crate::closure::body_call::js_body_fn_ty!(a), capture: f64) -> f64 { js_register_closure_arity(func as *const u8, 1); let closure = js_closure_alloc(func as *const u8, 1); js_closure_set_capture_f64(closure, 0, capture); @@ -311,7 +311,7 @@ fn read_file_or_reject(path: &str) -> Result, f64> { .map_err(|err| unsafe { crate::fs::build_fs_error_value(&err, "open", path) }) } -extern "C" fn bun_file_text(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_file_text(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let path = value_to_string(captured(closure)); match read_file_or_reject(&path) { Ok(bytes) => promise_value(boxed_str(String::from_utf8_lossy(&bytes).as_bytes())), @@ -319,7 +319,7 @@ extern "C" fn bun_file_text(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn bun_file_json(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_file_json(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let path = value_to_string(captured(closure)); match read_file_or_reject(&path) { Ok(bytes) => json_parse_promise(&bytes), @@ -327,7 +327,10 @@ extern "C" fn bun_file_json(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn bun_file_array_buffer(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_file_array_buffer( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let path = value_to_string(captured(closure)); match read_file_or_reject(&path) { Ok(bytes) => promise_value(array_buffer_from_bytes(&bytes)), @@ -335,7 +338,7 @@ extern "C" fn bun_file_array_buffer(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn bun_file_bytes(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_file_bytes(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let path = value_to_string(captured(closure)); match read_file_or_reject(&path) { Ok(bytes) => promise_value(uint8_array_from_bytes(&bytes)), @@ -343,7 +346,7 @@ extern "C" fn bun_file_bytes(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn bun_file_exists(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_file_exists(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let path = value_to_string(captured(closure)); let exists = if crate::embedded::lookup(&path).is_some() { true @@ -487,7 +490,7 @@ pub extern "C" fn js_bun_write(dest: f64, data: f64) -> f64 { // Bun.stdin / Bun.stdout / Bun.stderr // --------------------------------------------------------------------------- -extern "C" fn bun_stdin_text(_closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_stdin_text(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let mut buf = Vec::new(); match std::io::stdin().lock().read_to_end(&mut buf) { Ok(_) => promise_value(boxed_str(String::from_utf8_lossy(&buf).as_bytes())), @@ -495,7 +498,7 @@ extern "C" fn bun_stdin_text(_closure: *const ClosureHeader) -> f64 { } } -extern "C" fn bun_stdin_json(_closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_stdin_json(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let mut buf = Vec::new(); match std::io::stdin().lock().read_to_end(&mut buf) { Ok(_) => json_parse_promise(&buf), @@ -503,7 +506,10 @@ extern "C" fn bun_stdin_json(_closure: *const ClosureHeader) -> f64 { } } -extern "C" fn bun_stdin_array_buffer(_closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_stdin_array_buffer( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let mut buf = Vec::new(); match std::io::stdin().lock().read_to_end(&mut buf) { Ok(_) => promise_value(array_buffer_from_bytes(&buf)), diff --git a/crates/perry-runtime/src/bun_compat/plugin.rs b/crates/perry-runtime/src/bun_compat/plugin.rs index 2b265d5ee6..38cb2aaaf5 100644 --- a/crates/perry-runtime/src/bun_compat/plugin.rs +++ b/crates/perry-runtime/src/bun_compat/plugin.rs @@ -7,7 +7,11 @@ use crate::gc::{RuntimeHandle, RuntimeHandleScope}; use crate::object::{js_object_alloc, js_object_set_field_by_name}; use crate::value::{js_nanbox_pointer, JSValue}; -extern "C" fn ignore(_closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn ignore( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _value: f64, +) -> f64 { undefined() } @@ -96,16 +100,20 @@ fn register(plugin: f64) -> f64 { b"config", js_nanbox_pointer(js_object_alloc(0, 0) as i64), ); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set(if is_function { - undefined() - } else { - plugin.get_nanbox_f64() - })); let result = crate::exception::catch_js_throw(|| unsafe { let args = [build.get_nanbox_f64()]; - crate::closure::js_native_call_value(setup.get_nanbox_f64(), args.as_ptr(), 1) + let receiver = if is_function { + undefined() + } else { + plugin.get_nanbox_f64() + }; + crate::closure::native_call_value_this( + setup.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver), + args.as_ptr(), + 1, + ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); let result = match result { Ok(value) => value, Err(error) => crate::exception::js_throw(error), @@ -127,17 +135,25 @@ mod tests { static SETUP_CALLS: AtomicU32 = AtomicU32::new(0); static LOADER_CALLS: AtomicU32 = AtomicU32::new(0); - fn closure(func: extern "C" fn(*const ClosureHeader, f64) -> f64) -> f64 { + fn closure(func: crate::closure::body_call::js_body_fn_ty!(a)) -> f64 { js_register_closure_arity(func as *const u8, 1); js_nanbox_pointer(js_closure_alloc(func as *const u8, 0) as i64) } - extern "C" fn loader(_closure: *const ClosureHeader, _args: f64) -> f64 { + extern "C" fn loader( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _args: f64, + ) -> f64 { LOADER_CALLS.fetch_add(1, Ordering::SeqCst); undefined() } - extern "C" fn setup(_closure: *const ClosureHeader, build: f64) -> f64 { + extern "C" fn setup( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + build: f64, + ) -> f64 { SETUP_CALLS.fetch_add(1, Ordering::SeqCst); let scope = RuntimeHandleScope::new(); let build = scope.root_nanbox_f64(build); @@ -153,7 +169,14 @@ mod tests { ] { let hook = object_field(build.get_nanbox_f64(), name).expect("builder hook"); let args = [undefined(), callback.get_nanbox_f64()]; - let result = unsafe { crate::closure::js_native_call_value(hook, args.as_ptr(), 2) }; + let result = unsafe { + crate::closure::js_native_call_value( + hook, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 2, + ) + }; assert_eq!(result.to_bits(), undefined().to_bits()); } assert!(object_field(build.get_nanbox_f64(), b"config").is_some()); @@ -193,7 +216,11 @@ mod tests { assert_eq!(LOADER_CALLS.load(Ordering::SeqCst), 0); } - extern "C" fn async_setup(_closure: *const ClosureHeader, _build: f64) -> f64 { + extern "C" fn async_setup( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _build: f64, + ) -> f64 { js_nanbox_pointer(crate::promise::js_promise_resolved(42.0) as i64) } @@ -219,7 +246,14 @@ mod tests { let raw = JSValue::from_bits(value.get_nanbox_f64().to_bits()).as_pointer::(); let clear = crate::closure::closure_get_dynamic_prop(raw as usize, "clearAll"); assert!(!crate::fs::extract_closure_ptr(clear).is_null()); - let result = unsafe { crate::closure::js_native_call_value(clear, std::ptr::null(), 0) }; + let result = unsafe { + crate::closure::js_native_call_value( + clear, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(result.to_bits(), undefined().to_bits()); } } diff --git a/crates/perry-runtime/src/bun_compat/self_global.rs b/crates/perry-runtime/src/bun_compat/self_global.rs index 40490a5d00..8d77af1a48 100644 --- a/crates/perry-runtime/src/bun_compat/self_global.rs +++ b/crates/perry-runtime/src/bun_compat/self_global.rs @@ -49,11 +49,15 @@ pub(super) fn install_once() { ); } -extern "C" fn get_self(_closure: *const ClosureHeader) -> f64 { +extern "C" fn get_self(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { crate::object::js_get_global_this() } -extern "C" fn set_self(_closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn set_self( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { // Like Bun, even a borrowed setter replaces the current realm's property. // Once replaced, subsequent assignments are ordinary writable data stores. let _no_move = crate::gc::GcSuppressScope::new(); @@ -96,7 +100,7 @@ mod tests { global_value.to_bits() ); - set_self(std::ptr::null(), 42.0); + set_self(std::ptr::null(), crate::closure::JsThis::UNDEFINED, 42.0); assert!(crate::object::get_accessor_descriptor(global as usize, "self").is_none()); super::super::js_set_bun_platform(); assert_eq!( diff --git a/crates/perry-runtime/src/bun_compat/spawn.rs b/crates/perry-runtime/src/bun_compat/spawn.rs index 2ba472cecf..773490f969 100644 --- a/crates/perry-runtime/src/bun_compat/spawn.rs +++ b/crates/perry-runtime/src/bun_compat/spawn.rs @@ -72,7 +72,12 @@ fn call_value(callback: f64, args: &[f64]) -> f64 { let args = scope.root_nanbox_f64_slice(args); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&args); unsafe { - crate::closure::js_native_call_value(callback.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + callback.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) } } @@ -85,15 +90,15 @@ fn call_method(receiver: f64, name: &[u8], args: &[f64]) -> f64 { } let method = scope.root_nanbox_f64(method); let args = scope.root_nanbox_f64_slice(args); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - receiver.get_nanbox_f64(), - )); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&args); - let result = unsafe { - crate::closure::js_native_call_value(method.get_nanbox_f64(), args.as_ptr(), args.len()) - }; - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ) + } } fn closure_with_captures(func: *const u8, arity: u32, captures: &[f64]) -> f64 { @@ -179,19 +184,31 @@ fn clone_options(options: f64) -> f64 { // Readable / writable stream facade // ------------------------------------------------------------------------- -extern "C" fn bun_readable_text(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_readable_text( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::node_submodules::consume_text(captured_at(closure, 0)) } -extern "C" fn bun_readable_json(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_readable_json( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::node_submodules::consume_json(captured_at(closure, 0)) } -extern "C" fn bun_readable_array_buffer(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_readable_array_buffer( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::node_submodules::consume_array_buffer(captured_at(closure, 0)) } -extern "C" fn bun_readable_bytes(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_readable_bytes( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::node_submodules::consume_bytes(captured_at(closure, 0)) } @@ -220,7 +237,11 @@ fn decorate_readable(stream: f64) -> f64 { stream.get_nanbox_f64() } -extern "C" fn bun_sink_flush(_closure: *const ClosureHeader, _wait: f64) -> f64 { +extern "C" fn bun_sink_flush( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _wait: f64, +) -> f64 { 0.0 } @@ -394,7 +415,12 @@ fn normalized_options(options: f64) -> Result<(f64, Vec), f64> { // Non-PTY Subprocess facade // ------------------------------------------------------------------------- -extern "C" fn bun_subprocess_exit(closure: *const ClosureHeader, code: f64, signal: f64) -> f64 { +extern "C" fn bun_subprocess_exit( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + code: f64, + signal: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let subprocess = scope.root_nanbox_f64(captured_at(closure, 0)); let code = scope.root_nanbox_f64(code); @@ -570,7 +596,11 @@ fn terminal_set_refed(terminal: f64, refed: bool) { ); } -extern "C" fn bun_terminal_write(closure: *const ClosureHeader, data: f64) -> f64 { +extern "C" fn bun_terminal_write( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + data: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); let data = scope.root_nanbox_f64(data); @@ -589,7 +619,12 @@ extern "C" fn bun_terminal_write(closure: *const ClosureHeader, data: f64) -> f6 0.0 } -extern "C" fn bun_terminal_resize(closure: *const ClosureHeader, columns: f64, rows: f64) -> f64 { +extern "C" fn bun_terminal_resize( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + columns: f64, + rows: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); let columns = number_i32(columns).unwrap_or(0); @@ -608,7 +643,11 @@ extern "C" fn bun_terminal_resize(closure: *const ClosureHeader, columns: f64, r cp_undefined() } -extern "C" fn bun_terminal_set_raw_mode(closure: *const ClosureHeader, enabled: f64) -> f64 { +extern "C" fn bun_terminal_set_raw_mode( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + enabled: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); let enabled = crate::value::js_is_truthy(enabled) != 0; @@ -626,7 +665,10 @@ extern "C" fn bun_terminal_set_raw_mode(closure: *const ClosureHeader, enabled: cp_undefined() } -extern "C" fn bun_terminal_ref(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_terminal_ref( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); #[cfg(unix)] @@ -634,7 +676,10 @@ extern "C" fn bun_terminal_ref(closure: *const ClosureHeader) -> f64 { terminal.get_nanbox_f64() } -extern "C" fn bun_terminal_unref(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_terminal_unref( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); #[cfg(unix)] @@ -642,7 +687,10 @@ extern "C" fn bun_terminal_unref(closure: *const ClosureHeader) -> f64 { terminal.get_nanbox_f64() } -extern "C" fn bun_terminal_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_terminal_close( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); if bool_field(terminal.get_nanbox_f64(), b"closed") { @@ -740,7 +788,11 @@ pub extern "C" fn js_bun_terminal_new(options: f64) -> f64 { } #[cfg(unix)] -extern "C" fn bun_terminal_data_bridge(closure: *const ClosureHeader, text: f64) -> f64 { +extern "C" fn bun_terminal_data_bridge( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + text: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let terminal = scope.root_nanbox_f64(captured_at(closure, 0)); let text = scope.root_nanbox_f64(text); @@ -758,7 +810,11 @@ extern "C" fn bun_terminal_data_bridge(closure: *const ClosureHeader, text: f64) } #[cfg(unix)] -extern "C" fn bun_pty_subprocess_exit(closure: *const ClosureHeader, payload: f64) -> f64 { +extern "C" fn bun_pty_subprocess_exit( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + payload: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let subprocess = scope.root_nanbox_f64(captured_at(closure, 0)); let terminal = scope.root_nanbox_f64(captured_at(closure, 1)); @@ -816,7 +872,11 @@ extern "C" fn bun_pty_subprocess_exit(closure: *const ClosureHeader, payload: f6 } #[cfg(unix)] -extern "C" fn bun_pty_subprocess_kill(closure: *const ClosureHeader, signal: f64) -> f64 { +extern "C" fn bun_pty_subprocess_kill( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + signal: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let subprocess = scope.root_nanbox_f64(captured_at(closure, 0)); let terminal = scope.root_nanbox_f64(cp_get_field(subprocess.get_nanbox_f64(), b"terminal")); @@ -841,7 +901,10 @@ extern "C" fn bun_pty_subprocess_kill(closure: *const ClosureHeader, signal: f64 } #[cfg(unix)] -extern "C" fn bun_pty_subprocess_ref(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_pty_subprocess_ref( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let subprocess = scope.root_nanbox_f64(captured_at(closure, 0)); terminal_set_refed(cp_get_field(subprocess.get_nanbox_f64(), b"terminal"), true); @@ -849,7 +912,10 @@ extern "C" fn bun_pty_subprocess_ref(closure: *const ClosureHeader) -> f64 { } #[cfg(unix)] -extern "C" fn bun_pty_subprocess_unref(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_pty_subprocess_unref( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let subprocess = scope.root_nanbox_f64(captured_at(closure, 0)); terminal_set_refed( @@ -860,8 +926,11 @@ extern "C" fn bun_pty_subprocess_unref(closure: *const ClosureHeader) -> f64 { } #[cfg(unix)] -extern "C" fn bun_pty_subprocess_dispose(closure: *const ClosureHeader) -> f64 { - let _ = bun_pty_subprocess_kill(closure, cp_undefined()); +extern "C" fn bun_pty_subprocess_dispose( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let _ = bun_pty_subprocess_kill(closure, this, cp_undefined()); cp_undefined() } diff --git a/crates/perry-runtime/src/bun_ffi/callback.rs b/crates/perry-runtime/src/bun_ffi/callback.rs index f57f0e21d6..8f372d37e9 100644 --- a/crates/perry-runtime/src/bun_ffi/callback.rs +++ b/crates/perry-runtime/src/bun_ffi/callback.rs @@ -311,7 +311,10 @@ fn validate_callback_types(ret: u8, args: &[u8]) { } } -extern "C" fn callback_close_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn callback_close_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let index = crate::closure::js_closure_get_capture_bits(closure, 0) as usize; if let Some(record) = CALLBACKS.lock().unwrap().get_mut(index) { close_record(record); @@ -569,6 +572,7 @@ unsafe fn invoke_record( match crate::exception::js_call_catching(|| { crate::closure::js_native_call_value( callback.get_nanbox_f64(), + crate::closure::plain_call_receiver(), arguments.as_ptr(), arguments.len(), ) diff --git a/crates/perry-runtime/src/bun_ffi/dlopen.rs b/crates/perry-runtime/src/bun_ffi/dlopen.rs index 55ebca122b..a285c2810b 100644 --- a/crates/perry-runtime/src/bun_ffi/dlopen.rs +++ b/crates/perry-runtime/src/bun_ffi/dlopen.rs @@ -214,7 +214,11 @@ unsafe fn invoke_from_closure(closure: *const ClosureHeader, js_args: &[f64]) -> macro_rules! sym_thunk { ($name:ident $(, $a:ident)*) => { - extern "C" fn $name(closure: *const ClosureHeader $(, $a: f64)*) -> f64 { + extern "C" fn $name( + closure: *const ClosureHeader, + _this: crate::closure::JsThis + $(, $a: f64)* + ) -> f64 { let args = [$($a),*]; unsafe { invoke_from_closure(closure, &args) } } @@ -341,7 +345,7 @@ fn sym_thunk_for(arity: usize) -> *const u8 { } } -extern "C" fn close_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn close_thunk(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { let lib_index = crate::closure::js_closure_get_capture_bits(closure, 0) as usize; close_library_index(lib_index); super::undefined() @@ -374,7 +378,10 @@ fn index_closure(func: *const u8, index: usize, arity: u32, name: &str) -> f64 { crate::value::js_nanbox_pointer(closure as i64) } -extern "C" fn noop_close_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn noop_close_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { super::undefined() } @@ -1041,6 +1048,7 @@ unsafe fn parse_node_definition( extern "C" fn node_register_callback_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, signature: f64, callback: f64, ) -> f64 { @@ -1048,7 +1056,11 @@ extern "C" fn node_register_callback_thunk( unsafe { super::callback::node_register_callback_value(lib, signature, callback) } } -extern "C" fn node_unregister_callback_thunk(_closure: *const ClosureHeader, pointer: f64) -> f64 { +extern "C" fn node_unregister_callback_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + pointer: f64, +) -> f64 { unsafe { super::callback::node_unregister_callback_value(pointer) } } diff --git a/crates/perry-runtime/src/bun_ffi/read.rs b/crates/perry-runtime/src/bun_ffi/read.rs index 27602374da..b695ed73bc 100644 --- a/crates/perry-runtime/src/bun_ffi/read.rs +++ b/crates/perry-runtime/src/bun_ffi/read.rs @@ -104,7 +104,12 @@ unsafe fn read_value(kind: u8, pointer: f64, offset: f64) -> f64 { macro_rules! reader { ($name:ident, $kind:expr) => { - extern "C" fn $name(_closure: *const ClosureHeader, pointer: f64, offset: f64) -> f64 { + extern "C" fn $name( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + pointer: f64, + offset: f64, + ) -> f64 { unsafe { read_value($kind, pointer, offset) } } }; diff --git a/crates/perry-runtime/src/child_process/builder.rs b/crates/perry-runtime/src/child_process/builder.rs index caf1ca631e..159a1dbc9e 100644 --- a/crates/perry-runtime/src/child_process/builder.rs +++ b/crates/perry-runtime/src/child_process/builder.rs @@ -1,8 +1,6 @@ use super::*; -use crate::closure::{ - js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_arity, ClosureHeader, -}; +use crate::closure::{js_closure_alloc, js_closure_set_capture_ptr, js_register_closure_arity}; use crate::object::{js_object_alloc_with_shape, js_object_set_field, ObjectHeader}; use crate::value::JSValue; @@ -16,24 +14,24 @@ pub(crate) const CP_WRITABLE_SHAPE_ID: u32 = 0x7FFF_FD80; pub(crate) type CpFn = unsafe extern "C" fn(); #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cp_cast0(f: extern "C" fn(*const ClosureHeader) -> f64) -> CpFn { +pub(crate) fn cp_cast0(f: crate::closure::body_call::js_body_fn_ty!()) -> CpFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cp_cast1(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> CpFn { +pub(crate) fn cp_cast1(f: crate::closure::body_call::js_body_fn_ty!(a)) -> CpFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cp_cast2(f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64) -> CpFn { +pub(crate) fn cp_cast2(f: crate::closure::body_call::js_body_fn_ty!(a, a)) -> CpFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] /// Erase a three-argument native method to the common child-process method ABI. -pub(crate) fn cp_cast3(f: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64) -> CpFn { +pub(crate) fn cp_cast3(f: crate::closure::body_call::js_body_fn_ty!(a, a, a)) -> CpFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cp_cast4(f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64) -> CpFn { +pub(crate) fn cp_cast4(f: crate::closure::body_call::js_body_fn_ty!(a, a, a, a)) -> CpFn { unsafe { std::mem::transmute(f) } } diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs index 581a9b59ce..43b111b50c 100644 --- a/crates/perry-runtime/src/child_process/emitter.rs +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -2,9 +2,8 @@ use super::*; use crate::closure::{ js_closure_alloc, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_native_call_value, - js_register_closure_arity, ClosureHeader, + js_register_closure_arity, native_call_value_this, ClosureHeader, JsThis, }; -use crate::object::js_implicit_this_set; use crate::string::js_string_from_bytes; use crate::value::JSValue; @@ -54,9 +53,6 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { let key = cp_listener_key(event); let mut i: u32 = 0; let mut fired = false; - let this_scope = crate::gc::RuntimeHandleScope::new(); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let arr = match cp_array_ptr(cp_get_field(target.get_nanbox_f64(), &key)) { Some(a) => a, @@ -66,12 +62,15 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - js_implicit_this_set(target.get_nanbox_f64()); let current_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&args); unsafe { - let _ = js_native_call_value(cb, current_args.as_ptr(), current_args.len()); + let _ = native_call_value_this( + cb, + JsThis::from_f64(target.get_nanbox_f64()), + current_args.as_ptr(), + current_args.len(), + ); } - js_implicit_this_set(prev.get_nanbox_f64()); fired = true; i += 1; } @@ -111,17 +110,23 @@ pub(crate) fn cp_readable_end(stream: f64) { // ----- method bodies (each receives the closure; slot 0 = host `this`) ----- -pub(crate) extern "C" fn cp_method_on(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { - let this = cp_this(closure); +pub(crate) extern "C" fn cp_method_on( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + event: f64, + cb: f64, +) -> f64 { + let this = cp_this(_this, closure); cp_register(this, event, cb); this } pub(crate) extern "C" fn cp_method_emit( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event: f64, arg: f64, ) -> f64 { - let this = cp_this(closure); + let this = cp_this(_this, closure); let name = match cp_value_to_string(event) { Some(n) => n, None => return TAG_FALSE_F64, @@ -132,17 +137,27 @@ pub(crate) extern "C" fn cp_method_emit( TAG_FALSE_F64 } } -pub(crate) extern "C" fn cp_method_this0(closure: *const ClosureHeader) -> f64 { - cp_this(closure) +pub(crate) extern "C" fn cp_method_this0( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + cp_this(this, closure) } -pub(crate) extern "C" fn cp_method_this1(closure: *const ClosureHeader, _a: f64) -> f64 { - cp_this(closure) +pub(crate) extern "C" fn cp_method_this1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _a: f64, +) -> f64 { + cp_this(this, closure) } /// Keep a live child attached to the event loop. Calls are idempotent, matching /// Node and Bun's process-handle contract. -pub(crate) extern "C" fn cp_method_ref(closure: *const ClosureHeader) -> f64 { - let this = cp_this(closure); +pub(crate) extern "C" fn cp_method_ref( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let this = cp_this(_this, closure); if let Some(handle) = cp_handle_of(this) { reactor::cp_live_set_refed(handle, true); } @@ -152,8 +167,11 @@ pub(crate) extern "C" fn cp_method_ref(closure: *const ClosureHeader) -> f64 { /// Let the program terminate while this child continues running. The reactor /// still pumps and roots the child whenever another handle keeps the loop /// alive; only the active-handle accounting changes. -pub(crate) extern "C" fn cp_method_unref(closure: *const ClosureHeader) -> f64 { - let this = cp_this(closure); +pub(crate) extern "C" fn cp_method_unref( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let this = cp_this(_this, closure); if let Some(handle) = cp_handle_of(this) { reactor::cp_live_set_refed(handle, false); } @@ -163,23 +181,32 @@ pub(crate) extern "C" fn cp_method_unref(closure: *const ClosureHeader) -> f64 { /// Low-level `new ChildProcess().spawn(options)` validation boundary. Node's /// constructor is public even though normal callers use `spawn()`; keep the /// constructed idle object inert after its setup checks. -pub(crate) extern "C" fn cp_method_child_spawn(closure: *const ClosureHeader, options: f64) -> f64 { +pub(crate) extern "C" fn cp_method_child_spawn( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + options: f64, +) -> f64 { crate::child_process::validate::cp_validate_child_process_spawn(options); - cp_this(closure) + cp_this(this, closure) } /// `child.stdout.setEncoding(encoding)` switches emitted chunks from Buffers /// to decoded strings. The reactor reads this field when it delivers data. pub(crate) extern "C" fn cp_method_set_encoding( closure: *const ClosureHeader, + _this: crate::closure::JsThis, encoding: f64, ) -> f64 { - let this = cp_this(closure); + let this = cp_this(_this, closure); cp_set_field(this, b"__cpEncoding", encoding); this } -pub(crate) extern "C" fn cp_method_kill(closure: *const ClosureHeader, signal: f64) -> f64 { +pub(crate) extern "C" fn cp_method_kill( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + signal: f64, +) -> f64 { cp_validate_signal(signal); - let this = cp_this(closure); + let this = cp_this(_this, closure); cp_set_field(this, b"killed", TAG_TRUE_F64); // #1934: signal the live child if one is still running. `__cpHandle` is the // reactor registry key set by `spawn`. Returns true when the signal was @@ -194,8 +221,11 @@ pub(crate) extern "C" fn cp_method_kill(closure: *const ClosureHeader, signal: f /// `child[Symbol.dispose]()` — Node aliases this to `kill()` and returns /// `undefined`, so `using child = spawn(...)` terminates the subprocess on /// scope exit. #2556. -pub(crate) extern "C" fn cp_method_dispose(closure: *const ClosureHeader) -> f64 { - let _ = cp_method_kill(closure, cp_undefined()); +pub(crate) extern "C" fn cp_method_dispose( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let _ = cp_method_kill(closure, this, cp_undefined()); cp_undefined() } pub(crate) fn js_fork_child(args_len: usize) -> f64 { @@ -211,10 +241,11 @@ pub(crate) fn js_fork_child(args_len: usize) -> f64 { /// #1780. pub(crate) extern "C" fn cp_method_remove_listener( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { - let this = cp_this(closure); + let this = cp_this(_this, closure); if let Some(name) = cp_value_to_string(event) { let key = cp_listener_key(&name); if let Some(arr) = cp_array_ptr(cp_get_field(this, &key)) { @@ -236,9 +267,10 @@ pub(crate) extern "C" fn cp_method_remove_listener( /// `__cpL_*` list when called with no event. #1780. pub(crate) extern "C" fn cp_method_remove_all_listeners( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event: f64, ) -> f64 { - let this = cp_this(closure); + let this = cp_this(_this, closure); if let Some(name) = cp_value_to_string(event) { let key = cp_listener_key(&name); let empty = crate::array::js_array_alloc(0); @@ -263,7 +295,11 @@ pub(crate) extern "C" fn cp_method_remove_all_listeners( this } -pub(crate) extern "C" fn cp_method_read(_closure: *const ClosureHeader, _n: f64) -> f64 { +pub(crate) extern "C" fn cp_method_read( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _n: f64, +) -> f64 { TAG_NULL_F64 } @@ -272,8 +308,12 @@ pub(crate) extern "C" fn cp_method_read(_closure: *const ClosureHeader, _n: f64) /// `process.stdout`/`process.stderr`; those stream objects expose no `end` /// method, so the lookup-miss skip below matches that naturally. Returns /// `dest` (Node returns the destination for chaining). -pub(crate) extern "C" fn cp_method_pipe(closure: *const ClosureHeader, dest: f64) -> f64 { - let this = cp_this(closure); +pub(crate) extern "C" fn cp_method_pipe( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + dest: f64, +) -> f64 { + let this = cp_this(_this, closure); js_register_closure_arity(cp_pipe_data_thunk as *const u8, 1); js_register_closure_arity(cp_pipe_end_thunk as *const u8, 0); @@ -297,17 +337,19 @@ pub(crate) extern "C" fn cp_method_pipe(closure: *const ClosureHeader, dest: f64 } /// Pipe `data` forwarder: slot 0 = the destination; call `dest.write(chunk)`. -pub(crate) extern "C" fn cp_pipe_data_thunk(closure: *const ClosureHeader, chunk: f64) -> f64 { +pub(crate) extern "C" fn cp_pipe_data_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let write = cp_get_field(dest, b"write"); if !crate::fs::extract_closure_ptr(write).is_null() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(dest)); let args = [chunk]; unsafe { - let _ = js_native_call_value(write, args.as_ptr(), args.len()); + let _ = + native_call_value_this(write, JsThis::from_f64(dest), args.as_ptr(), args.len()); } - js_implicit_this_set(prev.get_nanbox_f64()); } cp_undefined() } @@ -315,17 +357,17 @@ pub(crate) extern "C" fn cp_pipe_data_thunk(closure: *const ClosureHeader, chunk /// Pipe `end` forwarder: slot 0 = the destination; call `dest.end()` when the /// destination has one (`process.stdout`/`process.stderr` do not — matching /// Node's doEnd exclusion for them). -pub(crate) extern "C" fn cp_pipe_end_thunk(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn cp_pipe_end_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let end = cp_get_field(dest, b"end"); if !crate::fs::extract_closure_ptr(end).is_null() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(dest)); let args = [cp_undefined()]; unsafe { - let _ = js_native_call_value(end, args.as_ptr(), 0); + let _ = native_call_value_this(end, JsThis::from_f64(dest), args.as_ptr(), 0); } - js_implicit_this_set(prev.get_nanbox_f64()); } cp_undefined() } @@ -355,11 +397,19 @@ fn cp_defer_stream_callback(callback: f64) { } /// Deliver a deferred child-stdin completion callback captured in slot zero. -pub(crate) extern "C" fn cp_stream_callback_thunk(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn cp_stream_callback_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let callback = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let args: [f64; 0] = []; unsafe { - let _ = js_native_call_value(callback, args.as_ptr(), 0); + let _ = js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 0, + ); } cp_undefined() } @@ -379,11 +429,12 @@ pub(crate) extern "C" fn cp_stream_callback_thunk(closure: *const ClosureHeader) /// deferred to the next turn as before. pub(crate) extern "C" fn cp_method_stdin_write( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, arg2: f64, arg3: f64, ) -> f64 { - let this = cp_this(closure); + let this = cp_this(_this, closure); let scope = crate::gc::RuntimeHandleScope::new(); let callback = cp_stream_callback(arg2, arg3).map(|cb| scope.root_nanbox_f64(cb)); let mut result = TAG_TRUE_F64; @@ -433,6 +484,7 @@ pub(crate) extern "C" fn cp_method_stdin_write( /// option forms are otherwise no-ops here, matching the prior behavior). pub(crate) extern "C" fn cp_method_send( closure: *const ClosureHeader, + _this: crate::closure::JsThis, message: f64, a2: f64, a3: f64, @@ -453,7 +505,7 @@ pub(crate) extern "C" fn cp_method_send( "ERR_INVALID_ARG_TYPE", ); } - let this = cp_this(closure); + let this = cp_this(_this, closure); // The callback is the last argument when it is a function. dispatch pads // missing slots with `undefined`, so scan slots 4→2 for a closure. @@ -502,7 +554,10 @@ pub(crate) fn cp_defer_send_callback(cb: f64, ok: bool) { /// Deferred `send` callback body. Slot 0 = the user callback; slot 1 = the /// success flag. Invokes `callback(null)` on success or `callback(err)` with a /// Node-shaped `ERR_IPC_CHANNEL_CLOSED` error on failure. -pub(crate) extern "C" fn cp_send_callback_thunk(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn cp_send_callback_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let cb = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); if crate::fs::extract_closure_ptr(cb).is_null() { return cp_undefined(); @@ -515,7 +570,14 @@ pub(crate) extern "C" fn cp_send_callback_thunk(closure: *const ClosureHeader) - cp_channel_closed_error() }; let args = [arg]; - unsafe { js_native_call_value(cb, args.as_ptr(), args.len()) }; + unsafe { + js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + }; cp_undefined() } @@ -530,8 +592,11 @@ pub(crate) fn cp_channel_closed_error() -> f64 { /// `child.disconnect()` — close the IPC channel (#1933). Flips `connected` to /// `false`, `channel` to `null`, and emits a `disconnect` event. -pub(crate) extern "C" fn cp_method_disconnect(closure: *const ClosureHeader) -> f64 { - let this = cp_this(closure); +pub(crate) extern "C" fn cp_method_disconnect( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let this = cp_this(_this, closure); if let Some(handle) = cp_handle_of(this) { reactor::cp_ipc_disconnect(handle); } @@ -544,7 +609,10 @@ pub(crate) extern "C" fn cp_method_disconnect(closure: *const ClosureHeader) -> cp_undefined() } -extern "C" fn cp_disconnect_emit_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn cp_disconnect_emit_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let child = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); cp_emit(child, "disconnect", &[]); cp_undefined() @@ -555,11 +623,12 @@ extern "C" fn cp_disconnect_emit_thunk(closure: *const ClosureHeader) -> f64 { /// optional callback asynchronously (#1934 / #8512). pub(crate) extern "C" fn cp_method_stdin_end( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, arg2: f64, arg3: f64, ) -> f64 { - let this = cp_this(closure); + let this = cp_this(_this, closure); let scope = crate::gc::RuntimeHandleScope::new(); let callback = cp_stream_callback(arg2, arg3) .or_else(|| (!crate::fs::extract_closure_ptr(chunk).is_null()).then_some(chunk)) diff --git a/crates/perry-runtime/src/child_process/emitter/relocation_tests.rs b/crates/perry-runtime/src/child_process/emitter/relocation_tests.rs index 475a197c24..f87d5edf34 100644 --- a/crates/perry-runtime/src/child_process/emitter/relocation_tests.rs +++ b/crates/perry-runtime/src/child_process/emitter/relocation_tests.rs @@ -2,7 +2,11 @@ use super::*; /// Simulate the runtime-root rewrite performed by a moving collection inside /// the first listener, without requiring a native stack map in a Rust test. -extern "C" fn relocate(closure: *const ClosureHeader, _arg: f64) -> f64 { +extern "C" fn relocate( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { for pair in 0..2 { let source = js_closure_get_capture_ptr(closure, pair * 2) as *mut u8; let destination = js_closure_get_capture_ptr(closure, pair * 2 + 1) as *mut u8; @@ -16,8 +20,12 @@ extern "C" fn relocate(closure: *const ClosureHeader, _arg: f64) -> f64 { cp_undefined() } -extern "C" fn observe(_closure: *const ClosureHeader, arg: f64) -> f64 { - let target = crate::object::js_implicit_this_get(); +extern "C" fn observe( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { + let target = this.as_f64(); cp_set_field(target, b"seen", arg); cp_undefined() } diff --git a/crates/perry-runtime/src/child_process/exec.rs b/crates/perry-runtime/src/child_process/exec.rs index 96f8e15768..2aeca9dea7 100644 --- a/crates/perry-runtime/src/child_process/exec.rs +++ b/crates/perry-runtime/src/child_process/exec.rs @@ -459,6 +459,7 @@ pub extern "C" fn js_child_process_exec_file_sync( /// the error and reject with it. Arity 3. #4912/#1857. extern "C" fn cp_promise_settle_cb( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, stdout: f64, stderr: f64, @@ -517,7 +518,12 @@ fn cp_promisified_run( crate::value::js_nanbox_pointer(promise as i64) } -extern "C" fn cp_promisified_exec(_closure: *const ClosureHeader, cmd_val: f64, opts: f64) -> f64 { +extern "C" fn cp_promisified_exec( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + cmd_val: f64, + opts: f64, +) -> f64 { validate::cp_validate_command(cmd_val, "command"); let cmd = cp_value_to_string(cmd_val).unwrap_or_default(); #[cfg(unix)] @@ -549,6 +555,7 @@ extern "C" fn cp_promisified_exec(_closure: *const ClosureHeader, cmd_val: f64, extern "C" fn cp_promisified_exec_file( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, file_val: f64, args_val: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/child_process/failed_spawn.rs b/crates/perry-runtime/src/child_process/failed_spawn.rs index d15ec5e5c7..0679ea9d77 100644 --- a/crates/perry-runtime/src/child_process/failed_spawn.rs +++ b/crates/perry-runtime/src/child_process/failed_spawn.rs @@ -5,10 +5,13 @@ use super::*; /// Schedule close only after delivering the error. A close timer armed at spawn /// time can already be overdue before the error's setImmediate callback runs. /// Keep fork's separate failure contract unchanged by using this for spawn only. -pub(super) extern "C" fn emit_error_then_close(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn emit_error_then_close( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let cp = scope.root_nanbox_f64(cp_this(closure)); - reactor::cp_emit_spawn_error(closure); + let cp = scope.root_nanbox_f64(cp_this(this, closure)); + reactor::cp_emit_spawn_error(closure, this); let close = crate::closure::js_closure_alloc(reactor::cp_emit_spawn_close as *const u8, 1); crate::closure::js_closure_set_capture_ptr(close, 0, cp.get_nanbox_f64().to_bits() as i64); crate::timer::js_set_timeout_callback(close as i64, 1.0); diff --git a/crates/perry-runtime/src/child_process/reactor.rs b/crates/perry-runtime/src/child_process/reactor.rs index 2ec9071b8a..c8bdd961e9 100644 --- a/crates/perry-runtime/src/child_process/reactor.rs +++ b/crates/perry-runtime/src/child_process/reactor.rs @@ -63,6 +63,9 @@ use streams::{ cp_pipe_from_child_stderr, cp_pipe_from_child_stdout, cp_pipe_from_file, cp_spawn_reader, CpPipe, }; +mod exec_async; +pub(super) use exec_async::{cp_defer_exec_callback, cp_exec_async}; +use exec_async::{cp_exec_accumulate, cp_exec_cb_thunk, cp_exec_fire_close}; /// Monotonic registry key for live children. static CP_NEXT_LIVE_ID: AtomicU64 = AtomicU64::new(1); @@ -620,7 +623,10 @@ fn cp_register_live_child_parts( handle } -extern "C" fn cp_abort_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn cp_abort_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let handle = js_closure_get_capture_ptr(closure, 0) as u64; cp_push_event(CpEvent::Abort { handle }); cp_undefined() @@ -1032,9 +1038,12 @@ pub extern "C" fn js_child_process_spawn_streams( /// Deferred single-`error` emit for the spawn/fork failure path. Slot 0 /// captures the ChildProcess value. -pub(super) extern "C" fn cp_emit_spawn_error(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn cp_emit_spawn_error( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let cp = scope.root_nanbox_f64(cp_this(closure)); + let cp = scope.root_nanbox_f64(cp_this(this, closure)); let err = scope.root_nanbox_f64(cp_get_field(cp.get_nanbox_f64(), b"__cpError")); if !JSValue::from_bits(err.get_nanbox_f64().to_bits()).is_undefined() { cp_emit(cp.get_nanbox_f64(), "error", &[err.get_nanbox_f64()]); @@ -1043,9 +1052,12 @@ pub(super) extern "C" fn cp_emit_spawn_error(closure: *const ClosureHeader) -> f cp_undefined() } -pub(super) extern "C" fn cp_emit_spawn_close(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn cp_emit_spawn_close( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let cp = scope.root_nanbox_f64(cp_this(closure)); + let cp = scope.root_nanbox_f64(cp_this(this, closure)); super::failed_spawn::finish_outputs(cp.get_nanbox_f64()); let code = cp_get_field(cp.get_nanbox_f64(), b"exitCode"); cp_emit(cp.get_nanbox_f64(), "close", &[code, TAG_NULL_F64]); @@ -1063,302 +1075,6 @@ pub(super) fn cp_register_reactor_arities() { crate::closure::js_register_closure_arity(cp_exec_cb_thunk as *const u8, 0); } -// ============================================================================ -// Async `exec` / `execFile` — run off the main thread, call back on a later -// tick (#4912). -// ============================================================================ - -/// Launch `command` (already shaped as `sh -c ` for `exec` or `file -/// args…` for `execFile`, with `cwd`/`env` applied) without blocking the main -/// thread, capturing stdout/stderr on background reader threads. When the child -/// exits, the pump fires `cb_val(err, stdout, stderr)` on a later event-loop -/// tick. A spawn failure (e.g. `ENOENT`) is reported the same way, deferred to -/// the next tick. Always returns `undefined` (the callback form's return). -pub(super) fn cp_exec_async( - mut command: Command, - cmd_str: String, - public_spawnfile: Option, - cb_val: f64, - run_options: CpRunOptions, - mode: CpOutput, -) -> f64 { - cp_register_arities(); - cp_register_reactor_arities(); - - // The program actually launched (`sh` for exec, the file for execFile) — - // Node's spawn-failure error keys `syscall`/`path`/message off this, not - // off the display command string. - let file = { - let program = command.get_program().to_string_lossy(); - #[cfg(unix)] - if program == "sh" { - "/bin/sh".to_string() - } else { - program.into_owned() - } - #[cfg(not(unix))] - program.into_owned() - }; - - // exec/execFile capture stdout+stderr and never feed stdin. - command.stdin(Stdio::null()); - command.stdout(Stdio::piped()); - command.stderr(Stdio::piped()); - - let timeout = run_options.timeout(); - let kill_signal = run_options.kill_signal(); - let stdout_obj = cp_build_readable(); - let stderr_obj = cp_build_readable(); - let methods: [(&str, CpFn); 11] = [ - ("on", cp_cast2(cp_method_on)), - ("once", cp_cast2(cp_method_on)), - ("addListener", cp_cast2(cp_method_on)), - ("prependListener", cp_cast2(cp_method_on)), - ("removeListener", cp_cast2(cp_method_remove_listener)), - ("off", cp_cast2(cp_method_remove_listener)), - ( - "removeAllListeners", - cp_cast1(cp_method_remove_all_listeners), - ), - ("emit", cp_cast2(cp_method_emit)), - ("kill", cp_cast1(cp_method_kill)), - ("ref", cp_cast0(cp_method_ref)), - ("unref", cp_cast0(cp_method_unref)), - ]; - let cp = cp_box_ptr(cp_build_object(&methods, CP_SHAPE_ID + methods.len() as u32) as *const u8); - cp_set_field(cp, b"stdout", stdout_obj); - cp_set_field(cp, b"stderr", stderr_obj); - cp_set_field(cp, b"stdin", TAG_NULL_F64); - let mut stdio = crate::array::js_array_alloc(3); - stdio = crate::array::js_array_push_f64(stdio, TAG_NULL_F64); - stdio = crate::array::js_array_push_f64(stdio, stdout_obj); - stdio = crate::array::js_array_push_f64(stdio, stderr_obj); - cp_set_field(cp, b"stdio", cp_box_ptr(stdio as *const u8)); - cp_set_field(cp, b"exitCode", TAG_NULL_F64); - cp_set_field(cp, b"signalCode", TAG_NULL_F64); - cp_set_field(cp, b"killed", TAG_FALSE_F64); - cp_set_field(cp, b"connected", TAG_FALSE_F64); - // `cp_command_for_program` may resolve a bare executable against PATH to - // keep macOS on `posix_spawn`, but Node exposes the caller's original - // `execFile` spelling through `ChildProcess.spawnfile`. Keep the resolved - // path above for launch-error metadata and publish the explicit spelling - // when that API supplied one. `exec` continues to expose its real shell. - cp_set_field( - cp, - b"spawnfile", - cp_box_string(public_spawnfile.as_deref().unwrap_or(&file)), - ); - - match command.spawn() { - Ok(mut child) => { - let pid = child.id(); - // Duplicate the process handle BEFORE `child` moves to the waiter - // thread — the kill paths act on it instead of the recyclable pid. - #[cfg(windows)] - let win_proc_handle = cp_win_dup_proc_handle(&child); - let stdout_pipe = child.stdout.take(); - let stderr_pipe = child.stderr.take(); - let stdout_open = stdout_pipe.is_some(); - let stderr_open = stderr_pipe.is_some(); - let handle = CP_NEXT_LIVE_ID.fetch_add(1, Ordering::SeqCst); - cp_set_field(cp, b"pid", pid as f64); - cp_set_field(cp, b"__cpHandle", handle as f64); - cp_set_field(stdout_obj, b"__cpHandle", handle as f64); - cp_set_field(stderr_obj, b"__cpHandle", handle as f64); - - let exec = Box::new(CpExecPending { - cb_bits: cb_val.to_bits(), - stdout: Vec::new(), - stderr: Vec::new(), - run_options, - mode, - cmd: cmd_str, - file, - exceeded: false, - timed_out: false, - }); - let (process_ids, pipe_ids) = cp_init_async_resources(cp, None, stdout_obj, stderr_obj); - - { - register_thread_exit_release(); - let mut guard = cp_live_lock(); - let map = guard.get_or_insert_with(HashMap::new); - map.insert( - handle, - LiveChild { - cp_bits: cp.to_bits(), - process_ids, - pipe_ids, - pipe_bits: [ - TAG_NULL_F64.to_bits(), - stdout_obj.to_bits(), - stderr_obj.to_bits(), - ], - pid: pid as i32, - stdin: None, - stdout_open, - stderr_open, - stdout_eof_pending: false, - extra_open: Vec::new(), - loop_streams: Vec::new(), - spawned: false, - exited: None, - exit_emitted: false, - closed: false, - refed: true, - ipc_send: None, - ipc_advanced: false, - abort_signal_bits: 0, - abort_listener_bits: 0, - abort_kill_signal: libc_sigterm(), - #[cfg(windows)] - win_kill_signal: None, - #[cfg(windows)] - win_proc_handle, - exec: Some(exec), - }, - ); - } - crate::stdlib_pump::register_runtime_pump(0, cp_reactor_pump_extern); - CP_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); - CP_REFED_COUNT.fetch_add(1, Ordering::SeqCst); - - if let Some(o) = stdout_pipe { - cp_spawn_reader(handle, cp_pipe_from_child_stdout(o), 1); - } - if let Some(e) = stderr_pipe { - cp_spawn_reader(handle, cp_pipe_from_child_stderr(e), 2); - } - let waiter: CpWaiter = Box::new(move || match child.wait() { - Ok(status) => { - #[cfg(unix)] - let signal = { - use std::os::unix::process::ExitStatusExt; - status.signal() - }; - #[cfg(not(unix))] - let signal: Option = None; - (status.code(), signal) - } - Err(_) => (Some(-1), None), - }); - cp_spawn_waiter( - handle, - waiter, - timeout.map(|timeout| (timeout, kill_signal)), - ); - crate::event_pump::js_notify_main_thread(); - cp - } - Err(e) => { - // Could not spawn at all (ENOENT, EACCES…). Build the same callback - // error the sync path produced and fire it on a later tick. - let run = CpRun { - stdout: Vec::new(), - stderr: Vec::new(), - stdout_piped: true, - stderr_piped: true, - code: None, - signal: None, - pid: None, - spawn_error: Some((super::cp_io_error_code(&e), e.to_string())), - run_error: None, - }; - let (err, out, errout) = - super::cp_exec_callback_args(&run, &run_options, &cmd_str, &file, &mode); - cp_defer_exec_callback(cb_val, err, out, errout); - cp - } - } -} - -/// Append a chunk to an exec child's captured stdout/stderr. Returns -/// `Some(kill_signal)` the first time the buffer overruns `maxBuffer`, so the -/// caller terminates the child (Node kills on a `maxBuffer` breach). Once -/// overrun, further chunks are dropped to bound memory — the one chunk that -/// crossed the limit keeps `len > maxBuffer`, which the output/error shapers -/// use to truncate and to name the offending stream. -fn cp_exec_accumulate(exec: &mut CpExecPending, stderr: bool, bytes: &[u8]) -> Option { - if exec.exceeded { - return None; - } - let max = exec.run_options.max_buffer; - let buf = if stderr { - &mut exec.stderr - } else { - &mut exec.stdout - }; - buf.extend_from_slice(bytes); - if buf.len() > max { - exec.exceeded = true; - return Some(exec.run_options.kill_signal()); - } - None -} - -/// Build the `CpRun` for a finished exec child and fire its callback. -fn cp_exec_fire_close(exec: Box, code: Option, signal: Option, pid: i32) { - let exec = *exec; - let run_error = if exec.exceeded { - Some(CpRunError::MaxBuffer) - } else if exec.timed_out { - Some(CpRunError::Timeout) - } else { - None - }; - // A timeout maps to Node's `(code: null, signal: killSignal)` shape. - let (code, signal) = if exec.timed_out { - (None, Some(exec.run_options.kill_signal())) - } else { - (code, signal) - }; - let run = CpRun { - stdout: exec.stdout, - stderr: exec.stderr, - stdout_piped: true, - stderr_piped: true, - code, - signal, - pid: Some(pid as u32), - spawn_error: None, - run_error, - }; - let (err, out, errout) = - super::cp_exec_callback_args(&run, &exec.run_options, &exec.cmd, &exec.file, &exec.mode); - let cb = crate::fs::extract_closure_ptr(f64::from_bits(exec.cb_bits)); - if !cb.is_null() { - crate::closure::js_closure_call3(cb, err, out, errout); - } -} - -/// Schedule a deferred `cb(err, stdout, stderr)` on the next `setImmediate` -/// macrotask — used for the empty-command, already-aborted, and spawn-failure -/// exec paths, which have no live child to ride the reactor close. The boxed -/// values are kept reachable by the immediate closure's captures. -pub(super) fn cp_defer_exec_callback(cb_val: f64, err: f64, stdout: f64, stderr: f64) { - cp_register_reactor_arities(); - let deferred = js_closure_alloc(cp_exec_cb_thunk as *const u8, 4); - js_closure_set_capture_ptr(deferred, 0, cb_val.to_bits() as i64); - js_closure_set_capture_ptr(deferred, 1, err.to_bits() as i64); - js_closure_set_capture_ptr(deferred, 2, stdout.to_bits() as i64); - js_closure_set_capture_ptr(deferred, 3, stderr.to_bits() as i64); - crate::timer::js_set_immediate_callback(deferred as i64); -} - -/// The deferred-callback thunk: slots 0..4 capture `cb`, `err`, `stdout`, -/// `stderr`. Takes no JS args. -extern "C" fn cp_exec_cb_thunk(closure: *const ClosureHeader) -> f64 { - let cb = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); - let err = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); - let out = f64::from_bits(js_closure_get_capture_ptr(closure, 2) as u64); - let errout = f64::from_bits(js_closure_get_capture_ptr(closure, 3) as u64); - let cbptr = crate::fs::extract_closure_ptr(cb); - if !cbptr.is_null() { - crate::closure::js_closure_call3(cbptr, err, out, errout); - } - cp_undefined() -} - // ============================================================================ // Main-thread pump // ============================================================================ diff --git a/crates/perry-runtime/src/child_process/reactor/exec_async.rs b/crates/perry-runtime/src/child_process/reactor/exec_async.rs new file mode 100644 index 0000000000..27bc4b2ef7 --- /dev/null +++ b/crates/perry-runtime/src/child_process/reactor/exec_async.rs @@ -0,0 +1,338 @@ +//! Async `exec`/`execFile` — run off the main thread, call back on a later +//! tick (#4912). +//! +//! Split out of `reactor.rs` only to keep that file under the repository's +//! 2000-line cap (`scripts/check_file_size.sh`); same subject as before, just +//! its own file (mirrors how `stdin.rs` / `streams.rs` were split out). + +use super::*; + +// ============================================================================ +// Async `exec` / `execFile` — run off the main thread, call back on a later +// tick (#4912). +// ============================================================================ + +/// Launch `command` (already shaped as `sh -c ` for `exec` or `file +/// args…` for `execFile`, with `cwd`/`env` applied) without blocking the main +/// thread, capturing stdout/stderr on background reader threads. When the child +/// exits, the pump fires `cb_val(err, stdout, stderr)` on a later event-loop +/// tick. A spawn failure (e.g. `ENOENT`) is reported the same way, deferred to +/// the next tick. Always returns `undefined` (the callback form's return). +pub(in crate::child_process) fn cp_exec_async( + mut command: Command, + cmd_str: String, + public_spawnfile: Option, + cb_val: f64, + run_options: CpRunOptions, + mode: CpOutput, +) -> f64 { + cp_register_arities(); + cp_register_reactor_arities(); + + // The program actually launched (`sh` for exec, the file for execFile) — + // Node's spawn-failure error keys `syscall`/`path`/message off this, not + // off the display command string. + let file = { + let program = command.get_program().to_string_lossy(); + #[cfg(unix)] + if program == "sh" { + "/bin/sh".to_string() + } else { + program.into_owned() + } + #[cfg(not(unix))] + program.into_owned() + }; + + // exec/execFile capture stdout+stderr and never feed stdin. + command.stdin(Stdio::null()); + command.stdout(Stdio::piped()); + command.stderr(Stdio::piped()); + + let timeout = run_options.timeout(); + let kill_signal = run_options.kill_signal(); + let stdout_obj = cp_build_readable(); + let stderr_obj = cp_build_readable(); + let methods: [(&str, CpFn); 11] = [ + ("on", cp_cast2(cp_method_on)), + ("once", cp_cast2(cp_method_on)), + ("addListener", cp_cast2(cp_method_on)), + ("prependListener", cp_cast2(cp_method_on)), + ("removeListener", cp_cast2(cp_method_remove_listener)), + ("off", cp_cast2(cp_method_remove_listener)), + ( + "removeAllListeners", + cp_cast1(cp_method_remove_all_listeners), + ), + ("emit", cp_cast2(cp_method_emit)), + ("kill", cp_cast1(cp_method_kill)), + ("ref", cp_cast0(cp_method_ref)), + ("unref", cp_cast0(cp_method_unref)), + ]; + let cp = cp_box_ptr(cp_build_object(&methods, CP_SHAPE_ID + methods.len() as u32) as *const u8); + cp_set_field(cp, b"stdout", stdout_obj); + cp_set_field(cp, b"stderr", stderr_obj); + cp_set_field(cp, b"stdin", TAG_NULL_F64); + let mut stdio = crate::array::js_array_alloc(3); + stdio = crate::array::js_array_push_f64(stdio, TAG_NULL_F64); + stdio = crate::array::js_array_push_f64(stdio, stdout_obj); + stdio = crate::array::js_array_push_f64(stdio, stderr_obj); + cp_set_field(cp, b"stdio", cp_box_ptr(stdio as *const u8)); + cp_set_field(cp, b"exitCode", TAG_NULL_F64); + cp_set_field(cp, b"signalCode", TAG_NULL_F64); + cp_set_field(cp, b"killed", TAG_FALSE_F64); + cp_set_field(cp, b"connected", TAG_FALSE_F64); + // `cp_command_for_program` may resolve a bare executable against PATH to + // keep macOS on `posix_spawn`, but Node exposes the caller's original + // `execFile` spelling through `ChildProcess.spawnfile`. Keep the resolved + // path above for launch-error metadata and publish the explicit spelling + // when that API supplied one. `exec` continues to expose its real shell. + cp_set_field( + cp, + b"spawnfile", + cp_box_string(public_spawnfile.as_deref().unwrap_or(&file)), + ); + + match command.spawn() { + Ok(mut child) => { + let pid = child.id(); + // Duplicate the process handle BEFORE `child` moves to the waiter + // thread — the kill paths act on it instead of the recyclable pid. + #[cfg(windows)] + let win_proc_handle = cp_win_dup_proc_handle(&child); + let stdout_pipe = child.stdout.take(); + let stderr_pipe = child.stderr.take(); + let stdout_open = stdout_pipe.is_some(); + let stderr_open = stderr_pipe.is_some(); + let handle = CP_NEXT_LIVE_ID.fetch_add(1, Ordering::SeqCst); + cp_set_field(cp, b"pid", pid as f64); + cp_set_field(cp, b"__cpHandle", handle as f64); + cp_set_field(stdout_obj, b"__cpHandle", handle as f64); + cp_set_field(stderr_obj, b"__cpHandle", handle as f64); + + let exec = Box::new(CpExecPending { + cb_bits: cb_val.to_bits(), + stdout: Vec::new(), + stderr: Vec::new(), + run_options, + mode, + cmd: cmd_str, + file, + exceeded: false, + timed_out: false, + }); + let (process_ids, pipe_ids) = cp_init_async_resources(cp, None, stdout_obj, stderr_obj); + + { + register_thread_exit_release(); + let mut guard = cp_live_lock(); + let map = guard.get_or_insert_with(HashMap::new); + map.insert( + handle, + LiveChild { + cp_bits: cp.to_bits(), + process_ids, + pipe_ids, + pipe_bits: [ + TAG_NULL_F64.to_bits(), + stdout_obj.to_bits(), + stderr_obj.to_bits(), + ], + pid: pid as i32, + stdin: None, + stdout_open, + stderr_open, + stdout_eof_pending: false, + extra_open: Vec::new(), + loop_streams: Vec::new(), + spawned: false, + exited: None, + exit_emitted: false, + closed: false, + refed: true, + ipc_send: None, + ipc_advanced: false, + abort_signal_bits: 0, + abort_listener_bits: 0, + abort_kill_signal: libc_sigterm(), + #[cfg(windows)] + win_kill_signal: None, + #[cfg(windows)] + win_proc_handle, + exec: Some(exec), + }, + ); + } + crate::stdlib_pump::register_runtime_pump(0, cp_reactor_pump_extern); + CP_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + CP_REFED_COUNT.fetch_add(1, Ordering::SeqCst); + + if let Some(o) = stdout_pipe { + cp_spawn_reader(handle, cp_pipe_from_child_stdout(o), 1); + } + if let Some(e) = stderr_pipe { + cp_spawn_reader(handle, cp_pipe_from_child_stderr(e), 2); + } + let waiter: CpWaiter = Box::new(move || match child.wait() { + Ok(status) => { + #[cfg(unix)] + let signal = { + use std::os::unix::process::ExitStatusExt; + status.signal() + }; + #[cfg(not(unix))] + let signal: Option = None; + (status.code(), signal) + } + Err(_) => (Some(-1), None), + }); + cp_spawn_waiter( + handle, + waiter, + timeout.map(|timeout| (timeout, kill_signal)), + ); + crate::event_pump::js_notify_main_thread(); + cp + } + Err(e) => { + // Could not spawn at all (ENOENT, EACCES…). Build the same callback + // error the sync path produced and fire it on a later tick. + let run = CpRun { + stdout: Vec::new(), + stderr: Vec::new(), + stdout_piped: true, + stderr_piped: true, + code: None, + signal: None, + pid: None, + spawn_error: Some((super::super::cp_io_error_code(&e), e.to_string())), + run_error: None, + }; + let (err, out, errout) = + super::super::cp_exec_callback_args(&run, &run_options, &cmd_str, &file, &mode); + cp_defer_exec_callback(cb_val, err, out, errout); + cp + } + } +} + +/// Append a chunk to an exec child's captured stdout/stderr. Returns +/// `Some(kill_signal)` the first time the buffer overruns `maxBuffer`, so the +/// caller terminates the child (Node kills on a `maxBuffer` breach). Once +/// overrun, further chunks are dropped to bound memory — the one chunk that +/// crossed the limit keeps `len > maxBuffer`, which the output/error shapers +/// use to truncate and to name the offending stream. +pub(super) fn cp_exec_accumulate( + exec: &mut CpExecPending, + stderr: bool, + bytes: &[u8], +) -> Option { + if exec.exceeded { + return None; + } + let max = exec.run_options.max_buffer; + let buf = if stderr { + &mut exec.stderr + } else { + &mut exec.stdout + }; + buf.extend_from_slice(bytes); + if buf.len() > max { + exec.exceeded = true; + return Some(exec.run_options.kill_signal()); + } + None +} + +/// Build the `CpRun` for a finished exec child and fire its callback. +pub(super) fn cp_exec_fire_close( + exec: Box, + code: Option, + signal: Option, + pid: i32, +) { + let exec = *exec; + let run_error = if exec.exceeded { + Some(CpRunError::MaxBuffer) + } else if exec.timed_out { + Some(CpRunError::Timeout) + } else { + None + }; + // A timeout maps to Node's `(code: null, signal: killSignal)` shape. + let (code, signal) = if exec.timed_out { + (None, Some(exec.run_options.kill_signal())) + } else { + (code, signal) + }; + let run = CpRun { + stdout: exec.stdout, + stderr: exec.stderr, + stdout_piped: true, + stderr_piped: true, + code, + signal, + pid: Some(pid as u32), + spawn_error: None, + run_error, + }; + let (err, out, errout) = super::super::cp_exec_callback_args( + &run, + &exec.run_options, + &exec.cmd, + &exec.file, + &exec.mode, + ); + let cb = crate::fs::extract_closure_ptr(f64::from_bits(exec.cb_bits)); + if !cb.is_null() { + crate::closure::js_closure_call3( + cb, + crate::closure::plain_call_receiver(), + err, + out, + errout, + ); + } +} + +/// Schedule a deferred `cb(err, stdout, stderr)` on the next `setImmediate` +/// macrotask — used for the empty-command, already-aborted, and spawn-failure +/// exec paths, which have no live child to ride the reactor close. The boxed +/// values are kept reachable by the immediate closure's captures. +pub(in crate::child_process) fn cp_defer_exec_callback( + cb_val: f64, + err: f64, + stdout: f64, + stderr: f64, +) { + cp_register_reactor_arities(); + let deferred = js_closure_alloc(cp_exec_cb_thunk as *const u8, 4); + js_closure_set_capture_ptr(deferred, 0, cb_val.to_bits() as i64); + js_closure_set_capture_ptr(deferred, 1, err.to_bits() as i64); + js_closure_set_capture_ptr(deferred, 2, stdout.to_bits() as i64); + js_closure_set_capture_ptr(deferred, 3, stderr.to_bits() as i64); + crate::timer::js_set_immediate_callback(deferred as i64); +} + +/// The deferred-callback thunk: slots 0..4 capture `cb`, `err`, `stdout`, +/// `stderr`. Takes no JS args. +pub(super) extern "C" fn cp_exec_cb_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let cb = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); + let err = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); + let out = f64::from_bits(js_closure_get_capture_ptr(closure, 2) as u64); + let errout = f64::from_bits(js_closure_get_capture_ptr(closure, 3) as u64); + let cbptr = crate::fs::extract_closure_ptr(cb); + if !cbptr.is_null() { + crate::closure::js_closure_call3( + cbptr, + crate::closure::plain_call_receiver(), + err, + out, + errout, + ); + } + cp_undefined() +} diff --git a/crates/perry-runtime/src/child_process/reactor/lifecycle_tests.rs b/crates/perry-runtime/src/child_process/reactor/lifecycle_tests.rs index 18713532da..cfb6fb1b52 100644 --- a/crates/perry-runtime/src/child_process/reactor/lifecycle_tests.rs +++ b/crates/perry-runtime/src/child_process/reactor/lifecycle_tests.rs @@ -50,7 +50,12 @@ fn native_process_fixture() { assert_eq!(tail, b"exit"); } -extern "C" fn event(closure: *const ClosureHeader, _a: f64, _b: f64) -> f64 { +extern "C" fn event( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _a: f64, + _b: f64, +) -> f64 { let id = crate::closure::js_closure_get_capture_f64(closure, 0) as usize; EVENTS.with(|e| { e.borrow_mut() @@ -59,7 +64,11 @@ extern "C" fn event(closure: *const ClosureHeader, _a: f64, _b: f64) -> f64 { cp_undefined() } -extern "C" fn data(_closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn data( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { OUTPUT.with(|o| o.borrow_mut().extend(cp_value_to_bytes(value))); cp_undefined() } diff --git a/crates/perry-runtime/src/child_process/reactor/stdin_drain.rs b/crates/perry-runtime/src/child_process/reactor/stdin_drain.rs index 2a6314485e..5593523db2 100644 --- a/crates/perry-runtime/src/child_process/reactor/stdin_drain.rs +++ b/crates/perry-runtime/src/child_process/reactor/stdin_drain.rs @@ -65,8 +65,12 @@ pub(super) fn cp_stdin_written(handle: u64, len: usize, broken: bool) { for callback in &handles { let args: [f64; 0] = []; unsafe { - let _ = - crate::closure::js_native_call_value(callback.get_nanbox_f64(), args.as_ptr(), 0); + let _ = crate::closure::js_native_call_value( + callback.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + 0, + ); } } } diff --git a/crates/perry-runtime/src/child_process/value_util.rs b/crates/perry-runtime/src/child_process/value_util.rs index 18dbb2f640..9b5ce088fc 100644 --- a/crates/perry-runtime/src/child_process/value_util.rs +++ b/crates/perry-runtime/src/child_process/value_util.rs @@ -1,10 +1,7 @@ use super::*; use crate::closure::{js_closure_get_capture_ptr, ClosureHeader}; -use crate::object::{ - js_implicit_this_get, js_object_get_field_by_name_f64, js_object_set_field_by_name, - ObjectHeader, -}; +use crate::object::{js_object_get_field_by_name_f64, js_object_set_field_by_name, ObjectHeader}; use crate::string::{js_string_from_bytes, StringHeader}; use crate::value::JSValue; @@ -20,9 +17,9 @@ pub(crate) fn cp_box_ptr(ptr: *const u8) -> f64 { /// Recover the host object value captured in closure slot 0 by `cp_build_object`. #[inline] -pub(crate) fn cp_this(closure: *const ClosureHeader) -> f64 { +pub(crate) fn cp_this(this: crate::closure::JsThis, closure: *const ClosureHeader) -> f64 { if closure.is_null() { - return js_implicit_this_get(); + return this.as_f64(); } f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64) } diff --git a/crates/perry-runtime/src/closure/alloc.rs b/crates/perry-runtime/src/closure/alloc.rs index 1e4695af69..4f0568f730 100644 --- a/crates/perry-runtime/src/closure/alloc.rs +++ b/crates/perry-runtime/src/closure/alloc.rs @@ -600,10 +600,10 @@ pub(crate) unsafe fn gc_capture_slot_range( } /// Allocate a closure with space for captured values. -/// The high bit of `capture_count` may contain CAPTURES_THIS_FLAG to indicate -/// that slot 0 is reserved for `this`. The flag is preserved in the header -/// for later use by `js_closure_unbind_this`, but the actual allocation size -/// uses only the lower 31 bits. +/// The two high bits of `capture_count` may carry `CAPTURES_THIS_FLAG` (the +/// LAST capture slot holds `this`) and `NO_THIS_REBIND_FLAG`. Both are +/// preserved in the stored header; the allocation size uses only the count +/// (`real_capture_count`). /// Returns pointer to ClosureHeader #[no_mangle] pub extern "C" fn js_closure_alloc(func_ptr: *const u8, capture_count: u32) -> *mut ClosureHeader { diff --git a/crates/perry-runtime/src/closure/body_call.rs b/crates/perry-runtime/src/closure/body_call.rs new file mode 100644 index 0000000000..61870b1aa1 --- /dev/null +++ b/crates/perry-runtime/src/closure/body_call.rs @@ -0,0 +1,169 @@ +//! THE funnel: the only Rust code that turns a JS function body's code +//! pointer into something callable. +//! +//! A *JS body* is native code a function object runs: a compiled closure +//! body, a value wrapper, or a native builtin installed as a function +//! object. Its native signature is +//! +//! ```text +//! double body(i64 callee, i64 this, double a0, double a1, ...) +//! ``` +//! +//! (`perry_abi::JS_BODY_*` names the parameter positions; the receiver is a +//! [`JsThis`]). Two other body kinds exist until they join the one body ABI, +//! and have their own macros: +//! +//! * METHOD bodies — class instance methods, constructors, instance +//! accessors: `double m(double this, double a0, ...)` +//! ([`js_method_body_call!`]); +//! * BARE bodies — static methods and static accessors, and the top-level +//! functions HIR lifts well-known hooks into (`static [Symbol.hasInstance]`, +//! `get [Symbol.toStringTag]`, whose receiver is an explicit parameter): +//! `double f(double a0, ...)` ([`js_bare_body_fn!`]). +//! +//! Every call site states the receiver and the JS arguments it passes and +//! nothing else, so the compiler finds every caller when the body ABI +//! changes. A `mem::transmute` to a body type anywhere else is refused by +//! `scripts/check_js_body_call_funnel.py`, and so is a native body definition +//! that does not declare the receiver. +//! +//! Over-application is safe and relied on: every supported calling +//! convention leaves the stack argument area to the caller, so a body that +//! declares N parameters reads only the first N (`wide_call.rs`). + +/// The receiver a JS body takes as its second native parameter +/// (`perry_abi::JS_BODY_THIS_PARAM`), defined once in perry-abi and shared +/// with perry-ffi. It is the ONLY way a body learns its receiver: a +/// method-style caller passes the receiver, a plain call +/// [`JsThis::UNDEFINED`]. There is no ambient `this` state. +pub use crate::codegen_abi::JsThis; + +const _: () = assert!(crate::codegen_abi::TAG_UNDEFINED == crate::value::TAG_UNDEFINED); + +/// The receiver of a PLAIN call — a call with no receiver: `f(x)` through a +/// function value, a callback a builtin invokes without a `thisArg`: +/// `undefined` (OrdinaryCallBindThis; a sloppy body coerces it to +/// `globalThis` itself). +#[inline(always)] +pub const fn plain_call_receiver() -> JsThis { + JsThis::UNDEFINED +} + +/// The native type of a JS body taking the receiver and one `f64` per +/// token: `js_body_fn_ty!(a, b)` is perry-abi's +/// `unsafe extern "C" fn(*const ClosureHeader, JsThis, f64, f64) -> f64`. +macro_rules! js_body_fn_ty { + ($($x:tt),* $(,)?) => { + ::perry_abi::js_body_fn_ty!($crate::closure::ClosureHeader; $($x),*) + }; +} + +/// Reinterpret a validated, non-sentinel body code pointer as a callable +/// taking the callee, the receiver and one `f64` per token +/// (`js_body_fn!(code; a, b)`). +/// +/// # Safety +/// `code` must be a JS body whose declared JS parameter count is at most the +/// number of tokens (over-application is safe, see the module docs). +macro_rules! js_body_fn { + ($code:expr; $($x:tt),* $(,)?) => { + ::std::mem::transmute::<*const u8, $crate::closure::body_call::js_body_fn_ty!($($x),*)>($code) + }; +} + +/// Call a JS body: `js_body_call!(code, callee, this, a0, a1, ...)`, where +/// `this` is a [`JsThis`]. +/// +/// # Safety +/// As [`js_body_fn!`]; `callee` is the function object whose body `code` is. +macro_rules! js_body_call { + ($code:expr, $callee:expr, $this:expr $(, $a:expr)* $(,)?) => {{ + let f = $crate::closure::body_call::js_body_fn!($code; $($a),*); + f($callee, $this $(, $a)*) + }}; +} + +/// [`js_body_call!`] through an unwind-capable pointer type in builds that +/// unwind Rust panics (test builds), for callers that must let a JS +/// exception travel through them there. Production (`panic = "abort"`) is +/// identical to [`js_body_call!`]. +macro_rules! js_body_call_unwind { + (@f64 $x:tt) => { f64 }; + ($code:expr, $callee:expr, $this:expr $(, $a:expr)* $(,)?) => {{ + #[cfg(panic = "abort")] + let f = $crate::closure::body_call::js_body_fn!($code; $($a),*); + #[cfg(not(panic = "abort"))] + let f = ::std::mem::transmute::< + *const u8, + extern "C-unwind" fn( + *const $crate::closure::ClosureHeader, + $crate::closure::body_call::JsThis + $(, $crate::closure::body_call::js_body_call_unwind!(@f64 $a))* + ) -> f64, + >($code); + f($callee, $this $(, $a)*) + }}; +} + +/// Reinterpret a METHOD body (`double m(double this, double a0, ...)`) as a +/// callable taking the receiver plus one `f64` per token: +/// `js_method_body_fn!(code; value)` is `extern "C" fn(f64, f64) -> f64`. +/// +/// # Safety +/// `code` must be a method body declaring at most the passed argument count. +macro_rules! js_method_body_fn { + (@f64 $x:tt) => { f64 }; + ($code:expr; $($x:tt),* $(,)?) => { + ::std::mem::transmute::< + *const u8, + extern "C" fn(f64 $(, $crate::closure::body_call::js_method_body_fn!(@f64 $x))*) -> f64, + >($code) + }; +} + +/// Call a METHOD body: `js_method_body_call!(code, this, a0, ...)`. +/// +/// # Safety +/// As [`js_method_body_fn!`]. +macro_rules! js_method_body_call { + ($code:expr, $this:expr $(, $a:expr)* $(,)?) => {{ + let f = $crate::closure::body_call::js_method_body_fn!($code; $($a),*); + f($this $(, $a)*) + }}; +} + +/// Reinterpret a BARE body (`double f(double a0, ...)`: no callee, no +/// implicit receiver) as a callable taking one `f64` per token. +/// +/// # Safety +/// `code` must be a bare body declaring at most the passed argument count. +macro_rules! js_bare_body_fn { + (@f64 $x:tt) => { f64 }; + ($code:expr; $($x:tt),* $(,)?) => { + ::std::mem::transmute::< + *const u8, + extern "C" fn($($crate::closure::body_call::js_bare_body_fn!(@f64 $x)),*) -> f64, + >($code) + }; +} + +pub(crate) use { + js_bare_body_fn, js_body_call, js_body_call_unwind, js_body_fn, js_body_fn_ty, + js_method_body_call, js_method_body_fn, +}; + +/// Call function value `func` with receiver `this` and `args`: the Rust-side +/// method-style call (`thisArg` of a builtin, an emitter, a getter's holder). +/// A plain call passes [`plain_call_receiver`]. +/// +/// # Safety +/// As [`crate::closure::js_native_call_value`]. +#[inline] +pub unsafe fn call_value(func: f64, this: JsThis, args: &[f64]) -> f64 { + let ptr = if args.is_empty() { + std::ptr::null() + } else { + args.as_ptr() + }; + crate::closure::native_call_value_this(func, this, ptr, args.len()) +} diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index 77caee6977..76e4390de4 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -34,12 +34,13 @@ pub use validate::{clean_closure_ptr, dispatch_proxy_callee_or_throw, get_valid_ pub use calln::{ js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, js_closure_call12, - js_closure_call13, js_closure_call14, js_closure_call15, js_closure_call16, - js_closure_call1_receiverless, js_closure_call2, js_closure_call3, js_closure_call4, - js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, js_closure_call9, + js_closure_call13, js_closure_call14, js_closure_call15, js_closure_call16, js_closure_call2, + js_closure_call3, js_closure_call4, js_closure_call5, js_closure_call6, js_closure_call7, + js_closure_call8, js_closure_call9, }; pub use direct::{DirectCall1, DirectCall2, DirectCall3, DirectCall4}; +pub(crate) use value_call::native_call_value_this; pub use value_call::{ js_closure_call_apply_with_spread, js_closure_call_array, js_native_call_value, }; diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index e459659281..e82b4f58e9 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -58,7 +58,11 @@ pub(crate) unsafe fn bound_method_source_func_ptr(closure: *const ClosureHeader) /// Extracts the namespace object and method name from the closure captures, /// then calls js_native_call_method with the packed arguments. #[inline] -pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) -> f64 { +pub unsafe fn dispatch_bound_method( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + args: &[f64], +) -> f64 { let mut namespace_obj = js_closure_get_capture_f64(closure, 0); let method_name_ptr = js_closure_get_capture_ptr(closure, 1) as *const i8; let method_name_len = js_closure_get_capture_ptr(closure, 2) as usize; @@ -95,10 +99,10 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) if let Some((func_ptr, param_count, has_synth_args, has_rest)) = crate::object::lookup_class_method_in_chain(owner_id, name) { - // The call-time `this` (IMPLICIT_THIS) is the receiver the - // private method body runs against — for `f.call(o)` it is + // The call-time `this` is the receiver the private + // method body runs against — for `f.call(o)` it is // `o`, for a bare `f()` it is undefined. - let call_this = crate::object::js_implicit_this_get(); + let call_this = this.as_f64(); return if let Some(brand) = private_brand { crate::object::call_vtable_method_with_private_brand( func_ptr, @@ -127,7 +131,7 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) if let (Some(owner_id), Some(brand)) = (crate::object::class_ref_id(namespace_obj), private_brand) { - let call_this = crate::object::js_implicit_this_get(); + let call_this = this.as_f64(); if let Some(result) = crate::object::call_private_static_method_for_owner( owner_id, name, @@ -147,10 +151,10 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) // Canonical class method value (test262 method identity): a class method is // a single shared function object whose captured receiver is the OWNER // class's prototype-ref — a marker, not the real `this`. The actual receiver - // is the call-site `this` (IMPLICIT_THIS): for `const f = c.m; f()` that is - // the spec `this`, and for `this.m = this.m.bind(this)` the outer - // `dispatch_bound_function` has already set IMPLICIT_THIS to the instance so - // the rebind targets the right object. Ordinary `obj.method(args)` calls do + // is the call-site `this` (the `this` this dispatch was handed): for + // `const f = c.m; f()` that is the spec `this`, and for + // `this.m = this.m.bind(this)` the outer `dispatch_bound_function` passes + // the instance so the rebind targets the right object. Ordinary `obj.method(args)` calls do // NOT reach here (they lower straight to `js_native_call_method`), so this // only governs method-as-value invocations. // The captured slot-0 prototype-ref names the OWNER class. A method value is a @@ -168,7 +172,7 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64]) // exactly the failure the comment below describes for the self-shadowing `bind` // case. let owner_proto_ref = namespace_obj; - let call_receiver = crate::object::canonical_bound_method_receiver(owner_proto_ref); + let call_receiver = crate::object::canonical_bound_method_receiver(owner_proto_ref, this); if method_name_len > 0 && !method_name_ptr.is_null() { if let Some(owner_id) = crate::object::class_prototype_ref_id(owner_proto_ref) { if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts( @@ -249,12 +253,13 @@ unsafe fn dispatch_symbol_bound_method( let has_rest = (meta >> 32) & 1 == 1; let is_static = (meta >> 33) & 1 == 1; if is_static { - // Bind IMPLICIT_THIS to the class ref for the duration, exactly like + // The static method runs with the class ref as `this`, exactly like // the direct-call path. The one-shot static-`this` override (armed by // the Function.prototype call/apply arms for a static bound-method // value) still wins in the static-method prologue. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); + // A static body binds `this` in its own prologue + // (`js_static_this_resolve`: this override, else its class ref), so + // it takes no receiver parameter. crate::object::static_private_owner_push(receiver); let result = crate::object::call_registered_static_method( func_ptr, @@ -264,7 +269,6 @@ unsafe fn dispatch_symbol_bound_method( has_rest, ); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else { // Computed symbol methods never synthesize an `arguments` object but @@ -283,8 +287,8 @@ unsafe fn dispatch_symbol_bound_method( /// Dispatch a `Function.prototype.bind` result (BOUND_FUNCTION_FUNC_PTR /// sentinel). Reads the bound target/this/partial-args from the closure -/// captures, prepends the bound args to the call-time args, sets -/// `IMPLICIT_THIS` to the bound receiver, and invokes the target closure. +/// captures, prepends the bound args to the call-time args, and invokes the +/// target closure with the bound receiver as its `this`. /// Refs #2840. #[inline] pub unsafe fn dispatch_bound_function(closure: *const ClosureHeader, args: &[f64]) -> f64 { @@ -328,14 +332,16 @@ pub unsafe fn dispatch_bound_function(closure: *const ClosureHeader, args: &[f64 }; // A bound concise/object-literal method reads `this` from its baked capture - // slot, not IMPLICIT_THIS — rebind it to the bound receiver so the bound - // `this` is honored (arrows/non-captures_this targets are returned as-is). + // slot, not only the `this` parameter — rebind it to the bound receiver so + // the bound `this` is honored (arrows/non-captures_this targets are + // returned as-is). let target = rebind_explicit_this(target, bound_this); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(bound_this)); - let result = js_native_call_value(target, call_ptr, call_len); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + super::value_call::native_call_value_this( + target, + crate::closure::JsThis::from_f64(bound_this), + call_ptr, + call_len, + ) } /// OrdinaryCallBindThis for the `call`/`apply`/`bind` entry points: box a @@ -400,7 +406,7 @@ pub(crate) fn coerce_call_this(target: f64, this_arg: f64) -> f64 { /// object-literal method (and object-literal accessor / symbol method) is /// lowered with `captures_this` and its reserved (last) capture slot baked to /// the *defining object* at construction time — so its body reads `this` from -/// that slot, NOT from `IMPLICIT_THIS`. Setting `IMPLICIT_THIS` therefore can't +/// that slot, NOT from its `this` parameter. Passing a receiver therefore can't /// redirect such a method to an explicit receiver: the baked slot wins, and the /// explicit `this` is silently ignored. /// @@ -415,7 +421,7 @@ pub(crate) fn coerce_call_this(target: f64, this_arg: f64) -> f64 { /// receiver is honored. Returns `target` UNCHANGED for: /// - arrow functions (lexical `this` — they must ignore an explicit receiver, /// yet still carry `CAPTURES_THIS_FLAG`, so they are excluded explicitly); -/// - non-`captures_this` functions (they already read `IMPLICIT_THIS`); +/// - non-`captures_this` functions (they already read the `this` parameter); /// - bound functions and non-closure values /// (`clone_closure_rebind_this` no-ops on these — it only rewrites a /// `CAPTURES_THIS` slot). @@ -891,11 +897,17 @@ mod rebind_predicate_tests { // two `extern "C"` bodies with identical machine code get folded to one // address by the linker — which silently makes every case in this test the // same closure body. - extern "C" fn arrow_probe(_closure: *const ClosureHeader) -> f64 { + extern "C" fn arrow_probe( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { 1.0 } - extern "C" fn method_probe(_closure: *const ClosureHeader) -> f64 { + extern "C" fn method_probe( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { 2.0 } diff --git a/crates/perry-runtime/src/closure/dispatch/bound/function_method_value_tests.rs b/crates/perry-runtime/src/closure/dispatch/bound/function_method_value_tests.rs index 9c69d9b475..07b0496a25 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound/function_method_value_tests.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound/function_method_value_tests.rs @@ -37,7 +37,10 @@ fn function_method_reads_share_the_prototype_function() { } } -extern "C" fn detached_function_fixture(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn detached_function_fixture( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 42.0 } diff --git a/crates/perry-runtime/src/closure/dispatch/calln.rs b/crates/perry-runtime/src/closure/dispatch/calln.rs index c4988925fe..b0de9c1f25 100644 --- a/crates/perry-runtime/src/closure/dispatch/calln.rs +++ b/crates/perry-runtime/src/closure/dispatch/calln.rs @@ -1,923 +1,644 @@ -//! Per-arity `js_closure_callN` FFI entry points (0..=16) and the shared -//! `dispatch_registered_call` / `dispatch_rest_or_declared_arity` routing +//! Per-arity closure-call FFI entry points (0..=16): +//! `js_closure_call{N}(closure, this, a0..)`, where `this` is the call's +//! receiver (`JsThis::UNDEFINED` for a plain call), and the shared routing //! helpers. //! +//! Each funnels into one per-arity dispatcher that routes bound +//! methods/functions, rest bundling, arity padding and the direct body call, +//! threading the receiver to the body as its `this` parameter +//! (`perry_abi::JS_BODY_THIS_PARAM`). +//! //! The hot-loop counterpart -- resolve a closure ONCE and call it directly for -//! the rest of the loop -- lives in the sibling `direct` module (#8180). It -//! subsumes the `resolve_call2_direct` helper that used to sit here and had -//! exactly one consumer. +//! the rest of the loop -- lives in the sibling `direct` module (#8180). use super::*; +use crate::closure::JsThis; + +macro_rules! closure_call_dispatch { + ($dispatch:ident, $n:literal; $($a:ident),*) => { + /// Route one closure call with a known receiver. + #[inline(always)] + pub(crate) fn $dispatch(closure: *const ClosureHeader, this: JsThis $(, $a: f64)*) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + return dispatch_proxy_callee_or_throw(closure, this, &[$($a),*]); + } + match resolve_strategy(func_ptr).kind() { + DispatchKind::BoundMethod => unsafe { + dispatch_bound_method(closure, this, &[$($a),*]) + }, + DispatchKind::BoundFunction => unsafe { + dispatch_bound_function(closure, &[$($a),*]) + }, + DispatchKind::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled(closure, func_ptr, this, &[$($a),*], fixed_arity, synth) + }, + DispatchKind::Arity(declared) if arity_needs_dispatch(declared, $n) => unsafe { + dispatch_with_arity(closure, func_ptr, this, &[$($a),*], declared) + }, + _ => unsafe { + crate::closure::body_call::js_body_call!(func_ptr, closure, this $(, $a)*) + }, + } + } + }; +} -/// Call a closure with 0 arguments, returning f64 +// `js_closure_call0` is `extern "C-unwind"` in unwinding (test) builds so an +// interpreted throw can reach a generated caller's catch landing pad; +// production keeps plain C (#8479). +closure_call_dispatch!(dispatch_call0, 0;); + +/// Call a closure with receiver `this` and no arguments. #[cfg(panic = "abort")] #[no_mangle] -pub extern "C" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { - js_closure_call0_impl(closure) +pub extern "C" fn js_closure_call0(closure: *const ClosureHeader, this: JsThis) -> f64 { + dispatch_call0(closure, this) } -/// Test/debug builds use Rust unwinding for JS exceptions. Keep this entry -/// point unwind-capable there so an interpreted throw can reach a generated -/// caller's catch landing pad. Production builds use the plain-C definition -/// above because their raw Itanium exceptions must cross it without Rust's -/// abort-on-unwind guard (#8479). +/// See the production definition above. #[cfg(not(panic = "abort"))] #[no_mangle] -pub extern "C-unwind" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { - js_closure_call0_impl(closure) +pub extern "C-unwind" fn js_closure_call0(closure: *const ClosureHeader, this: JsThis) -> f64 { + dispatch_call0(closure, this) } -#[inline(always)] -fn js_closure_call0_impl(closure: *const ClosureHeader) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[]); - } - match resolve_strategy(func_ptr).kind() { - DispatchKind::BoundMethod => unsafe { dispatch_bound_method(closure, &[]) }, - DispatchKind::BoundFunction => unsafe { dispatch_bound_function(closure, &[]) }, - DispatchKind::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[], fixed_arity, synth) - }, - DispatchKind::Arity(declared) if arity_needs_dispatch(declared, 0) => unsafe { - dispatch_with_arity(closure, func_ptr, &[], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure) - } - } -} - -/// Call a closure with 1 argument, returning f64 +// #8479: NOT `C-unwind` (below). The runtime is built `panic=abort` and JS +// throws travel as a raw Itanium `_Unwind_Exception` that must step THROUGH +// these frames untouched (see `crate::eh` and the panic=abort rationale in the +// workspace Cargo.toml). Marking a frame `extern "C-unwind"` in a panic=abort +// crate does not enable that — it makes rustc wrap the call in an +// abort-on-unwind landing pad, which is exactly the RFC-2945 guard a JS throw +// trips. +closure_call_dispatch!(dispatch_call1, 1; a0); +/// Call a closure with receiver `this` and 1 argument. #[no_mangle] -// The one-argument value-call path can run arbitrary generated code and must -// let a JS exception unwind to the generated caller's catch landing pad. -// #8479: NOT `C-unwind`. The runtime is built `panic=abort` and JS throws -// travel as a raw Itanium `_Unwind_Exception` that must step THROUGH these -// frames untouched (see `crate::eh` and the panic=abort rationale in the -// workspace Cargo.toml). Marking a frame `extern "C-unwind"` in a -// panic=abort crate does not enable that — it makes rustc wrap the call in -// an abort-on-unwind landing pad, which is exactly the RFC-2945 guard a JS -// throw trips ("panic in a function that cannot unwind"). #8416 introduced -// the first two such guards here; #8464 added ~40 more and measurably -// regressed main (+20 gap crashes, gc-stress) before being reverted. -pub extern "C" fn js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[arg0]); - } - dispatch_call1_resolved(closure, func_ptr, arg0, resolve_strategy(func_ptr)) -} - -#[inline(always)] -fn dispatch_call1_resolved( - closure: *const ClosureHeader, - func_ptr: *const u8, - arg0: f64, - strategy: DispatchStrategy, -) -> f64 { - match strategy.kind() { - DispatchKind::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0]) }, - DispatchKind::BoundFunction => unsafe { dispatch_bound_function(closure, &[arg0]) }, - DispatchKind::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[arg0], fixed_arity, synth) - }, - DispatchKind::Arity(declared) if arity_needs_dispatch(declared, 1) => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0) - } - } -} - -/// Receiverless one-argument call. Arrow functions have lexical `this`, so -/// resetting the dynamic `IMPLICIT_THIS` cell around them is unobservable and -/// needlessly expensive in callback pipelines. Arrow-ness is already folded -/// into the unified dispatch cache; non-arrows retain OrdinaryCallBindThis and -/// root the displaced receiver across arbitrary generated code. -#[no_mangle] -// Same unwind contract as `js_closure_call1` above: keep `extern "C"`, not -// `extern "C-unwind"`, so raw JS exceptions can traverse this bridge. -#[cfg(panic = "abort")] -pub extern "C" fn js_closure_call1_receiverless(closure: *const ClosureHeader, arg0: f64) -> f64 { - js_closure_call1_receiverless_impl(closure, arg0) +pub extern "C" fn js_closure_call1(closure: *const ClosureHeader, this: JsThis, a0: f64) -> f64 { + dispatch_call1(closure, this, a0) } +closure_call_dispatch!(dispatch_call2, 2; a0, a1); +/// Call a closure with receiver `this` and 2 arguments. #[no_mangle] -#[cfg(not(panic = "abort"))] -pub extern "C-unwind" fn js_closure_call1_receiverless( +pub extern "C" fn js_closure_call2( closure: *const ClosureHeader, - arg0: f64, + this: JsThis, + a0: f64, + a1: f64, ) -> f64 { - js_closure_call1_receiverless_impl(closure, arg0) -} - -#[inline(always)] -fn js_closure_call1_receiverless_impl(closure: *const ClosureHeader, arg0: f64) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - let strategy = (!func_ptr.is_null()).then(|| resolve_strategy(func_ptr)); - if let Some(arrow_strategy) = strategy.filter(|strategy| strategy.is_arrow()) { - return dispatch_call1_resolved(closure, func_ptr, arg0, arrow_strategy); - } - - let scope = crate::gc::RuntimeHandleScope::new(); - let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); - let previous_this = crate::object::js_implicit_this_set(undefined); - let previous_this_handle = scope.root_nanbox_f64(previous_this); - let result = match strategy { - Some(strategy) => dispatch_call1_resolved(closure, func_ptr, arg0, strategy), - None => dispatch_proxy_callee_or_throw(closure, &[arg0]), - }; - crate::object::js_implicit_this_set(previous_this_handle.get_nanbox_f64()); - result -} - -/// Call a closure with 2 arguments, returning f64 -#[no_mangle] -// A dynamically-dispatched closure can throw into a generated caller's catch -// landing pad; this bridge is on Next's loadManifest/readFileSync path. -pub extern "C" fn js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1]); - } - match resolve_strategy(func_ptr).kind() { - DispatchKind::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0, arg1]) }, - DispatchKind::BoundFunction => unsafe { dispatch_bound_function(closure, &[arg0, arg1]) }, - DispatchKind::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[arg0, arg1], fixed_arity, synth) - }, - DispatchKind::Arity(declared) if arity_needs_dispatch(declared, 2) => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1) - } - } + dispatch_call2(closure, this, a0, a1) } -/// Call a closure with 3 arguments, returning f64 +closure_call_dispatch!(dispatch_call3, 3; a0, a1, a2); +/// Call a closure with receiver `this` and 3 arguments. #[no_mangle] pub extern "C" fn js_closure_call3( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2]); - } - match resolve_strategy(func_ptr).kind() { - DispatchKind::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2]) }, - DispatchKind::BoundFunction => unsafe { - dispatch_bound_function(closure, &[arg0, arg1, arg2]) - }, - DispatchKind::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled(closure, func_ptr, &[arg0, arg1, arg2], fixed_arity, synth) - }, - DispatchKind::Arity(declared) if arity_needs_dispatch(declared, 3) => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2) - } - } + dispatch_call3(closure, this, a0, a1, a2) } -/// Call a closure with 4 arguments, returning f64 +closure_call_dispatch!(dispatch_call4, 4; a0, a1, a2, a3); +/// Call a closure with receiver `this` and 4 arguments. #[no_mangle] pub extern "C" fn js_closure_call4( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2, arg3]); - } - match resolve_strategy(func_ptr).kind() { - DispatchKind::BoundMethod => unsafe { - dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3]) - }, - DispatchKind::BoundFunction => unsafe { - dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3]) - }, - DispatchKind::Rest(fixed_arity, synth) => unsafe { - dispatch_rest_bundled( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3], - fixed_arity, - synth, - ) - }, - DispatchKind::Arity(declared) if arity_needs_dispatch(declared, 4) => unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3], declared) - }, - _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3) - } - } + dispatch_call4(closure, this, a0, a1, a2, a3) } -/// Call a closure with 5 arguments, returning f64 +closure_call_dispatch!(dispatch_call5, 5; a0, a1, a2, a3, a4); +/// Call a closure with receiver `this` and 5 arguments. #[no_mangle] pub extern "C" fn js_closure_call5( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2, arg3, arg4]); - } - if func_ptr == BOUND_METHOD_FUNC_PTR { - return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4]) }; - } - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return unsafe { dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3, arg4]) }; - } - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - return unsafe { - dispatch_rest_bundled( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3, arg4], - fixed_arity, - synth, - ) - }; - } - if let Some(declared) = dispatch_arity(func_ptr) { - if arity_needs_dispatch(declared, 5) { - return unsafe { - dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3, arg4], declared) - }; - } - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4) + dispatch_call5(closure, this, a0, a1, a2, a3, a4) } -/// Call a closure with 6 arguments, returning f64 +closure_call_dispatch!(dispatch_call6, 6; a0, a1, a2, a3, a4, a5); +/// Call a closure with receiver `this` and 6 arguments. #[no_mangle] pub extern "C" fn js_closure_call6( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]); - } - if func_ptr == BOUND_METHOD_FUNC_PTR { - return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) }; - } - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return unsafe { dispatch_bound_function(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) }; - } - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - return unsafe { - dispatch_rest_bundled( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3, arg4, arg5], - fixed_arity, - synth, - ) - }; - } - if let Some(declared) = dispatch_arity(func_ptr) { - if arity_needs_dispatch(declared, 6) { - return unsafe { - dispatch_with_arity( - closure, - func_ptr, - &[arg0, arg1, arg2, arg3, arg4, arg5], - declared, - ) - }; - } - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4, arg5) -} - -#[inline] -pub(crate) fn dispatch_registered_call( - closure: *const ClosureHeader, - func_ptr: *const u8, - args: &[f64], -) -> Option { - if func_ptr == BOUND_METHOD_FUNC_PTR { - return Some(unsafe { dispatch_bound_method(closure, args) }); - } - if func_ptr == BOUND_FUNCTION_FUNC_PTR { - return Some(unsafe { dispatch_bound_function(closure, args) }); - } - None -} - -#[inline] -pub(crate) fn dispatch_rest_or_declared_arity( - closure: *const ClosureHeader, - func_ptr: *const u8, - args: &[f64], - provided: u32, -) -> Option { - if let Some((fixed_arity, synth)) = lookup_closure_rest_full(func_ptr) { - return Some(unsafe { dispatch_rest_bundled(closure, func_ptr, args, fixed_arity, synth) }); - } - if let Some(declared) = dispatch_arity(func_ptr) { - if arity_needs_dispatch(declared, provided) { - return Some(unsafe { dispatch_with_arity(closure, func_ptr, args, declared) }); - } - } - None + dispatch_call6(closure, this, a0, a1, a2, a3, a4, a5) } -/// Call a closure with 7 arguments, returning f64 +closure_call_dispatch!(dispatch_call7, 7; a0, a1, a2, a3, a4, a5, a6); +/// Call a closure with receiver `this` and 7 arguments. #[no_mangle] pub extern "C" fn js_closure_call7( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[arg0, arg1, arg2, arg3, arg4, arg5, arg6], - ); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 7) { - return result; - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6) + dispatch_call7(closure, this, a0, a1, a2, a3, a4, a5, a6) } -/// Call a closure with 8 arguments, returning f64 +closure_call_dispatch!(dispatch_call8, 8; a0, a1, a2, a3, a4, a5, a6, a7); +/// Call a closure with receiver `this` and 8 arguments. #[no_mangle] pub extern "C" fn js_closure_call8( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7], - ); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 8) { - return result; - } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; - func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) + dispatch_call8(closure, this, a0, a1, a2, a3, a4, a5, a6, a7) } -/// Call a closure with 9 arguments, returning f64 +closure_call_dispatch!(dispatch_call9, 9; a0, a1, a2, a3, a4, a5, a6, a7, a8); +/// Call a closure with receiver `this` and 9 arguments. #[no_mangle] pub extern "C" fn js_closure_call9( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8], - ); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 9) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, - ) + dispatch_call9(closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8) } -/// Call a closure with 10 arguments, returning f64 +closure_call_dispatch!(dispatch_call10, 10; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9); +/// Call a closure with receiver `this` and 10 arguments. #[no_mangle] pub extern "C" fn js_closure_call10( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9], - ); - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 10) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, - ) + dispatch_call10(closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) } -/// Call a closure with 11 arguments, returning f64 +closure_call_dispatch!(dispatch_call11, 11; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); +/// Call a closure with receiver `this` and 11 arguments. #[no_mangle] pub extern "C" fn js_closure_call11( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ], - ); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 11) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, - ) + dispatch_call11(closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) } -/// Call a closure with 12 arguments, returning f64 +closure_call_dispatch!(dispatch_call12, 12; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); +/// Call a closure with receiver `this` and 12 arguments. #[no_mangle] pub extern "C" fn js_closure_call12( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, - ], - ); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 12) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, + dispatch_call12( + closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, ) } -/// Call a closure with 13 arguments, returning f64 +closure_call_dispatch!(dispatch_call13, 13; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); +/// Call a closure with receiver `this` and 13 arguments. #[no_mangle] pub extern "C" fn js_closure_call13( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, + a12: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - ], - ); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 13) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + dispatch_call13( + closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, ) } -/// Call a closure with 14 arguments, returning f64 +closure_call_dispatch!(dispatch_call14, 14; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13); +/// Call a closure with receiver `this` and 14 arguments. #[no_mangle] pub extern "C" fn js_closure_call14( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, - arg13: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, + a12: f64, + a13: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, - ], - ); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 14) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, + dispatch_call14( + closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, ) } -/// Call a closure with 15 arguments, returning f64 +closure_call_dispatch!(dispatch_call15, 15; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14); +/// Call a closure with receiver `this` and 15 arguments. #[no_mangle] pub extern "C" fn js_closure_call15( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, - arg13: f64, - arg14: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, + a12: f64, + a13: f64, + a14: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( - closure, - &[ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, arg14, - ], - ); - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; - } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 15) { - return result; - } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, arg14, + dispatch_call15( + closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, ) } -/// Call a closure with 16 arguments, returning f64 +closure_call_dispatch!(dispatch_call16, 16; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15); +/// Call a closure with receiver `this` and 16 arguments. #[no_mangle] pub extern "C" fn js_closure_call16( closure: *const ClosureHeader, - arg0: f64, - arg1: f64, - arg2: f64, - arg3: f64, - arg4: f64, - arg5: f64, - arg6: f64, - arg7: f64, - arg8: f64, - arg9: f64, - arg10: f64, - arg11: f64, - arg12: f64, - arg13: f64, - arg14: f64, - arg15: f64, + this: JsThis, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, + a12: f64, + a13: f64, + a14: f64, + a15: f64, ) -> f64 { - let func_ptr = get_valid_func_ptr(closure); - if func_ptr.is_null() { - return dispatch_proxy_callee_or_throw( + dispatch_call16( + closure, this, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, + ) +} + +/// Dispatch `args` (any count) to `closure` with receiver `this`: the +/// per-arity dispatchers up to 16 arguments, the padded wide ladder past +/// them. The one Rust-side way to call a closure pointer. +pub(crate) fn dispatch_call_slice( + closure: *const ClosureHeader, + this: JsThis, + args: &[f64], +) -> f64 { + let a = |i: usize| args[i]; + match args.len() { + 0 => dispatch_call0(closure, this), + 1 => dispatch_call1(closure, this, a(0)), + 2 => dispatch_call2(closure, this, a(0), a(1)), + 3 => dispatch_call3(closure, this, a(0), a(1), a(2)), + 4 => dispatch_call4(closure, this, a(0), a(1), a(2), a(3)), + 5 => dispatch_call5(closure, this, a(0), a(1), a(2), a(3), a(4)), + 6 => dispatch_call6(closure, this, a(0), a(1), a(2), a(3), a(4), a(5)), + 7 => dispatch_call7(closure, this, a(0), a(1), a(2), a(3), a(4), a(5), a(6)), + 8 => dispatch_call8( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + ), + 9 => dispatch_call9( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + ), + 10 => dispatch_call10( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + ), + 11 => dispatch_call11( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + ), + 12 => dispatch_call12( closure, - &[ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, arg14, arg15, - ], - ); + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + ), + 13 => dispatch_call13( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + ), + 14 => dispatch_call14( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + a(13), + ), + 15 => dispatch_call15( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + a(13), + a(14), + ), + 16 => dispatch_call16( + closure, + this, + a(0), + a(1), + a(2), + a(3), + a(4), + a(5), + a(6), + a(7), + a(8), + a(9), + a(10), + a(11), + a(12), + a(13), + a(14), + a(15), + ), + _ => dispatch_call_wide(closure, this, args), } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, arg15, - ]; - if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) { - return result; +} + +/// More than 16 arguments: route as the per-arity entries do, then call the +/// body through the padded ladder (`wide_call`). +fn dispatch_call_wide(closure: *const ClosureHeader, this: JsThis, args: &[f64]) -> f64 { + let func_ptr = get_valid_func_ptr(closure); + if func_ptr.is_null() { + return dispatch_proxy_callee_or_throw(closure, this, args); } - let args = [ - arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, - arg14, arg15, - ]; - if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 16) { - return result; + match resolve_strategy(func_ptr).kind() { + DispatchKind::BoundMethod => unsafe { dispatch_bound_method(closure, this, args) }, + DispatchKind::BoundFunction => unsafe { dispatch_bound_function(closure, args) }, + DispatchKind::Rest(fixed_arity, synth) => unsafe { + dispatch_rest_bundled(closure, func_ptr, this, args, fixed_arity, synth) + }, + DispatchKind::Arity(declared) if arity_needs_dispatch(declared, args.len() as u32) => unsafe { + dispatch_with_arity(closure, func_ptr, this, args, declared) + }, + _ => unsafe { super::super::dispatch_wide_abi(closure, func_ptr, this, args, args.len()) }, } - let func: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; - func( - closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, - arg13, arg14, arg15, - ) } #[cfg(test)] -mod receiverless_tests { +mod plain_call_tests { use super::*; - extern "C" fn observe_dynamic_this(_: *const ClosureHeader, _: f64) -> f64 { - crate::object::js_implicit_this_get() + extern "C" fn observe_dynamic_this( + _: *const ClosureHeader, + this: crate::closure::JsThis, + _: f64, + ) -> f64 { + this.as_f64() } #[test] - fn one_arg_receiverless_dispatch_only_resets_this_for_non_arrows() { + fn a_plain_call_passes_undefined_this() { let body = observe_dynamic_this as *const u8; let closure = crate::closure::js_closure_alloc(body, 0); crate::closure::js_register_closure_arity(body, 1); - let sentinel = 42.0; - let original = crate::object::js_implicit_this_set(sentinel); - let regular_result = js_closure_call1_receiverless(closure, 0.0); + // A receiver passed to an enclosing call must not leak into a plain + // call: the body sees exactly the `undefined` this entry passes. + let explicit = js_closure_call1(closure, JsThis::from_f64(42.0), 0.0); + assert_eq!(explicit, 42.0); + let regular_result = js_closure_call1(closure, crate::closure::plain_call_receiver(), 0.0); assert_eq!(regular_result.to_bits(), crate::value::TAG_UNDEFINED); - assert_eq!(crate::object::js_implicit_this_get(), sentinel); + } +} + +/// `perry_abi::JS_CLOSURE_CALL_ENTRIES` is what codegen DECLARES; these are +/// the functions it links to. Each entry must name the function taking +/// exactly its index's JS argument count (the coercion below fails to compile +/// otherwise), in order. +#[cfg(test)] +mod abi_table_tests { + use super::*; - crate::closure::js_register_closure_arrow_function(body); - let arrow_result = js_closure_call1_receiverless(closure, 0.0); - assert_eq!(arrow_result, sentinel); - assert_eq!(crate::object::js_implicit_this_get(), sentinel); - crate::object::js_implicit_this_set(original); + // An ENTRY takes the callee, the receiver and the JS arguments. + macro_rules! entry { + (@f64 $x:tt) => { f64 }; + ($f:ident; $($x:tt),*) => {{ + let f: extern "C" fn(*const ClosureHeader, JsThis $(, entry!(@f64 $x))*) -> f64 = $f; + (stringify!($f), f as *const u8) + }}; + } + + #[test] + fn closure_call_entries_match_the_abi_table() { + // `js_closure_call0` is `extern "C-unwind"` in unwinding (test) builds. + let call0 = { + let f: extern "C-unwind" fn(*const ClosureHeader, JsThis) -> f64 = js_closure_call0; + ("js_closure_call0", f as *const u8) + }; + let real = [ + call0, + entry!(js_closure_call1; a), + entry!(js_closure_call2; a, a), + entry!(js_closure_call3; a, a, a), + entry!(js_closure_call4; a, a, a, a), + entry!(js_closure_call5; a, a, a, a, a), + entry!(js_closure_call6; a, a, a, a, a, a), + entry!(js_closure_call7; a, a, a, a, a, a, a), + entry!(js_closure_call8; a, a, a, a, a, a, a, a), + entry!(js_closure_call9; a, a, a, a, a, a, a, a, a), + entry!(js_closure_call10; a, a, a, a, a, a, a, a, a, a), + entry!(js_closure_call11; a, a, a, a, a, a, a, a, a, a, a), + entry!(js_closure_call12; a, a, a, a, a, a, a, a, a, a, a, a), + entry!(js_closure_call13; a, a, a, a, a, a, a, a, a, a, a, a, a), + entry!(js_closure_call14; a, a, a, a, a, a, a, a, a, a, a, a, a, a), + entry!(js_closure_call15; a, a, a, a, a, a, a, a, a, a, a, a, a, a, a), + entry!(js_closure_call16; a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a), + ]; + let table = crate::codegen_abi::JS_CLOSURE_CALL_ENTRIES; + assert_eq!(real.len(), table.len()); + for (argc, ((name, ptr), declared)) in real.iter().zip(table.iter()).enumerate() { + assert_eq!(name, declared, "JS_CLOSURE_CALL_ENTRIES[{argc}]"); + assert!(!ptr.is_null()); + } + for name in table { + assert!( + crate::codegen_abi::JS_CALL_ENTRIES.contains(&name), + "{name} is a JS-call entry but not in JS_CALL_ENTRIES" + ); + } } } diff --git a/crates/perry-runtime/src/closure/dispatch/direct.rs b/crates/perry-runtime/src/closure/dispatch/direct.rs index c434844351..cd766a818b 100644 --- a/crates/perry-runtime/src/closure/dispatch/direct.rs +++ b/crates/perry-runtime/src/closure/dispatch/direct.rs @@ -82,21 +82,19 @@ pub(crate) fn resolve_direct_func_ptr( Some(func_ptr) } -/// Resolve one receiverless arrow callback at a method boundary. Ordinary -/// functions stay on `js_closure_callN`, which is responsible for binding a -/// bare call's dynamic `this` to `undefined`; bound/rest/padded calls likewise -/// retain their full dispatcher semantics. +/// Resolve one callback for PLAIN calls (`f(x)`) at a method boundary: the +/// body the caller may call directly, passing `undefined` as its receiver — +/// exactly what `js_closure_callN` passes. An arrow ignores the receiver; an +/// ordinary function binds it (a sloppy body coerces it in its own +/// prologue). Bound/rest/padded calls retain their full dispatcher semantics. #[no_mangle] -pub extern "C" fn js_closure_resolve_arrow_direct_call( +pub extern "C" fn js_closure_resolve_plain_direct_call( closure: *const ClosureHeader, arity: u32, ) -> *const u8 { let Some(func_ptr) = resolve_direct_func_ptr(closure, arity) else { return std::ptr::null(); }; - if !resolve_strategy(func_ptr).is_arrow() { - return std::ptr::null(); - } let Some(trusted) = super::super::registry::lookup_closure_trusted_direct(func_ptr) else { return func_ptr; }; @@ -153,30 +151,45 @@ pub extern "C" fn js_closure_resolve_versioned_loop_direct_call( macro_rules! define_direct_call_site { ( $(#[$meta:meta])* - $site:ident, $arity:literal, $slow:ident, $($arg:ident),+ + $site:ident, $arity:literal, $entry:ident, $slow:ident, $($arg:ident),+ ) => { + /// The fallback, a JS body: the full dispatcher with the receiver it + /// is handed. + extern "C" fn $slow( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + $($arg: f64),+ + ) -> f64 { + $entry(closure, this, $($arg),+) + } + $(#[$meta])* #[derive(Clone, Copy)] - pub struct $site(extern "C" fn(*const ClosureHeader, $(define_direct_call_site!(@f64 $arg)),+) -> f64); + pub struct $site(crate::closure::body_call::js_body_fn_ty!($($arg),+)); impl $site { /// Resolve `closure` once, before the loop. #[inline] pub fn resolve(closure: *const ClosureHeader) -> Self { $site(resolve_direct_func_ptr(closure, $arity).map_or($slow, |func_ptr| unsafe { - std::mem::transmute::< - *const u8, - extern "C" fn(*const ClosureHeader, $(define_direct_call_site!(@f64 $arg)),+) -> f64, - >(func_ptr) + crate::closure::body_call::js_body_fn!(func_ptr; $($arg),+) })) } /// Invoke with the CURRENT closure address (see the module docs on - /// rooting). Falls back to the full dispatcher when the closure - /// did not resolve. + /// rooting) and receiver `this` (`plain_call_receiver()` for a + /// plain call, a builtin's `thisArg` otherwise). Falls back to + /// the full dispatcher when the closure did not resolve. #[inline] - pub fn call(&self, closure: *const ClosureHeader, $($arg: f64),+) -> f64 { - (self.0)(closure, $($arg),+) + pub fn call( + &self, + closure: *const ClosureHeader, + this: crate::closure::JsThis, + $($arg: f64),+ + ) -> f64 { + // SAFETY: `self.0` is the resolved body of `closure` at this + // arity, or the dispatcher fallback. + unsafe { (self.0)(closure, this, $($arg),+) } } /// Whether the direct target was resolved. Test-only: a "fast @@ -186,12 +199,11 @@ macro_rules! define_direct_call_site { pub(crate) fn is_direct(&self) -> bool { !std::ptr::fn_addr_eq( self.0, - $slow as extern "C" fn(*const ClosureHeader, $(define_direct_call_site!(@f64 $arg)),+) -> f64, + $slow as crate::closure::body_call::js_body_fn_ty!($($arg),+), ) } } }; - (@f64 $arg:ident) => { f64 }; } define_direct_call_site!( @@ -199,6 +211,7 @@ define_direct_call_site!( DirectCall1, 1, js_closure_call1, + direct_call1_fallback, arg0 ); @@ -208,6 +221,7 @@ define_direct_call_site!( DirectCall2, 2, js_closure_call2, + direct_call2_fallback, arg0, arg1 ); @@ -219,6 +233,7 @@ define_direct_call_site!( DirectCall3, 3, js_closure_call3, + direct_call3_fallback, arg0, arg1, arg2 @@ -230,6 +245,7 @@ define_direct_call_site!( DirectCall4, 4, js_closure_call4, + direct_call4_fallback, arg0, arg1, arg2, @@ -242,35 +258,75 @@ mod tests { // A capture-less body behind a real `ClosureHeader`, the same way // `array/tests.rs` and `array/typed_array_receiver_tests.rs` build theirs. - extern "C" fn add3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { + extern "C" fn add3( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, + c: f64, + ) -> f64 { a * 100.0 + b * 10.0 + c } - extern "C" fn sum2(_c: *const ClosureHeader, a: f64, b: f64) -> f64 { + extern "C" fn sum2( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, + ) -> f64 { a + b } - extern "C" fn ordinary3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { + extern "C" fn ordinary3( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, + c: f64, + ) -> f64 { a + b + c } - extern "C" fn rest_body(_c: *const ClosureHeader, _a: f64, _rest: f64) -> f64 { + extern "C" fn rest_body( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + _a: f64, + _rest: f64, + ) -> f64 { 0.0 } - extern "C" fn trusted_add3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { + extern "C" fn trusted_add3( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, + c: f64, + ) -> f64 { a * 100.0 + b * 10.0 + c } - extern "C" fn boxed1(_c: *const ClosureHeader, value: f64) -> f64 { + extern "C" fn boxed1( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, + ) -> f64 { value } - extern "C" fn trusted_boxed1(_c: *const ClosureHeader, value: f64) -> f64 { + extern "C" fn trusted_boxed1( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, + ) -> f64 { value } - extern "C" fn versioned_source(_c: *const ClosureHeader, value: f64) -> f64 { + extern "C" fn versioned_source( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, + ) -> f64 { value } @@ -283,12 +339,12 @@ mod tests { } #[test] - fn exported_resolver_admits_only_directly_callable_arrows() { + fn exported_resolver_admits_directly_callable_bodies() { let arrow = closure_for(add3 as *const u8); crate::closure::js_register_closure_arity(add3 as *const u8, 3); crate::closure::js_register_closure_arrow_function(add3 as *const u8); assert_eq!( - js_closure_resolve_arrow_direct_call(arrow, 3), + js_closure_resolve_plain_direct_call(arrow, 3), add3 as *const u8 ); crate::closure::js_register_closure_trusted_direct( @@ -298,10 +354,10 @@ mod tests { 0, ); assert_eq!( - js_closure_resolve_arrow_direct_call(arrow, 3), + js_closure_resolve_plain_direct_call(arrow, 3), trusted_add3 as *const u8 ); - assert!(js_closure_resolve_arrow_direct_call(arrow, 2).is_null()); + assert!(js_closure_resolve_plain_direct_call(arrow, 2).is_null()); let ordinary = closure_for(ordinary3 as *const u8); crate::closure::js_register_closure_arity(ordinary3 as *const u8, 3); @@ -311,16 +367,23 @@ mod tests { 0, 0, ); - assert!(js_closure_resolve_arrow_direct_call(ordinary, 3).is_null()); + // An ordinary function is as directly callable as an arrow: the + // caller passes the plain-call `undefined` receiver itself. Trusted + // clones attach to arrow bodies only, so it resolves to its public + // body. + assert_eq!( + js_closure_resolve_plain_direct_call(ordinary, 3), + ordinary3 as *const u8 + ); let rest = closure_for(rest_body as *const u8); crate::closure::js_register_closure_rest(rest_body as *const u8, 1); crate::closure::js_register_closure_arrow_function(rest_body as *const u8); - assert!(js_closure_resolve_arrow_direct_call(rest, 3).is_null()); + assert!(js_closure_resolve_plain_direct_call(rest, 3).is_null()); for sentinel in [BOUND_METHOD_FUNC_PTR, BOUND_FUNCTION_FUNC_PTR] { let bound = closure_for(sentinel); - assert!(js_closure_resolve_arrow_direct_call(bound, 3).is_null()); + assert!(js_closure_resolve_plain_direct_call(bound, 3).is_null()); } } @@ -337,7 +400,7 @@ mod tests { let wrong_count = closure_for(boxed1 as *const u8); assert_eq!( - js_closure_resolve_arrow_direct_call(wrong_count, 1), + js_closure_resolve_plain_direct_call(wrong_count, 1), boxed1 as *const u8, "a wrong capture count must retain the checked public body" ); @@ -345,7 +408,7 @@ mod tests { let non_box = crate::closure::js_closure_alloc(boxed1 as *const u8, 1); crate::closure::js_closure_set_capture_bits(non_box, 0, crate::value::TAG_UNDEFINED); assert_eq!( - js_closure_resolve_arrow_direct_call(non_box, 1), + js_closure_resolve_plain_direct_call(non_box, 1), boxed1 as *const u8, "a non-box payload must retain the checked public body" ); @@ -354,7 +417,7 @@ mod tests { let cell = crate::r#box::js_box_alloc_bits(crate::value::TAG_UNDEFINED as i64); crate::closure::js_closure_set_box_capture_ptr(valid, 0, cell as i64); assert_eq!( - js_closure_resolve_arrow_direct_call(valid, 1), + js_closure_resolve_plain_direct_call(valid, 1), trusted_boxed1 as *const u8, "the exact compiler-installed layout must select the private body" ); @@ -401,10 +464,13 @@ mod tests { "an unregistered, capture-less, non-bound callback must resolve to \ a direct target -- otherwise this whole module is inert" ); - assert_eq!(site.call(c, 1.0, 2.0, 3.0), 123.0); assert_eq!( - site.call(c, 1.0, 2.0, 3.0), - js_closure_call3(c, 1.0, 2.0, 3.0) + site.call(c, crate::closure::plain_call_receiver(), 1.0, 2.0, 3.0), + 123.0 + ); + assert_eq!( + site.call(c, crate::closure::plain_call_receiver(), 1.0, 2.0, 3.0), + js_closure_call3(c, crate::closure::plain_call_receiver(), 1.0, 2.0, 3.0) ); } diff --git a/crates/perry-runtime/src/closure/dispatch/validate.rs b/crates/perry-runtime/src/closure/dispatch/validate.rs index aace5ac249..376cab5f43 100644 --- a/crates/perry-runtime/src/closure/dispatch/validate.rs +++ b/crates/perry-runtime/src/closure/dispatch/validate.rs @@ -141,18 +141,21 @@ pub fn get_valid_func_ptr(closure: *const ClosureHeader) -> *const u8 { /// checked`) — so a proxy callee arrives here as its bare registry id /// (`PROXY_ID_BAND_START + id`). The compiler emits a `ProxyApply` node only /// when it can statically prove the callee is a proxy; a proxy read out of a -/// dynamically-typed slot (`const g = obj.m` / `arr[0]` / `map.get(k)`, or a -/// method detached by `js_closure_unbind_this`) reaches this generic path with +/// dynamically-typed slot (`const g = obj.m` / `arr[0]` / `map.get(k)`) +/// reaches this generic path with /// no static hint, and `get_valid_func_ptr` correctly refuses to dereference the /// id (#5976/#6321) — which turned the SIGSEGV into a spurious TypeError. Node /// calls the proxy. Re-boxing the id and asking the proxy registry costs nothing /// on the hot path: this runs only where the code used to throw unconditionally. -pub fn dispatch_proxy_callee_or_throw(closure: *const ClosureHeader, args: &[f64]) -> f64 { +pub fn dispatch_proxy_callee_or_throw( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + args: &[f64], +) -> f64 { let boxed = f64::from_bits(crate::value::POINTER_TAG | (closure as u64 & crate::value::POINTER_MASK)); if crate::proxy::js_proxy_is_proxy(boxed) == 1 { - let this_arg = f64::from_bits(crate::value::TAG_UNDEFINED); - return crate::proxy::call_proxy_value_with_this(boxed, this_arg, args); + return crate::proxy::call_proxy_value_with_this(boxed, this.as_f64(), args); } throw_not_callable() } diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index d86caf37b3..5264ceeb73 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -1,6 +1,8 @@ -//! Dynamic value-call entry points: `js_native_call_value` (the generic -//! NaN-boxed callee dispatcher), the V8 trampoline bridge `js_closure_call_array`, -//! and the spread-apply bridge `js_closure_call_apply_with_spread`. +//! Dynamic value-call entry points, each taking the receiver after the +//! function: `js_native_call_value` (the generic NaN-boxed callee +//! dispatcher), `js_closure_call_array` (any argument count) and the +//! spread-apply bridge `js_closure_call_apply_with_spread`; plus the V8 +//! trampoline adapter `js_closure_v8_callback`. use super::*; @@ -26,14 +28,18 @@ use super::*; // throw trips ("panic in a function that cannot unwind"). #8416 introduced // the first two such guards here; #8464 added ~40 more and measurably // regressed main (+20 gap crashes, gc-stress) before being reverted. +/// Call function value `func_value` with receiver `this` +/// (`JsThis::UNDEFINED` for a plain call) and `args_len` arguments at +/// `args_ptr`: what `func.call(this, ...args)` does. #[cfg(panic = "abort")] #[no_mangle] pub unsafe extern "C" fn js_native_call_value( func_value: f64, + this: crate::closure::JsThis, args_ptr: *const f64, args_len: usize, ) -> f64 { - unsafe { js_native_call_value_impl(func_value, args_ptr, args_len) } + unsafe { native_call_value_this_impl(func_value, this, args_ptr, args_len) } } // Debug/test static archives transport Perry exceptions with Rust unwinding, @@ -43,14 +49,38 @@ pub unsafe extern "C" fn js_native_call_value( #[no_mangle] pub unsafe extern "C-unwind" fn js_native_call_value( func_value: f64, + this: crate::closure::JsThis, args_ptr: *const f64, args_len: usize, ) -> f64 { - unsafe { js_native_call_value_impl(func_value, args_ptr, args_len) } + unsafe { native_call_value_this_impl(func_value, this, args_ptr, args_len) } } +/// Call function value `func_value` with receiver `this` and `args_len` +/// arguments at `args_ptr`: the one value-call dispatcher, for runtime callers. +/// +/// This calls the exported `js_native_call_value` instead of inlining the +/// dispatcher: the runtime has over a hundred callers, and inlining the whole +/// dispatch into each of them grew every linked program by ~190 KB. +#[inline(always)] +pub(crate) unsafe fn native_call_value_this( + func_value: f64, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: usize, +) -> f64 { + unsafe { js_native_call_value(func_value, this, args_ptr, args_len) } +} + +/// The dispatcher body, inlined only into the two `js_native_call_value` +/// definitions (one per panic strategy). #[inline(always)] -unsafe fn js_native_call_value_impl(func_value: f64, args_ptr: *const f64, args_len: usize) -> f64 { +unsafe fn native_call_value_this_impl( + func_value: f64, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: usize, +) -> f64 { use crate::value::JSValue; let jsval = JSValue::from_bits(func_value.to_bits()); @@ -78,8 +108,7 @@ unsafe fn js_native_call_value_impl(func_value: f64, args_ptr: *const f64, args_ } } let arr_box = f64::from_bits(0x7FFD_0000_0000_0000 | (a as u64 & 0x0000_FFFF_FFFF_FFFF)); - let this_arg = f64::from_bits(crate::value::TAG_UNDEFINED); - return crate::proxy::js_proxy_apply(func_value, this_arg, arr_box); + return crate::proxy::js_proxy_apply(func_value, this.as_f64(), arr_box); } // Dynamic `super()` for `class X extends = (0..args_len).map(arg_at).collect(); - return dispatch_rest_bundled(closure, func_ptr, &all, fixed_arity, synth); + return dispatch_rest_bundled(closure, func_ptr, this, &all, fixed_arity, synth); } } - // Call with the appropriate arity - match dispatch_args_len { - 0 => js_closure_call0(closure), - 1 => js_closure_call1(closure, arg_at(0)), - 2 => js_closure_call2(closure, arg_at(0), arg_at(1)), - 3 => js_closure_call3(closure, arg_at(0), arg_at(1), arg_at(2)), - 4 => js_closure_call4(closure, arg_at(0), arg_at(1), arg_at(2), arg_at(3)), - 5 => js_closure_call5( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - ), - 6 => js_closure_call6( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - ), - 7 => js_closure_call7( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - ), - 8 => js_closure_call8( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - ), - // Arities 9..=16 must each dispatch through their own - // `js_closure_call{N}` so the func-ptr is transmuted to a signature - // with the matching number of `f64` params. Collapsing these into - // `js_closure_call8` (the pre-fix `_` arm) silently dropped args 9+ for - // any closure VALUE / method invoked with >8 args — the codegen-side - // wrapper now carries up to 16 params (see artifacts.rs), so the runtime - // dispatch must reach them. >16 args fall back to the array path. - 9 => js_closure_call9( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - ), - 10 => js_closure_call10( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - ), - 11 => js_closure_call11( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - ), - 12 => js_closure_call12( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - ), - 13 => js_closure_call13( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - ), - 14 => js_closure_call14( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - arg_at(13), - ), - 15 => js_closure_call15( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - arg_at(13), - arg_at(14), - ), - 16 => js_closure_call16( - closure, - arg_at(0), - arg_at(1), - arg_at(2), - arg_at(3), - arg_at(4), - arg_at(5), - arg_at(6), - arg_at(7), - arg_at(8), - arg_at(9), - arg_at(10), - arg_at(11), - arg_at(12), - arg_at(13), - arg_at(14), - arg_at(15), - ), - // >16 args: marshal into a stack buffer and dispatch via the variadic - // array path (which itself fans back out to `js_closure_call{N}`). - _ => { - let mut buf: Vec = Vec::with_capacity(dispatch_args_len); - for i in 0..dispatch_args_len { - buf.push(arg_at(i)); - } - js_closure_call_array(closure as i64, buf.as_ptr(), buf.len() as i64) + // Call with the appropriate arity (padded with `undefined` up to the + // body's declared arity). + if dispatch_args_len <= 16 { + let mut buf = [undef; 16]; + for (i, slot) in buf.iter_mut().enumerate().take(dispatch_args_len) { + *slot = arg_at(i); } + return super::calln::dispatch_call_slice(closure, this, &buf[..dispatch_args_len]); } + // >16 args: marshal into a buffer and dispatch via the variadic array + // path. + let buf: Vec = (0..dispatch_args_len).map(arg_at).collect(); + call_array_this(closure, this, buf.as_ptr(), buf.len() as i64) +} + +/// Call a closure with receiver `this` (`JsThis::UNDEFINED` for a plain +/// call) and `args_len` arguments at `args_ptr` — any count, including more +/// than the 16 `js_closure_callN` covers. Takes the closure pointer already +/// unboxed (an integer register), unlike [`js_native_call_value`]. +#[no_mangle] +pub unsafe extern "C" fn js_closure_call_array( + closure_env: i64, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: i64, +) -> f64 { + call_array_this( + closure_env as *const ClosureHeader, + this, + args_ptr, + args_len, + ) } /// Adapter for V8's `native_callback_trampoline` (perry-jsruntime). /// /// `js_create_callback(func_ptr, closure_env, param_count)` registers a JS /// callable whose trampoline invokes `func_ptr(closure_env, args_ptr, -/// args_len)`. Perry closure bodies have signature -/// `(closure_ptr, arg0, arg1, ...)` per arity instead, so the codegen -/// arm for `Expr::JsCreateCallback` (issue #248 Phase 2B) passes -/// `js_closure_call_array` as the trampoline `func_ptr` and the raw -/// `*const ClosureHeader` (NaN-boxing stripped) as `closure_env`. The -/// trampoline then ends up calling THIS function, which dispatches to -/// the right `js_closure_callN` per `args_len`. -/// -/// Mirrors `js_native_call_value` exactly but takes an i64 closure -/// pointer (already unboxed) instead of an f64 NaN-boxed value, so the -/// SysV-x64 / Win64 first-arg register lands in rdi/rcx (integer) -/// rather than xmm0 — matching the trampoline's `extern "C"` int-arg -/// expectation. +/// args_len)` — a contract with no receiver. The codegen arm for +/// `Expr::JsCreateCallback` (issue #248 Phase 2B) passes THIS function as +/// `func_ptr` and the raw `*const ClosureHeader` (NaN-boxing stripped) as +/// `closure_env`; it calls the closure as a plain call. #[no_mangle] -pub unsafe extern "C" fn js_closure_call_array( +pub unsafe extern "C" fn js_closure_v8_callback( closure_env: i64, args_ptr: *const f64, args_len: i64, ) -> f64 { - let closure = closure_env as *const ClosureHeader; + call_array_this( + closure_env as *const ClosureHeader, + crate::closure::JsThis::UNDEFINED, + args_ptr, + args_len, + ) +} + +unsafe fn call_array_this( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: i64, +) -> f64 { if closure.is_null() { throw_not_callable(); } @@ -484,7 +365,7 @@ pub unsafe extern "C" fn js_closure_call_array( tmp.push(unboxed); } } - return dispatch_rest_bundled(closure, fp_for_rest, &tmp, fixed_arity, synth); + return dispatch_rest_bundled(closure, fp_for_rest, this, &tmp, fixed_arity, synth); } // Perry's closure-body arithmetic uses plain `fadd`/`fmul`/etc on // f64 inputs and assumes its arguments arrive as plain doubles, not @@ -516,140 +397,14 @@ pub unsafe extern "C" fn js_closure_call_array( } raw }; + if n <= 16 { + let mut buf = [0.0f64; 16]; + for (i, slot) in buf.iter_mut().enumerate().take(n) { + *slot = a(i); + } + return super::calln::dispatch_call_slice(closure, this, &buf[..n]); + } match n { - 0 => js_closure_call0(closure), - 1 => js_closure_call1(closure, a(0)), - 2 => js_closure_call2(closure, a(0), a(1)), - 3 => js_closure_call3(closure, a(0), a(1), a(2)), - 4 => js_closure_call4(closure, a(0), a(1), a(2), a(3)), - 5 => js_closure_call5(closure, a(0), a(1), a(2), a(3), a(4)), - 6 => js_closure_call6(closure, a(0), a(1), a(2), a(3), a(4), a(5)), - 7 => js_closure_call7(closure, a(0), a(1), a(2), a(3), a(4), a(5), a(6)), - 8 => js_closure_call8(closure, a(0), a(1), a(2), a(3), a(4), a(5), a(6), a(7)), - 9 => js_closure_call9( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - ), - 10 => js_closure_call10( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - ), - 11 => js_closure_call11( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - ), - 12 => js_closure_call12( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - ), - 13 => js_closure_call13( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - ), - 14 => js_closure_call14( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - a(13), - ), - 15 => js_closure_call15( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - a(13), - a(14), - ), - 16 => js_closure_call16( - closure, - a(0), - a(1), - a(2), - a(3), - a(4), - a(5), - a(6), - a(7), - a(8), - a(9), - a(10), - a(11), - a(12), - a(13), - a(14), - a(15), - ), // #3527: arities above 16 can't go through a fixed per-arity // `js_closure_callN` (none exist past 16). Build the full unboxed // arg slice and dispatch through the strategy resolver so the @@ -675,17 +430,17 @@ pub unsafe extern "C" fn js_closure_call_array( throw_not_callable(); } match resolve_strategy(func_ptr).kind() { - DispatchKind::BoundMethod => dispatch_bound_method(closure, &full), + DispatchKind::BoundMethod => dispatch_bound_method(closure, this, &full), DispatchKind::BoundFunction => dispatch_bound_function(closure, &full), DispatchKind::Rest(fixed_arity, synth) => { - dispatch_rest_bundled(closure, func_ptr, &full, fixed_arity, synth) + dispatch_rest_bundled(closure, func_ptr, this, &full, fixed_arity, synth) } DispatchKind::Arity(declared) => { - dispatch_with_arity(closure, func_ptr, &full, declared) + dispatch_with_arity(closure, func_ptr, this, &full, declared) } DispatchKind::Direct => { let width = n.min(crate::closure::MAX_DYNAMIC_CALL_WIDTH) as u32; - dispatch_with_arity(closure, func_ptr, &full, width) + dispatch_with_arity(closure, func_ptr, this, &full, width) } } } @@ -707,6 +462,7 @@ pub unsafe extern "C" fn js_closure_call_array( #[no_mangle] pub unsafe extern "C" fn js_closure_call_apply_with_spread( closure_box: f64, + this: crate::closure::JsThis, regular_args: *const f64, regular_count: i64, spread_arr_handle: i64, @@ -774,5 +530,5 @@ pub unsafe extern "C" fn js_closure_call_apply_with_spread( heap_buf.as_ptr() }; - js_closure_call_array(closure_ptr as i64, buf_ptr, total as i64) + js_closure_call_array(closure_ptr as i64, this, buf_ptr, total as i64) } diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 76bed81da7..80f511d556 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -375,11 +375,10 @@ pub(crate) fn closure_get_dynamic_prop_keyed( return f64::from_bits(crate::value::TAG_UNDEFINED); } let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return crate::closure::js_closure_call0( + closure, + crate::closure::JsThis::from_f64(receiver), + ); } if let Some(val) = closure_get_own_dynamic_prop(ptr, prop) { @@ -449,19 +448,19 @@ pub(crate) fn closure_get_dynamic_prop_keyed( if acc.get == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let getter_bits = clone_closure_rebind_this(acc.get, receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let receiver = + this_scope.root_nanbox_f64(crate::value::js_nanbox_pointer(ptr as i64)); + let getter_bits = clone_closure_rebind_this(acc.get, receiver.get_nanbox_f64()); let getter = (getter_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if getter.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return crate::closure::js_closure_call0( + getter, + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + ); } if let Some(p) = closure_get_own_dynamic_prop(proto_ptr, prop) { return p; @@ -480,18 +479,18 @@ pub(crate) fn closure_get_dynamic_prop_keyed( if acc.get == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let getter_bits = clone_closure_rebind_this(acc.get, receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let receiver = this_scope.root_nanbox_f64(crate::value::js_nanbox_pointer(ptr as i64)); + let getter_bits = clone_closure_rebind_this(acc.get, receiver.get_nanbox_f64()); let getter = (getter_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if getter.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return crate::closure::js_closure_call0( + getter, + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + ); } { // The thread's canonical interned header: no allocation per read, @@ -555,12 +554,10 @@ pub(crate) fn function_prototype_inherited_get( let getter = (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !getter.is_null() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return Some(result); + return Some(crate::closure::js_closure_call0( + getter, + crate::closure::JsThis::from_f64(receiver), + )); } } return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); @@ -740,72 +737,6 @@ pub fn closure_dynamic_props_snapshot(ptr: usize) -> Vec<(String, f64)> { unsafe { super::props::bag_snapshot(ptr) } } -/// Unbind `this` from a detached method closure. -/// -/// When a method is read from an object via PropertyGet (e.g., `const fn = holder.getX`), -/// this function is called on the result. If the value is a closure whose capture_count -/// has CAPTURES_THIS_FLAG set (indicating slot 0 is `this`), it allocates a new closure -/// with the same func_ptr and captures but slot 0 set to undefined. -/// -/// For non-closure values (numbers, strings, objects, arrays), this is a no-op. -#[no_mangle] -pub extern "C" fn js_closure_unbind_this(val: f64) -> f64 { - let bits = val.to_bits(); - let tag = bits & 0xFFFF_0000_0000_0000; - // Only process POINTER_TAG values (closures are NaN-boxed with POINTER_TAG) - if tag != 0x7FFD_0000_0000_0000 { - return val; - } - let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - // #6320: the old `< 0x10000` floor is an order of magnitude below - // `HANDLE_BAND_MAX`, so a registry handle NaN-boxed under POINTER_TAG — most - // sharply a revocable-Proxy id at `0xF0000 + id` — passed it and the - // CLOSURE_MAGIC probe below dereferenced unmapped low memory. Detaching a - // proxy-valued method (`const g = obj.m` where `obj.m = new Proxy(fn, {})`) - // reaches exactly here. `is_closure_ptr` subsumes the band, heap-range, - // alignment and magic checks; a non-closure value has no `this` slot to - // unbind, so it flows through untouched. - if !is_closure_ptr(ptr) { - return val; - } - unsafe { - let header = ptr as *const ClosureHeader; - let raw_count = (*header).capture_count; - // Only unbind if the closure has the CAPTURES_THIS_FLAG - if raw_count & CAPTURES_THIS_FLAG == 0 { - return val; - } - let count = real_capture_count(raw_count) as usize; - if count == 0 { - return val; - } - // Clone the closure with slot 0 set to undefined - let scope = crate::gc::RuntimeHandleScope::new(); - let val_handle = scope.root_nanbox_f64(val); - let func_ptr = (*header).func_ptr; - let new_closure = js_closure_alloc(func_ptr, raw_count); - let source_bits = val_handle.get_nanbox_f64().to_bits(); - let source_ptr = (source_bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if !closure_kind_probe(source_ptr) { - return val_handle.get_nanbox_f64(); - } - let src_captures = closure_capture_slots_mut(source_ptr as *mut ClosureHeader); - let dst_captures = closure_capture_slots_mut(new_closure); - // Set slot 0 to undefined - // GC_STORE_AUDIT(BARRIERED): cloned closure capture stores are followed by layout/barrier rebuild. - *dst_captures = crate::value::TAG_UNDEFINED; - // Copy remaining captures (slots 1..count) - for i in 1..count { - *dst_captures.add(i) = *src_captures.add(i); - } - rebuild_closure_layout_and_barriers(new_closure, count); - super::clone_closure_box_captures(source_ptr as *const ClosureHeader, new_closure); - // NaN-box the new closure pointer - let new_ptr = new_closure as u64; - f64::from_bits(0x7FFD_0000_0000_0000 | (new_ptr & 0x0000_FFFF_FFFF_FFFF)) - } -} - #[cfg(test)] mod tests_1802 { use super::*; diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index ea3980712a..0abe4f6023 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -6,6 +6,7 @@ //! - Followed by captured values (as f64 or i64 pointers) mod alloc; +pub(crate) mod body_call; mod box_captures; mod dispatch; mod dynamic_props; @@ -16,6 +17,8 @@ mod unbox; mod v8_stubs; mod wide_call; +#[cfg(test)] +mod receiver_param_tests; #[cfg(test)] mod tests; @@ -32,6 +35,7 @@ pub use alloc::{ pub(crate) use alloc::{ closure_install_boxed_captures, gc_capture_slot_range, singleton_closure_if_cached, }; +pub use body_call::{call_value, plain_call_receiver, JsThis}; pub use shape::closure_kind_probe; pub(crate) use registry::closure_registry_census; @@ -63,6 +67,7 @@ pub use registry::{ BOUND_METHOD_FUNC_PTR, CAPTURES_THIS_FLAG, NO_THIS_REBIND_FLAG, }; +pub(crate) use dispatch::native_call_value_this; pub(crate) use dispatch::{ bound_function_lazy_name, bound_function_length, bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this, rebind_explicit_this_allocates, @@ -71,11 +76,11 @@ pub(crate) use dispatch::{ pub use dispatch::{ clean_closure_ptr, dispatch_bound_function, dispatch_bound_method, get_valid_func_ptr, js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, js_closure_call12, - js_closure_call13, js_closure_call14, js_closure_call15, js_closure_call16, - js_closure_call1_receiverless, js_closure_call2, js_closure_call3, js_closure_call4, - js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, js_closure_call9, - js_closure_call_apply_with_spread, js_closure_call_array, js_function_bind, - js_native_call_value, throw_not_callable, DirectCall1, DirectCall2, DirectCall3, DirectCall4, + js_closure_call13, js_closure_call14, js_closure_call15, js_closure_call16, js_closure_call2, + js_closure_call3, js_closure_call4, js_closure_call5, js_closure_call6, js_closure_call7, + js_closure_call8, js_closure_call9, js_closure_call_apply_with_spread, js_closure_call_array, + js_function_bind, js_native_call_value, throw_not_callable, DirectCall1, DirectCall2, + DirectCall3, DirectCall4, }; pub use unbox::{js_closure_unbox_callee_checked, js_closure_unbox_callee_checked_rebind}; @@ -102,8 +107,7 @@ pub use dynamic_props::{ closure_delete_own_dynamic_prop, closure_dynamic_props_snapshot, closure_get_dynamic_prop, closure_get_own_dynamic_prop, closure_has_own_dynamic_prop, closure_is_key_deleted, closure_mark_key_deleted, closure_set_dynamic_prop, closure_set_static_prototype, - closure_static_prototype, is_closure_ptr, js_closure_unbind_this, - scan_closure_dynamic_props_roots_mut, + closure_static_prototype, is_closure_ptr, scan_closure_dynamic_props_roots_mut, }; // v8_stubs re-exports the AOT stubs + non-macOS Rust V8-interop stubs. diff --git a/crates/perry-runtime/src/closure/receiver_param_tests.rs b/crates/perry-runtime/src/closure/receiver_param_tests.rs new file mode 100644 index 0000000000..1660da56dc --- /dev/null +++ b/crates/perry-runtime/src/closure/receiver_param_tests.rs @@ -0,0 +1,150 @@ +//! This-as-a-parameter: a JS body is `body(callee, this, a0, ...)` +//! (`perry_abi::JS_BODY_*`), and every runtime route into it passes, as `this`, +//! the receiver its caller supplied — with the JS arguments still in their own +//! positions after it. A plain call supplies `undefined`. +//! +//! Each test calls a probe body with an explicit receiver through one dispatch +//! route (exact arity, padded arity, rest bundling, the padded wide ladder, a +//! hoisted `DirectCallN`), and checks what the probe received. +//! Sabotage: a route passes anything but its `this` argument -> the receiver +//! assertions fail; a route drops the receiver argument (the arguments shift +//! one slot) -> the argument assertions fail. + +use super::*; +use std::cell::{Cell, RefCell}; + +thread_local! { + static SEEN_THIS: Cell = const { Cell::new(0) }; + static SEEN_ARGS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +fn seen() -> (u64, Vec) { + ( + SEEN_THIS.with(Cell::get), + SEEN_ARGS.with(|a| a.borrow().clone()), + ) +} + +fn record(this: JsThis, args: &[f64]) { + SEEN_THIS.with(|c| c.set(this.bits())); + SEEN_ARGS.with(|a| *a.borrow_mut() = args.iter().map(|v| v.to_bits()).collect()); +} + +extern "C" fn probe3(_c: *const ClosureHeader, this: JsThis, a: f64, b: f64, d: f64) -> f64 { + record(this, &[a, b, d]); + a + b +} + +extern "C" fn probe_rest(_c: *const ClosureHeader, this: JsThis, a: f64, rest: f64) -> f64 { + let rest_len = crate::array::js_array_length( + crate::value::js_nanbox_get_pointer(rest) as *const crate::array::ArrayHeader + ); + record(this, &[a, rest_len as f64]); + 0.0 +} + +macro_rules! wide_probe { + ($($p:ident),+) => { + extern "C" fn probe_wide(_c: *const ClosureHeader, this: JsThis, $($p: f64),+) -> f64 { + record(this, &[$($p),+]); + 0.0 + } + }; +} +wide_probe!( + a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, + a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32, a33, a34, a35 +); + +const UNDEF: u64 = crate::value::TAG_UNDEFINED; + +fn receiver() -> f64 { + f64::from_bits(crate::value::JSValue::int32(4242).bits()) +} + +fn this() -> JsThis { + JsThis::from_f64(receiver()) +} + +#[test] +fn an_exact_arity_call_passes_the_receiver_then_the_arguments() { + let closure = js_closure_alloc(probe3 as *const u8, 0); + js_register_closure_arity(probe3 as *const u8, 3); + let r = js_closure_call3(closure, this(), 1.0, 2.0, 3.0); + assert_eq!(r, 3.0); + let (this, args) = seen(); + assert_eq!(this, receiver().to_bits(), "the body's `this` parameter"); + assert_eq!(args, vec![1f64.to_bits(), 2f64.to_bits(), 3f64.to_bits()]); +} + +#[test] +fn a_padded_call_passes_the_receiver_and_pads_after_it() { + let closure = js_closure_alloc(probe3 as *const u8, 0); + js_register_closure_arity(probe3 as *const u8, 3); + js_closure_call1(closure, this(), 9.0); + let (this, args) = seen(); + assert_eq!(this, receiver().to_bits()); + assert_eq!(args, vec![9f64.to_bits(), UNDEF, UNDEF]); +} + +#[test] +fn a_rest_bundled_call_passes_the_receiver_before_the_fixed_arguments() { + let closure = js_closure_alloc(probe_rest as *const u8, 0); + js_register_closure_rest(probe_rest as *const u8, 1); + js_closure_call4(closure, this(), 5.0, 6.0, 7.0, 8.0); + let (this, args) = seen(); + assert_eq!(this, receiver().to_bits()); + assert_eq!( + args, + vec![5f64.to_bits(), 3f64.to_bits()], + "fixed arg, then rest length" + ); +} + +#[test] +fn a_wide_call_passes_the_receiver_and_every_argument_slot() { + let closure = js_closure_alloc(probe_wide as *const u8, 0); + js_register_closure_arity(probe_wide as *const u8, 36); + let args: Vec = (0..36).map(f64::from).collect(); + let r = + unsafe { js_closure_call_array(closure as i64, this(), args.as_ptr(), args.len() as i64) }; + assert_eq!(r, 0.0); + let (this, seen_args) = seen(); + assert_eq!(this, receiver().to_bits()); + assert_eq!( + seen_args, + args.iter().map(|v| v.to_bits()).collect::>() + ); +} + +#[test] +fn a_hoisted_direct_call_passes_the_receiver() { + let closure = js_closure_alloc(probe3 as *const u8, 0); + js_register_closure_arity(probe3 as *const u8, 3); + let site = DirectCall3::resolve(closure); + assert!(site.is_direct(), "the probe resolves to a direct call"); + site.call(closure, this(), 1.0, 2.0, 3.0); + let (this, args) = seen(); + assert_eq!(this, receiver().to_bits()); + assert_eq!(args, vec![1f64.to_bits(), 2f64.to_bits(), 3f64.to_bits()]); +} + +#[test] +fn a_plain_call_passes_undefined_even_inside_a_receiver_call() { + let closure = js_closure_alloc(probe3 as *const u8, 0); + js_register_closure_arity(probe3 as *const u8, 3); + js_closure_call3(closure, this(), 1.0, 2.0, 3.0); + js_closure_call3( + closure, + crate::closure::plain_call_receiver(), + 4.0, + 5.0, + 6.0, + ); + let (this, args) = seen(); + assert_eq!( + this, UNDEF, + "a plain call must not inherit an earlier receiver" + ); + assert_eq!(args, vec![4f64.to_bits(), 5f64.to_bits(), 6f64.to_bits()]); +} diff --git a/crates/perry-runtime/src/closure/registry.rs b/crates/perry-runtime/src/closure/registry.rs index b2b702feed..a08ead8d05 100644 --- a/crates/perry-runtime/src/closure/registry.rs +++ b/crates/perry-runtime/src/closure/registry.rs @@ -436,9 +436,9 @@ fn wasi_exact_arity(func_ptr: *const u8, strategy: DispatchStrategy) -> Dispatch } /// WASI: the number of `f64` parameters `func_ptr` really takes after the -/// closure pointer, asked of the engine with `ref.test` -/// (`ffi/perry_wasi_sig.c`). `None` for a type outside `(closure, f64 x -/// 0..=32) -> f64`. +/// closure pointer and the receiver, asked of the engine with `ref.test` +/// (`ffi/perry_wasi_sig.c`). `None` for a type outside `(closure, this, f64 +/// x 0..=32) -> f64`. #[cfg(target_os = "wasi")] pub(crate) fn wasi_body_params(func_ptr: *const u8) -> Option { extern "C" { @@ -484,24 +484,33 @@ fn invalidate_dispatch_strategy(func_ptr: *const u8) { mod dispatch_recent_tests { use super::*; - extern "C" fn body_a(_: *const ClosureHeader) -> f64 { + extern "C" fn body_a(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 1.0 } - extern "C" fn body_b(_: *const ClosureHeader) -> f64 { + extern "C" fn body_b(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 2.0 } - extern "C" fn body_c(_: *const ClosureHeader) -> f64 { + extern "C" fn body_c(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 3.0 } - extern "C" fn body_d(_: *const ClosureHeader) -> f64 { + extern "C" fn body_d(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 4.0 } - extern "C" fn add_two(_: *const ClosureHeader, left: f64, right: f64) -> f64 { + extern "C" fn add_two( + _: *const ClosureHeader, + _this: crate::closure::JsThis, + left: f64, + right: f64, + ) -> f64 { left + right } - extern "C" fn identify_rest_array(_: *const ClosureHeader, value: f64) -> f64 { + extern "C" fn identify_rest_array( + _: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, + ) -> f64 { let is_pointer = value.to_bits() >> 48 == crate::value::POINTER_TAG >> 48; if is_pointer { 1.0 @@ -581,6 +590,7 @@ mod dispatch_recent_tests { unsafe { crate::closure::js_closure_call_array( closure as i64, + crate::closure::plain_call_receiver(), args.as_ptr(), args.len() as i64, ) @@ -615,6 +625,7 @@ mod dispatch_recent_tests { unsafe { crate::closure::js_closure_call_array( closure as i64, + crate::closure::plain_call_receiver(), direct_arg.as_ptr(), direct_arg.len() as i64, ) @@ -632,6 +643,7 @@ mod dispatch_recent_tests { unsafe { crate::closure::js_closure_call_array( closure as i64, + crate::closure::plain_call_receiver(), rest_args.as_ptr(), rest_args.len() as i64, ) @@ -1129,6 +1141,7 @@ pub unsafe fn build_rest_array_rooted( pub unsafe fn dispatch_rest_bundled( closure: *const ClosureHeader, func_ptr: *const u8, + this: crate::closure::JsThis, args: &[f64], fixed_arity: u32, kind: RestDispatchKind, @@ -1180,229 +1193,31 @@ pub unsafe fn dispatch_rest_bundled( // Use a macro for higher rest arities so this stays in sync with the // documented 0..=15 support and the generated closure-call ceiling. + // One arm per fixed arity: the body takes `(callee, fixed..., rest + // [, arguments])`. Every arm goes through the body-call funnel. macro_rules! rest_arm { - (@ty $i:tt) => { - f64 - }; ($($i:tt),* $(,)?) => {{ if let Some(arguments_double) = all_arguments_double { - #[cfg(panic = "abort")] - let f: extern "C" fn(*const ClosureHeader $(, rest_arm!(@ty $i))*, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - #[cfg(not(panic = "abort"))] - let f: extern "C-unwind" fn(*const ClosureHeader $(, rest_arm!(@ty $i))*, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure $(, a!($i))*, rest_double, arguments_double) + crate::closure::body_call::js_body_call_unwind!( + func_ptr, closure, this $(, a!($i))*, rest_double, arguments_double + ) } else { - #[cfg(panic = "abort")] - let f: extern "C" fn(*const ClosureHeader $(, rest_arm!(@ty $i))*, f64) -> f64 = - std::mem::transmute(func_ptr); - #[cfg(not(panic = "abort"))] - let f: extern "C-unwind" fn(*const ClosureHeader $(, rest_arm!(@ty $i))*, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure $(, a!($i))*, rest_double) + crate::closure::body_call::js_body_call_unwind!( + func_ptr, closure, this $(, a!($i))*, rest_double + ) } }}; } match k { - 0 => { - if let Some(arguments_double) = all_arguments_double { - #[cfg(panic = "abort")] - let f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - #[cfg(not(panic = "abort"))] - let f: extern "C-unwind" fn(*const ClosureHeader, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, rest_double, arguments_double) - } else { - #[cfg(panic = "abort")] - let f: extern "C" fn(*const ClosureHeader, f64) -> f64 = - std::mem::transmute(func_ptr); - #[cfg(not(panic = "abort"))] - let f: extern "C-unwind" fn(*const ClosureHeader, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, rest_double) - } - } - 1 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), rest_double, arguments_double) - } else { - let f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), rest_double) - } - } - 2 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), a!(1), rest_double, arguments_double) - } else { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), a!(1), rest_double) - } - } - 3 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), a!(1), a!(2), rest_double, arguments_double) - } else { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), a!(1), a!(2), rest_double) - } - } - 4 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f( - closure, - a!(0), - a!(1), - a!(2), - a!(3), - rest_double, - arguments_double, - ) - } else { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), a!(1), a!(2), a!(3), rest_double) - } - } - 5 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = std::mem::transmute(func_ptr); - f( - closure, - a!(0), - a!(1), - a!(2), - a!(3), - a!(4), - rest_double, - arguments_double, - ) - } else { - let f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - f(closure, a!(0), a!(1), a!(2), a!(3), a!(4), rest_double) - } - } - 6 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = std::mem::transmute(func_ptr); - f( - closure, - a!(0), - a!(1), - a!(2), - a!(3), - a!(4), - a!(5), - rest_double, - arguments_double, - ) - } else { - let f: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = std::mem::transmute(func_ptr); - f( - closure, - a!(0), - a!(1), - a!(2), - a!(3), - a!(4), - a!(5), - rest_double, - ) - } - } - 7 => { - if let Some(arguments_double) = all_arguments_double { - let f: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = std::mem::transmute(func_ptr); - f( - closure, - a!(0), - a!(1), - a!(2), - a!(3), - a!(4), - a!(5), - a!(6), - rest_double, - arguments_double, - ) - } else { - let f: extern "C" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = std::mem::transmute(func_ptr); - f( - closure, - a!(0), - a!(1), - a!(2), - a!(3), - a!(4), - a!(5), - a!(6), - rest_double, - ) - } - } + 0 => rest_arm!(), + 1 => rest_arm!(0), + 2 => rest_arm!(0, 1), + 3 => rest_arm!(0, 1, 2), + 4 => rest_arm!(0, 1, 2, 3), + 5 => rest_arm!(0, 1, 2, 3, 4), + 6 => rest_arm!(0, 1, 2, 3, 4, 5), + 7 => rest_arm!(0, 1, 2, 3, 4, 5, 6), 8 => rest_arm!(0, 1, 2, 3, 4, 5, 6, 7), 9 => rest_arm!(0, 1, 2, 3, 4, 5, 6, 7, 8), 10 => rest_arm!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), @@ -1423,7 +1238,7 @@ pub unsafe fn dispatch_rest_bundled( slots.push(arguments_double); } let width = slots.len(); - super::dispatch_wide_abi(closure, func_ptr, &slots, width) + super::dispatch_wide_abi(closure, func_ptr, this, &slots, width) } } } @@ -1447,6 +1262,7 @@ pub unsafe fn dispatch_rest_bundled( pub unsafe fn dispatch_with_arity( closure: *const ClosureHeader, func_ptr: *const u8, + this: crate::closure::JsThis, args: &[f64], declared_arity: u32, ) -> f64 { @@ -1462,28 +1278,20 @@ pub unsafe fn dispatch_with_arity( } }; } - // One match arm per declared arity. Each arm transmutes `func_ptr` to - // the concrete `(closure, f64 x N)` signature and forwards the (padded) - // args. Arities up to 32 have exact arms so high-arity closures dispatched + // One match arm per declared arity. Each arm calls the body through the + // body-call funnel with exactly N (padded) JS arguments. Arities up to 32 have exact arms so high-arity closures dispatched // dynamically — e.g. qs's recursive `stringify`, which declares 18 // params and self-calls with 18 args (#3527) — call their body // correctly instead of mis-calling and corrupting registers; wider // bodies take the padded ladder in `wide_call` (#10420). The - // `arm!` macro builds the fn type and the (padded) call args from the - // arg-index token list; `arm!(@ty $i)` maps any index token to `f64`. + // `arm!` macro builds the (padded) call args from the arg-index token list. macro_rules! arm { - (@ty $i:tt) => { f64 }; - ($($i:tt),* $(,)?) => {{ - let f: extern "C" fn(*const ClosureHeader $(, arm!(@ty $i))*) -> f64 = - std::mem::transmute(func_ptr); - f(closure $(, a!($i))*) - }}; + ($($i:tt),* $(,)?) => { + crate::closure::body_call::js_body_call!(func_ptr, closure, this $(, a!($i))*) + }; } match k { - 0 => { - let f: extern "C" fn(*const ClosureHeader) -> f64 = std::mem::transmute(func_ptr); - f(closure) - } + 0 => arm!(), 1 => arm!(0), 2 => arm!(0, 1), 3 => arm!(0, 1, 2), @@ -1546,7 +1354,7 @@ pub unsafe fn dispatch_with_arity( ), // #10420: more than 32 declared params used to return `undefined` // without calling the body. - _ => super::dispatch_wide_abi(closure, func_ptr, args, k), + _ => super::dispatch_wide_abi(closure, func_ptr, this, args, k), } } @@ -1559,19 +1367,23 @@ pub const BOUND_METHOD_FUNC_PTR: *const u8 = 0xBADD_DEAD_u64 as *const u8; /// result: a bound function with a fixed `this`, prepended partial args, and an /// adjusted `.name` / `.length`. When `js_closure_callN` (or `js_native_call_value`) /// detects this sentinel it dispatches via `dispatch_bound_function`, which -/// prepends the bound args, sets `IMPLICIT_THIS` to the bound receiver, and calls -/// the target closure. +/// prepends the bound args and calls the target closure with the bound receiver +/// as its `this`. /// -/// Captures layout: +/// Captures layout (`js_function_bind`, `dispatch/bound.rs`): /// [0] = target closure value (f64, NaN-boxed) /// [1] = bound `this` value (f64) /// [2] = bound-args JS Array pointer (i64; 0 when no partial args) +/// [3] = target name snapshot (for the `bound ` name) +/// [4] = `length` snapshot pub const BOUND_FUNCTION_FUNC_PTR: *const u8 = 0xBADD_B12D_u64 as *const u8; -/// Flag stored in the high bit of capture_count to indicate that capture slot 0 -/// holds `this` (i.e., this closure is an object literal method that captures `this`). -/// When the closure is detached from the object (assigned to a variable via PropertyGet), -/// `js_closure_unbind_this` clones it and clears slot 0 so `this` becomes undefined. +/// Flag stored in the high bit of capture_count: the closure's LAST capture +/// slot (index `real_capture_count - 1`) holds its `this` — an arrow's lexical +/// receiver, or the object an object-literal method was created on (patched +/// by `lower_object_literal`). `clone_closure_rebind_this` rewrites that slot +/// for `call`/`apply`/borrowed-method dispatch unless `NO_THIS_REBIND_FLAG` is +/// also set. pub const CAPTURES_THIS_FLAG: u32 = 0x8000_0000; /// Flag stored in bit 30 of `capture_count` marking a closure whose captured @@ -1615,28 +1427,28 @@ pub(crate) fn closure_registry_census() -> Vec mod body_record_tests { use super::*; - extern "C" fn rec_a(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_a(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 1.0 } - extern "C" fn rec_b(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_b(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 2.0 } - extern "C" fn rec_c(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_c(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 3.0 } - extern "C" fn rec_d(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_d(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 4.0 } - extern "C" fn rec_trusted(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_trusted(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 5.0 } - extern "C" fn rec_versioned(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_versioned(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 6.0 } - extern "C" fn rec_e(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_e(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 7.0 } - extern "C" fn rec_f(_: *const ClosureHeader) -> f64 { + extern "C" fn rec_f(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 8.0 } diff --git a/crates/perry-runtime/src/closure/shape.rs b/crates/perry-runtime/src/closure/shape.rs index 5802ca3120..f37b6f1625 100644 --- a/crates/perry-runtime/src/closure/shape.rs +++ b/crates/perry-runtime/src/closure/shape.rs @@ -470,10 +470,10 @@ mod tests { closure_set_dynamic_prop, closure_set_static_prototype, js_closure_alloc, }; - extern "C" fn plain_body(_c: *const ClosureHeader) -> f64 { + extern "C" fn plain_body(_c: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 1.0 } - extern "C" fn async_body(_c: *const ClosureHeader) -> f64 { + extern "C" fn async_body(_c: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 2.0 } @@ -481,7 +481,7 @@ mod tests { shapes::shape_object_kind_by_id(unsafe { (*c).shape_id }) } - fn fresh(body: extern "C" fn(*const ClosureHeader) -> f64) -> *mut ClosureHeader { + fn fresh(body: crate::closure::body_call::js_body_fn_ty!()) -> *mut ClosureHeader { js_closure_alloc(body as *const u8, 0) } @@ -619,7 +619,13 @@ mod tests { assert!(crate::closure::is_closure_ptr(fresh(plain_body) as usize)); } - extern "C" fn three_arg_body(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { + extern "C" fn three_arg_body( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, + c: f64, + ) -> f64 { a + b + c } @@ -653,7 +659,12 @@ mod tests { "target, this, partial args, name snapshot, bound length" ); // The bound call still sees target + partial args + call args. - let r = crate::closure::js_closure_call2(b as *const ClosureHeader, 2.0, 3.0); + let r = crate::closure::js_closure_call2( + b as *const ClosureHeader, + crate::closure::plain_call_receiver(), + 2.0, + 3.0, + ); assert_eq!(r, 6.0); } diff --git a/crates/perry-runtime/src/closure/tests.rs b/crates/perry-runtime/src/closure/tests.rs index da8448cd1e..491553e50e 100644 --- a/crates/perry-runtime/src/closure/tests.rs +++ b/crates/perry-runtime/src/closure/tests.rs @@ -1,6 +1,9 @@ use super::*; -extern "C" fn test_closure_func(closure: *const ClosureHeader) -> f64 { +extern "C" fn test_closure_func( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let captured = js_closure_get_capture_f64(closure, 0); captured * 2.0 } @@ -9,7 +12,7 @@ extern "C" fn test_closure_func(closure: *const ClosureHeader) -> f64 { fn test_closure_basic() { let closure = js_closure_alloc(test_closure_func as *const u8, 1); js_closure_set_capture_f64(closure, 0, 21.0); - let result = js_closure_call0(closure); + let result = js_closure_call0(closure, crate::closure::plain_call_receiver()); assert_eq!(result, 42.0); } diff --git a/crates/perry-runtime/src/closure/unbox.rs b/crates/perry-runtime/src/closure/unbox.rs index 2646d79520..d86797dceb 100644 --- a/crates/perry-runtime/src/closure/unbox.rs +++ b/crates/perry-runtime/src/closure/unbox.rs @@ -39,7 +39,7 @@ pub extern "C-unwind" fn js_closure_unbox_callee_checked(callee: f64) -> i64 { /// the fused member-call lowering (`o.m(args)` compiled as property-get + /// `js_closure_callN`). An object-literal method carries its `this` baked /// into a reserved capture slot at construction time, and that slot WINS over -/// the `IMPLICIT_THIS` cell codegen sets around the call — so a method +/// the `this` parameter codegen passes to the call — so a method /// inherited through `Object.setPrototypeOf(obj, proto)` ran with `this` /// bound to the PROTO literal instead of the receiver. `js_native_call_method` /// (the by-name dispatcher) already rebinds via `clone_closure_rebind_this`; diff --git a/crates/perry-runtime/src/closure/wide_call.rs b/crates/perry-runtime/src/closure/wide_call.rs index 8104e8001d..2665c886b5 100644 --- a/crates/perry-runtime/src/closure/wide_call.rs +++ b/crates/perry-runtime/src/closure/wide_call.rs @@ -38,6 +38,7 @@ pub(crate) const MAX_DYNAMIC_CALL_WIDTH: usize = 1024; pub(crate) unsafe fn dispatch_wide_abi( closure: *const ClosureHeader, func_ptr: *const u8, + this: crate::closure::JsThis, args: &[f64], width: usize, ) -> f64 { @@ -47,7 +48,7 @@ pub(crate) unsafe fn dispatch_wide_abi( // even a valid wasm component). Say so instead. #[cfg(target_os = "wasi")] { - let _ = (closure, func_ptr, args); + let _ = (closure, func_ptr, this, args); throw_too_wide_for_wasi(width) } #[cfg(not(target_os = "wasi"))] @@ -56,22 +57,15 @@ pub(crate) unsafe fn dispatch_wide_abi( let provided = args.len().min(width); // `padded!(slots, [0], 1, + + …)` doubles the index list once per `+` - // (six doublings = 64 indices) and emits the transmuted call with one - // `f64` parameter per index. The indices are constant expressions into a + // (six doublings = 64 indices) and emits the body call with one + // `f64` argument per index. The indices are constant expressions into a // fixed-size array, so the loads carry no bounds checks. macro_rules! padded { - (@f64 $i:expr) => { f64 }; ($slots:ident, [$($i:expr),+], $step:expr, + $($more:tt)*) => { padded!($slots, [$($i,)+ $($i + $step),+], $step * 2, $($more)*) }; ($slots:ident, [$($i:expr),+], $step:expr,) => {{ - #[cfg(panic = "abort")] - let f: extern "C" fn(*const ClosureHeader $(, padded!(@f64 $i))+) -> f64 = - std::mem::transmute(func_ptr); - #[cfg(not(panic = "abort"))] - let f: extern "C-unwind" fn(*const ClosureHeader $(, padded!(@f64 $i))+) -> f64 = - std::mem::transmute(func_ptr); - f(closure $(, $slots[$i])+) + crate::closure::body_call::js_body_call_unwind!(func_ptr, closure, this $(, $slots[$i])+) }}; } macro_rules! fill { @@ -133,6 +127,7 @@ mod tests { extern "C" fn body_40( _: *const ClosureHeader, + _this: crate::closure::JsThis, a0: f64, a1: f64, a2: f64, @@ -212,7 +207,15 @@ mod tests { let args: Vec = (0..40).map(|i| i as f64 + 100.0).collect(); // Exactly the declared width, through each ladder rung. for width in [40usize, 64, 65, 256, 257, 1024] { - let result = unsafe { dispatch_wide_abi(std::ptr::null(), body, &args, width) }; + let result = unsafe { + dispatch_wide_abi( + std::ptr::null(), + body, + crate::closure::JsThis::UNDEFINED, + &args, + width, + ) + }; assert_eq!(result, expected(40), "width {width}"); } } @@ -221,7 +224,15 @@ mod tests { fn missing_slots_are_padded_with_undefined() { let body = body_40 as *const u8; let args: Vec = (0..23).map(|i| i as f64 + 100.0).collect(); - let result = unsafe { dispatch_wide_abi(std::ptr::null(), body, &args, 40) }; + let result = unsafe { + dispatch_wide_abi( + std::ptr::null(), + body, + crate::closure::JsThis::UNDEFINED, + &args, + 40, + ) + }; assert_eq!(result, expected(23)); } @@ -231,13 +242,21 @@ mod tests { fn declared_arity_past_the_exact_arms_reaches_the_body() { let body = body_40 as *const u8; let args: Vec = (0..40).map(|i| i as f64 + 100.0).collect(); - let result = - unsafe { crate::closure::dispatch_with_arity(std::ptr::null(), body, &args, 40) }; + let result = unsafe { + crate::closure::dispatch_with_arity( + std::ptr::null(), + body, + crate::closure::JsThis::UNDEFINED, + &args, + 40, + ) + }; assert_eq!(result, expected(40)); } extern "C" fn rest_after_16( _: *const ClosureHeader, + _this: crate::closure::JsThis, a0: f64, a1: f64, a2: f64, @@ -275,6 +294,7 @@ mod tests { crate::closure::dispatch_rest_bundled( std::ptr::null(), body, + crate::closure::JsThis::UNDEFINED, &args, 16, crate::closure::registry::RestDispatchKind::UserRest, @@ -289,7 +309,15 @@ mod tests { let body = body_40 as *const u8; let args: Vec = (0..40).map(|i| i as f64 + 100.0).collect(); // A width of 30 means the body only owns 30 slots; 30..40 read as undefined. - let result = unsafe { dispatch_wide_abi(std::ptr::null(), body, &args, 30) }; + let result = unsafe { + dispatch_wide_abi( + std::ptr::null(), + body, + crate::closure::JsThis::UNDEFINED, + &args, + 30, + ) + }; assert_eq!(result, expected(30)); } } diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index 3aad7432a2..4e12a5c36b 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -18,8 +18,8 @@ use std::sync::{Once, OnceLock}; use crate::array::ArrayHeader; use crate::closure::{js_closure_get_capture_f64, ClosureHeader}; use crate::object::{ - js_implicit_this_set, js_object_alloc, js_object_delete_field, js_object_get_field_by_name_f64, - js_object_keys, js_object_set_field_by_name, ObjectHeader, + js_object_alloc, js_object_delete_field, js_object_get_field_by_name_f64, js_object_keys, + js_object_set_field_by_name, ObjectHeader, }; use crate::string::{js_string_from_bytes, StringHeader}; use crate::value::JSValue; @@ -343,16 +343,16 @@ pub(crate) fn cluster_emit_event(event: &str, args: &[f64]) -> bool { if listeners.is_empty() { return false; } - let this_scope = crate::gc::RuntimeHandleScope::new(); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for listener in listeners { let cb = f64::from_bits(listener.callback_bits); - js_implicit_this_set(cluster_default_value()); unsafe { - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); + let _ = crate::closure::native_call_value_this( + cb, + crate::closure::JsThis::from_f64(cluster_default_value()), + args.as_ptr(), + args.len(), + ); } - js_implicit_this_set(prev.get_nanbox_f64()); } true } @@ -809,43 +809,58 @@ pub(crate) fn is_worker_instance_value(value: f64) -> bool { extern "C" fn cluster_worker_send( _closure: *const ClosureHeader, + this: crate::closure::JsThis, message: f64, a2: f64, a3: f64, a4: f64, ) -> f64 { - if is_self_worker_value(crate::object::js_implicit_this_get()) { + if is_self_worker_value(this.as_f64()) { return crate::process::process_ipc_send_call(message, a2, a3, a4); } - crate::child_process::cp_method_send(std::ptr::null(), message, a2, a3, a4) + crate::child_process::cp_method_send(std::ptr::null(), this, message, a2, a3, a4) } -extern "C" fn cluster_worker_kill(_closure: *const ClosureHeader, signal: f64) -> f64 { - if is_self_worker_value(crate::object::js_implicit_this_get()) { +extern "C" fn cluster_worker_kill( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + signal: f64, +) -> f64 { + if is_self_worker_value(this.as_f64()) { let _ = crate::os::js_process_kill(crate::os::js_process_pid(), signal); return TAG_UNDEFINED_F64; } - let _ = crate::child_process::cp_method_kill(std::ptr::null(), signal); + let _ = crate::child_process::cp_method_kill(std::ptr::null(), this, signal); TAG_UNDEFINED_F64 } -extern "C" fn cluster_worker_destroy(_closure: *const ClosureHeader, signal: f64) -> f64 { - cluster_worker_kill(std::ptr::null(), signal) +extern "C" fn cluster_worker_destroy( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + signal: f64, +) -> f64 { + cluster_worker_kill(std::ptr::null(), this, signal) } -extern "C" fn cluster_worker_disconnect(_closure: *const ClosureHeader) -> f64 { - let worker = crate::object::js_implicit_this_get(); +extern "C" fn cluster_worker_disconnect( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let worker = this.as_f64(); set_field(worker, b"exitedAfterDisconnect", TAG_TRUE_F64); if is_self_worker_value(worker) { let _ = crate::process::process_ipc_disconnect_call(); } else { - let _ = crate::child_process::cp_method_disconnect(std::ptr::null()); + let _ = crate::child_process::cp_method_disconnect(std::ptr::null(), this); } worker } -extern "C" fn cluster_worker_is_connected(_closure: *const ClosureHeader) -> f64 { - let worker = crate::object::js_implicit_this_get(); +extern "C" fn cluster_worker_is_connected( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let worker = this.as_f64(); let connected = if is_self_worker_value(worker) { crate::process::ipc::process_ipc_property("connected").unwrap_or(TAG_FALSE_F64) } else { @@ -858,8 +873,11 @@ extern "C" fn cluster_worker_is_connected(_closure: *const ClosureHeader) -> f64 } } -extern "C" fn cluster_worker_is_dead(_closure: *const ClosureHeader) -> f64 { - if is_worker_dead(crate::object::js_implicit_this_get()) { +extern "C" fn cluster_worker_is_dead( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + if is_worker_dead(this.as_f64()) { TAG_TRUE_F64 } else { TAG_FALSE_F64 @@ -950,7 +968,10 @@ fn apply_setup_primary(settings_arg: f64) { crate::timer::js_set_immediate_callback(deferred as i64); } -extern "C" fn cluster_setup_emit_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_setup_emit_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let settings = js_closure_get_capture_f64(closure, 0); cluster_emit_event("setup", &[settings]); TAG_UNDEFINED_F64 @@ -962,7 +983,10 @@ fn defer_cluster_fork_event(worker: f64) { crate::timer::js_set_immediate_callback(deferred as i64); } -extern "C" fn cluster_fork_emit_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_fork_emit_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let worker = js_closure_get_capture_f64(closure, 0); cluster_emit_event("fork", &[worker]); TAG_UNDEFINED_F64 @@ -1316,25 +1340,43 @@ fn drain_disconnect_callbacks_if_idle() { } } -extern "C" fn cluster_callback_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_callback_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let callback = f64::from_bits(crate::closure::js_closure_get_capture_ptr(closure, 0) as u64); unsafe { - let _ = crate::closure::js_native_call_value(callback, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } TAG_UNDEFINED_F64 } -extern "C" fn cluster_internal_online(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_internal_online( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { mark_worker_online(closure_this(closure)); TAG_UNDEFINED_F64 } -extern "C" fn cluster_internal_disconnect(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_internal_disconnect( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { mark_worker_disconnected(closure_this(closure)); TAG_UNDEFINED_F64 } -extern "C" fn cluster_internal_message(closure: *const ClosureHeader, message: f64) -> f64 { +extern "C" fn cluster_internal_message( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + message: f64, +) -> f64 { cluster_emit_event("message", &[closure_this(closure), message]); TAG_UNDEFINED_F64 } @@ -1355,13 +1397,21 @@ fn mark_worker_disconnected(worker: f64) { crate::timer::js_set_immediate_callback(deferred as i64); } -extern "C" fn cluster_disconnect_emit_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_disconnect_emit_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let worker = js_closure_get_capture_f64(closure, 0); cluster_emit_event("disconnect", &[worker]); TAG_UNDEFINED_F64 } -extern "C" fn cluster_internal_exit(closure: *const ClosureHeader, code: f64, signal: f64) -> f64 { +extern "C" fn cluster_internal_exit( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + code: f64, + signal: f64, +) -> f64 { let worker = closure_this(closure); if JSValue::from_bits(get_field(worker, b"exitedAfterDisconnect").to_bits()).is_undefined() { set_field(worker, b"exitedAfterDisconnect", TAG_FALSE_F64); @@ -1395,12 +1445,20 @@ fn invoke_disconnect_callbacks_if_idle() { for bits in callbacks { let callback = f64::from_bits(bits); unsafe { - let _ = crate::closure::js_native_call_value(callback, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } } } -extern "C" fn cluster_exit_emit_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn cluster_exit_emit_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let worker = js_closure_get_capture_f64(closure, 0); let code = js_closure_get_capture_f64(closure, 1); let signal = js_closure_get_capture_f64(closure, 2); @@ -1412,7 +1470,12 @@ fn call_worker_disconnect(worker: f64) { let disconnect = get_field(worker, b"disconnect"); if is_closure_value(disconnect) { unsafe { - let _ = crate::closure::js_native_call_value(disconnect, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + disconnect, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } } } @@ -1459,21 +1522,25 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { let mut i = 0; let mut fired = false; let this_scope = crate::gc::RuntimeHandleScope::new(); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + // A listener can collect, so the emitter is rooted ONCE here and re-read + // at each use. + let target_handle = this_scope.root_nanbox_f64(target); loop { - let Some(arr) = array_ptr(get_field(target, &key)) else { + let Some(arr) = array_ptr(get_field(target_handle.get_nanbox_f64(), &key)) else { break; }; if i >= crate::array::js_array_length(arr) { break; } let cb = crate::array::js_array_get_f64(arr, i); - js_implicit_this_set(target); unsafe { - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); + let _ = crate::closure::native_call_value_this( + cb, + crate::closure::JsThis::from_f64(target_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ); } - js_implicit_this_set(prev.get_nanbox_f64()); fired = true; i += 1; } diff --git a/crates/perry-runtime/src/collection_iter.rs b/crates/perry-runtime/src/collection_iter.rs index 1e64d79e15..5dac128736 100644 --- a/crates/perry-runtime/src/collection_iter.rs +++ b/crates/perry-runtime/src/collection_iter.rs @@ -283,13 +283,14 @@ pub(crate) fn call_with_this_capturing_throw( this_value: f64, args: &[f64], ) -> Result { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); - let result = call_capturing_throw(|| unsafe { - crate::closure::js_native_call_value(callee, args.as_ptr(), args.len()) - }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + call_capturing_throw(|| unsafe { + crate::closure::native_call_value_this( + callee, + crate::closure::JsThis::from_f64(this_value), + args.as_ptr(), + args.len(), + ) + }) } pub(crate) fn iterator_close(iter: f64) { diff --git a/crates/perry-runtime/src/dgram.rs b/crates/perry-runtime/src/dgram.rs index 1f7e38cc79..4ce677afd4 100644 --- a/crates/perry-runtime/src/dgram.rs +++ b/crates/perry-runtime/src/dgram.rs @@ -112,7 +112,7 @@ pub(crate) const KEY_REACTOR_ID: &[u8] = b"__perryDgramReactorId"; pub(crate) const KEY_ASYNC_ID: &[u8] = b"__perryDgramAsyncId"; pub(crate) const KEY_TRIGGER_ASYNC_ID: &[u8] = b"__perryDgramTriggerAsyncId"; -type MethodThunk = extern "C" fn(*const ClosureHeader, f64) -> f64; +type MethodThunk = crate::closure::body_call::js_body_fn_ty!(a); struct MethodSpec { name: &'static str, @@ -499,14 +499,16 @@ pub(crate) fn collect_rest_args(rest: f64) -> Vec { collect_args(raw as *const ArrayHeader) } -pub(crate) fn this_value(closure: *const ClosureHeader) -> f64 { +/// The socket a method thunk acts on: the socket the method was bound to +/// (capture 0), else the call's receiver. +pub(crate) fn this_value(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { if !closure.is_null() { let bits = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; if bits != 0 { return f64::from_bits(bits); } } - crate::object::js_implicit_this_get() + this.as_f64() } pub(crate) fn socket_value_from_handle(handle: i64) -> f64 { @@ -580,8 +582,11 @@ pub(crate) fn socket_async_ids(socket: f64) -> crate::async_hooks::AsyncResource } } -extern "C" fn dgram_async_dispose(closure: *const ClosureHeader) -> f64 { - close_impl(this_value(closure), &[]); +extern "C" fn dgram_async_dispose( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + close_impl(this_value(closure, this), &[]); let promise = crate::promise::js_promise_resolved(undefined_value()); boxed_pointer(promise as *const u8) } @@ -818,12 +823,14 @@ pub(crate) fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined_value(); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); - let result = - unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + callback, + crate::closure::JsThis::from_f64(this), + args.as_ptr(), + args.len(), + ) + } } #[cfg(test)] diff --git a/crates/perry-runtime/src/dgram/net.rs b/crates/perry-runtime/src/dgram/net.rs index fc313e7448..b335c9b44e 100644 --- a/crates/perry-runtime/src/dgram/net.rs +++ b/crates/perry-runtime/src/dgram/net.rs @@ -481,7 +481,10 @@ pub(crate) fn finish_send(socket: f64, args: &[f64], outcome: Result undefined_value() } -extern "C" fn deferred_send_callback(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn deferred_send_callback( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let callback = crate::closure::js_closure_get_capture_f64(closure, 0); let socket = crate::closure::js_closure_get_capture_f64(closure, 1); let error = crate::closure::js_closure_get_capture_f64(closure, 2); diff --git a/crates/perry-runtime/src/dgram/ops.rs b/crates/perry-runtime/src/dgram/ops.rs index 813b995683..76711a6fbf 100644 --- a/crates/perry-runtime/src/dgram/ops.rs +++ b/crates/perry-runtime/src/dgram/ops.rs @@ -9,7 +9,10 @@ use super::*; use std::net::Ipv4Addr; -extern "C" fn dgram_abort_close_task(closure: *const ClosureHeader) -> f64 { +extern "C" fn dgram_abort_close_task( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return undefined_value(); } @@ -26,7 +29,10 @@ fn schedule_abort_close(socket: f64) { crate::builtins::js_queue_microtask(task as i64); } -extern "C" fn dgram_abort_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn dgram_abort_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if !closure.is_null() { schedule_abort_close(js_closure_get_capture_f64(closure, 0)); } @@ -485,7 +491,11 @@ fn send_blocked(socket: f64, address: &str) -> bool { )) != 0 } -extern "C" fn lookup_callback(_closure: *const ClosureHeader, _rest: f64) -> f64 { +extern "C" fn lookup_callback( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _rest: f64, +) -> f64 { undefined_value() } diff --git a/crates/perry-runtime/src/dgram/thunks.rs b/crates/perry-runtime/src/dgram/thunks.rs index d03771fbe3..34d6013eec 100644 --- a/crates/perry-runtime/src/dgram/thunks.rs +++ b/crates/perry-runtime/src/dgram/thunks.rs @@ -8,43 +8,76 @@ use super::*; use crate::closure::ClosureHeader; -pub(crate) extern "C" fn dgram_send_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - send_impl(this_value(closure), &collect_rest_args(rest)) +pub(crate) extern "C" fn dgram_send_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + send_impl(this_value(closure, this), &collect_rest_args(rest)) } -pub(crate) extern "C" fn dgram_sendto_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - sendto_impl(this_value(closure), &collect_rest_args(rest)) +pub(crate) extern "C" fn dgram_sendto_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + sendto_impl(this_value(closure, this), &collect_rest_args(rest)) } -pub(crate) extern "C" fn dgram_bind_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - bind_impl(this_value(closure), &collect_rest_args(rest)) +pub(crate) extern "C" fn dgram_bind_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + bind_impl(this_value(closure, this), &collect_rest_args(rest)) } -pub(crate) extern "C" fn dgram_close_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - close_impl(this_value(closure), &collect_rest_args(rest)) +pub(crate) extern "C" fn dgram_close_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + close_impl(this_value(closure, this), &collect_rest_args(rest)) } -pub(crate) extern "C" fn dgram_address_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - address_impl(this_value(closure)) +pub(crate) extern "C" fn dgram_address_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _rest: f64, +) -> f64 { + address_impl(this_value(closure, this)) } pub(crate) extern "C" fn dgram_remote_address_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, _rest: f64, ) -> f64 { - remote_address_impl(this_value(closure)) + remote_address_impl(this_value(closure, this)) } -pub(crate) extern "C" fn dgram_connect_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - connect_impl(this_value(closure), &collect_rest_args(rest)) +pub(crate) extern "C" fn dgram_connect_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + connect_impl(this_value(closure, this), &collect_rest_args(rest)) } -pub(crate) extern "C" fn dgram_disconnect_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - disconnect_impl(this_value(closure)) +pub(crate) extern "C" fn dgram_disconnect_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _rest: f64, +) -> f64 { + disconnect_impl(this_value(closure, this)) } -pub(crate) extern "C" fn dgram_on_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); +pub(crate) extern "C" fn dgram_on_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + let socket = this_value(closure, this); let args = collect_rest_args(rest); let event = args.first().copied().unwrap_or_else(undefined_value); let listener = args.get(1).copied().unwrap_or_else(undefined_value); @@ -52,8 +85,12 @@ pub(crate) extern "C" fn dgram_on_thunk(closure: *const ClosureHeader, rest: f64 socket } -pub(crate) extern "C" fn dgram_once_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); +pub(crate) extern "C" fn dgram_once_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + let socket = this_value(closure, this); let args = collect_rest_args(rest); let event = args.first().copied().unwrap_or_else(undefined_value); let listener = args.get(1).copied().unwrap_or_else(undefined_value); @@ -63,9 +100,10 @@ pub(crate) extern "C" fn dgram_once_thunk(closure: *const ClosureHeader, rest: f pub(crate) extern "C" fn dgram_remove_listener_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let socket = this_value(closure); + let socket = this_value(closure, this); let args = collect_rest_args(rest); if args.len() >= 2 { remove_listener(socket, args[0], args[1]); @@ -73,8 +111,12 @@ pub(crate) extern "C" fn dgram_remove_listener_thunk( socket } -pub(crate) extern "C" fn dgram_emit_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - let socket = this_value(closure); +pub(crate) extern "C" fn dgram_emit_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + let socket = this_value(closure, this); let args = collect_rest_args(rest); let event = args.first().copied().unwrap_or_else(undefined_value); let emitted = emit_event_value(socket, event, args.get(1..).unwrap_or(&[])); @@ -83,23 +125,29 @@ pub(crate) extern "C" fn dgram_emit_thunk(closure: *const ClosureHeader, rest: f pub(crate) extern "C" fn dgram_listener_count_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { let args = collect_rest_args(rest); let event = args.first().copied().unwrap_or_else(undefined_value); - listener_snapshot(this_value(closure), event).len() as f64 + listener_snapshot(this_value(closure, this), event).len() as f64 } -pub(crate) extern "C" fn dgram_event_names_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - event_names_impl(this_value(closure)) +pub(crate) extern "C" fn dgram_event_names_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _rest: f64, +) -> f64 { + event_names_impl(this_value(closure, this)) } pub(crate) extern "C" fn dgram_add_membership_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { membership_impl( - this_value(closure), + this_value(closure, this), &collect_rest_args(rest), "addMembership", ) @@ -107,10 +155,11 @@ pub(crate) extern "C" fn dgram_add_membership_thunk( pub(crate) extern "C" fn dgram_drop_membership_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { membership_impl( - this_value(closure), + this_value(closure, this), &collect_rest_args(rest), "dropMembership", ) @@ -118,10 +167,11 @@ pub(crate) extern "C" fn dgram_drop_membership_thunk( pub(crate) extern "C" fn dgram_add_source_membership_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { source_membership_impl( - this_value(closure), + this_value(closure, this), &collect_rest_args(rest), "addSourceSpecificMembership", ) @@ -129,10 +179,11 @@ pub(crate) extern "C" fn dgram_add_source_membership_thunk( pub(crate) extern "C" fn dgram_drop_source_membership_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { source_membership_impl( - this_value(closure), + this_value(closure, this), &collect_rest_args(rest), "dropSourceSpecificMembership", ) @@ -140,42 +191,51 @@ pub(crate) extern "C" fn dgram_drop_source_membership_thunk( pub(crate) extern "C" fn dgram_set_broadcast_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - set_broadcast_impl(this_value(closure), &collect_rest_args(rest)) + set_broadcast_impl(this_value(closure, this), &collect_rest_args(rest)) } pub(crate) extern "C" fn dgram_set_multicast_ttl_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - set_multicast_ttl_impl(this_value(closure), &collect_rest_args(rest)) + set_multicast_ttl_impl(this_value(closure, this), &collect_rest_args(rest)) } pub(crate) extern "C" fn dgram_set_multicast_loopback_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - set_multicast_loopback_impl(this_value(closure), &collect_rest_args(rest)) + set_multicast_loopback_impl(this_value(closure, this), &collect_rest_args(rest)) } pub(crate) extern "C" fn dgram_set_multicast_interface_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - set_multicast_interface_impl(this_value(closure), &collect_rest_args(rest)) + set_multicast_interface_impl(this_value(closure, this), &collect_rest_args(rest)) } -pub(crate) extern "C" fn dgram_set_ttl_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { - set_ttl_impl(this_value(closure), &collect_rest_args(rest)) +pub(crate) extern "C" fn dgram_set_ttl_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + set_ttl_impl(this_value(closure, this), &collect_rest_args(rest)) } pub(crate) extern "C" fn dgram_set_recv_buffer_size_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { set_buffer_size_impl( - this_value(closure), + this_value(closure, this), &collect_rest_args(rest), KEY_RECV_BUFFER_SIZE, "uv_recv_buffer_size", @@ -184,10 +244,11 @@ pub(crate) extern "C" fn dgram_set_recv_buffer_size_thunk( pub(crate) extern "C" fn dgram_set_send_buffer_size_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { set_buffer_size_impl( - this_value(closure), + this_value(closure, this), &collect_rest_args(rest), KEY_SEND_BUFFER_SIZE, "uv_send_buffer_size", @@ -196,10 +257,11 @@ pub(crate) extern "C" fn dgram_set_send_buffer_size_thunk( pub(crate) extern "C" fn dgram_get_recv_buffer_size_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, _rest: f64, ) -> f64 { get_buffer_size_impl( - this_value(closure), + this_value(closure, this), KEY_RECV_BUFFER_SIZE, "uv_recv_buffer_size", ) @@ -207,23 +269,36 @@ pub(crate) extern "C" fn dgram_get_recv_buffer_size_thunk( pub(crate) extern "C" fn dgram_get_send_buffer_size_thunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, _rest: f64, ) -> f64 { get_buffer_size_impl( - this_value(closure), + this_value(closure, this), KEY_SEND_BUFFER_SIZE, "uv_send_buffer_size", ) } -pub(crate) extern "C" fn dgram_ref_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - ref_impl(this_value(closure), true) +pub(crate) extern "C" fn dgram_ref_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _rest: f64, +) -> f64 { + ref_impl(this_value(closure, this), true) } -pub(crate) extern "C" fn dgram_unref_thunk(closure: *const ClosureHeader, _rest: f64) -> f64 { - ref_impl(this_value(closure), false) +pub(crate) extern "C" fn dgram_unref_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _rest: f64, +) -> f64 { + ref_impl(this_value(closure, this), false) } -pub(crate) extern "C" fn dgram_zero_thunk(_closure: *const ClosureHeader, _rest: f64) -> f64 { +pub(crate) extern "C" fn dgram_zero_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _rest: f64, +) -> f64 { 0.0 } diff --git a/crates/perry-runtime/src/disposable.rs b/crates/perry-runtime/src/disposable.rs index 268d79fe71..426bce4e2b 100644 --- a/crates/perry-runtime/src/disposable.rs +++ b/crates/perry-runtime/src/disposable.rs @@ -32,11 +32,12 @@ use crate::array::{js_array_alloc, js_array_get_f64, js_array_length, js_array_push_f64}; use crate::closure::{ is_closure_ptr, js_closure_alloc, js_closure_call0, js_closure_get_capture_ptr, - js_closure_set_capture_ptr, js_native_call_value, js_register_closure_arity, ClosureHeader, + js_closure_set_capture_ptr, js_register_closure_arity, native_call_value_this, ClosureHeader, + JsThis, }; use crate::object::{ - js_implicit_this_set, js_object_alloc, js_object_get_field_f64, js_object_set_field_by_name, - js_object_set_field_f64, js_register_class_extends_error, + js_object_alloc, js_object_get_field_f64, js_object_set_field_by_name, js_object_set_field_f64, + js_register_class_extends_error, }; use crate::string::js_string_from_bytes; use crate::value::{ @@ -97,17 +98,16 @@ fn resolve_dispose_method(resource: f64, want_async: bool) -> f64 { // read `this` observe the resource. Used by `stack.use(resource)`. // --------------------------------------------------------------------------- -extern "C" fn bound_dispose_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn bound_dispose_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let method = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let resource = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); if !is_callable_value(method) { return undefined(); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(resource)); - let result = unsafe { js_native_call_value(method, std::ptr::null(), 0) }; - js_implicit_this_set(prev.get_nanbox_f64()); - result + unsafe { native_call_value_this(method, JsThis::from_f64(resource), std::ptr::null(), 0) } } fn make_bound_dispose_thunk(method: f64, resource: f64) -> f64 { @@ -216,7 +216,7 @@ fn call_disposer(callable: f64) { if ptr.is_null() { return; } - js_closure_call0(ptr); + js_closure_call0(ptr, crate::closure::plain_call_receiver()); } /// Run every registered disposer in LIFO order and clear the array. Marks the @@ -245,14 +245,17 @@ fn run_disposers(stack: *mut ObjectHeader) { // `adopt` closure: captures (value, onDispose) and calls onDispose(value). // --------------------------------------------------------------------------- -extern "C" fn adopt_disposer_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn adopt_disposer_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let value = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let on_dispose = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); let cb = JSValue::from_bits(on_dispose.to_bits()); if cb.is_pointer() { let cb_ptr = js_nanbox_get_pointer(on_dispose) as *const ClosureHeader; if !cb_ptr.is_null() { - crate::closure::js_closure_call1(cb_ptr, value); + crate::closure::js_closure_call1(cb_ptr, crate::closure::plain_call_receiver(), value); } } undefined() diff --git a/crates/perry-runtime/src/dns.rs b/crates/perry-runtime/src/dns.rs index ea60a53806..ac25e4c635 100644 --- a/crates/perry-runtime/src/dns.rs +++ b/crates/perry-runtime/src/dns.rs @@ -1206,16 +1206,24 @@ fn dns_promise_reverse(args: i64) -> f64 { } } -extern "C" fn dns_noop_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn dns_noop_thunk(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { undefined_value() } -extern "C" fn dns_noop2_thunk(_closure: *const ClosureHeader, _a: f64, _b: f64) -> f64 { +extern "C" fn dns_noop2_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _a: f64, + _b: f64, +) -> f64 { undefined_value() } -extern "C" fn dns_resolver_get_servers_thunk(_closure: *const ClosureHeader) -> f64 { - let this_value = crate::object::js_implicit_this_get(); +extern "C" fn dns_resolver_get_servers_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this_value = this.as_f64(); let Some(obj) = resolver_object_from_value(this_value) else { return empty_array_value(); }; @@ -1224,9 +1232,10 @@ extern "C" fn dns_resolver_get_servers_thunk(_closure: *const ClosureHeader) -> extern "C" fn dns_resolver_set_servers_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, servers_value: f64, ) -> f64 { - let this_value = crate::object::js_implicit_this_get(); + let this_value = this.as_f64(); let Some(obj) = resolver_object_from_value(this_value) else { return dns_promises_set_servers_value(servers_value); }; diff --git a/crates/perry-runtime/src/dyn_eval/bench.rs b/crates/perry-runtime/src/dyn_eval/bench.rs index b206b24260..659b93335d 100644 --- a/crates/perry-runtime/src/dyn_eval/bench.rs +++ b/crates/perry-runtime/src/dyn_eval/bench.rs @@ -21,7 +21,14 @@ use super::dyn_function_from_strings; use super::{root_get, root_push, roots_truncate}; fn call(f: f64, args: &[f64]) -> f64 { - unsafe { crate::closure::js_native_call_value(f, args.as_ptr(), args.len()) } + unsafe { + crate::closure::js_native_call_value( + f, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + } } fn env_usize(key: &str, default: usize) -> usize { diff --git a/crates/perry-runtime/src/dyn_eval/bridge.rs b/crates/perry-runtime/src/dyn_eval/bridge.rs index 56809a4a08..2a420db441 100644 --- a/crates/perry-runtime/src/dyn_eval/bridge.rs +++ b/crates/perry-runtime/src/dyn_eval/bridge.rs @@ -250,17 +250,17 @@ pub(crate) fn set_member(base: f64, name: &str, value: f64) { /// Plain call `f(args…)` with an explicit `this` (undefined for ordinary /// calls). Mirrors the runtime's own `Function.prototype.call` plumbing: -/// arm the implicit-`this` slot, rebind explicit-`this` closures, dispatch -/// through the arity-aware value-call tower, restore. +/// pass the receiver explicitly and dispatch through the arity-aware +/// value-call tower. pub(crate) fn call_function(callee: f64, this: f64, args: &[f64]) -> f64 { - let prev = crate::object::js_implicit_this_set(this); - let prev_idx = root_push(prev); - let result = unsafe { crate::closure::js_native_call_value(callee, args.as_ptr(), args.len()) }; - let result_idx = root_push(result); - crate::object::js_implicit_this_set(root_get(prev_idx)); - let result = root_get(result_idx); - roots_truncate(prev_idx); - result + unsafe { + crate::closure::native_call_value_this( + callee, + crate::closure::JsThis::from_f64(this), + args.as_ptr(), + args.len(), + ) + } } /// Method call `base.name(args…)` — the generic dispatch tower compiled code diff --git a/crates/perry-runtime/src/dyn_eval/interp.rs b/crates/perry-runtime/src/dyn_eval/interp.rs index 63fed8ff81..58f74b7c93 100644 --- a/crates/perry-runtime/src/dyn_eval/interp.rs +++ b/crates/perry-runtime/src/dyn_eval/interp.rs @@ -275,23 +275,32 @@ fn ensure_thunk_registered() { #[cfg(not(panic = "abort"))] extern "C-unwind" fn interp_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, raw_args: f64, ) -> f64 { - interp_thunk_impl(closure, raw_args) + interp_thunk_impl(this, closure, raw_args) } #[cfg(panic = "abort")] -extern "C" fn interp_thunk(closure: *const crate::closure::ClosureHeader, raw_args: f64) -> f64 { - interp_thunk_impl(closure, raw_args) +extern "C" fn interp_thunk( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + raw_args: f64, +) -> f64 { + interp_thunk_impl(this, closure, raw_args) } -fn interp_thunk_impl(closure: *const crate::closure::ClosureHeader, raw_args: f64) -> f64 { +fn interp_thunk_impl( + this: crate::closure::JsThis, + closure: *const crate::closure::ClosureHeader, + raw_args: f64, +) -> f64 { let fn_id = crate::closure::js_closure_get_capture_f64(closure, 0) as u32; let def_env = f64::from_bits(crate::closure::js_closure_get_capture_bits(closure, 1)); let this_bits = crate::closure::js_closure_get_capture_bits(closure, 2); let is_arrow = this_bits != NO_LEXICAL_THIS; let this = if this_bits == NO_LEXICAL_THIS { - crate::object::js_implicit_this_get() + this.as_f64() } else { f64::from_bits(this_bits) }; diff --git a/crates/perry-runtime/src/dyn_eval/registry_lifetime_tests.rs b/crates/perry-runtime/src/dyn_eval/registry_lifetime_tests.rs index 4d0956a65e..443592c378 100644 --- a/crates/perry-runtime/src/dyn_eval/registry_lifetime_tests.rs +++ b/crates/perry-runtime/src/dyn_eval/registry_lifetime_tests.rs @@ -9,7 +9,14 @@ fn closure_fn_id(closure: f64) -> u32 { } fn call0(function: f64) -> f64 { - unsafe { crate::closure::js_native_call_value(function, std::ptr::null(), 0) } + unsafe { + crate::closure::js_native_call_value( + function, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + } } /// Disable the source cache (its byte cap is "full") for one test body, so diff --git a/crates/perry-runtime/src/dyn_eval/tests.rs b/crates/perry-runtime/src/dyn_eval/tests.rs index 9f436ab22d..5b96d8ec9a 100644 --- a/crates/perry-runtime/src/dyn_eval/tests.rs +++ b/crates/perry-runtime/src/dyn_eval/tests.rs @@ -11,7 +11,14 @@ fn dyn_fn(args: &[&str]) -> f64 { } fn call(f: f64, args: &[f64]) -> f64 { - unsafe { crate::closure::js_native_call_value(f, args.as_ptr(), args.len()) } + unsafe { + crate::closure::js_native_call_value( + f, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + } } fn num(n: f64) -> f64 { @@ -444,7 +451,11 @@ fn generator_body_rejected_at_construction() { // ── host bridging: interpreted → host ────────────────────────────────────── -extern "C" fn host_double_thunk(_closure: *const crate::closure::ClosureHeader, v: f64) -> f64 { +extern "C" fn host_double_thunk( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + v: f64, +) -> f64 { num(as_num_raw(v) * 2.0) } diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 17158a0c16..f0321eeea7 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -1180,22 +1180,40 @@ fn throw_capture_stack_trace_target_type_error() -> ! { // .getFileName()` throws `(string).getFileName is not a function` and modules // like `next/dist/compiled/send` (bundled depd) crash at eager init. -extern "C" fn callsite_undefined(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn callsite_undefined( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn callsite_null(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn callsite_null( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_NULL) } -extern "C" fn callsite_zero(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn callsite_zero( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 0.0 } -extern "C" fn callsite_false(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn callsite_false( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_FALSE) } -extern "C" fn callsite_true(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn callsite_true( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_TRUE) } -extern "C" fn callsite_to_string(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn callsite_to_string( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let s = js_string_from_bytes(b"".as_ptr(), 11); crate::value::js_nanbox_string(s as i64) } @@ -1294,7 +1312,12 @@ unsafe fn compute_stack_value(receiver: f64) -> f64 { let structured = build_structured_stack(10); let prep_ptr = crate::value::js_nanbox_get_pointer(prep) as *const crate::closure::ClosureHeader; - return crate::closure::js_closure_call2(prep_ptr, receiver, structured); + return crate::closure::js_closure_call2( + prep_ptr, + crate::closure::plain_call_receiver(), + receiver, + structured, + ); } let s = make_stack("Error", ""); crate::value::js_nanbox_string(s as i64) @@ -1303,8 +1326,11 @@ unsafe fn compute_stack_value(receiver: f64) -> f64 { /// Lazy `stack` accessor installed by `Error.captureStackTrace`. Fires on read /// with `this` bound to the target object (V8 semantics: `prepareStackTrace` is /// consulted at access time, not capture time). -extern "C" fn error_stack_lazy_getter(_closure: *const crate::closure::ClosureHeader) -> f64 { - let receiver = crate::object::js_implicit_this_get(); +extern "C" fn error_stack_lazy_getter( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let receiver = this.as_f64(); unsafe { compute_stack_value(receiver) } } diff --git a/crates/perry-runtime/src/error_subclass_stack.rs b/crates/perry-runtime/src/error_subclass_stack.rs index 2dbd4f96de..9224b05191 100644 --- a/crates/perry-runtime/src/error_subclass_stack.rs +++ b/crates/perry-runtime/src/error_subclass_stack.rs @@ -77,14 +77,22 @@ unsafe fn error_subclass_stack_head(receiver: f64) -> String { /// reports `"E: m"`, and the assignment happens after `super()` returns. A /// user `Error.prepareStackTrace` still wins, same as `captureStackTrace`'s /// getter. -extern "C" fn error_subclass_stack_getter(closure: *const crate::closure::ClosureHeader) -> f64 { - let receiver = crate::object::js_implicit_this_get(); +extern "C" fn error_subclass_stack_getter( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let receiver = this.as_f64(); unsafe { if let Some(prep) = error_prepare_stack_trace_override() { let structured = build_structured_stack(10); let prep_ptr = crate::value::js_nanbox_get_pointer(prep) as *const crate::closure::ClosureHeader; - return crate::closure::js_closure_call2(prep_ptr, receiver, structured); + return crate::closure::js_closure_call2( + prep_ptr, + crate::closure::plain_call_receiver(), + receiver, + structured, + ); } // #9486: capture slot 0 holds the ENCODED capture (native return // addresses, plus the #5247 line when one was recorded), not a @@ -122,9 +130,10 @@ extern "C" fn error_subclass_stack_getter(closure: *const crate::closure::Closur /// enumerability, different reflection — a deliberate simplification.) extern "C" fn error_subclass_stack_setter( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let ptr = crate::value::js_nanbox_get_pointer(receiver); if ptr != 0 && crate::value::addr_class::is_above_handle_band(ptr as usize) diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index b23257d546..1f0cee8846 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -167,8 +167,11 @@ fn event_bool_field(event: *mut ObjectHeader, name: &[u8]) -> bool { crate::value::js_is_truthy(value) != 0 } -extern "C" fn event_prevent_default_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this_value = crate::object::js_implicit_this_get(); +extern "C" fn event_prevent_default_thunk( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this_value = this.as_f64(); let Some(event) = value_as_ptr::(this_value) else { return undefined_value(); }; @@ -178,8 +181,11 @@ extern "C" fn event_prevent_default_thunk(_closure: *const crate::closure::Closu undefined_value() } -extern "C" fn event_stop_propagation_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this_value = crate::object::js_implicit_this_get(); +extern "C" fn event_stop_propagation_thunk( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this_value = this.as_f64(); if let Some(event) = value_as_ptr::(this_value) { set_event_field(event, b"_stopped", bool_value(true)); } @@ -188,8 +194,9 @@ extern "C" fn event_stop_propagation_thunk(_closure: *const crate::closure::Clos extern "C" fn event_stop_immediate_propagation_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this_value = crate::object::js_implicit_this_get(); + let this_value = this.as_f64(); if let Some(event) = value_as_ptr::(this_value) { set_event_field(event, b"_stopped", bool_value(true)); set_event_field(event, b"_immediateStopped", bool_value(true)); @@ -200,7 +207,7 @@ extern "C" fn event_stop_immediate_propagation_thunk( fn install_event_method( event: *mut ObjectHeader, name: &str, - func: extern "C" fn(*const crate::closure::ClosureHeader) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(), ) { let func_ptr = func as *const u8; let closure = crate::closure::js_closure_alloc(func_ptr, 0); @@ -817,6 +824,7 @@ unsafe fn remove_event_listener_with_capture( extern "C" fn event_target_abort_remove_listener( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let target = crate::closure::js_closure_get_capture_ptr(closure, 0) as *mut ObjectHeader; let event_name_ptr = @@ -1014,8 +1022,9 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( let args = [event]; let this_scope = crate::gc::RuntimeHandleScope::new(); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + // A listener can collect, so the receiver is rooted ONCE here and re-read + // at each call. + let target_value_handle = this_scope.root_nanbox_f64(target_value); for (callback, capture, once) in callbacks { let Some(callable) = closure_value_from_listener(callback) else { continue; @@ -1023,9 +1032,12 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( if once { remove_event_listener_value_with_capture(target, event_name_ptr, callback, capture); } - crate::object::js_implicit_this_set(target_value); - let _ = crate::closure::js_native_call_value(callable, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let _ = crate::closure::native_call_value_this( + callable, + crate::closure::JsThis::from_f64(target_value_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ); if event_bool_field(event_ptr, b"_immediateStopped") { break; } @@ -1123,10 +1135,13 @@ pub unsafe extern "C" fn js_event_target_set_max_listeners( /// The receiver a bound EventTarget method closure captured in slot 0 (stored /// as NaN-boxed bits so the GC's closure scan keeps it alive and relocates it). -unsafe fn bound_event_target(closure: *const crate::closure::ClosureHeader) -> *mut ObjectHeader { +unsafe fn bound_event_target( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> *mut ObjectHeader { let bits = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; if bits == 0 { - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); if let Some(target) = value_as_ptr::(receiver) { let valid = crate::value::addr_class::try_read_gc_header(target as usize) .is_some_and(|h| h.obj_type == crate::gc::GC_TYPE_OBJECT); @@ -1155,8 +1170,8 @@ unsafe fn bound_event_target(closure: *const crate::closure::ClosureHeader) -> * crate::value::js_nanbox_get_pointer(f64::from_bits(bits)) as *mut ObjectHeader } -fn event_proto_receiver() -> *mut ObjectHeader { - let receiver = crate::object::js_implicit_this_get(); +fn event_proto_receiver(this: crate::closure::JsThis) -> *mut ObjectHeader { + let receiver = this.as_f64(); if let Some(event) = value_as_ptr::(receiver) { let valid = unsafe { crate::value::addr_class::try_read_gc_header(event as usize) @@ -1174,33 +1189,37 @@ fn event_proto_receiver() -> *mut ObjectHeader { extern "C" fn event_proto_prevent_default_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - event_proto_receiver(); - event_prevent_default_thunk(closure) + event_proto_receiver(this); + event_prevent_default_thunk(closure, this) } extern "C" fn event_proto_stop_propagation_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - event_proto_receiver(); - event_stop_propagation_thunk(closure) + event_proto_receiver(this); + event_stop_propagation_thunk(closure, this) } extern "C" fn event_proto_stop_immediate_propagation_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - event_proto_receiver(); - event_stop_immediate_propagation_thunk(closure) + event_proto_receiver(this); + event_stop_immediate_propagation_thunk(closure, this) } extern "C" fn event_proto_init_event_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event_type: f64, bubbles: f64, cancelable: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let event = scope.root_raw_mut_ptr(event_proto_receiver()); + let event = scope.root_raw_mut_ptr(event_proto_receiver(this)); let event_type = scope.root_nanbox_f64(event_type); let phase_key = key(b"eventPhase"); if event @@ -1235,9 +1254,10 @@ extern "C" fn event_proto_init_event_thunk( extern "C" fn event_proto_composed_path_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let event = scope.root_raw_mut_ptr(event_proto_receiver()); + let event = scope.root_raw_mut_ptr(event_proto_receiver(this)); let current_key = key(b"currentTarget"); let current = event.with_mut_ptr::(|event| { js_object_get_field_by_name_f64(event, current_key) @@ -1343,6 +1363,7 @@ pub(crate) fn install_web_event_proto_methods(name: &str, proto_obj: *mut Object /// arguments are rooted across it and re-read afterwards. unsafe fn bound_listener_call( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event_type: f64, listener: f64, options: f64, @@ -1356,7 +1377,7 @@ unsafe fn bound_listener_call( return undefined_value(); } let scope = crate::gc::RuntimeHandleScope::new(); - let target_handle = scope.root_raw_mut_ptr(bound_event_target(closure)); + let target_handle = scope.root_raw_mut_ptr(bound_event_target(closure, this)); let listener_handle = scope.root_nanbox_f64(listener); let options_handle = scope.root_nanbox_f64(options); @@ -1376,27 +1397,30 @@ unsafe fn bound_listener_call( extern "C" fn event_target_add_event_listener_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event_type: f64, listener: f64, options: f64, ) -> f64 { - unsafe { bound_listener_call(closure, event_type, listener, options, true) } + unsafe { bound_listener_call(closure, this, event_type, listener, options, true) } } extern "C" fn event_target_remove_event_listener_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event_type: f64, listener: f64, options: f64, ) -> f64 { - unsafe { bound_listener_call(closure, event_type, listener, options, false) } + unsafe { bound_listener_call(closure, this, event_type, listener, options, false) } } extern "C" fn event_target_dispatch_event_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event: f64, ) -> f64 { - unsafe { js_event_target_dispatch_event(bound_event_target(closure), event) } + unsafe { js_event_target_dispatch_event(bound_event_target(closure, this), event) } } /// The EventTarget method names, in the order Node's `EventTarget.prototype` diff --git a/crates/perry-runtime/src/exception.rs b/crates/perry-runtime/src/exception.rs index 9a038cd6b0..259dfcec9b 100644 --- a/crates/perry-runtime/src/exception.rs +++ b/crates/perry-runtime/src/exception.rs @@ -842,7 +842,7 @@ mod tests { } /// PR #10564 review finding, reproduced without touching the new - /// `implicit_this`/`new_target` savepoints directly: a bare + /// `new_target` savepoint directly: a bare /// save/call/restore pair — no RAII guard — around a call that throws. /// This is exactly the shape at the 4 named sites (fetch_globals.rs's /// Temporal/Intl subclass bridges, handle_methods.rs's prototype-walk @@ -851,15 +851,13 @@ mod tests { /// `longjmp` nor a system unwind ever reaches it once the call throws. /// /// Deliberately uses only pre-existing public entry points - /// (`js_implicit_this_set`/`js_new_target_set`/`catch_js_throw`/ - /// `js_throw`) that this fix does not change, so the same test body - /// fails on the unfixed tree (nothing restores either cell across the - /// inner `js_throw`) and passes once `implicit_this`/`new_target` join - /// `catch_savepoints!` — the fail-before/pass-after proof for the - /// mechanism every one of the 4 sites shares. + /// (`js_new_target_set`/`catch_js_throw`/`js_throw`) that this fix does + /// not change, so the same test body fails on the unfixed tree (nothing + /// restores the cell across the inner `js_throw`) and passes once + /// `new_target` joins `catch_savepoints!` — the fail-before/pass-after + /// proof for the mechanism every one of the 4 sites shares. #[test] fn a_bare_save_call_restore_site_is_made_exception_safe_by_the_savepoint() { - let base_this = crate::object::js_implicit_this_get().to_bits(); let base_nt = crate::object::js_new_target_get().to_bits(); let outcome: Result<(), f64> = catch_js_throw(|| { @@ -868,16 +866,10 @@ mod tests { let inner: Result<(), f64> = catch_js_throw(|| { // The bare save/call/restore pair itself: set, call something // that throws, restore — except the restore is unreachable. - let _prev_this = crate::object::js_implicit_this_set(11.0); let _prev_nt = crate::object::js_new_target_set(22.0); js_throw(99.0) }); assert_eq!(inner, Err(99.0)); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - base_this, - "the try open around the bare site must have restored IMPLICIT_THIS" - ); assert_eq!( crate::object::js_new_target_get().to_bits(), base_nt, @@ -885,7 +877,6 @@ mod tests { ); }); assert_eq!(outcome, Ok(())); - assert_eq!(crate::object::js_implicit_this_get().to_bits(), base_this); assert_eq!(crate::object::js_new_target_get().to_bits(), base_nt); } diff --git a/crates/perry-runtime/src/exception/savepoints.rs b/crates/perry-runtime/src/exception/savepoints.rs index a4f6a9e7c4..3cbf510f26 100644 --- a/crates/perry-runtime/src/exception/savepoints.rs +++ b/crates/perry-runtime/src/exception/savepoints.rs @@ -189,20 +189,10 @@ catch_savepoints! { capture: crate::object::call_method_depth_savepoint, restore: crate::object::call_method_depth_restore, latch: catch_subsystem::ALWAYS, idle: 0; - // PR #10564 review finding: the runtime guards that displace IMPLICIT_THIS - // around a `super()`/accessor/listener call they don't own (Temporal/Intl - // subclass bridges, the handle-method prototype-walk accessor dispatch, - // the stdlib listener/getter dispatchers) are a bare save/call/restore - // pair, not `ImplicitThisScope` — neither transport runs the restore - // statement that follows the call. The captured value is a second root - // for the object the live cell's own scanner already protects; see - // `scan_pending_trap_roots_mut` below. - implicit_this: u64, - capture: crate::object::implicit_this_trap_savepoint, - restore: crate::object::implicit_this_trap_restore, - latch: catch_subsystem::ALWAYS, idle: crate::value::TAG_UNDEFINED; - // Same shape as `implicit_this`, for `new.target` (the Temporal/Intl - // subclass `super()` bridges save/restore both together). + // PR #10564 review finding: the Temporal/Intl subclass `super()` bridges + // save/restore `new.target` with a bare statement pair that neither + // transport runs; the captured value is a second root for the object the + // live cell's scanner protects (see `scan_pending_trap_roots_mut`). new_target: u64, capture: crate::object::new_target_trap_savepoint, restore: crate::object::new_target_trap_restore, @@ -261,15 +251,15 @@ catch_savepoints! { latch: catch_subsystem::DYN_EVAL, idle: 0; } -/// Root + rewrite `implicit_this`/`new_target` in every OPEN `try`'s captured -/// savepoint (PR #10564 review finding). +/// Root + rewrite `new_target` in every OPEN `try`'s captured savepoint +/// (PR #10564 review finding). /// -/// `capture()` copies the live cells' bits into this per-depth slab +/// `capture()` copies the live cell's bits into this per-depth slab /// precisely so `js_throw` can put them back after a bare save/call/restore -/// site (see `object::this_binding::implicit_this_trap_savepoint`) gets +/// site (see `object::this_binding::new_target_trap_savepoint`) gets /// longjmp'd or unwound past. That copy is a second root for the same value /// the live cell's own scanner (`object::this_binding:: -/// scan_implicit_this_roots_mut`) already protects, and it is invisible to +/// scan_dispatch_binding_roots_mut`) already protects, and it is invisible to /// that scanner. A moving minor that runs while a `try` is open — before any /// throw crosses it — must rewrite this copy too, or a later throw restores a /// from-space address. Bounded by `try_depth <= MAX_TRY_DEPTH`, same as every @@ -283,7 +273,6 @@ pub(super) fn scan_pending_trap_roots_mut( // SAFETY: every slot below `try_depth` was written by `capture()` in // `try_push_with_kind` before `try_depth` advanced past it. let entry = unsafe { entry.assume_init_mut() }; - visitor.visit_nanbox_u64_slot(&mut entry.implicit_this); visitor.visit_nanbox_u64_slot(&mut entry.new_target); } } diff --git a/crates/perry-runtime/src/exception/savepoints/tests.rs b/crates/perry-runtime/src/exception/savepoints/tests.rs index 81a39e4c0e..7c5858360c 100644 --- a/crates/perry-runtime/src/exception/savepoints/tests.rs +++ b/crates/perry-runtime/src/exception/savepoints/tests.rs @@ -93,10 +93,6 @@ pub(super) fn call_method(_: u32) { /// unchanged (no tag rewriting to worry about), so a distinct marker per call /// is enough to make `capture()` observe a change — mirrors how /// `static_private_owner`'s witness below reuses `marker as f64`. -pub(super) fn implicit_this(marker: u32) { - crate::object::js_implicit_this_set(marker as f64); -} - pub(super) fn new_target(marker: u32) { crate::object::js_new_target_set(marker as f64); } diff --git a/crates/perry-runtime/src/ffi/perry_wasi_sig.c b/crates/perry-runtime/src/ffi/perry_wasi_sig.c index ec1e05014d..e685919f53 100644 --- a/crates/perry-runtime/src/ffi/perry_wasi_sig.c +++ b/crates/perry-runtime/src/ffi/perry_wasi_sig.c @@ -12,46 +12,54 @@ * (the GC proposal, hence `-mgc` on this file only), which asks the engine for * the function's actual type. The dispatch layer calls this once per body and * caches the answer, then always calls with exactly that many arguments. + * + * Every JS body takes the receiver (`this`, `perry_abi::JsThis`, a NaN-boxed + * `u64`) as its second native parameter (this-as-a-parameter, #11637): the + * candidate types below must carry that `unsigned long long` between the + * closure pointer and the `double` arguments, or none of them ever matches a + * real body's type and every WASI dispatch falls back to the caller's raw + * argument count. */ typedef struct ClosureHeader ClosureHeader; -typedef double (*perry_body_0)(const ClosureHeader *); -typedef double (*perry_body_1)(const ClosureHeader *, double); -typedef double (*perry_body_2)(const ClosureHeader *, double, double); -typedef double (*perry_body_3)(const ClosureHeader *, double, double, double); -typedef double (*perry_body_4)(const ClosureHeader *, double, double, double, double); -typedef double (*perry_body_5)(const ClosureHeader *, double, double, double, double, double); -typedef double (*perry_body_6)(const ClosureHeader *, double, double, double, double, double, double); -typedef double (*perry_body_7)(const ClosureHeader *, double, double, double, double, double, double, double); -typedef double (*perry_body_8)(const ClosureHeader *, double, double, double, double, double, double, double, double); -typedef double (*perry_body_9)(const ClosureHeader *, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_10)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_11)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_12)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_13)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_14)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_15)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_16)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_17)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_18)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_19)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_20)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_21)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_22)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_23)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_24)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_25)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_26)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_27)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_28)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_29)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_30)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_31)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -typedef double (*perry_body_32)(const ClosureHeader *, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_0)(const ClosureHeader *, unsigned long long); +typedef double (*perry_body_1)(const ClosureHeader *, unsigned long long, double); +typedef double (*perry_body_2)(const ClosureHeader *, unsigned long long, double, double); +typedef double (*perry_body_3)(const ClosureHeader *, unsigned long long, double, double, double); +typedef double (*perry_body_4)(const ClosureHeader *, unsigned long long, double, double, double, double); +typedef double (*perry_body_5)(const ClosureHeader *, unsigned long long, double, double, double, double, double); +typedef double (*perry_body_6)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double); +typedef double (*perry_body_7)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double); +typedef double (*perry_body_8)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double); +typedef double (*perry_body_9)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_10)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_11)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_12)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_13)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_14)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_15)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_16)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_17)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_18)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_19)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_20)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_21)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_22)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_23)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_24)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_25)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_26)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_27)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_28)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_29)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_30)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_31)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); +typedef double (*perry_body_32)(const ClosureHeader *, unsigned long long, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double, double); -/* The body's f64 parameter count after the closure pointer, or -1 if its - * type is none of `double (closure, double x k)` for k in 0..=32. */ +/* The body's f64 parameter count after the closure pointer and the receiver, + * or -1 if its type is none of `double (closure, this, double x k)` for k in + * 0..=32. */ int perry_wasi_closure_params(const void *fp) { if (__builtin_wasm_test_function_pointer_signature((perry_body_0)fp)) return 0; if (__builtin_wasm_test_function_pointer_signature((perry_body_1)fp)) return 1; diff --git a/crates/perry-runtime/src/frame.rs b/crates/perry-runtime/src/frame.rs index 563cf259b4..52edc617a2 100644 --- a/crates/perry-runtime/src/frame.rs +++ b/crates/perry-runtime/src/frame.rs @@ -180,7 +180,12 @@ pub extern "C" fn js_frame_tick(timestamp_ms: f64) -> i32 { let previous_roots = crate::async_context::root_snapshot(&scope, &previous); let cb_ptr = cb_handle.get_raw_const_ptr::(); with_frame_uncaught_trap(|| { - js_closure_call2(cb_ptr, timestamp_ms, delta_ms); + js_closure_call2( + cb_ptr, + crate::closure::plain_call_receiver(), + timestamp_ms, + delta_ms, + ); }); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); crate::async_context::restore_context(previous); diff --git a/crates/perry-runtime/src/fs/cp.rs b/crates/perry-runtime/src/fs/cp.rs index f69df97c1e..3cf7e8ab10 100644 --- a/crates/perry-runtime/src/fs/cp.rs +++ b/crates/perry-runtime/src/fs/cp.rs @@ -150,7 +150,12 @@ pub(crate) fn copy_filter_allows(src: &Path, dst: &Path, opts: FsCopyOptions) -> let src_value = crate::value::js_nanbox_string(s as i64); let s = js_string_from_bytes(dst_string.as_bytes().as_ptr(), dst_string.len() as u32); let dst_value = crate::value::js_nanbox_string(s as i64); - let result = crate::closure::js_closure_call2(filter, src_value, dst_value); + let result = crate::closure::js_closure_call2( + filter, + crate::closure::plain_call_receiver(), + src_value, + dst_value, + ); unsafe { if let Some(promise) = promise_ptr_from_value(result) { if opts.sync_filter { diff --git a/crates/perry-runtime/src/fs/deferred.rs b/crates/perry-runtime/src/fs/deferred.rs index 1b8197cb70..dbc8d5786e 100644 --- a/crates/perry-runtime/src/fs/deferred.rs +++ b/crates/perry-runtime/src/fs/deferred.rs @@ -264,7 +264,10 @@ fn callback_from_sink(sink: f64) -> *const ClosureHeader { /// The parked operation. Runs on a later event-loop turn; never on the turn /// that scheduled it, which is the whole point. -extern "C" fn perform_deferred_fs_op(closure: *const ClosureHeader) -> f64 { +extern "C" fn perform_deferred_fs_op( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let mode = js_closure_get_capture_f64(closure, CAP_MODE); // The operation allocates (error values, decoded strings), so the captured // JS values are read through handles rather than held raw across it — the diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs b/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs index 2411355661..23a509df8b 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/glob.rs @@ -739,7 +739,6 @@ fn excluded_by_function(entry: &FsGlobMatch, options: &FsGlobOptions<'_>) -> Res return Ok(false); }; let scope = RuntimeHandleScope::new(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let result = crate::exception::catch_js_throw(|| { let arg = if options.with_file_types { unsafe { build_dirent_object(&entry.dirent_name, &entry.dirent_parent, entry.kind) } @@ -749,10 +748,10 @@ fn excluded_by_function(entry: &FsGlobMatch, options: &FsGlobOptions<'_>) -> Res let arg = scope.root_nanbox_f64(arg); crate::closure::js_closure_call1( extract_closure_ptr(callback.get_nanbox_f64()), + crate::closure::plain_call_receiver(), arg.get_nanbox_f64(), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); result.map(|value| crate::value::js_is_truthy(value) != 0) } diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index 68e127ee70..950673ca4a 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -231,7 +231,7 @@ fn signal_abort_reason(signal: f64) -> f64 { fn add_abort_listener( signal: f64, id: usize, - func: extern "C" fn(*const ClosureHeader) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(), ) -> f64 { let Some(signal_ptr) = crate::url::abort::abort_signal_ptr_from_value(signal) else { return undefined_value(); @@ -412,13 +412,12 @@ fn emit_listener0(object_value: f64, callback: f64) { if cb.is_null() { return; } - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - object_handle.get_nanbox_f64(), - )); with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call0(cb); + crate::closure::js_closure_call0( + cb, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } fn emit_fs_watch_event( @@ -442,18 +441,19 @@ fn emit_fs_watch_event( let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); + crate::closure::js_closure_call2( + cb, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + refreshed_args[0], + refreshed_args[1], + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -479,18 +479,19 @@ fn emit_watch_file_change( let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); + crate::closure::js_closure_call2( + cb, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + refreshed_args[0], + refreshed_args[1], + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -900,17 +901,18 @@ fn emit_fs_watch_error( let err_handle = scope.root_nanbox_f64(err_value); let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { - crate::closure::js_closure_call1(cb, err_handle.get_nanbox_f64()); + crate::closure::js_closure_call1( + cb, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + err_handle.get_nanbox_f64(), + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -923,7 +925,10 @@ fn start_promise_watcher(id: usize, state: &mut PromiseWatchState) { state.active = true; } -extern "C" fn watch_file_poll_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn watch_file_poll_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let delivery = WATCH_FILE_STATES.with(|states| { let mut states = states.borrow_mut(); @@ -946,13 +951,19 @@ extern "C" fn watch_file_poll_impl(closure: *const ClosureHeader) -> f64 { undefined_value() } -extern "C" fn fs_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn fs_watcher_abort_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; close_fs_watcher(id); undefined_value() } -extern "C" fn promise_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn promise_watcher_abort_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let signal = PROMISE_WATCHERS.with(|watchers| { watchers @@ -969,14 +980,20 @@ extern "C" fn promise_watcher_abort_impl(closure: *const ClosureHeader) -> f64 { undefined_value() } -extern "C" fn fs_watcher_close_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn fs_watcher_close_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let self_value = js_closure_get_capture_f64(closure, 1); close_fs_watcher(id); self_value } -extern "C" fn fs_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn fs_watcher_ref_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let self_value = js_closure_get_capture_f64(closure, 1); FS_WATCHERS.with(|watchers| { @@ -987,7 +1004,10 @@ extern "C" fn fs_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { self_value } -extern "C" fn fs_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn fs_watcher_unref_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let self_value = js_closure_get_capture_f64(closure, 1); FS_WATCHERS.with(|watchers| { @@ -1000,6 +1020,7 @@ extern "C" fn fs_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { extern "C" fn fs_watcher_on_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event_value: f64, listener: f64, ) -> f64 { @@ -1017,6 +1038,7 @@ extern "C" fn fs_watcher_on_impl( extern "C" fn fs_watcher_once_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event_value: f64, listener: f64, ) -> f64 { @@ -1034,6 +1056,7 @@ extern "C" fn fs_watcher_once_impl( extern "C" fn fs_watcher_off_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event_value: f64, listener: f64, ) -> f64 { @@ -1049,7 +1072,10 @@ extern "C" fn fs_watcher_off_impl( self_value } -extern "C" fn stat_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stat_watcher_ref_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let self_value = js_closure_get_capture_f64(closure, 1); WATCH_FILE_STATES.with(|states| { @@ -1060,7 +1086,10 @@ extern "C" fn stat_watcher_ref_impl(closure: *const ClosureHeader) -> f64 { self_value } -extern "C" fn stat_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stat_watcher_unref_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let self_value = js_closure_get_capture_f64(closure, 1); WATCH_FILE_STATES.with(|states| { @@ -1073,6 +1102,7 @@ extern "C" fn stat_watcher_unref_impl(closure: *const ClosureHeader) -> f64 { extern "C" fn stat_watcher_on_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event_value: f64, listener: f64, ) -> f64 { @@ -1090,6 +1120,7 @@ extern "C" fn stat_watcher_on_impl( extern "C" fn stat_watcher_once_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event_value: f64, listener: f64, ) -> f64 { @@ -1107,6 +1138,7 @@ extern "C" fn stat_watcher_once_impl( extern "C" fn stat_watcher_off_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event_value: f64, listener: f64, ) -> f64 { @@ -1135,7 +1167,10 @@ enum GlobNextAction { Entry(FsGlobMatch, bool), } -extern "C" fn glob_iterator_next_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn glob_iterator_next_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let action = GLOB_ITERATORS.with(|iterators| { let mut iterators = iterators.borrow_mut(); @@ -1163,7 +1198,10 @@ extern "C" fn glob_iterator_next_impl(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn glob_iterator_return_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn glob_iterator_return_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; GLOB_ITERATORS.with(|iterators| { iterators.borrow_mut().remove(&id); @@ -1171,11 +1209,17 @@ extern "C" fn glob_iterator_return_impl(closure: *const ClosureHeader) -> f64 { resolved_iterator_promise(undefined_value(), true) } -extern "C" fn glob_iterator_self_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn glob_iterator_self_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_closure_get_capture_f64(closure, 1) } -extern "C" fn bun_glob_sync_iterator_next_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_glob_sync_iterator_next_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let action = GLOB_ITERATORS.with(|iterators| { let mut iterators = iterators.borrow_mut(); @@ -1205,7 +1249,10 @@ extern "C" fn bun_glob_sync_iterator_next_impl(closure: *const ClosureHeader) -> } } -extern "C" fn bun_glob_sync_iterator_return_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_glob_sync_iterator_return_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; GLOB_ITERATORS.with(|iterators| { iterators.borrow_mut().remove(&id); @@ -1213,11 +1260,17 @@ extern "C" fn bun_glob_sync_iterator_return_impl(closure: *const ClosureHeader) iterator_result(undefined_value(), true) } -extern "C" fn bun_glob_sync_iterator_self_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_glob_sync_iterator_self_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_closure_get_capture_f64(closure, 1) } -extern "C" fn promise_watcher_next_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn promise_watcher_next_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let action = PROMISE_WATCHERS.with(|watchers| { let mut watchers = watchers.borrow_mut(); @@ -1255,7 +1308,10 @@ extern "C" fn promise_watcher_next_impl(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn promise_watcher_return_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn promise_watcher_return_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as usize; let pending = close_promise_watcher_return(id); for promise in pending { @@ -1264,7 +1320,10 @@ extern "C" fn promise_watcher_return_impl(closure: *const ClosureHeader) -> f64 resolved_iterator_promise(undefined_value(), true) } -extern "C" fn promise_watcher_self_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn promise_watcher_self_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_closure_get_capture_f64(closure, 1) } diff --git a/crates/perry-runtime/src/fs/fd_ops.rs b/crates/perry-runtime/src/fs/fd_ops.rs index 3cd693f0ad..4309d106ab 100644 --- a/crates/perry-runtime/src/fs/fd_ops.rs +++ b/crates/perry-runtime/src/fs/fd_ops.rs @@ -916,11 +916,17 @@ pub(crate) fn make_dir_method(id: usize, func: *const u8) -> f64 { f64::from_bits(crate::value::JSValue::pointer(closure as *const u8).bits()) } -pub(crate) extern "C" fn dir_read_sync_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn dir_read_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { dir_read_next(dir_id_of(closure)) } -pub(crate) extern "C" fn dir_close_sync_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn dir_close_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = dir_id_of(closure); match dir_close_sync_result(id) { Ok(()) => f64::from_bits(crate::value::TAG_UNDEFINED), @@ -950,7 +956,10 @@ fn dir_clear_operation_pending(id: usize) { }); } -extern "C" fn dir_finish_read_promise_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_finish_read_promise_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = dir_id_of(closure); let promise = crate::closure::js_closure_get_capture_ptr(closure, 1) as *mut crate::promise::Promise; @@ -982,7 +991,10 @@ fn dir_pending_read_promise(id: usize, result: Result, f64>) -> f64 f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) } -extern "C" fn dir_finish_read_callback_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_finish_read_callback_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; let id = dir_id_of(closure); let callback = crate::closure::js_closure_get_capture_ptr(closure, 1) as *const ClosureHeader; @@ -990,17 +1002,28 @@ extern "C" fn dir_finish_read_callback_impl(closure: *const ClosureHeader) -> f6 let kind = crate::closure::js_closure_get_capture_f64(closure, 3) as i32; match kind { 0 => { - crate::closure::js_closure_call2(callback, f64::from_bits(TAG_NULL), value); + crate::closure::js_closure_call2( + callback, + crate::closure::plain_call_receiver(), + f64::from_bits(TAG_NULL), + value, + ); } 1 => { crate::closure::js_closure_call2( callback, + crate::closure::plain_call_receiver(), f64::from_bits(TAG_NULL), f64::from_bits(TAG_NULL), ); } _ => { - crate::closure::js_closure_call2(callback, value, f64::from_bits(TAG_NULL)); + crate::closure::js_closure_call2( + callback, + crate::closure::plain_call_receiver(), + value, + f64::from_bits(TAG_NULL), + ); } } dir_clear_operation_pending(id); @@ -1026,7 +1049,11 @@ fn dir_schedule_read_callback( crate::builtins::js_queue_microtask(closure as i64); } -pub(crate) extern "C" fn dir_read_impl(closure: *const ClosureHeader, callback: f64) -> f64 { +pub(crate) extern "C" fn dir_read_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + callback: f64, +) -> f64 { const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; let id = dir_id_of(closure); let begin = dir_begin_async_operation(id); @@ -1042,17 +1069,28 @@ pub(crate) extern "C" fn dir_read_impl(closure: *const ClosureHeader, callback: } else { match entry { Ok(Some(value)) => { - crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), value); + crate::closure::js_closure_call2( + cb, + crate::closure::plain_call_receiver(), + f64::from_bits(TAG_NULL), + value, + ); } Ok(None) => { crate::closure::js_closure_call2( cb, + crate::closure::plain_call_receiver(), f64::from_bits(TAG_NULL), f64::from_bits(TAG_NULL), ); } Err(err) => { - crate::closure::js_closure_call2(cb, err, f64::from_bits(TAG_NULL)); + crate::closure::js_closure_call2( + cb, + crate::closure::plain_call_receiver(), + err, + f64::from_bits(TAG_NULL), + ); } } } @@ -1068,7 +1106,11 @@ pub(crate) extern "C" fn dir_read_impl(closure: *const ClosureHeader, callback: } } -pub(crate) extern "C" fn dir_close_impl(closure: *const ClosureHeader, callback: f64) -> f64 { +pub(crate) extern "C" fn dir_close_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + callback: f64, +) -> f64 { const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; let closed = dir_close_result(dir_id_of(closure)); let cb = extract_closure_ptr(callback); @@ -1077,7 +1119,7 @@ pub(crate) extern "C" fn dir_close_impl(closure: *const ClosureHeader, callback: Ok(()) => f64::from_bits(TAG_NULL), Err(err) => err, }; - crate::closure::js_closure_call1(cb, err); + crate::closure::js_closure_call1(cb, crate::closure::plain_call_receiver(), err); return f64::from_bits(crate::value::TAG_UNDEFINED); } match closed { @@ -1121,7 +1163,10 @@ fn dir_iterator_result(value: f64, done: bool) -> f64 { f64::from_bits(crate::value::JSValue::pointer(obj as *const u8).bits()) } -extern "C" fn dir_iterator_next_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_iterator_next_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match dir_read_next_result(dir_iterator_id_of(closure)) { Ok(Some(value)) => promise_value_fs(dir_iterator_result(value, false)), Ok(None) => { @@ -1135,7 +1180,10 @@ extern "C" fn dir_iterator_next_impl(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn dir_iterator_return_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_iterator_return_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { dir_mark_closed(dir_iterator_id_of(closure)); promise_value_fs(dir_iterator_result( f64::from_bits(crate::value::TAG_UNDEFINED), @@ -1143,25 +1191,40 @@ extern "C" fn dir_iterator_return_impl(closure: *const ClosureHeader) -> f64 { )) } -extern "C" fn dir_iterator_self_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_iterator_self_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { dir_iterator_self_value(closure) } -extern "C" fn dir_entries_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_entries_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { unsafe { build_dir_iterator_object(dir_id_of(closure)) } } -extern "C" fn dir_dispose_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_dispose_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { dir_mark_closed(dir_id_of(closure)); f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn dir_async_dispose_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_async_dispose_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { dir_mark_closed(dir_id_of(closure)); promise_undefined_fs() } -extern "C" fn dir_path_getter_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn dir_path_getter_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::closure::js_closure_get_capture_f64(closure, 0) } diff --git a/crates/perry-runtime/src/fs/filehandle.rs b/crates/perry-runtime/src/fs/filehandle.rs index d01cef4e87..639a1325d5 100644 --- a/crates/perry-runtime/src/fs/filehandle.rs +++ b/crates/perry-runtime/src/fs/filehandle.rs @@ -267,7 +267,11 @@ fn build_read_lines_step(value: f64, done: bool) -> f64 { f64::from_bits(crate::value::JSValue::pointer(obj as *const u8).bits()) } -extern "C" fn read_lines_next_impl(closure: *const ClosureHeader, _arg: f64) -> f64 { +extern "C" fn read_lines_next_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { let id = read_lines_id(closure); let next_line = READ_LINES_REGISTRY.with(|states| { let mut states = states.borrow_mut(); @@ -294,7 +298,11 @@ extern "C" fn read_lines_next_impl(closure: *const ClosureHeader, _arg: f64) -> promise_value_fs(build_read_lines_step(value, false)) } -extern "C" fn read_lines_return_impl(closure: *const ClosureHeader, _arg: f64) -> f64 { +extern "C" fn read_lines_return_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { READ_LINES_REGISTRY.with(|states| { states.borrow_mut().remove(&read_lines_id(closure)); }); @@ -304,14 +312,20 @@ extern "C" fn read_lines_return_impl(closure: *const ClosureHeader, _arg: f64) - )) } -extern "C" fn read_lines_close_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn read_lines_close_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { READ_LINES_REGISTRY.with(|states| { states.borrow_mut().remove(&read_lines_id(closure)); }); f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn read_lines_iterator_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn read_lines_iterator_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::closure::js_closure_get_capture_ptr(closure, 0) as u64) } @@ -396,7 +410,10 @@ fn read_filehandle_webstream_chunk(fd: i32) -> Option> { }) } -extern "C" fn filehandle_webstream_pull_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn filehandle_webstream_pull_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let handle = webstream_handle(closure); let fallback_fd = filehandle_fd(closure); let fd = filehandle_field_fd(handle).unwrap_or(fallback_fd); @@ -412,7 +429,11 @@ extern "C" fn filehandle_webstream_pull_impl(closure: *const ClosureHeader) -> f f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn filehandle_webstream_cancel_impl(closure: *const ClosureHeader, _reason: f64) -> f64 { +extern "C" fn filehandle_webstream_cancel_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _reason: f64, +) -> f64 { let handle = webstream_handle(closure); let fallback_fd = filehandle_fd(closure); let fd = filehandle_field_fd(handle).unwrap_or(fallback_fd); @@ -441,6 +462,7 @@ fn readable_webstream_auto_close(options: f64) -> bool { pub(crate) extern "C" fn filehandle_readable_web_stream_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { let raw = READABLE_WEB_STREAM_FACTORY.load(Ordering::Acquire); @@ -470,6 +492,7 @@ pub(crate) extern "C" fn filehandle_readable_web_stream_impl( auto_close, filehandle_webstream_cancel_impl as *const u8, ); + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let factory: ReadableWebStreamFactory = unsafe { std::mem::transmute(raw) }; unsafe { factory(pull, cancel) } } @@ -701,7 +724,10 @@ fn read_stream_iter_action(id: usize) -> StreamIterAction { }) } -extern "C" fn stream_iter_next_async_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_iter_next_async_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match read_stream_iter_action(stream_iter_id(closure)) { StreamIterAction::Batch(bytes) => { promise_value_fs(build_read_lines_step(build_chunk_batch(&bytes), false)) @@ -714,7 +740,10 @@ extern "C" fn stream_iter_next_async_impl(closure: *const ClosureHeader) -> f64 } } -extern "C" fn stream_iter_return_async_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_iter_return_async_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let close = STREAM_ITER_REGISTRY.with(|states| { let mut states = states.borrow_mut(); let Some(mut state) = states.remove(&stream_iter_id(closure)) else { @@ -727,7 +756,10 @@ extern "C" fn stream_iter_return_async_impl(closure: *const ClosureHeader) -> f6 promise_value_fs(build_read_lines_step(undefined_value(), true)) } -extern "C" fn stream_iter_next_sync_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_iter_next_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match read_stream_iter_action(stream_iter_id(closure)) { StreamIterAction::Batch(bytes) => build_read_lines_step(build_chunk_batch(&bytes), false), StreamIterAction::Done(close) => { @@ -738,7 +770,10 @@ extern "C" fn stream_iter_next_sync_impl(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn stream_iter_return_sync_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_iter_return_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let close = STREAM_ITER_REGISTRY.with(|states| { let mut states = states.borrow_mut(); let Some(mut state) = states.remove(&stream_iter_id(closure)) else { @@ -751,7 +786,10 @@ extern "C" fn stream_iter_return_sync_impl(closure: *const ClosureHeader) -> f64 build_read_lines_step(undefined_value(), true) } -extern "C" fn stream_iter_self_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_iter_self_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { stream_iter_self(closure) } @@ -803,7 +841,11 @@ fn build_stream_iter_state( } } -pub(crate) extern "C" fn filehandle_pull_impl(closure: *const ClosureHeader, options: f64) -> f64 { +pub(crate) extern "C" fn filehandle_pull_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + options: f64, +) -> f64 { let state = build_stream_iter_state(closure, options); let id = next_stream_iter_id(); STREAM_ITER_REGISTRY.with(|states| { @@ -814,6 +856,7 @@ pub(crate) extern "C" fn filehandle_pull_impl(closure: *const ClosureHeader, opt pub(crate) extern "C" fn filehandle_pull_sync_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { let state = build_stream_iter_state(closure, options); @@ -917,60 +960,86 @@ fn writer_end_common(id: usize) -> Result { Ok(total) } -extern "C" fn writer_write_impl(closure: *const ClosureHeader, data: f64) -> f64 { +extern "C" fn writer_write_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + data: f64, +) -> f64 { match writer_write_common(writer_id(closure), data, false, true) { Ok(_) => promise_undefined_fs(), Err(err) => promise_rejected_fs(err), } } -extern "C" fn writer_writev_impl(closure: *const ClosureHeader, buffers: f64) -> f64 { +extern "C" fn writer_writev_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + buffers: f64, +) -> f64 { match writer_write_common(writer_id(closure), buffers, true, true) { Ok(_) => promise_undefined_fs(), Err(err) => promise_rejected_fs(err), } } -extern "C" fn writer_write_sync_impl(closure: *const ClosureHeader, data: f64) -> f64 { +extern "C" fn writer_write_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + data: f64, +) -> f64 { match writer_write_common(writer_id(closure), data, false, false) { Ok(wrote) => bool_value(wrote), Err(err) => crate::exception::js_throw(err), } } -extern "C" fn writer_writev_sync_impl(closure: *const ClosureHeader, buffers: f64) -> f64 { +extern "C" fn writer_writev_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + buffers: f64, +) -> f64 { match writer_write_common(writer_id(closure), buffers, true, false) { Ok(wrote) => bool_value(wrote), Err(err) => crate::exception::js_throw(err), } } -extern "C" fn writer_end_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn writer_end_impl(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { match writer_end_common(writer_id(closure)) { Ok(total) => promise_value_fs(total as f64), Err(err) => promise_rejected_fs(err), } } -extern "C" fn writer_end_sync_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn writer_end_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match writer_end_common(writer_id(closure)) { Ok(total) => total as f64, Err(err) => crate::exception::js_throw(err), } } -extern "C" fn writer_fail_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn writer_fail_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let _ = writer_end_common(writer_id(closure)); undefined_value() } -extern "C" fn writer_async_dispose_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn writer_async_dispose_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let _ = writer_end_common(writer_id(closure)); promise_undefined_fs() } pub(crate) extern "C" fn filehandle_writer_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { let handle = filehandle_object(closure).unwrap_or(undefined_value()); @@ -1058,7 +1127,10 @@ fn filehandle_close_promise() -> f64 { }) } -pub(crate) extern "C" fn filehandle_close_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn filehandle_close_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let fd = filehandle_fd(closure); if let Some(handle) = filehandle_object(closure) { close_filehandle_fd(filehandle_field_fd(handle).unwrap_or(fd), handle); @@ -1068,7 +1140,10 @@ pub(crate) extern "C" fn filehandle_close_impl(closure: *const ClosureHeader) -> filehandle_close_promise() } -pub(crate) extern "C" fn filehandle_sync_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn filehandle_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { // Bypass `js_fs_fsync_sync`'s arg-validation: FileHandle may legitimately // hold a `-1` fd sentinel from a failed open, and Node's API surfaces that // earlier (at `open`), not here. @@ -1076,12 +1151,19 @@ pub(crate) extern "C" fn filehandle_sync_impl(closure: *const ClosureHeader) -> promise_undefined_fs() } -pub(crate) extern "C" fn filehandle_datasync_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn filehandle_datasync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let _ = crate::fs::fdatasync_sync_inner(filehandle_fd(closure)); promise_undefined_fs() } -pub(crate) extern "C" fn filehandle_stat_impl(closure: *const ClosureHeader, options: f64) -> f64 { +pub(crate) extern "C" fn filehandle_stat_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + options: f64, +) -> f64 { let fd = match live_filehandle_fd_or_ebadf(closure, "fstat") { Ok(fd) => fd, Err(rejection) => return rejection, @@ -1089,7 +1171,11 @@ pub(crate) extern "C" fn filehandle_stat_impl(closure: *const ClosureHeader, opt promise_value_fs(js_fs_fstat_sync_options(fd as f64, options)) } -pub(crate) extern "C" fn filehandle_truncate_impl(closure: *const ClosureHeader, len: f64) -> f64 { +pub(crate) extern "C" fn filehandle_truncate_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + len: f64, +) -> f64 { let fd = match live_filehandle_fd_or_ebadf(closure, "ftruncate") { Ok(fd) => fd, Err(rejection) => return rejection, @@ -1100,6 +1186,7 @@ pub(crate) extern "C" fn filehandle_truncate_impl(closure: *const ClosureHeader, pub(crate) extern "C" fn filehandle_utimes_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, atime: f64, mtime: f64, ) -> f64 { @@ -1111,7 +1198,11 @@ pub(crate) extern "C" fn filehandle_utimes_impl( promise_undefined_fs() } -pub(crate) extern "C" fn filehandle_chmod_impl(closure: *const ClosureHeader, mode: f64) -> f64 { +pub(crate) extern "C" fn filehandle_chmod_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + mode: f64, +) -> f64 { let fd = match live_filehandle_fd_or_ebadf(closure, "fchmod") { Ok(fd) => fd, Err(rejection) => return rejection, @@ -1122,6 +1213,7 @@ pub(crate) extern "C" fn filehandle_chmod_impl(closure: *const ClosureHeader, mo pub(crate) extern "C" fn filehandle_chown_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, uid: f64, gid: f64, ) -> f64 { @@ -1135,6 +1227,7 @@ pub(crate) extern "C" fn filehandle_chown_impl( pub(crate) extern "C" fn filehandle_read_file_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, encoding: f64, ) -> f64 { let fd = filehandle_fd(closure); @@ -1173,6 +1266,7 @@ pub(crate) extern "C" fn filehandle_read_file_impl( pub(crate) extern "C" fn filehandle_write_file_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, data: f64, options: f64, ) -> f64 { @@ -1190,6 +1284,7 @@ pub(crate) extern "C" fn filehandle_write_file_impl( pub(crate) extern "C" fn filehandle_append_file_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, data: f64, options: f64, ) -> f64 { @@ -1205,6 +1300,7 @@ pub(crate) extern "C" fn filehandle_append_file_impl( pub(crate) extern "C" fn filehandle_read_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, buffer: f64, offset: f64, length: f64, @@ -1257,6 +1353,7 @@ pub(crate) extern "C" fn filehandle_read_impl( pub(crate) extern "C" fn filehandle_write_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, data: f64, offset: f64, length: f64, @@ -1287,6 +1384,7 @@ pub(crate) extern "C" fn filehandle_write_impl( pub(crate) extern "C" fn filehandle_readv_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, buffers: f64, position: f64, ) -> f64 { @@ -1304,6 +1402,7 @@ pub(crate) extern "C" fn filehandle_readv_impl( pub(crate) extern "C" fn filehandle_writev_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, buffers: f64, position: f64, ) -> f64 { @@ -1325,6 +1424,7 @@ pub(crate) fn path_for_fd(fd: i32) -> Option { pub(crate) extern "C" fn filehandle_create_read_stream_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { let fallback_fd = filehandle_fd(closure); @@ -1351,6 +1451,7 @@ pub(crate) extern "C" fn filehandle_create_read_stream_impl( pub(crate) extern "C" fn filehandle_create_write_stream_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { let fallback_fd = filehandle_fd(closure); @@ -1377,6 +1478,7 @@ pub(crate) extern "C" fn filehandle_create_write_stream_impl( pub(crate) extern "C" fn filehandle_read_lines_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { let fallback_fd = filehandle_fd(closure); diff --git a/crates/perry-runtime/src/fs/mkdtemp_disposable.rs b/crates/perry-runtime/src/fs/mkdtemp_disposable.rs index f1c4ee5fc5..3df41715eb 100644 --- a/crates/perry-runtime/src/fs/mkdtemp_disposable.rs +++ b/crates/perry-runtime/src/fs/mkdtemp_disposable.rs @@ -38,7 +38,10 @@ fn rejected_promise(reason: f64) -> f64 { f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) } -extern "C" fn mkdtemp_disposable_remove_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn mkdtemp_disposable_remove_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let path_value = crate::closure::js_closure_get_capture_f64(closure, 0); match remove_temp_dir_result(path_value) { Ok(()) => f64::from_bits(crate::value::TAG_UNDEFINED), @@ -46,7 +49,10 @@ extern "C" fn mkdtemp_disposable_remove_impl(closure: *const ClosureHeader) -> f } } -extern "C" fn mkdtemp_disposable_async_remove_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn mkdtemp_disposable_async_remove_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let path_value = crate::closure::js_closure_get_capture_f64(closure, 0); match remove_temp_dir_result(path_value) { Ok(()) => resolved_promise(f64::from_bits(crate::value::TAG_UNDEFINED)), @@ -56,7 +62,7 @@ extern "C" fn mkdtemp_disposable_async_remove_impl(closure: *const ClosureHeader fn mkdtemp_disposable_method( path_value: f64, - func: extern "C" fn(*const ClosureHeader) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(), ) -> f64 { crate::closure::js_register_closure_arity(func as *const u8, 0); let closure = crate::closure::js_closure_alloc(func as *const u8, 1); @@ -80,9 +86,9 @@ fn build_mkdtemp_disposable_object( let display_path = encoded_string_ptr(&actual_path_bytes, &display_encoding); let display_path_value = f64::from_bits(crate::value::JSValue::string_ptr(display_path).bits()); let remove_func = if async_remove { - mkdtemp_disposable_async_remove_impl as extern "C" fn(*const ClosureHeader) -> f64 + mkdtemp_disposable_async_remove_impl as crate::closure::body_call::js_body_fn_ty!() } else { - mkdtemp_disposable_remove_impl as extern "C" fn(*const ClosureHeader) -> f64 + mkdtemp_disposable_remove_impl as crate::closure::body_call::js_body_fn_ty!() }; let remove_method = mkdtemp_disposable_method(actual_path_value, remove_func); let symbol_method = mkdtemp_disposable_method(actual_path_value, remove_func); diff --git a/crates/perry-runtime/src/fs/stats.rs b/crates/perry-runtime/src/fs/stats.rs index 05092058d1..8bb4ec8209 100644 --- a/crates/perry-runtime/src/fs/stats.rs +++ b/crates/perry-runtime/src/fs/stats.rs @@ -16,6 +16,7 @@ use super::*; pub(crate) extern "C" fn stats_closure_return_captured( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { // Slot 0 holds the pre-computed NaN-boxed boolean. crate::closure::js_closure_get_capture_f64(closure, 0) diff --git a/crates/perry-runtime/src/fs/stream.rs b/crates/perry-runtime/src/fs/stream.rs index 9e972ffca5..40aa9814cd 100644 --- a/crates/perry-runtime/src/fs/stream.rs +++ b/crates/perry-runtime/src/fs/stream.rs @@ -436,8 +436,8 @@ fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHeader> { } } -fn current_receiver_value() -> f64 { - let this_value = crate::object::js_implicit_this_get(); +fn current_receiver_value(this: crate::closure::JsThis) -> f64 { + let this_value = this.as_f64(); if object_ptr_from_value(this_value).is_some() { this_value } else { @@ -649,7 +649,7 @@ fn emit_event0(id: usize, event: &str) { for cb in callbacks { let cb_ptr = extract_closure_ptr(cb); if !cb_ptr.is_null() { - js_closure_call0(cb_ptr); + js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } } bridge_to_stream_listeners(id, event, &[], handled_locally); @@ -662,7 +662,7 @@ fn emit_event1(id: usize, event: &str, arg: f64) { for cb in callbacks { let cb_ptr = extract_closure_ptr(cb); if !cb_ptr.is_null() { - js_closure_call1(cb_ptr, arg); + js_closure_call1(cb_ptr, crate::closure::plain_call_receiver(), arg); } } bridge_to_stream_listeners(id, event, &[arg], handled_locally); @@ -835,7 +835,7 @@ fn call_stream_callback0(callback: f64) { if is_callable_value(callback) { let cb_ptr = extract_closure_ptr(callback); if !cb_ptr.is_null() { - crate::closure::js_closure_call0(cb_ptr); + crate::closure::js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } } } @@ -844,273 +844,19 @@ fn call_stream_callback1(callback: f64, arg: f64) { if is_callable_value(callback) { let cb_ptr = extract_closure_ptr(callback); if !cb_ptr.is_null() { - crate::closure::js_closure_call1(cb_ptr, arg); + crate::closure::js_closure_call1(cb_ptr, crate::closure::plain_call_receiver(), arg); } } } -/// #9493: park one `WriteStream` turn on the callback-timer queue. At most one -/// is pending per stream; a turn re-schedules while work remains. -/// -/// This is the mechanism `fs::deferred` uses for `fs.writeFile`: the timer -/// queue roots the closure; a pending refed callback timer is a live event -/// source, so a program that ends by draining its loop still lands every byte -/// and `'finish'` still fires; and `process.exit()` terminates through -/// `libc::_exit` without ticking the queue, so an exit in the same tick -/// abandons the parked open and writes the way Node abandons its not-yet-run -/// thread-pool requests. -fn schedule_write_stream_turn(id: usize) { - let should_schedule = STREAM_REGISTRY.with(|registry| { - let mut registry = registry.borrow_mut(); - let Some(state) = registry.get_mut(&id) else { - return false; - }; - if state.turn_pending || state.closed { - return false; - } - state.turn_pending = true; - true - }); - if should_schedule { - let closure = js_closure_alloc(write_stream_turn_impl as *const u8, 1); - js_closure_set_capture_ptr(closure, 0, id as i64); - let _ = crate::timer::js_set_timeout_callback(closure as i64, 0.0); - } -} - -extern "C" fn write_stream_turn_impl(closure: *const ClosureHeader) -> f64 { - let id = stream_id_of(closure); - STREAM_REGISTRY.with(|registry| { - if let Some(state) = registry.borrow_mut().get_mut(&id) { - state.turn_pending = false; - } - }); - run_write_stream_turn(id); - undefined_value() -} - -/// What a `WriteStream` turn does, decided from the state when it runs. One -/// step per turn, each the analogue of one Node thread-pool request: the open -/// (`_construct` → `fs.open`), then the queued writes ending in `'finish'`, -/// then the close. A step schedules the next when more work remains, so a -/// microtask queued by an `'open'` or `'finish'` listener runs before the -/// first write callback / before `'close'`, as it does in Node. -#[derive(Clone, Copy, PartialEq, Eq)] -enum WriteStreamStep { - Open, - Drain, - Close, - Idle, -} - -fn write_stream_step(id: usize) -> WriteStreamStep { - STREAM_REGISTRY.with(|registry| { - let registry = registry.borrow(); - let Some(state) = registry.get(&id) else { - return WriteStreamStep::Idle; - }; - if state.kind != StreamKind::Write || state.closed { - return WriteStreamStep::Idle; - } - if state.destroyed { - return WriteStreamStep::Close; - } - if !state.opened && state.error_msg.is_none() && matches!(state.owner, FdOwner::Path) { - return WriteStreamStep::Open; - } - if !state.pending_writes.is_empty() || (state.ended && !state.finished) { - return WriteStreamStep::Drain; - } - if state.finished && state.auto_close { - return WriteStreamStep::Close; - } - WriteStreamStep::Idle - }) -} - -fn run_write_stream_turn(id: usize) { - match write_stream_step(id) { - WriteStreamStep::Open => write_stream_open_step(id), - WriteStreamStep::Drain => write_stream_drain_step(id), - WriteStreamStep::Close => write_stream_close_step(id), - WriteStreamStep::Idle => {} - } -} - -fn schedule_next_write_stream_step(id: usize) { - if write_stream_step(id) != WriteStreamStep::Idle { - schedule_write_stream_turn(id); - } -} - -/// The deferred `open(2)`: Node's `_construct` runs `fs.open` on the pool and -/// then emits `'open'` and `'ready'`. The queued writes are performed on a -/// LATER turn — their `fs.write` requests are only dispatched once the open -/// callback has returned. -fn write_stream_open_step(id: usize) { - let (path, flags) = STREAM_REGISTRY.with(|registry| { - registry - .borrow() - .get(&id) - .map(|state| (state.path.clone(), state.flags.clone())) - .unwrap_or_default() - }); - match fs_open_path_str_result(&path, &flags) { - Ok(fd) => { - STREAM_REGISTRY.with(|registry| { - if let Some(state) = registry.borrow_mut().get_mut(&id) { - state.fd = Some(fd); - state.opened = true; - if matches!(state.flags.as_str(), "a" | "a+" | "ax" | "ax+") { - state.position = end_position_for_fd(fd); - } - update_common_props(state); - } - }); - emit_event1(id, "open", fd as f64); - emit_event0(id, "ready"); - schedule_next_write_stream_step(id); - } - Err(err) => { - let message = err.to_string(); - let error_value = unsafe { build_fs_error_value(&err, "open", &path) }; - write_stream_fail(id, error_value, message); - } - } -} - -/// The error cascade Node runs when the open fails: every pending write -/// callback and the `end()` callback receive the error, then `'error'` fires, -/// then the stream is destroyed and `'close'` follows on a later turn. -fn write_stream_fail(id: usize, error_value: f64, message: String) { - let scope = crate::gc::RuntimeHandleScope::new(); - let error_handle = scope.root_nanbox_f64(error_value); - // The value goes into the state first — the registry is a GC root and the - // callbacks below allocate. - let (writes, end_callback) = STREAM_REGISTRY.with(|registry| { - let mut registry = registry.borrow_mut(); - let Some(state) = registry.get_mut(&id) else { - return (Vec::new(), undefined_value()); - }; - state.errored = true; - state.error_msg = Some(message); - state.error_value = error_handle.get_nanbox_f64(); - state.destroyed = true; - state.writable_length = 0; - state.writable_need_drain = false; - let writes = std::mem::take(&mut state.pending_writes); - let end_callback = std::mem::replace(&mut state.end_callback, undefined_value()); - update_common_props(state); - (writes, end_callback) - }); - let callbacks: Vec<_> = writes - .iter() - .map(|write| scope.root_nanbox_f64(write.callback)) - .collect(); - let end_handle = scope.root_nanbox_f64(end_callback); - for callback in &callbacks { - call_stream_callback1(callback.get_nanbox_f64(), error_handle.get_nanbox_f64()); - } - call_stream_callback1(end_handle.get_nanbox_f64(), error_handle.get_nanbox_f64()); - emit_event1(id, "error", error_handle.get_nanbox_f64()); - schedule_next_write_stream_step(id); -} - -/// The queued writes, in order — Node batches them into one `writev` — then -/// Node's `afterWrite` order: `'drain'` (when a `write()` returned `false` -/// and the stream is not ending) BEFORE the completed writes' callbacks. Once -/// `end()` has been called and nothing is left: the `end()` callback and -/// `'finish'`; with `autoClose`, the close lands on the next turn. -fn write_stream_drain_step(id: usize) { - let scope = crate::gc::RuntimeHandleScope::new(); - let mut completed = Vec::new(); - loop { - let next = STREAM_REGISTRY.with(|registry| { - let mut registry = registry.borrow_mut(); - let state = registry.get_mut(&id)?; - if state.destroyed || state.pending_writes.is_empty() { - return None; - } - Some(state.pending_writes.remove(0)) - }); - let Some(write) = next else { - break; - }; - let callback = scope.root_nanbox_f64(write.callback); - match write_to_stream_fd(id, &write.bytes) { - Ok(()) => completed.push(callback), - Err(message) => { - // The writes that did land complete normally; the failing one - // gets the error, then the rest of the queue does via the - // error cascade. - for done in &completed { - call_stream_callback0(done.get_nanbox_f64()); - } - let error_value = scope.root_nanbox_f64(make_error_value(&message)); - call_stream_callback1(callback.get_nanbox_f64(), error_value.get_nanbox_f64()); - write_stream_fail(id, error_value.get_nanbox_f64(), message); - return; - } - } - } - let (emit_drain, finish) = STREAM_REGISTRY.with(|registry| { - let mut registry = registry.borrow_mut(); - let Some(state) = registry.get_mut(&id) else { - return (false, false); - }; - if state.destroyed { - return (false, false); - } - state.writable_length = 0; - let emit_drain = state.writable_need_drain && !state.ended; - state.writable_need_drain = false; - let mut finish = false; - if state.ended && !state.finished { - if state.error_msg.is_none() { - state.finished = true; - finish = true; - } else { - state.destroyed = true; - } - } - update_common_props(state); - (emit_drain, finish) - }); - if emit_drain { - emit_event0(id, "drain"); - } - for callback in &completed { - call_stream_callback0(callback.get_nanbox_f64()); - } - if finish { - let end_callback = STREAM_REGISTRY.with(|registry| { - registry - .borrow_mut() - .get_mut(&id) - .map(|state| std::mem::replace(&mut state.end_callback, undefined_value())) - .unwrap_or_else(undefined_value) - }); - let scope = crate::gc::RuntimeHandleScope::new(); - let end_callback = scope.root_nanbox_f64(end_callback); - call_stream_callback0(end_callback.get_nanbox_f64()); - emit_event0(id, "finish"); - } - schedule_next_write_stream_step(id); -} - -fn write_stream_close_step(id: usize) { - let force = STREAM_REGISTRY.with(|registry| { - registry - .borrow() - .get(&id) - .map(|state| state.destroyed) - .unwrap_or(false) - }); - maybe_close_stream(id, force); -} +mod write_turn; +use write_turn::{ + schedule_next_write_stream_step, schedule_write_stream_turn, write_stream_turn_impl, +}; pub(crate) extern "C" fn write_stream_write_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, encoding: f64, cb: f64, @@ -1155,6 +901,7 @@ pub(crate) extern "C" fn write_stream_write_impl( pub(crate) extern "C" fn write_stream_end_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, chunk: f64, encoding: f64, cb: f64, @@ -1194,29 +941,32 @@ pub(crate) extern "C" fn write_stream_end_impl( } else { emit_stored_error(id); } - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn write_stream_on_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { stream_on_common(stream_id_of(closure), event, cb, false); - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn write_stream_once_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { stream_on_common(stream_id_of(closure), event, cb, true); - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn stream_emit_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event: f64, arg: f64, ) -> f64 { @@ -1245,7 +995,11 @@ use stream_errors::*; mod utf8_stream; pub(crate) use utf8_stream::*; -pub(crate) extern "C" fn write_stream_close_impl(closure: *const ClosureHeader, cb: f64) -> f64 { +pub(crate) extern "C" fn write_stream_close_impl( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + cb: f64, +) -> f64 { let id = stream_id_of(closure); if is_callable_value(cb) { add_listener(id, "close", cb, true); @@ -1260,7 +1014,7 @@ pub(crate) extern "C" fn write_stream_close_impl(closure: *const ClosureHeader, } }); maybe_close_stream(id, true); - current_receiver_value() + current_receiver_value(this) } fn read_chunk_value(bytes: &[u8], encoding: Option<&str>) -> f64 { @@ -1352,7 +1106,10 @@ fn install_pipe_drain_resume(source_id: usize, dest: f64) { let _ = call_js_method2(dest, b"once", string_value(b"drain"), listener); } -extern "C" fn read_stream_resume_from_drain_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn read_stream_resume_from_drain_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); STREAM_REGISTRY.with(|registry| { if let Some(state) = registry.borrow_mut().get_mut(&id) { @@ -1474,6 +1231,7 @@ fn read_stream_pump(id: usize) { pub(crate) extern "C" fn read_stream_on_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { @@ -1487,11 +1245,12 @@ pub(crate) extern "C" fn read_stream_on_impl( }); schedule_read_stream_turn(id); } - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn read_stream_once_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { @@ -1505,11 +1264,12 @@ pub(crate) extern "C" fn read_stream_once_impl( }); schedule_read_stream_turn(id); } - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn read_stream_pipe_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, dest: f64, options: f64, ) -> f64 { @@ -1528,16 +1288,22 @@ pub(crate) extern "C" fn read_stream_pipe_impl( dest } -pub(crate) extern "C" fn read_stream_pause_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn read_stream_pause_impl( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { STREAM_REGISTRY.with(|registry| { if let Some(state) = registry.borrow_mut().get_mut(&stream_id_of(closure)) { state.paused = true; } }); - current_receiver_value() + current_receiver_value(this) } -pub(crate) extern "C" fn read_stream_resume_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn read_stream_resume_impl( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); STREAM_REGISTRY.with(|registry| { if let Some(state) = registry.borrow_mut().get_mut(&id) { @@ -1545,10 +1311,13 @@ pub(crate) extern "C" fn read_stream_resume_impl(closure: *const ClosureHeader) } }); schedule_read_stream_turn(id); - current_receiver_value() + current_receiver_value(this) } -pub(crate) extern "C" fn read_stream_is_paused_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn read_stream_is_paused_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let paused = STREAM_REGISTRY.with(|registry| { registry .borrow() @@ -1559,7 +1328,11 @@ pub(crate) extern "C" fn read_stream_is_paused_impl(closure: *const ClosureHeade bool_value(paused) } -pub(crate) extern "C" fn read_stream_close_impl(closure: *const ClosureHeader, cb: f64) -> f64 { +pub(crate) extern "C" fn read_stream_close_impl( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + cb: f64, +) -> f64 { let id = stream_id_of(closure); if is_callable_value(cb) { add_listener(id, "close", cb, true); @@ -1572,7 +1345,7 @@ pub(crate) extern "C" fn read_stream_close_impl(closure: *const ClosureHeader, c } }); maybe_close_stream(id, true); - current_receiver_value() + current_receiver_value(this) } fn stream_on_common(id: usize, event_value: f64, cb: f64, once: bool) { @@ -1613,9 +1386,13 @@ fn stream_on_common(id: usize, event_value: f64, cb: f64, once: bool) { let cb_ptr = extract_closure_ptr(cb); if !cb_ptr.is_null() { if name == "open" || name == "error" { - crate::closure::js_closure_call1(cb_ptr, arg); + crate::closure::js_closure_call1( + cb_ptr, + crate::closure::plain_call_receiver(), + arg, + ); } else { - crate::closure::js_closure_call0(cb_ptr); + crate::closure::js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } } } @@ -1649,50 +1426,44 @@ fn create_write_stream_with_state(state: StreamState) -> f64 { let id = alloc_stream(state); let method_funcs: [(&str, extern "C" fn()); 8] = [ ("write", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64, - extern "C" fn(), - >(write_stream_write_impl) + std::mem::transmute::( + write_stream_write_impl, + ) }), ("end", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64, - extern "C" fn(), - >(write_stream_end_impl) + std::mem::transmute::( + write_stream_end_impl, + ) }), ("on", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(write_stream_on_impl) + std::mem::transmute::( + write_stream_on_impl, + ) }), ("once", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(write_stream_once_impl) + std::mem::transmute::( + write_stream_once_impl, + ) }), ("addListener", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(write_stream_on_impl) + std::mem::transmute::( + write_stream_on_impl, + ) }), ("close", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( write_stream_close_impl, ) }), ("destroy", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( write_stream_close_impl, ) }), ("emit", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(stream_emit_impl) + std::mem::transmute::( + stream_emit_impl, + ) }), ]; let obj = build_stream_object(id, CLASS_ID_FS_WRITE_STREAM, &method_funcs); @@ -1722,59 +1493,54 @@ fn create_read_stream_with_state(state: StreamState) -> f64 { store_open_failure(id); let method_funcs: [(&str, extern "C" fn()); 10] = [ ("on", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(read_stream_on_impl) + std::mem::transmute::( + read_stream_on_impl, + ) }), ("once", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(read_stream_once_impl) + std::mem::transmute::( + read_stream_once_impl, + ) }), ("addListener", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(read_stream_on_impl) + std::mem::transmute::( + read_stream_on_impl, + ) }), ("pipe", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(read_stream_pipe_impl) + std::mem::transmute::( + read_stream_pipe_impl, + ) }), ("pause", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( read_stream_pause_impl, ) }), ("resume", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( read_stream_resume_impl, ) }), ("isPaused", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( read_stream_is_paused_impl, ) }), ("close", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( read_stream_close_impl, ) }), ("destroy", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( read_stream_close_impl, ) }), ("emit", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(stream_emit_impl) + std::mem::transmute::( + stream_emit_impl, + ) }), ]; let obj = build_stream_object(id, CLASS_ID_FS_READ_STREAM, &method_funcs); @@ -1821,88 +1587,82 @@ fn create_utf8_stream_with_state(state: Utf8StreamState) -> f64 { let id = alloc_utf8_stream(state); let method_funcs: [(&str, extern "C" fn()); 16] = [ ("write", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_write_impl, ) }), ("flush", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_flush_impl, ) }), ("flushSync", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_flush_sync_impl, ) }), ("end", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_end_impl, ) }), ("destroy", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_destroy_impl, ) }), ("reopen", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_reopen_impl, ) }), ("on", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(utf8_stream_on_impl) + std::mem::transmute::( + utf8_stream_on_impl, + ) }), ("once", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(utf8_stream_once_impl) + std::mem::transmute::( + utf8_stream_once_impl, + ) }), ("addListener", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(utf8_stream_on_impl) + std::mem::transmute::( + utf8_stream_on_impl, + ) }), ("off", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(utf8_stream_off_impl) + std::mem::transmute::( + utf8_stream_off_impl, + ) }), ("removeListener", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(utf8_stream_off_impl) + std::mem::transmute::( + utf8_stream_off_impl, + ) }), ("removeAllListeners", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_remove_all_impl, ) }), ("listenerCount", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_listener_count_impl, ) }), ("emit", unsafe { - std::mem::transmute::< - extern "C" fn(*const ClosureHeader, f64, f64) -> f64, - extern "C" fn(), - >(utf8_stream_emit_impl) + std::mem::transmute::( + utf8_stream_emit_impl, + ) }), ("close", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_destroy_impl, ) }), ("@@__perry_wk_dispose", unsafe { - std::mem::transmute:: f64, extern "C" fn()>( + std::mem::transmute::( utf8_stream_destroy_impl, ) }), diff --git a/crates/perry-runtime/src/fs/stream/read_turn.rs b/crates/perry-runtime/src/fs/stream/read_turn.rs index d39e0332c1..09884d0bcc 100644 --- a/crates/perry-runtime/src/fs/stream/read_turn.rs +++ b/crates/perry-runtime/src/fs/stream/read_turn.rs @@ -26,7 +26,10 @@ pub(super) fn schedule_read_stream_turn(id: usize) { } } -pub(super) extern "C" fn read_stream_turn_impl(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn read_stream_turn_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); let should_close = STREAM_REGISTRY.with(|registry| { if let Some(state) = registry.borrow_mut().get_mut(&id) { diff --git a/crates/perry-runtime/src/fs/stream/utf8_stream.rs b/crates/perry-runtime/src/fs/stream/utf8_stream.rs index 0840ae4920..d6d2ab85ff 100644 --- a/crates/perry-runtime/src/fs/stream/utf8_stream.rs +++ b/crates/perry-runtime/src/fs/stream/utf8_stream.rs @@ -470,7 +470,7 @@ pub(crate) fn utf8_emit_event0(id: usize, event: &str) { for cb in callbacks { let cb_ptr = extract_closure_ptr(cb); if !cb_ptr.is_null() { - crate::closure::js_closure_call0(cb_ptr); + crate::closure::js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } } } @@ -480,7 +480,7 @@ fn utf8_emit_event1(id: usize, event: &str, arg: f64) { for cb in callbacks { let cb_ptr = extract_closure_ptr(cb); if !cb_ptr.is_null() { - crate::closure::js_closure_call1(cb_ptr, arg); + crate::closure::js_closure_call1(cb_ptr, crate::closure::plain_call_receiver(), arg); } } } @@ -504,7 +504,7 @@ fn utf8_add_listener(id: usize, event: &str, cb: f64, once: bool) { if let Some((_name, _arg)) = immediate { let cb_ptr = extract_closure_ptr(cb); if !cb_ptr.is_null() { - crate::closure::js_closure_call0(cb_ptr); + crate::closure::js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } return; } @@ -754,7 +754,11 @@ fn utf8_stream_write_by_id(id: usize, data: f64) -> f64 { bool_value(len_after < high_water_mark) } -pub(crate) extern "C" fn utf8_stream_write_impl(closure: *const ClosureHeader, data: f64) -> f64 { +pub(crate) extern "C" fn utf8_stream_write_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + data: f64, +) -> f64 { utf8_stream_write_by_id(stream_id_of(closure), data) } @@ -798,7 +802,7 @@ fn utf8_stream_flush_by_id(id: usize, callback: f64) -> f64 { if is_callable_value(callback) { let cb = extract_closure_ptr(callback); if !cb.is_null() { - crate::closure::js_closure_call1(cb, err); + crate::closure::js_closure_call1(cb, crate::closure::plain_call_receiver(), err); } return undefined_value(); } @@ -808,7 +812,7 @@ fn utf8_stream_flush_by_id(id: usize, callback: f64) -> f64 { if is_callable_value(callback) { let cb = extract_closure_ptr(callback); if !cb.is_null() { - crate::closure::js_closure_call0(cb); + crate::closure::js_closure_call0(cb, crate::closure::plain_call_receiver()); } } return undefined_value(); @@ -825,13 +829,13 @@ fn utf8_stream_flush_by_id(id: usize, callback: f64) -> f64 { if is_callable_value(callback) { let cb = extract_closure_ptr(callback); if !cb.is_null() { - crate::closure::js_closure_call1(cb, err); + crate::closure::js_closure_call1(cb, crate::closure::plain_call_receiver(), err); } } } else if is_callable_value(callback) { let cb = extract_closure_ptr(callback); if !cb.is_null() { - crate::closure::js_closure_call0(cb); + crate::closure::js_closure_call0(cb, crate::closure::plain_call_receiver()); } } undefined_value() @@ -839,6 +843,7 @@ fn utf8_stream_flush_by_id(id: usize, callback: f64) -> f64 { pub(crate) extern "C" fn utf8_stream_flush_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, callback: f64, ) -> f64 { utf8_stream_flush_by_id(stream_id_of(closure), callback) @@ -879,7 +884,10 @@ fn utf8_stream_flush_sync_by_id(id: usize) -> f64 { undefined_value() } -pub(crate) extern "C" fn utf8_stream_flush_sync_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn utf8_stream_flush_sync_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { utf8_stream_flush_sync_by_id(stream_id_of(closure)) } @@ -898,7 +906,10 @@ fn utf8_emit_close_events(id: usize, emit_finish: bool) { utf8_emit_event0(id, "close"); } -pub(crate) extern "C" fn utf8_close_events_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn utf8_close_events_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); let emit_finish = js_closure_get_capture_ptr(closure, 1) != 0; utf8_emit_close_events(id, emit_finish); @@ -970,7 +981,10 @@ fn utf8_stream_end_by_id(id: usize) -> f64 { undefined_value() } -pub(crate) extern "C" fn utf8_stream_end_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn utf8_stream_end_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { utf8_stream_end_by_id(stream_id_of(closure)) } @@ -992,7 +1006,10 @@ fn utf8_stream_destroy_by_id(id: usize) -> f64 { undefined_value() } -pub(crate) extern "C" fn utf8_stream_destroy_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn utf8_stream_destroy_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { utf8_stream_destroy_by_id(stream_id_of(closure)) } @@ -1085,6 +1102,7 @@ fn utf8_stream_reopen_by_id(id: usize, file_value: f64) -> f64 { pub(crate) extern "C" fn utf8_stream_reopen_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, file_value: f64, ) -> f64 { utf8_stream_reopen_by_id(stream_id_of(closure), file_value) @@ -1100,35 +1118,39 @@ pub extern "C" fn js_fs_utf8_stream_reopen(stream_value: f64, file_value: f64) - pub(crate) extern "C" fn utf8_stream_on_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { let id = stream_id_of(closure); utf8_add_listener(id, &event_name(event), cb, false); - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn utf8_stream_once_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { let id = stream_id_of(closure); utf8_add_listener(id, &event_name(event), cb, true); - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn utf8_stream_off_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { utf8_remove_listener(stream_id_of(closure), &event_name(event), cb); - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn utf8_stream_remove_all_impl( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, ) -> f64 { let id = stream_id_of(closure); @@ -1141,11 +1163,12 @@ pub(crate) extern "C" fn utf8_stream_remove_all_impl( } } }); - current_receiver_value() + current_receiver_value(this) } pub(crate) extern "C" fn utf8_stream_listener_count_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event: f64, ) -> f64 { let id = stream_id_of(closure); @@ -1162,6 +1185,7 @@ pub(crate) extern "C" fn utf8_stream_listener_count_impl( pub(crate) extern "C" fn utf8_stream_emit_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, event: f64, arg: f64, ) -> f64 { @@ -1260,7 +1284,10 @@ pub extern "C" fn js_fs_utf8_stream_emit(stream_value: f64, event: f64, arg: f64 utf8_stream_emit_value(stream_value, event, arg).unwrap_or_else(|| bool_value(false)) } -pub(crate) extern "C" fn utf8_periodic_flush_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn utf8_periodic_flush_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); let destroyed = UTF8_STREAM_REGISTRY.with(|registry| { registry @@ -1369,6 +1396,7 @@ fn utf8_async_open_finish(id: usize, err: f64, fd_value: f64) -> f64 { pub(crate) extern "C" fn utf8_async_open_done_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, fd_value: f64, ) -> f64 { @@ -1406,6 +1434,7 @@ fn utf8_custom_open(id: usize) -> bool { pub(crate) extern "C" fn utf8_async_mkdir_done_impl( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, ) -> f64 { let id = stream_id_of(closure); @@ -1497,7 +1526,10 @@ pub(crate) fn utf8_start_async_open(id: usize) { utf8_schedule_native_open(id); } -extern "C" fn utf8_async_native_mkdir_then_custom_open_impl(closure: *const ClosureHeader) -> f64 { +extern "C" fn utf8_async_native_mkdir_then_custom_open_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); let Some(file) = UTF8_STREAM_REGISTRY.with(|registry| { registry @@ -1517,7 +1549,10 @@ extern "C" fn utf8_async_native_mkdir_then_custom_open_impl(closure: *const Clos undefined_value() } -pub(crate) extern "C" fn utf8_async_open_impl(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn utf8_async_open_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = stream_id_of(closure); let Some(file) = UTF8_STREAM_REGISTRY.with(|registry| { registry diff --git a/crates/perry-runtime/src/fs/stream/write_file_input.rs b/crates/perry-runtime/src/fs/stream/write_file_input.rs index e8042a059a..d19e6a0641 100644 --- a/crates/perry-runtime/src/fs/stream/write_file_input.rs +++ b/crates/perry-runtime/src/fs/stream/write_file_input.rs @@ -150,10 +150,14 @@ fn well_known_iterator_method(value: f64, name: &str) -> Option { fn call_well_known_iterator(value: f64, name: &str) -> Option { let method = well_known_iterator_method(value, name)?; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); - let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let iterator = unsafe { + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(value), + std::ptr::null(), + 0, + ) + }; if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { diff --git a/crates/perry-runtime/src/fs/stream/write_turn.rs b/crates/perry-runtime/src/fs/stream/write_turn.rs new file mode 100644 index 0000000000..389a7a4d92 --- /dev/null +++ b/crates/perry-runtime/src/fs/stream/write_turn.rs @@ -0,0 +1,273 @@ +//! The `WriteStream` turn state machine — one parked callback-timer turn per +//! stream, deciding open/drain/close from the current state each time it +//! runs (#9493). +//! +//! Split out of `stream.rs` only to keep that file under the repository's +//! 2000-line cap (`scripts/check_file_size.sh`); same subject as before, just +//! its own file (mirrors how `utf8_stream.rs`/`stream_errors.rs`/etc. were +//! already split out of this same file). + +use super::*; + +/// #9493: park one `WriteStream` turn on the callback-timer queue. At most one +/// is pending per stream; a turn re-schedules while work remains. +/// +/// This is the mechanism `fs::deferred` uses for `fs.writeFile`: the timer +/// queue roots the closure; a pending refed callback timer is a live event +/// source, so a program that ends by draining its loop still lands every byte +/// and `'finish'` still fires; and `process.exit()` terminates through +/// `libc::_exit` without ticking the queue, so an exit in the same tick +/// abandons the parked open and writes the way Node abandons its not-yet-run +/// thread-pool requests. +pub(super) fn schedule_write_stream_turn(id: usize) { + let should_schedule = STREAM_REGISTRY.with(|registry| { + let mut registry = registry.borrow_mut(); + let Some(state) = registry.get_mut(&id) else { + return false; + }; + if state.turn_pending || state.closed { + return false; + } + state.turn_pending = true; + true + }); + if should_schedule { + let closure = js_closure_alloc(write_stream_turn_impl as *const u8, 1); + js_closure_set_capture_ptr(closure, 0, id as i64); + let _ = crate::timer::js_set_timeout_callback(closure as i64, 0.0); + } +} + +pub(super) extern "C" fn write_stream_turn_impl( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let id = stream_id_of(closure); + STREAM_REGISTRY.with(|registry| { + if let Some(state) = registry.borrow_mut().get_mut(&id) { + state.turn_pending = false; + } + }); + run_write_stream_turn(id); + undefined_value() +} + +/// What a `WriteStream` turn does, decided from the state when it runs. One +/// step per turn, each the analogue of one Node thread-pool request: the open +/// (`_construct` → `fs.open`), then the queued writes ending in `'finish'`, +/// then the close. A step schedules the next when more work remains, so a +/// microtask queued by an `'open'` or `'finish'` listener runs before the +/// first write callback / before `'close'`, as it does in Node. +#[derive(Clone, Copy, PartialEq, Eq)] +enum WriteStreamStep { + Open, + Drain, + Close, + Idle, +} + +fn write_stream_step(id: usize) -> WriteStreamStep { + STREAM_REGISTRY.with(|registry| { + let registry = registry.borrow(); + let Some(state) = registry.get(&id) else { + return WriteStreamStep::Idle; + }; + if state.kind != StreamKind::Write || state.closed { + return WriteStreamStep::Idle; + } + if state.destroyed { + return WriteStreamStep::Close; + } + if !state.opened && state.error_msg.is_none() && matches!(state.owner, FdOwner::Path) { + return WriteStreamStep::Open; + } + if !state.pending_writes.is_empty() || (state.ended && !state.finished) { + return WriteStreamStep::Drain; + } + if state.finished && state.auto_close { + return WriteStreamStep::Close; + } + WriteStreamStep::Idle + }) +} + +fn run_write_stream_turn(id: usize) { + match write_stream_step(id) { + WriteStreamStep::Open => write_stream_open_step(id), + WriteStreamStep::Drain => write_stream_drain_step(id), + WriteStreamStep::Close => write_stream_close_step(id), + WriteStreamStep::Idle => {} + } +} + +pub(super) fn schedule_next_write_stream_step(id: usize) { + if write_stream_step(id) != WriteStreamStep::Idle { + schedule_write_stream_turn(id); + } +} + +/// The deferred `open(2)`: Node's `_construct` runs `fs.open` on the pool and +/// then emits `'open'` and `'ready'`. The queued writes are performed on a +/// LATER turn — their `fs.write` requests are only dispatched once the open +/// callback has returned. +fn write_stream_open_step(id: usize) { + let (path, flags) = STREAM_REGISTRY.with(|registry| { + registry + .borrow() + .get(&id) + .map(|state| (state.path.clone(), state.flags.clone())) + .unwrap_or_default() + }); + match fs_open_path_str_result(&path, &flags) { + Ok(fd) => { + STREAM_REGISTRY.with(|registry| { + if let Some(state) = registry.borrow_mut().get_mut(&id) { + state.fd = Some(fd); + state.opened = true; + if matches!(state.flags.as_str(), "a" | "a+" | "ax" | "ax+") { + state.position = end_position_for_fd(fd); + } + update_common_props(state); + } + }); + emit_event1(id, "open", fd as f64); + emit_event0(id, "ready"); + schedule_next_write_stream_step(id); + } + Err(err) => { + let message = err.to_string(); + let error_value = unsafe { build_fs_error_value(&err, "open", &path) }; + write_stream_fail(id, error_value, message); + } + } +} + +/// The error cascade Node runs when the open fails: every pending write +/// callback and the `end()` callback receive the error, then `'error'` fires, +/// then the stream is destroyed and `'close'` follows on a later turn. +fn write_stream_fail(id: usize, error_value: f64, message: String) { + let scope = crate::gc::RuntimeHandleScope::new(); + let error_handle = scope.root_nanbox_f64(error_value); + // The value goes into the state first — the registry is a GC root and the + // callbacks below allocate. + let (writes, end_callback) = STREAM_REGISTRY.with(|registry| { + let mut registry = registry.borrow_mut(); + let Some(state) = registry.get_mut(&id) else { + return (Vec::new(), undefined_value()); + }; + state.errored = true; + state.error_msg = Some(message); + state.error_value = error_handle.get_nanbox_f64(); + state.destroyed = true; + state.writable_length = 0; + state.writable_need_drain = false; + let writes = std::mem::take(&mut state.pending_writes); + let end_callback = std::mem::replace(&mut state.end_callback, undefined_value()); + update_common_props(state); + (writes, end_callback) + }); + let callbacks: Vec<_> = writes + .iter() + .map(|write| scope.root_nanbox_f64(write.callback)) + .collect(); + let end_handle = scope.root_nanbox_f64(end_callback); + for callback in &callbacks { + call_stream_callback1(callback.get_nanbox_f64(), error_handle.get_nanbox_f64()); + } + call_stream_callback1(end_handle.get_nanbox_f64(), error_handle.get_nanbox_f64()); + emit_event1(id, "error", error_handle.get_nanbox_f64()); + schedule_next_write_stream_step(id); +} + +/// The queued writes, in order — Node batches them into one `writev` — then +/// Node's `afterWrite` order: `'drain'` (when a `write()` returned `false` +/// and the stream is not ending) BEFORE the completed writes' callbacks. Once +/// `end()` has been called and nothing is left: the `end()` callback and +/// `'finish'`; with `autoClose`, the close lands on the next turn. +fn write_stream_drain_step(id: usize) { + let scope = crate::gc::RuntimeHandleScope::new(); + let mut completed = Vec::new(); + loop { + let next = STREAM_REGISTRY.with(|registry| { + let mut registry = registry.borrow_mut(); + let state = registry.get_mut(&id)?; + if state.destroyed || state.pending_writes.is_empty() { + return None; + } + Some(state.pending_writes.remove(0)) + }); + let Some(write) = next else { + break; + }; + let callback = scope.root_nanbox_f64(write.callback); + match write_to_stream_fd(id, &write.bytes) { + Ok(()) => completed.push(callback), + Err(message) => { + // The writes that did land complete normally; the failing one + // gets the error, then the rest of the queue does via the + // error cascade. + for done in &completed { + call_stream_callback0(done.get_nanbox_f64()); + } + let error_value = scope.root_nanbox_f64(make_error_value(&message)); + call_stream_callback1(callback.get_nanbox_f64(), error_value.get_nanbox_f64()); + write_stream_fail(id, error_value.get_nanbox_f64(), message); + return; + } + } + } + let (emit_drain, finish) = STREAM_REGISTRY.with(|registry| { + let mut registry = registry.borrow_mut(); + let Some(state) = registry.get_mut(&id) else { + return (false, false); + }; + if state.destroyed { + return (false, false); + } + state.writable_length = 0; + let emit_drain = state.writable_need_drain && !state.ended; + state.writable_need_drain = false; + let mut finish = false; + if state.ended && !state.finished { + if state.error_msg.is_none() { + state.finished = true; + finish = true; + } else { + state.destroyed = true; + } + } + update_common_props(state); + (emit_drain, finish) + }); + if emit_drain { + emit_event0(id, "drain"); + } + for callback in &completed { + call_stream_callback0(callback.get_nanbox_f64()); + } + if finish { + let end_callback = STREAM_REGISTRY.with(|registry| { + registry + .borrow_mut() + .get_mut(&id) + .map(|state| std::mem::replace(&mut state.end_callback, undefined_value())) + .unwrap_or_else(undefined_value) + }); + let scope = crate::gc::RuntimeHandleScope::new(); + let end_callback = scope.root_nanbox_f64(end_callback); + call_stream_callback0(end_callback.get_nanbox_f64()); + emit_event0(id, "finish"); + } + schedule_next_write_stream_step(id); +} + +fn write_stream_close_step(id: usize) { + let force = STREAM_REGISTRY.with(|registry| { + registry + .borrow() + .get(&id) + .map(|state| state.destroyed) + .unwrap_or(false) + }); + maybe_close_stream(id, force); +} diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 5536fa94a9..b6de990e12 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -1123,12 +1123,13 @@ pub fn gc_init() { crate::symbol::new_symbol_side_table_root_scan_state, MutableRootScannerSource::RuntimeMutableScanner, ); - // Issue #1813: the implicit-`this` cell holds the live receiver across a - // dynamically-dispatched method body. A moving GC triggered from inside - // that body (e.g. @perryts/mysql Pool.acquire → handshake → nativeScramble - // under concurrent load) must rewrite the cell, or the body's next - // `this`-derived dispatch derefs a relocated receiver → SIGSEGV. - reg_scanner!(crate::object::scan_implicit_this_roots_mut); + // Issue #1813: the dispatch-binding cells (`new.target`, the one-shot + // static-`this` override, the static private-owner stack) hold a live heap + // value across a call body. A moving GC triggered from inside that body + // (e.g. @perryts/mysql Pool.acquire → handshake → nativeScramble under + // concurrent load, when the implicit-`this` cell still existed) must + // rewrite them, or the body's next read derefs a relocated value. + reg_scanner!(crate::object::scan_dispatch_binding_roots_mut); // Fresh class evaluations are lexical environments, not merely template // class ids. Method dispatch keeps the active evaluation here so private // accesses remain exact across `.call`/`.apply`; root and rewrite those diff --git a/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs b/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs index 00976de64e..45a1017b10 100644 --- a/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs +++ b/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs @@ -119,12 +119,16 @@ fn array_slot_enumeration_walks_the_heap() { extern "C" fn species_destination( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _length: f64, ) -> f64 { crate::closure::js_closure_get_capture_f64(closure, 0) } -extern "C" fn interrupt_species_copy(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn interrupt_species_copy( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::exception::js_throw(9983.0) } diff --git a/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs b/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs index cd2b46e64e..1e4a623ccb 100644 --- a/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs +++ b/crates/perry-runtime/src/gc/tests/copying/pointer_publish_7154.rs @@ -30,7 +30,10 @@ use super::*; -extern "C" fn test_bound_method_body(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn test_bound_method_body( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 0.0 } diff --git a/crates/perry-runtime/src/gc/tests/copying/weak_holder_registry.rs b/crates/perry-runtime/src/gc/tests/copying/weak_holder_registry.rs index 1ea175c046..617a4b07b1 100644 --- a/crates/perry-runtime/src/gc/tests/copying/weak_holder_registry.rs +++ b/crates/perry-runtime/src/gc/tests/copying/weak_holder_registry.rs @@ -303,6 +303,7 @@ fn test_registry_tracks_holder_across_three_moving_minors() { extern "C" fn finreg_registry_test_callback( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _held: f64, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) diff --git a/crates/perry-runtime/src/gc/tests/copying/weak_semantics.rs b/crates/perry-runtime/src/gc/tests/copying/weak_semantics.rs index 21dc222b33..478303a479 100644 --- a/crates/perry-runtime/src/gc/tests/copying/weak_semantics.rs +++ b/crates/perry-runtime/src/gc/tests/copying/weak_semantics.rs @@ -116,6 +116,7 @@ fn test_copying_minor_weakmap_dead_key_entry_clears() { extern "C" fn finreg_test_callback( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _held: f64, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs b/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs index bbfb2fd0d4..4627097e4c 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/object_closure_slots.rs @@ -125,7 +125,10 @@ fn test_typed_shape_descriptor_dynamic_pointer_mutation_falls_back_to_unknown_la clear_mark_seeds(); } -extern "C" fn layout_mask_test_closure(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn layout_mask_test_closure( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 0.0 } diff --git a/crates/perry-runtime/src/gc/tests/pinned_roots.rs b/crates/perry-runtime/src/gc/tests/pinned_roots.rs index fc53f567f4..711cb70b2b 100644 --- a/crates/perry-runtime/src/gc/tests/pinned_roots.rs +++ b/crates/perry-runtime/src/gc/tests/pinned_roots.rs @@ -261,12 +261,20 @@ std::thread_local! { static SETTLED_WITH: std::cell::Cell = const { std::cell::Cell::new(f64::NAN) }; } -extern "C" fn record_cb(_c: *const crate::closure::ClosureHeader, v: f64) -> f64 { +extern "C" fn record_cb( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + v: f64, +) -> f64 { SETTLED_WITH.with(|s| s.set(v)); v } -extern "C" fn overwrite_cb(_c: *const crate::closure::ClosureHeader, v: f64) -> f64 { +extern "C" fn overwrite_cb( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + v: f64, +) -> f64 { SETTLED_WITH.with(|s| s.set(-1.0)); v } diff --git a/crates/perry-runtime/src/gc/tests/rooted_container_values.rs b/crates/perry-runtime/src/gc/tests/rooted_container_values.rs index a3d765182b..d08ebcf323 100644 --- a/crates/perry-runtime/src/gc/tests/rooted_container_values.rs +++ b/crates/perry-runtime/src/gc/tests/rooted_container_values.rs @@ -141,6 +141,7 @@ thread_local! { /// already-collected `(key, item)` pairs goes stale. extern "C" fn group_by_moving_callback( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _item: f64, index: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/gc/tests/rooted_define_property.rs b/crates/perry-runtime/src/gc/tests/rooted_define_property.rs index 7bdd253754..9f6105cabd 100644 --- a/crates/perry-runtime/src/gc/tests/rooted_define_property.rs +++ b/crates/perry-runtime/src/gc/tests/rooted_define_property.rs @@ -77,7 +77,10 @@ fn addr_of(value: f64) -> usize { /// receiver `js_object_define_property` is holding — and then allocates the /// payload string, so the retired from-space bytes are reused before the caller /// reads its locals again. -extern "C" fn moving_value_getter(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn moving_value_getter( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let trace = collect_minor_trace(GcTriggerKind::Direct); GETTER_COPIED_OBJECTS.with(|c| c.set(c.get() + trace.copying_nursery.copied_objects)); string_value("payload") @@ -252,7 +255,10 @@ fn desc_view_field_values_are_rooted() { } } -extern "C" fn moving_writable_getter(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn moving_writable_getter( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let trace = collect_minor_trace(GcTriggerKind::Direct); GETTER_COPIED_OBJECTS.with(|c| c.set(c.get() + trace.copying_nursery.copied_objects)); f64::from_bits(crate::value::TAG_TRUE) diff --git a/crates/perry-runtime/src/gc/tests/rooted_for_in.rs b/crates/perry-runtime/src/gc/tests/rooted_for_in.rs index 206abc0be8..991780912f 100644 --- a/crates/perry-runtime/src/gc/tests/rooted_for_in.rs +++ b/crates/perry-runtime/src/gc/tests/rooted_for_in.rs @@ -8,7 +8,11 @@ thread_local! { static COPIED: std::cell::Cell = const { std::cell::Cell::new(0) }; } -extern "C" fn moving_own_keys(_closure: *const crate::closure::ClosureHeader, target: f64) -> f64 { +extern "C" fn moving_own_keys( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + target: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let target = scope.root_nanbox_f64(target); let trace = collect_minor_trace(GcTriggerKind::Direct); @@ -131,6 +135,7 @@ fn for_in_grown_result_and_receiver_survive_prototype_collection() { extern "C" fn moving_descriptor( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, ) -> f64 { @@ -150,7 +155,10 @@ fn descriptor_trap_collection_preserves_for_in_target_and_keys() { run(false, true); } -extern "C" fn moving_value(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn moving_value( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let trace = collect_minor_trace(GcTriggerKind::Direct); COPIED.with(|count| count.set(count.get() + trace.copying_nursery.copied_objects)); 23.0 @@ -158,6 +166,7 @@ extern "C" fn moving_value(_closure: *const crate::closure::ClosureHeader) -> f6 extern "C" fn descriptor_with_moving_field( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _target: f64, _key: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots.rs b/crates/perry-runtime/src/gc/tests/runtime_roots.rs index b78396beca..d7dbf94794 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots.rs @@ -9,7 +9,6 @@ mod fs_options_object; mod generator_attach_prototype; mod handle_stack; mod hook_dispatch_handles; -mod implicit_this_scope; mod interned_string_caches; mod iter_result_keys; mod json_construction; @@ -167,7 +166,10 @@ fn assert_callable_closure(bits: u64) -> usize { assert_eq!(bits & TAG_MASK, POINTER_TAG); let ptr = (bits & POINTER_MASK) as usize; assert_eq!( - crate::closure::js_closure_call0(ptr as *const crate::closure::ClosureHeader), + crate::closure::js_closure_call0( + ptr as *const crate::closure::ClosureHeader, + crate::closure::plain_call_receiver() + ), 0.0 ); ptr @@ -454,6 +456,7 @@ fn test_prototype_resolution_stack_scanner_is_registered() { extern "C" fn test_reviver_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _key: f64, value: f64, ) -> f64 { @@ -467,6 +470,7 @@ thread_local! { extern "C" fn test_reviver_count_closure_leaf( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _key: f64, value: f64, ) -> f64 { @@ -486,6 +490,7 @@ extern "C" fn test_reviver_count_closure_leaf( extern "C" fn test_promise_identity_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, ) -> f64 { let _ = crate::gc::gc_collect_minor(); @@ -494,6 +499,7 @@ extern "C" fn test_promise_identity_force_minor_gc( extern "C" fn test_promise_finally_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, ) -> f64 { let _ = crate::gc::gc_collect_minor(); @@ -502,6 +508,7 @@ extern "C" fn test_promise_finally_force_minor_gc( extern "C" fn test_array_identity_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, _index: f64, ) -> f64 { @@ -517,6 +524,7 @@ thread_local! { extern "C" fn test_foreach_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, key: f64, ) -> f64 { @@ -532,6 +540,7 @@ extern "C" fn test_foreach_force_minor_gc( extern "C" fn test_async_hook_init_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _async_id: f64, _type_name: f64, _trigger_async_id: f64, @@ -543,6 +552,7 @@ extern "C" fn test_async_hook_init_force_minor_gc( extern "C" fn test_async_hook_event_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _async_id: f64, ) -> f64 { let _ = crate::gc::gc_collect_minor(); @@ -557,6 +567,7 @@ thread_local! { extern "C" fn test_timer_capture_arg( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, arg: f64, ) -> f64 { TEST_TIMER_CALLBACK_PTR.with(|slot| slot.set(closure as usize)); @@ -565,13 +576,17 @@ extern "C" fn test_timer_capture_arg( f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn test_timer_force_minor_gc(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn test_timer_force_minor_gc( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let _ = crate::gc::gc_collect_minor(); f64::from_bits(crate::value::TAG_UNDEFINED) } extern "C" fn test_rest_first_value( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let rest_ptr = (rest.to_bits() & POINTER_MASK) as *const crate::array::ArrayHeader; diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/arraylike_callbacks.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/arraylike_callbacks.rs index c642cd13e8..0ecfb78796 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/arraylike_callbacks.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/arraylike_callbacks.rs @@ -13,6 +13,7 @@ use super::super::support::*; extern "C" fn collect_then_double( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, _index: f64, _recv: f64, diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/call_argument_lists.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/call_argument_lists.rs index 13f79d52f9..5afdf33174 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/call_argument_lists.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/call_argument_lists.rs @@ -31,6 +31,7 @@ fn seen() -> Vec { extern "C" fn record_this_and_six_args( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, a0: f64, a1: f64, _a2: f64, @@ -38,17 +39,13 @@ extern "C" fn record_this_and_six_args( _a4: f64, a5: f64, ) -> f64 { - record(&[ - crate::object::js_implicit_this_get().to_bits(), - a0.to_bits(), - a1.to_bits(), - a5.to_bits(), - ]); + record(&[this.bits(), a0.to_bits(), a1.to_bits(), a5.to_bits()]); 0.0 } extern "C" fn record_two_args( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a0: f64, a1: f64, ) -> f64 { @@ -59,6 +56,7 @@ extern "C" fn record_two_args( #[allow(clippy::too_many_arguments)] extern "C" fn record_rest_and_arguments_after_16( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _a0: f64, _a1: f64, _a2: f64, @@ -300,7 +298,12 @@ fn rest_bundling_roots_the_rest_array_across_the_arguments_array() { // under test here, so the closure pointer is a scoped argument to it — // `with_mut_ptr` is the blessed shape for that instead of a bare read. closure_handle.with_mut_ptr::(|ptr| unsafe { - crate::closure::js_closure_call_array(ptr as i64, args.as_ptr(), args.len() as i64); + crate::closure::js_closure_call_array( + ptr as i64, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len() as i64, + ); }); assert!( diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index 03929ff367..f2d2ef32e8 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -214,7 +214,8 @@ fn test_json_reviver_treats_closure_property_as_leaf_after_copied_minor_gc() { assert_eq!(stored & TAG_MASK, POINTER_TAG); assert_eq!( crate::closure::js_closure_call0( - (stored & POINTER_MASK) as *const crate::closure::ClosureHeader + (stored & POINTER_MASK) as *const crate::closure::ClosureHeader, + crate::closure::plain_call_receiver() ), 0.0 ); @@ -1262,7 +1263,10 @@ impl Drop for RuntimeCallbackRootGuard { fn assert_moved_callable_closure(bits: u64, original: usize) { let rewritten = assert_moved_closure_ptr(bits, original); assert_eq!( - crate::closure::js_closure_call0(rewritten as *const crate::closure::ClosureHeader), + crate::closure::js_closure_call0( + rewritten as *const crate::closure::ClosureHeader, + crate::closure::plain_call_receiver() + ), 0.0 ); } @@ -1655,6 +1659,7 @@ thread_local! { extern "C" fn test_sort_comparator_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a: f64, b: f64, ) -> f64 { @@ -1747,7 +1752,10 @@ fn test_array_sort_comparator_rooted_buffers_survive_copied_minor_gc() { } } -extern "C" fn test_tojson_force_minor_gc(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn test_tojson_force_minor_gc( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let _ = crate::gc::gc_collect_minor(); test_string_value(b"tojson-out") } @@ -1864,6 +1872,7 @@ fn test_json_stringify_array_rederives_elements_after_tojson_minor_gc() { extern "C" fn test_replacer_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, matched: f64, _offset: f64, _whole: f64, @@ -1912,6 +1921,7 @@ fn test_string_replace_all_fn_rederives_subject_after_callback_minor_gc() { extern "C" fn test_bigint_comparator_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a: f64, b: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs index e08b2eda40..a27f628429 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs @@ -36,7 +36,11 @@ use super::*; /// A function pointer to register in the generator-function registry. Never /// called — only its address is used. -extern "C" fn fake_generator_body(_c: *const crate::closure::ClosureHeader, _a: f64) -> f64 { +extern "C" fn fake_generator_body( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _a: f64, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs index d249f341eb..c872fcd4ab 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs @@ -1,6 +1,9 @@ use super::*; -extern "C" fn test_current_async_id(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn test_current_async_id( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::async_hooks::execution_async_id_u64() as f64 } @@ -88,7 +91,14 @@ fn test_closure_rest_dispatch_roots_args_during_rest_array_alloc_gc() { force_next_general_arena_alloc_slow(); trigger_guard.make_arena_trigger_due(); let before = gc_collection_count(); - let result = unsafe { crate::closure::js_closure_call_array(closure as i64, args.as_ptr(), 1) }; + let result = unsafe { + crate::closure::js_closure_call_array( + closure as i64, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 1, + ) + }; let result_scope = RuntimeHandleScope::new(); let result_root = result_scope.root_nanbox_f64(result); drain_scheduled_minor_gc(before, "rest-array creation"); @@ -124,6 +134,7 @@ fn test_bound_timer_dispatch_roots_args_during_async_hook_init_gc() { let timer_value = crate::closure::js_closure_call3( bound, + crate::closure::plain_call_receiver(), f64::from_bits(ptr_bits(timer_callback as usize)), 0.0, arg, diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/implicit_this_scope.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/implicit_this_scope.rs deleted file mode 100644 index 1275d81e81..0000000000 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/implicit_this_scope.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Moving-GC regression for the runtime's implicit-`this` guards (#10490). -//! -//! A runtime entry point that binds `IMPLICIT_THIS` for a callback displaces -//! the CALLER's receiver and writes it back when the callback returns. The -//! callback is user code, so a copying minor inside it can move that receiver; -//! a displaced value kept in a plain Rust field is not a root and is restored -//! as a retired from-space address — the caller's next `this.x` reads -//! `undefined`. `js_native_call_method`'s prototype-override early path and -//! the dense/array-like `Array.prototype` callback engines each carried such a -//! guard. Every test here plants a callback that runs a copying minor, then -//! asserts the restored cell is the caller's RELOCATED receiver — and that the -//! receiver actually moved, so a green run cannot be vacuous. - -use super::super::super::*; -use super::super::support::*; - -extern "C" fn collect_arity0(_closure: *const crate::closure::ClosureHeader) -> f64 { - crate::gc::gc_collect_minor(); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -extern "C" fn collect_arity3( - _closure: *const crate::closure::ClosureHeader, - _value: f64, - _index: f64, - _recv: f64, -) -> f64 { - crate::gc::gc_collect_minor(); - f64::from_bits(crate::value::TAG_UNDEFINED) -} - -fn boxed(ptr: *const u8) -> f64 { - f64::from_bits(crate::JSValue::pointer(ptr).bits()) -} - -/// Install a young object as the caller's `this`, run `call`, and assert the -/// cell afterwards holds that object's post-collection address. -fn assert_caller_this_survives(what: &str, call: impl FnOnce()) { - let _guard = CopyingNurseryTestGuard::new(0); - let _scan = ConservativeScanDisabledGuard::new(); - let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - let _force = ForcedEvacuationTestGuard::on(); - register_runtime_handle_root_scanner_for_tests(); - - let scope = crate::gc::RuntimeHandleScope::new(); - let caller = scope.root_nanbox_f64(boxed(crate::object::js_object_alloc(0, 0) as *const u8)); - let caller_before = caller.get_nanbox_u64(); - let outer = crate::object::js_implicit_this_set(caller.get_nanbox_f64()); - - let cycles_before = copying_minor_cycles(); - call(); - assert!( - copying_minor_cycles() > cycles_before, - "{what}: the callback must run a copying minor" - ); - assert_ne!( - caller.get_nanbox_u64(), - caller_before, - "{what}: the collection must actually move the caller's receiver" - ); - let restored = crate::object::js_implicit_this_set(outer); - assert_eq!( - restored.to_bits(), - caller.get_nanbox_u64(), - "{what}: the displaced `this` must be restored at its relocated address, \ - not the pre-collection one ({caller_before:#x})" - ); -} - -#[test] -fn implicit_this_scope_restores_a_relocated_receiver() { - assert_caller_this_survives("ImplicitThisScope", || { - let scope = crate::gc::RuntimeHandleScope::new(); - let _bound = crate::object::ImplicitThisScope::bind( - &scope, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - crate::gc::gc_collect_minor(); - }); -} - -#[test] -fn prototype_override_method_call_restores_a_relocated_caller_this() { - assert_caller_this_survives("js_native_call_method prototype override", || { - crate::closure::js_register_closure_arity(collect_arity0 as *const u8, 0); - let scope = crate::gc::RuntimeHandleScope::new(); - let method = scope - .root_nanbox_f64(boxed( - crate::closure::js_closure_alloc(collect_arity0 as *const u8, 0) as *const u8, - )); - let proto = scope.root_nanbox_f64(boxed(crate::object::js_object_alloc(0, 1) as *const u8)); - let key = crate::string::js_string_from_bytes(b"run".as_ptr(), 3); - crate::object::js_object_set_field_by_name( - crate::value::js_nanbox_get_pointer(proto.get_nanbox_f64()) as *mut crate::ObjectHeader, - key, - method.get_nanbox_f64(), - ); - let receiver = - scope.root_nanbox_f64(boxed(crate::object::js_object_alloc(0, 0) as *const u8)); - crate::object::object_ops::js_object_set_prototype_of( - receiver.get_nanbox_f64(), - proto.get_nanbox_f64(), - ); - assert!( - crate::object::prototype_chain::object_has_individual_class_prototype( - crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as usize - ), - "the receiver must take the #9247 prototype-override early path" - ); - unsafe { - crate::object::js_native_call_method( - receiver.get_nanbox_f64(), - b"run".as_ptr().cast(), - 3, - std::ptr::null(), - 0, - ); - } - }); -} - -#[test] -fn dense_array_for_each_restores_a_relocated_caller_this() { - assert_caller_this_survives("js_array_forEach", || { - let scope = crate::gc::RuntimeHandleScope::new(); - let arr = crate::array::js_array_push_f64(crate::array::js_array_alloc(0), 1.0); - let arr = scope.root_raw_const_ptr(arr); - let cb = crate::closure::js_closure_alloc_singleton(collect_arity3 as *const u8); - // `js_array_forEach` roots the receiver itself, so a scoped argument is - // the right shape here (#7341). - arr.with_const_ptr(|ptr| crate::array::js_array_forEach(ptr, cb)); - }); -} - -#[test] -fn arraylike_for_each_restores_a_relocated_caller_this() { - assert_caller_this_survives("js_arraylike_forEach", || { - let scope = crate::gc::RuntimeHandleScope::new(); - let arr = crate::array::js_array_push_f64(crate::array::js_array_alloc(0), 1.0); - let arr = scope.root_nanbox_f64(boxed(arr as *const u8)); - let cb = crate::closure::js_closure_alloc_singleton(collect_arity3 as *const u8); - crate::array::js_arraylike_forEach( - arr.get_nanbox_f64(), - boxed(cb as *const u8), - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - }); -} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/json_record_output.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/json_record_output.rs index 42c6dd1245..3b577a35f7 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/json_record_output.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/json_record_output.rs @@ -72,7 +72,10 @@ fn json_nested_records_fallback_runs_getter_once_and_survives_actual_movement() } } -extern "C" fn array_getter_collects(_closure: *const crate::ClosureHeader) -> f64 { +extern "C" fn array_getter_collects( + _closure: *const crate::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { GETTER_CALLS.with(|c| c.set(c.get() + 1)); crate::gc::gc_collect_minor(); 17.0 diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs index fbf11f0a06..c86568e799 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs @@ -296,12 +296,18 @@ fn perex_recompile_publishes_after_success_before_throwing_lastindex_write() { ); } -extern "C" fn flags_collect_then_throw(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn flags_collect_then_throw( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(812.0) } -extern "C" fn flags_collect_then_return(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn flags_collect_then_return( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); js_nanbox_string(crate::string::js_string_from_bytes(b"g".as_ptr(), 1) as i64) } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs index d1988a7019..88d89cd4be 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs @@ -67,37 +67,60 @@ fn test(receiver: &RuntimeHandle<'_>, input: &RuntimeHandle<'_>) -> bool { input.with_const_ptr(|input| crate::regex::js_regexp_test(raw, input)) != 0 } -extern "C" fn return_this(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { - crate::object::js_implicit_this_get() +extern "C" fn return_this( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { + this.as_f64() } -extern "C" fn return_null(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn return_null( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { f64::from_bits(TAG_NULL) } -extern "C" fn read_answer(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn read_answer( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); api::finish(dispatch::get(&receiver, b"answer")) } -extern "C" fn collect_and_echo(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { +extern "C" fn collect_and_echo( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); let argument = scope.root_nanbox_f64(arg); gc_collect_minor(); put(&receiver, b"seen", &argument); receiver.get_nanbox_f64() } -extern "C" fn collecting_getter(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn collecting_getter( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); let fp = collect_and_echo as *const u8; crate::closure::js_register_closure_arity(fp, 1); js_nanbox_pointer(crate::closure::js_closure_alloc_singleton(fp) as i64) } -extern "C" fn collect_missing_apply(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn collect_missing_apply( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); f64::from_bits(TAG_UNDEFINED) } extern "C" fn echo_apply( _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _target: f64, receiver: f64, args: f64, @@ -115,23 +138,40 @@ extern "C" fn echo_apply( put(&receiver, b"seen", &value); receiver.get_nanbox_f64() } -extern "C" fn throw_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn throw_exec( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(843.0) } -extern "C" fn throw_getter(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn throw_getter( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(844.0) } -extern "C" fn ordered_string(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn ordered_string( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { ORDER.with(|n| n.set(n.get() * 10 + 1)); js_nanbox_string(crate::string::js_string_from_bytes(b"x".as_ptr(), 1) as i64) } -extern "C" fn ordered_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn ordered_exec( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { ORDER.with(|n| n.set(n.get() * 10 + 3)); js_nanbox_pointer(crate::array::js_array_alloc(0) as i64) } -extern "C" fn ordered_getter(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn ordered_getter( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { ORDER.with(|n| n.set(n.get() * 10 + 2)); let fp = ordered_exec as *const u8; crate::closure::js_register_closure_arity(fp, 1); @@ -191,19 +231,24 @@ fn perex_dispatch_generic_prototype_test_coerces_before_getting_exec() { )); assert!(crate::proxy::proxy_wraps_callable(method.get_nanbox_f64())); ORDER.with(|n| n.set(0)); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - receiver.get_nanbox_f64(), - )); let args = [argument.get_nanbox_f64()]; let result = crate::exception::catch_js_throw(|| unsafe { - crate::closure::js_native_call_value(method.get_nanbox_f64(), args.as_ptr(), 1) + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + args.as_ptr(), + 1, + ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); assert_eq!(result.unwrap().to_bits(), crate::value::TAG_TRUE); assert_eq!(ORDER.with(Cell::get), 123); ORDER.with(|n| n.set(0)); assert!(matches!( - dispatch::test_value(1.0, argument.get_nanbox_f64()), + dispatch::test_value( + crate::closure::plain_call_receiver(), + 1.0, + argument.get_nanbox_f64() + ), Err(EngineError::Type(_)) )); assert_eq!( @@ -226,10 +271,6 @@ fn perex_dispatch_getter_and_callback_reacquire_original_input_after_gc() { let before = handle_address::(&input); let method = function(&scope, collecting_getter as *const u8, 0); getter(&receiver, &method); - let displaced = object(&scope); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - displaced.get_nanbox_f64(), - )); let roots = RuntimeHandleScope::active_len_for_tests(); let result = api::finish(dispatch::execute( &receiver, @@ -249,11 +290,6 @@ fn perex_dispatch_getter_and_callback_reacquire_original_input_after_gc() { api::finish(dispatch::get(&receiver, b"seen")).to_bits(), handle_string_value(&input).to_bits() ); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - displaced.get_nanbox_f64().to_bits() - ); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); } #[test] @@ -288,10 +324,6 @@ fn perex_dispatch_actual_throws_restore_this_roots_and_accounting() { } else { put(&receiver, b"exec", &method); } - let displaced = object(&scope); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - displaced.get_nanbox_f64(), - )); let roots = RuntimeHandleScope::active_len_for_tests(); let live = external_side_live_bytes(); let result = @@ -304,11 +336,6 @@ fn perex_dispatch_actual_throws_restore_this_roots_and_accounting() { assert_ne!(handle_address::(&input), before); assert_eq!(RuntimeHandleScope::active_len_for_tests(), roots); assert_eq!(external_side_live_bytes(), live); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - displaced.get_nanbox_f64().to_bits() - ); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); } } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_glob.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_glob.rs index f925f4d5ea..2bc3485c95 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_glob.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_glob.rs @@ -151,7 +151,11 @@ impl Drop for Directory { std::fs::remove_dir_all(&self.0).unwrap(); } } -extern "C" fn exclude(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { +extern "C" fn exclude( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let arg = scope.root_nanbox_f64(arg); gc_collect_minor(); @@ -164,7 +168,11 @@ extern "C" fn exclude(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 TAG_FALSE }) } -extern "C" fn throw_exclude(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn throw_exclude( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(991.0) } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_all.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_all.rs index 3fb3eb69c2..3e041d91b7 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_all.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_all.rs @@ -132,23 +132,47 @@ fn retained_input_population() -> usize { && bytes(js_nanbox_string(p as i64)) == b"a unique input retained only by the iterator" }) } -extern "C" fn identity(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { +extern "C" fn identity( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { arg } -extern "C" fn throw_getter(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn throw_getter( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::exception::js_throw(941.0) } -extern "C" fn throw_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn throw_exec( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(942.0) } -extern "C" fn no_match(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn no_match( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { f64::from_bits(TAG_NULL) } -extern "C" fn return_this(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { - crate::object::js_implicit_this_get() +extern "C" fn return_this( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { + this.as_f64() } -extern "C" fn factory(_: *const crate::closure::ClosureHeader, receiver: f64, flags: f64) -> f64 { +extern "C" fn factory( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + receiver: f64, + flags: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let flags = scope.root_nanbox_f64(flags); @@ -511,7 +535,10 @@ fn perex_match_all_proxy_species_and_dynamic_regexp_constructors_keep_original_s assert_eq!(get(&step, b"done").to_bits(), crate::value::TAG_TRUE); } -extern "C" fn collecting_missing_construct(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn collecting_missing_construct( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); f64::from_bits(TAG_UNDEFINED) } @@ -520,41 +547,61 @@ thread_local! { static ORDER: Cell = const { Cell::new(0) }; } fn ordered(event: u64) { ORDER.with(|n| n.set(n.get() * 10 + event)); } -fn ordered_field(event: u64, name: &[u8]) -> f64 { +fn ordered_field(this: crate::closure::JsThis, event: u64, name: &[u8]) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); ordered(event); gc_collect_minor(); get(&receiver, name) } -extern "C" fn ordered_input(_: *const crate::closure::ClosureHeader) -> f64 { - ordered_field(1, b"text") +extern "C" fn ordered_input( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + ordered_field(this, 1, b"text") } -extern "C" fn ordered_constructor(_: *const crate::closure::ClosureHeader) -> f64 { - ordered_field(2, b"holder") +extern "C" fn ordered_constructor( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + ordered_field(this, 2, b"holder") } -extern "C" fn ordered_species(_: *const crate::closure::ClosureHeader) -> f64 { - ordered_field(3, b"factory") +extern "C" fn ordered_species( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + ordered_field(this, 3, b"factory") } -extern "C" fn ordered_flags(_: *const crate::closure::ClosureHeader) -> f64 { - ordered_field(4, b"flagText") +extern "C" fn ordered_flags( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + ordered_field(this, 4, b"flagText") } extern "C" fn ordered_factory( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, receiver: f64, flags: f64, ) -> f64 { ordered(5); - factory(closure, receiver, flags) + factory(closure, this, receiver, flags) } -extern "C" fn ordered_index(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn ordered_index( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { ordered(6); gc_collect_minor(); 1.75 } -extern "C" fn ordered_set_index(_: *const crate::closure::ClosureHeader, index: f64) -> f64 { +extern "C" fn ordered_set_index( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + index: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); ordered(7); gc_collect_minor(); put(&receiver, b"seenIndex", index); @@ -619,7 +666,11 @@ fn perex_match_all_species_and_lastindex_order_survives_collecting_callbacks() { assert_eq!(bytes(get(&matcher, b"seenFlags")), b"gu"); } -extern "C" fn arrow_species(_: *const crate::closure::ClosureHeader, value: f64) -> f64 { +extern "C" fn arrow_species( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { value } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_search.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_search.rs index d47029787f..7518c8fa5b 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_search.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_match_search.rs @@ -69,28 +69,47 @@ fn bytes(value: f64) -> Vec { let (data, n) = crate::string::str_bytes_from_jsvalue(value, &mut short).unwrap(); unsafe { std::slice::from_raw_parts(data, n as usize).to_vec() } } -extern "C" fn identity(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { +extern "C" fn identity( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { arg } -extern "C" fn throw_string(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn throw_string( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::exception::js_throw(901.0) } -extern "C" fn search_override(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn search_override( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); gc_collect_minor(); api::finish(dispatch::set_last_index(&receiver, 7.0)); receiver.get_nanbox_f64() } -extern "C" fn search_throw(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn search_throw( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); api::finish(dispatch::set_last_index(&receiver, 7.0)); crate::exception::js_throw(902.0) } -extern "C" fn global_override(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn global_override( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); gc_collect_minor(); let count = api::finish(dispatch::get(&receiver, b"calls")); if count == 0.0 { @@ -106,7 +125,10 @@ extern "C" fn global_override(_: *const crate::closure::ClosureHeader, _: f64) - put(&receiver, b"0", &capture); receiver.get_nanbox_f64() } -extern "C" fn collecting_flags(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn collecting_flags( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); js_nanbox_string(crate::string::js_string_from_bytes(b"gu".as_ptr(), 2) as i64) } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs index b368f6c518..8ffc0b3854 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs @@ -33,10 +33,9 @@ pub(super) fn register_host_roots() { gc_register_mutable_root_scanner( crate::object::regex_proto_thunks::scan_canonical_test_site_roots_mut, ); - gc_register_mutable_root_scanner(crate::object::scan_implicit_this_roots_mut); - // PR #10564 review finding: implicit_this/new_target savepoints in - // exception.rs are a second root for whatever these tests displace - // IMPLICIT_THIS to across a throw. gc_init registers this in + gc_register_mutable_root_scanner(crate::object::scan_dispatch_binding_roots_mut); + // PR #10564 review finding: the new_target savepoint in exception.rs is + // a second root for whatever a throw displaces. gc_init registers this in // production; the isolation guard clears that registry too. gc_register_mutable_root_scanner(crate::exception::scan_exception_roots_mut); gc_register_mutable_root_scanner(crate::closure::scan_singleton_closure_roots_mut); @@ -314,7 +313,10 @@ fn perex_public_throwing_lastindex_write_releases_native_scratch_and_roots() { ); } -extern "C" fn throw_on_coercion(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn throw_on_coercion( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(731.0) } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs index ae48894c50..6dd759f8ff 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs @@ -106,10 +106,18 @@ pub(super) fn captured<'s>( ); f } -extern "C" fn throw_text(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn throw_text( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::exception::js_throw(971.0) } -extern "C" fn hook(_: *const crate::closure::ClosureHeader, input: f64, replacement: f64) -> f64 { +extern "C" fn hook( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + input: f64, + replacement: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let input = scope.root_nanbox_f64(input); let replacement = scope.root_nanbox_f64(replacement); @@ -119,6 +127,7 @@ extern "C" fn hook(_: *const crate::closure::ClosureHeader, input: f64, replacem } extern "C" fn builtin_callback( c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, matched: f64, first: f64, absent: f64, @@ -133,10 +142,7 @@ extern "C" fn builtin_callback( let input = scope.root_nanbox_f64(input); let groups = scope.root_nanbox_f64(groups); gc_collect_minor(); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - TAG_UNDEFINED - ); + assert_eq!(this.as_f64().to_bits(), TAG_UNDEFINED); assert_eq!( get(&state, b"lastIndex"), if position == 0.0 { 0.0 } else { 77.0 } @@ -163,9 +169,13 @@ extern "C" fn builtin_callback( ); matched.get_nanbox_f64() } -extern "C" fn alias_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn alias_exec( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); - let state = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let state = scope.root_nanbox_f64(this.as_f64()); gc_collect_minor(); let n = get(&state, b"calls"); put(&state, b"calls", n + 1.0); @@ -180,6 +190,7 @@ extern "C" fn alias_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 } extern "C" fn alias_callback( c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, matched: f64, position: f64, input: f64, @@ -205,7 +216,10 @@ extern "C" fn alias_callback( put(&state, b"replacements", get(&state, b"replacements") + 1.0); js_nanbox_string(crate::string::js_string_from_bytes(b"X".as_ptr(), 1) as i64) } -extern "C" fn named_getter(c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn named_getter( + c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = RuntimeHandleScope::new(); let state = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(c, 0)); gc_collect_minor(); @@ -216,6 +230,7 @@ extern "C" fn named_getter(c: *const crate::closure::ClosureHeader) -> f64 { } extern "C" fn collecting_throw( _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _: f64, _: f64, _: f64, @@ -405,7 +420,7 @@ fn perex_replace_hooks_preserve_receiver_and_arbitrary_result_before_coercion() } #[test] -fn perex_replace_collecting_throw_cleans_native_arguments_roots_and_this() { +fn perex_replace_collecting_throw_cleans_native_arguments_and_roots() { let _guard = CopyingNurseryTestGuard::new(0); let _scan = ConservativeScanDisabledGuard::new(); let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); @@ -415,9 +430,8 @@ fn perex_replace_collecting_throw_cleans_native_arguments_roots_and_this() { let input = text(&scope, b"abc"); let search = text(&scope, b"b"); let callback = function(&scope, collecting_throw as *const u8, 3); + // A young object whose move proves the throwing callback's collection ran. let previous = object(&scope); - let displaced = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); // Warm lazy String/Function prototype setup before checking native bytes. let empty = text(&scope, b"none"); let _ = crate::regex::js_string_replace_js( @@ -440,15 +454,13 @@ fn perex_replace_collecting_throw_cleans_native_arguments_roots_and_this() { assert_eq!(RuntimeHandleScope::active_len_for_tests(), roots); assert_eq!(external_side_live_bytes(), live); assert_ne!(previous.get_nanbox_f64().to_bits(), before); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - previous.get_nanbox_f64().to_bits() - ); - crate::object::js_implicit_this_set(displaced.get_nanbox_f64()); } -extern "C" fn primitive_hook_getter(_: *const crate::closure::ClosureHeader) -> f64 { - assert_eq!(crate::object::js_implicit_this_get(), 23.0); +extern "C" fn primitive_hook_getter( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + assert_eq!(this.as_f64(), 23.0); gc_collect_minor(); crate::closure::js_register_closure_arity(hook as *const u8, 2); js_nanbox_pointer(crate::closure::js_closure_alloc_singleton(hook as *const u8) as i64) diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace_direct.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace_direct.rs index a3839ddeaf..894dd8a0d6 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace_direct.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace_direct.rs @@ -114,6 +114,7 @@ fn direct_templates_match_the_ordinary_loop() { extern "C" fn describe( _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, matched: f64, capture: f64, position: f64, @@ -197,6 +198,7 @@ fn direct_callbacks_receive_the_ordinary_arguments() { extern "C" fn meddle( c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, matched: f64, _position: f64, _input: f64, @@ -212,7 +214,11 @@ extern "C" fn meddle( matched.get_nanbox_f64() } -extern "C" fn never_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn never_exec( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { f64::from_bits(crate::value::TAG_NULL) } @@ -229,7 +235,11 @@ fn a_replacer_cannot_change_which_matches_are_replaced() { assert_eq!(last, 0.0); } -extern "C" fn counting_exec(c: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn counting_exec( + c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let state = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(c, 0)); put(&state, b"calls", get(&state, b"calls") + 1.0); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs index 8c4c192ea1..7a8b517d1b 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs @@ -134,7 +134,10 @@ fn getter(owner: &RuntimeHandle<'_>, name: &[u8], value: &RuntimeHandle<'_>) { f64::from_bits(TAG_UNDEFINED), ); } -extern "C" fn throwing(_: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn throwing( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::exception::js_throw(983.0) } @@ -239,7 +242,12 @@ fn perex_split_literal_half_pairs_nonoverlap_and_uint32_limits() { assert_eq!(bytes(item(&result, 4095)), b"\xed\xb8\x80"); } -extern "C" fn hook(_: *const crate::closure::ClosureHeader, input: f64, limit: f64) -> f64 { +extern "C" fn hook( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + input: f64, + limit: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let input = scope.root_nanbox_f64(input); let limit = scope.root_nanbox_f64(limit); @@ -251,8 +259,11 @@ extern "C" fn hook(_: *const crate::closure::ClosureHeader, input: f64, limit: f )); input.get_nanbox_f64() } -extern "C" fn primitive_hook(_: *const crate::closure::ClosureHeader) -> f64 { - assert_eq!(crate::object::js_implicit_this_get(), 23.0); +extern "C" fn primitive_hook( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + assert_eq!(this.as_f64(), 23.0); gc_collect_minor(); crate::closure::js_register_closure_arity(hook as *const u8, 2); js_nanbox_pointer(crate::closure::js_closure_alloc_singleton(hook as *const u8) as i64) @@ -310,14 +321,22 @@ fn event(n: u64) { ORDER.with(|o| o.set(o.get() * 10 + n)); } -extern "C" fn matrix_third(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(2, b"value") +extern "C" fn matrix_third( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 2, b"value") } -extern "C" fn matrix_hook(_: *const crate::closure::ClosureHeader, input: f64, _: f64) -> f64 { +extern "C" fn matrix_hook( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + input: f64, + _: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let input = scope.root_nanbox_f64(input); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); assert!(crate::proxy::reflect_value_is_object( receiver.get_nanbox_f64() )); @@ -328,10 +347,11 @@ extern "C" fn matrix_hook(_: *const crate::closure::ClosureHeader, input: f64, _ input.get_nanbox_f64() } -extern "C" fn matrix_hook_getter(_: *const crate::closure::ClosureHeader) -> f64 { - assert!(crate::proxy::reflect_value_is_object( - crate::object::js_implicit_this_get() - )); +extern "C" fn matrix_hook_getter( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + assert!(crate::proxy::reflect_value_is_object(this.as_f64())); event(3); gc_collect_minor(); crate::closure::js_register_closure_arity(matrix_hook as *const u8, 2); @@ -552,29 +572,49 @@ fn perex_string_methods_ignore_primitive_hooks_and_preserve_boxed_hooks_after_gc } } -fn field_event(n: u64, name: &[u8]) -> f64 { +fn field_event(this: crate::closure::JsThis, n: u64, name: &[u8]) -> f64 { let scope = RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); event(n); gc_collect_minor(); get(&receiver, name) } -extern "C" fn input_text(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(1, b"text") +extern "C" fn input_text( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 1, b"text") } -extern "C" fn constructor_get(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(2, b"holder") +extern "C" fn constructor_get( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 2, b"holder") } -extern "C" fn species_get(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(3, b"factory") +extern "C" fn species_get( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 3, b"factory") } -extern "C" fn flags_get(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(4, b"flagText") +extern "C" fn flags_get( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 4, b"flagText") } -extern "C" fn limit_get(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(6, b"number") +extern "C" fn limit_get( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 6, b"number") } -extern "C" fn factory(_: *const crate::closure::ClosureHeader, receiver: f64, flags: f64) -> f64 { +extern "C" fn factory( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + receiver: f64, + flags: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let flags = scope.root_nanbox_f64(flags); @@ -584,7 +624,11 @@ extern "C" fn factory(_: *const crate::closure::ClosureHeader, receiver: f64, fl put(&matcher, b"seenFlags", flags.get_nanbox_f64()); matcher.get_nanbox_f64() } -extern "C" fn empty_exec(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { +extern "C" fn empty_exec( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, +) -> f64 { event(7); gc_collect_minor(); f64::from_bits(TAG_NULL) @@ -661,11 +705,15 @@ fn perex_split_species_order_zero_limit_and_empty_input() { ); } -extern "C" fn custom_exec(c: *const crate::closure::ClosureHeader, input: f64) -> f64 { +extern "C" fn custom_exec( + c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + input: f64, +) -> f64 { let scope = RuntimeHandleScope::new(); let state = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(c, 0)); let input = scope.root_nanbox_f64(input); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); gc_collect_minor(); assert_eq!( input.get_nanbox_f64().to_bits(), @@ -682,7 +730,10 @@ extern "C" fn custom_exec(c: *const crate::closure::ClosureHeader, input: f64) - check(&nested, &[Some(b"a"), Some(b"c")]); get(&state, b"result") } -extern "C" fn capture_get(c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn capture_get( + c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = RuntimeHandleScope::new(); let state = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(c, 0)); gc_collect_minor(); @@ -749,12 +800,17 @@ fn perex_split_custom_exec_capture_values_reentrancy_and_limit_short_circuit() { ); } -extern "C" fn throwing_hook(_: *const crate::closure::ClosureHeader, _: f64, _: f64) -> f64 { +extern "C" fn throwing_hook( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, + _: f64, +) -> f64 { gc_collect_minor(); crate::exception::js_throw(984.0) } #[test] -fn perex_split_collecting_throw_releases_native_arguments_and_restores_this() { +fn perex_split_collecting_throw_releases_native_arguments() { let _guard = CopyingNurseryTestGuard::new(0); let _scan = ConservativeScanDisabledGuard::new(); let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); @@ -765,9 +821,8 @@ fn perex_split_collecting_throw_releases_native_arguments_and_restores_this() { let sep = object(&scope); let method = function(&scope, throwing_hook as *const u8, 2); symbol(&sep, "split", method.get_nanbox_f64()); + // A young object whose move proves the throwing hook's collection ran. let previous = object(&scope); - let displaced = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); let roots = RuntimeHandleScope::active_len_for_tests(); let live = external_side_live_bytes(); let before = previous.get_nanbox_f64().to_bits(); @@ -779,11 +834,6 @@ fn perex_split_collecting_throw_releases_native_arguments_and_restores_this() { assert_eq!(RuntimeHandleScope::active_len_for_tests(), roots); assert_eq!(external_side_live_bytes(), live); assert_ne!(previous.get_nanbox_f64().to_bits(), before); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - previous.get_nanbox_f64().to_bits() - ); - crate::object::js_implicit_this_set(displaced.get_nanbox_f64()); } #[test] @@ -908,19 +958,30 @@ fn perex_split_raw_copy_reacquires_moved_storage_and_cleans_up_partial_output() assert_eq!(external_side_live_bytes(), live); } -extern "C" fn primitive_result(c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn primitive_result( + c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(c, 0)); gc_collect_minor(); value.get_nanbox_f64() } -extern "C" fn primitive_number_hint(c: *const crate::closure::ClosureHeader, hint: f64) -> f64 { +extern "C" fn primitive_number_hint( + c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + hint: f64, +) -> f64 { assert_eq!(bytes(hint), b"number"); - primitive_result(c) + primitive_result(c, this) } -extern "C" fn primitive_string_hint(c: *const crate::closure::ClosureHeader, hint: f64) -> f64 { +extern "C" fn primitive_string_hint( + c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + hint: f64, +) -> f64 { assert_eq!(bytes(hint), b"string"); - primitive_result(c) + primitive_result(c, this) } #[test] @@ -933,9 +994,6 @@ fn perex_abstract_string_conversion_rejects_symbols_after_collecting_object_hook let scope = RuntimeHandleScope::new(); let symbol_value = scope.root_nanbox_f64(unsafe { crate::symbol::js_symbol_new_empty() }); let string_value = text(&scope, b"23"); - let previous = object(&scope); - let displaced = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); for value in [&symbol_value, &string_value] { for mode in 0..4 { let local = RuntimeHandleScope::new(); @@ -979,13 +1037,8 @@ fn perex_abstract_string_conversion_rejects_symbols_after_collecting_object_hook let result = local.root_string_ptr(result.unwrap()); assert_eq!(bytes(handle_string_value(&result)), b"23"); } - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - previous.get_nanbox_f64().to_bits() - ); } } - crate::object::js_implicit_this_set(displaced.get_nanbox_f64()); } #[test] @@ -1048,8 +1101,11 @@ fn perex_construction_uses_strict_string_conversion_for_pattern_and_flags() { } } } -extern "C" fn separator_text(_: *const crate::closure::ClosureHeader) -> f64 { - field_event(8, b"text") +extern "C" fn separator_text( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_event(this, 8, b"text") } #[test] @@ -1242,6 +1298,7 @@ fn perex_split_forward_search_matches_the_sticky_specification() { /// JavaScript can reach it afterwards, as any user factory could. extern "C" fn recording_regexp_species( _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, receiver: f64, flags: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/regexp_last_index.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/regexp_last_index.rs index 6b1b323f33..db3c3eb259 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/regexp_last_index.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/regexp_last_index.rs @@ -27,7 +27,10 @@ use crate::regex::RegExpHeader; use crate::string::StringHeader; /// Stand-in for `{ valueOf() { …allocating user JS…; return 0 } }`. -extern "C" fn collect_then_zero(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn collect_then_zero( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::gc::gc_collect_minor(); // Recycle the just-retired from-space blocks back out as fresh strings, so // a stale borrow reads THESE bytes rather than the subject's old ones. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs index 1a1943d11f..0c06d6312f 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/side_table_scanners.rs @@ -1,66 +1,5 @@ use super::*; -#[test] -fn test_implicit_this_root_scanner_marks_and_rewrites() { - // Regression for #1813. The implicit-`this` cell holds the NaN-boxed - // receiver across a dynamically-dispatched non-arrow method body. Under a - // moving GC triggered from inside that body (the @perryts/mysql - // Pool.acquire → handshake → nativeScramble path under concurrent load) - // the receiver relocates. The scanner must (a) MARK it so it is not swept - // when the cell is its only root, and (b) REWRITE the cell to the moved - // copy so the body's next `this`-derived dispatch derefs live memory - // instead of the stale slot (the reported SIGSEGV in js_native_call_method). - // `nursery_user` is live across the `arena_alloc_gc_old` call below (used - // afterwards to build `nursery_hdr`); the block-full slow path in that - // allocation can reach `gc_check_trigger()`, so suppress automatic - // triggers for the setup below. - let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - clear_marks(); - clear_mark_seeds(); - let prev_this = crate::object::js_implicit_this_get(); - - let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); - let valid_ptrs = build_valid_pointer_set(); - let old_user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT); - let nursery_hdr = unsafe { header_from_user_ptr(nursery_user) as *mut GcHeader }; - - crate::object::js_implicit_this_set(f64::from_bits(ptr_bits(nursery_user as usize))); - - // Mark phase: the live receiver must be discovered as a root. - crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_mark(&valid_ptrs)); - unsafe { - assert_ne!( - (*nursery_hdr).gc_flags & GC_FLAG_MARKED, - 0, - "IMPLICIT_THIS scanner must mark the receiver so GC does not sweep `this`" - ); - } - - // Rewrite phase: the cell must follow the forwarding pointer. - unsafe { - set_forwarding_address(nursery_hdr, old_user); - } - crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - ptr_bits(old_user as usize), - "IMPLICIT_THIS must be rewritten to the receiver's relocated copy (#1813)" - ); - - // Idle / undefined cell must be a no-op (the default state between calls). - crate::object::js_implicit_this_set(f64::from_bits(crate::value::TAG_UNDEFINED)); - crate::object::scan_implicit_this_roots_mut(&mut RuntimeRootVisitor::for_rewrite(&valid_ptrs)); - assert_eq!( - crate::object::js_implicit_this_get().to_bits(), - crate::value::TAG_UNDEFINED, - "scanning the idle implicit-`this` cell must leave TAG_UNDEFINED untouched" - ); - - crate::object::js_implicit_this_set(prev_this); - clear_marks(); - clear_mark_seeds(); -} - #[test] fn test_class_side_table_scanner_marks_values_but_not_function_keys() { let _guard = GcTestIsolationGuard::new(); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/sort_collection.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/sort_collection.rs index cce9203fd5..1c439da528 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/sort_collection.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/sort_collection.rs @@ -1,7 +1,12 @@ use super::*; use crate::closure::ClosureHeader; -extern "C" fn captured_direction(closure: *const ClosureHeader, a: f64, b: f64) -> f64 { +extern "C" fn captured_direction( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, +) -> f64 { // Model generated capture loads: no forwarding lookup can repair a stale // closure argument. The getter changes only the relocated capture. let direction = unsafe { @@ -10,7 +15,10 @@ extern "C" fn captured_direction(closure: *const ClosureHeader, a: f64, b: f64) direction * (a - b) } -extern "C" fn collecting_getter(closure: *const ClosureHeader) -> f64 { +extern "C" fn collecting_getter( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope = RuntimeHandleScope::new(); let getter = scope.root_raw_const_ptr(closure); gc_collect_minor(); @@ -25,11 +33,19 @@ extern "C" fn collecting_getter(closure: *const ClosureHeader) -> f64 { 3.0 } -extern "C" fn accept_sorted_value(_closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn accept_sorted_value( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _value: f64, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn collecting_index_setter(_closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn collecting_index_setter( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _value: f64, +) -> f64 { gc_collect_minor(); f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/string_normalize_form.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/string_normalize_form.rs index 3b471a048d..e6cfd19ac1 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/string_normalize_form.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/string_normalize_form.rs @@ -125,7 +125,10 @@ thread_local! { /// The form object's `toString`: forces a real copying minor — the moving /// collection a user `toString`'s loop back-edge polls would run — then /// returns the form name. -extern "C" fn normalize_form_force_minor_gc(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn normalize_form_force_minor_gc( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { NORMALIZE_FORM_COERCIONS.with(|c| c.set(c.get() + 1)); let _ = crate::gc::gc_collect_minor(); test_string_value(b"NFC") diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/thenable_assimilation.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/thenable_assimilation.rs index 4865bab032..5ceb821087 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/thenable_assimilation.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/thenable_assimilation.rs @@ -21,6 +21,7 @@ use super::*; /// RELOCATED rather than freed. extern "C" fn test_thenable_then_force_minor_gc( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, resolve: f64, _reject: f64, ) -> f64 { @@ -31,6 +32,7 @@ extern "C" fn test_thenable_then_force_minor_gc( unsafe { crate::closure::js_native_call_value( resolve_handle.get_nanbox_f64(), + crate::closure::plain_call_receiver(), args.as_ptr(), args.len(), ); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs index 49313c7fea..3f860c9987 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs @@ -686,7 +686,10 @@ fn test_spill_store_past_length_ignores_tail_garbage_11550() { #[test] fn test_transient_runtime_handle_closure_captures_gc() { let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); - extern "C" fn captured_func(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn captured_func( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { 0.0 } diff --git a/crates/perry-runtime/src/gc/tests/step_bounds.rs b/crates/perry-runtime/src/gc/tests/step_bounds.rs index 11977024bf..e8faf54ed2 100644 --- a/crates/perry-runtime/src/gc/tests/step_bounds.rs +++ b/crates/perry-runtime/src/gc/tests/step_bounds.rs @@ -24,6 +24,7 @@ fn reset_old_reclaim_pressure() { extern "C" fn finreg_step_bounds_callback( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _held: f64, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) diff --git a/crates/perry-runtime/src/gc/tests/support.rs b/crates/perry-runtime/src/gc/tests/support.rs index 8cfd348382..ff2e3cce5f 100644 --- a/crates/perry-runtime/src/gc/tests/support.rs +++ b/crates/perry-runtime/src/gc/tests/support.rs @@ -747,12 +747,14 @@ pub(super) fn old_page_dirty_for(page: usize) -> bool { pub(super) extern "C" fn test_no_capture_singleton_func( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { 0.0 } pub(super) extern "C" fn test_captured_singleton_func( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { 0.0 } diff --git a/crates/perry-runtime/src/geisterhand_registry.rs b/crates/perry-runtime/src/geisterhand_registry.rs index 5086c26d41..db3e071d6e 100644 --- a/crates/perry-runtime/src/geisterhand_registry.rs +++ b/crates/perry-runtime/src/geisterhand_registry.rs @@ -109,8 +109,8 @@ static TREE_CONDVAR: Condvar = Condvar::new(); static TREE_REQUESTED: Mutex = Mutex::new(false); extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: crate::closure::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: crate::closure::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -446,10 +446,10 @@ pub extern "C" fn perry_geisterhand_pump() { unsafe { match args.len() { 0 => { - js_closure_call0(ptr); + js_closure_call0(ptr, crate::closure::plain_call_receiver()); } _ => { - js_closure_call1(ptr, args[0]); + js_closure_call1(ptr, crate::closure::plain_call_receiver(), args[0]); } } } @@ -487,7 +487,7 @@ pub extern "C" fn perry_geisterhand_pump() { let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; let ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(ptr, nanboxed); + js_closure_call1(ptr, crate::closure::plain_call_receiver(), nanboxed); } } } diff --git a/crates/perry-runtime/src/intl.rs b/crates/perry-runtime/src/intl.rs index 7e195ee4ec..74e3c70604 100644 --- a/crates/perry-runtime/src/intl.rs +++ b/crates/perry-runtime/src/intl.rs @@ -987,11 +987,31 @@ fn dt_component_option_from_handle( /// (`"MEZ"`, `"invalid"`, `"Europe/İstanbul"`, …) do not. Returns the (best /// effort, un-recased) canonical identifier, or `None` to signal `RangeError`. fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, options: f64) -> f64 { + make_instance_this( + closure, + crate::closure::JsThis::UNDEFINED, + kind, + locales, + options, + ) +} + +/// [`make_instance`] for a constructor native called with receiver `this`: +/// the receiver the legacy ChainNumberFormat / ChainDateTimeFormat path +/// (`ctor_guard::chain_legacy_constructed`) installs the instance on. +fn make_instance_this( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + kind: &str, + locales: f64, + options: f64, +) -> f64 { // Locale/option access can invoke user Proxy traps. Keep both arguments and // the partially initialized result live across those calls; the handles are // refreshed explicitly in the long PluralRules read-order sequence below. let scope = crate::gc::RuntimeHandleScope::new(); let closure_handle = scope.root_raw_const_ptr(closure); + let this_handle = scope.root_nanbox_f64(this.as_f64()); let locales_handle = scope.root_nanbox_f64(locales); let options_handle = scope.root_nanbox_f64(options); let locale = locale_or_default(locales_handle.get_nanbox_f64()); @@ -1631,92 +1651,21 @@ fn make_instance(closure: *const ClosureHeader, kind: &str, locales: f64, option // ChainNumberFormat / ChainDateTimeFormat only (see `chain_legacy_constructed`): // Intl.Collator ignores its this-value, so it is deliberately excluded. if matches!(kind, KIND_NUMBER | KIND_DATE_TIME) { - if let Some(this_value) = closure_handle - .with_const_ptr(|closure| ctor_guard::chain_legacy_constructed(closure, instance)) - { + if let Some(this_value) = closure_handle.with_const_ptr(|closure| { + ctor_guard::chain_legacy_constructed(closure, this_handle.get_nanbox_f64(), instance) + }) { return this_value; } } instance } -pub(super) extern "C" fn number_format_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - make_instance(closure, KIND_NUMBER, rest_arg(rest, 0), rest_arg(rest, 1)) -} - -pub(super) extern "C" fn date_time_format_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - make_instance( - closure, - KIND_DATE_TIME, - rest_arg(rest, 0), - rest_arg(rest, 1), - ) -} - -pub(super) extern "C" fn collator_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - make_instance(closure, KIND_COLLATOR, rest_arg(rest, 0), rest_arg(rest, 1)) -} - -pub(super) extern "C" fn segmenter_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - require_new_target("Segmenter"); - make_instance( - closure, - KIND_SEGMENTER, - rest_arg(rest, 0), - rest_arg(rest, 1), - ) -} - -pub(super) extern "C" fn list_format_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - require_new_target("ListFormat"); - make_instance( - closure, - KIND_LIST_FORMAT, - rest_arg(rest, 0), - rest_arg(rest, 1), - ) -} - -pub(super) extern "C" fn relative_time_format_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - require_new_target("RelativeTimeFormat"); - make_instance( - closure, - KIND_RELATIVE_TIME, - rest_arg(rest, 0), - rest_arg(rest, 1), - ) -} - -pub(super) extern "C" fn plural_rules_constructor_thunk( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - require_new_target("PluralRules"); - make_instance( - closure, - KIND_PLURAL_RULES, - rest_arg(rest, 0), - rest_arg(rest, 1), - ) -} +mod constructor_thunks; +pub(super) use constructor_thunks::{ + collator_constructor_thunk, date_time_format_constructor_thunk, list_format_constructor_thunk, + number_format_constructor_thunk, plural_rules_constructor_thunk, + relative_time_format_constructor_thunk, segmenter_constructor_thunk, +}; fn supported_locales_array(locales: f64, options: f64) -> f64 { // `supportedLocalesOf(locales, options)`: @@ -1751,7 +1700,11 @@ fn supported_locales_array(locales: f64, options: f64) -> f64 { js_nanbox_pointer(arr as i64) } -extern "C" fn supported_locales_of_thunk(_closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn supported_locales_of_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { supported_locales_array(rest_arg(rest, 0), rest_arg(rest, 1)) } diff --git a/crates/perry-runtime/src/intl/constructor_thunks.rs b/crates/perry-runtime/src/intl/constructor_thunks.rs new file mode 100644 index 0000000000..70b3406679 --- /dev/null +++ b/crates/perry-runtime/src/intl/constructor_thunks.rs @@ -0,0 +1,96 @@ +//! The `Intl.*` constructor natives (split out of `intl.rs` for the +//! file-size gate; unchanged). + +use super::*; + +pub(crate) extern "C" fn number_format_constructor_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + make_instance_this( + closure, + this, + KIND_NUMBER, + rest_arg(rest, 0), + rest_arg(rest, 1), + ) +} + +pub(crate) extern "C" fn date_time_format_constructor_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { + make_instance_this( + closure, + this, + KIND_DATE_TIME, + rest_arg(rest, 0), + rest_arg(rest, 1), + ) +} + +pub(crate) extern "C" fn collator_constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { + make_instance(closure, KIND_COLLATOR, rest_arg(rest, 0), rest_arg(rest, 1)) +} + +pub(crate) extern "C" fn segmenter_constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { + require_new_target("Segmenter"); + make_instance( + closure, + KIND_SEGMENTER, + rest_arg(rest, 0), + rest_arg(rest, 1), + ) +} + +pub(crate) extern "C" fn list_format_constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { + require_new_target("ListFormat"); + make_instance( + closure, + KIND_LIST_FORMAT, + rest_arg(rest, 0), + rest_arg(rest, 1), + ) +} + +pub(crate) extern "C" fn relative_time_format_constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { + require_new_target("RelativeTimeFormat"); + make_instance( + closure, + KIND_RELATIVE_TIME, + rest_arg(rest, 0), + rest_arg(rest, 1), + ) +} + +pub(crate) extern "C" fn plural_rules_constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { + require_new_target("PluralRules"); + make_instance( + closure, + KIND_PLURAL_RULES, + rest_arg(rest, 0), + rest_arg(rest, 1), + ) +} diff --git a/crates/perry-runtime/src/intl/ctor_guard.rs b/crates/perry-runtime/src/intl/ctor_guard.rs index 912149cbfb..9996c6d755 100644 --- a/crates/perry-runtime/src/intl/ctor_guard.rs +++ b/crates/perry-runtime/src/intl/ctor_guard.rs @@ -113,6 +113,7 @@ fn receiver_on_constructor_chain(receiver: f64, closure: *const ClosureHeader) - /// this for the Collator kind. pub(super) fn chain_legacy_constructed( closure: *const ClosureHeader, + this_value: f64, instance: f64, ) -> Option { // Only the legacy path: a `new`-invocation (new.target present) always @@ -120,7 +121,6 @@ pub(super) fn chain_legacy_constructed( if crate::object::js_new_target_get().to_bits() != crate::value::TAG_UNDEFINED { return None; } - let this_value = crate::object::js_implicit_this_get(); let this_bits = this_value.to_bits(); // `this` must be an Object (pointer-tagged, valid heap object). if (this_bits & !POINTER_MASK) != POINTER_TAG { diff --git a/crates/perry-runtime/src/intl/date_collator.rs b/crates/perry-runtime/src/intl/date_collator.rs index 396c1fc7ad..ce1bebe390 100644 --- a/crates/perry-runtime/src/intl/date_collator.rs +++ b/crates/perry-runtime/src/intl/date_collator.rs @@ -61,6 +61,7 @@ fn date_arg_to_clipped_ms(value: f64) -> f64 { pub(crate) extern "C" fn date_time_format_bound_format_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "format", KIND_DATE_TIME); @@ -78,16 +79,18 @@ pub(crate) extern "C" fn date_time_format_bound_format_thunk( /// construction with name `""` and length 1). pub(crate) extern "C" fn date_time_format_format_getter_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let obj = this_intl_object("format", KIND_DATE_TIME); + let obj = this_intl_object(this, "format", KIND_DATE_TIME); get_field(obj, KEY_DTF_BOUND_FORMAT) } pub(crate) extern "C" fn date_time_format_to_parts_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let obj = this_intl_object("formatToParts", KIND_DATE_TIME); + let obj = this_intl_object(this, "formatToParts", KIND_DATE_TIME); if let Some(kind) = crate::temporal::temporal_kind(value) { validate_temporal_dtf_overlap(kind, obj); } @@ -96,6 +99,7 @@ pub(crate) extern "C" fn date_time_format_to_parts_thunk( pub(crate) extern "C" fn date_time_format_bound_to_parts_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "formatToParts", KIND_DATE_TIME); @@ -1381,13 +1385,15 @@ pub(crate) use range::*; pub(crate) extern "C" fn date_time_format_resolved_options_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_DATE_TIME); + let obj = this_intl_object(this, "resolvedOptions", KIND_DATE_TIME); date_time_format_resolved_options_object(obj) } pub(crate) extern "C" fn date_time_format_bound_resolved_options_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_DATE_TIME); date_time_format_resolved_options_object(obj) @@ -1687,6 +1693,7 @@ pub(super) fn validate_collator_options(options: f64) { pub(crate) extern "C" fn collator_bound_compare_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, left: f64, right: f64, ) -> f64 { @@ -1697,8 +1704,11 @@ pub(crate) extern "C" fn collator_bound_compare_thunk( /// `get Intl.Collator.prototype.compare` — validate the receiver and return its /// stable [[BoundCompare]] function. The constructor gives that function the /// anonymous built-in shape required by ECMA-402 (`name: ""`, `length: 2`). -pub(crate) extern "C" fn collator_compare_getter_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("compare", KIND_COLLATOR); +pub(crate) extern "C" fn collator_compare_getter_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "compare", KIND_COLLATOR); get_field(obj, KEY_COL_BOUND_COMPARE) } @@ -1765,13 +1775,17 @@ pub(crate) fn collator_compare_object(obj: *const ObjectHeader, left: f64, right } } -pub(crate) extern "C" fn collator_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_COLLATOR); +pub(crate) extern "C" fn collator_resolved_options_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "resolvedOptions", KIND_COLLATOR); collator_resolved_options_object(obj) } pub(crate) extern "C" fn collator_bound_resolved_options_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_COLLATOR); collator_resolved_options_object(obj) diff --git a/crates/perry-runtime/src/intl/date_collator/range.rs b/crates/perry-runtime/src/intl/date_collator/range.rs index 3f6b6f42a5..f158344262 100644 --- a/crates/perry-runtime/src/intl/date_collator/range.rs +++ b/crates/perry-runtime/src/intl/date_collator/range.rs @@ -182,10 +182,11 @@ pub(crate) fn date_time_format_range_parts_value( pub(crate) extern "C" fn date_time_format_range_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - let obj = this_intl_object("formatRange", KIND_DATE_TIME); + let obj = this_intl_object(this, "formatRange", KIND_DATE_TIME); if let Some(kind) = crate::temporal::temporal_kind(start) { validate_temporal_dtf_overlap(kind, obj); } @@ -194,6 +195,7 @@ pub(crate) extern "C" fn date_time_format_range_thunk( pub(crate) extern "C" fn date_time_format_bound_range_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { @@ -206,10 +208,11 @@ pub(crate) extern "C" fn date_time_format_bound_range_thunk( pub(crate) extern "C" fn date_time_format_range_to_parts_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - let obj = this_intl_object("formatRangeToParts", KIND_DATE_TIME); + let obj = this_intl_object(this, "formatRangeToParts", KIND_DATE_TIME); if let Some(kind) = crate::temporal::temporal_kind(start) { validate_temporal_dtf_overlap(kind, obj); } @@ -218,6 +221,7 @@ pub(crate) extern "C" fn date_time_format_range_to_parts_thunk( pub(crate) extern "C" fn date_time_format_bound_range_to_parts_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/intl/display_names.rs b/crates/perry-runtime/src/intl/display_names.rs index e3aa6f1b01..11a11620d9 100644 --- a/crates/perry-runtime/src/intl/display_names.rs +++ b/crates/perry-runtime/src/intl/display_names.rs @@ -188,13 +188,19 @@ fn resolved_options_object(obj: *const ObjectHeader) -> f64 { js_nanbox_pointer(out as i64) } -extern "C" fn bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn bound_resolved_options_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", super::KIND_DISPLAY_NAMES); resolved_options_object(obj) } -pub(super) extern "C" fn resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", super::KIND_DISPLAY_NAMES); +pub(super) extern "C" fn resolved_options_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "resolvedOptions", super::KIND_DISPLAY_NAMES); resolved_options_object(obj) } @@ -339,17 +345,29 @@ fn of_value(obj: *const ObjectHeader, code_value: f64) -> f64 { } } -extern "C" fn bound_of_thunk(closure: *const ClosureHeader, code: f64) -> f64 { +extern "C" fn bound_of_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + code: f64, +) -> f64 { let obj = captured_intl_object(closure, "of", super::KIND_DISPLAY_NAMES); of_value(obj, code) } -pub(super) extern "C" fn of_thunk(_closure: *const ClosureHeader, code: f64) -> f64 { - let obj = this_intl_object("of", super::KIND_DISPLAY_NAMES); +pub(super) extern "C" fn of_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + code: f64, +) -> f64 { + let obj = this_intl_object(this, "of", super::KIND_DISPLAY_NAMES); of_value(obj, code) } -pub(super) extern "C" fn constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { super::make_instance( closure, super::KIND_DISPLAY_NAMES, diff --git a/crates/perry-runtime/src/intl/duration_format.rs b/crates/perry-runtime/src/intl/duration_format.rs index c55e3eefa8..d71791d8fa 100644 --- a/crates/perry-runtime/src/intl/duration_format.rs +++ b/crates/perry-runtime/src/intl/duration_format.rs @@ -367,8 +367,11 @@ fn resolved_options_object(obj: *const ObjectHeader) -> f64 { js_nanbox_pointer(out as i64) } -pub(super) extern "C" fn resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", super::KIND_DURATION_FORMAT); +pub(super) extern "C" fn resolved_options_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "resolvedOptions", super::KIND_DURATION_FORMAT); resolved_options_object(obj) } @@ -881,18 +884,30 @@ pub(super) fn format_temporal_duration(duration: f64, locales: f64, options: f64 format_value(obj, duration) } -pub(super) extern "C" fn format_thunk(_closure: *const ClosureHeader, duration: f64) -> f64 { - let obj = this_intl_object("format", super::KIND_DURATION_FORMAT); +pub(super) extern "C" fn format_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + duration: f64, +) -> f64 { + let obj = this_intl_object(this, "format", super::KIND_DURATION_FORMAT); format_value(obj, duration) } -pub(super) extern "C" fn to_parts_thunk(_closure: *const ClosureHeader, duration: f64) -> f64 { - let obj = this_intl_object("formatToParts", super::KIND_DURATION_FORMAT); +pub(super) extern "C" fn to_parts_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + duration: f64, +) -> f64 { + let obj = this_intl_object(this, "formatToParts", super::KIND_DURATION_FORMAT); let vals = to_duration_record(duration); df_parts_to_js_array(&partition(obj, &vals)) } -pub(super) extern "C" fn constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { // `Intl.DurationFormat` is `[[Construct]]`-only: a bare call is a TypeError. if crate::object::js_new_target_get().to_bits() == crate::value::TAG_UNDEFINED { throw_type_error("Constructor Intl.DurationFormat requires 'new'"); diff --git a/crates/perry-runtime/src/intl/list_relative_plural.rs b/crates/perry-runtime/src/intl/list_relative_plural.rs index bb5dc6ed95..9e60b5e81b 100644 --- a/crates/perry-runtime/src/intl/list_relative_plural.rs +++ b/crates/perry-runtime/src/intl/list_relative_plural.rs @@ -204,9 +204,10 @@ pub(crate) fn list_format_instance_parts( pub(crate) extern "C" fn list_format_format_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let obj = this_intl_object("format", KIND_LIST_FORMAT); + let obj = this_intl_object(this, "format", KIND_LIST_FORMAT); string_value( &list_format_instance_parts(obj, value) .iter() @@ -217,6 +218,7 @@ pub(crate) extern "C" fn list_format_format_thunk( pub(crate) extern "C" fn list_format_bound_format_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "format", KIND_LIST_FORMAT); @@ -230,14 +232,16 @@ pub(crate) extern "C" fn list_format_bound_format_thunk( pub(crate) extern "C" fn list_format_to_parts_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let obj = this_intl_object("formatToParts", KIND_LIST_FORMAT); + let obj = this_intl_object(this, "formatToParts", KIND_LIST_FORMAT); parts_to_js_array(&list_format_instance_parts(obj, value)) } pub(crate) extern "C" fn list_format_bound_to_parts_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "formatToParts", KIND_LIST_FORMAT); @@ -264,13 +268,17 @@ pub(crate) fn list_format_resolved_options_object(obj: *const ObjectHeader) -> f js_nanbox_pointer(out as i64) } -pub(crate) extern "C" fn list_format_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_LIST_FORMAT); +pub(crate) extern "C" fn list_format_resolved_options_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "resolvedOptions", KIND_LIST_FORMAT); list_format_resolved_options_object(obj) } pub(crate) extern "C" fn list_format_bound_resolved_options_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_LIST_FORMAT); list_format_resolved_options_object(obj) @@ -585,10 +593,11 @@ fn rtf_parts_to_js_array(parts: &[(&'static str, String)], unit: &str) -> f64 { pub(crate) extern "C" fn rtf_format_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, unit: f64, ) -> f64 { - let obj = this_intl_object("format", KIND_RELATIVE_TIME); + let obj = this_intl_object(this, "format", KIND_RELATIVE_TIME); string_value( &rtf_instance_parts(obj, value, unit) .iter() @@ -599,6 +608,7 @@ pub(crate) extern "C" fn rtf_format_thunk( pub(crate) extern "C" fn rtf_bound_format_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, unit: f64, ) -> f64 { @@ -613,16 +623,18 @@ pub(crate) extern "C" fn rtf_bound_format_thunk( pub(crate) extern "C" fn rtf_to_parts_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, unit: f64, ) -> f64 { - let obj = this_intl_object("formatToParts", KIND_RELATIVE_TIME); + let obj = this_intl_object(this, "formatToParts", KIND_RELATIVE_TIME); let (parts, unit) = rtf_instance_parts_and_unit(obj, value, unit); rtf_parts_to_js_array(&parts, unit) } pub(crate) extern "C" fn rtf_bound_to_parts_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, unit: f64, ) -> f64 { @@ -658,12 +670,18 @@ pub(crate) fn rtf_resolved_options_object(obj: *const ObjectHeader) -> f64 { js_nanbox_pointer(out as i64) } -pub(crate) extern "C" fn rtf_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_RELATIVE_TIME); +pub(crate) extern "C" fn rtf_resolved_options_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "resolvedOptions", KIND_RELATIVE_TIME); rtf_resolved_options_object(obj) } -pub(crate) extern "C" fn rtf_bound_resolved_options_thunk(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn rtf_bound_resolved_options_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_RELATIVE_TIME); rtf_resolved_options_object(obj) } @@ -987,14 +1005,16 @@ mod plural_category_tests { pub(crate) extern "C" fn plural_rules_select_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let obj = this_intl_object("select", KIND_PLURAL_RULES); + let obj = this_intl_object(this, "select", KIND_PLURAL_RULES); plural_rules_select(obj, value) } pub(crate) extern "C" fn plural_rules_bound_select_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "select", KIND_PLURAL_RULES); @@ -1003,10 +1023,11 @@ pub(crate) extern "C" fn plural_rules_bound_select_thunk( pub(crate) extern "C" fn plural_rules_select_range_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - let _obj = this_intl_object("selectRange", KIND_PLURAL_RULES); + let _obj = this_intl_object(this, "selectRange", KIND_PLURAL_RULES); plural_select_range(start, end) } @@ -1103,13 +1124,15 @@ pub(crate) fn plural_rules_resolved_options_object(obj: *const ObjectHeader) -> pub(crate) extern "C" fn plural_rules_resolved_options_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_PLURAL_RULES); + let obj = this_intl_object(this, "resolvedOptions", KIND_PLURAL_RULES); plural_rules_resolved_options_object(obj) } pub(crate) extern "C" fn plural_rules_bound_resolved_options_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_PLURAL_RULES); plural_rules_resolved_options_object(obj) diff --git a/crates/perry-runtime/src/intl/locale.rs b/crates/perry-runtime/src/intl/locale.rs index 837d898a7a..0d8f477175 100644 --- a/crates/perry-runtime/src/intl/locale.rs +++ b/crates/perry-runtime/src/intl/locale.rs @@ -606,44 +606,74 @@ fn make_locale_instance(proto_bits: u64, p: &ParsedLocale) -> f64 { js_nanbox_pointer(obj as i64) } -extern "C" fn locale_bound_to_string(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_to_string( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let obj = captured_intl_object(closure, "toString", KIND_LOCALE); string_value(&get_string_field(obj, KEY_FULL).unwrap_or_default()) } -extern "C" fn locale_bound_maximize(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_maximize( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let obj = captured_intl_object(closure, "maximize", KIND_LOCALE); transform_instance(obj, likely_subtags::maximize) } -extern "C" fn locale_bound_minimize(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_minimize( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let obj = captured_intl_object(closure, "minimize", KIND_LOCALE); transform_instance(obj, likely_subtags::minimize) } -extern "C" fn locale_bound_get_calendars(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_calendars( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { calendars_of(captured_intl_object(closure, "getCalendars", KIND_LOCALE)) } -extern "C" fn locale_bound_get_collations(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_collations( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { collations_of(captured_intl_object(closure, "getCollations", KIND_LOCALE)) } -extern "C" fn locale_bound_get_hour_cycles(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_hour_cycles( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { hour_cycles_of(captured_intl_object(closure, "getHourCycles", KIND_LOCALE)) } -extern "C" fn locale_bound_get_numbering_systems(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_numbering_systems( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { numbering_systems_of(captured_intl_object( closure, "getNumberingSystems", KIND_LOCALE, )) } -extern "C" fn locale_bound_get_time_zones(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_time_zones( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { time_zones_of(captured_intl_object(closure, "getTimeZones", KIND_LOCALE)) } -extern "C" fn locale_bound_get_text_info(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_text_info( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { text_info_of(captured_intl_object(closure, "getTextInfo", KIND_LOCALE)) } -extern "C" fn locale_bound_get_week_info(closure: *const ClosureHeader) -> f64 { +extern "C" fn locale_bound_get_week_info( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { week_info_of(captured_intl_object(closure, "getWeekInfo", KIND_LOCALE)) } @@ -656,7 +686,11 @@ fn transform_instance(obj: *const ObjectHeader, transform: fn(&mut ParsedLocale) make_locale_instance(proto, &p) } -pub(super) extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +pub(super) extern "C" fn locale_constructor_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { super::require_new_target("Locale"); let tag_value = super::rest_arg(rest, 0); let options_value = super::rest_arg(rest, 1); @@ -712,8 +746,8 @@ pub(super) extern "C" fn locale_constructor_thunk(closure: *const ClosureHeader, // ---- prototype methods & getters ------------------------------------------ -fn locale_this(method: &str) -> *mut ObjectHeader { - let this = crate::object::js_implicit_this_get(); +fn locale_this(this: crate::closure::JsThis, method: &str) -> *mut ObjectHeader { + let this = this.as_f64(); let Some(obj) = object_ptr_from_value(this) else { throw_type_error(&format!( "Intl.Locale.prototype.{method} called on incompatible receiver" @@ -736,39 +770,69 @@ fn field_or_undefined(obj: *const ObjectHeader, key: &str) -> f64 { } } -extern "C" fn locale_to_string_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = locale_this("toString"); +extern "C" fn locale_to_string_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = locale_this(this, "toString"); string_value(&get_string_field(obj, KEY_FULL).unwrap_or_default()) } -extern "C" fn locale_maximize_thunk(_closure: *const ClosureHeader) -> f64 { - transform_instance(locale_this("maximize"), likely_subtags::maximize) +extern "C" fn locale_maximize_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + transform_instance(locale_this(this, "maximize"), likely_subtags::maximize) } -extern "C" fn locale_minimize_thunk(_closure: *const ClosureHeader) -> f64 { - transform_instance(locale_this("minimize"), likely_subtags::minimize) +extern "C" fn locale_minimize_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + transform_instance(locale_this(this, "minimize"), likely_subtags::minimize) } -extern "C" fn locale_get_calendars_thunk(_closure: *const ClosureHeader) -> f64 { - calendars_of(locale_this("getCalendars")) +extern "C" fn locale_get_calendars_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + calendars_of(locale_this(this, "getCalendars")) } -extern "C" fn locale_get_collations_thunk(_closure: *const ClosureHeader) -> f64 { - collations_of(locale_this("getCollations")) +extern "C" fn locale_get_collations_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + collations_of(locale_this(this, "getCollations")) } -extern "C" fn locale_get_hour_cycles_thunk(_closure: *const ClosureHeader) -> f64 { - hour_cycles_of(locale_this("getHourCycles")) +extern "C" fn locale_get_hour_cycles_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + hour_cycles_of(locale_this(this, "getHourCycles")) } -extern "C" fn locale_get_numbering_systems_thunk(_closure: *const ClosureHeader) -> f64 { - numbering_systems_of(locale_this("getNumberingSystems")) +extern "C" fn locale_get_numbering_systems_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + numbering_systems_of(locale_this(this, "getNumberingSystems")) } -extern "C" fn locale_get_time_zones_thunk(_closure: *const ClosureHeader) -> f64 { - time_zones_of(locale_this("getTimeZones")) +extern "C" fn locale_get_time_zones_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + time_zones_of(locale_this(this, "getTimeZones")) } -extern "C" fn locale_get_text_info_thunk(_closure: *const ClosureHeader) -> f64 { - text_info_of(locale_this("getTextInfo")) +extern "C" fn locale_get_text_info_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + text_info_of(locale_this(this, "getTextInfo")) } -extern "C" fn locale_get_week_info_thunk(_closure: *const ClosureHeader) -> f64 { - week_info_of(locale_this("getWeekInfo")) +extern "C" fn locale_get_week_info_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + week_info_of(locale_this(this, "getWeekInfo")) } // ---- Intl.Locale-info computations ----------------------------------------- @@ -896,38 +960,44 @@ fn parsed_from_instance(obj: *const ObjectHeader) -> ParsedLocale { parse_language_tag(&full).unwrap_or_default() } -extern "C" fn getter_base_name(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("baseName"), KEY_BASENAME) +extern "C" fn getter_base_name(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "baseName"), KEY_BASENAME) } -extern "C" fn getter_language(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("language"), KEY_LANGUAGE) +extern "C" fn getter_language(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "language"), KEY_LANGUAGE) } -extern "C" fn getter_script(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("script"), KEY_SCRIPT) +extern "C" fn getter_script(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "script"), KEY_SCRIPT) } -extern "C" fn getter_region(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("region"), KEY_REGION) +extern "C" fn getter_region(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "region"), KEY_REGION) } -extern "C" fn getter_calendar(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("calendar"), KEY_CALENDAR) +extern "C" fn getter_calendar(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "calendar"), KEY_CALENDAR) } -extern "C" fn getter_case_first(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("caseFirst"), KEY_CASEFIRST) +extern "C" fn getter_case_first(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "caseFirst"), KEY_CASEFIRST) } -extern "C" fn getter_collation(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("collation"), KEY_COLLATION) +extern "C" fn getter_collation(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "collation"), KEY_COLLATION) } -extern "C" fn getter_hour_cycle(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("hourCycle"), KEY_HOURCYCLE) +extern "C" fn getter_hour_cycle(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + field_or_undefined(locale_this(this, "hourCycle"), KEY_HOURCYCLE) } -extern "C" fn getter_numbering_system(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("numberingSystem"), KEY_NUMBERINGSYSTEM) +extern "C" fn getter_numbering_system( + _c: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_or_undefined(locale_this(this, "numberingSystem"), KEY_NUMBERINGSYSTEM) } -extern "C" fn getter_first_day_of_week(_c: *const ClosureHeader) -> f64 { - field_or_undefined(locale_this("firstDayOfWeek"), KEY_FIRSTDAYOFWEEK) +extern "C" fn getter_first_day_of_week( + _c: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + field_or_undefined(locale_this(this, "firstDayOfWeek"), KEY_FIRSTDAYOFWEEK) } -extern "C" fn getter_numeric(_c: *const ClosureHeader) -> f64 { - let obj = locale_this("numeric"); +extern "C" fn getter_numeric(_c: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + let obj = locale_this(this, "numeric"); let value = get_field(obj, KEY_NUMERIC); if value.to_bits() == crate::value::TAG_TRUE { bool_value(true) diff --git a/crates/perry-runtime/src/intl/locales.rs b/crates/perry-runtime/src/intl/locales.rs index 22dbe4cb58..7922bdea5e 100644 --- a/crates/perry-runtime/src/intl/locales.rs +++ b/crates/perry-runtime/src/intl/locales.rs @@ -27,6 +27,7 @@ pub(super) fn get_canonical_locales(locales: f64) -> f64 { pub(super) extern "C" fn get_canonical_locales_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, locales: f64, ) -> f64 { get_canonical_locales(locales) @@ -151,6 +152,7 @@ fn supported_values_list(key: &str) -> Option<&'static [&'static str]> { pub(super) extern "C" fn supported_values_of_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, key: f64, ) -> f64 { // Coerce `key` to String first (the spec's GetOption-like step), then a diff --git a/crates/perry-runtime/src/intl/number_format.rs b/crates/perry-runtime/src/intl/number_format.rs index b20e3bfa5e..5a8bc2acdd 100644 --- a/crates/perry-runtime/src/intl/number_format.rs +++ b/crates/perry-runtime/src/intl/number_format.rs @@ -1309,8 +1309,12 @@ pub(crate) fn parts_to_js_array(parts: &[(&'static str, String)]) -> f64 { js_nanbox_pointer(arr as i64) } -pub(crate) fn this_intl_object(method: &str, expected_kind: &str) -> *mut ObjectHeader { - let this_value = crate::object::js_implicit_this_get(); +pub(crate) fn this_intl_object( + this: crate::closure::JsThis, + method: &str, + expected_kind: &str, +) -> *mut ObjectHeader { + let this_value = this.as_f64(); intl_object_from_value(this_value, method, expected_kind) } @@ -1385,13 +1389,17 @@ fn unwrap_legacy_constructed(value: f64) -> Option { /// KEY_NF_BOUND_FORMAT slot (set at construction, name `""`, length 1) rather /// than the public own `format` property, so user mutation/deletion of that /// property can't change what the accessor returns. -pub(crate) extern "C" fn number_format_format_getter_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("format", KIND_NUMBER); +pub(crate) extern "C" fn number_format_format_getter_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "format", KIND_NUMBER); get_field(obj, KEY_NF_BOUND_FORMAT) } pub(crate) extern "C" fn number_format_bound_format_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "format", KIND_NUMBER); @@ -1416,13 +1424,15 @@ pub(crate) fn number_format_format_object(obj: *const ObjectHeader, value: f64) pub(crate) extern "C" fn number_format_resolved_options_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_NUMBER); + let obj = this_intl_object(this, "resolvedOptions", KIND_NUMBER); number_format_resolved_options_object(obj) } pub(crate) extern "C" fn number_format_bound_resolved_options_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_NUMBER); number_format_resolved_options_object(obj) @@ -1430,15 +1440,17 @@ pub(crate) extern "C" fn number_format_bound_resolved_options_thunk( pub(crate) extern "C" fn number_format_to_parts_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let obj = this_intl_object("formatToParts", KIND_NUMBER); + let obj = this_intl_object(this, "formatToParts", KIND_NUMBER); let number = nf_coerce_number(value); parts_to_js_array(&number_instance_parts(obj, number)) } pub(crate) extern "C" fn number_format_bound_to_parts_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "formatToParts", KIND_NUMBER); @@ -1534,19 +1546,21 @@ pub(crate) fn number_format_range_parts_value( pub(crate) extern "C" fn number_format_range_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - let obj = this_intl_object("formatRange", KIND_NUMBER); + let obj = this_intl_object(this, "formatRange", KIND_NUMBER); number_format_range_value(obj, "formatRange", start, end) } pub(crate) extern "C" fn number_format_range_to_parts_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - let obj = this_intl_object("formatRangeToParts", KIND_NUMBER); + let obj = this_intl_object(this, "formatRangeToParts", KIND_NUMBER); number_format_range_parts_value(obj, "formatRangeToParts", start, end) } diff --git a/crates/perry-runtime/src/intl/segmenter.rs b/crates/perry-runtime/src/intl/segmenter.rs index 0986d6f0f1..4ba34920fb 100644 --- a/crates/perry-runtime/src/intl/segmenter.rs +++ b/crates/perry-runtime/src/intl/segmenter.rs @@ -420,16 +420,19 @@ fn install_segments_iterator(segments: &crate::gc::RuntimeHandle<'_>) { }); } -extern "C" fn segments_iterator_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn segments_iterator_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let segments = scope.root_raw_const_ptr(segments_from_this()); + let segments = scope.root_raw_const_ptr(segments_from_this(this)); segments.with_const_ptr(|segments: *const crate::ArrayHeader| { crate::array::array_values_iter(js_nanbox_pointer(segments as i64)) }) } -fn segments_from_this() -> *const crate::ArrayHeader { - let this_value = crate::object::js_implicit_this_get(); +fn segments_from_this(this: crate::closure::JsThis) -> *const crate::ArrayHeader { + let this_value = this.as_f64(); let Some(segments) = array_ptr_from_value(this_value) else { throw_type_error("Intl.Segments.prototype.containing called on incompatible receiver"); }; @@ -442,9 +445,10 @@ fn segments_from_this() -> *const crate::ArrayHeader { pub(crate) extern "C" fn segmenter_containing_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, index: f64, ) -> f64 { - let segments = segments_from_this(); + let segments = segments_from_this(this); let input_len = get_number_field(segments as *const ObjectHeader, KEY_SEGMENTS_LENGTH).unwrap_or(0.0); @@ -492,14 +496,16 @@ pub(crate) extern "C" fn segmenter_containing_thunk( pub(crate) extern "C" fn segmenter_segment_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let obj = this_intl_object("segment", KIND_SEGMENTER); + let obj = this_intl_object(this, "segment", KIND_SEGMENTER); segmenter_segment_object(obj, value) } pub(crate) extern "C" fn segmenter_bound_segment_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let obj = captured_intl_object(closure, "segment", KIND_SEGMENTER); @@ -512,13 +518,17 @@ pub(crate) fn segmenter_segment_object(obj: *const ObjectHeader, value: f64) -> build_segments(&granularity, value) } -pub(crate) extern "C" fn segmenter_resolved_options_thunk(_closure: *const ClosureHeader) -> f64 { - let obj = this_intl_object("resolvedOptions", KIND_SEGMENTER); +pub(crate) extern "C" fn segmenter_resolved_options_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = this_intl_object(this, "resolvedOptions", KIND_SEGMENTER); segmenter_resolved_options_object(obj) } pub(crate) extern "C" fn segmenter_bound_resolved_options_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let obj = captured_intl_object(closure, "resolvedOptions", KIND_SEGMENTER); segmenter_resolved_options_object(obj) diff --git a/crates/perry-runtime/src/intl/segments_view.rs b/crates/perry-runtime/src/intl/segments_view.rs index ab579eb174..34d89cfed6 100644 --- a/crates/perry-runtime/src/intl/segments_view.rs +++ b/crates/perry-runtime/src/intl/segments_view.rs @@ -956,7 +956,10 @@ mod view_mode_tests { ); } - extern "C" fn patched_test_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn patched_test_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { f64::from_bits(JSValue::bool(true).bits()) } diff --git a/crates/perry-runtime/src/intl/subclass.rs b/crates/perry-runtime/src/intl/subclass.rs index 94fee140bf..f077ff7bac 100644 --- a/crates/perry-runtime/src/intl/subclass.rs +++ b/crates/perry-runtime/src/intl/subclass.rs @@ -147,16 +147,19 @@ pub(crate) unsafe fn intl_subclass_super( if !is_intl_constructor_value(parent_val) { return false; } - // #9445: the parent constructor is user-reachable code; root the displaced - // receiver AND `this_box`, which is consumed again after the call. + // #9445: the parent constructor is user-reachable code; root `this_box`, + // which is passed as the receiver and consumed again after the call. let this_scope = crate::gc::RuntimeHandleScope::new(); let this_h = this_scope.root_nanbox_f64(this_box); - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); // #10490: the displaced `new.target` crosses the same call. let prev_nt = this_scope.root_nanbox_f64(crate::object::js_new_target_set(parent_val)); - let instance = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); + let instance = crate::closure::native_call_value_this( + parent_val, + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), + args_ptr, + args_len, + ); crate::object::js_new_target_set(prev_nt.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // Re-home the freshly-built instance's brand + bound methods onto `this`. let this_bits = this_h.get_nanbox_f64().to_bits(); if (this_bits >> 48) == 0x7FFD { diff --git a/crates/perry-runtime/src/iterator_helpers.rs b/crates/perry-runtime/src/iterator_helpers.rs index 4d4043494f..81fe1e8762 100644 --- a/crates/perry-runtime/src/iterator_helpers.rs +++ b/crates/perry-runtime/src/iterator_helpers.rs @@ -99,12 +99,11 @@ pub fn is_iterator_helper_addr(addr: usize) -> bool { /// it with the INHERITING getter is what killed the entire helper surface. /// Every built-in iterator now inherits `.next` from its shared /// `%…IteratorPrototype%` singleton (`object/iterator_prototypes.rs`), and that -/// inherited `next` is a THUNK that resolves its receiver from -/// `js_implicit_this_get()` and dispatches by class id. So -/// `js_object_get_field_by_name` found a perfectly callable closure for an -/// array / Map / Set / String iterator, this function took the closure-call -/// branch, and the thunk ran with whatever `this` happened to be left in the -/// thread-local — reporting `done` on the first step for every source kind, or +/// inherited `next` is a THUNK that resolves its receiver from the call's +/// `this` and dispatches by class id. So `js_object_get_field_by_name` found a +/// perfectly callable closure for an array / Map / Set / String iterator, this +/// function took the closure-call branch, and the thunk ran with whatever +/// receiver that call supplied rather than the iterator — reporting `done` on the first step for every source kind, or /// throwing `Method %IteratorPrototype%.next called on incompatible receiver`. /// `js_iterator_to_array` already carries the own-field version of this fix /// (#321); this is the same fix for the helper chain. @@ -148,16 +147,17 @@ unsafe fn iterator_step(iter_f64: f64) -> (f64, bool) { }; let use_field = !next_ptr.is_null() && is_closure_ptr(next_ptr as usize); - // The user's `next` runs here and can collect anything, so the receiver and - // the saved implicit-`this` both come out of handles rather than out of - // bare locals that predate the call. + // The user's `next` runs here and can collect anything, so the receiver + // comes out of a handle rather than out of a bare local that predates the + // call. let result_f64 = if use_field { let next_h = scope.root_nanbox_f64(f64::from_bits(next_val.bits())); - let prev_this_h = - scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); - let r = crate::closure::js_native_call_value(next_h.get_nanbox_f64(), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this_h.get_nanbox_f64()); - r + crate::closure::native_call_value_this( + next_h.get_nanbox_f64(), + crate::closure::JsThis::from_f64(iter_h.get_nanbox_f64()), + std::ptr::null(), + 0, + ) } else { crate::object::js_native_call_method( iter_h.get_nanbox_f64(), @@ -271,7 +271,7 @@ unsafe fn helper_next(obj: *mut ObjectHeader) -> f64 { let mapped = if cb.is_null() { v } else { - js_closure_call1(cb, v) + js_closure_call1(cb, crate::closure::plain_call_receiver(), v) }; make_iter_result(JSValue::from_bits(mapped.to_bits()), false) } @@ -285,7 +285,11 @@ unsafe fn helper_next(obj: *mut ObjectHeader) -> f64 { let keep = if cb.is_null() { true } else { - crate::value::js_is_truthy(js_closure_call1(cb, v)) != 0 + crate::value::js_is_truthy(js_closure_call1( + cb, + crate::closure::plain_call_receiver(), + v, + )) != 0 }; if keep { return make_iter_result(JSValue::from_bits(v.to_bits()), false); @@ -340,7 +344,7 @@ unsafe fn helper_next(obj: *mut ObjectHeader) -> f64 { let produced = if cb.is_null() { v } else { - js_closure_call1(cb, v) + js_closure_call1(cb, crate::closure::plain_call_receiver(), v) }; // Per spec each produced value must itself be iterable; wrap it. let inner_iter = get_iterator(produced); @@ -528,7 +532,12 @@ pub unsafe fn dispatch_iterator_helper_method( break; } if !cb.is_null() { - js_closure_call2(cb, v, f64::from_bits(JSValue::number(i).bits())); + js_closure_call2( + cb, + crate::closure::plain_call_receiver(), + v, + f64::from_bits(JSValue::number(i).bits()), + ); } i += 1.0; } @@ -550,7 +559,7 @@ pub unsafe fn dispatch_iterator_helper_method( continue; } if !cb.is_null() { - acc = js_closure_call2(cb, acc, v); + acc = js_closure_call2(cb, crate::closure::plain_call_receiver(), acc, v); } } if !started { @@ -567,7 +576,13 @@ pub unsafe fn dispatch_iterator_helper_method( if done { return f64::from_bits(crate::value::TAG_FALSE); } - if !cb.is_null() && crate::value::js_is_truthy(js_closure_call1(cb, v)) != 0 { + if !cb.is_null() + && crate::value::js_is_truthy(js_closure_call1( + cb, + crate::closure::plain_call_receiver(), + v, + )) != 0 + { return f64::from_bits(TAG_TRUE); } } @@ -579,7 +594,13 @@ pub unsafe fn dispatch_iterator_helper_method( if done { return f64::from_bits(TAG_TRUE); } - if !cb.is_null() && crate::value::js_is_truthy(js_closure_call1(cb, v)) == 0 { + if !cb.is_null() + && crate::value::js_is_truthy(js_closure_call1( + cb, + crate::closure::plain_call_receiver(), + v, + )) == 0 + { return f64::from_bits(crate::value::TAG_FALSE); } } @@ -591,7 +612,13 @@ pub unsafe fn dispatch_iterator_helper_method( if done { return f64::from_bits(TAG_UNDEFINED); } - if !cb.is_null() && crate::value::js_is_truthy(js_closure_call1(cb, v)) != 0 { + if !cb.is_null() + && crate::value::js_is_truthy(js_closure_call1( + cb, + crate::closure::plain_call_receiver(), + v, + )) != 0 + { return f64::from_bits(v.to_bits()); } } diff --git a/crates/perry-runtime/src/iterator_helpers/tests.rs b/crates/perry-runtime/src/iterator_helpers/tests.rs index cb96a7af71..317454bcf9 100644 --- a/crates/perry-runtime/src/iterator_helpers/tests.rs +++ b/crates/perry-runtime/src/iterator_helpers/tests.rs @@ -107,11 +107,19 @@ unsafe fn helper_over(values: &[f64]) -> f64 { /// The closure the combinator tests map with. Perry closures are invoked with /// the argument in the first `f64` slot; `js_closure_alloc` + a registered /// arity is the runtime-side equivalent of a compiled arrow function. -extern "C" fn double_it(_c: *const crate::closure::ClosureHeader, x: f64) -> f64 { +extern "C" fn double_it( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + x: f64, +) -> f64 { f64::from_bits(JSValue::number(f64::from_bits(x.to_bits()) * 2.0).bits()) } -extern "C" fn is_even(_c: *const crate::closure::ClosureHeader, x: f64) -> f64 { +extern "C" fn is_even( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + x: f64, +) -> f64 { let v = f64::from_bits(x.to_bits()); f64::from_bits(if v as i64 % 2 == 0 { crate::value::TAG_TRUE @@ -121,24 +129,33 @@ extern "C" fn is_even(_c: *const crate::closure::ClosureHeader, x: f64) -> f64 { } /// `(x) => [x, x * 10]`, for `flatMap`. -extern "C" fn pair_with_ten_times(_c: *const crate::closure::ClosureHeader, x: f64) -> f64 { +extern "C" fn pair_with_ten_times( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + x: f64, +) -> f64 { let v = f64::from_bits(x.to_bits()); unsafe { number_array(&[v, v * 10.0]) } } /// `(acc, v) => acc + v`, for `reduce`. -extern "C" fn add(_c: *const crate::closure::ClosureHeader, a: f64, b: f64) -> f64 { +extern "C" fn add( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, +) -> f64 { let sum = f64::from_bits(a.to_bits()) + f64::from_bits(b.to_bits()); f64::from_bits(JSValue::number(sum).bits()) } -unsafe fn closure1(f: extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64) -> f64 { +unsafe fn closure1(f: crate::closure::body_call::js_body_fn_ty!(a)) -> f64 { let p = f as *const u8; crate::closure::js_register_closure_arity(p, 1); crate::value::js_nanbox_pointer(crate::closure::js_closure_alloc(p, 0) as i64) } -unsafe fn closure2(f: extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64) -> f64 { +unsafe fn closure2(f: crate::closure::body_call::js_body_fn_ty!(a, a)) -> f64 { let p = f as *const u8; crate::closure::js_register_closure_arity(p, 2); crate::value::js_nanbox_pointer(crate::closure::js_closure_alloc(p, 0) as i64) @@ -302,7 +319,12 @@ fn helper_next_rejects_a_non_helper_iterator_receiver() { ); let rebound_h = scope.root_nanbox_f64(f64::from_bits(rebound)); let result = crate::exception::js_call_catching(|| { - crate::closure::js_native_call_value(rebound_h.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::js_native_call_value( + rebound_h.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) }); assert!( result.is_err(), @@ -487,20 +509,24 @@ fn symbol_iterator_returns_the_helper_itself() { /// /// This pins the second half of the `iterator_step` fix independently of the /// own-vs-inherited lookup: an own `next` takes the closure branch either way, -/// so only the `js_implicit_this_set` around the call makes this pass. It is +/// so only the explicit receiver on the call makes this pass. It is /// the shape a hand-written `next() { return this.#impl.next(); }` takes, and /// pre-fix it saw `undefined`. /// -/// SABOTAGE CHECK: drop the `js_implicit_this_set` pair in `iterator_step` and -/// this reports a receiver of `undefined`. +/// SABOTAGE CHECK: pass `JsThis::UNDEFINED` instead of the iterator in +/// `iterator_step` and this reports a receiver of `undefined`. #[test] fn an_own_next_method_is_called_with_the_iterator_as_this() { use std::sync::atomic::{AtomicU64, Ordering}; /// Bits of the `this` the last `next()` observed. static OBSERVED_THIS: AtomicU64 = AtomicU64::new(0); - extern "C" fn next_recording_this(_c: *const crate::closure::ClosureHeader, _arg: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); + extern "C" fn next_recording_this( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _arg: f64, + ) -> f64 { + let this = this.as_f64(); OBSERVED_THIS.store(this.to_bits(), Ordering::SeqCst); unsafe { crate::iter_result::make_iter_result(JSValue::undefined(), true) } } diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index df6c11a894..b64d2d3a6f 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -57,8 +57,8 @@ fn tombstoned_key_slot(key: f64) -> bool { /// keys its already-serialized/dedup Maps by `this`) breaks without it — the /// Flight encoder's `referenceMap.get(this)` then never finds the parent path, /// so it re-serializes endlessly (Next.js standalone startup runaway). Mirror -/// the reviver path (`internalize_json_property`), which sets the implicit -/// `this` to the holder around the user-callback call. +/// the reviver path (`internalize_json_property`), which passes the holder as +/// the user callback's `this`. #[inline] pub(crate) unsafe fn call_replacer( replacer: *const crate::ClosureHeader, @@ -66,10 +66,12 @@ pub(crate) unsafe fn call_replacer( value_f64: f64, holder_f64: f64, ) -> f64 { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_f64)); - let result = crate::js_closure_call2(replacer, key_f64, value_f64); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::js_closure_call2( + replacer, + crate::closure::JsThis::from_f64(holder_f64), + key_f64, + value_f64, + ); // The user callback may have installed/removed `Object.prototype.toJSON` // (#6009 fast-probe cache). super::invalidate_object_proto_tojson_state(); diff --git a/crates/perry-runtime/src/json/replacer_hidden_fields_tests.rs b/crates/perry-runtime/src/json/replacer_hidden_fields_tests.rs index af066bfadb..3df6808b00 100644 --- a/crates/perry-runtime/src/json/replacer_hidden_fields_tests.rs +++ b/crates/perry-runtime/src/json/replacer_hidden_fields_tests.rs @@ -1,6 +1,11 @@ use super::*; -extern "C" fn identity(_closure: *const crate::ClosureHeader, _key: f64, value: f64) -> f64 { +extern "C" fn identity( + _closure: *const crate::ClosureHeader, + _this: crate::closure::JsThis, + _key: f64, + value: f64, +) -> f64 { value } diff --git a/crates/perry-runtime/src/json/reviver.rs b/crates/perry-runtime/src/json/reviver.rs index 8280dfe10b..6b9659ab3c 100644 --- a/crates/perry-runtime/src/json/reviver.rs +++ b/crates/perry-runtime/src/json/reviver.rs @@ -715,10 +715,13 @@ unsafe fn call_reviver( let holder_arg = holder_handle.get_nanbox_f64(); let key_arg = key_handle.get_nanbox_f64(); let value_arg = value_handle.get_nanbox_f64(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); - let result = - crate::js_closure_call3(reviver, key_arg, value_arg, context_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::js_closure_call3( + reviver, + crate::closure::JsThis::from_f64(holder_arg), + key_arg, + value_arg, + context_handle.get_nanbox_f64(), + ); let result_bits = result.to_bits(); let revived_bits = if result_bits == value_arg.to_bits() { value_handle.get_nanbox_u64() diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index c200a700a2..12b0dc140f 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -313,10 +313,12 @@ pub(crate) unsafe fn object_get_to_json(ptr: *const u8) -> Option { // record it in `TO_JSON_KEY` before recursing here. let key_f64_arg = current_to_json_key_arg(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); - let result = crate::closure::js_native_call_value(f64::from_bits(bound), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(recv_handle.get_nanbox_f64()), + &key_f64_arg, + 1, + ); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) @@ -365,11 +367,12 @@ pub(crate) unsafe fn array_get_to_json(arr: *const crate::ArrayHeader) -> Option let recv_handle = scope.root_nanbox_f64(recv); // `toJSON(key)` receives the property key of this array value (#5909). let key_f64_arg = current_to_json_key_arg(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - recv_handle.get_nanbox_f64(), - )); - let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + f64::from_bits(method_bits), + crate::closure::JsThis::from_f64(recv_handle.get_nanbox_f64()), + &key_f64_arg, + 1, + ); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) diff --git a/crates/perry-runtime/src/json/stringify_scalars.rs b/crates/perry-runtime/src/json/stringify_scalars.rs index 7e2cd2fbde..de3884b973 100644 --- a/crates/perry-runtime/src/json/stringify_scalars.rs +++ b/crates/perry-runtime/src/json/stringify_scalars.rs @@ -362,13 +362,14 @@ pub(crate) unsafe fn bigint_apply_to_json(value: f64) -> Option { if !crate::closure::is_closure_ptr(method_ptr) { return None; } - let recv = value_handle.get_nanbox_f64(); // `toJSON(key)` receives the property key of this BigInt value (#5909). let key_f64_arg = super::stringify_tojson_probe::current_to_json_key_arg(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); - let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + f64::from_bits(method_bits), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), + &key_f64_arg, + 1, + ); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) diff --git a/crates/perry-runtime/src/json_tape/cached_read.rs b/crates/perry-runtime/src/json_tape/cached_read.rs index 7ca2152bf5..6047154fe0 100644 --- a/crates/perry-runtime/src/json_tape/cached_read.rs +++ b/crates/perry-runtime/src/json_tape/cached_read.rs @@ -379,7 +379,10 @@ mod tests { } } - extern "C" fn descriptor_getter(_: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn descriptor_getter( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { 61.0 } diff --git a/crates/perry-runtime/src/jsx.rs b/crates/perry-runtime/src/jsx.rs index 04ce561b0a..9eb7cdb473 100644 --- a/crates/perry-runtime/src/jsx.rs +++ b/crates/perry-runtime/src/jsx.rs @@ -58,7 +58,14 @@ fn dispatch(type_arg: f64, props: f64) -> f64 { // the value-call path binds. if crate::proxy::js_proxy_is_proxy(type_arg) == 1 { if crate::proxy::proxy_wraps_callable(type_arg) { - return unsafe { crate::closure::js_native_call_value(type_arg, &props, 1) }; + return unsafe { + crate::closure::js_native_call_value( + type_arg, + crate::closure::plain_call_receiver(), + &props, + 1, + ) + }; } return f64::from_bits(crate::value::TAG_UNDEFINED); } @@ -68,7 +75,7 @@ fn dispatch(type_arg: f64, props: f64) -> f64 { if jsval.is_pointer() { let ptr = jsval.as_pointer::(); if !ptr.is_null() && is_valid_closure(ptr) { - return js_closure_call1(ptr, props); + return js_closure_call1(ptr, crate::closure::plain_call_receiver(), props); } } @@ -398,7 +405,11 @@ mod tests { } /// A function component: `(props) => {props.children}`. - extern "C" fn span_component(_closure: *const ClosureHeader, props: f64) -> f64 { + extern "C" fn span_component( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + props: f64, + ) -> f64 { js_jsx(str_val("span"), props) } diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index 61dc60ee1e..68349fcaf3 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -360,7 +360,7 @@ pub mod win_console; pub use array::ArrayHeader; pub use bigint::BigIntHeader; pub use buffer::BufferHeader; -pub use closure::ClosureHeader; +pub use closure::{ClosureHeader, JsThis}; pub use map::MapHeader; pub use object::ObjectHeader; pub use object::{object_live_slot_count, perry_object_header_abi_revision}; @@ -758,6 +758,7 @@ pub(crate) mod stdlib_pump { } // SAFETY: `js_register_stdlib_next_wake` only stores callbacks with // this exact ABI and signature. + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: extern "C" fn() -> f64 = unsafe { std::mem::transmute(f) }; func() } diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index b0ac88ef19..6b07189464 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -3052,8 +3052,6 @@ fn js_map_foreach_impl( // appends are visited too. Bounding the walk by `size` exposed holes // and truncated later entries (#9072). let mut i = 0usize; - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let map = map_handle.get_raw_const_ptr::(); if i >= (*map).used as usize { @@ -3076,13 +3074,10 @@ fn js_map_foreach_impl( } let args = [value, key, map_value]; let cb = callback_handle.get_nanbox_f64(); - let this_v = this_handle.get_nanbox_f64(); - // Bind `thisArg` for the duration of the call (matches the - // URLSearchParams.forEach pattern); `js_native_call_value` + let this_v = crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()); + // `thisArg` is the callback's receiver; the value-call path // dispatches the NaN-boxed callback with the full arg vector. - crate::object::js_implicit_this_set(this_v); - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let _ = crate::closure::native_call_value_this(cb, this_v, args.as_ptr(), args.len()); } } let map = map_handle.get_raw_const_ptr::(); diff --git a/crates/perry-runtime/src/map_tombstone_tests.rs b/crates/perry-runtime/src/map_tombstone_tests.rs index eb06abc483..028ca68add 100644 --- a/crates/perry-runtime/src/map_tombstone_tests.rs +++ b/crates/perry-runtime/src/map_tombstone_tests.rs @@ -17,6 +17,7 @@ crate::perry_thread_local! { extern "C" fn delete_current_map_entry( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, key: f64, collection: f64, @@ -29,6 +30,7 @@ extern "C" fn delete_current_map_entry( extern "C" fn delete_earlier_map_entry( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, key: f64, collection: f64, @@ -488,6 +490,7 @@ fn clear_truncates_the_squeeze_history() { extern "C" fn delete_earlier_then_live_read( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, key: f64, collection: f64, diff --git a/crates/perry-runtime/src/media_playback.rs b/crates/perry-runtime/src/media_playback.rs index 61c7f83ae7..a75915a093 100644 --- a/crates/perry-runtime/src/media_playback.rs +++ b/crates/perry-runtime/src/media_playback.rs @@ -532,18 +532,27 @@ fn fire_state_callback(closure_d: f64, state: MediaState) { let s = state.as_str(); let header = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); if header.is_null() { - let _ = js_closure_call1(closure, f64::from_bits(TAG_UNDEFINED)); + let _ = js_closure_call1( + closure, + crate::closure::plain_call_receiver(), + f64::from_bits(TAG_UNDEFINED), + ); return; } let str_d = crate::value::js_nanbox_string(header as i64); - let _ = js_closure_call1(closure, str_d); + let _ = js_closure_call1(closure, crate::closure::plain_call_receiver(), str_d); } fn fire_time_callback(closure_d: f64, current: f64, duration: f64) { let Some(closure) = unbox_closure(closure_d) else { return; }; - let _ = js_closure_call2(closure, current, duration); + let _ = js_closure_call2( + closure, + crate::closure::plain_call_receiver(), + current, + duration, + ); } // --------------------------------------------------------------------------- diff --git a/crates/perry-runtime/src/messaging.rs b/crates/perry-runtime/src/messaging.rs index c5d21f9ef1..a200e3982e 100644 --- a/crates/perry-runtime/src/messaging.rs +++ b/crates/perry-runtime/src/messaging.rs @@ -103,19 +103,28 @@ fn closure_value(func_ptr: *const u8, name: &str, arity: u32) -> f64 { crate::value::js_nanbox_pointer(closure as i64) } -extern "C" fn noop0(_closure: *const ClosureHeader) -> f64 { +extern "C" fn noop0(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { js_undefined() } -extern "C" fn noop1(_closure: *const ClosureHeader, _arg0: f64) -> f64 { +extern "C" fn noop1( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg0: f64, +) -> f64 { js_undefined() } -extern "C" fn noop2(_closure: *const ClosureHeader, _arg0: f64, _arg1: f64) -> f64 { +extern "C" fn noop2( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg0: f64, + _arg1: f64, +) -> f64 { js_undefined() } -extern "C" fn has_ref(_closure: *const ClosureHeader) -> f64 { +extern "C" fn has_ref(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { js_bool(false) } @@ -269,8 +278,8 @@ fn with_port_states(f: impl FnOnce(&mut HashMap) -> R) -> R } /// Resolve the current method receiver (`this`) to a port object pointer. -fn this_port_ptr() -> usize { - let this = object::js_implicit_this_get(); +fn this_port_ptr(this: crate::closure::JsThis) -> usize { + let this = this.as_f64(); crate::value::js_nanbox_get_pointer(this) as usize } @@ -322,22 +331,25 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { let handler_h = scope.root_nanbox_f64(handler); let event_h = scope.root_nanbox_f64(event); let port_h = scope.root_nanbox_f64(port_box); - let prev_this = scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); let args = [event_h.get_nanbox_f64()]; unsafe { - let _ = crate::closure::js_native_call_value( + let _ = crate::closure::native_call_value_this( handler_h.get_nanbox_f64(), + crate::closure::JsThis::from_f64(port_h.get_nanbox_f64()), args.as_ptr(), args.len(), ); } - object::js_implicit_this_set(prev_this.get_nanbox_f64()); } /// Macrotask body: deliver exactly one queued message to `port_box`'s port. /// Scheduled via `setImmediate`, so the event loop pumps it; chains naturally /// because a handler that posts again schedules a fresh macrotask. -extern "C" fn deliver_one_message(_closure: *const ClosureHeader, port_box: f64) -> f64 { +extern "C" fn deliver_one_message( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + port_box: f64, +) -> f64 { let port_ptr = crate::value::js_nanbox_get_pointer(port_box) as usize; if port_ptr == 0 { return js_undefined(); @@ -388,8 +400,13 @@ fn schedule_delivery(port_ptr: usize) { } /// `port.postMessage(data[, transferList])`. -extern "C" fn port_post_message(_closure: *const ClosureHeader, data: f64, _transfer: f64) -> f64 { - let self_ptr = this_port_ptr(); +extern "C" fn port_post_message( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + data: f64, + _transfer: f64, +) -> f64 { + let self_ptr = this_port_ptr(this); if self_ptr == 0 { return js_undefined(); } @@ -433,13 +450,13 @@ fn start_port(self_ptr: usize) { } } -extern "C" fn port_start(_closure: *const ClosureHeader) -> f64 { - start_port(this_port_ptr()); +extern "C" fn port_start(_closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + start_port(this_port_ptr(this)); js_undefined() } -extern "C" fn port_close(_closure: *const ClosureHeader) -> f64 { - let self_ptr = this_port_ptr(); +extern "C" fn port_close(_closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + let self_ptr = this_port_ptr(this); if self_ptr != 0 { with_port_states(|map| { let Some(state) = map.get_mut(&self_ptr) else { @@ -467,8 +484,11 @@ extern "C" fn port_close(_closure: *const ClosureHeader) -> f64 { } /// `port.onmessage` getter — return the stored handler (null if unset). -extern "C" fn port_onmessage_get(_closure: *const ClosureHeader) -> f64 { - let self_ptr = this_port_ptr(); +extern "C" fn port_onmessage_get( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let self_ptr = this_port_ptr(this); if self_ptr == 0 { return js_null(); } @@ -477,8 +497,12 @@ extern "C" fn port_onmessage_get(_closure: *const ClosureHeader) -> f64 { /// `port.onmessage` setter — store the handler and (per HTML) implicitly /// start the port, flushing any queued messages. -extern "C" fn port_onmessage_set(_closure: *const ClosureHeader, value: f64) -> f64 { - let self_ptr = this_port_ptr(); +extern "C" fn port_onmessage_set( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + let self_ptr = this_port_ptr(this); if self_ptr == 0 { return js_undefined(); } @@ -491,10 +515,11 @@ extern "C" fn port_onmessage_set(_closure: *const ClosureHeader, value: f64) -> extern "C" fn port_add_event_listener( _closure: *const ClosureHeader, + this: crate::closure::JsThis, type_value: f64, listener: f64, ) -> f64 { - let self_ptr = this_port_ptr(); + let self_ptr = this_port_ptr(this); if self_ptr == 0 || !is_message_type(type_value) || !value_is_callable(listener) { return js_undefined(); } @@ -522,10 +547,11 @@ extern "C" fn port_add_event_listener( extern "C" fn port_remove_event_listener( _closure: *const ClosureHeader, + this: crate::closure::JsThis, type_value: f64, listener: f64, ) -> f64 { - let self_ptr = this_port_ptr(); + let self_ptr = this_port_ptr(this); if self_ptr == 0 || !is_message_type(type_value) { return js_undefined(); } @@ -654,6 +680,7 @@ fn same_thread_message_channel_new() -> f64 { pub(crate) extern "C" fn js_message_channel_constructor_call_error( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { throw_constructor_call_error() } @@ -692,6 +719,7 @@ pub extern "C" fn js_broadcast_channel_new(name: f64) -> f64 { pub(crate) extern "C" fn js_broadcast_channel_constructor_call_error( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { throw_constructor_call_error() @@ -699,6 +727,7 @@ pub(crate) extern "C" fn js_broadcast_channel_constructor_call_error( pub(crate) extern "C" fn js_message_port_constructor_call_error( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { throw_constructor_call_error() } diff --git a/crates/perry-runtime/src/module_require.rs b/crates/perry-runtime/src/module_require.rs index 10c918edf4..28d7c275b9 100644 --- a/crates/perry-runtime/src/module_require.rs +++ b/crates/perry-runtime/src/module_require.rs @@ -765,6 +765,7 @@ fn require_path(cache: f64, path: &std::path::Path, parent_filename: &str) -> f6 let exports = crate::closure::js_closure_call0( crate::value::js_nanbox_get_pointer(factory_handle.get_nanbox_f64()) as *const ClosureHeader, + crate::closure::plain_call_receiver(), ); if let Some((record, cached)) = cached_record(cache_handle.get_nanbox_f64(), &filename) @@ -877,6 +878,7 @@ fn run_custom_extension(path: &std::path::Path, record: f64, filename: &str) { let filename_value = string_value(&filename); crate::closure::js_closure_call2( handler_ptr as *mut ClosureHeader, + crate::closure::plain_call_receiver(), record_handle.get_nanbox_f64(), filename_value, ); @@ -885,7 +887,11 @@ fn run_custom_extension(path: &std::path::Path, record: f64, filename: &str) { } } -extern "C" fn require_thunk(closure: *const ClosureHeader, id: f64) -> f64 { +extern "C" fn require_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + id: f64, +) -> f64 { let specifier = value_to_string(id, "id"); if specifier.is_empty() { let message = "The argument 'id' must be a non-empty string"; @@ -907,7 +913,12 @@ extern "C" fn require_thunk(closure: *const ClosureHeader, id: f64) -> f64 { } } -extern "C" fn resolve_thunk(closure: *const ClosureHeader, request: f64, _options: f64) -> f64 { +extern "C" fn resolve_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + request: f64, + _options: f64, +) -> f64 { let specifier = value_to_string(request, "request"); if let Some(resolved) = resolve_builtin(&specifier) { return string_value(resolved); @@ -920,7 +931,11 @@ extern "C" fn resolve_thunk(closure: *const ClosureHeader, request: f64, _option } } -extern "C" fn resolve_paths_thunk(closure: *const ClosureHeader, request: f64) -> f64 { +extern "C" fn resolve_paths_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + request: f64, +) -> f64 { let specifier = value_to_string(request, "request"); if supported_require_builtin(&specifier).is_some() { return null(); @@ -1512,7 +1527,11 @@ static KEEP_JS_MODULE_AMBIENT_REQUIRE: extern "C" fn() -> f64 = js_module_ambien /// Returns the required value directly (no Promise). #[no_mangle] pub extern "C" fn js_module_ambient_require_apply(spec: f64) -> f64 { - require_thunk(std::ptr::null(), spec) + require_thunk( + std::ptr::null(), + crate::closure::plain_call_receiver(), + spec, + ) } /// Keepalive anchor for the auto-optimize whole-program build (generated-code-only diff --git a/crates/perry-runtime/src/module_require/import_meta_resolve.rs b/crates/perry-runtime/src/module_require/import_meta_resolve.rs index 1f93ba4e2e..7ac7562954 100644 --- a/crates/perry-runtime/src/module_require/import_meta_resolve.rs +++ b/crates/perry-runtime/src/module_require/import_meta_resolve.rs @@ -191,7 +191,12 @@ pub extern "C" fn js_import_meta_resolve(specifier: f64, parent: f64, fallback: )) } -extern "C" fn resolve_closure(closure: *mut ClosureHeader, specifier: f64, parent: f64) -> f64 { +extern "C" fn resolve_closure( + closure: *mut ClosureHeader, + _this: crate::closure::JsThis, + specifier: f64, + parent: f64, +) -> f64 { let fallback = js_closure_get_capture_f64(closure, 0); js_import_meta_resolve(specifier, parent, fallback) } diff --git a/crates/perry-runtime/src/node_api_host/functions.rs b/crates/perry-runtime/src/node_api_host/functions.rs index 9c4471529a..670b85a15f 100644 --- a/crates/perry-runtime/src/node_api_host/functions.rs +++ b/crates/perry-runtime/src/node_api_host/functions.rs @@ -37,6 +37,7 @@ fn current_callback_record(index: usize) -> Option { extern "C" fn napi_callback_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arguments: f64, ) -> f64 { let env = current_env(); @@ -66,7 +67,7 @@ extern "C" fn napi_callback_thunk( } } } - let this_bits = crate::object::js_implicit_this_get().to_bits(); + let this_bits = this.as_f64().to_bits(); let this_value = add_handle(env, this_bits).unwrap_or(std::ptr::null_mut()); let new_target_bits = crate::object::js_new_target_get().to_bits(); let new_target = if JSValue::from_bits(new_target_bits).is_undefined() { @@ -291,18 +292,14 @@ pub unsafe extern "C" fn napi_call_function( }; arguments.push(f64::from_bits(bits)); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - f64::from_bits(receiver_bits), - )); let call_result = catch_value_call(env, || { crate::closure::js_native_call_value( f64::from_bits(function_bits), + crate::closure::JsThis(receiver_bits), arguments.as_ptr(), arguments.len(), ) }); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match call_result { Ok(value) => { if !result.is_null() { diff --git a/crates/perry-runtime/src/node_api_host/v10_tests.rs b/crates/perry-runtime/src/node_api_host/v10_tests.rs index d870cd12f7..c1b2420efa 100644 --- a/crates/perry-runtime/src/node_api_host/v10_tests.rs +++ b/crates/perry-runtime/src/node_api_host/v10_tests.rs @@ -71,7 +71,10 @@ fn take_exception(env: NapiEnv) -> NapiValue { exception } -extern "C" fn plain_closure_body(_closure: *const ClosureHeader) -> f64 { +extern "C" fn plain_closure_body( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -557,7 +560,11 @@ thread_local! { static UNCAUGHT_CALLBACK_ERRORS: RefCell = const { RefCell::new(0) }; } -extern "C" fn count_uncaught_callback_error(_closure: *const ClosureHeader, _error: f64) -> f64 { +extern "C" fn count_uncaught_callback_error( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _error: f64, +) -> f64 { UNCAUGHT_CALLBACK_ERRORS.with(|count| *count.borrow_mut() += 1); f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index f7c6fb4ca8..0054c5a77c 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -255,12 +255,14 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined(); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); - let result = - unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + callback, + crate::closure::JsThis::from_f64(this), + args.as_ptr(), + args.len(), + ) + } } fn node_error_value(message: &str, code: &'static str) -> f64 { @@ -1108,7 +1110,10 @@ fn promise_value(result: Result) -> f64 { boxed_pointer(promise as *const u8) } -extern "C" fn endpoint_dispose(_closure: *const ClosureHeader) -> f64 { +extern "C" fn endpoint_dispose( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_node_inspector_close() } @@ -1180,6 +1185,7 @@ fn inspector_console_emit(kind: &str, first: f64, second: f64, third: f64) -> f6 extern "C" fn inspector_console_log( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -1189,6 +1195,7 @@ extern "C" fn inspector_console_log( extern "C" fn inspector_console_info( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -1198,6 +1205,7 @@ extern "C" fn inspector_console_info( extern "C" fn inspector_console_debug( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -1207,6 +1215,7 @@ extern "C" fn inspector_console_debug( extern "C" fn inspector_console_warn( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -1216,6 +1225,7 @@ extern "C" fn inspector_console_warn( extern "C" fn inspector_console_error( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -1347,38 +1357,49 @@ pub extern "C" fn js_node_inspector_wait_for_debugger() -> f64 { undefined() } -extern "C" fn session_connect_thunk(_closure: *const ClosureHeader) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_connect_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_connect(raw_ptr_from_value(this) as i64) } -extern "C" fn session_connect_main_thunk(_closure: *const ClosureHeader) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_connect_main_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_connect_to_main_thread(raw_ptr_from_value(this) as i64) } -extern "C" fn session_disconnect_thunk(_closure: *const ClosureHeader) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_disconnect_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_disconnect(raw_ptr_from_value(this) as i64) } extern "C" fn session_post_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, method: f64, params: f64, callback: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); js_node_inspector_session_post(raw_ptr_from_value(this) as i64, method, params, callback) } extern "C" fn promises_session_post_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, method: f64, params: f64, callback: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); js_node_inspector_promises_session_post( raw_ptr_from_value(this) as i64, method, @@ -1387,31 +1408,51 @@ extern "C" fn promises_session_post_thunk( ) } -extern "C" fn session_on_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_on_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + listener: f64, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_on(raw_ptr_from_value(this) as i64, event, listener) } -extern "C" fn session_once_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_once_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + listener: f64, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_once(raw_ptr_from_value(this) as i64, event, listener) } -extern "C" fn session_off_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_off_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + listener: f64, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_off(raw_ptr_from_value(this) as i64, event, listener) } -extern "C" fn session_listener_count_thunk(_closure: *const ClosureHeader, event: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn session_listener_count_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, +) -> f64 { + let this = this.as_f64(); js_node_inspector_session_listener_count(raw_ptr_from_value(this) as i64, event) } extern "C" fn session_remove_all_listeners_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); js_node_inspector_session_remove_all_listeners(raw_ptr_from_value(this) as i64, event) } diff --git a/crates/perry-runtime/src/node_repl.rs b/crates/perry-runtime/src/node_repl.rs index 9b8656ae9f..550da09b40 100644 --- a/crates/perry-runtime/src/node_repl.rs +++ b/crates/perry-runtime/src/node_repl.rs @@ -164,16 +164,21 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined(); } + let this_scope = crate::gc::RuntimeHandleScope::new(); + // The rebind clone allocates, so the receiver is re-read from a root. + let this_h = this_scope.root_nanbox_f64(this); let rebound = f64::from_bits(crate::closure::clone_closure_rebind_this( callback.to_bits(), this, )); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); - let result = - unsafe { crate::closure::js_native_call_value(rebound, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + rebound, + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ) + } } fn call_method(receiver: f64, name: &str, args: &[f64]) -> f64 { @@ -366,25 +371,36 @@ fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { crate::value::js_nanbox_pointer(closure as i64) } -extern "C" fn repl_on_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { - let server = crate::object::js_implicit_this_get(); +extern "C" fn repl_on_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + listener: f64, +) -> f64 { + let server = this.as_f64(); add_listener(server, event, listener, false); server } -extern "C" fn repl_once_thunk(_closure: *const ClosureHeader, event: f64, listener: f64) -> f64 { - let server = crate::object::js_implicit_this_get(); +extern "C" fn repl_once_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + listener: f64, +) -> f64 { + let server = this.as_f64(); add_listener(server, event, listener, true); server } extern "C" fn repl_emit_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, arg0: f64, arg1: f64, ) -> f64 { - let server = crate::object::js_implicit_this_get(); + let server = this.as_f64(); let Some(event_name) = string_to_rust(event) else { return bool_value(false); }; @@ -398,23 +414,28 @@ extern "C" fn repl_emit_thunk( extern "C" fn repl_display_prompt_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, _preserve_cursor: f64, ) -> f64 { - let server = crate::object::js_implicit_this_get(); + let server = this.as_f64(); display_prompt_for(server); undefined() } -extern "C" fn repl_clear_buffered_command_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn repl_clear_buffered_command_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { undefined() } extern "C" fn repl_setup_history_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, _path: f64, callback: f64, ) -> f64 { - let server = crate::object::js_implicit_this_get(); + let server = this.as_f64(); if let Some(obj) = object_ptr_from_value(server) { set_field(obj, "history", array_value(crate::array::js_array_alloc(0))); set_field(obj, "historySize", 30.0); @@ -425,10 +446,11 @@ extern "C" fn repl_setup_history_thunk( extern "C" fn repl_define_command_thunk( _closure: *const ClosureHeader, + this: crate::closure::JsThis, keyword: f64, command: f64, ) -> f64 { - let server = crate::object::js_implicit_this_get(); + let server = this.as_f64(); let Some(name) = string_to_rust(keyword) else { return undefined(); }; @@ -439,8 +461,12 @@ extern "C" fn repl_define_command_thunk( undefined() } -extern "C" fn repl_write_thunk(_closure: *const ClosureHeader, chunk: f64) -> f64 { - let server = crate::object::js_implicit_this_get(); +extern "C" fn repl_write_thunk( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + chunk: f64, +) -> f64 { + let server = this.as_f64(); let input = string_to_rust(chunk).unwrap_or_default(); for line in input.split_inclusive('\n') { let line = line.strip_suffix('\n').unwrap_or(line); diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index 03baefbc16..b497f239f3 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -162,7 +162,7 @@ pub use destroy_state::{js_node_stream_method_destroy, js_node_stream_method_des // ───────────────────────────────────────────────────────────────── #[inline] -fn this_value(closure: *const ClosureHeader) -> f64 { +fn this_value(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { // Slot 0 was set by `build_object` to the NaN-boxed bits of the // host object value cast to i64; reverse the cast. if !closure.is_null() { @@ -170,8 +170,8 @@ fn this_value(closure: *const ClosureHeader) -> f64 { // A `TAG_UNDEFINED` slot-0 marks a *prototype-method value* (e.g. // `Stream.prototype.on`, installed by `attach_event_emitter_prototype_methods`) // rather than an instance-bound method: it has no fixed receiver and must - // read the call-site `this` (set by `Function.prototype.call`/`apply` into - // IMPLICIT_THIS), so `Stream.prototype.on.call(streamInstance, ev, fn)` + // read the call-site `this` (the method's receiver parameter, which + // `Function.prototype.call`/`apply` supply), so `Stream.prototype.on.call(streamInstance, ev, fn)` // — readable-stream's `Readable.prototype.on` wrapper — registers the // listener on the instance, not on the prototype. `build_object` always // captures a real object pointer, so existing instance-bound closures @@ -180,20 +180,35 @@ fn this_value(closure: *const ClosureHeader) -> f64 { return f64::from_bits(bits); } } - crate::object::js_implicit_this_get() + this.as_f64() } -extern "C" fn ns_chain0(closure: *const ClosureHeader) -> f64 { - this_value(closure) +extern "C" fn ns_chain0(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + this_value(closure, this) } -extern "C" fn ns_chain1(closure: *const ClosureHeader, _a: f64) -> f64 { - this_value(closure) -} -extern "C" fn ns_chain2(closure: *const ClosureHeader, _a: f64, _b: f64) -> f64 { - this_value(closure) +extern "C" fn ns_chain1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _a: f64, +) -> f64 { + this_value(closure, this) } -extern "C" fn ns_chain3(closure: *const ClosureHeader, _a: f64, _b: f64, _c: f64) -> f64 { - this_value(closure) +extern "C" fn ns_chain2( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _a: f64, + _b: f64, +) -> f64 { + this_value(closure, this) +} +extern "C" fn ns_chain3( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _a: f64, + _b: f64, + _c: f64, +) -> f64 { + this_value(closure, this) } fn call_old_stream_on(old_stream: f64, event: &[u8], listener: *const ClosureHeader) { @@ -230,7 +245,11 @@ fn call_old_stream_on(old_stream: f64, event: &[u8], listener: *const ClosureHea } } -extern "C" fn ns_wrap_data(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn ns_wrap_data( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -239,7 +258,7 @@ extern "C" fn ns_wrap_data(closure: *const ClosureHeader, chunk: f64) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_wrap_end(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_wrap_end(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -248,7 +267,11 @@ extern "C" fn ns_wrap_end(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_wrap_error(closure: *const ClosureHeader, err: f64) -> f64 { +extern "C" fn ns_wrap_error( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -257,7 +280,7 @@ extern "C" fn ns_wrap_error(closure: *const ClosureHeader, err: f64) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_wrap_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_wrap_close(closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -266,8 +289,12 @@ extern "C" fn ns_wrap_close(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_wrap1(closure: *const ClosureHeader, old_stream: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_wrap1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + old_stream: f64, +) -> f64 { + let stream = this_value(closure, this); crate::closure::js_register_closure_arity(ns_wrap_data as *const u8, 1); crate::closure::js_register_closure_arity(ns_wrap_end as *const u8, 0); crate::closure::js_register_closure_arity(ns_wrap_error as *const u8, 1); @@ -289,7 +316,10 @@ extern "C" fn ns_wrap1(closure: *const ClosureHeader, old_stream: f64) -> f64 { stream } -extern "C" fn ns_readable_from_drain(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_readable_from_drain( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -303,7 +333,10 @@ extern "C" fn ns_readable_from_drain(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_readable_event_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_readable_event_microtask( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -320,7 +353,10 @@ extern "C" fn ns_readable_event_microtask(closure: *const ClosureHeader) -> f64 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_readable_end_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_readable_end_microtask( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -336,7 +372,10 @@ extern "C" fn ns_readable_end_microtask(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_readable_resume_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_readable_resume_microtask( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -355,7 +394,10 @@ extern "C" fn ns_readable_resume_microtask(closure: *const ClosureHeader) -> f64 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_finished_error_false_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_finished_error_false_close( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -373,7 +415,10 @@ extern "C" fn ns_finished_error_false_close(closure: *const ClosureHeader) -> f6 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_finished_default_completion(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_finished_default_completion( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() || js_closure_get_capture_f64(closure, 2).to_bits() == TAG_TRUE { return f64::from_bits(TAG_UNDEFINED); } @@ -398,7 +443,10 @@ extern "C" fn ns_finished_default_completion(closure: *const ClosureHeader) -> f f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_finished_signal_abort(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_finished_signal_abort( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -420,7 +468,10 @@ extern "C" fn ns_finished_signal_abort(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_writable_finish_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_writable_finish_microtask( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -454,7 +505,11 @@ extern "C" fn ns_writable_finish_microtask(closure: *const ClosureHeader) -> f64 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_construct_callback_done(closure: *const ClosureHeader, err: f64) -> f64 { +extern "C" fn ns_construct_callback_done( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -465,7 +520,11 @@ extern "C" fn ns_construct_callback_done(closure: *const ClosureHeader, err: f64 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_writable_final_callback_done(closure: *const ClosureHeader, err: f64) -> f64 { +extern "C" fn ns_writable_final_callback_done( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -494,8 +553,13 @@ extern "C" fn ns_writable_final_callback_done(closure: *const ClosureHeader, err f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_emit2(closure: *const ClosureHeader, event: f64, arg: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_emit2( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + arg: f64, +) -> f64 { + let stream = this_value(closure, this); let mut args = crate::array::js_array_alloc(0); if arg.to_bits() != TAG_UNDEFINED { args = crate::array::js_array_push_f64(args, arg); @@ -503,42 +567,51 @@ extern "C" fn ns_emit2(closure: *const ClosureHeader, event: f64, arg: f64) -> f emit_stream_event_from_array(stream, event, args) } -extern "C" fn ns_emit_rest(closure: *const ClosureHeader, event: f64, rest: f64) -> f64 { +extern "C" fn ns_emit_rest( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + rest: f64, +) -> f64 { emit_stream_event_from_array( - this_value(closure), + this_value(closure, this), event, raw_ptr_from_value(rest) as *const _, ) } -extern "C" fn ns_resume0(closure: *const ClosureHeader) -> f64 { - resume_readable_stream(this_value(closure)) +extern "C" fn ns_resume0(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + resume_readable_stream(this_value(closure, this)) } -extern "C" fn ns_pause0(closure: *const ClosureHeader) -> f64 { - pause_readable_stream(this_value(closure)) +extern "C" fn ns_pause0(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + pause_readable_stream(this_value(closure, this)) } -extern "C" fn ns_is_paused0(closure: *const ClosureHeader) -> f64 { - f64::from_bits(if readable_is_paused(this_value(closure)) { +extern "C" fn ns_is_paused0(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + f64::from_bits(if readable_is_paused(this_value(closure, this)) { TAG_TRUE } else { TAG_FALSE }) } -extern "C" fn ns_async_dispose(closure: *const ClosureHeader) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_async_dispose(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + let stream = this_value(closure, this); destroy_stream(stream, abort_error()); resolved_promise(f64::from_bits(TAG_UNDEFINED)) } -extern "C" fn ns_read1(closure: *const ClosureHeader, n: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_read1(closure: *const ClosureHeader, this: crate::closure::JsThis, n: f64) -> f64 { + let stream = this_value(closure, this); read_stream_with_size_arg(stream, n) } -extern "C" fn ns_set_encoding1(closure: *const ClosureHeader, encoding: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_set_encoding1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + encoding: f64, +) -> f64 { + let stream = this_value(closure, this); set_visible_readable_encoding(stream, normalize_readable_encoding(encoding)); stream } @@ -768,21 +841,25 @@ fn push_chunk_backpressure_result(stream: f64, total: f64) -> f64 { } /// `readable.push(chunk)` for the untyped/`as any` object-method path. -extern "C" fn ns_push1(closure: *const ClosureHeader, chunk: f64) -> f64 { - let stream = readable_push_receiver(closure); +extern "C" fn ns_push1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + chunk: f64, +) -> f64 { + let stream = readable_push_receiver(this, closure); if get_hidden_value(stream, hidden_readable_flag_key()).is_none() { throw_readable_push_invalid_receiver(); } push_chunk(stream, chunk) } -fn readable_push_receiver(closure: *const ClosureHeader) -> f64 { - let implicit = crate::object::js_implicit_this_get(); +fn readable_push_receiver(this: crate::closure::JsThis, closure: *const ClosureHeader) -> f64 { + let implicit = this.as_f64(); if implicit.to_bits() != TAG_UNDEFINED { return implicit; } if closure.is_null() { - return this_value(closure); + return this_value(closure, this); } throw_readable_push_invalid_receiver() } @@ -838,25 +915,42 @@ fn readable_unshift_after_end_error() -> f64 { crate::value::js_nanbox_pointer(err as i64) } -extern "C" fn ns_unshift1(closure: *const ClosureHeader, chunk: f64) -> f64 { - unshift_chunk(this_value(closure), chunk) +extern "C" fn ns_unshift1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + chunk: f64, +) -> f64 { + unshift_chunk(this_value(closure, this), chunk) } /// `readable.compose(stream)` (#1539): the instance-method form of /// `stream.compose`, routed through the shared compose builder so the same /// data-flow and error handling paths cover module and prototype calls. -extern "C" fn ns_compose1(closure: *const ClosureHeader, arg: f64) -> f64 { - build_node_stream_compose(vec![this_value(closure), arg]) +extern "C" fn ns_compose1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { + build_node_stream_compose(vec![this_value(closure, this), arg]) } -extern "C" fn ns_pipe2(closure: *const ClosureHeader, dest: f64, options: f64) -> f64 { +extern "C" fn ns_pipe2( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + dest: f64, + options: f64, +) -> f64 { if pipe_destination_is_missing(dest) { throw_readable_pipe_missing_destination(); } - let stream = this_value(closure); + let stream = this_value(closure, this); pipe_stream_to_destination(stream, dest, pipe_options_end(options)) } -extern "C" fn ns_writable_write_done(closure: *const ClosureHeader, err: f64) -> f64 { +extern "C" fn ns_writable_write_done( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -867,8 +961,12 @@ extern "C" fn ns_writable_write_done(closure: *const ClosureHeader, err: f64) -> f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_unpipe1(closure: *const ClosureHeader, dest: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_unpipe1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + dest: f64, +) -> f64 { + let stream = this_value(closure, this); if dest.to_bits() == TAG_UNDEFINED { unpipe_all_destinations(stream); } else { @@ -877,140 +975,12 @@ extern "C" fn ns_unpipe1(closure: *const ClosureHeader, dest: f64) -> f64 { stream } -fn pipe_listener_value(listener: *const ClosureHeader) -> f64 { - box_pointer(listener as *const u8) -} - -fn set_pipe_listener_captures( - listener: *mut ClosureHeader, - src: f64, - dest: f64, - unpipe: f64, - error: f64, - close: f64, - finish: f64, -) { - js_closure_set_capture_f64(listener, 0, src); - js_closure_set_capture_f64(listener, 1, dest); - js_closure_set_capture_f64(listener, 2, unpipe); - js_closure_set_capture_f64(listener, 3, error); - js_closure_set_capture_f64(listener, 4, close); - js_closure_set_capture_f64(listener, 5, finish); -} - -fn cleanup_pipe_listeners_from_closure(closure: *const ClosureHeader) { - if closure.is_null() { - return; - } - let dest = js_closure_get_capture_f64(closure, 1); - let unpipe = js_closure_get_capture_f64(closure, 2); - let error = js_closure_get_capture_f64(closure, 3); - let close = js_closure_get_capture_f64(closure, 4); - let finish = js_closure_get_capture_f64(closure, 5); - let _ = remove_stream_listener_for_event(dest, string_value(b"unpipe"), unpipe); - let _ = remove_stream_listener_for_event(dest, string_value(b"error"), error); - let _ = remove_stream_listener_for_event(dest, string_value(b"close"), close); - let _ = remove_stream_listener_for_event(dest, string_value(b"finish"), finish); -} - -extern "C" fn pipe_unpipe_callback(closure: *const ClosureHeader, src: f64) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let expected_src = js_closure_get_capture_f64(closure, 0); - if src.to_bits() == expected_src.to_bits() { - cleanup_pipe_listeners_from_closure(closure); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn pipe_error_callback(closure: *const ClosureHeader, err: f64) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let src = js_closure_get_capture_f64(closure, 0); - let dest = js_closure_get_capture_f64(closure, 1); - if !unpipe_destination(src, dest) { - cleanup_pipe_listeners_from_closure(closure); - } - if stream_listener_count_for_event(dest, string_value(b"error")) == 0 { - crate::exception::js_throw(err); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn pipe_close_callback(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let src = js_closure_get_capture_f64(closure, 0); - let dest = js_closure_get_capture_f64(closure, 1); - if !unpipe_destination(src, dest) { - cleanup_pipe_listeners_from_closure(closure); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn pipe_finish_callback(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let src = js_closure_get_capture_f64(closure, 0); - let dest = js_closure_get_capture_f64(closure, 1); - if !unpipe_destination(src, dest) { - cleanup_pipe_listeners_from_closure(closure); - } - f64::from_bits(TAG_UNDEFINED) -} - -extern "C" fn pipe_drain_callback(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let src = js_closure_get_capture_f64(closure, 0); - let dest = js_closure_get_capture_f64(closure, 1); - let listener = js_closure_get_capture_f64(closure, 2); - let _ = remove_stream_listener_for_event(dest, string_value(b"drain"), listener); - if pipe_destination_contains(src, dest) && !stream_destroyed(src) { - if stream_hidden_ended(src) && pending_readable_chunk_count(src) == 0 { - set_readable_flowing(src, f64::from_bits(TAG_TRUE)); - schedule_readable_end(src); - return f64::from_bits(TAG_UNDEFINED); - } - let _ = resume_readable_stream_from_pipe(src); - } - f64::from_bits(TAG_UNDEFINED) -} - -fn finish_pipe_destination(dest: f64) { - if stream_destroyed(dest) || has_truthy_hidden(dest, hidden_finish_emitted_key()) { - return; - } - if writable_length(dest) > 0.0 { - set_hidden_value( - dest, - hidden_stream_pipe_end_pending_key(), - f64::from_bits(TAG_TRUE), - ); - } else { - set_hidden_value( - dest, - hidden_stream_pipe_end_pending_key(), - f64::from_bits(TAG_FALSE), - ); - if !finish_transform_stream(dest, None) { - finish_stream(dest, None); - } - } -} - -extern "C" fn pipe_finish_destination_callback(closure: *const ClosureHeader) -> f64 { - if closure.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - finish_pipe_destination(js_closure_get_capture_f64(closure, 0)); - f64::from_bits(TAG_UNDEFINED) -} +mod pipe_listeners; +use pipe_listeners::{ + finish_pipe_destination, pipe_close_callback, pipe_drain_callback, pipe_error_callback, + pipe_finish_callback, pipe_finish_destination_callback, pipe_listener_value, + pipe_unpipe_callback, set_pipe_listener_captures, +}; fn install_pipe_destination_listeners(src: f64, dest: f64) { let unpipe = js_closure_alloc(pipe_unpipe_callback as *const u8, 6); @@ -1115,7 +1085,12 @@ fn pipe_destination_is_missing(dest: f64) -> bool { value.is_undefined() || value.is_null() } -extern "C" fn transform_write_callback(closure: *const ClosureHeader, err: f64, value: f64) -> f64 { +extern "C" fn transform_write_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, + value: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -1132,7 +1107,12 @@ extern "C" fn transform_write_callback(closure: *const ClosureHeader, err: f64, f64::from_bits(TAG_UNDEFINED) } -extern "C" fn transform_flush_callback(closure: *const ClosureHeader, err: f64, value: f64) -> f64 { +extern "C" fn transform_flush_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, + value: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -1162,26 +1142,41 @@ extern "C" fn transform_flush_callback(closure: *const ClosureHeader, err: f64, f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_write3(closure: *const ClosureHeader, chunk: f64, enc: f64, cb: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_write3( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + chunk: f64, + enc: f64, + cb: f64, +) -> f64 { + let stream = this_value(closure, this); write_writable_chunk(stream, chunk, enc, cb) } -extern "C" fn ns_end3(closure: *const ClosureHeader, chunk: f64, encoding: f64, cb: f64) -> f64 { - let stream = this_value(closure); +extern "C" fn ns_end3( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + chunk: f64, + encoding: f64, + cb: f64, +) -> f64 { + let stream = this_value(closure, this); finish_stream_with_args(stream, chunk, encoding, cb); stream } -extern "C" fn ns_cork0(closure: *const ClosureHeader) -> f64 { - cork_stream(this_value(closure)) +extern "C" fn ns_cork0(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + cork_stream(this_value(closure, this)) } -extern "C" fn ns_uncork0(closure: *const ClosureHeader) -> f64 { - uncork_stream(this_value(closure)) +extern "C" fn ns_uncork0(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + uncork_stream(this_value(closure, this)) } -extern "C" fn writable_write_callback_noop(_closure: *const ClosureHeader) -> f64 { +extern "C" fn writable_write_callback_noop( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(TAG_UNDEFINED) } @@ -1193,12 +1188,14 @@ fn invoke_writable_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: js_closure_set_capture_f64(cb, 2, callback); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunk, enc, cb_value]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(write, args.as_ptr(), args.len()); + let _ = crate::closure::native_call_value_this( + write, + crate::closure::JsThis::from_f64(stream), + args.as_ptr(), + args.len(), + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } else { throw_missing_stream_method("The _write() method is not implemented"); } @@ -1209,12 +1206,14 @@ fn invoke_writable_writev(stream: f64, chunks: f64) { let cb = js_closure_alloc(writable_write_callback_noop as *const u8, 0); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunks, cb_value]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(writev, args.as_ptr(), args.len()); + let _ = crate::closure::native_call_value_this( + writev, + crate::closure::JsThis::from_f64(stream), + args.as_ptr(), + args.len(), + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -1254,12 +1253,14 @@ fn invoke_transform_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: js_closure_set_capture_f64(cb, 2, callback); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunk, enc, cb_value]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(transform, args.as_ptr(), args.len()); + let _ = crate::closure::native_call_value_this( + transform, + crate::closure::JsThis::from_f64(stream), + args.as_ptr(), + args.len(), + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } throw_missing_stream_method("The _transform() method is not implemented"); @@ -1388,7 +1389,12 @@ fn complete_writable_write(stream: f64, len: f64, callback: f64, err: f64) { }; let args = [arg]; unsafe { - let _ = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); + let _ = crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } } if has_error { diff --git a/crates/perry-runtime/src/node_stream/async_iterator.rs b/crates/perry-runtime/src/node_stream/async_iterator.rs index 2160a0a1e7..6bb38ff58f 100644 --- a/crates/perry-runtime/src/node_stream/async_iterator.rs +++ b/crates/perry-runtime/src/node_stream/async_iterator.rs @@ -54,6 +54,7 @@ fn readable_iterator_done() -> f64 { extern "C" fn ns_readable_iterator_chunk_fulfilled( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let outer = js_closure_get_capture_ptr(closure, 0) as *mut crate::promise::Promise; @@ -65,6 +66,7 @@ extern "C" fn ns_readable_iterator_chunk_fulfilled( extern "C" fn ns_readable_iterator_chunk_rejected( closure: *const ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let outer = js_closure_get_capture_ptr(closure, 0) as *mut crate::promise::Promise; @@ -76,6 +78,7 @@ extern "C" fn ns_readable_iterator_chunk_rejected( extern "C" fn ns_readable_source_iterator_fulfilled( closure: *const ClosureHeader, + _this: crate::closure::JsThis, result: f64, ) -> f64 { let iterator = js_closure_get_capture_f64(closure, 0); @@ -99,6 +102,7 @@ extern "C" fn ns_readable_source_iterator_fulfilled( extern "C" fn ns_readable_source_iterator_rejected( closure: *const ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let iterator = js_closure_get_capture_f64(closure, 0); @@ -339,7 +343,11 @@ fn iterator_from_listener(closure: *const ClosureHeader) -> f64 { } /// `data` listener: resolve a waiting `next()` or buffer the chunk. -extern "C" fn ns_readable_iter_on_data(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn ns_readable_iter_on_data( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -377,7 +385,10 @@ extern "C" fn ns_readable_iter_on_data(closure: *const ClosureHeader, chunk: f64 /// `end` listener: a waiting `next()` resolves to `{done:true}`; otherwise the /// end is recorded so a later `next()` (after the queue drains) reports done. -extern "C" fn ns_readable_iter_on_end(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_readable_iter_on_end( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -400,7 +411,11 @@ extern "C" fn ns_readable_iter_on_end(closure: *const ClosureHeader) -> f64 { /// `error` listener: reject a waiting `next()` or store the error for the next /// pull. -extern "C" fn ns_readable_iter_on_error(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn ns_readable_iter_on_error( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -572,9 +587,12 @@ pub(super) fn call_source_iterator_return(stream: f64) { settle_iterator_return_value(returned); } -extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_readable_iterator_next( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let iterator = scope.root_nanbox_f64(this_value(closure)); + let iterator = scope.root_nanbox_f64(this_value(closure, this)); if iterator_is_done(iterator.get_nanbox_f64()) { return readable_iterator_done(); } @@ -675,8 +693,11 @@ extern "C" fn ns_readable_iterator_next(closure: *const ClosureHeader) -> f64 { box_pointer(promise.get_raw_const_ptr()) } -extern "C" fn ns_readable_iterator_return(closure: *const ClosureHeader) -> f64 { - let iterator = this_value(closure); +extern "C" fn ns_readable_iterator_return( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let iterator = this_value(closure, this); let already_done = iterator_is_done(iterator); iterator_mark_done(iterator); iterator_remove_listeners(iterator); @@ -692,19 +713,32 @@ extern "C" fn ns_readable_iterator_return(closure: *const ClosureHeader) -> f64 readable_iterator_done() } -extern "C" fn ns_readable_iterator_self(closure: *const ClosureHeader) -> f64 { - this_value(closure) +extern "C" fn ns_readable_iterator_self( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + this_value(closure, this) } -pub(super) extern "C" fn ns_async_iterator(closure: *const ClosureHeader) -> f64 { - build_readable_async_iterator(this_value(closure), true) +pub(super) extern "C" fn ns_async_iterator( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + build_readable_async_iterator(this_value(closure, this), true) } -pub(super) extern "C" fn ns_iterator1(closure: *const ClosureHeader, opts: f64) -> f64 { - build_readable_async_iterator(this_value(closure), destroy_on_return_from_options(opts)) +pub(super) extern "C" fn ns_iterator1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + opts: f64, +) -> f64 { + build_readable_async_iterator( + this_value(closure, this), + destroy_on_return_from_options(opts), + ) } -fn install_async_iterator_symbol(target: f64, func: extern "C" fn(*const ClosureHeader) -> f64) { +fn install_async_iterator_symbol(target: f64, func: crate::closure::body_call::js_body_fn_ty!()) { let scope = crate::gc::RuntimeHandleScope::new(); let target = scope.root_nanbox_f64(target); let async_iterator = crate::symbol::well_known_symbol("asyncIterator"); @@ -808,8 +842,11 @@ pub extern "C" fn js_make_single_value_async_iterator(value: f64) -> f64 { iterator } -extern "C" fn single_value_iterator_next(closure: *const ClosureHeader) -> f64 { - let iterator = this_value(closure); +extern "C" fn single_value_iterator_next( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let iterator = this_value(closure, this); if iterator_is_done(iterator) { return readable_iterator_done(); } @@ -819,8 +856,11 @@ extern "C" fn single_value_iterator_next(closure: *const ClosureHeader) -> f64 { resolved_promise(iterator_result(value, false)) } -extern "C" fn single_value_iterator_return(closure: *const ClosureHeader) -> f64 { - iterator_mark_done(this_value(closure)); +extern "C" fn single_value_iterator_return( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + iterator_mark_done(this_value(closure, this)); readable_iterator_done() } @@ -932,9 +972,9 @@ mod fifo_pending_tests { js_closure_set_capture_f64(data_cb, 0, iterator); set_readable_flowing(stream, f64::from_bits(TAG_TRUE)); - ns_readable_iter_on_data(data_cb, 1.0); + ns_readable_iter_on_data(data_cb, crate::closure::JsThis::UNDEFINED, 1.0); assert!(readable_is_flowing(stream)); - ns_readable_iter_on_data(data_cb, 2.0); + ns_readable_iter_on_data(data_cb, crate::closure::JsThis::UNDEFINED, 2.0); assert!(readable_is_paused(stream)); assert_eq!( @@ -1006,7 +1046,8 @@ mod fifo_pending_tests { let rejected = js_closure_alloc(ns_readable_source_iterator_rejected as *const u8, 1); js_closure_set_capture_f64(rejected, 0, iterator); - let result = ns_readable_source_iterator_rejected(rejected, 7.0); + let result = + ns_readable_source_iterator_rejected(rejected, crate::closure::JsThis::UNDEFINED, 7.0); let promise = crate::value::js_nanbox_get_pointer(result) as *mut crate::promise::Promise; assert!(iterator_is_done(iterator)); assert!(stream_destroyed(stream)); diff --git a/crates/perry-runtime/src/node_stream/async_iterator/foreign_eof_tests.rs b/crates/perry-runtime/src/node_stream/async_iterator/foreign_eof_tests.rs index 04b09e56c9..95011b1540 100644 --- a/crates/perry-runtime/src/node_stream/async_iterator/foreign_eof_tests.rs +++ b/crates/perry-runtime/src/node_stream/async_iterator/foreign_eof_tests.rs @@ -9,7 +9,10 @@ fn readable() -> f64 { fn pull(iterator: f64) -> f64 { let next = get_hidden_value(iterator, hidden_key(b"next")).expect("iterator.next"); - crate::closure::js_closure_call0(raw_ptr_from_value(next) as *const ClosureHeader) + crate::closure::js_closure_call0( + raw_ptr_from_value(next) as *const ClosureHeader, + crate::closure::plain_call_receiver(), + ) } fn result_field(promise: f64, name: &[u8]) -> f64 { diff --git a/crates/perry-runtime/src/node_stream/pipe_listeners.rs b/crates/perry-runtime/src/node_stream/pipe_listeners.rs new file mode 100644 index 0000000000..f7b66dd0d8 --- /dev/null +++ b/crates/perry-runtime/src/node_stream/pipe_listeners.rs @@ -0,0 +1,158 @@ +//! The `pipe()` destination listeners: closures capturing (src, dest, and their siblings) that unpipe, forward errors and finish the destination. + +use super::*; + +pub(super) fn pipe_listener_value(listener: *const ClosureHeader) -> f64 { + box_pointer(listener as *const u8) +} + +pub(super) fn set_pipe_listener_captures( + listener: *mut ClosureHeader, + src: f64, + dest: f64, + unpipe: f64, + error: f64, + close: f64, + finish: f64, +) { + js_closure_set_capture_f64(listener, 0, src); + js_closure_set_capture_f64(listener, 1, dest); + js_closure_set_capture_f64(listener, 2, unpipe); + js_closure_set_capture_f64(listener, 3, error); + js_closure_set_capture_f64(listener, 4, close); + js_closure_set_capture_f64(listener, 5, finish); +} + +fn cleanup_pipe_listeners_from_closure(closure: *const ClosureHeader) { + if closure.is_null() { + return; + } + let dest = js_closure_get_capture_f64(closure, 1); + let unpipe = js_closure_get_capture_f64(closure, 2); + let error = js_closure_get_capture_f64(closure, 3); + let close = js_closure_get_capture_f64(closure, 4); + let finish = js_closure_get_capture_f64(closure, 5); + let _ = remove_stream_listener_for_event(dest, string_value(b"unpipe"), unpipe); + let _ = remove_stream_listener_for_event(dest, string_value(b"error"), error); + let _ = remove_stream_listener_for_event(dest, string_value(b"close"), close); + let _ = remove_stream_listener_for_event(dest, string_value(b"finish"), finish); +} + +pub(super) extern "C" fn pipe_unpipe_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + src: f64, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let expected_src = js_closure_get_capture_f64(closure, 0); + if src.to_bits() == expected_src.to_bits() { + cleanup_pipe_listeners_from_closure(closure); + } + f64::from_bits(TAG_UNDEFINED) +} + +pub(super) extern "C" fn pipe_error_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let src = js_closure_get_capture_f64(closure, 0); + let dest = js_closure_get_capture_f64(closure, 1); + if !unpipe_destination(src, dest) { + cleanup_pipe_listeners_from_closure(closure); + } + if stream_listener_count_for_event(dest, string_value(b"error")) == 0 { + crate::exception::js_throw(err); + } + f64::from_bits(TAG_UNDEFINED) +} + +pub(super) extern "C" fn pipe_close_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let src = js_closure_get_capture_f64(closure, 0); + let dest = js_closure_get_capture_f64(closure, 1); + if !unpipe_destination(src, dest) { + cleanup_pipe_listeners_from_closure(closure); + } + f64::from_bits(TAG_UNDEFINED) +} + +pub(super) extern "C" fn pipe_finish_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let src = js_closure_get_capture_f64(closure, 0); + let dest = js_closure_get_capture_f64(closure, 1); + if !unpipe_destination(src, dest) { + cleanup_pipe_listeners_from_closure(closure); + } + f64::from_bits(TAG_UNDEFINED) +} + +pub(super) extern "C" fn pipe_drain_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + let src = js_closure_get_capture_f64(closure, 0); + let dest = js_closure_get_capture_f64(closure, 1); + let listener = js_closure_get_capture_f64(closure, 2); + let _ = remove_stream_listener_for_event(dest, string_value(b"drain"), listener); + if pipe_destination_contains(src, dest) && !stream_destroyed(src) { + if stream_hidden_ended(src) && pending_readable_chunk_count(src) == 0 { + set_readable_flowing(src, f64::from_bits(TAG_TRUE)); + schedule_readable_end(src); + return f64::from_bits(TAG_UNDEFINED); + } + let _ = resume_readable_stream_from_pipe(src); + } + f64::from_bits(TAG_UNDEFINED) +} + +pub(super) fn finish_pipe_destination(dest: f64) { + if stream_destroyed(dest) || has_truthy_hidden(dest, hidden_finish_emitted_key()) { + return; + } + if writable_length(dest) > 0.0 { + set_hidden_value( + dest, + hidden_stream_pipe_end_pending_key(), + f64::from_bits(TAG_TRUE), + ); + } else { + set_hidden_value( + dest, + hidden_stream_pipe_end_pending_key(), + f64::from_bits(TAG_FALSE), + ); + if !finish_transform_stream(dest, None) { + finish_stream(dest, None); + } + } +} + +pub(super) extern "C" fn pipe_finish_destination_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + if closure.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + finish_pipe_destination(js_closure_get_capture_f64(closure, 0)); + f64::from_bits(TAG_UNDEFINED) +} diff --git a/crates/perry-runtime/src/node_stream/readable_from_promises.rs b/crates/perry-runtime/src/node_stream/readable_from_promises.rs index e2d1be22d7..5eba52f560 100644 --- a/crates/perry-runtime/src/node_stream/readable_from_promises.rs +++ b/crates/perry-runtime/src/node_stream/readable_from_promises.rs @@ -110,6 +110,7 @@ fn settle_readable_from_promise_rejected(stream: f64, chunk: f64, reason: f64) { pub(super) extern "C" fn ns_readable_from_promise_fulfilled( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { if closure.is_null() { @@ -123,6 +124,7 @@ pub(super) extern "C" fn ns_readable_from_promise_fulfilled( pub(super) extern "C" fn ns_readable_from_promise_rejected( closure: *const ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { if closure.is_null() { diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index cfbbac66e4..3b4acfa968 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -286,6 +286,7 @@ pub unsafe extern "C" fn js_array_subclass_init_args( /// Legacy shape-carried compatibility closure for `Array.prototype.fill`. pub(super) extern "C" fn ns_array_fill( closure: *const ClosureHeader, + this: crate::closure::JsThis, value: f64, start: f64, end: f64, @@ -296,7 +297,7 @@ pub(super) extern "C" fn ns_array_fill( // from index 1 (node: `7|8|8`, perry: `8|8|8`). let present = |v: f64| i32::from(!JSValue::from_bits(v.to_bits()).is_undefined()); crate::array::js_array_fill_generic( - super::this_value(closure), + super::this_value(closure, this), value, present(start), start, diff --git a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs index c9a2f992e7..228aca61e8 100644 --- a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs +++ b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs @@ -45,6 +45,7 @@ fn node_stream_duplex_from_source_chunks(source: f64) -> f64 { pub(super) extern "C" fn duplex_from_writable_write_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, encoding: f64, cb: f64, @@ -58,6 +59,7 @@ pub(super) extern "C" fn duplex_from_writable_write_callback( pub(super) extern "C" fn duplex_from_writable_final_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, cb: f64, ) -> f64 { if closure.is_null() { @@ -292,6 +294,7 @@ pub extern "C" fn js_node_stream_pipeline(args: *const crate::array::ArrayHeader pub(crate) extern "C" fn duplex_pair_write_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, _encoding: f64, cb: f64, @@ -311,7 +314,11 @@ pub(crate) extern "C" fn duplex_pair_write_callback( f64::from_bits(TAG_UNDEFINED) } -pub(crate) extern "C" fn duplex_pair_final_callback(closure: *const ClosureHeader, cb: f64) -> f64 { +pub(crate) extern "C" fn duplex_pair_final_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + cb: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } diff --git a/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs b/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs index 9f6c80aa46..81d357b50c 100644 --- a/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs +++ b/crates/perry-runtime/src/node_stream_constructors/web_adapter.rs @@ -210,11 +210,20 @@ fn call_stream_callback(callback: f64, err: f64) { err }; unsafe { - let _ = crate::closure::js_native_call_value(callback, [arg].as_ptr(), 1); + let _ = crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + [arg].as_ptr(), + 1, + ); } } -extern "C" fn node_to_web_readable_pull(closure: *const ClosureHeader, controller: f64) -> f64 { +extern "C" fn node_to_web_readable_pull( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + controller: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -262,14 +271,22 @@ fn settle_foreign_readable_pull(controller: f64, result: f64) { } } -extern "C" fn foreign_readable_pull_fulfilled(closure: *const ClosureHeader, result: f64) -> f64 { +extern "C" fn foreign_readable_pull_fulfilled( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + result: f64, +) -> f64 { if !closure.is_null() { settle_foreign_readable_pull(js_closure_get_capture_f64(closure, 0), result); } f64::from_bits(TAG_UNDEFINED) } -extern "C" fn foreign_readable_pull_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn foreign_readable_pull_rejected( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if !closure.is_null() { if let Some(error) = web_readable_error() { let scope = crate::gc::RuntimeHandleScope::new(); @@ -288,7 +305,11 @@ extern "C" fn foreign_readable_pull_rejected(closure: *const ClosureHeader, reas /// other foreign Readables whose bytes live outside node:stream's hidden chunk /// array. Returning the chained promise keeps the Web-stream pull in flight /// until the foreign source yields, ends, or rejects (#9616). -extern "C" fn foreign_readable_to_web_pull(closure: *const ClosureHeader, controller: f64) -> f64 { +extern "C" fn foreign_readable_to_web_pull( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + controller: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -328,7 +349,11 @@ extern "C" fn foreign_readable_to_web_pull(closure: *const ClosureHeader, contro box_pointer(chained as *const u8) } -extern "C" fn foreign_readable_to_web_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn foreign_readable_to_web_cancel( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return resolved_promise(f64::from_bits(TAG_UNDEFINED)); } @@ -341,7 +366,11 @@ extern "C" fn foreign_readable_to_web_cancel(closure: *const ClosureHeader, reas returned.get_nanbox_f64() } -extern "C" fn node_to_web_readable_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn node_to_web_readable_cancel( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if !closure.is_null() { destroy_stream(js_closure_get_capture_f64(closure, 0), reason); } @@ -408,7 +437,10 @@ fn node_readable_to_web(node_stream: f64) -> Option { })) } -extern "C" fn fallback_web_reader_read(closure: *const ClosureHeader) -> f64 { +extern "C" fn fallback_web_reader_read( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return resolved_promise(build_web_read_result(f64::from_bits(TAG_UNDEFINED), true)); } @@ -424,14 +456,21 @@ extern "C" fn fallback_web_reader_read(closure: *const ClosureHeader) -> f64 { resolved_promise(result) } -extern "C" fn fallback_web_reader_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn fallback_web_reader_cancel( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if !closure.is_null() { destroy_stream(js_closure_get_capture_f64(closure, 0), reason); } resolved_promise(f64::from_bits(TAG_UNDEFINED)) } -extern "C" fn fallback_web_readable_get_reader(closure: *const ClosureHeader) -> f64 { +extern "C" fn fallback_web_readable_get_reader( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -450,7 +489,10 @@ extern "C" fn fallback_web_readable_get_reader(closure: *const ClosureHeader) -> ]) } -extern "C" fn fallback_foreign_reader_read(closure: *const ClosureHeader) -> f64 { +extern "C" fn fallback_foreign_reader_read( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return resolved_promise(build_web_read_result(f64::from_bits(TAG_UNDEFINED), true)); } @@ -459,11 +501,18 @@ extern "C" fn fallback_foreign_reader_read(closure: *const ClosureHeader) -> f64 call_method_no_args(iterator.get_nanbox_f64(), b"next") } -extern "C" fn fallback_foreign_reader_cancel(closure: *const ClosureHeader, reason: f64) -> f64 { - foreign_readable_to_web_cancel(closure, reason) +extern "C" fn fallback_foreign_reader_cancel( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + reason: f64, +) -> f64 { + foreign_readable_to_web_cancel(closure, this, reason) } -extern "C" fn fallback_foreign_get_reader(closure: *const ClosureHeader) -> f64 { +extern "C" fn fallback_foreign_get_reader( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -548,7 +597,11 @@ fn fallback_node_readable_to_web(node_stream: f64) -> f64 { ]) } -extern "C" fn node_to_web_writable_write(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn node_to_web_writable_write( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { if !closure.is_null() { let node_stream = js_closure_get_capture_f64(closure, 0); let _ = write_writable_chunk( @@ -561,7 +614,10 @@ extern "C" fn node_to_web_writable_write(closure: *const ClosureHeader, chunk: f f64::from_bits(TAG_UNDEFINED) } -extern "C" fn node_to_web_writable_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn node_to_web_writable_close( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if !closure.is_null() { let node_stream = js_closure_get_capture_f64(closure, 0); finish_stream_with_args( @@ -574,7 +630,11 @@ extern "C" fn node_to_web_writable_close(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn node_to_web_writable_abort(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn node_to_web_writable_abort( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if !closure.is_null() { destroy_stream(js_closure_get_capture_f64(closure, 0), reason); } @@ -603,7 +663,11 @@ fn node_writable_to_web(node_stream: f64) -> Option { }) } -extern "C" fn fallback_web_writer_write(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn fallback_web_writer_write( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { if !closure.is_null() { let node_stream = js_closure_get_capture_f64(closure, 0); let _ = write_writable_chunk( @@ -616,7 +680,10 @@ extern "C" fn fallback_web_writer_write(closure: *const ClosureHeader, chunk: f6 resolved_promise(f64::from_bits(TAG_UNDEFINED)) } -extern "C" fn fallback_web_writer_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn fallback_web_writer_close( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if !closure.is_null() { finish_stream_with_args( js_closure_get_capture_f64(closure, 0), @@ -628,14 +695,21 @@ extern "C" fn fallback_web_writer_close(closure: *const ClosureHeader) -> f64 { resolved_promise(f64::from_bits(TAG_UNDEFINED)) } -extern "C" fn fallback_web_writer_abort(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn fallback_web_writer_abort( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if !closure.is_null() { destroy_stream(js_closure_get_capture_f64(closure, 0), reason); } resolved_promise(f64::from_bits(TAG_UNDEFINED)) } -extern "C" fn fallback_web_writable_get_writer(closure: *const ClosureHeader) -> f64 { +extern "C" fn fallback_web_writable_get_writer( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -714,7 +788,11 @@ fn install_web_readable_adapter(node_stream: f64, web_stream: f64) -> bool { true } -extern "C" fn web_to_node_readable_read(closure: *const ClosureHeader, _size: f64) -> f64 { +extern "C" fn web_to_node_readable_read( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _size: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -755,6 +833,7 @@ fn pump_web_reader(node_stream: f64, reader: f64) { extern "C" fn web_to_node_readable_read_fulfilled( closure: *const ClosureHeader, + _this: crate::closure::JsThis, result: f64, ) -> f64 { if closure.is_null() { @@ -780,6 +859,7 @@ extern "C" fn web_to_node_readable_read_fulfilled( extern "C" fn web_to_node_readable_read_rejected( closure: *const ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { if !closure.is_null() { @@ -837,6 +917,7 @@ fn install_web_writable_adapter(node_stream: f64, web_stream: f64) -> bool { extern "C" fn web_to_node_writable_write( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, _encoding: f64, callback: f64, @@ -855,7 +936,11 @@ extern "C" fn web_to_node_writable_write( f64::from_bits(TAG_UNDEFINED) } -extern "C" fn web_to_node_writable_final(closure: *const ClosureHeader, callback: f64) -> f64 { +extern "C" fn web_to_node_writable_final( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + callback: f64, +) -> f64 { if closure.is_null() { call_stream_callback(callback, f64::from_bits(TAG_UNDEFINED)); return f64::from_bits(TAG_UNDEFINED); @@ -872,6 +957,7 @@ extern "C" fn web_to_node_writable_final(closure: *const ClosureHeader, callback extern "C" fn web_to_node_writable_destroy( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, callback: f64, ) -> f64 { @@ -908,7 +994,11 @@ fn attach_web_writable_callback(promise: *mut crate::promise::Promise, callback: crate::promise::js_promise_attach_handlers(promise, fulfilled, rejected); } -extern "C" fn web_to_node_writable_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn web_to_node_writable_fulfilled( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _value: f64, +) -> f64 { if !closure.is_null() { call_stream_callback( js_closure_get_capture_f64(closure, 0), @@ -918,7 +1008,11 @@ extern "C" fn web_to_node_writable_fulfilled(closure: *const ClosureHeader, _val f64::from_bits(TAG_UNDEFINED) } -extern "C" fn web_to_node_writable_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn web_to_node_writable_rejected( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if !closure.is_null() { call_stream_callback(js_closure_get_capture_f64(closure, 0), reason); } diff --git a/crates/perry-runtime/src/node_stream_destroy_state.rs b/crates/perry-runtime/src/node_stream_destroy_state.rs index 71a83596b9..584b5c9d80 100644 --- a/crates/perry-runtime/src/node_stream_destroy_state.rs +++ b/crates/perry-runtime/src/node_stream_destroy_state.rs @@ -9,7 +9,10 @@ use super::{ stream_value_from_handle, this_value, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, }; -pub(super) extern "C" fn ns_destroy_error_microtask(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn ns_destroy_error_microtask( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -25,7 +28,11 @@ pub(super) extern "C" fn ns_destroy_error_microtask(closure: *const ClosureHeade f64::from_bits(TAG_UNDEFINED) } -extern "C" fn ns_destroy_option_done(closure: *const ClosureHeader, err: f64) -> f64 { +extern "C" fn ns_destroy_option_done( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -66,20 +73,26 @@ pub(super) fn destroy_stream(stream: f64, err: f64) { err }; let args = [destroy_arg, cb_value]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(destroy, args.as_ptr(), args.len()); + let _ = crate::closure::native_call_value_this( + destroy, + crate::closure::JsThis::from_f64(stream), + args.as_ptr(), + args.len(), + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } } queue_destroy_events(stream, err); } -pub(super) extern "C" fn ns_destroy1(closure: *const ClosureHeader, err: f64) -> f64 { - let stream = this_value(closure); +pub(super) extern "C" fn ns_destroy1( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + err: f64, +) -> f64 { + let stream = this_value(closure, this); destroy_stream(stream, err); stream } diff --git a/crates/perry-runtime/src/node_stream_dispatch.rs b/crates/perry-runtime/src/node_stream_dispatch.rs index bb883745b1..da3ae6ba19 100644 --- a/crates/perry-runtime/src/node_stream_dispatch.rs +++ b/crates/perry-runtime/src/node_stream_dispatch.rs @@ -12,19 +12,19 @@ use crate::value::JSValue; pub(crate) type StubFn = unsafe extern "C" fn(); #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast0(f: extern "C" fn(*const ClosureHeader) -> f64) -> StubFn { +pub(crate) fn cast0(f: crate::closure::body_call::js_body_fn_ty!()) -> StubFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast1(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> StubFn { +pub(crate) fn cast1(f: crate::closure::body_call::js_body_fn_ty!(a)) -> StubFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast2(f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64) -> StubFn { +pub(crate) fn cast2(f: crate::closure::body_call::js_body_fn_ty!(a, a)) -> StubFn { unsafe { std::mem::transmute(f) } } #[allow(clippy::missing_transmute_annotations)] -pub(super) fn cast3(f: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64) -> StubFn { +pub(super) fn cast3(f: crate::closure::body_call::js_body_fn_ty!(a, a, a)) -> StubFn { unsafe { std::mem::transmute(f) } } @@ -309,8 +309,9 @@ fn event_emitter_async_resource_backing(receiver: f64) -> Option EventEmitterAsyncResourceBacking { - if let Some(backing) = event_emitter_async_resource_backing(this_value(closure)) { + if let Some(backing) = event_emitter_async_resource_backing(this_value(closure, this)) { return backing; } crate::node_submodules::diagnostics::throw_type_error_no_code( @@ -320,10 +321,11 @@ fn require_event_emitter_async_resource_receiver( extern "C" fn ns_ee_async_resource_emit_rest( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, rest: f64, ) -> f64 { - let backing = require_event_emitter_async_resource_receiver(closure); + let backing = require_event_emitter_async_resource_receiver(closure, this); let runtime_async_id = match backing { EventEmitterAsyncResourceBacking::RuntimeResource(resource) => { crate::async_hooks::js_async_resource_async_id(resource) as u64 @@ -333,15 +335,18 @@ extern "C" fn ns_ee_async_resource_emit_rest( if runtime_async_id != 0 { crate::async_hooks::js_async_hooks_provider_enter(runtime_async_id); } - let result = ns_emit_rest(closure, event, rest); + let result = ns_emit_rest(closure, this, event, rest); if runtime_async_id != 0 { crate::async_hooks::js_async_hooks_provider_leave(runtime_async_id); } result } -extern "C" fn ns_ee_async_resource_destroy(closure: *const ClosureHeader) -> f64 { - match require_event_emitter_async_resource_receiver(closure) { +extern "C" fn ns_ee_async_resource_destroy( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + match require_event_emitter_async_resource_receiver(closure, this) { EventEmitterAsyncResourceBacking::ExternalEmitter(handle) => { crate::object::event_emitter_async_resource_dispatch() .map(|dispatch| unsafe { dispatch(handle, 3) }) @@ -353,9 +358,12 @@ extern "C" fn ns_ee_async_resource_destroy(closure: *const ClosureHeader) -> f64 } } -extern "C" fn ns_ee_async_resource_getter(closure: *const ClosureHeader) -> f64 { +extern "C" fn ns_ee_async_resource_getter( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let operation = crate::closure::js_closure_get_capture_ptr(closure, 1) as u32; - match require_event_emitter_async_resource_receiver(closure) { + match require_event_emitter_async_resource_receiver(closure, this) { EventEmitterAsyncResourceBacking::ExternalEmitter(handle) => { crate::object::event_emitter_async_resource_dispatch() .map(|dispatch| unsafe { dispatch(handle, operation) }) diff --git a/crates/perry-runtime/src/node_stream_event_emitter.rs b/crates/perry-runtime/src/node_stream_event_emitter.rs index 4aababf4f1..7484c91fe4 100644 --- a/crates/perry-runtime/src/node_stream_event_emitter.rs +++ b/crates/perry-runtime/src/node_stream_event_emitter.rs @@ -7,8 +7,12 @@ const STREAM_EVENT_NAMES_KEY: &[u8] = b"__perryStreamEventNames"; const STREAM_LISTENERS_PREFIX: &[u8] = b"__perryStreamListeners:"; const STREAM_ONCE_PREFIX: &[u8] = b"__perryStreamOnce:"; -pub(super) extern "C" fn ns_set_max_listeners(closure: *const ClosureHeader, value: f64) -> f64 { - set_stream_max_listeners(super::this_value(closure), value) +pub(super) extern "C" fn ns_set_max_listeners( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + set_stream_max_listeners(super::this_value(closure, this), value) } #[no_mangle] @@ -73,8 +77,11 @@ pub(crate) fn validate_max_listeners(value: f64) -> f64 { n } -pub(super) extern "C" fn ns_get_max_listeners(closure: *const ClosureHeader) -> f64 { - stream_max_listeners(super::this_value(closure)) +pub(super) extern "C" fn ns_get_max_listeners( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + stream_max_listeners(super::this_value(closure, this)) } #[no_mangle] @@ -86,57 +93,76 @@ fn stream_max_listeners(stream: f64) -> f64 { super::get_hidden_value(stream, super::hidden_max_listeners_key()).unwrap_or(10.0) } -pub(super) extern "C" fn ns_on2(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { - let stream = super::this_value(closure); +pub(super) extern "C" fn ns_on2( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + cb: f64, +) -> f64 { + let stream = super::this_value(closure, this); add_stream_listener_for_event(stream, event, cb); stream } -pub(super) extern "C" fn ns_once2(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { - let stream = super::this_value(closure); +pub(super) extern "C" fn ns_once2( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + cb: f64, +) -> f64 { + let stream = super::this_value(closure, this); add_stream_listener_for_event_with_options(stream, event, cb, true, false); stream } pub(super) extern "C" fn ns_prepend_listener2( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { - let stream = super::this_value(closure); + let stream = super::this_value(closure, this); add_stream_listener_for_event_with_options(stream, event, cb, false, true); stream } pub(super) extern "C" fn ns_prepend_once_listener2( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { - let stream = super::this_value(closure); + let stream = super::this_value(closure, this); add_stream_listener_for_event_with_options(stream, event, cb, true, true); stream } pub(super) extern "C" fn ns_remove_listener2( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, cb: f64, ) -> f64 { - let stream = super::this_value(closure); + let stream = super::this_value(closure, this); remove_stream_listener_for_event(stream, event, cb); stream } -pub(super) extern "C" fn ns_off2(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 { - ns_remove_listener2(closure, event, cb) +pub(super) extern "C" fn ns_off2( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, + cb: f64, +) -> f64 { + ns_remove_listener2(closure, this, event, cb) } pub(super) extern "C" fn ns_remove_all_listeners1( closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, ) -> f64 { - let stream = super::this_value(closure); + let stream = super::this_value(closure, this); remove_all_stream_listeners_for_event(stream, event); stream } @@ -215,8 +241,12 @@ pub extern "C" fn js_node_stream_method_remove_all_listeners( stream } -pub(super) extern "C" fn ns_listener_count(closure: *const ClosureHeader, event: f64) -> f64 { - stream_listener_count_for_event(super::this_value(closure), event) as f64 +pub(super) extern "C" fn ns_listener_count( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, +) -> f64 { + stream_listener_count_for_event(super::this_value(closure, this), event) as f64 } #[no_mangle] @@ -228,8 +258,11 @@ pub extern "C" fn js_node_stream_method_listener_count(stream_handle: i64, event stream_listener_count_for_event(stream, event) as f64 } -pub(super) extern "C" fn ns_event_names(closure: *const ClosureHeader) -> f64 { - let stream = super::this_value(closure); +pub(super) extern "C" fn ns_event_names( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let stream = super::this_value(closure, this); f64::from_bits(JSValue::pointer(stream_event_names_array(stream) as *const u8).bits()) } @@ -238,16 +271,24 @@ pub extern "C" fn js_node_stream_method_event_names(stream_handle: i64) -> i64 { stream_event_names_array(super::stream_value_from_handle(stream_handle)) as i64 } -pub(super) extern "C" fn ns_listeners(closure: *const ClosureHeader, event: f64) -> f64 { - let stream = super::this_value(closure); +pub(super) extern "C" fn ns_listeners( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, +) -> f64 { + let stream = super::this_value(closure, this); f64::from_bits( JSValue::pointer(stream_listeners_array_for_event(stream, event, false) as *const u8) .bits(), ) } -pub(super) extern "C" fn ns_raw_listeners(closure: *const ClosureHeader, event: f64) -> f64 { - let stream = super::this_value(closure); +pub(super) extern "C" fn ns_raw_listeners( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + event: f64, +) -> f64 { + let stream = super::this_value(closure, this); f64::from_bits( JSValue::pointer(stream_listeners_array_for_event(stream, event, true) as *const u8).bits(), ) @@ -670,15 +711,21 @@ pub(super) fn call_listener_args(stream: f64, listener: f64, args: &[f64]) -> f6 if !is_callable_value(listener) { return f64::from_bits(super::TAG_UNDEFINED); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); - let result = - unsafe { crate::closure::js_native_call_value(listener, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + listener, + crate::closure::JsThis::from_f64(stream), + args.as_ptr(), + args.len(), + ) + } } -pub(super) extern "C" fn ns_capture_rejection(closure: *const ClosureHeader, reason: f64) -> f64 { +pub(super) extern "C" fn ns_capture_rejection( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(super::TAG_UNDEFINED); } diff --git a/crates/perry-runtime/src/node_stream_iter_helpers.rs b/crates/perry-runtime/src/node_stream_iter_helpers.rs index d6d81e3a92..850740474c 100644 --- a/crates/perry-runtime/src/node_stream_iter_helpers.rs +++ b/crates/perry-runtime/src/node_stream_iter_helpers.rs @@ -9,7 +9,10 @@ use crate::closure::{ use crate::object::{js_object_get_field_by_name_f64, js_object_set_field_by_name, ObjectHeader}; use crate::value::JSValue; -pub(super) extern "C" fn ns_undefined0(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn ns_undefined0( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(TAG_UNDEFINED) } @@ -142,7 +145,10 @@ pub(super) fn promise_from_capture( crate::value::js_nanbox_get_pointer(f64::from_bits(bits)) as *mut crate::promise::Promise } -pub(super) extern "C" fn ns_stream_abort_listener(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn ns_stream_abort_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -239,7 +245,11 @@ pub(super) fn settle_result(value: f64) -> Result { /// Invoke a single-argument stream callback and settle an async result. #[inline] pub(super) fn call_settled_result(cb: *const ClosureHeader, arg: f64) -> Result { - settle_result(crate::closure::js_closure_call1(cb, arg)) + settle_result(crate::closure::js_closure_call1( + cb, + crate::closure::plain_call_receiver(), + arg, + )) } /// Coerce a `take(n)` / `drop(n)` count argument to a clamped element @@ -270,8 +280,12 @@ pub(super) fn extend_with_array( out } -pub(super) extern "C" fn ns_iter_to_array(closure: *const ClosureHeader, opts: f64) -> f64 { - let this = this_value(closure); +pub(super) extern "C" fn ns_iter_to_array( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + opts: f64, +) -> f64 { + let this = this_value(closure, this); // An already-errored stream rejects immediately (matches the error-first // check `settle_consuming` used). if let Some(err) = readable_hidden_error(this) { @@ -321,7 +335,10 @@ fn register_to_array_abort(stream: f64, opts: f64, result: f64) { /// Abort-listener body for `toArray`: cancel the live stream (terminating the /// internal `from_async` iterator that drives it) and reject the result with an /// AbortError. A no-op if the result already settled. -pub(super) extern "C" fn ns_to_array_abort(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn ns_to_array_abort( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -334,8 +351,13 @@ pub(super) extern "C" fn ns_to_array_abort(closure: *const ClosureHeader) -> f64 f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn ns_iter_map(closure: *const ClosureHeader, mapper: f64, opts: f64) -> f64 { - let this = this_value(closure); +pub(super) extern "C" fn ns_iter_map( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + mapper: f64, + opts: f64, +) -> f64 { + let this = this_value(closure, this); prepare_readable_for_iteration(this); let arr = readable_chunks_array(this); let cb = callback_closure(mapper); @@ -364,10 +386,11 @@ pub(super) extern "C" fn ns_iter_map(closure: *const ClosureHeader, mapper: f64, pub(super) extern "C" fn ns_iter_filter( closure: *const ClosureHeader, + this: crate::closure::JsThis, predicate: f64, opts: f64, ) -> f64 { - let this = this_value(closure); + let this = this_value(closure, this); prepare_readable_for_iteration(this); let arr = readable_chunks_array(this); let cb = callback_closure(predicate); @@ -530,7 +553,11 @@ fn consume_finalize(state: *const ClosureHeader) { /// `.then` fulfilment for `iterator.next()`: read the iter-result, then /// either finalize (done) or invoke the user callback and await its result. -extern "C" fn consume_on_next(state: *const ClosureHeader, iter_result: f64) -> f64 { +extern "C" fn consume_on_next( + state: *const ClosureHeader, + _this: crate::closure::JsThis, + iter_result: f64, +) -> f64 { if state.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -561,9 +588,13 @@ extern "C" fn consume_on_next(state: *const ClosureHeader, iter_result: f64) -> return f64::from_bits(TAG_UNDEFINED); } let acc = js_closure_get_capture_f64(state, SC_ACC); - catch_pipeline_throw(|| crate::closure::js_closure_call2(cb, acc, value)) + catch_pipeline_throw(|| { + crate::closure::js_closure_call2(cb, crate::closure::plain_call_receiver(), acc, value) + }) } else { - catch_pipeline_throw(|| crate::closure::js_closure_call1(cb, value)) + catch_pipeline_throw(|| { + crate::closure::js_closure_call1(cb, crate::closure::plain_call_receiver(), value) + }) }; match cb_result { @@ -584,7 +615,11 @@ extern "C" fn consume_on_next(state: *const ClosureHeader, iter_result: f64) -> /// `.then` fulfilment for the awaited callback result: update the /// accumulator / short-circuit, then pull the next element. -extern "C" fn consume_on_cb(closure: *const ClosureHeader, cb_result: f64) -> f64 { +extern "C" fn consume_on_cb( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + cb_result: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -618,7 +653,11 @@ extern "C" fn consume_on_cb(closure: *const ClosureHeader, cb_result: f64) -> f6 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn consume_reject(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn consume_reject( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -719,7 +758,10 @@ fn register_consume_abort(stream: f64, opts: f64, state: *const ClosureHeader) { /// Abort-listener body for a consuming helper (`forEach`/`reduce`/`find`/ /// `some`/`every`): close the iterator and reject the result with an /// AbortError. A no-op if the result already settled. -pub(super) extern "C" fn ns_consume_abort(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn ns_consume_abort( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -733,12 +775,13 @@ pub(super) extern "C" fn ns_consume_abort(closure: *const ClosureHeader) -> f64 pub(super) extern "C" fn ns_iter_reduce( closure: *const ClosureHeader, + this: crate::closure::JsThis, reducer: f64, initial: f64, opts: f64, ) -> f64 { consume_stream( - this_value(closure), + this_value(closure, this), reducer, CONSUME_OP_REDUCE, initial, @@ -748,11 +791,12 @@ pub(super) extern "C" fn ns_iter_reduce( pub(super) extern "C" fn ns_iter_for_each( closure: *const ClosureHeader, + this: crate::closure::JsThis, action: f64, opts: f64, ) -> f64 { consume_stream( - this_value(closure), + this_value(closure, this), action, CONSUME_OP_FOR_EACH, f64::from_bits(TAG_UNDEFINED), @@ -762,11 +806,12 @@ pub(super) extern "C" fn ns_iter_for_each( pub(super) extern "C" fn ns_iter_find( closure: *const ClosureHeader, + this: crate::closure::JsThis, predicate: f64, opts: f64, ) -> f64 { consume_stream( - this_value(closure), + this_value(closure, this), predicate, CONSUME_OP_FIND, f64::from_bits(TAG_UNDEFINED), @@ -776,11 +821,12 @@ pub(super) extern "C" fn ns_iter_find( pub(super) extern "C" fn ns_iter_some( closure: *const ClosureHeader, + this: crate::closure::JsThis, predicate: f64, opts: f64, ) -> f64 { consume_stream( - this_value(closure), + this_value(closure, this), predicate, CONSUME_OP_SOME, f64::from_bits(TAG_UNDEFINED), @@ -790,11 +836,12 @@ pub(super) extern "C" fn ns_iter_some( pub(super) extern "C" fn ns_iter_every( closure: *const ClosureHeader, + this: crate::closure::JsThis, predicate: f64, opts: f64, ) -> f64 { consume_stream( - this_value(closure), + this_value(closure, this), predicate, CONSUME_OP_EVERY, f64::from_bits(TAG_UNDEFINED), @@ -804,10 +851,11 @@ pub(super) extern "C" fn ns_iter_every( pub(super) extern "C" fn ns_iter_flat_map( closure: *const ClosureHeader, + this: crate::closure::JsThis, mapper: f64, opts: f64, ) -> f64 { - let this = this_value(closure); + let this = this_value(closure, this); prepare_readable_for_iteration(this); let arr = readable_chunks_array(this); let cb = callback_closure(mapper); @@ -981,6 +1029,7 @@ fn take_source_fulfilled(iterator: f64, result: f64) -> f64 { pub(super) extern "C" fn ns_take_source_fulfilled( closure: *const ClosureHeader, + _this: crate::closure::JsThis, result: f64, ) -> f64 { take_source_fulfilled(js_closure_get_capture_f64(closure, 0), result) @@ -988,6 +1037,7 @@ pub(super) extern "C" fn ns_take_source_fulfilled( pub(super) extern "C" fn ns_take_source_rejected( closure: *const ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); @@ -1002,6 +1052,7 @@ pub(super) extern "C" fn ns_take_source_rejected( pub(super) extern "C" fn ns_take_limit_fulfilled( closure: *const ClosureHeader, + _this: crate::closure::JsThis, result: f64, ) -> f64 { let iterator = js_closure_get_capture_f64(closure, 0); @@ -1012,15 +1063,19 @@ pub(super) extern "C" fn ns_take_limit_fulfilled( pub(super) extern "C" fn ns_take_limit_rejected( closure: *const ClosureHeader, + _this: crate::closure::JsThis, _reason: f64, ) -> f64 { finish_take_source(js_closure_get_capture_f64(closure, 0), None, true); take_source_done_result() } -pub(super) extern "C" fn ns_take_source_next(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn ns_take_source_next( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let iterator = scope.root_nanbox_f64(this_value(closure)); + let iterator = scope.root_nanbox_f64(this_value(closure, this)); if has_truthy_hidden(iterator.get_nanbox_f64(), hidden_key(TAKE_SOURCE_DONE_KEY)) { return take_source_done_result(); } @@ -1111,8 +1166,11 @@ pub(super) extern "C" fn ns_take_source_next(closure: *const ClosureHeader) -> f ) as *const u8) } -pub(super) extern "C" fn ns_take_source_return(closure: *const ClosureHeader) -> f64 { - finish_take_source(this_value(closure), None, true); +pub(super) extern "C" fn ns_take_source_return( + closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + finish_take_source(this_value(closure, this), None, true); take_source_done_result() } @@ -1143,9 +1201,13 @@ fn take_source_iterator(source: f64, result: f64, count: u32) -> f64 { iterator } -pub(super) extern "C" fn ns_iter_take(closure: *const ClosureHeader, count: f64) -> f64 { +pub(super) extern "C" fn ns_iter_take( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + count: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let this = scope.root_nanbox_f64(this_value(closure)); + let this = scope.root_nanbox_f64(this_value(closure, this)); if !readable_chunks_nonempty(this.get_nanbox_f64()) { if get_hidden_value( this.get_nanbox_f64(), @@ -1188,8 +1250,12 @@ pub(super) extern "C" fn ns_iter_take(closure: *const ClosureHeader, count: f64) result } -pub(super) extern "C" fn ns_iter_drop(closure: *const ClosureHeader, count: f64) -> f64 { - let this = this_value(closure); +pub(super) extern "C" fn ns_iter_drop( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + count: f64, +) -> f64 { + let this = this_value(closure, this); prepare_readable_for_iteration(this); let arr = readable_chunks_array(this); let mut out = crate::array::js_array_alloc(0); @@ -1208,8 +1274,8 @@ pub(super) extern "C" fn ns_iter_drop(closure: *const ClosureHeader, count: f64) mod take_tests { use super::*; - extern "C" fn source_next(closure: *const ClosureHeader) -> f64 { - let source = this_value(closure); + extern "C" fn source_next(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + let source = this_value(closure, this); let value = get_hidden_value(source, hidden_key(b"count")).unwrap_or(0.0) + 1.0; set_hidden_value(source, hidden_key(b"count"), value); if value > 2.0 && has_truthy_hidden(source, hidden_key(b"failAfterLimit")) { @@ -1230,8 +1296,11 @@ mod take_tests { box_pointer(result as *const u8) } - extern "C" fn source_return(closure: *const ClosureHeader) -> f64 { - let source = this_value(closure); + extern "C" fn source_return( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { + let source = this_value(closure, this); set_hidden_value(source, hidden_key(b"returned"), f64::from_bits(TAG_TRUE)); f64::from_bits(TAG_UNDEFINED) } @@ -1262,7 +1331,7 @@ mod take_tests { let take = js_closure_alloc(ns_iter_take as *const u8, 1); js_closure_set_capture_ptr(take, 0, stream.to_bits() as i64); - let result = ns_iter_take(take, 2.0); + let result = ns_iter_take(take, crate::closure::JsThis::UNDEFINED, 2.0); let iterator = get_hidden_value(result, hidden_key(READABLE_SOURCE_ITERATOR_KEY)).unwrap(); assert!(!has_truthy_hidden(source, hidden_key(b"returned"))); @@ -1290,7 +1359,7 @@ mod take_tests { let take = js_closure_alloc(ns_iter_take as *const u8, 1); js_closure_set_capture_ptr(take, 0, stream.to_bits() as i64); - let result = ns_iter_take(take, 2.0); + let result = ns_iter_take(take, crate::closure::JsThis::UNDEFINED, 2.0); let iterator = get_hidden_value(result, hidden_key(READABLE_SOURCE_ITERATOR_KEY)).unwrap(); assert!(!stream_destroyed(stream)); @@ -1318,7 +1387,7 @@ mod take_tests { let take = js_closure_alloc(ns_iter_take as *const u8, 1); js_closure_set_capture_ptr(take, 0, stream.to_bits() as i64); - let result = ns_iter_take(take, 2.0); + let result = ns_iter_take(take, crate::closure::JsThis::UNDEFINED, 2.0); assert_eq!(readable_hidden_error(result), Some(7.0)); assert_eq!(get_hidden_value(result, hidden_signal_key()), Some(8.0)); diff --git a/crates/perry-runtime/src/node_stream_pipeline.rs b/crates/perry-runtime/src/node_stream_pipeline.rs index b028ceacb0..2beaf7d020 100644 --- a/crates/perry-runtime/src/node_stream_pipeline.rs +++ b/crates/perry-runtime/src/node_stream_pipeline.rs @@ -15,7 +15,10 @@ pub(super) struct PipelineOptions { pub(super) signal: Option, } -pub(super) extern "C" fn pipeline_success_callback(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn pipeline_success_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -30,7 +33,11 @@ pub(super) extern "C" fn pipeline_success_callback(closure: *const ClosureHeader f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn pipeline_error_callback(closure: *const ClosureHeader, err: f64) -> f64 { +pub(super) extern "C" fn pipeline_error_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -47,7 +54,10 @@ pub(super) extern "C" fn pipeline_error_callback(closure: *const ClosureHeader, f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn pipeline_close_callback(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn pipeline_close_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -480,7 +490,12 @@ pub(super) fn call_pipeline_function_stage( } let args = [source.get_nanbox_f64()]; let result = catch_pipeline_throw(|| unsafe { - crate::closure::js_native_call_value(stage.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + stage.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) })?; settle_pipeline_value_with_origin(result) } @@ -527,7 +542,11 @@ pub(super) fn fail_collected_pipeline(stages: &[f64], callback: f64, err: f64) { } } -extern "C" fn collected_pipeline_error_noop(_closure: *const ClosureHeader, _err: f64) -> f64 { +extern "C" fn collected_pipeline_error_noop( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _err: f64, +) -> f64 { f64::from_bits(TAG_UNDEFINED) } @@ -890,6 +909,7 @@ fn fail_composed_duplex(composite: f64, source: f64, stages: f64, err: f64) { pub(super) extern "C" fn compose_stage_error_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, ) -> f64 { if closure.is_null() { @@ -904,6 +924,7 @@ pub(super) extern "C" fn compose_stage_error_callback( pub(super) extern "C" fn compose_source_data_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, ) -> f64 { if closure.is_null() { @@ -922,7 +943,10 @@ pub(super) extern "C" fn compose_source_data_callback( f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn compose_source_end_callback(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn compose_source_end_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -940,6 +964,7 @@ pub(super) extern "C" fn compose_source_end_callback(closure: *const ClosureHead pub(super) extern "C" fn compose_source_error_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, ) -> f64 { if closure.is_null() { @@ -954,6 +979,7 @@ pub(super) extern "C" fn compose_source_error_callback( pub(super) extern "C" fn compose_duplex_write_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, _encoding: f64, cb: f64, @@ -979,6 +1005,7 @@ pub(super) extern "C" fn compose_duplex_write_callback( pub(super) extern "C" fn compose_duplex_final_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, cb: f64, ) -> f64 { if closure.is_null() { @@ -1215,7 +1242,6 @@ fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f hidden_key(b"__perryStreamComposePriming"), f64::from_bits(TAG_TRUE), ); - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let primed = catch_pipeline_throw(|| { prime_composed_duplex_from_source( composite.get_nanbox_f64(), @@ -1224,7 +1250,6 @@ fn new_composed_duplex(stages: &[f64], source: Option, writable: bool) -> f ); f64::from_bits(TAG_UNDEFINED) }); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); if let Err(err) = primed { let err = scope.root_nanbox_f64(err); fail_composed_duplex( diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index c4bfa7ff0e..9964b85786 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -707,13 +707,14 @@ pub(super) fn schedule_writable_finish(stream: f64, callback: Option) { callback.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)), ); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = - crate::closure::js_native_call_value(final_callback, [cb_value].as_ptr(), 1); + let _ = crate::closure::native_call_value_this( + final_callback, + crate::closure::JsThis::from_f64(stream), + [cb_value].as_ptr(), + 1, + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } } @@ -1034,12 +1035,14 @@ pub(super) fn finish_transform_stream(stream: f64, callback: Option) -> boo callback.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)), ); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(flush, [cb_value].as_ptr(), 1); + let _ = crate::closure::native_call_value_this( + flush, + crate::closure::JsThis::from_f64(stream), + [cb_value].as_ptr(), + 1, + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); true } @@ -1311,12 +1314,14 @@ pub(super) fn invoke_construct_callback(stream: f64, opts: f64) { let cb = js_closure_alloc(ns_construct_callback_done as *const u8, 1); js_closure_set_capture_f64(cb, 0, stream); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(construct, [cb_value].as_ptr(), 1); + let _ = crate::closure::native_call_value_this( + construct, + crate::closure::JsThis::from_f64(stream), + [cb_value].as_ptr(), + 1, + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } pub(super) fn invoke_read_once(stream: f64) { @@ -1346,12 +1351,14 @@ fn invoke_read_once_inner(stream: f64, emit_default_error: bool) { } set_hidden_value(stream, hidden_read_invoked_key(), f64::from_bits(TAG_TRUE)); let size = get_hidden_value(stream, hidden_hwm_key()).unwrap_or_else(|| default_hwm(false)); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { - let _ = crate::closure::js_native_call_value(read, [size].as_ptr(), 1); + let _ = crate::closure::native_call_value_this( + read, + crate::closure::JsThis::from_f64(stream), + [size].as_ptr(), + 1, + ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } pub(super) fn maybe_emit_default_read_error(stream: f64) { diff --git a/crates/perry-runtime/src/node_stream_state_view.rs b/crates/perry-runtime/src/node_stream_state_view.rs index d3aeb2bc94..47298536b3 100644 --- a/crates/perry-runtime/src/node_stream_state_view.rs +++ b/crates/perry-runtime/src/node_stream_state_view.rs @@ -121,8 +121,8 @@ fn bool_bits(value: bool) -> f64 { } /// The stream a state view belongs to, from the accessor's receiver. -fn view_owner() -> Option { - let this = crate::object::js_implicit_this_get(); +fn view_owner(this: crate::closure::JsThis) -> Option { + let this = this.as_f64(); get_hidden_value(this, hidden_key(STREAM_STATE_OWNER_KEY)) } @@ -232,11 +232,11 @@ fn captured_field(closure: *const ClosureHeader) -> Option<(usize, &'static str) field_name(id as usize) } -extern "C" fn stream_state_get(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_state_get(closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { let Some((kind, field)) = captured_field(closure) else { return f64::from_bits(TAG_UNDEFINED); }; - let Some(stream) = view_owner() else { + let Some(stream) = view_owner(this) else { return f64::from_bits(TAG_UNDEFINED); }; let scope = crate::gc::RuntimeHandleScope::new(); @@ -248,12 +248,16 @@ extern "C" fn stream_state_get(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn stream_state_set(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn stream_state_set( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { let Some((_, field)) = captured_field(closure) else { return f64::from_bits(TAG_UNDEFINED); }; if field == "dataEmitted" { - if let Some(stream) = view_owner() { + if let Some(stream) = view_owner(this) { let scope = crate::gc::RuntimeHandleScope::new(); let stream = scope.root_nanbox_f64(stream); let emitted = crate::value::js_is_truthy(value) != 0; diff --git a/crates/perry-runtime/src/node_stream_tests.rs b/crates/perry-runtime/src/node_stream_tests.rs index 69b11548f4..88f18fb97d 100644 --- a/crates/perry-runtime/src/node_stream_tests.rs +++ b/crates/perry-runtime/src/node_stream_tests.rs @@ -35,7 +35,10 @@ fn catches_runtime_throw(f: impl FnOnce()) -> bool { crate::exception::catch_js_throw(f).is_err() } -extern "C" fn capture_finished_callback(_closure: *const ClosureHeader) -> f64 { +extern "C" fn capture_finished_callback( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { FINISHED_CALLBACK_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } @@ -113,7 +116,11 @@ fn bare_writable_and_transform_reject_missing_methods() { })); } -extern "C" fn capture_uncaught_stream_error(_closure: *const ClosureHeader, _error: f64) -> f64 { +extern "C" fn capture_uncaught_stream_error( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _error: f64, +) -> f64 { UNCAUGHT_STREAM_ERROR_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } @@ -167,7 +174,11 @@ fn pipe_cleanup_does_not_swallow_destination_error() { crate::os::test_clear_process_event_listeners(); } -extern "C" fn return_pipeline_source(_closure: *const ClosureHeader, source: f64) -> f64 { +extern "C" fn return_pipeline_source( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + source: f64, +) -> f64 { source } @@ -212,6 +223,7 @@ fn string_contents(value: f64) -> String { pub(super) extern "C" fn write_capture( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, _enc: f64, cb: f64, @@ -220,13 +232,19 @@ pub(super) extern "C" fn write_capture( let bytes = js_node_stream_collect_bytes(readable); WRITE_CAPTURED.with(|captured| captured.borrow_mut().push(bytes)); unsafe { - let _ = crate::closure::js_native_call_value(cb, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } f64::from_bits(TAG_UNDEFINED) } pub(super) extern "C" fn write_capture_pending( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, _enc: f64, cb: f64, @@ -240,6 +258,7 @@ pub(super) extern "C" fn write_capture_pending( extern "C" fn write_capture_encoding( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, enc: f64, cb: f64, @@ -254,25 +273,41 @@ extern "C" fn write_capture_encoding( .push(JSValue::from_bits(chunk.to_bits()).is_any_string()) }); unsafe { - let _ = crate::closure::js_native_call_value(cb, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } f64::from_bits(TAG_UNDEFINED) } extern "C" fn write_callback_error( closure: *const ClosureHeader, + _this: crate::closure::JsThis, _chunk: f64, _enc: f64, cb: f64, ) -> f64 { let err = crate::closure::js_closure_get_capture_f64(closure, 0); unsafe { - let _ = crate::closure::js_native_call_value(cb, [err].as_ptr(), 1); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + [err].as_ptr(), + 1, + ); } f64::from_bits(TAG_UNDEFINED) } -extern "C" fn writev_capture(_closure: *const ClosureHeader, chunks: f64, cb: f64) -> f64 { +extern "C" fn writev_capture( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunks: f64, + cb: f64, +) -> f64 { let chunks = raw_ptr_from_value(chunks) as *const crate::array::ArrayHeader; let len = crate::array::js_array_length(chunks); for i in 0..len { @@ -294,23 +329,38 @@ extern "C" fn writev_capture(_closure: *const ClosureHeader, chunks: f64, cb: f6 }); } unsafe { - let _ = crate::closure::js_native_call_value(cb, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } f64::from_bits(TAG_UNDEFINED) } -extern "C" fn capture_write_callback(_closure: *const ClosureHeader) -> f64 { +extern "C" fn capture_write_callback( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { WRITE_CALLBACK_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn noop_listener(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn noop_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_data_listener(closure: *const ClosureHeader, chunk: f64) -> f64 { +pub(super) extern "C" fn capture_data_listener( + closure: *const ClosureHeader, + receiver: crate::closure::JsThis, + chunk: f64, +) -> f64 { let expected = crate::closure::js_closure_get_capture_f64(closure, 0); - let actual = crate::object::js_implicit_this_get(); + let actual = receiver.as_f64(); READABLE_THIS_MATCHES.with(|matches| { matches .borrow_mut() @@ -325,7 +375,11 @@ pub(super) extern "C" fn capture_data_listener(closure: *const ClosureHeader, ch f64::from_bits(TAG_UNDEFINED) } -extern "C" fn capture_data_text_listener(_closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn capture_data_text_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { READABLE_DATA_STRING_FLAGS.with(|flags| { flags .borrow_mut() @@ -335,7 +389,10 @@ extern "C" fn capture_data_text_listener(_closure: *const ClosureHeader, chunk: f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_readable_listener(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_readable_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let stream = crate::closure::js_closure_get_capture_f64(closure, 0); let got = js_node_stream_method_read(raw_ptr_from_value(stream) as i64, f64::NAN); READABLE_READ_CAPTURED.with(|captured| { @@ -349,9 +406,12 @@ pub(super) extern "C" fn capture_readable_listener(closure: *const ClosureHeader f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_end_listener(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_end_listener( + closure: *const ClosureHeader, + receiver: crate::closure::JsThis, +) -> f64 { let expected = crate::closure::js_closure_get_capture_f64(closure, 0); - let actual = crate::object::js_implicit_this_get(); + let actual = receiver.as_f64(); READABLE_THIS_MATCHES.with(|matches| { matches .borrow_mut() @@ -409,13 +469,18 @@ fn readable_set_encoding_read_returns_decoded_string() { assert_eq!(string_contents(got), "abcd"); } -extern "C" fn capture_error_listener(_closure: *const ClosureHeader, _err: f64) -> f64 { +extern "C" fn capture_error_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _err: f64, +) -> f64 { ERROR_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } pub(super) extern "C" fn capture_expected_arg_listener( closure: *const ClosureHeader, + _this: crate::closure::JsThis, arg: f64, ) -> f64 { let expected = crate::closure::js_closure_get_capture_f64(closure, 0); @@ -427,27 +492,42 @@ pub(super) extern "C" fn capture_expected_arg_listener( f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_pause_listener(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_pause_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { STREAM_EVENT_ORDER.with(|events| events.borrow_mut().push(b'P')); f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_resume_listener(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_resume_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { STREAM_EVENT_ORDER.with(|events| events.borrow_mut().push(b'R')); f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_finish_listener(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_finish_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { WRITABLE_FINISH_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_close_listener(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_close_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { WRITABLE_CLOSE_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_end_callback_state(closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_end_callback_state( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let stream = crate::closure::js_closure_get_capture_f64(closure, 0); let handle = raw_ptr_from_value(stream) as i64; let finish_count = WRITABLE_FINISH_COUNT.with(|count| *count.borrow()); @@ -459,28 +539,39 @@ pub(super) extern "C" fn capture_end_callback_state(closure: *const ClosureHeade f64::from_bits(TAG_UNDEFINED) } -pub(super) extern "C" fn capture_drain_listener(_closure: *const ClosureHeader) -> f64 { +pub(super) extern "C" fn capture_drain_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { WRITABLE_DRAIN_COUNT.with(|count| *count.borrow_mut() += 1); f64::from_bits(TAG_UNDEFINED) } -extern "C" fn read_records_this(closure: *const ClosureHeader) -> f64 { +extern "C" fn read_records_this( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let stream = crate::closure::js_closure_get_capture_f64(closure, 0); set_hidden_value(stream, hidden_error_key(), string_value("from-read")); f64::from_bits(TAG_UNDEFINED) } -extern "C" fn read_throws(closure: *const ClosureHeader, _size: f64) -> f64 { +extern "C" fn read_throws( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _size: f64, +) -> f64 { crate::exception::js_throw(crate::closure::js_closure_get_capture_f64(closure, 0)) } extern "C" fn transform_upper_callback( _closure: *const ClosureHeader, + receiver: crate::closure::JsThis, chunk: f64, _enc: f64, cb: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = receiver.as_f64(); TRANSFORM_THIS_HAS_STREAM_STATE.with(|matches| { matches.borrow_mut().push( get_hidden_value(this, hidden_readable_flag_key()).is_some() @@ -492,59 +583,87 @@ extern "C" fn transform_upper_callback( let upper = String::from_utf8(bytes).unwrap().to_uppercase(); let args = [f64::from_bits(TAG_UNDEFINED), string_value(&upper)]; unsafe { - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } f64::from_bits(TAG_UNDEFINED) } extern "C" fn transform_identity_callback( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, _enc: f64, cb: f64, ) -> f64 { let args = [f64::from_bits(TAG_UNDEFINED), chunk]; unsafe { - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } f64::from_bits(TAG_UNDEFINED) } extern "C" fn transform_error_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, _chunk: f64, _enc: f64, cb: f64, ) -> f64 { let err = crate::closure::js_closure_get_capture_f64(closure, 0); unsafe { - let _ = crate::closure::js_native_call_value(cb, [err].as_ptr(), 1); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + [err].as_ptr(), + 1, + ); } f64::from_bits(TAG_UNDEFINED) } extern "C" fn transform_push_pair_callback( _closure: *const ClosureHeader, + receiver: crate::closure::JsThis, _chunk: f64, _enc: f64, cb: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = receiver.as_f64(); let push = js_object_get_field_by_name_f64( raw_ptr_from_value(this) as *const ObjectHeader, hidden_key(b"push"), ); unsafe { - let _ = crate::closure::js_native_call_value(push, [string_value("a")].as_ptr(), 1); - let _ = crate::closure::js_native_call_value(push, [string_value("b")].as_ptr(), 1); let _ = - crate::closure::js_native_call_value(cb, [f64::from_bits(TAG_UNDEFINED)].as_ptr(), 1); + crate::closure::native_call_value_this(push, receiver, [string_value("a")].as_ptr(), 1); + let _ = + crate::closure::native_call_value_this(push, receiver, [string_value("b")].as_ptr(), 1); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + [f64::from_bits(TAG_UNDEFINED)].as_ptr(), + 1, + ); } f64::from_bits(TAG_UNDEFINED) } -extern "C" fn transform_flush_tail_callback(_closure: *const ClosureHeader, cb: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn transform_flush_tail_callback( + _closure: *const ClosureHeader, + receiver: crate::closure::JsThis, + cb: f64, +) -> f64 { + let this = receiver.as_f64(); TRANSFORM_THIS_HAS_STREAM_STATE.with(|matches| { matches.borrow_mut().push( get_hidden_value(this, hidden_readable_flag_key()).is_some() @@ -554,7 +673,12 @@ extern "C" fn transform_flush_tail_callback(_closure: *const ClosureHeader, cb: TRANSFORM_FLUSH_COUNT.with(|count| *count.borrow_mut() += 1); let args = [f64::from_bits(TAG_UNDEFINED), string_value("!")]; unsafe { - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } f64::from_bits(TAG_UNDEFINED) } @@ -688,7 +812,12 @@ fn writable_options_write_callback_is_invoked_by_stub_write() { ); let args = [string_value("chunk"), f64::from_bits(TAG_UNDEFINED)]; unsafe { - let _ = crate::closure::js_native_call_value(write, args.as_ptr(), args.len()); + let _ = crate::closure::js_native_call_value( + write, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } WRITE_CAPTURED.with(|captured| { @@ -775,8 +904,22 @@ fn transform_pipe_chain_applies_callback_output() { raw_ptr_from_value(upper) as *const ObjectHeader, hidden_key(b"pipe"), ); - let _ = unsafe { crate::closure::js_native_call_value(src_pipe, [upper].as_ptr(), 1) }; - let _ = unsafe { crate::closure::js_native_call_value(upper_pipe, [sink].as_ptr(), 1) }; + let _ = unsafe { + crate::closure::js_native_call_value( + src_pipe, + crate::closure::plain_call_receiver(), + [upper].as_ptr(), + 1, + ) + }; + let _ = unsafe { + crate::closure::js_native_call_value( + upper_pipe, + crate::closure::plain_call_receiver(), + [sink].as_ptr(), + 1, + ) + }; let _ = crate::promise::js_promise_run_microtasks(); READABLE_DATA_CAPTURED.with(|captured| { @@ -1170,22 +1313,21 @@ fn stream_json_stringify_uses_node_state_shape() { } #[test] -fn stream_methods_use_implicit_this_without_closure_capture() { +fn stream_methods_use_their_receiver_without_closure_capture() { let stream = js_node_stream_passthrough_new(f64::from_bits(TAG_UNDEFINED)); - let prev_this = crate::object::js_implicit_this_set(stream); let _ = ns_end3( std::ptr::null(), + crate::closure::JsThis::from_f64(stream), f64::from_bits(TAG_UNDEFINED), f64::from_bits(TAG_UNDEFINED), f64::from_bits(TAG_UNDEFINED), ); - crate::object::js_implicit_this_set(prev_this); assert!(js_node_stream_is_stub_ended_after_read(stream)); } #[test] -fn stream_method_closure_capture_wins_over_stale_implicit_this() { +fn stream_method_closure_capture_wins_over_a_foreign_receiver() { let stream = js_node_stream_passthrough_new(f64::from_bits(TAG_UNDEFINED)); let other = box_pointer(crate::object::js_object_alloc(0, 0) as *const u8); let end = js_object_get_field_by_name_f64( @@ -1193,11 +1335,14 @@ fn stream_method_closure_capture_wins_over_stale_implicit_this() { hidden_key(b"end"), ); - let prev_this = crate::object::js_implicit_this_set(other); unsafe { - let _ = crate::closure::js_native_call_value(end, std::ptr::null(), 0); + let _ = crate::closure::native_call_value_this( + end, + crate::closure::JsThis::from_f64(other), + std::ptr::null(), + 0, + ); } - crate::object::js_implicit_this_set(prev_this); assert!(js_node_stream_is_stub_ended_after_read(stream)); assert!(!stream_hidden_ended(other)); @@ -1222,7 +1367,14 @@ fn stream_methods_dispatch_through_dynamic_method_call() { #[test] fn callable_stream_constructor_autoinstantiates_passthrough() { let ctor = crate::object::bound_native_callable_export_value("stream", "PassThrough"); - let stream = unsafe { crate::closure::js_native_call_value(ctor, std::ptr::null(), 0) }; + let stream = unsafe { + crate::closure::js_native_call_value( + ctor, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert!(raw_ptr_from_value(stream) >= 0x10000); assert!(get_hidden_value(stream, hidden_readable_flag_key()).is_some()); @@ -1243,7 +1395,13 @@ fn readable_pipe_stub_returns_destination_and_rejects_missing_destination() { let dest = js_node_stream_writable_new(f64::from_bits(TAG_UNDEFINED)); assert_eq!( - ns_pipe2(std::ptr::null(), dest, f64::from_bits(TAG_UNDEFINED)).to_bits(), + ns_pipe2( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + dest, + f64::from_bits(TAG_UNDEFINED) + ) + .to_bits(), dest.to_bits() ); assert!(pipe_destination_is_missing(f64::from_bits(TAG_UNDEFINED))); @@ -1260,7 +1418,14 @@ fn readable_wrap_method_is_present_and_chainable() { let wrapped = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); let args = [wrapped]; - let result = unsafe { crate::closure::js_native_call_value(wrap, args.as_ptr(), args.len()) }; + let result = unsafe { + crate::closure::js_native_call_value( + wrap, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + }; assert_eq!(result.to_bits(), stream.to_bits()); } @@ -1274,7 +1439,14 @@ fn writable_cork_and_uncork_update_counter_and_return_undefined() { assert_eq!(writable_corked_count(stream), 0.0); - let ret = unsafe { crate::closure::js_native_call_value(cork, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + cork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), TAG_UNDEFINED); assert_eq!(writable_corked_count(stream), 1.0); assert_eq!( @@ -1282,15 +1454,36 @@ fn writable_cork_and_uncork_update_counter_and_return_undefined() { 1.0 ); - let ret = unsafe { crate::closure::js_native_call_value(cork, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + cork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), TAG_UNDEFINED); assert_eq!(writable_corked_count(stream), 2.0); - let ret = unsafe { crate::closure::js_native_call_value(uncork, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + uncork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), TAG_UNDEFINED); assert_eq!(writable_corked_count(stream), 1.0); - let ret = unsafe { crate::closure::js_native_call_value(uncork, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + uncork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), TAG_UNDEFINED); assert_eq!(writable_corked_count(stream), 0.0); @@ -1301,7 +1494,14 @@ fn writable_cork_and_uncork_update_counter_and_return_undefined() { assert_eq!(ret.to_bits(), TAG_UNDEFINED); assert_eq!(js_node_stream_method_writable_corked(handle), 0.0); - let ret = unsafe { crate::closure::js_native_call_value(uncork, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + uncork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), TAG_UNDEFINED); assert_eq!(writable_corked_count(stream), 0.0); } diff --git a/crates/perry-runtime/src/node_stream_tests_extra.rs b/crates/perry-runtime/src/node_stream_tests_extra.rs index 88c53fa42e..ca63e98b03 100644 --- a/crates/perry-runtime/src/node_stream_tests_extra.rs +++ b/crates/perry-runtime/src/node_stream_tests_extra.rs @@ -22,13 +22,34 @@ fn stream_max_listeners_default_and_override_round_trip() { let get_max = js_object_get_field_by_name_f64(obj, hidden_key(b"getMaxListeners")); let set_max = js_object_get_field_by_name_f64(obj, hidden_key(b"setMaxListeners")); - let initial = unsafe { crate::closure::js_native_call_value(get_max, std::ptr::null(), 0) }; + let initial = unsafe { + crate::closure::js_native_call_value( + get_max, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(initial, 10.0); - let returned = unsafe { crate::closure::js_native_call_value(set_max, [25.0].as_ptr(), 1) }; + let returned = unsafe { + crate::closure::js_native_call_value( + set_max, + crate::closure::plain_call_receiver(), + [25.0].as_ptr(), + 1, + ) + }; assert_eq!(returned.to_bits(), stream.to_bits()); - let updated = unsafe { crate::closure::js_native_call_value(get_max, std::ptr::null(), 0) }; + let updated = unsafe { + crate::closure::js_native_call_value( + get_max, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(updated, 25.0); let other = js_node_stream_readable_new(f64::from_bits(TAG_UNDEFINED)); @@ -36,8 +57,14 @@ fn stream_max_listeners_default_and_override_round_trip() { raw_ptr_from_value(other) as *const ObjectHeader, hidden_key(b"getMaxListeners"), ); - let other_initial = - unsafe { crate::closure::js_native_call_value(other_get, std::ptr::null(), 0) }; + let other_initial = unsafe { + crate::closure::js_native_call_value( + other_get, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(other_initial, 10.0); let native_handle = raw_ptr_from_value(other) as i64; @@ -58,14 +85,26 @@ fn stream_event_names_raw_listeners_and_prepend_chainability() { let prepend = js_object_get_field_by_name_f64(obj, hidden_key(b"prependListener")); let raw = unsafe { - crate::closure::js_native_call_value(raw_listeners, [string_value("never")].as_ptr(), 1) + crate::closure::js_native_call_value( + raw_listeners, + crate::closure::plain_call_receiver(), + [string_value("never")].as_ptr(), + 1, + ) }; assert_eq!( crate::array::js_array_length(raw_ptr_from_value(raw) as *const crate::array::ArrayHeader), 0 ); - let names = unsafe { crate::closure::js_native_call_value(event_names, std::ptr::null(), 0) }; + let names = unsafe { + crate::closure::js_native_call_value( + event_names, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!( crate::array::js_array_length(raw_ptr_from_value(names) as *const crate::array::ArrayHeader), 0 @@ -73,12 +112,22 @@ fn stream_event_names_raw_listeners_and_prepend_chainability() { let cb = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); let returned = unsafe { - crate::closure::js_native_call_value(prepend, [string_value("data"), cb].as_ptr(), 2) + crate::closure::js_native_call_value( + prepend, + crate::closure::plain_call_receiver(), + [string_value("data"), cb].as_ptr(), + 2, + ) }; assert_eq!(returned.to_bits(), stream.to_bits()); let raw = unsafe { - crate::closure::js_native_call_value(raw_listeners, [string_value("data")].as_ptr(), 1) + crate::closure::js_native_call_value( + raw_listeners, + crate::closure::plain_call_receiver(), + [string_value("data")].as_ptr(), + 1, + ) }; let raw_arr = raw_ptr_from_value(raw) as *const crate::array::ArrayHeader; assert_eq!(crate::array::js_array_length(raw_arr), 1); @@ -87,7 +136,14 @@ fn stream_event_names_raw_listeners_and_prepend_chainability() { cb.to_bits() ); - let names = unsafe { crate::closure::js_native_call_value(event_names, std::ptr::null(), 0) }; + let names = unsafe { + crate::closure::js_native_call_value( + event_names, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; let names_arr = raw_ptr_from_value(names) as *const crate::array::ArrayHeader; assert_eq!(crate::array::js_array_length(names_arr), 1); assert!(string_value_eq( @@ -128,20 +184,45 @@ fn stream_listener_count_and_listeners_reflect_data_end_storage() { let cb3 = box_pointer(js_closure_alloc(noop_listener as *const u8, 0) as *const u8); unsafe { - let _ = crate::closure::js_native_call_value(on, [string_value("data"), cb1].as_ptr(), 2); - let _ = crate::closure::js_native_call_value(on, [string_value("data"), cb2].as_ptr(), 2); - let _ = crate::closure::js_native_call_value(on, [string_value("end"), cb3].as_ptr(), 2); + let _ = crate::closure::js_native_call_value( + on, + crate::closure::plain_call_receiver(), + [string_value("data"), cb1].as_ptr(), + 2, + ); + let _ = crate::closure::js_native_call_value( + on, + crate::closure::plain_call_receiver(), + [string_value("data"), cb2].as_ptr(), + 2, + ); + let _ = crate::closure::js_native_call_value( + on, + crate::closure::plain_call_receiver(), + [string_value("end"), cb3].as_ptr(), + 2, + ); } let count_for = |event: &str| unsafe { - crate::closure::js_native_call_value(listener_count, [string_value(event)].as_ptr(), 1) + crate::closure::js_native_call_value( + listener_count, + crate::closure::plain_call_receiver(), + [string_value(event)].as_ptr(), + 1, + ) }; assert_eq!(count_for("data"), 2.0); assert_eq!(count_for("end"), 1.0); assert_eq!(count_for("error"), 0.0); let data_listeners = unsafe { - crate::closure::js_native_call_value(listeners, [string_value("data")].as_ptr(), 1) + crate::closure::js_native_call_value( + listeners, + crate::closure::plain_call_receiver(), + [string_value("data")].as_ptr(), + 1, + ) }; let data_arr = raw_ptr_from_value(data_listeners) as *mut crate::array::ArrayHeader; assert_eq!(crate::array::js_array_length(data_arr), 2); @@ -158,7 +239,12 @@ fn stream_listener_count_and_listeners_reflect_data_end_storage() { assert_eq!(count_for("data"), 2.0); let missing_listeners = unsafe { - crate::closure::js_native_call_value(listeners, [string_value("error")].as_ptr(), 1) + crate::closure::js_native_call_value( + listeners, + crate::closure::plain_call_receiver(), + [string_value("error")].as_ptr(), + 1, + ) }; let missing_arr = raw_ptr_from_value(missing_listeners) as *const crate::array::ArrayHeader; assert_eq!(crate::array::js_array_length(missing_arr), 0); @@ -202,7 +288,14 @@ fn pipe_and_unpipe_emit_destination_events_with_source() { let _ = js_node_stream_method_on(dest_handle, string_value("pipe"), listener); let _ = js_node_stream_method_on(dest_handle, string_value("unpipe"), listener); - let ret = unsafe { crate::closure::js_native_call_value(pipe, [dest].as_ptr(), 1) }; + let ret = unsafe { + crate::closure::js_native_call_value( + pipe, + crate::closure::plain_call_receiver(), + [dest].as_ptr(), + 1, + ) + }; assert_eq!(ret.to_bits(), dest.to_bits()); assert_eq!( js_object_get_field_by_name_f64(dest_obj, readable_flowing_key()).to_bits(), @@ -217,7 +310,14 @@ fn pipe_and_unpipe_emit_destination_events_with_source() { TAG_TRUE ); - let ret = unsafe { crate::closure::js_native_call_value(unpipe, [dest].as_ptr(), 1) }; + let ret = unsafe { + crate::closure::js_native_call_value( + unpipe, + crate::closure::plain_call_receiver(), + [dest].as_ptr(), + 1, + ) + }; assert_eq!(ret.to_bits(), src.to_bits()); STREAM_EVENT_ARG_MATCHES.with(|matches| { assert_eq!(matches.borrow().as_slice(), &[true, true]); @@ -248,7 +348,14 @@ fn readable_from_pipe_ends_destination() { let _ = js_node_stream_method_on(dest_handle, string_value("data"), data); let _ = js_node_stream_method_on(dest_handle, string_value("end"), end); - let ret = unsafe { crate::closure::js_native_call_value(pipe, [dest].as_ptr(), 1) }; + let ret = unsafe { + crate::closure::js_native_call_value( + pipe, + crate::closure::plain_call_receiver(), + [dest].as_ptr(), + 1, + ) + }; assert_eq!(ret.to_bits(), dest.to_bits()); let _ = crate::promise::js_promise_run_microtasks(); @@ -287,11 +394,27 @@ fn readable_from_pipe_chain_ends_tail_destination() { let _ = js_node_stream_method_on(sink_handle, string_value("end"), sink_end); assert_eq!( - unsafe { crate::closure::js_native_call_value(src_pipe, [middle].as_ptr(), 1) }.to_bits(), + unsafe { + crate::closure::js_native_call_value( + src_pipe, + crate::closure::plain_call_receiver(), + [middle].as_ptr(), + 1, + ) + } + .to_bits(), middle.to_bits() ); assert_eq!( - unsafe { crate::closure::js_native_call_value(middle_pipe, [sink].as_ptr(), 1) }.to_bits(), + unsafe { + crate::closure::js_native_call_value( + middle_pipe, + crate::closure::plain_call_receiver(), + [sink].as_ptr(), + 1, + ) + } + .to_bits(), sink.to_bits() ); assert_eq!( @@ -327,7 +450,15 @@ fn pause_resume_track_readable_flowing_and_events() { TAG_NULL ); assert_eq!( - unsafe { crate::closure::js_native_call_value(is_paused, std::ptr::null(), 0) }.to_bits(), + unsafe { + crate::closure::js_native_call_value( + is_paused, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + } + .to_bits(), TAG_FALSE ); @@ -344,26 +475,56 @@ fn pause_resume_track_readable_flowing_and_events() { js_object_get_field_by_name_f64(obj, readable_flowing_key()).to_bits(), TAG_TRUE ); - let ret = unsafe { crate::closure::js_native_call_value(pause, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + pause, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), stream.to_bits()); assert_eq!( js_object_get_field_by_name_f64(obj, readable_flowing_key()).to_bits(), TAG_FALSE ); assert_eq!( - unsafe { crate::closure::js_native_call_value(is_paused, std::ptr::null(), 0) }.to_bits(), + unsafe { + crate::closure::js_native_call_value( + is_paused, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + } + .to_bits(), TAG_TRUE ); STREAM_EVENT_ORDER.with(|events| assert_eq!(events.borrow().as_slice(), b"P")); - let ret = unsafe { crate::closure::js_native_call_value(resume, std::ptr::null(), 0) }; + let ret = unsafe { + crate::closure::js_native_call_value( + resume, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(ret.to_bits(), stream.to_bits()); assert_eq!( js_object_get_field_by_name_f64(obj, readable_flowing_key()).to_bits(), TAG_TRUE ); assert_eq!( - unsafe { crate::closure::js_native_call_value(is_paused, std::ptr::null(), 0) }.to_bits(), + unsafe { + crate::closure::js_native_call_value( + is_paused, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + } + .to_bits(), TAG_FALSE ); STREAM_EVENT_ORDER.with(|events| assert_eq!(events.borrow().as_slice(), b"P")); @@ -727,19 +888,55 @@ fn writable_corked_counter_tracks_cork_balance() { ); assert_eq!( - unsafe { crate::closure::js_native_call_value(cork, std::ptr::null(), 0) }.to_bits(), + unsafe { + crate::closure::js_native_call_value( + cork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + } + .to_bits(), TAG_UNDEFINED ); assert_eq!(js_node_stream_method_writable_corked(handle), 1.0); - let _ = unsafe { crate::closure::js_native_call_value(cork, std::ptr::null(), 0) }; + let _ = unsafe { + crate::closure::js_native_call_value( + cork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(js_node_stream_method_writable_corked(handle), 2.0); - let _ = unsafe { crate::closure::js_native_call_value(uncork, std::ptr::null(), 0) }; + let _ = unsafe { + crate::closure::js_native_call_value( + uncork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(js_node_stream_method_writable_corked(handle), 1.0); - let _ = unsafe { crate::closure::js_native_call_value(uncork, std::ptr::null(), 0) }; - let _ = unsafe { crate::closure::js_native_call_value(uncork, std::ptr::null(), 0) }; + let _ = unsafe { + crate::closure::js_native_call_value( + uncork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; + let _ = unsafe { + crate::closure::js_native_call_value( + uncork, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!(js_node_stream_method_writable_corked(handle), 0.0); assert_eq!( js_object_get_field_by_name_f64(obj, hidden_key(b"writableCorked")), @@ -789,7 +986,12 @@ fn writable_backpressure_tracks_length_need_drain_and_drain_event() { let cb = PENDING_WRITE_CALLBACK.with(|pending| pending.borrow_mut().take().unwrap()); unsafe { - let _ = crate::closure::js_native_call_value(cb, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } assert_eq!(js_node_stream_method_writable_length(handle), 0.0); @@ -862,7 +1064,12 @@ fn writable_end_waits_for_pending_write_before_finish() { let cb = PENDING_WRITE_CALLBACK.with(|pending| pending.borrow_mut().take().unwrap()); unsafe { - let _ = crate::closure::js_native_call_value(cb, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } let _ = crate::promise::js_promise_run_microtasks(); WRITABLE_FINISH_COUNT.with(|count| assert_eq!(*count.borrow(), 1)); @@ -904,7 +1111,12 @@ fn writable_end_callback_runs_before_finish_and_close_events() { let cb = PENDING_WRITE_CALLBACK.with(|pending| pending.borrow_mut().take().unwrap()); unsafe { - let _ = crate::closure::js_native_call_value(cb, std::ptr::null(), 0); + let _ = crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } let _ = crate::promise::js_promise_run_microtasks(); @@ -1075,7 +1287,12 @@ fn stream_destroy_with_error_marks_errored_state() { assert_eq!(js_node_stream_method_errored(handle()).to_bits(), TAG_NULL); let args = [err.get_nanbox_f64()]; let ret = unsafe { - crate::closure::js_native_call_value(destroy.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + destroy.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) }; assert_eq!(ret.to_bits(), stream.get_nanbox_f64().to_bits()); @@ -1118,7 +1335,14 @@ fn readable_exposes_async_dispose_symbol_method() { }; assert!(is_callable_value(method)); - let result = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; + let result = unsafe { + crate::closure::js_native_call_value( + method, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_ne!(result.to_bits(), TAG_UNDEFINED); assert_eq!(js_node_stream_method_destroyed(handle).to_bits(), TAG_TRUE); } diff --git a/crates/perry-runtime/src/node_submodules/blob.rs b/crates/perry-runtime/src/node_submodules/blob.rs index 1558b90783..b447044bde 100644 --- a/crates/perry-runtime/src/node_submodules/blob.rs +++ b/crates/perry-runtime/src/node_submodules/blob.rs @@ -88,23 +88,33 @@ fn blob_reader_promise_rejected(reason: f64) -> f64 { }) } -extern "C" fn blob_text_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn blob_text_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let bytes = captured_blob_bytes(closure); blob_reader_promise_value(bytes_to_text_value(&bytes)) } -extern "C" fn blob_array_buffer_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn blob_array_buffer_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let bytes = captured_blob_bytes(closure); blob_reader_promise_value(bytes_to_array_buffer_value(&bytes)) } -extern "C" fn blob_bytes_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn blob_bytes_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let bytes = captured_blob_bytes(closure); blob_reader_promise_value(bytes_to_uint8_array_value(&bytes)) } extern "C" fn blob_slice_method( closure: *const ClosureHeader, + _this: crate::closure::JsThis, start: f64, end: f64, content_type: f64, @@ -129,26 +139,38 @@ extern "C" fn blob_slice_method( blob_value_from_bytes_and_type(&bytes[lo as usize..hi as usize], &content_type) } -extern "C" fn blob_stream_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn blob_stream_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let bytes = captured_blob_bytes(closure); crate::node_stream::js_node_stream_readable_from(bytes_to_uint8_array_value(&bytes)) } -extern "C" fn file_blob_text_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_text_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match read_file_blob_bytes(captured_file_blob_id(closure)) { Ok(bytes) => blob_reader_promise_value(bytes_to_text_value(&bytes)), Err(reason) => blob_reader_promise_rejected(reason), } } -extern "C" fn file_blob_array_buffer_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_array_buffer_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match read_file_blob_bytes(captured_file_blob_id(closure)) { Ok(bytes) => blob_reader_promise_value(bytes_to_array_buffer_value(&bytes)), Err(reason) => blob_reader_promise_rejected(reason), } } -extern "C" fn file_blob_bytes_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_bytes_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { match read_file_blob_bytes(captured_file_blob_id(closure)) { Ok(bytes) => blob_reader_promise_value(bytes_to_uint8_array_value(&bytes)), Err(reason) => blob_reader_promise_rejected(reason), @@ -157,6 +179,7 @@ extern "C" fn file_blob_bytes_method(closure: *const ClosureHeader) -> f64 { extern "C" fn file_blob_slice_method( closure: *const ClosureHeader, + _this: crate::closure::JsThis, start: f64, end: f64, content_type: f64, @@ -193,11 +216,17 @@ extern "C" fn file_blob_slice_method( blob_value_from_file_state(child) } -extern "C" fn file_blob_stream_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_stream_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { file_blob_stream_value(captured_file_blob_id(closure)) } -extern "C" fn file_blob_stream_get_reader_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_stream_get_reader_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let stream_id = captured_file_blob_stream_id(closure); let obj = js_object_alloc(0, 4); set_named_value( @@ -223,11 +252,17 @@ extern "C" fn file_blob_stream_get_reader_method(closure: *const ClosureHeader) object_value(obj) } -extern "C" fn file_blob_stream_next_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_stream_next_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { file_blob_stream_next(captured_file_blob_stream_id(closure)) } -extern "C" fn file_blob_stream_return_method(closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_stream_return_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let stream_id = captured_file_blob_stream_id(closure); FILE_BLOB_STREAMS.with(|streams| { if let Some(state) = streams.borrow_mut().get_mut(&stream_id) { @@ -237,20 +272,34 @@ extern "C" fn file_blob_stream_return_method(closure: *const ClosureHeader) -> f resolved_iterator_promise(f64::from_bits(crate::value::TAG_UNDEFINED), true) } -extern "C" fn file_blob_stream_values_method(closure: *const ClosureHeader, _options: f64) -> f64 { +extern "C" fn file_blob_stream_values_method( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + _options: f64, +) -> f64 { let _ = closure; - crate::object::js_implicit_this_get() + this.as_f64() } -extern "C" fn file_blob_stream_async_iterator_method(_closure: *const ClosureHeader) -> f64 { - crate::object::js_implicit_this_get() +extern "C" fn file_blob_stream_async_iterator_method( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + this.as_f64() } -extern "C" fn file_blob_stream_undefined_method(_closure: *const ClosureHeader) -> f64 { +extern "C" fn file_blob_stream_undefined_method( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn file_blob_stream_cancel_method(closure: *const ClosureHeader, _reason: f64) -> f64 { +extern "C" fn file_blob_stream_cancel_method( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _reason: f64, +) -> f64 { let stream_id = captured_file_blob_stream_id(closure); FILE_BLOB_STREAMS.with(|streams| { if let Some(state) = streams.borrow_mut().get_mut(&stream_id) { diff --git a/crates/perry-runtime/src/node_submodules/consumers.rs b/crates/perry-runtime/src/node_submodules/consumers.rs index 33a7f0dcea..661cf4d002 100644 --- a/crates/perry-runtime/src/node_submodules/consumers.rs +++ b/crates/perry-runtime/src/node_submodules/consumers.rs @@ -427,6 +427,7 @@ fn registered_readable_stream_reader(stream: f64) -> Option { return None; } let reader = unsafe { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: StreamGetReaderFn = std::mem::transmute(f); func(stream) }; @@ -477,7 +478,7 @@ fn call_collector_method( if let Some(promise) = promise_ptr_from_value(result) { crate::promise::js_promise_then(promise, step, reject); } else { - consumer_collect_step(step, result); + consumer_collect_step(step, crate::closure::plain_call_receiver(), result); } } @@ -507,10 +508,14 @@ fn call_symbol_async_iterator(stream: f64) -> Option { if !is_callable_value(method) { return None; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); - let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let iterator = unsafe { + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(stream), + std::ptr::null(), + 0, + ) + }; if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { @@ -582,13 +587,21 @@ pub(crate) fn consume_bytes(stream: f64) -> f64 { consume_stream(ConsumerKind::Bytes, stream) } -extern "C" fn consumer_collect_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn consumer_collect_rejected( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { let promise = js_closure_get_capture_ptr(closure, 0) as *mut crate::Promise; crate::promise::js_promise_reject(promise, reason); 0.0 } -extern "C" fn consumer_collect_step(closure: *const ClosureHeader, iter_result: f64) -> f64 { +extern "C" fn consumer_collect_step( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + iter_result: f64, +) -> f64 { let promise = js_closure_get_capture_ptr(closure, 0) as *mut crate::Promise; let mut result_arr = js_closure_get_capture_ptr(closure, 1) as *mut crate::array::ArrayHeader; let receiver = js_closure_get_capture_f64(closure, 2); @@ -619,16 +632,25 @@ extern "C" fn consumer_collect_step(closure: *const ClosureHeader, iter_result: 0.0 } -pub(crate) extern "C" fn thunk_consumers_text(_closure: *const ClosureHeader, stream: f64) -> f64 { +pub(crate) extern "C" fn thunk_consumers_text( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + stream: f64, +) -> f64 { consume_stream(ConsumerKind::Text, stream) } -pub(crate) extern "C" fn thunk_consumers_json(_closure: *const ClosureHeader, stream: f64) -> f64 { +pub(crate) extern "C" fn thunk_consumers_json( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + stream: f64, +) -> f64 { consume_stream(ConsumerKind::Json, stream) } pub(crate) extern "C" fn thunk_consumers_buffer( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, stream: f64, ) -> f64 { consume_stream(ConsumerKind::Buffer, stream) @@ -637,15 +659,24 @@ pub(crate) extern "C" fn thunk_consumers_buffer( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_consumers_arrayBuffer( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, stream: f64, ) -> f64 { consume_stream(ConsumerKind::ArrayBuffer, stream) } -pub(crate) extern "C" fn thunk_consumers_bytes(_closure: *const ClosureHeader, stream: f64) -> f64 { +pub(crate) extern "C" fn thunk_consumers_bytes( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + stream: f64, +) -> f64 { consume_stream(ConsumerKind::Bytes, stream) } -pub(crate) extern "C" fn thunk_consumers_blob(_closure: *const ClosureHeader, stream: f64) -> f64 { +pub(crate) extern "C" fn thunk_consumers_blob( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + stream: f64, +) -> f64 { consume_stream(ConsumerKind::Blob, stream) } diff --git a/crates/perry-runtime/src/node_submodules/diagnostics.rs b/crates/perry-runtime/src/node_submodules/diagnostics.rs index 5da13e6024..75d9a70c8e 100644 --- a/crates/perry-runtime/src/node_submodules/diagnostics.rs +++ b/crates/perry-runtime/src/node_submodules/diagnostics.rs @@ -47,7 +47,11 @@ use super::*; // surface as no-op thrower thunks the same way the other submodules do — // real-tracing semantics are a follow-up under #793. -pub(crate) extern "C" fn thunk_diag_noop(_closure: *const ClosureHeader, _arg: f64) -> f64 { +pub(crate) extern "C" fn thunk_diag_noop( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { f64::from_bits(crate::value::JSValue::undefined().bits()) } @@ -631,129 +635,10 @@ pub enum ErrUserProp { Bits(u64), } -unsafe fn error_user_prop_string(value: f64) -> String { - let ptr = crate::value::js_jsvalue_to_string(value); - if ptr.is_null() { - return String::new(); - } - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() -} - -/// Record a user-assigned own property on an `Error` object. `error_ptr` is -/// the `ErrorHeader` pointer (the NaN-box pointer payload). Called from the -/// `GC_TYPE_ERROR` branch of `js_object_set_field_by_name`. -pub fn set_error_user_prop(error_ptr: usize, key: &str, value: f64) { - if error_ptr == 0 { - return; - } - // #6759 phase 1: the property bag now hangs off the error's own metadata - // record instead of a table keyed by its address, so it moves with the - // error, dies with it, and cannot be inherited by a later tenant of a - // recycled address. Insertion order comes free from the bag object's - // `keys_array`. - unsafe { - let Some(bag) = crate::object::cell_expando_ensure(error_ptr) else { - return; - }; - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - crate::object::js_object_set_field_by_name(bag, key_ptr, value); - } -} - -/// Look up a user-assigned own property on an `Error` object, materialising it -/// back into a NaN-boxed `f64`. Returns `None` if no such property was set. -/// Called from the `GC_TYPE_ERROR` branch of `js_object_get_field_by_name`. -pub fn error_user_prop(error_ptr: usize, key: &str) -> Option { - if error_ptr == 0 { - return None; - } - unsafe { - let bag = crate::object::cell_expando_get(error_ptr)?; - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - // Distinguish "absent" from "present and undefined": a bare get would - // return `undefined` for both, and the caller uses `None` to mean the - // error has no such own property at all. - let key_boxed = f64::from_bits(crate::js_nanbox_string(key_ptr as i64).to_bits()); - if !crate::object::obj_value_has_own_key( - crate::value::js_nanbox_pointer(bag as i64), - key_boxed, - ) { - return None; - } - Some(f64::from_bits( - crate::object::js_object_get_field_by_name(bag, key_ptr).bits(), - )) - } -} - -/// Remove a user-assigned own property from an Error object. Returns true -/// when the property existed (used by `delete err.prop` and data↔accessor -/// descriptor conversions). -pub fn remove_error_user_prop(error_ptr: usize, key: &str) -> bool { - if error_ptr == 0 { - return false; - } - unsafe { - let Some(bag) = crate::object::cell_expando_get(error_ptr) else { - return false; - }; - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let key_boxed = f64::from_bits(crate::js_nanbox_string(key_ptr as i64).to_bits()); - if !crate::object::obj_value_has_own_key( - crate::value::js_nanbox_pointer(bag as i64), - key_boxed, - ) { - return false; - } - crate::object::js_object_delete_field(bag, key_ptr); - true - } -} - -/// Return user-assigned own properties on an Error object as materialized JS -/// values so util.inspect/console formatting can show them. -pub fn error_user_props(error_ptr: usize) -> Vec<(String, f64)> { - if error_ptr == 0 { - return Vec::new(); - } - unsafe { - let Some(bag) = crate::object::cell_expando_get(error_ptr) else { - return Vec::new(); - }; - // The bag is an ordinary object, so its `keys_array` already holds the - // keys in ECMA-262 insertion order — no sort, and no ordering of our - // own to keep in step with node's. - let keys_view = crate::object::object_keys(bag); - let keys = keys_view.arr(); - if keys.is_null() { - return Vec::new(); - } - let len = keys_view.count() as usize; - let mut out = Vec::with_capacity(len); - for i in 0..len { - let key_val = crate::array::js_array_get_f64(keys, i as u32); - // A tombstoned key slot (#9029) reads back as undefined through - // the hole canonicalization (#323); stringifying it would mint a - // phantom "undefined" prop. Undefined is never a legal key. - if key_val.to_bits() == crate::value::TAG_UNDEFINED { - continue; - } - let name_ptr = crate::value::js_jsvalue_to_string(key_val); - if name_ptr.is_null() { - continue; - } - let name = error_user_prop_string(f64::from_bits( - crate::js_nanbox_string(name_ptr as i64).to_bits(), - )); - let value = - f64::from_bits(crate::object::js_object_get_field_by_name(bag, name_ptr).bits()); - out.push((name, value)); - } - out - } -} +mod user_props; +pub use user_props::{ + error_user_prop, error_user_props, remove_error_user_prop, set_error_user_prop, +}; pub(crate) fn throw_invalid_arg() -> ! { let msg = b"The argument is invalid"; @@ -813,25 +698,23 @@ pub(crate) fn get_field_value(obj: *mut ObjectHeader, name: &str) -> f64 { } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast0(f: extern "C" fn(*const ClosureHeader) -> f64) -> *const u8 { +pub(crate) fn cast0(f: crate::closure::body_call::js_body_fn_ty!()) -> *const u8 { f as *const u8 } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast1(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> *const u8 { +pub(crate) fn cast1(f: crate::closure::body_call::js_body_fn_ty!(a)) -> *const u8 { f as *const u8 } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast2(f: extern "C" fn(*const ClosureHeader, f64, f64) -> f64) -> *const u8 { +pub(crate) fn cast2(f: crate::closure::body_call::js_body_fn_ty!(a, a)) -> *const u8 { f as *const u8 } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast3(f: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64) -> *const u8 { +pub(crate) fn cast3(f: crate::closure::body_call::js_body_fn_ty!(a, a, a)) -> *const u8 { f as *const u8 } #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn cast4( - f: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64, -) -> *const u8 { +pub(crate) fn cast4(f: crate::closure::body_call::js_body_fn_ty!(a, a, a, a)) -> *const u8 { f as *const u8 } pub(crate) fn method_closure(func: *const u8, arity: u32, id: i64) -> f64 { @@ -851,7 +734,10 @@ pub(crate) fn catch_js f64>(f: F) -> Result { // #854: diagnostics_channel captured-error helper retained for the subsystem #[allow(dead_code)] -pub(crate) extern "C" fn throw_captured_error(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn throw_captured_error( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let err = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); crate::exception::js_throw(err) } @@ -887,14 +773,6 @@ pub(crate) fn suppress_uncaught_drain f64>(f: F) -> f64 { result } -pub(crate) fn with_implicit_this f64>(this_arg: f64, f: F) -> f64 { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); - let result = f(); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result -} - pub(crate) fn update_channel_active(id: i64) { DIAG_CHANNELS.with(|channels| { if let Some(ch) = channels.borrow_mut().get_mut(&id) { @@ -1110,7 +988,9 @@ pub(crate) fn publish_channel_local(id: i64, data: f64) { // Match Node's safe subscriber behavior for the happy path; exceptions // propagate through Perry's exception mechanism and are catchable. let cb = closure_ptr(subscriber); - if let Err(err) = catch_js(|| js_closure_call2(cb, data, name)) { + if let Err(err) = + catch_js(|| js_closure_call2(cb, crate::closure::plain_call_receiver(), data, name)) + { schedule_uncaught(err); } } @@ -1229,15 +1109,24 @@ pub(crate) fn run_store_wrapped( if !valid_closure_value(fn_value) { crate::closure::throw_not_callable(); } + let scope = crate::gc::RuntimeHandleScope::new(); + // The rebind clone allocates, so the receiver is re-read from a root. + let this_arg_handle = scope.root_nanbox_f64(this_arg); let rebound = crate::closure::clone_closure_rebind_this(fn_value.to_bits(), this_arg); let cb = (rebound & crate::value::POINTER_MASK) as *const ClosureHeader; - with_implicit_this(this_arg, || unsafe { - js_closure_call_array(cb as i64, args.as_ptr(), args.len() as i64) - }) + unsafe { + crate::closure::js_closure_call_array( + cb as i64, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + args.as_ptr(), + args.len() as i64, + ) + } } pub(crate) extern "C" fn diag_channel_subscribe( closure: *const ClosureHeader, + _this: crate::closure::JsThis, subscriber: f64, ) -> f64 { add_subscriber(method_id(closure), subscriber); @@ -1246,18 +1135,24 @@ pub(crate) extern "C" fn diag_channel_subscribe( pub(crate) extern "C" fn diag_channel_unsubscribe( closure: *const ClosureHeader, + _this: crate::closure::JsThis, subscriber: f64, ) -> f64 { bool_value(remove_subscriber(method_id(closure), subscriber)) } -pub(crate) extern "C" fn diag_channel_publish(closure: *const ClosureHeader, data: f64) -> f64 { +pub(crate) extern "C" fn diag_channel_publish( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + data: f64, +) -> f64 { publish_channel(method_id(closure), data); undefined() } pub(crate) extern "C" fn diag_channel_bind_store( closure: *const ClosureHeader, + _this: crate::closure::JsThis, store: f64, transform: f64, ) -> f64 { @@ -1300,6 +1195,7 @@ pub(crate) extern "C" fn diag_channel_bind_store( pub(crate) extern "C" fn diag_channel_unbind_store( closure: *const ClosureHeader, + _this: crate::closure::JsThis, store: f64, ) -> f64 { let id = method_id(closure); @@ -1326,7 +1222,7 @@ pub(crate) extern "C" fn diag_channel_unbind_store( pub(crate) fn call_store_run(store: f64, context: f64, next: f64) -> f64 { fn run_als(handle: i64, context: f64, next: f64) -> f64 { crate::async_context::push_store(handle, context); - let result = js_closure_call0(closure_ptr(next)); + let result = js_closure_call0(closure_ptr(next), crate::closure::plain_call_receiver()); crate::async_context::pop_store(handle); result } @@ -1346,10 +1242,18 @@ pub(crate) fn call_store_run(store: f64, context: f64, next: f64) -> f64 { } let obj = crate::value::js_nanbox_get_pointer(store) as *mut ObjectHeader; let run = get_field_value(obj, "run"); - js_closure_call2(closure_ptr(run), context, next) + js_closure_call2( + closure_ptr(run), + crate::closure::plain_call_receiver(), + context, + next, + ) } -pub(crate) extern "C" fn store_next_thunk(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn store_next_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_ptr(closure, 0); let data = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); let fn_value = f64::from_bits(js_closure_get_capture_ptr(closure, 2) as u64); @@ -1362,7 +1266,10 @@ pub(crate) extern "C" fn store_next_thunk(closure: *const ClosureHeader) -> f64 run_store_wrapped(id, data, fn_value, this_arg, &args) } -pub(crate) extern "C" fn store_chain_thunk(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn store_chain_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let store = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let context = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); let next = f64::from_bits(js_closure_get_capture_ptr(closure, 2) as u64); @@ -1427,6 +1334,7 @@ fn unbox_arg_array(arr_value: f64) -> Vec { pub(crate) extern "C" fn diag_channel_run_stores( closure: *const ClosureHeader, + _this: crate::closure::JsThis, all_args: f64, ) -> f64 { let id = method_id(closure); @@ -1468,7 +1376,9 @@ pub(crate) extern "C" fn diag_channel_run_stores( for (store, transform) in stores.into_iter().rev() { let context = match transform { StoreTransform::Callable(t) => { - match catch_js(|| js_closure_call1(closure_ptr(t), data)) { + match catch_js(|| { + js_closure_call1(closure_ptr(t), crate::closure::plain_call_receiver(), data) + }) { Ok(context) => context, Err(err) => { schedule_uncaught(err); @@ -1492,16 +1402,26 @@ pub(crate) extern "C" fn diag_channel_run_stores( js_closure_set_capture_ptr(chain, 2, next_value.to_bits() as i64); next_value = boxed_ptr(chain); } - suppress_uncaught_drain(|| js_closure_call0(closure_ptr(next_value))) + suppress_uncaught_drain(|| { + js_closure_call0( + closure_ptr(next_value), + crate::closure::plain_call_receiver(), + ) + }) } -pub(crate) extern "C" fn thunk_diag_channel(closure: *const ClosureHeader, name: f64) -> f64 { +pub(crate) extern "C" fn thunk_diag_channel( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + name: f64, +) -> f64 { let _ = closure; channel_obj(ensure_channel(name)) } pub(crate) extern "C" fn thunk_diag_subscribe( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, subscriber: f64, ) -> f64 { @@ -1512,6 +1432,7 @@ pub(crate) extern "C" fn thunk_diag_subscribe( pub(crate) extern "C" fn thunk_diag_unsubscribe( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, subscriber: f64, ) -> f64 { @@ -1521,6 +1442,7 @@ pub(crate) extern "C" fn thunk_diag_unsubscribe( pub(crate) extern "C" fn thunk_diag_has_subscribers( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, ) -> f64 { let key = match channel_key(name) { @@ -1578,7 +1500,11 @@ pub(crate) fn channel_from_object_property(obj_value: f64, prop: &str) -> i64 { }) } -pub(crate) extern "C" fn diag_trace_subscribe(closure: *const ClosureHeader, handlers: f64) -> f64 { +pub(crate) extern "C" fn diag_trace_subscribe( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + handlers: f64, +) -> f64 { let id = method_id(closure); let events = DIAG_TRACES.with(|m| m.borrow().get(&id).map(|t| t.events).unwrap_or([0; 5])); for (idx, name) in ["start", "end", "asyncStart", "asyncEnd", "error"] @@ -1606,6 +1532,7 @@ pub(crate) extern "C" fn diag_trace_subscribe(closure: *const ClosureHeader, han pub(crate) extern "C" fn diag_trace_unsubscribe( closure: *const ClosureHeader, + _this: crate::closure::JsThis, handlers: f64, ) -> f64 { let id = method_id(closure); @@ -1630,15 +1557,24 @@ pub(crate) fn call_fn_value(fn_value: f64, this_arg: f64, args: &[f64]) -> f64 { if !valid_closure_value(fn_value) { crate::closure::throw_not_callable(); } + let scope = crate::gc::RuntimeHandleScope::new(); + // The rebind clone allocates, so the receiver is re-read from a root. + let this_arg_handle = scope.root_nanbox_f64(this_arg); let rebound = crate::closure::clone_closure_rebind_this(fn_value.to_bits(), this_arg); let cb = (rebound & crate::value::POINTER_MASK) as *const ClosureHeader; - with_implicit_this(this_arg, || unsafe { - js_closure_call_array(cb as i64, args.as_ptr(), args.len() as i64) - }) + unsafe { + crate::closure::js_closure_call_array( + cb as i64, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + args.as_ptr(), + args.len() as i64, + ) + } } pub(crate) extern "C" fn diag_trace_sync( closure: *const ClosureHeader, + _this: crate::closure::JsThis, fn_value: f64, context: f64, this_arg: f64, @@ -1685,6 +1621,7 @@ pub(crate) extern "C" fn diag_trace_sync( pub(crate) extern "C" fn diag_trace_promise( closure: *const ClosureHeader, + _this: crate::closure::JsThis, fn_value: f64, context: f64, this_arg: f64, @@ -1764,6 +1701,7 @@ pub(crate) extern "C" fn diag_trace_promise( /// (#3086). `arguments[0]` is `err`, `arguments[1]` is `res`. pub(crate) extern "C" fn diag_trace_wrapped_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, all_args: f64, ) -> f64 { let callback = js_closure_get_capture_f64(closure, 0); @@ -1806,7 +1744,11 @@ fn throw_trace_callback_not_function() -> ! { crate::exception::js_throw(boxed_ptr(err)) } -pub(crate) extern "C" fn diag_trace_callback(closure: *const ClosureHeader, all_args: f64) -> f64 { +pub(crate) extern "C" fn diag_trace_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + all_args: f64, +) -> f64 { // Synthetic-arguments rest array: [fn, position, context, thisArg, ...args] // (#3086). `position` defaults to -1 (the last trailing arg), the callback // lives at `args[position]`, and the wrapped callback is spliced in at that @@ -1904,6 +1846,7 @@ pub(crate) extern "C" fn diag_trace_callback(closure: *const ClosureHeader, all_ pub(crate) extern "C" fn thunk_diag_tracing_channel( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name_or_channels: f64, ) -> f64 { let id = next_diag_id(); diff --git a/crates/perry-runtime/src/node_submodules/diagnostics/user_props.rs b/crates/perry-runtime/src/node_submodules/diagnostics/user_props.rs new file mode 100644 index 0000000000..df69cfe00b --- /dev/null +++ b/crates/perry-runtime/src/node_submodules/diagnostics/user_props.rs @@ -0,0 +1,127 @@ +//! User-assigned own properties on `Error` objects (`err.code = ...`), kept beside the error's diagnostics. + +use super::*; + +unsafe fn error_user_prop_string(value: f64) -> String { + let ptr = crate::value::js_jsvalue_to_string(value); + if ptr.is_null() { + return String::new(); + } + let len = (*ptr).byte_len as usize; + let data = (ptr as *const u8).add(std::mem::size_of::()); + String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() +} + +/// Record a user-assigned own property on an `Error` object. `error_ptr` is +/// the `ErrorHeader` pointer (the NaN-box pointer payload). Called from the +/// `GC_TYPE_ERROR` branch of `js_object_set_field_by_name`. +pub fn set_error_user_prop(error_ptr: usize, key: &str, value: f64) { + if error_ptr == 0 { + return; + } + // #6759 phase 1: the property bag now hangs off the error's own metadata + // record instead of a table keyed by its address, so it moves with the + // error, dies with it, and cannot be inherited by a later tenant of a + // recycled address. Insertion order comes free from the bag object's + // `keys_array`. + unsafe { + let Some(bag) = crate::object::cell_expando_ensure(error_ptr) else { + return; + }; + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + crate::object::js_object_set_field_by_name(bag, key_ptr, value); + } +} + +/// Look up a user-assigned own property on an `Error` object, materialising it +/// back into a NaN-boxed `f64`. Returns `None` if no such property was set. +/// Called from the `GC_TYPE_ERROR` branch of `js_object_get_field_by_name`. +pub fn error_user_prop(error_ptr: usize, key: &str) -> Option { + if error_ptr == 0 { + return None; + } + unsafe { + let bag = crate::object::cell_expando_get(error_ptr)?; + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + // Distinguish "absent" from "present and undefined": a bare get would + // return `undefined` for both, and the caller uses `None` to mean the + // error has no such own property at all. + let key_boxed = f64::from_bits(crate::js_nanbox_string(key_ptr as i64).to_bits()); + if !crate::object::obj_value_has_own_key( + crate::value::js_nanbox_pointer(bag as i64), + key_boxed, + ) { + return None; + } + Some(f64::from_bits( + crate::object::js_object_get_field_by_name(bag, key_ptr).bits(), + )) + } +} + +/// Remove a user-assigned own property from an Error object. Returns true +/// when the property existed (used by `delete err.prop` and data↔accessor +/// descriptor conversions). +pub fn remove_error_user_prop(error_ptr: usize, key: &str) -> bool { + if error_ptr == 0 { + return false; + } + unsafe { + let Some(bag) = crate::object::cell_expando_get(error_ptr) else { + return false; + }; + let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); + let key_boxed = f64::from_bits(crate::js_nanbox_string(key_ptr as i64).to_bits()); + if !crate::object::obj_value_has_own_key( + crate::value::js_nanbox_pointer(bag as i64), + key_boxed, + ) { + return false; + } + crate::object::js_object_delete_field(bag, key_ptr); + true + } +} + +/// Return user-assigned own properties on an Error object as materialized JS +/// values so util.inspect/console formatting can show them. +pub fn error_user_props(error_ptr: usize) -> Vec<(String, f64)> { + if error_ptr == 0 { + return Vec::new(); + } + unsafe { + let Some(bag) = crate::object::cell_expando_get(error_ptr) else { + return Vec::new(); + }; + // The bag is an ordinary object, so its `keys_array` already holds the + // keys in ECMA-262 insertion order — no sort, and no ordering of our + // own to keep in step with node's. + let keys_view = crate::object::object_keys(bag); + let keys = keys_view.arr(); + if keys.is_null() { + return Vec::new(); + } + let len = keys_view.count() as usize; + let mut out = Vec::with_capacity(len); + for i in 0..len { + let key_val = crate::array::js_array_get_f64(keys, i as u32); + // A tombstoned key slot (#9029) reads back as undefined through + // the hole canonicalization (#323); stringifying it would mint a + // phantom "undefined" prop. Undefined is never a legal key. + if key_val.to_bits() == crate::value::TAG_UNDEFINED { + continue; + } + let name_ptr = crate::value::js_jsvalue_to_string(key_val); + if name_ptr.is_null() { + continue; + } + let name = error_user_prop_string(f64::from_bits( + crate::js_nanbox_string(name_ptr as i64).to_bits(), + )); + let value = + f64::from_bits(crate::object::js_object_get_field_by_name(bag, name_ptr).bits()); + out.push((name, value)); + } + out + } +} diff --git a/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs b/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs index 38fe92dc6c..1fe9815e30 100644 --- a/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs +++ b/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs @@ -99,7 +99,10 @@ fn install_dispose(obj: *mut ObjectHeader, method: f64) { } } -pub(crate) extern "C" fn diag_store_scope_dispose(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn diag_store_scope_dispose( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let scope_id = js_closure_get_capture_ptr(closure, 0); let handles = DIAG_STORE_SCOPES.with(|m| { let mut m = m.borrow_mut(); @@ -120,6 +123,7 @@ pub(crate) extern "C" fn diag_store_scope_dispose(closure: *const ClosureHeader) pub(crate) extern "C" fn diag_channel_with_store_scope( closure: *const ClosureHeader, + _this: crate::closure::JsThis, data: f64, ) -> f64 { let id = method_id(closure); @@ -133,7 +137,9 @@ pub(crate) extern "C" fn diag_channel_with_store_scope( for (store, transform) in stores { let context = match transform { StoreTransform::Callable(t) => { - match catch_js(|| js_closure_call1(closure_ptr(t), data)) { + match catch_js(|| { + js_closure_call1(closure_ptr(t), crate::closure::plain_call_receiver(), data) + }) { Ok(context) => context, Err(err) => { schedule_uncaught(err); @@ -208,6 +214,7 @@ pub(crate) fn update_all_bounded_active() { pub(crate) extern "C" fn diag_bounded_subscribe( closure: *const ClosureHeader, + _this: crate::closure::JsThis, handlers: f64, ) -> f64 { let events = bounded_events(method_id(closure)); @@ -227,6 +234,7 @@ pub(crate) extern "C" fn diag_bounded_subscribe( pub(crate) extern "C" fn diag_bounded_unsubscribe( closure: *const ClosureHeader, + _this: crate::closure::JsThis, handlers: f64, ) -> f64 { let events = bounded_events(method_id(closure)); @@ -240,7 +248,11 @@ pub(crate) extern "C" fn diag_bounded_unsubscribe( bool_value(ok) } -pub(crate) extern "C" fn diag_bounded_run(closure: *const ClosureHeader, all_args: f64) -> f64 { +pub(crate) extern "C" fn diag_bounded_run( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + all_args: f64, +) -> f64 { let all = unbox_arg_array(all_args); let undef = undefined(); let context = all.first().copied().unwrap_or(undef); @@ -268,7 +280,10 @@ pub(crate) extern "C" fn diag_bounded_run(closure: *const ClosureHeader, all_arg } } -pub(crate) extern "C" fn diag_bounded_scope_dispose(closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn diag_bounded_scope_dispose( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if js_closure_get_capture_ptr(closure, 2) != 0 { return undefined(); } @@ -281,6 +296,7 @@ pub(crate) extern "C" fn diag_bounded_scope_dispose(closure: *const ClosureHeade pub(crate) extern "C" fn diag_bounded_with_scope( closure: *const ClosureHeader, + _this: crate::closure::JsThis, context: f64, ) -> f64 { let events = bounded_events(method_id(closure)); @@ -306,6 +322,7 @@ fn bounded_run_method_closure(id: i64) -> f64 { pub(crate) extern "C" fn thunk_diag_bounded_channel( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name_or_channels: f64, ) -> f64 { if is_symbol_value(name_or_channels) { diff --git a/crates/perry-runtime/src/node_submodules/fs_promises.rs b/crates/perry-runtime/src/node_submodules/fs_promises.rs index 96da1cd208..1f78e6cab6 100644 --- a/crates/perry-runtime/src/node_submodules/fs_promises.rs +++ b/crates/perry-runtime/src/node_submodules/fs_promises.rs @@ -76,32 +76,60 @@ fn promise_from_result_value(call: impl FnOnce() -> Result) -> f64 { #[no_mangle] pub extern "C" fn js_fs_promises_read_file(path: f64, options: f64) -> f64 { - thunk_fs_promises_readFile(std::ptr::null(), path, options) + thunk_fs_promises_readFile( + std::ptr::null(), + crate::closure::plain_call_receiver(), + path, + options, + ) } #[no_mangle] pub extern "C" fn js_fs_promises_write_file(path: f64, data: f64, options: f64) -> f64 { - thunk_fs_promises_writeFile(std::ptr::null(), path, data, options) + thunk_fs_promises_writeFile( + std::ptr::null(), + crate::closure::plain_call_receiver(), + path, + data, + options, + ) } #[no_mangle] pub extern "C" fn js_fs_promises_append_file(path: f64, data: f64, options: f64) -> f64 { - thunk_fs_promises_appendFile(std::ptr::null(), path, data, options) + thunk_fs_promises_appendFile( + std::ptr::null(), + crate::closure::plain_call_receiver(), + path, + data, + options, + ) } #[no_mangle] pub extern "C" fn js_fs_promises_mkdir(path: f64, options: f64) -> f64 { - thunk_fs_promises_mkdir(std::ptr::null(), path, options) + thunk_fs_promises_mkdir( + std::ptr::null(), + crate::closure::plain_call_receiver(), + path, + options, + ) } #[no_mangle] pub extern "C" fn js_fs_promises_rmdir(path: f64, options: f64) -> f64 { - thunk_fs_promises_rmdir(std::ptr::null(), path, options) + thunk_fs_promises_rmdir( + std::ptr::null(), + crate::closure::plain_call_receiver(), + path, + options, + ) } #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_fs_promises_readFile( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, encoding: f64, ) -> f64 { @@ -114,6 +142,7 @@ pub(crate) extern "C" fn thunk_fs_promises_readFile( pub(crate) extern "C" fn thunk_fs_promises_open( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, flags: f64, _mode: f64, @@ -143,6 +172,7 @@ pub(crate) extern "C" fn thunk_fs_promises_open( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_fs_promises_writeFile( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, data: f64, options: f64, @@ -160,6 +190,7 @@ pub(crate) extern "C" fn thunk_fs_promises_writeFile( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_fs_promises_appendFile( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, data: f64, options: f64, @@ -175,6 +206,7 @@ pub(crate) extern "C" fn thunk_fs_promises_appendFile( pub(crate) extern "C" fn thunk_fs_promises_chmod( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, mode: f64, ) -> f64 { @@ -183,6 +215,7 @@ pub(crate) extern "C" fn thunk_fs_promises_chmod( pub(crate) extern "C" fn thunk_fs_promises_chown( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, uid: f64, gid: f64, @@ -192,6 +225,7 @@ pub(crate) extern "C" fn thunk_fs_promises_chown( pub(crate) extern "C" fn thunk_fs_promises_lchown( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, uid: f64, gid: f64, @@ -203,6 +237,7 @@ pub(crate) extern "C" fn thunk_fs_promises_lchown( pub(crate) extern "C" fn thunk_fs_promises_lchmod( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, mode: f64, ) -> f64 { @@ -219,6 +254,7 @@ pub(crate) extern "C" fn thunk_fs_promises_lchmod( pub(crate) extern "C" fn thunk_fs_promises_mkdir( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -229,6 +265,7 @@ pub(crate) extern "C" fn thunk_fs_promises_mkdir( pub(crate) extern "C" fn thunk_fs_promises_readdir( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -240,6 +277,7 @@ pub(crate) extern "C" fn thunk_fs_promises_readdir( pub(crate) extern "C" fn thunk_fs_promises_stat( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -248,6 +286,7 @@ pub(crate) extern "C" fn thunk_fs_promises_stat( pub(crate) extern "C" fn thunk_fs_promises_statfs( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -256,6 +295,7 @@ pub(crate) extern "C" fn thunk_fs_promises_statfs( pub(crate) extern "C" fn thunk_fs_promises_lstat( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -264,6 +304,7 @@ pub(crate) extern "C" fn thunk_fs_promises_lstat( pub(crate) extern "C" fn thunk_fs_promises_rm( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -272,6 +313,7 @@ pub(crate) extern "C" fn thunk_fs_promises_rm( pub(crate) extern "C" fn thunk_fs_promises_rmdir( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -280,6 +322,7 @@ pub(crate) extern "C" fn thunk_fs_promises_rmdir( pub(crate) extern "C" fn thunk_fs_promises_unlink( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, ) -> f64 { // #9574: writeFile/appendFile are parked on the timer queue, while most @@ -297,6 +340,7 @@ pub(crate) extern "C" fn thunk_fs_promises_unlink( pub(crate) extern "C" fn thunk_fs_promises_rename( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, from: f64, to: f64, ) -> f64 { @@ -306,6 +350,7 @@ pub(crate) extern "C" fn thunk_fs_promises_rename( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_fs_promises_copyFile( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, from: f64, to: f64, flags: f64, @@ -315,6 +360,7 @@ pub(crate) extern "C" fn thunk_fs_promises_copyFile( pub(crate) extern "C" fn thunk_fs_promises_cp( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, from: f64, to: f64, options: f64, @@ -324,6 +370,7 @@ pub(crate) extern "C" fn thunk_fs_promises_cp( pub(crate) extern "C" fn thunk_fs_promises_truncate( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, len: f64, ) -> f64 { @@ -332,6 +379,7 @@ pub(crate) extern "C" fn thunk_fs_promises_truncate( pub(crate) extern "C" fn thunk_fs_promises_utimes( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, atime: f64, mtime: f64, @@ -343,6 +391,7 @@ pub(crate) extern "C" fn thunk_fs_promises_utimes( pub(crate) extern "C" fn thunk_fs_promises_lutimes( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, atime: f64, mtime: f64, @@ -354,6 +403,7 @@ pub(crate) extern "C" fn thunk_fs_promises_lutimes( pub(crate) extern "C" fn thunk_fs_promises_link( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, from: f64, to: f64, ) -> f64 { @@ -362,6 +412,7 @@ pub(crate) extern "C" fn thunk_fs_promises_link( pub(crate) extern "C" fn thunk_fs_promises_symlink( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, target: f64, path: f64, _type: f64, @@ -371,6 +422,7 @@ pub(crate) extern "C" fn thunk_fs_promises_symlink( pub(crate) extern "C" fn thunk_fs_promises_readlink( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -379,6 +431,7 @@ pub(crate) extern "C" fn thunk_fs_promises_readlink( pub(crate) extern "C" fn thunk_fs_promises_realpath( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -387,6 +440,7 @@ pub(crate) extern "C" fn thunk_fs_promises_realpath( pub(crate) extern "C" fn thunk_fs_promises_mkdtemp( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, prefix: f64, options: f64, ) -> f64 { @@ -396,6 +450,7 @@ pub(crate) extern "C" fn thunk_fs_promises_mkdtemp( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_fs_promises_mkdtempDisposable( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, prefix: f64, options: f64, ) -> f64 { @@ -404,6 +459,7 @@ pub(crate) extern "C" fn thunk_fs_promises_mkdtempDisposable( pub(crate) extern "C" fn thunk_fs_promises_opendir( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, ) -> f64 { match crate::fs::js_fs_opendir_value_with_path(path) { @@ -420,6 +476,7 @@ pub(crate) extern "C" fn thunk_fs_promises_opendir( pub(crate) extern "C" fn thunk_fs_promises_glob( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, pattern: f64, options: f64, ) -> f64 { @@ -428,6 +485,7 @@ pub(crate) extern "C" fn thunk_fs_promises_glob( pub(crate) extern "C" fn thunk_fs_promises_watch( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, options: f64, ) -> f64 { @@ -436,6 +494,7 @@ pub(crate) extern "C" fn thunk_fs_promises_watch( pub(crate) extern "C" fn thunk_fs_promises_access( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, path: f64, mode: f64, ) -> f64 { @@ -589,7 +648,12 @@ fn call_write_value(output: f64, text: &str) { if let Some(write) = object_field(output, b"write").filter(|v| is_callable(*v)) { let args = [chunk]; unsafe { - let _ = js_native_call_value(write, args.as_ptr(), args.len()); + let _ = js_native_call_value( + write, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } } } @@ -599,7 +663,7 @@ fn promise_ptr_from_value(value: f64) -> Option<*mut crate::promise::Promise> { } fn readline_bound_method0( - func: extern "C" fn(*const ClosureHeader) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(), this_value: f64, ) -> f64 { js_register_closure_arity(func as *const u8, 0); @@ -609,7 +673,7 @@ fn readline_bound_method0( } fn readline_bound_method1( - func: extern "C" fn(*const ClosureHeader, f64) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(a), this_value: f64, ) -> f64 { js_register_closure_arity(func as *const u8, 1); @@ -619,7 +683,7 @@ fn readline_bound_method1( } fn readline_bound_method2( - func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(a, a), this_value: f64, ) -> f64 { js_register_closure_arity(func as *const u8, 2); @@ -672,7 +736,10 @@ fn abort_pending_question(interface: f64) { crate::promise::js_promise_reject(promise, crate::url::js_abort_error_value()); } -extern "C" fn readline_promises_abort_question(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_promises_abort_question( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { abort_pending_question(this_value(closure)); undefined() } @@ -697,12 +764,19 @@ fn append_interface_input(interface: f64, chunk: f64) { set_value_field(interface, RL_IF_PENDING_TEXT, boxed_str(pending.as_bytes())); } -extern "C" fn readline_promises_input_data(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn readline_promises_input_data( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { append_interface_input(this_value(closure), chunk); undefined() } -extern "C" fn readline_promises_input_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_promises_input_close( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let interface = this_value(closure); cleanup_question_abort_listener(interface); set_value_field(interface, RL_IF_CLOSED, bool_value(true)); @@ -730,7 +804,10 @@ fn attach_interface_input(interface: f64, input: f64) { let _ = crate::node_stream::js_node_stream_method_on(raw, boxed_str(b"close"), close_value); } -extern "C" fn readline_promises_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_promises_close( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let interface = this_value(closure); cleanup_question_abort_listener(interface); set_value_field(interface, RL_IF_PENDING_PROMISE, undefined()); @@ -740,6 +817,7 @@ extern "C" fn readline_promises_close(closure: *const ClosureHeader) -> f64 { extern "C" fn readline_promises_question( closure: *const ClosureHeader, + _this: crate::closure::JsThis, query: f64, options: f64, ) -> f64 { @@ -814,6 +892,7 @@ fn readline_promises_create_interface(opts: f64) -> f64 { #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_readline_createInterface( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, opts: f64, ) -> f64 { readline_promises_create_interface(opts) @@ -822,6 +901,7 @@ pub(crate) extern "C" fn thunk_readline_createInterface( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_readline_Interface( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, opts: f64, ) -> f64 { readline_promises_create_interface(opts) @@ -856,7 +936,10 @@ fn flush_readline_actions(value: f64) { } } -extern "C" fn readline_auto_commit_callback(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_auto_commit_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { flush_readline_actions(this_value(closure)); undefined() } @@ -886,7 +969,11 @@ fn append_readline_action(value: f64, sequence: String) { schedule_readline_auto_commit(value); } -extern "C" fn readline_action_clear_line(closure: *const ClosureHeader, dir: f64) -> f64 { +extern "C" fn readline_action_clear_line( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + dir: f64, +) -> f64 { let value = this_value(closure); let mode = match number_to_i32(dir) { 1 => 0, @@ -897,13 +984,21 @@ extern "C" fn readline_action_clear_line(closure: *const ClosureHeader, dir: f64 value } -extern "C" fn readline_action_clear_screen_down(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_action_clear_screen_down( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let value = this_value(closure); append_readline_action(value, "\x1b[0J".to_string()); value } -extern "C" fn readline_action_cursor_to(closure: *const ClosureHeader, x: f64, y: f64) -> f64 { +extern "C" fn readline_action_cursor_to( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + x: f64, + y: f64, +) -> f64 { let value = this_value(closure); let col = number_to_i32(x).saturating_add(1); let sequence = if is_null_or_undefined(y) { @@ -916,7 +1011,12 @@ extern "C" fn readline_action_cursor_to(closure: *const ClosureHeader, x: f64, y value } -extern "C" fn readline_action_move_cursor(closure: *const ClosureHeader, dx: f64, dy: f64) -> f64 { +extern "C" fn readline_action_move_cursor( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + dx: f64, + dy: f64, +) -> f64 { let value = this_value(closure); let dx = number_to_i32(dx); let dy = number_to_i32(dy); @@ -935,12 +1035,18 @@ extern "C" fn readline_action_move_cursor(closure: *const ClosureHeader, dx: f64 value } -extern "C" fn readline_action_commit(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_action_commit( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { flush_readline_actions(this_value(closure)); promise_value(null_value()) } -extern "C" fn readline_action_rollback(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_action_rollback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let value = this_value(closure); set_value_field(value, RL_ACTION_QUEUE, boxed_str(b"")); set_value_field(value, RL_ACTION_AUTO_PENDING, bool_value(false)); @@ -995,6 +1101,7 @@ pub extern "C" fn js_readline_promises_readline_new(output: f64, options: f64) - #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_readline_Readline( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, output: f64, options: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/node_submodules/hono_jsx.rs b/crates/perry-runtime/src/node_submodules/hono_jsx.rs index 3991b95b17..e524aad3f2 100644 --- a/crates/perry-runtime/src/node_submodules/hono_jsx.rs +++ b/crates/perry-runtime/src/node_submodules/hono_jsx.rs @@ -30,6 +30,7 @@ fn fragment_marker_f64() -> f64 { /// `jsx(type, props)` — forward to the built-in renderer. pub(crate) extern "C" fn thunk_hono_jsx( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, type_arg: f64, props: f64, ) -> f64 { @@ -39,6 +40,7 @@ pub(crate) extern "C" fn thunk_hono_jsx( /// `jsxs(type, props)` — multi-child shape, same dispatch. pub(crate) extern "C" fn thunk_hono_jsxs( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, type_arg: f64, props: f64, ) -> f64 { @@ -48,14 +50,22 @@ pub(crate) extern "C" fn thunk_hono_jsxs( /// `Fragment` — when used as a component (`jsx(Fragment, { children })`) it /// renders its children with no wrapping element. Reuse `js_jsx`'s Fragment /// path by passing the `"__Fragment"` marker tag. -pub(crate) extern "C" fn thunk_hono_fragment(_closure: *const ClosureHeader, props: f64) -> f64 { +pub(crate) extern "C" fn thunk_hono_fragment( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + props: f64, +) -> f64 { crate::jsx::js_jsx(fragment_marker_f64(), props) } /// `Suspense` — Perry renders synchronously (server-side), so a Suspense /// boundary just renders its children (the fallback is never needed because /// there is no streaming-suspension point). Same shape as Fragment. -pub(crate) extern "C" fn thunk_hono_suspense(_closure: *const ClosureHeader, props: f64) -> f64 { +pub(crate) extern "C" fn thunk_hono_suspense( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + props: f64, +) -> f64 { crate::jsx::js_jsx(fragment_marker_f64(), props) } @@ -63,7 +73,11 @@ pub(crate) extern "C" fn thunk_hono_suspense(_closure: *const ClosureHeader, pro /// (`js_jsx` boxes nodes itself), so this is an exposed stub returning the /// props/value it was handed. Its sole purpose is to make the named export /// resolve with `typeof === "function"`. -pub(crate) extern "C" fn thunk_hono_jsxnode(_closure: *const ClosureHeader, arg: f64) -> f64 { +pub(crate) extern "C" fn thunk_hono_jsxnode( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { arg } @@ -88,6 +102,7 @@ pub unsafe extern "C" fn js_register_jsx_render_stream(f: RenderStreamFn) { /// when no stream backend is linked. pub(crate) extern "C" fn thunk_hono_render_to_readable_stream( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, node: f64, _options: f64, ) -> f64 { @@ -105,12 +120,17 @@ pub(crate) extern "C" fn thunk_hono_render_to_readable_stream( // value is at least usable (degraded vs. a real ReadableStream). return html_value; } + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let f: RenderStreamFn = unsafe { std::mem::transmute(raw) }; unsafe { f(html_value) } } /// `undefined` helper for any export we deliberately don't back. #[allow(dead_code)] -pub(crate) extern "C" fn thunk_hono_undefined(_closure: *const ClosureHeader, _arg: f64) -> f64 { +pub(crate) extern "C" fn thunk_hono_undefined( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { f64::from_bits(TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/node_submodules/mod.rs b/crates/perry-runtime/src/node_submodules/mod.rs index 92561cd327..f876c2739c 100644 --- a/crates/perry-runtime/src/node_submodules/mod.rs +++ b/crates/perry-runtime/src/node_submodules/mod.rs @@ -24,7 +24,7 @@ use std::cell::RefCell; use std::sync::atomic::{AtomicI64, Ordering}; use crate::closure::{ - js_closure_alloc, js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call_array, + js_closure_alloc, js_closure_call0, js_closure_call1, js_closure_call2, js_closure_get_capture_ptr, js_closure_set_capture_ptr, js_register_closure_arity, ClosureHeader, }; @@ -48,9 +48,9 @@ struct ExportSpec { } enum ExportThunk { - Fn1(extern "C" fn(*const ClosureHeader, f64) -> f64), - Fn2(extern "C" fn(*const ClosureHeader, f64, f64) -> f64), - Fn3(extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64), + Fn1(crate::closure::body_call::js_body_fn_ty!(a)), + Fn2(crate::closure::body_call::js_body_fn_ty!(a, a)), + Fn3(crate::closure::body_call::js_body_fn_ty!(a, a, a)), } impl ExportThunk { @@ -96,6 +96,7 @@ macro_rules! thunk { #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn $name( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { let msg: &'static str = $msg; @@ -197,6 +198,7 @@ thunk!( extern "C" fn thunk_vm_create_context( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, sandbox: f64, options: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/node_submodules/stream_promises.rs b/crates/perry-runtime/src/node_submodules/stream_promises.rs index 1b6d5e1120..59d409d09f 100644 --- a/crates/perry-runtime/src/node_submodules/stream_promises.rs +++ b/crates/perry-runtime/src/node_submodules/stream_promises.rs @@ -176,7 +176,10 @@ pub(crate) fn signal_reason(_signal: f64) -> f64 { abort_error_value() } -extern "C" fn stream_promises_abort_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_promises_abort_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let promise_value = js_closure_get_capture_f64(closure, 0); let signal = js_closure_get_capture_f64(closure, 1); let promise = @@ -216,6 +219,7 @@ fn event_value(name: &[u8]) -> f64 { extern "C" fn stream_promises_finished_error_listener( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, ) -> f64 { let state = js_closure_get_capture_ptr(closure, 0) as *mut ObjectHeader; @@ -223,7 +227,10 @@ extern "C" fn stream_promises_finished_error_listener( undefined_value() } -extern "C" fn stream_promises_finished_side_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_promises_finished_side_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let state = js_closure_get_capture_ptr(closure, 0) as *mut ObjectHeader; let readable_side = js_closure_get_capture_f64(closure, 1).to_bits() == crate::value::TAG_TRUE; if readable_side { @@ -235,7 +242,10 @@ extern "C" fn stream_promises_finished_side_listener(closure: *const ClosureHead undefined_value() } -extern "C" fn stream_promises_finished_close_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn stream_promises_finished_close_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let state = js_closure_get_capture_ptr(closure, 0) as *mut ObjectHeader; if finished_state_bool(state, b"settled") { return undefined_value(); @@ -442,11 +452,14 @@ fn invoke_destination_method(destination: f64, method: &[u8], args: &[f64]) -> f let Some(func) = get_object_property(destination, method) else { return undefined_value(); }; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(destination)); - let result = unsafe { crate::closure::js_native_call_value(func, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + func, + crate::closure::JsThis::from_f64(destination), + args.as_ptr(), + args.len(), + ) + } } fn write_chunks_to_destination(destination: f64, chunks: &[f64]) { @@ -537,6 +550,7 @@ fn validate_pipeline_promise_args(source: f64, destination: f64) -> Option extern "C" fn stream_promises_pipeline_callback( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, value: f64, ) -> f64 { @@ -562,6 +576,7 @@ fn catch_stream_promises_throw(call: impl FnOnce()) -> Result<(), f64> { #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_streamP_pipeline( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, source: f64, destination: f64, options_or_rest: f64, @@ -607,6 +622,7 @@ pub(crate) extern "C" fn thunk_streamP_pipeline( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_streamP_finished( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, stream: f64, options: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/node_submodules/test.rs b/crates/perry-runtime/src/node_submodules/test.rs index 15acb0c498..a00a1c2b8d 100644 --- a/crates/perry-runtime/src/node_submodules/test.rs +++ b/crates/perry-runtime/src/node_submodules/test.rs @@ -218,13 +218,21 @@ fn assert_mock_target_method(value: f64) { } } -extern "C" fn mock_timers_enable(_closure: *const ClosureHeader, options: f64) -> f64 { +extern "C" fn mock_timers_enable( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + options: f64, +) -> f64 { let (apis, now) = parse_mock_timer_options(options); crate::timer::js_mock_timers_enable(apis, now); undefined_value() } -extern "C" fn mock_timers_tick(_closure: *const ClosureHeader, ms: f64) -> f64 { +extern "C" fn mock_timers_tick( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ms: f64, +) -> f64 { let delay = if is_undefined_value(ms) { 1.0 } else { @@ -234,18 +242,28 @@ extern "C" fn mock_timers_tick(_closure: *const ClosureHeader, ms: f64) -> f64 { undefined_value() } -extern "C" fn mock_timers_run_all(_closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_timers_run_all( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::timer::js_mock_timers_run_all(); undefined_value() } -extern "C" fn mock_timers_set_time(_closure: *const ClosureHeader, ms: f64) -> f64 { +extern "C" fn mock_timers_set_time( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ms: f64, +) -> f64 { let time = validate_mock_timer_number("time", ms, false); crate::timer::js_mock_timers_set_time(time); undefined_value() } -extern "C" fn mock_timers_reset(_closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_timers_reset( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::timer::js_mock_timers_reset(); undefined_value() } @@ -775,7 +793,11 @@ fn record_mock_call(id: i64, args_value: f64, this_value: f64, result: f64, erro #[path = "test_metadata_unit_tests.rs"] mod metadata_tests; -extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn mock_function_invoke( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { let id = closure_id(closure); let args = array_values(rest).unwrap_or_default(); let (implementation, is_symbol_method) = MOCK_STATES.with(|states| { @@ -789,7 +811,7 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> ) }); - let this_value = crate::object::js_implicit_this_get(); + let this_value = this.as_f64(); if JSValue::from_bits(implementation.to_bits()).is_undefined() { if is_symbol_method { let scope = crate::gc::RuntimeHandleScope::new(); @@ -818,15 +840,15 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> let rest_handle = scope.root_nanbox_f64(rest); let arg_handles = scope.root_nanbox_f64_slice(&args); let call_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); // #9445 + let this_handle = scope.root_nanbox_f64(this_value); let call_result = catch_js(|| unsafe { - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( implementation_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), call_args.as_ptr(), call_args.len(), ) }); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match call_result { Ok(result) => { @@ -854,7 +876,10 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> } } -extern "C" fn mock_context_call_count(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_context_call_count( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = closure_id(closure); MOCK_STATES.with(|states| { states @@ -872,7 +897,10 @@ extern "C" fn mock_context_call_count(closure: *const ClosureHeader) -> f64 { }) } -extern "C" fn mock_context_reset_calls(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_context_reset_calls( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = closure_id(closure); MOCK_STATES.with(|states| { if let Some(state) = states.borrow_mut().iter_mut().find(|state| state.id == id) { @@ -884,6 +912,7 @@ extern "C" fn mock_context_reset_calls(closure: *const ClosureHeader) -> f64 { extern "C" fn mock_context_mock_implementation( closure: *const ClosureHeader, + _this: crate::closure::JsThis, implementation: f64, ) -> f64 { assert_callable_arg("implementation", implementation); @@ -898,6 +927,7 @@ extern "C" fn mock_context_mock_implementation( extern "C" fn mock_context_mock_implementation_once( closure: *const ClosureHeader, + _this: crate::closure::JsThis, implementation: f64, on_call: f64, ) -> f64 { @@ -929,13 +959,17 @@ extern "C" fn mock_context_mock_implementation_once( undefined_value() } -extern "C" fn mock_context_restore(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_context_restore( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { restore_mock_state(closure_id(closure)); undefined_value() } extern "C" fn mock_fn_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, original: f64, implementation_or_options: f64, options: f64, @@ -970,6 +1004,7 @@ extern "C" fn mock_fn_thunk( extern "C" fn mock_method_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, target: f64, property: f64, implementation: f64, @@ -1100,6 +1135,7 @@ fn create_getter_mock(target: f64, property: f64, implementation: f64, times: Op extern "C" fn mock_getter_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, target: f64, property: f64, implementation: f64, @@ -1167,6 +1203,7 @@ fn create_setter_mock(target: f64, property: f64, implementation: f64, times: Op extern "C" fn mock_setter_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, target: f64, property: f64, implementation: f64, @@ -1190,7 +1227,10 @@ extern "C" fn mock_setter_thunk( ) } -extern "C" fn mock_reset_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_reset_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { restore_tracked_mocks(); MOCK_STATES.with(|states| { for state in states.borrow_mut().iter_mut() { @@ -1216,7 +1256,10 @@ fn restore_tracked_mocks() { property_mock::restore_all(); } -extern "C" fn mock_restore_all_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_restore_all_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { restore_tracked_mocks(); undefined_value() } @@ -1301,14 +1344,22 @@ fn mock_object_value() -> f64 { }) } -extern "C" fn test_context_diagnostic(_closure: *const ClosureHeader, message: f64) -> f64 { +extern "C" fn test_context_diagnostic( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + message: f64, +) -> f64 { let message = value_to_string(message).unwrap_or_else(|| crate::builtins::format_jsvalue(message, 0)); CURRENT_DIAGNOSTICS.with(|diagnostics| diagnostics.borrow_mut().push(message)); undefined_value() } -extern "C" fn test_context_plan(_closure: *const ClosureHeader, expected: f64) -> f64 { +extern "C" fn test_context_plan( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + expected: f64, +) -> f64 { let n = crate::builtins::js_number_coerce(expected); if !n.is_finite() || n < 0.0 { let message = format!( @@ -1321,7 +1372,11 @@ extern "C" fn test_context_plan(_closure: *const ClosureHeader, expected: f64) - undefined_value() } -extern "C" fn test_context_skip(_closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn test_context_skip( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { CURRENT_TEST_OVERRIDE.with(|slot| slot.set(TEST_OVERRIDE_SKIP)); if let Some(reason) = value_to_string(reason) { CURRENT_DIAGNOSTICS @@ -1330,7 +1385,11 @@ extern "C" fn test_context_skip(_closure: *const ClosureHeader, reason: f64) -> undefined_value() } -extern "C" fn test_context_todo(_closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn test_context_todo( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { CURRENT_TEST_OVERRIDE.with(|slot| slot.set(TEST_OVERRIDE_TODO)); if let Some(reason) = value_to_string(reason) { CURRENT_DIAGNOSTICS @@ -1347,6 +1406,7 @@ macro_rules! test_context_assertion3 { ($name:ident, $assertion:path) => { extern "C" fn $name( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, actual: f64, expected: f64, message: f64, @@ -1399,6 +1459,7 @@ test_context_assertion3!(test_context_assert_throws, crate::object::js_assert_th extern "C" fn test_context_assert_ok( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, message: f64, ) -> f64 { @@ -1406,12 +1467,20 @@ extern "C" fn test_context_assert_ok( crate::object::js_assert_ok(value, message) } -extern "C" fn test_context_assert_fail(_closure: *const ClosureHeader, message: f64) -> f64 { +extern "C" fn test_context_assert_fail( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + message: f64, +) -> f64 { record_test_context_assertion(); crate::object::js_assert_fail(message) } -extern "C" fn test_context_assert_if_error(_closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn test_context_assert_if_error( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { record_test_context_assertion(); crate::object::js_assert_if_error(value) } @@ -1530,6 +1599,7 @@ pub extern "C" fn js_node_test_mock_fn( ) -> f64 { mock_fn_thunk( std::ptr::null(), + crate::closure::plain_call_receiver(), original, implementation_or_options, options, @@ -1543,7 +1613,14 @@ pub extern "C" fn js_node_test_mock_method( implementation: f64, options: f64, ) -> f64 { - mock_method_thunk(std::ptr::null(), target, property, implementation, options) + mock_method_thunk( + std::ptr::null(), + crate::closure::plain_call_receiver(), + target, + property, + implementation, + options, + ) } #[no_mangle] @@ -1553,7 +1630,14 @@ pub extern "C" fn js_node_test_mock_getter( implementation: f64, options: f64, ) -> f64 { - mock_getter_thunk(std::ptr::null(), target, property, implementation, options) + mock_getter_thunk( + std::ptr::null(), + crate::closure::plain_call_receiver(), + target, + property, + implementation, + options, + ) } #[no_mangle] @@ -1563,7 +1647,14 @@ pub extern "C" fn js_node_test_mock_setter( implementation: f64, options: f64, ) -> f64 { - mock_setter_thunk(std::ptr::null(), target, property, implementation, options) + mock_setter_thunk( + std::ptr::null(), + crate::closure::plain_call_receiver(), + target, + property, + implementation, + options, + ) } #[no_mangle] @@ -1583,47 +1674,59 @@ pub extern "C" fn js_node_test_mock_property_with_presence( #[no_mangle] pub extern "C" fn js_node_test_mock_reset() -> f64 { - mock_reset_thunk(std::ptr::null()) + mock_reset_thunk(std::ptr::null(), crate::closure::plain_call_receiver()) } #[no_mangle] pub extern "C" fn js_node_test_mock_restore_all() -> f64 { - mock_restore_all_thunk(std::ptr::null()) + mock_restore_all_thunk(std::ptr::null(), crate::closure::plain_call_receiver()) } #[no_mangle] pub extern "C" fn js_node_test_snapshot_set_default_serializers(serializers: f64) -> f64 { - snapshot_set_default_serializers(std::ptr::null(), serializers) + snapshot_set_default_serializers( + std::ptr::null(), + crate::closure::plain_call_receiver(), + serializers, + ) } #[no_mangle] pub extern "C" fn js_node_test_snapshot_set_resolve_snapshot_path(resolver: f64) -> f64 { - snapshot_set_resolve_snapshot_path(std::ptr::null(), resolver) + snapshot_set_resolve_snapshot_path( + std::ptr::null(), + crate::closure::plain_call_receiver(), + resolver, + ) } #[no_mangle] pub extern "C" fn js_node_test_mock_timers_enable(options: f64) -> f64 { - mock_timers_enable(std::ptr::null(), options) + mock_timers_enable( + std::ptr::null(), + crate::closure::plain_call_receiver(), + options, + ) } #[no_mangle] pub extern "C" fn js_node_test_mock_timers_tick(ms: f64) -> f64 { - mock_timers_tick(std::ptr::null(), ms) + mock_timers_tick(std::ptr::null(), crate::closure::plain_call_receiver(), ms) } #[no_mangle] pub extern "C" fn js_node_test_mock_timers_run_all() -> f64 { - mock_timers_run_all(std::ptr::null()) + mock_timers_run_all(std::ptr::null(), crate::closure::plain_call_receiver()) } #[no_mangle] pub extern "C" fn js_node_test_mock_timers_set_time(ms: f64) -> f64 { - mock_timers_set_time(std::ptr::null(), ms) + mock_timers_set_time(std::ptr::null(), crate::closure::plain_call_receiver(), ms) } #[no_mangle] pub extern "C" fn js_node_test_mock_timers_reset() -> f64 { - mock_timers_reset(std::ptr::null()) + mock_timers_reset(std::ptr::null(), crate::closure::plain_call_receiver()) } pub(crate) fn decorate_test_export( diff --git a/crates/perry-runtime/src/node_submodules/test_metadata_unit_tests.rs b/crates/perry-runtime/src/node_submodules/test_metadata_unit_tests.rs index 72d3699d0b..b8ed9b1290 100644 --- a/crates/perry-runtime/src/node_submodules/test_metadata_unit_tests.rs +++ b/crates/perry-runtime/src/node_submodules/test_metadata_unit_tests.rs @@ -1,6 +1,11 @@ use super::*; -extern "C" fn original_two_args(_closure: *const ClosureHeader, _first: f64, _second: f64) -> f64 { +extern "C" fn original_two_args( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _first: f64, + _second: f64, +) -> f64 { undefined_value() } diff --git a/crates/perry-runtime/src/node_submodules/test_once_unit_tests.rs b/crates/perry-runtime/src/node_submodules/test_once_unit_tests.rs index b87c52cb40..4852eb8bf3 100644 --- a/crates/perry-runtime/src/node_submodules/test_once_unit_tests.rs +++ b/crates/perry-runtime/src/node_submodules/test_once_unit_tests.rs @@ -5,21 +5,27 @@ thread_local! { static REENTRANT_ACTIVE: Cell = const { Cell::new(false) }; } -extern "C" fn reentrant_implementation(_closure: *const ClosureHeader) -> f64 { +extern "C" fn reentrant_implementation( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let recurse = REENTRANT_ACTIVE.with(|active| !active.replace(true)); if recurse { let mock = REENTRANT_MOCK.with(Cell::get); - js_closure_call0(raw_ptr_from_value(mock) as *const ClosureHeader); + js_closure_call0( + raw_ptr_from_value(mock) as *const ClosureHeader, + crate::closure::plain_call_receiver(), + ); REENTRANT_ACTIVE.with(|active| active.set(false)); } 10.0 } -extern "C" fn return_twenty(_closure: *const ClosureHeader) -> f64 { +extern "C" fn return_twenty(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 20.0 } -extern "C" fn return_thirty(_closure: *const ClosureHeader) -> f64 { +extern "C" fn return_thirty(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 30.0 } @@ -125,7 +131,10 @@ fn reentrant_dispatch_uses_completed_call_indices_like_node() { let state = states.iter_mut().find(|state| state.id == id).unwrap(); schedule_mock_implementation_once(state, 1, explicit_once); }); - assert_eq!(js_closure_call0(mock_ptr), 10.0); + assert_eq!( + js_closure_call0(mock_ptr, crate::closure::plain_call_receiver()), + 10.0 + ); MOCK_STATES.with(|states| { let mut states = states.borrow_mut(); @@ -138,7 +147,10 @@ fn reentrant_dispatch_uses_completed_call_indices_like_node() { let current = mock_state_call_count(state); schedule_mock_implementation_once(state, current, scheduled_inside); }); - assert_eq!(js_closure_call0(mock_ptr), 30.0); + assert_eq!( + js_closure_call0(mock_ptr, crate::closure::plain_call_receiver()), + 30.0 + ); MOCK_STATES.with(|states| { let states = states.borrow(); diff --git a/crates/perry-runtime/src/node_submodules/test_property.rs b/crates/perry-runtime/src/node_submodules/test_property.rs index 6100c0beb6..cc8818ffad 100644 --- a/crates/perry-runtime/src/node_submodules/test_property.rs +++ b/crates/perry-runtime/src/node_submodules/test_property.rs @@ -286,7 +286,10 @@ fn record_property_access(id: i64, access_type: &str, fallback: f64) -> f64 { value.get_nanbox_f64() } -extern "C" fn mock_property_get(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_property_get( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = closure_id(closure); let value = PROPERTY_STATES.with(|states| { states @@ -299,7 +302,11 @@ extern "C" fn mock_property_get(closure: *const ClosureHeader) -> f64 { record_property_access(id, "get", value) } -extern "C" fn mock_property_set(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn mock_property_set( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { let id = closure_id(closure); let (writable, property) = PROPERTY_STATES.with(|states| { states @@ -322,7 +329,10 @@ extern "C" fn mock_property_set(closure: *const ClosureHeader, value: f64) -> f6 undefined_value() } -extern "C" fn mock_property_access_count(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_property_access_count( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = closure_id(closure); PROPERTY_STATES.with(|states| { states @@ -334,7 +344,10 @@ extern "C" fn mock_property_access_count(closure: *const ClosureHeader) -> f64 { }) } -extern "C" fn mock_property_accesses(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_property_accesses( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = closure_id(closure); let accesses = PROPERTY_STATES.with(|states| { states @@ -354,7 +367,10 @@ extern "C" fn mock_property_accesses(closure: *const ClosureHeader) -> f64 { boxed_ptr(crate::array::js_array_slice(ptr, 0, len as i32)) } -extern "C" fn mock_property_reset_accesses(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_property_reset_accesses( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = closure_id(closure); let accesses = boxed_ptr(crate::array::js_array_alloc(0)); PROPERTY_STATES.with(|states| { @@ -366,12 +382,17 @@ extern "C" fn mock_property_reset_accesses(closure: *const ClosureHeader) -> f64 undefined_value() } -extern "C" fn mock_property_implementation(closure: *const ClosureHeader, value: f64) -> f64 { - mock_property_set(closure, value) +extern "C" fn mock_property_implementation( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + mock_property_set(closure, this, value) } extern "C" fn mock_property_implementation_once( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, rest: f64, ) -> f64 { @@ -407,13 +428,17 @@ extern "C" fn mock_property_implementation_once( undefined_value() } -extern "C" fn mock_property_restore(closure: *const ClosureHeader) -> f64 { +extern "C" fn mock_property_restore( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { restore_property_state(closure_id(closure)); undefined_value() } extern "C" fn mock_property_proxy_get( closure: *const ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, receiver: f64, @@ -434,6 +459,7 @@ extern "C" fn mock_property_proxy_get( extern "C" fn tracker_property_thunk( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, target: f64, property: f64, rest: f64, @@ -587,7 +613,10 @@ mod tests { let context = proxy_context(proxy); let count = object_property(context, b"accessCount").expect("accessCount"); assert_eq!( - js_closure_call0(raw_ptr_from_value(count) as *const ClosureHeader), + js_closure_call0( + raw_ptr_from_value(count) as *const ClosureHeader, + crate::closure::plain_call_receiver() + ), 1.0 ); restore_all(); @@ -609,12 +638,14 @@ mod tests { object_property(context, b"mockImplementation").expect("mockImplementation"); js_closure_call1( raw_ptr_from_value(implementation) as *const ClosureHeader, + crate::closure::plain_call_receiver(), 9.0, ); let once = object_property(context, b"mockImplementationOnce").expect("mockImplementationOnce"); crate::closure::js_closure_call2( raw_ptr_from_value(once) as *const ClosureHeader, + crate::closure::plain_call_receiver(), 4.0, 1.0, ); @@ -623,7 +654,10 @@ mod tests { assert_eq!(get_property_value(target, "value"), 9.0); let count = object_property(context, b"accessCount").expect("accessCount"); assert_eq!( - js_closure_call0(raw_ptr_from_value(count) as *const ClosureHeader), + js_closure_call0( + raw_ptr_from_value(count) as *const ClosureHeader, + crate::closure::plain_call_receiver() + ), 3.0 ); let accesses = object_property(context, b"accesses").expect("accesses"); diff --git a/crates/perry-runtime/src/node_submodules/test_reporters.rs b/crates/perry-runtime/src/node_submodules/test_reporters.rs index 1d647dbd73..8f6877931a 100644 --- a/crates/perry-runtime/src/node_submodules/test_reporters.rs +++ b/crates/perry-runtime/src/node_submodules/test_reporters.rs @@ -15,23 +15,43 @@ fn reporter_with_kind(kind: i32, source: f64) -> f64 { readable_from_text(output) } -pub(crate) extern "C" fn thunk_reporter_spec(_closure: *const ClosureHeader, source: f64) -> f64 { +pub(crate) extern "C" fn thunk_reporter_spec( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + source: f64, +) -> f64 { reporter_with_kind(REPORTER_SPEC, source) } -pub(crate) extern "C" fn thunk_reporter_tap(_closure: *const ClosureHeader, source: f64) -> f64 { +pub(crate) extern "C" fn thunk_reporter_tap( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + source: f64, +) -> f64 { reporter_with_kind(REPORTER_TAP, source) } -pub(crate) extern "C" fn thunk_reporter_dot(_closure: *const ClosureHeader, source: f64) -> f64 { +pub(crate) extern "C" fn thunk_reporter_dot( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + source: f64, +) -> f64 { reporter_with_kind(REPORTER_DOT, source) } -pub(crate) extern "C" fn thunk_reporter_junit(_closure: *const ClosureHeader, source: f64) -> f64 { +pub(crate) extern "C" fn thunk_reporter_junit( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + source: f64, +) -> f64 { reporter_with_kind(REPORTER_JUNIT, source) } -pub(crate) extern "C" fn thunk_reporter_lcov(_closure: *const ClosureHeader, source: f64) -> f64 { +pub(crate) extern "C" fn thunk_reporter_lcov( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + source: f64, +) -> f64 { reporter_with_kind(REPORTER_LCOV, source) } @@ -49,6 +69,7 @@ fn reporter_transform(kind: i32) -> f64 { extern "C" fn reporter_transform_chunk( closure: *const ClosureHeader, + this: crate::closure::JsThis, chunk: f64, _encoding: f64, callback: f64, @@ -75,12 +96,15 @@ extern "C" fn reporter_transform_chunk( }); } if !output.is_empty() { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let handle = (this.to_bits() & POINTER_MASK) as i64; crate::node_stream::js_node_stream_method_push(handle, string_value(&output)); } if is_callable_value(callback) { - js_closure_call0(raw_ptr_from_value(callback) as *const ClosureHeader); + js_closure_call0( + raw_ptr_from_value(callback) as *const ClosureHeader, + crate::closure::plain_call_receiver(), + ); } undefined_value() } diff --git a/crates/perry-runtime/src/node_submodules/test_runner.rs b/crates/perry-runtime/src/node_submodules/test_runner.rs index a1c4a17821..9d1c821f5c 100644 --- a/crates/perry-runtime/src/node_submodules/test_runner.rs +++ b/crates/perry-runtime/src/node_submodules/test_runner.rs @@ -361,7 +361,11 @@ fn take_directive_reason( } } -extern "C" fn test_done(_closure: *const ClosureHeader, error: f64) -> f64 { +extern "C" fn test_done( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + error: f64, +) -> f64 { if !is_undefined_value(error) && !JSValue::from_bits(error.to_bits()).is_null() { crate::exception::js_throw(error); } @@ -381,11 +385,16 @@ fn call_test_callback(callback: f64, name: &str) -> Result { let done = scope.root_nanbox_f64(closure_value(test_done as *const u8, 1)); crate::closure::js_closure_call2( callback_ptr, + crate::closure::plain_call_receiver(), context.get_nanbox_f64(), done.get_nanbox_f64(), ) } else { - js_closure_call1(callback_ptr, context.get_nanbox_f64()) + js_closure_call1( + callback_ptr, + crate::closure::plain_call_receiver(), + context.get_nanbox_f64(), + ) } }) } @@ -422,7 +431,13 @@ fn run_hook(callback: f64, context_name: &str) -> Result<(), f64> { let scope = crate::gc::RuntimeHandleScope::new(); let context = scope.root_nanbox_f64(test_context_value(context_name)); let callback_ptr = raw_ptr_from_value(callback) as *const ClosureHeader; - let result = catch_js(|| js_closure_call1(callback_ptr, context.get_nanbox_f64()))?; + let result = catch_js(|| { + js_closure_call1( + callback_ptr, + crate::closure::plain_call_receiver(), + context.get_nanbox_f64(), + ) + })?; await_callback_result(result) } @@ -722,7 +737,10 @@ fn print_summary(stats: &Stats) { println!("ℹ duration_ms 0"); } -extern "C" fn test_runner_task(_closure: *const ClosureHeader) -> f64 { +extern "C" fn test_runner_task( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let root = TEST_RUNNER.with(|runner| { let mut runner = runner.borrow_mut(); runner.running = true; @@ -766,6 +784,7 @@ extern "C" fn test_runner_task(_closure: *const ClosureHeader) -> f64 { pub(crate) extern "C" fn thunk_test( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -775,6 +794,7 @@ pub(crate) extern "C" fn thunk_test( pub(crate) extern "C" fn thunk_test_skip( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -784,6 +804,7 @@ pub(crate) extern "C" fn thunk_test_skip( pub(crate) extern "C" fn thunk_test_todo( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -793,6 +814,7 @@ pub(crate) extern "C" fn thunk_test_todo( pub(crate) extern "C" fn thunk_test_only( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -802,6 +824,7 @@ pub(crate) extern "C" fn thunk_test_only( pub(crate) extern "C" fn thunk_test_suite( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -811,6 +834,7 @@ pub(crate) extern "C" fn thunk_test_suite( pub(crate) extern "C" fn thunk_test_suite_skip( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -820,6 +844,7 @@ pub(crate) extern "C" fn thunk_test_suite_skip( pub(crate) extern "C" fn thunk_test_suite_todo( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -829,6 +854,7 @@ pub(crate) extern "C" fn thunk_test_suite_todo( pub(crate) extern "C" fn thunk_test_suite_only( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, name: f64, options: f64, callback: f64, @@ -836,16 +862,25 @@ pub(crate) extern "C" fn thunk_test_suite_only( register_suite(TestMode::Only, name, options, callback) } -pub(crate) extern "C" fn thunk_test_before(_closure: *const ClosureHeader, callback: f64) -> f64 { +pub(crate) extern "C" fn thunk_test_before( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + callback: f64, +) -> f64 { register_hook(HookKind::Before, callback) } -pub(crate) extern "C" fn thunk_test_after(_closure: *const ClosureHeader, callback: f64) -> f64 { +pub(crate) extern "C" fn thunk_test_after( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + callback: f64, +) -> f64 { register_hook(HookKind::After, callback) } pub(crate) extern "C" fn thunk_test_before_each( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, callback: f64, ) -> f64 { register_hook(HookKind::BeforeEach, callback) @@ -853,12 +888,17 @@ pub(crate) extern "C" fn thunk_test_before_each( pub(crate) extern "C" fn thunk_test_after_each( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, callback: f64, ) -> f64 { register_hook(HookKind::AfterEach, callback) } -pub(crate) extern "C" fn thunk_test_run(_closure: *const ClosureHeader, _options: f64) -> f64 { +pub(crate) extern "C" fn thunk_test_run( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _options: f64, +) -> f64 { let mut array = crate::array::js_array_alloc(0); for _ in 0..10 { array = crate::array::js_array_push_f64(array, undefined_value()); @@ -868,52 +908,102 @@ pub(crate) extern "C" fn thunk_test_run(_closure: *const ClosureHeader, _options #[no_mangle] pub extern "C" fn js_node_test_register(name: f64, options: f64, callback: f64) -> f64 { - thunk_test(std::ptr::null(), name, options, callback) + thunk_test( + std::ptr::null(), + crate::closure::plain_call_receiver(), + name, + options, + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_skip(name: f64, options: f64, callback: f64) -> f64 { - thunk_test_skip(std::ptr::null(), name, options, callback) + thunk_test_skip( + std::ptr::null(), + crate::closure::plain_call_receiver(), + name, + options, + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_todo(name: f64, options: f64, callback: f64) -> f64 { - thunk_test_todo(std::ptr::null(), name, options, callback) + thunk_test_todo( + std::ptr::null(), + crate::closure::plain_call_receiver(), + name, + options, + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_only(name: f64, options: f64, callback: f64) -> f64 { - thunk_test_only(std::ptr::null(), name, options, callback) + thunk_test_only( + std::ptr::null(), + crate::closure::plain_call_receiver(), + name, + options, + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_suite(name: f64, options: f64, callback: f64) -> f64 { - thunk_test_suite(std::ptr::null(), name, options, callback) + thunk_test_suite( + std::ptr::null(), + crate::closure::plain_call_receiver(), + name, + options, + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_before(callback: f64) -> f64 { - thunk_test_before(std::ptr::null(), callback) + thunk_test_before( + std::ptr::null(), + crate::closure::plain_call_receiver(), + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_after(callback: f64) -> f64 { - thunk_test_after(std::ptr::null(), callback) + thunk_test_after( + std::ptr::null(), + crate::closure::plain_call_receiver(), + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_before_each(callback: f64) -> f64 { - thunk_test_before_each(std::ptr::null(), callback) + thunk_test_before_each( + std::ptr::null(), + crate::closure::plain_call_receiver(), + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_after_each(callback: f64) -> f64 { - thunk_test_after_each(std::ptr::null(), callback) + thunk_test_after_each( + std::ptr::null(), + crate::closure::plain_call_receiver(), + callback, + ) } #[no_mangle] pub extern "C" fn js_node_test_run(options: f64) -> f64 { - thunk_test_run(std::ptr::null(), options) + thunk_test_run( + std::ptr::null(), + crate::closure::plain_call_receiver(), + options, + ) } pub(crate) fn scan_node_test_runner_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { diff --git a/crates/perry-runtime/src/node_submodules/test_snapshot.rs b/crates/perry-runtime/src/node_submodules/test_snapshot.rs index 4f169e6818..53102e35f1 100644 --- a/crates/perry-runtime/src/node_submodules/test_snapshot.rs +++ b/crates/perry-runtime/src/node_submodules/test_snapshot.rs @@ -29,6 +29,7 @@ fn snapshot_payload(value: f64) -> String { pub(super) extern "C" fn snapshot_set_default_serializers( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, serializers: f64, ) -> f64 { if !is_array_value(serializers) { @@ -44,6 +45,7 @@ pub(super) extern "C" fn snapshot_set_default_serializers( pub(super) extern "C" fn snapshot_set_resolve_snapshot_path( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, resolver: f64, ) -> f64 { if !is_callable_value(resolver) { @@ -61,6 +63,7 @@ fn validate_snapshot_assertion_options(options: f64) { pub(super) extern "C" fn assert_snapshot( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, options: f64, ) -> f64 { @@ -74,7 +77,11 @@ pub(super) extern "C" fn assert_snapshot( ); } let resolver_ptr = raw_ptr_from_value(resolver) as *const ClosureHeader; - let path_value = js_closure_call1(resolver_ptr, string_value("")); + let path_value = js_closure_call1( + resolver_ptr, + crate::closure::plain_call_receiver(), + string_value(""), + ); let Some(path) = value_to_string(path_value) else { throw_invalid_arg_type("snapshot path", "string", path_value); }; @@ -117,6 +124,7 @@ pub(super) extern "C" fn assert_snapshot( pub(super) extern "C" fn assert_file_snapshot( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, path_value: f64, options: f64, diff --git a/crates/perry-runtime/src/node_submodules/test_unit_tests.rs b/crates/perry-runtime/src/node_submodules/test_unit_tests.rs index c27a4bc471..8f4a4cfe9d 100644 --- a/crates/perry-runtime/src/node_submodules/test_unit_tests.rs +++ b/crates/perry-runtime/src/node_submodules/test_unit_tests.rs @@ -24,7 +24,10 @@ fn caught_error_code(f: impl FnOnce() -> f64) -> &'static str { .expect("error should have a registered Node code") } -extern "C" fn allocating_true_option(_closure: *const ClosureHeader) -> f64 { +extern "C" fn allocating_true_option( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let _ = js_object_alloc(0, 0); f64::from_bits(crate::value::TAG_TRUE) } diff --git a/crates/perry-runtime/src/node_submodules/tests.rs b/crates/perry-runtime/src/node_submodules/tests.rs index a6abfdede0..d5050afb2b 100644 --- a/crates/perry-runtime/src/node_submodules/tests.rs +++ b/crates/perry-runtime/src/node_submodules/tests.rs @@ -600,11 +600,14 @@ fn sys_namespace_types_member_reuses_util_types() { fn stream_promises_finished_resolves_for_finished_writable_side_stub_stream() { let stream = crate::node_stream::js_node_stream_passthrough_new(undefined_value()); let end = get_object_property(stream, b"end").expect("stream.end should exist"); - let prev_this = crate::object::js_implicit_this_set(stream); unsafe { - let _ = crate::closure::js_native_call_value(end, std::ptr::null(), 0); + let _ = crate::closure::native_call_value_this( + end, + crate::closure::JsThis::from_f64(stream), + std::ptr::null(), + 0, + ); } - crate::object::js_implicit_this_set(prev_this); let _ = crate::promise::js_promise_run_microtasks(); let opts = js_object_alloc(0, 1); @@ -614,8 +617,12 @@ fn stream_promises_finished_resolves_for_finished_writable_side_stub_stream() { f64::from_bits(crate::value::TAG_FALSE), ); - let promise_value = - thunk_streamP_finished(std::ptr::null(), stream, boxed_ptr(opts as *const u8)); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + boxed_ptr(opts as *const u8), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 1); @@ -627,15 +634,24 @@ fn stream_promises_finished_resolves_for_finished_writable_side_stub_stream() { #[test] fn stream_promises_finished_rejects_invalid_inputs() { - let number_promise = thunk_streamP_finished(std::ptr::null(), 123.0, undefined_value()); + let number_promise = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + 123.0, + undefined_value(), + ); assert_rejected_node_error( number_promise, "ERR_INVALID_ARG_TYPE", "The \"stream\" argument must be an instance of ReadableStream, WritableStream, or Stream. Received type number (123)", ); - let string_promise = - thunk_streamP_finished(std::ptr::null(), string_value("x"), undefined_value()); + let string_promise = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + string_value("x"), + undefined_value(), + ); assert_rejected_node_error( string_promise, "ERR_INVALID_ARG_TYPE", @@ -645,6 +661,7 @@ fn stream_promises_finished_rejects_invalid_inputs() { let object = js_object_alloc(0, 0); let object_promise = thunk_streamP_finished( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, boxed_ptr(object as *const u8), undefined_value(), ); @@ -661,7 +678,12 @@ fn stream_promises_finished_rejects_hidden_stream_error() { let err = abort_error_value(); crate::node_stream::test_set_hidden_error(stream, err); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, undefined_value()); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + undefined_value(), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 2); @@ -675,7 +697,12 @@ fn stream_promises_finished_rejects_hidden_stream_error() { fn stream_promises_finished_rejects_later_destroy_error() { let stream = crate::node_stream::js_node_stream_readable_new(undefined_value()); crate::node_stream::test_install_manual_read(stream); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, undefined_value()); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + undefined_value(), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -744,7 +771,12 @@ fn stream_promises_finished_resolves_for_resumed_readable_from() { let handle = object_ptr_from_value(stream).expect("stream object") as i64; let _ = crate::node_stream::js_node_stream_method_resume(handle); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, undefined_value()); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + undefined_value(), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -756,7 +788,12 @@ fn stream_promises_finished_resolves_for_resumed_readable_from() { #[test] fn stream_promises_finished_duplex_default_waits_after_writable_finish_only() { let stream = crate::node_stream::js_node_stream_duplex_new(undefined_value()); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, undefined_value()); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + undefined_value(), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -775,7 +812,12 @@ fn stream_promises_finished_duplex_default_waits_after_writable_finish_only() { #[test] fn stream_promises_finished_duplex_default_waits_after_readable_end_only() { let stream = crate::node_stream::js_node_stream_duplex_new(undefined_value()); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, undefined_value()); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + undefined_value(), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -795,7 +837,12 @@ fn stream_promises_finished_duplex_default_waits_after_readable_end_only() { fn stream_promises_finished_duplex_readable_false_resolves_after_writable_finish() { let stream = crate::node_stream::js_node_stream_duplex_new(undefined_value()); let options = finished_bool_option(b"readable", false); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, options); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + options, + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -810,7 +857,12 @@ fn stream_promises_finished_duplex_readable_false_resolves_after_writable_finish fn stream_promises_finished_duplex_writable_false_resolves_after_readable_end() { let stream = crate::node_stream::js_node_stream_duplex_new(undefined_value()); let options = finished_bool_option(b"writable", false); - let promise_value = thunk_streamP_finished(std::ptr::null(), stream, options); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + options, + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -825,7 +877,12 @@ thread_local! { static PIPELINE_CAPTURED: RefCell> = const { RefCell::new(Vec::new()) }; } -extern "C" fn pipeline_write_capture(_closure: *const ClosureHeader, chunk: f64, _enc: f64) -> f64 { +extern "C" fn pipeline_write_capture( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, + _enc: f64, +) -> f64 { PIPELINE_CAPTURED.with(|captured| { captured .borrow_mut() @@ -834,7 +891,11 @@ extern "C" fn pipeline_write_capture(_closure: *const ClosureHeader, chunk: f64, f64::from_bits(crate::value::TAG_TRUE) } -extern "C" fn pipeline_end_capture(_closure: *const ClosureHeader, _arg: f64) -> f64 { +extern "C" fn pipeline_end_capture( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { undefined_value() } @@ -865,6 +926,7 @@ fn stream_promises_pipeline_transfers_readable_from_chunks() { let promise_value = thunk_streamP_pipeline( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, source, boxed_ptr(sink as *const u8), undefined_value(), @@ -881,6 +943,7 @@ fn stream_promises_pipeline_transfers_readable_from_chunks() { fn stream_promises_pipeline_rejects_missing_streams() { let direct_promise = thunk_streamP_pipeline( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, undefined_value(), undefined_value(), undefined_value(), @@ -894,6 +957,7 @@ fn stream_promises_pipeline_rejects_missing_streams() { let empty_rest = crate::array::js_array_alloc(0); let rest_promise = thunk_streamP_pipeline( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, 123.0, undefined_value(), boxed_ptr(empty_rest as *const u8), @@ -907,7 +971,13 @@ fn stream_promises_pipeline_rejects_missing_streams() { #[test] fn stream_promises_pipeline_rejects_invalid_source_body() { - let direct_promise = thunk_streamP_pipeline(std::ptr::null(), 123.0, 456.0, undefined_value()); + let direct_promise = thunk_streamP_pipeline( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + 123.0, + 456.0, + undefined_value(), + ); assert_rejected_node_error( direct_promise, "ERR_INVALID_ARG_TYPE", @@ -917,6 +987,7 @@ fn stream_promises_pipeline_rejects_invalid_source_body() { let empty_rest = crate::array::js_array_alloc(0); let rest_promise = thunk_streamP_pipeline( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, 123.0, 456.0, boxed_ptr(empty_rest as *const u8), @@ -940,8 +1011,12 @@ fn stream_promises_finished_rejects_when_signal_aborts() { ); let stream = crate::node_stream::js_node_stream_passthrough_new(undefined_value()); - let promise_value = - thunk_streamP_finished(std::ptr::null(), stream, boxed_ptr(opts as *const u8)); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + boxed_ptr(opts as *const u8), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 0); @@ -967,15 +1042,22 @@ fn stream_promises_finished_with_signal_resolves_for_finished_writable_side_stub ); let stream = crate::node_stream::js_node_stream_passthrough_new(undefined_value()); let end = get_object_property(stream, b"end").expect("stream.end should exist"); - let prev_this = crate::object::js_implicit_this_set(stream); unsafe { - let _ = crate::closure::js_native_call_value(end, std::ptr::null(), 0); + let _ = crate::closure::native_call_value_this( + end, + crate::closure::JsThis::from_f64(stream), + std::ptr::null(), + 0, + ); } - crate::object::js_implicit_this_set(prev_this); let _ = crate::promise::js_promise_run_microtasks(); - let promise_value = - thunk_streamP_finished(std::ptr::null(), stream, boxed_ptr(opts as *const u8)); + let promise_value = thunk_streamP_finished( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + stream, + boxed_ptr(opts as *const u8), + ); let promise = promise_ptr(promise_value); assert_eq!(crate::promise::js_promise_state(promise), 1); diff --git a/crates/perry-runtime/src/node_submodules/timers.rs b/crates/perry-runtime/src/node_submodules/timers.rs index c4df18185c..570c177ad3 100644 --- a/crates/perry-runtime/src/node_submodules/timers.rs +++ b/crates/perry-runtime/src/node_submodules/timers.rs @@ -92,6 +92,7 @@ fn promise_timer(delay_ms: f64, value: f64, has_ref: bool) -> *mut crate::promis /// Refs #1213. pub(crate) extern "C" fn timers_promises_set_timeout( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, delay_ms: f64, value: f64, options: f64, @@ -126,6 +127,7 @@ pub(crate) extern "C" fn timers_promises_set_timeout( /// Refs #1213. pub(crate) extern "C" fn timers_promises_set_immediate( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, options: f64, ) -> f64 { @@ -152,13 +154,23 @@ pub(crate) extern "C" fn timers_promises_set_immediate( pub(crate) extern "C" fn timers_promises_scheduler_wait( _closure: *const ClosureHeader, + this: crate::closure::JsThis, delay_ms: f64, options: f64, ) -> f64 { - timers_promises_set_timeout(_closure, delay_ms, f64::from_bits(TAG_UNDEFINED), options) + timers_promises_set_timeout( + _closure, + this, + delay_ms, + f64::from_bits(TAG_UNDEFINED), + options, + ) } -pub(crate) extern "C" fn timers_promises_scheduler_yield(_closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn timers_promises_scheduler_yield( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let promise = promise_timer(0.0, f64::from_bits(TAG_UNDEFINED), true); crate::value::js_nanbox_pointer(promise as i64) } @@ -191,7 +203,10 @@ fn iter_result(value: f64, done: bool) -> f64 { boxed_ptr(obj as *const u8) } -extern "C" fn timers_promises_interval_next(closure: *const ClosureHeader) -> f64 { +extern "C" fn timers_promises_interval_next( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let value = js_closure_get_capture_f64(closure, 0); let signal = js_closure_get_capture_f64(closure, 1); let delay_ms = js_closure_get_capture_f64(closure, 2); @@ -228,11 +243,17 @@ extern "C" fn timers_promises_interval_next(closure: *const ClosureHeader) -> f6 boxed_ptr(promise as *const u8) } -extern "C" fn timers_promises_interval_self(closure: *const ClosureHeader) -> f64 { +extern "C" fn timers_promises_interval_self( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_closure_get_capture_f64(closure, 0) } -extern "C" fn timers_promises_interval_return(closure: *const ClosureHeader) -> f64 { +extern "C" fn timers_promises_interval_return( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let next_value = js_closure_get_capture_f64(closure, 0); let next = boxed_value_to_ptr::(next_value); js_closure_set_capture_f64(next, 3, 1.0); @@ -247,6 +268,7 @@ extern "C" fn timers_promises_interval_return(closure: *const ClosureHeader) -> /// returned or the optional AbortSignal rejects the pending tick. pub(crate) extern "C" fn timers_promises_set_interval( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, delay_ms: f64, value: f64, options: f64, @@ -334,6 +356,7 @@ fn rest_array_values(rest: f64) -> Vec { pub(crate) extern "C" fn timers_ns_set_timeout( _c: *const ClosureHeader, + _this: crate::closure::JsThis, cb: f64, ms: f64, rest: f64, @@ -350,6 +373,7 @@ pub(crate) extern "C" fn timers_ns_set_timeout( } pub(crate) extern "C" fn timers_ns_set_interval( _c: *const ClosureHeader, + _this: crate::closure::JsThis, cb: f64, ms: f64, rest: f64, @@ -366,6 +390,7 @@ pub(crate) extern "C" fn timers_ns_set_interval( } pub(crate) extern "C" fn timers_ns_set_immediate( _c: *const ClosureHeader, + _this: crate::closure::JsThis, cb: f64, rest: f64, ) -> f64 { @@ -378,21 +403,34 @@ pub(crate) extern "C" fn timers_ns_set_immediate( ) }) } -pub(crate) extern "C" fn timers_ns_clear_timeout(_c: *const ClosureHeader, arg: f64) -> f64 { +pub(crate) extern "C" fn timers_ns_clear_timeout( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { crate::timer::js_clear_timeout_value(arg); f64::from_bits(TAG_UNDEFINED) } -pub(crate) extern "C" fn timers_ns_clear_interval(_c: *const ClosureHeader, arg: f64) -> f64 { +pub(crate) extern "C" fn timers_ns_clear_interval( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { crate::timer::js_clear_interval_value(arg); f64::from_bits(TAG_UNDEFINED) } -pub(crate) extern "C" fn timers_ns_clear_immediate(_c: *const ClosureHeader, arg: f64) -> f64 { +pub(crate) extern "C" fn timers_ns_clear_immediate( + _c: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { crate::timer::js_clear_immediate_value(arg); f64::from_bits(TAG_UNDEFINED) } pub(crate) extern "C" fn timers_promises_scheduler( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { let msg = b"scheduler is not a function"; diff --git a/crates/perry-runtime/src/node_submodules/trace_events.rs b/crates/perry-runtime/src/node_submodules/trace_events.rs index 58542a2e4e..d4626369a4 100644 --- a/crates/perry-runtime/src/node_submodules/trace_events.rs +++ b/crates/perry-runtime/src/node_submodules/trace_events.rs @@ -202,8 +202,8 @@ fn trace_id_symbol() -> f64 { }) } -fn this_trace_id() -> i64 { - let this_value = crate::object::js_implicit_this_get(); +fn this_trace_id(this: crate::closure::JsThis) -> i64 { + let this_value = this.as_f64(); let Some(_) = object_ptr_from_value(this_value) else { throw_invalid_this(); }; @@ -303,7 +303,11 @@ fn set_trace_enabled(id: i64, enabled: bool) { } } -extern "C" fn trace_tracing_constructor(_closure: *const ClosureHeader, categories: f64) -> f64 { +extern "C" fn trace_tracing_constructor( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + categories: f64, +) -> f64 { if crate::object::js_new_target_get().to_bits() == crate::value::TAG_UNDEFINED { throw_type_error_no_code(b"Class constructor Tracing cannot be invoked without 'new'"); } @@ -313,26 +317,38 @@ extern "C" fn trace_tracing_constructor(_closure: *const ClosureHeader, categori create_trace(categories_handle.get_nanbox_f64(), category_names) } -extern "C" fn trace_tracing_enable(_closure: *const ClosureHeader) -> f64 { - set_trace_enabled(this_trace_id(), true); +extern "C" fn trace_tracing_enable( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + set_trace_enabled(this_trace_id(this), true); undefined() } -extern "C" fn trace_tracing_disable(_closure: *const ClosureHeader) -> f64 { - set_trace_enabled(this_trace_id(), false); +extern "C" fn trace_tracing_disable( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + set_trace_enabled(this_trace_id(this), false); undefined() } -extern "C" fn trace_categories_getter(_closure: *const ClosureHeader) -> f64 { - let id = this_trace_id(); +extern "C" fn trace_categories_getter( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let id = this_trace_id(this); let categories = trace_state_value(id, |state| state.categories); let scope = crate::gc::RuntimeHandleScope::new(); let categories = scope.root_nanbox_f64(categories); string_value(&categories_from_array(categories.get_nanbox_f64()).join(",")) } -extern "C" fn trace_enabled_getter(_closure: *const ClosureHeader) -> f64 { - let id = this_trace_id(); +extern "C" fn trace_enabled_getter( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let id = this_trace_id(this); trace_state_value(id, |state| bool_value(state.enabled)) } @@ -445,6 +461,7 @@ fn create_trace(categories: f64, source_categories: Vec) -> f64 { #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_trace_events_createTracing( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, options: f64, ) -> f64 { init_trace_events_runtime(); @@ -456,6 +473,7 @@ pub(crate) extern "C" fn thunk_trace_events_createTracing( #[allow(non_snake_case)] // thunk name mirrors JS API surface pub(crate) extern "C" fn thunk_trace_events_getEnabledCategories( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { init_trace_events_runtime(); @@ -632,17 +650,14 @@ fn emit_enabled_trace_warning() { let process = scope.root_nanbox_f64(process); let callback = scope.root_nanbox_f64(callback); let warning = scope.root_nanbox_f64(warning); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - process.get_nanbox_f64(), - )); unsafe { - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( callback.get_nanbox_f64(), + crate::closure::JsThis::from_f64(process.get_nanbox_f64()), [warning.get_nanbox_f64()].as_ptr(), 1, ); } - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); } #[cfg(test)] diff --git a/crates/perry-runtime/src/node_test.rs b/crates/perry-runtime/src/node_test.rs index b6d4c7e0da..8b550151ae 100644 --- a/crates/perry-runtime/src/node_test.rs +++ b/crates/perry-runtime/src/node_test.rs @@ -40,19 +40,29 @@ fn fn_value(func: *const u8, name: &str, arity: u32) -> f64 { boxed_pointer(closure as *const u8) } -extern "C" fn noop0(_closure: *const ClosureHeader) -> f64 { +extern "C" fn noop0(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { undefined() } -extern "C" fn noop1(_closure: *const ClosureHeader, _arg0: f64) -> f64 { +extern "C" fn noop1( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg0: f64, +) -> f64 { undefined() } -extern "C" fn noop3(_closure: *const ClosureHeader, _arg0: f64, _arg1: f64, _arg2: f64) -> f64 { +extern "C" fn noop3( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _arg0: f64, + _arg1: f64, + _arg2: f64, +) -> f64 { undefined() } -extern "C" fn zero0(_closure: *const ClosureHeader) -> f64 { +extern "C" fn zero0(_closure: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 0.0 } diff --git a/crates/perry-runtime/src/node_v8.rs b/crates/perry-runtime/src/node_v8.rs index 3c651066de..2414424848 100644 --- a/crates/perry-runtime/src/node_v8.rs +++ b/crates/perry-runtime/src/node_v8.rs @@ -614,6 +614,15 @@ pub extern "C" fn js_v8_noop_undefined() -> f64 { undefined() } +/// [`js_v8_noop_undefined`] as a JS body (the function object +/// `v8.promiseHooks.onInit(fn)` returns). +extern "C" fn v8_noop_undefined_body( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + undefined() +} + /// `v8.startupSnapshot.isBuildingSnapshot()` — Node returns the *number* `0` /// (never building a snapshot under Perry), NOT a boolean. #[no_mangle] @@ -645,7 +654,7 @@ pub extern "C" fn js_v8_throw_not_building_snapshot() -> f64 { /// `const stop = onInit(fn); stop()` round-trips. #[no_mangle] pub extern "C" fn js_v8_promise_hook_register() -> f64 { - let c = crate::closure::js_closure_alloc_singleton(js_v8_noop_undefined as *const u8); + let c = crate::closure::js_closure_alloc_singleton(v8_noop_undefined_body as *const u8); crate::value::js_nanbox_pointer(c as i64) } diff --git a/crates/perry-runtime/src/node_vm.rs b/crates/perry-runtime/src/node_vm.rs index dd25c08d8f..78341aa249 100644 --- a/crates/perry-runtime/src/node_vm.rs +++ b/crates/perry-runtime/src/node_vm.rs @@ -1311,7 +1311,7 @@ fn script_metadata(script_value: f64) -> Option { fn install_script_method( obj: *mut ObjectHeader, name: &str, - func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(a, a), arity: u32, ) { let scope = crate::gc::RuntimeHandleScope::new(); @@ -1341,8 +1341,8 @@ fn install_script_method( }); } -fn script_receiver() -> f64 { - crate::object::js_implicit_this_get() +fn script_receiver(this: crate::closure::JsThis) -> f64 { + this.as_f64() } pub(crate) fn install_script_prototypes(constructor: f64) { @@ -1485,10 +1485,11 @@ fn make_script(code: String, options: f64) -> f64 { extern "C" fn vm_script_create_cached_data_method( _closure: *const ClosureHeader, + this: crate::closure::JsThis, _unused1: f64, _unused2: f64, ) -> f64 { - let script = script_receiver(); + let script = script_receiver(this); let Some(metadata) = script_metadata(script) else { return cached_data_buffer(CACHE_KIND_SCRIPT, 0); }; @@ -1500,10 +1501,11 @@ extern "C" fn vm_script_create_cached_data_method( extern "C" fn vm_script_run_in_this_context_method( _closure: *const ClosureHeader, + this: crate::closure::JsThis, _options: f64, _unused: f64, ) -> f64 { - let script = script_receiver(); + let script = script_receiver(this); let Some(metadata) = script_metadata(script) else { return undefined_value(); }; @@ -1520,10 +1522,11 @@ extern "C" fn vm_script_run_in_this_context_method( extern "C" fn vm_script_run_in_context_method( _closure: *const ClosureHeader, + this: crate::closure::JsThis, contextified_object: f64, _options: f64, ) -> f64 { - let script = script_receiver(); + let script = script_receiver(this); let Some(metadata) = script_metadata(script) else { return undefined_value(); }; @@ -1539,10 +1542,11 @@ extern "C" fn vm_script_run_in_context_method( extern "C" fn vm_script_run_in_new_context_method( _closure: *const ClosureHeader, + this: crate::closure::JsThis, context_object: f64, options: f64, ) -> f64 { - let script = script_receiver(); + let script = script_receiver(this); let Some(metadata) = script_metadata(script) else { return undefined_value(); }; diff --git a/crates/perry-runtime/src/node_vm/modules.rs b/crates/perry-runtime/src/node_vm/modules.rs index 9d27ad49fd..68bde0b489 100644 --- a/crates/perry-runtime/src/node_vm/modules.rs +++ b/crates/perry-runtime/src/node_vm/modules.rs @@ -75,14 +75,14 @@ fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { })); let js = JSValue::from_bits(callback.get_nanbox_f64().to_bits()); if !js.is_undefined() && !js.is_null() { - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( - &module, - object_value, - ))); let outcome = crate::exception::js_call_catching(|| unsafe { - crate::closure::js_native_call_value(callback.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::native_call_value_this( + callback.get_nanbox_f64(), + crate::closure::JsThis::from_f64(with_hmut(&module, object_value)), + std::ptr::null(), + 0, + ) }); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); if let Err(error) = outcome { with_hmut(&module, |module| set_field(module, FIELD_ERROR, error)); with_hmut(&module, |module| set_status(module, STATUS_ERRORED)); @@ -159,18 +159,24 @@ fn new_module_base(kind: &str, status: &str, identifier: String) -> *mut ObjectH module } -extern "C" fn module_namespace_getter(closure: *const ClosureHeader) -> f64 { +extern "C" fn module_namespace_getter( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_vm_module_namespace(crate::closure::js_closure_get_capture_f64(closure, 0)) } -extern "C" fn module_error_getter(closure: *const ClosureHeader) -> f64 { +extern "C" fn module_error_getter( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { js_vm_module_error(crate::closure::js_closure_get_capture_f64(closure, 0)) } fn install_module_accessor( module: *mut ObjectHeader, name: &str, - getter: extern "C" fn(*const ClosureHeader) -> f64, + getter: crate::closure::body_call::js_body_fn_ty!(), ) { let scope = crate::gc::RuntimeHandleScope::new(); let module = scope.root_raw_mut_ptr(module); @@ -437,7 +443,12 @@ pub extern "C" fn js_vm_module_link(module_value: f64, linker: f64) -> f64 { module_request_extra(), ]; let dep = unsafe { - crate::closure::js_native_call_value(linker.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + linker.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) }; linked = crate::array::js_array_push_f64(linked, dep); } diff --git a/crates/perry-runtime/src/object/accessor_pair_tests.rs b/crates/perry-runtime/src/object/accessor_pair_tests.rs index 87e84f1835..308a42f48e 100644 --- a/crates/perry-runtime/src/object/accessor_pair_tests.rs +++ b/crates/perry-runtime/src/object/accessor_pair_tests.rs @@ -7,7 +7,10 @@ fn key(name: &str) -> *mut crate::StringHeader { } unsafe fn closure_bits() -> u64 { - extern "C" fn noop(_c: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn noop( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } let c = crate::closure::js_closure_alloc(noop as *const u8, 0); diff --git a/crates/perry-runtime/src/object/arguments.rs b/crates/perry-runtime/src/object/arguments.rs index 38d0ba4d39..7563128249 100644 --- a/crates/perry-runtime/src/object/arguments.rs +++ b/crates/perry-runtime/src/object/arguments.rs @@ -202,7 +202,10 @@ fn bool_value(value: bool) -> f64 { }) } -extern "C" fn arguments_throw_type_error(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn arguments_throw_type_error( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { super::throw_object_type_error( b"'caller', 'callee', and 'arguments' properties may not be accessed", ); @@ -694,7 +697,7 @@ pub(crate) unsafe fn arguments_object_get_field( }); if name == "callee" && state.restricted_callee() { - arguments_throw_type_error(std::ptr::null()); + arguments_throw_type_error(std::ptr::null(), crate::closure::JsThis::UNDEFINED); } if let Some(box_ptr) = mapped_box { let value = crate::r#box::js_box_get(box_ptr); @@ -706,7 +709,11 @@ pub(crate) unsafe fn arguments_object_get_field( let closure = (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !closure.is_null() { - let value = crate::closure::js_closure_call0(closure); + // An accessor on the arguments object runs with it as `this`. + let this = crate::closure::JsThis::from_f64(crate::value::js_nanbox_pointer( + obj as i64, + )); + let value = crate::closure::js_closure_call0(closure, this); return Some(JSValue::from_bits(value.to_bits())); } } @@ -737,7 +744,7 @@ pub(crate) unsafe fn arguments_object_set_field( }); if name == "callee" && state.restricted_callee() { - arguments_throw_type_error(std::ptr::null()); + arguments_throw_type_error(std::ptr::null(), crate::closure::JsThis::UNDEFINED); } if !super::own_key_present(obj, key) { return false; @@ -747,7 +754,9 @@ pub(crate) unsafe fn arguments_object_set_field( let closure = (acc.set & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !closure.is_null() { - crate::closure::js_closure_call1(closure, value); + let this = + crate::closure::JsThis::from_f64(crate::value::js_nanbox_pointer(obj as i64)); + crate::closure::js_closure_call1(closure, this, value); } } return true; diff --git a/crates/perry-runtime/src/object/assert.rs b/crates/perry-runtime/src/object/assert.rs index 65602cd86e..5906c3d842 100644 --- a/crates/perry-runtime/src/object/assert.rs +++ b/crates/perry-runtime/src/object/assert.rs @@ -322,13 +322,25 @@ fn expected_is_callable(value: f64) -> bool { fn call_validator_capturing(validator: f64, arg: f64) -> Result { crate::exception::catch_js_throw(|| { let args = [arg]; - unsafe { crate::closure::js_native_call_value(validator, args.as_ptr(), 1) } + unsafe { + crate::closure::js_native_call_value( + validator, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 1, + ) + } }) } fn call_block_capturing_throw(block: f64) -> Result { crate::exception::catch_js_throw(|| unsafe { - crate::closure::js_native_call_value(block, std::ptr::null(), 0) + crate::closure::js_native_call_value( + block, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) }) } @@ -508,6 +520,7 @@ fn promise_from_assert_async_input(input: f64) -> AssertAsyncInput { extern "C" fn assert_rejects_fulfilled( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, ) -> f64 { let result = @@ -527,6 +540,7 @@ extern "C" fn assert_rejects_fulfilled( extern "C" fn assert_rejects_rejected( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let result = @@ -550,6 +564,7 @@ extern "C" fn assert_rejects_rejected( extern "C" fn assert_does_not_reject_fulfilled( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, ) -> f64 { let result = @@ -560,6 +575,7 @@ extern "C" fn assert_does_not_reject_fulfilled( extern "C" fn assert_does_not_reject_rejected( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let result = diff --git a/crates/perry-runtime/src/object/async_generator_queue.rs b/crates/perry-runtime/src/object/async_generator_queue.rs index 16c8b30a67..95964ae426 100644 --- a/crates/perry-runtime/src/object/async_generator_queue.rs +++ b/crates/perry-runtime/src/object/async_generator_queue.rs @@ -120,7 +120,7 @@ pub(crate) fn wrap_async_generator_instance(obj: *mut ObjectHeader) { ( b"next".as_slice(), &next_h, - async_generator_next_wrapper as extern "C" fn(*const ClosureHeader, f64) -> f64, + async_generator_next_wrapper as crate::closure::body_call::js_body_fn_ty!(a), ), (b"return".as_slice(), &ret_h, async_generator_return_wrapper), (b"throw".as_slice(), &throw_h, async_generator_throw_wrapper), @@ -289,15 +289,27 @@ fn original_from_wrapper(closure: *const ClosureHeader) -> *const ClosureHeader js_closure_get_capture_ptr(closure, 1) as *const ClosureHeader } -extern "C" fn async_generator_next_wrapper(closure: *const ClosureHeader, arg: f64) -> f64 { +extern "C" fn async_generator_next_wrapper( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { async_generator_request(closure, arg, RequestKind::NextOrThrow) } -extern "C" fn async_generator_return_wrapper(closure: *const ClosureHeader, arg: f64) -> f64 { +extern "C" fn async_generator_return_wrapper( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { async_generator_request(closure, arg, RequestKind::Return) } -extern "C" fn async_generator_throw_wrapper(closure: *const ClosureHeader, arg: f64) -> f64 { +extern "C" fn async_generator_throw_wrapper( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { async_generator_request(closure, arg, RequestKind::NextOrThrow) } @@ -429,6 +441,7 @@ fn make_return_step_wrapper( extern "C" fn async_generator_return_step_fulfill( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { if let Some(state_id) = state_id_from_wrapper(closure) { @@ -445,6 +458,7 @@ extern "C" fn async_generator_return_step_fulfill( extern "C" fn async_generator_return_step_reject( closure: *const ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { if let Some(state_id) = state_id_from_wrapper(closure) { @@ -456,14 +470,21 @@ extern "C" fn async_generator_return_step_reject( reason } -extern "C" fn async_generator_drain_wrapper(closure: *const ClosureHeader) -> f64 { +extern "C" fn async_generator_drain_wrapper( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if let Some(state_id) = state_id_from_wrapper(closure) { process_one_queued_request(state_id); } f64::from_bits(TAG_UNDEFINED) } -extern "C" fn async_generator_settle_fulfill(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn async_generator_settle_fulfill( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { if let Some(state_id) = state_id_from_wrapper(closure) { let out = js_closure_get_capture_ptr(closure, 1) as *mut Promise; finish_after_pending_result(state_id, out, true, value); @@ -471,7 +492,11 @@ extern "C" fn async_generator_settle_fulfill(closure: *const ClosureHeader, valu value } -extern "C" fn async_generator_settle_reject(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn async_generator_settle_reject( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if let Some(state_id) = state_id_from_wrapper(closure) { let out = js_closure_get_capture_ptr(closure, 1) as *mut Promise; finish_after_pending_result(state_id, out, false, reason); @@ -485,7 +510,7 @@ fn call_original(original: *const ClosureHeader, arg: f64) -> f64 { TAG_UNDEFINED, ))); } - js_closure_call1(original, arg) + js_closure_call1(original, crate::closure::plain_call_receiver(), arg) } fn after_initial_result(state_id: usize, result: f64) { diff --git a/crates/perry-runtime/src/object/bound_method_receiver_tests.rs b/crates/perry-runtime/src/object/bound_method_receiver_tests.rs index 681878fb2f..280496797f 100644 --- a/crates/perry-runtime/src/object/bound_method_receiver_tests.rs +++ b/crates/perry-runtime/src/object/bound_method_receiver_tests.rs @@ -77,12 +77,14 @@ unsafe fn register_class() -> (f64, f64) { (method, proto) } -/// `method.call(receiver)`: IMPLICIT_THIS bound to `receiver` for the call. +/// `method.call(receiver)`: `receiver` passed as the call's `this`. unsafe fn call_with_this(method: f64, receiver: f64) -> f64 { - let prev = crate::object::js_implicit_this_set(receiver); - let result = crate::closure::js_native_call_value(method, std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev); - result + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(receiver), + std::ptr::null(), + 0, + ) } /// `Object.setPrototypeOf(Object.create(null), proto)`. diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 2aef916811..2f542a2aca 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -865,9 +865,6 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( if let Some((func_ptr, param_count, has_rest)) = super::class_registry::lookup_static_method_in_chain(parent_cid, name) { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); crate::object::static_this_arm_if_unarmed(this_value); let result = if has_rest { // Mirror `js_class_static_method_call`'s rest bundling: fixed @@ -899,7 +896,6 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( super::class_registry::call_static_method(func_ptr, args_ptr, args_len, param_count) }; crate::object::static_this_disarm(); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -931,15 +927,12 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( // walks the parent chain and drops its read lock before returning, so the // invoked body may re-take the registry lock without deadlocking (wall-37). if let Some(method_value) = super::class_registry::lookup_prototype_method(parent_cid, name) { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(super::IMPLICIT_THIS.with(|c| c.replace(this_value.to_bits()))); - let result = crate::closure::js_native_call_value(method_value, args_ptr, args_len); - super::IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - return result; + return crate::closure::native_call_value_this( + method_value, + crate::closure::JsThis::from_f64(this_value), + args_ptr, + args_len, + ); } // #6316: the parent chain is real (an intermediate user class) but bottoms // out in a NATIVE base whose surface perry stamps onto the instance — @@ -979,19 +972,16 @@ unsafe fn call_displaced_native_base_method( return crate::object::map_set_subclass::super_collection_method(this_value, name, args) .unwrap_or(undef); }; - // The stashed closure already captures the receiver in slot 0, but bind - // IMPLICIT_THIS too: the shared emitter/stream stubs read their receiver - // through `this_value(closure)`, which falls back to IMPLICIT_THIS when the - // capture is undefined (the prototype-installed form). - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(super::IMPLICIT_THIS.with(|c| c.replace(this_value.to_bits()))); - let result = crate::closure::js_native_call_value(method_value, args_ptr, args_len); - super::IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - result + // The stashed closure already captures the receiver in slot 0, but pass + // it as `this` too: the shared emitter/stream stubs fall back to their + // `this` argument when the capture is undefined (the prototype-installed + // form). + crate::closure::native_call_value_this( + method_value, + crate::closure::JsThis::from_f64(this_value), + args_ptr, + args_len, + ) } /// Keepalive anchor (generated-code-only callee). diff --git a/crates/perry-runtime/src/object/class_gc_roots.rs b/crates/perry-runtime/src/object/class_gc_roots.rs index 221d4df315..495f926b43 100644 --- a/crates/perry-runtime/src/object/class_gc_roots.rs +++ b/crates/perry-runtime/src/object/class_gc_roots.rs @@ -27,8 +27,8 @@ use super::class_registry::{ /// stored address would point at the stale/forwarded location. /// /// This is the same rooting-hazard class the integer-hack design (#1772) was -/// rejected for, and a sibling of the `IMPLICIT_THIS` fix in #1813 -/// (`scan_implicit_this_roots_mut`). The keys are scalar `class_id`s (no GC +/// rejected for, and a sibling of the #1813 fix to the (since removed) +/// implicit-`this` cell. The keys are scalar `class_id`s (no GC /// concern); only the pointer *values* are visited. `CLASS_STATIC_METHODS` /// stores code `func_ptr`s, not heap pointers, so it is deliberately not /// scanned here. diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index 383672d44c..636dd7afee 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -7,7 +7,7 @@ crate::perry_thread_local! { /// **This is a GC root, and must stay one (#7231).** It holds a NaN-boxed /// closure/class value for the whole constructor body, and a constructor /// body runs arbitrary user code. `this_binding.rs`'s `NEW_TARGET` holds - /// the same value under `scan_implicit_this_roots_mut`; this is a second + /// the same value under `scan_dispatch_binding_roots_mut`; this is a second /// copy on a different path, and a second copy of a root that is not /// itself a root is exactly the shape #7226 found in `prev_this`. /// @@ -47,8 +47,8 @@ pub(crate) use rooted_arguments::construct_two_rooted; /// through `synthetic_class_id_for_function` so the instance's /// `class_id` matches the bucket prototype methods were registered /// against. Allocates a fresh object stamped with the synthetic id, -/// then invokes the function as the constructor with `IMPLICIT_THIS` -/// bound to the new object so any `this.foo = …` writes in the +/// then invokes the function as the constructor with the new object +/// as `this` so any `this.foo = …` writes in the /// function body land on the instance. Returns the NaN-boxed new /// instance pointer. /// @@ -606,7 +606,12 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( .get(1) .copied() .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_blob_thunk(std::ptr::null(), parts, options); + return crate::object::global_this_blob_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + parts, + options, + ); } #[cfg(feature = "global-webfetch")] "File" => { @@ -624,6 +629,7 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); return crate::object::global_this_file_thunk( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, parts, name, options, @@ -653,7 +659,11 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( .first() .copied() .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_headers_thunk(std::ptr::null(), init); + return crate::object::global_this_headers_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + init, + ); } #[cfg(feature = "global-webfetch")] "Request" => { @@ -665,7 +675,12 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( .get(1) .copied() .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_request_thunk(std::ptr::null(), input, init); + return crate::object::global_this_request_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + input, + init, + ); } #[cfg(feature = "global-webfetch")] "Response" => { @@ -677,7 +692,12 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( .get(1) .copied() .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); - return crate::object::global_this_response_thunk(std::ptr::null(), body, init); + return crate::object::global_this_response_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + body, + init, + ); } "Event" => { let event_type = args @@ -856,7 +876,10 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( return crate::messaging::js_message_port_constructor_error(); } "Storage" => { - return crate::web_storage::storage_constructor_illegal(std::ptr::null()); + return crate::web_storage::storage_constructor_illegal( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + ); } "BroadcastChannel" => { let name = args @@ -1185,13 +1208,13 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( // here — otherwise `new ()` would dereference an // arbitrary pointer as a `ClosureHeader` and crash. if is_callable_function_value(func_value) { - // Bind `this` to the new instance, dispatch the constructor, - // then restore the previous IMPLICIT_THIS. The dispatch + // Pass the new instance as `this` and dispatch the constructor. + // The dispatch // result is discarded — JS `new` semantics use the receiver, // not the returned value (object returns would override, but // dayjs and siblings rely on the receiver mutation pattern). - // #7280: `nan_boxed` (the implicit `this` this call is building) and - // the three DISPLACED cell values are held across a call that runs a + // #7280: `nan_boxed` (the `this` this call is building) and + // the two DISPLACED new.target values are held across a call that runs a // user constructor body — see the long note in // `construct_registered_class_ref`. Unrooted, the evacuating minor // moves the instance and this arm returns the pre-move address; @@ -1200,19 +1223,20 @@ pub unsafe extern "C-unwind" fn js_new_function_construct( // `PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1`. let scope = crate::gc::RuntimeHandleScope::new(); let inst_handle = scope.root_nanbox_f64(nan_boxed); - let prev_this = crate::object::js_implicit_this_get(); - let prev_this_handle = scope.root_nanbox_f64(prev_this); let prev_new_target = crate::object::js_new_target_get(); let prev_new_target_handle = scope.root_nanbox_f64(prev_new_target); - crate::object::js_implicit_this_set(nan_boxed); crate::object::js_new_target_set(func_value); let prev_current_new_target = CURRENT_NEW_TARGET.with(|value| value.replace(func_value.to_bits())); let prev_current_new_target_handle = scope.root_nanbox_u64(prev_current_new_target); - let result = crate::closure::js_native_call_value(func_value, args_ptr, args_len); + let result = crate::closure::native_call_value_this( + func_value, + crate::closure::JsThis::from_f64(inst_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target_handle.get_nanbox_u64())); crate::object::js_new_target_set(prev_new_target_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this_handle.get_nanbox_f64()); if constructor_return_overrides_this(result) { return result; } @@ -1871,24 +1895,25 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( } // #7280: same unrooted-receiver shape as the plain-`new` tail above — - // `nan_boxed` and the three displaced cell values cross a user + // `nan_boxed` and the two displaced new.target values cross a user // constructor body. Reproduced by // `Reflect.construct(plainFn, [x], otherFn)`, 200/200 iterations wrong // under `PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1`. let scope = crate::gc::RuntimeHandleScope::new(); let inst_handle = scope.root_nanbox_f64(nan_boxed); - let prev_this = crate::object::js_implicit_this_get(); - let prev_this_handle = scope.root_nanbox_f64(prev_this); let prev_new_target = crate::object::js_new_target_get(); let prev_new_target_handle = scope.root_nanbox_f64(prev_new_target); - crate::object::js_implicit_this_set(nan_boxed); crate::object::js_new_target_set(nt); let prev_current_new_target = CURRENT_NEW_TARGET.with(|value| value.replace(nt.to_bits())); let prev_current_new_target_handle = scope.root_nanbox_u64(prev_current_new_target); - let result = crate::closure::js_native_call_value(func_value, args_ptr, args_len); + let result = crate::closure::native_call_value_this( + func_value, + crate::closure::JsThis::from_f64(inst_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); CURRENT_NEW_TARGET.with(|value| value.set(prev_current_new_target_handle.get_nanbox_u64())); crate::object::js_new_target_set(prev_new_target_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this_handle.get_nanbox_f64()); if constructor_return_overrides_this(result) { return result; } @@ -1901,9 +1926,8 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( /// the `new (args)` widened path here in /// `js_new_function_construct` needs to gate the constructor dispatch /// on a real closure to avoid SIGSEGV'ing on non-callable callees -/// (`new someObject()`, `new someStringVar()`, etc.). Uses the -/// `_reserved` magic word `crate::closure::CLOSURE_MAGIC` that every -/// `js_closure_alloc*` site stamps on allocation. +/// (`new someObject()`, `new someStringVar()`, etc.). The kind is the GC +/// header's type byte (`closure_kind_probe`), not a payload magic word. pub(crate) fn is_callable_function_value(value: f64) -> bool { use crate::value::JSValue; let jv = JSValue::from_bits(value.to_bits()); @@ -1944,43 +1968,5 @@ pub(super) fn is_arrow_function_value(value: f64) -> bool { crate::closure::closure_is_arrow(ptr) } -/// Lookup helper: returns the registered prototype-method value for -/// `(class_id, name)`, or None if no assignment matched. Walks the -/// parent-class chain so methods registered on a base class are found -/// via subclass instances. -pub(crate) fn lookup_own_prototype_method(class_id: u32, name: &str) -> Option { - if class_is_key_deleted(class_id, name) { - return None; - } - CLASS_PROTOTYPE_METHODS.with(|table| { - let guard = table.read().ok()?; - let bits = guard.as_ref()?.get(&class_id)?.get(name)?; - Some(f64::from_bits(*bits)) - }) -} - -pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option { - CLASS_PROTOTYPE_METHODS.with(|table| { - let guard = table.read().ok()?; - let map = guard.as_ref()?; - let mut cid = class_id; - let mut depth = 0usize; - while depth < 32 { - if !class_is_key_deleted(cid, name) { - if let Some(per_class) = map.get(&cid) { - if let Some(&bits) = per_class.get(name) { - return Some(f64::from_bits(bits)); - } - } - } - match crate::object::class_generic_origin(cid).or_else(|| get_parent_class_id(cid)) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None - }) -} +mod prototype_methods; +pub(crate) use prototype_methods::{lookup_own_prototype_method, lookup_prototype_method}; diff --git a/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs new file mode 100644 index 0000000000..532d49a560 --- /dev/null +++ b/crates/perry-runtime/src/object/class_registry/construct/prototype_methods.rs @@ -0,0 +1,44 @@ +//! Prototype-method lookups by (class id, name), own and through the parent chain. + +use super::*; + +/// Lookup helper: returns the registered prototype-method value for +/// `(class_id, name)`, or None if no assignment matched. Walks the +/// parent-class chain so methods registered on a base class are found +/// via subclass instances. +pub(crate) fn lookup_own_prototype_method(class_id: u32, name: &str) -> Option { + if class_is_key_deleted(class_id, name) { + return None; + } + CLASS_PROTOTYPE_METHODS.with(|table| { + let guard = table.read().ok()?; + let bits = guard.as_ref()?.get(&class_id)?.get(name)?; + Some(f64::from_bits(*bits)) + }) +} + +pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option { + CLASS_PROTOTYPE_METHODS.with(|table| { + let guard = table.read().ok()?; + let map = guard.as_ref()?; + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + if !class_is_key_deleted(cid, name) { + if let Some(per_class) = map.get(&cid) { + if let Some(&bits) = per_class.get(name) { + return Some(f64::from_bits(bits)); + } + } + } + match crate::object::class_generic_origin(cid).or_else(|| get_parent_class_id(cid)) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + None + }) +} diff --git a/crates/perry-runtime/src/object/class_registry/construct/rooted_arguments.rs b/crates/perry-runtime/src/object/class_registry/construct/rooted_arguments.rs index 9729cf4ce1..4ad01fda8f 100644 --- a/crates/perry-runtime/src/object/class_registry/construct/rooted_arguments.rs +++ b/crates/perry-runtime/src/object/class_registry/construct/rooted_arguments.rs @@ -3,13 +3,12 @@ use super::*; /// Construct with argument slots the collector can rewrite even while native /// constructor setup allocates before entering the JavaScript body. The trap /// sits below both the buffer owner and shadow frame, so JS throws release -/// storage and restore both new.target cells as well as implicit this. +/// storage and restore both new.target cells. pub(crate) fn construct_rooted_arguments(function: f64, args: &[f64], new_target: f64) -> f64 { use std::cell::UnsafeCell; let scope = crate::gc::RuntimeHandleScope::new(); let function = scope.root_nanbox_f64(function); let new_target = scope.root_nanbox_f64(new_target); - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let previous_target = scope.root_nanbox_f64(crate::object::js_new_target_get()); let previous_current = scope.root_nanbox_f64(js_new_target_value()); let args: Vec> = args.iter().copied().map(UnsafeCell::new).collect(); @@ -35,7 +34,6 @@ pub(crate) fn construct_rooted_arguments(function: f64, args: &[f64], new_target }); CURRENT_NEW_TARGET.with(|cell| cell.set(previous_current.get_nanbox_f64().to_bits())); crate::object::js_new_target_set(previous_target.get_nanbox_f64()); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); drop(frame); drop(args); match result { diff --git a/crates/perry-runtime/src/object/class_registry/decl_accessors.rs b/crates/perry-runtime/src/object/class_registry/decl_accessors.rs index b126b10078..bfc72cc328 100644 --- a/crates/perry-runtime/src/object/class_registry/decl_accessors.rs +++ b/crates/perry-runtime/src/object/class_registry/decl_accessors.rs @@ -175,14 +175,10 @@ pub(crate) unsafe fn class_chain_getter_value( if acc.raw_get != 0 { let scope = crate::gc::RuntimeHandleScope::new(); let this = scope.root_nanbox_f64(this_of()); - // The compiled getter reads `this` from its parameter; nested code it - // calls may read the implicit `this`, so publish the receiver too. - let prev = - scope.root_nanbox_f64(crate::object::js_implicit_this_set(this.get_nanbox_f64())); + // The compiled getter reads `this` from its parameter. let _boundary = crate::object::prototype_chain::UserCodeResolutionBoundary::enter(); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(acc.raw_get); + let f = crate::closure::body_call::js_method_body_fn!(acc.raw_get as *const u8;); let v = f(this.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return Some((crate::JSValue::from_bits(v.to_bits()), acc.raw_get)); } if acc.get != 0 { @@ -196,8 +192,7 @@ pub(crate) unsafe fn class_chain_getter_value( /// declares an accessor for it: `None` when no accessor answers (the caller /// keeps resolving), `Some(true)` when a setter ran, `Some(false)` when the /// accessor has no setter — the write must not create a data property. -/// `this` is the receiver the setter sees, also published as the implicit -/// `this` for the duration of the call. +/// `this` is the receiver the setter sees. /// /// # Safety /// `this` and `value` are live values. @@ -215,11 +210,8 @@ pub(crate) unsafe fn class_chain_setter_apply( let value_h = scope.root_nanbox_f64(value); let (_holder, acc) = class_proto_accessor(class_id, name)?; if acc.raw_set != 0 { - let prev = - scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_h.get_nanbox_f64())); - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(acc.raw_set); + let f = crate::closure::body_call::js_method_body_fn!(acc.raw_set as *const u8; value); let _ = f(this_h.get_nanbox_f64(), value_h.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return Some(true); } if acc.set != 0 { diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index c9c75951d1..b06a4522c4 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -1061,17 +1061,13 @@ pub(crate) unsafe fn class_symbol_getter_value( return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); } let result = if is_static { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); - let f: extern "C" fn() -> f64 = std::mem::transmute(getter); + let f = crate::closure::body_call::js_bare_body_fn!(getter as *const u8;); let result = f(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else { - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); + let f = crate::closure::body_call::js_method_body_fn!(getter as *const u8;); f(receiver) }; return Some(result); @@ -1109,16 +1105,14 @@ pub(crate) unsafe fn class_symbol_setter_apply( if let Some(&(_, setter)) = map.get(&(cid, sym_key, is_static)) { if setter != 0 { if is_static { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope - .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); + let f = + crate::closure::body_call::js_bare_body_fn!(setter as *const u8; a0); let _ = f(value); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } else { - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter); + let f = + crate::closure::body_call::js_method_body_fn!(setter as *const u8; a0); let _ = f(receiver, value); } } @@ -1302,7 +1296,7 @@ pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option } /// Call a static method func_ptr with `args` (no `this` prepend — static -/// methods read `this` from the implicit-this slot, set by the caller). +/// methods resolve `this` through `js_static_this_resolve`). /// Mirrors the arity dispatch of `call_vtable_method` minus the receiver arg. pub(crate) unsafe fn call_static_method( func_ptr: usize, @@ -1321,27 +1315,27 @@ pub(crate) unsafe fn call_static_method( } } match param_count { - 0 => (std::mem::transmute:: f64>(func_ptr))(), - 1 => (std::mem::transmute:: f64>(func_ptr))(a( + 0 => (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8;))(), + 1 => (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0))(a( args_ptr, args_len, 0, )), - 2 => (std::mem::transmute:: f64>(func_ptr))( + 2 => (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1))( a(args_ptr, args_len, 0), a(args_ptr, args_len, 1), ), - 3 => (std::mem::transmute:: f64>(func_ptr))( + 3 => (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1, a2))( a(args_ptr, args_len, 0), a(args_ptr, args_len, 1), a(args_ptr, args_len, 2), ), - 4 => (std::mem::transmute:: f64>(func_ptr))( + 4 => (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1, a2, a3))( a(args_ptr, args_len, 0), a(args_ptr, args_len, 1), a(args_ptr, args_len, 2), a(args_ptr, args_len, 3), ), 5 => { - (std::mem::transmute:: f64>(func_ptr))( + (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1, a2, a3, a4))( a(args_ptr, args_len, 0), a(args_ptr, args_len, 1), a(args_ptr, args_len, 2), @@ -1349,20 +1343,18 @@ pub(crate) unsafe fn call_static_method( a(args_ptr, args_len, 4), ) } - 6 => (std::mem::transmute:: f64>( - func_ptr, - ))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - a(args_ptr, args_len, 4), - a(args_ptr, args_len, 5), - ), + 6 => { + (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1, a2, a3, a4, a5))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + a(args_ptr, args_len, 4), + a(args_ptr, args_len, 5), + ) + } 7 => { - (std::mem::transmute:: f64>( - func_ptr, - ))( + (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1, a2, a3, a4, a5, a6))( a(args_ptr, args_len, 0), a(args_ptr, args_len, 1), a(args_ptr, args_len, 2), @@ -1372,19 +1364,18 @@ pub(crate) unsafe fn call_static_method( a(args_ptr, args_len, 6), ) } - _ => (std::mem::transmute::< - usize, - extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64, - >(func_ptr))( - a(args_ptr, args_len, 0), - a(args_ptr, args_len, 1), - a(args_ptr, args_len, 2), - a(args_ptr, args_len, 3), - a(args_ptr, args_len, 4), - a(args_ptr, args_len, 5), - a(args_ptr, args_len, 6), - a(args_ptr, args_len, 7), - ), + _ => { + (crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0, a1, a2, a3, a4, a5, a6, a7))( + a(args_ptr, args_len, 0), + a(args_ptr, args_len, 1), + a(args_ptr, args_len, 2), + a(args_ptr, args_len, 3), + a(args_ptr, args_len, 4), + a(args_ptr, args_len, 5), + a(args_ptr, args_len, 6), + a(args_ptr, args_len, 7), + ) + } } } @@ -1478,8 +1469,8 @@ pub(crate) unsafe fn nm_static_buffer_proto_chain( /// #1788: dispatch a static method on a class value (`Sub.greet()` where /// `Sub extends make(...)`, or a class-object value) by walking the class_id /// parent chain in `CLASS_STATIC_METHODS`. Binds `this` to the receiver (so -/// `this.` resolves through the subclass's static-field chain), calls -/// the method, and restores the previous implicit-this. On miss returns the +/// `this.` resolves through the subclass's static-field chain) and calls +/// the method. On miss returns the /// receiver unchanged — preserving the prior "yield the class ref for a /// chained call during module init" behavior for genuinely-absent methods. #[no_mangle] @@ -1525,8 +1516,6 @@ pub unsafe extern "C" fn js_class_static_method_call( return receiver; } if let Some((func_ptr, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); // Receiver-sensitive static `this`: arm the one-shot override so the // method prologue (`js_static_this_resolve`) sees the DYNAMIC receiver @@ -1564,7 +1553,6 @@ pub unsafe extern "C" fn js_class_static_method_call( }; crate::object::static_this_disarm(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } // #10893: not a static METHOD — a static ACCESSOR on the class-id chain @@ -1592,7 +1580,12 @@ pub unsafe extern "C" fn js_class_static_method_call( if let Some(v) = field_val { let fv = crate::value::JSValue::from_bits(v.to_bits()); if !fv.is_undefined() && !fv.is_null() { - return crate::closure::js_native_call_value(v, args_ptr, args_len); + return crate::closure::js_native_call_value( + v, + crate::closure::plain_call_receiver(), + args_ptr, + args_len, + ); } } cid = get_parent_class_id(cid).unwrap_or(0); @@ -1624,11 +1617,12 @@ pub unsafe extern "C" fn js_class_static_method_call( let member = f64::from_bits(member.bits()); let mv = crate::value::JSValue::from_bits(member.to_bits()); if !mv.is_undefined() && !mv.is_null() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + member, + crate::closure::JsThis::from_f64(receiver), + args_ptr, + args_len, + ); return result; } } @@ -1642,22 +1636,21 @@ pub unsafe extern "C" fn js_class_static_method_call( // `class X extends Promise` — inherited builtin static (`X.all(...)`, // `X.resolve(...)`, …). Dispatch the spec static with `this` = the subclass // receiver so `NewPromiseCapability(X)` constructs the subclass. Resolves the - // reified static value and calls it (its thunk reads `this` from the - // implicit-this slot, already bound to `receiver` by the caller above). + // reified static value and calls it with `receiver` as its `this`. if super::promise_parent_in_chain(class_id) && crate::object::promise_static_function_spec(name).is_some() { let static_val = crate::object::js_promise_static_function_value(name.as_ptr(), name.len()); if static_val.to_bits() != crate::value::TAG_UNDEFINED { - // The reified static thunk reads its `this` constructor from the - // implicit-this slot, so bind it to the subclass receiver for the - // duration of the call — `NewPromiseCapability(receiver)` then - // constructs the subclass. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value(static_val, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + // The reified static thunk reads its `this` constructor from its + // `this` argument, so pass the subclass receiver — + // `NewPromiseCapability(receiver)` then constructs the subclass. + let result = crate::closure::native_call_value_this( + static_val, + crate::closure::JsThis::from_f64(receiver), + args_ptr, + args_len, + ); return result; } } @@ -1732,11 +1725,12 @@ pub unsafe extern "C" fn js_class_static_method_call( // not a real inherited member. && member.to_bits() != closure_val.to_bits() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + member, + crate::closure::JsThis::from_f64(receiver), + args_ptr, + args_len, + ); return result; } } @@ -1750,11 +1744,12 @@ pub unsafe extern "C" fn js_class_static_method_call( }; if let Some(member) = fn_proto_member { if crate::collection_iter::is_callable(member) { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + member, + crate::closure::JsThis::from_f64(receiver), + args_ptr, + args_len, + ); return result; } } diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index 3af1e17e3a..cd3b3493af 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -288,7 +288,7 @@ pub(crate) unsafe fn class_private_instance_getter_value( if getter == 0 { return None; } - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); + let f = crate::closure::body_call::js_method_body_fn!(getter as *const u8;); Some(f(receiver)) } @@ -306,7 +306,7 @@ pub(crate) unsafe fn class_private_instance_setter_apply( if decl.set == 0 { return false; } - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(decl.set); + let f = crate::closure::body_call::js_method_body_fn!(decl.set as *const u8; value); let _ = f(receiver, value); true } @@ -329,9 +329,6 @@ pub(crate) unsafe fn call_private_static_method_for_owner( let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); let private_brand = scope.root_nanbox_f64(private_brand); - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - this_value.get_nanbox_f64(), - )); crate::object::static_private_owner_push(private_brand.get_nanbox_f64()); crate::object::private_lexical_brand_push(private_brand.get_nanbox_f64()); crate::object::static_this_arm_if_unarmed(this_value.get_nanbox_f64()); @@ -339,6 +336,5 @@ pub(crate) unsafe fn call_private_static_method_for_owner( crate::object::static_this_disarm(); crate::object::private_lexical_brand_pop(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs b/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs index d919359b38..37600b1a2c 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs @@ -98,15 +98,12 @@ pub(crate) unsafe fn try_static_accessor_value_call( } let callee_handle = scope.root_nanbox_f64(callee); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - let receiver = receiver_handle.get_nanbox_f64(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value( + Some(crate::closure::native_call_value_this( callee_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver_handle.get_nanbox_f64()), args.as_ptr(), args.len(), - ); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - Some(result) + )) } pub(crate) unsafe fn class_static_accessor_getter_value( @@ -166,7 +163,7 @@ unsafe fn private_static_accessor_getter_value( crate::object::field_get_set::accessor_receiver_override_take().unwrap_or(receiver); crate::object::static_this_arm_if_unarmed(receiver); crate::object::static_private_owner_push(owner); - let f: extern "C" fn() -> f64 = std::mem::transmute(getter); + let f = crate::closure::body_call::js_bare_body_fn!(getter as *const u8;); let result = f(); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); @@ -246,7 +243,7 @@ unsafe fn private_static_accessor_setter_apply( if setter != 0 { crate::object::static_this_arm_if_unarmed(receiver); crate::object::static_private_owner_push(receiver); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); + let f = crate::closure::body_call::js_bare_body_fn!(setter as *const u8; value); let _ = f(value); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 680fd95ade..52ddaa0132 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -109,7 +109,7 @@ pub(crate) fn ensure_function_prototype_object( // synthetic prototype must carry the EventEmitter methods (`emit`/`on`/`once`/ // …) so the `Object.setPrototypeOf(x, EventEmitter.prototype)` mixin pattern // (pino's logger prototype) gives `x` a working `emit`/`on`. The installed - // closures read IMPLICIT_THIS, so a plain object that merely inherits this + // closures read their `this` argument, so a plain object that merely inherits this // prototype dispatches against ITSELF (listener state is keyed by the receiver // object, not a captured instance). Mirrors what `Stream.prototype` already // does. This proto is cached (`class_prototype_object_root_store` above), so @@ -565,11 +565,11 @@ unsafe fn inherited_proto_accessor_value( if acc.get == 0 { return Some(JSValue::undefined()); } - // Route through `invoke_accessor_getter` rather than a bare - // `js_implicit_this_set` + `js_closure_call0`. A getter installed via + // Route through `invoke_accessor_getter` rather than a bare closure call + // with the receiver as `this`. A getter installed via // `Object.defineProperty(Class.prototype, name, { get })` is an ORDINARY // method closure whose body reads `this` from its captured receiver slot — - // not from IMPLICIT_THIS — so merely setting IMPLICIT_THIS left the getter + // not from its `this` argument — so merely passing `this` left the getter // observing the prototype it lives on instead of the instance (winston's // `get transports()` saw the prototype, whose `this._readableState` is // undefined → "Cannot convert undefined or null to object"). @@ -851,8 +851,6 @@ unsafe fn resolve_proto_chain_field_inner( } } let field_val = if let Some(receiver) = receiver { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous_this = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); // The recursive `get_field(proto_obj, key)` re-derives a class // getter's `this` from `proto_obj`; stash the real instance so an // inherited getter (object-literal `get x()` on an @@ -861,7 +859,6 @@ unsafe fn resolve_proto_chain_field_inner( super::super::field_get_set::accessor_receiver_override_begin(receiver); let value = js_object_get_field_by_name(proto_obj as *const _, key); super::super::field_get_set::accessor_receiver_override_end(prev_override); - js_implicit_this_set(previous_this.get_nanbox_f64()); value } else { js_object_get_field_by_name(proto_obj as *const _, key) @@ -931,12 +928,9 @@ unsafe fn bound_native_parent_prototype_field( { return None; } - let this_scope = crate::gc::RuntimeHandleScope::new(); - let previous_this = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); let prev_override = super::super::field_get_set::accessor_receiver_override_begin(receiver); let value = js_object_get_field_by_name(proto_obj as *const _, key); super::super::field_get_set::accessor_receiver_override_end(prev_override); - js_implicit_this_set(previous_this.get_nanbox_f64()); (!value.is_undefined() && !value.is_null()).then_some(value) } diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index 4ab6a4c3eb..1262ec73fb 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -210,29 +210,33 @@ pub(crate) fn class_registered_static_accessor_ptrs( } /// Trampoline giving a raw vtable getter func_ptr (`fn(this) -> f64`) the -/// closure calling convention. The receiver comes from `IMPLICIT_THIS`, set +/// closure calling convention. The receiver is the `this` argument passed /// by the method-call dispatch the closure value travels through. -extern "C" fn class_accessor_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn class_accessor_getter_thunk( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let raw = crate::closure::js_closure_get_capture_ptr(closure, 0) as usize; if raw == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let this = crate::object::js_implicit_this_get(); - let f: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(raw) }; + let this = this.as_f64(); + let f = unsafe { crate::closure::body_call::js_method_body_fn!(raw as *const u8;) }; f(this) } /// Trampoline for a raw vtable setter func_ptr (`fn(this, value) -> f64`). extern "C" fn class_accessor_setter_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { let raw = crate::closure::js_closure_get_capture_ptr(closure, 0) as usize; if raw == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let this = crate::object::js_implicit_this_get(); - let f: extern "C" fn(f64, f64) -> f64 = unsafe { std::mem::transmute(raw) }; + let this = this.as_f64(); + let f = unsafe { crate::closure::body_call::js_method_body_fn!(raw as *const u8; a0) }; f(this, value) } diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index da95bbd52f..69a5583a89 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -369,7 +369,7 @@ pub static CLASS_VTABLE_REGISTRY: ImageTable< /// #1788: per-class STATIC-method registry: class_id -> { name -> (func_ptr, /// param_count, has_rest) }. Static methods are emitted as `perry_static_*` -/// (no `this` param — they read `this` from the implicit-this slot) and are +/// (no `this` param — they resolve `this` via `js_static_this_resolve`) and are /// NOT in the instance vtable above, so a subclass whose parent is a /// class-expression value (`class Sub extends make(...) {}`) can't resolve an /// inherited static method (`Sub.greet()`) at compile time. This table is diff --git a/crates/perry-runtime/src/object/class_value.rs b/crates/perry-runtime/src/object/class_value.rs index 1d6909fed8..68606494dd 100644 --- a/crates/perry-runtime/src/object/class_value.rs +++ b/crates/perry-runtime/src/object/class_value.rs @@ -102,7 +102,10 @@ pub(crate) unsafe fn class_closure_id_unchecked(closure: *const ClosureHeader) - /// class function object (and how one is recognized); [[Construct]] never /// reaches it — `new` decodes the class id and runs the class's constructor. #[no_mangle] -pub unsafe extern "C" fn js_class_constructor_called(closure: *const ClosureHeader) -> f64 { +pub unsafe extern "C" fn js_class_constructor_called( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let name = unsafe { class_closure_id_unchecked(closure) } .and_then(super::class_registry::class_name_for_id) .unwrap_or_default(); @@ -694,7 +697,7 @@ pub(crate) unsafe fn class_static_accessor_call_get( if acc.raw_get != 0 { crate::object::static_this_arm_if_unarmed(this); crate::object::static_private_owner_push(receiver); - let f: extern "C" fn() -> f64 = std::mem::transmute(acc.raw_get); + let f = crate::closure::body_call::js_bare_body_fn!(acc.raw_get as *const u8;); let result = f(); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); @@ -718,7 +721,7 @@ pub(crate) unsafe fn class_static_accessor_call_set( if acc.raw_set != 0 { crate::object::static_this_arm_if_unarmed(receiver); crate::object::static_private_owner_push(receiver); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(acc.raw_set); + let f = crate::closure::body_call::js_bare_body_fn!(acc.raw_set as *const u8; value); let _ = f(value); crate::object::static_private_owner_pop(); crate::object::static_this_disarm(); diff --git a/crates/perry-runtime/src/object/collection_proto_thunks.rs b/crates/perry-runtime/src/object/collection_proto_thunks.rs index 277aabb80b..84ed851edc 100644 --- a/crates/perry-runtime/src/object/collection_proto_thunks.rs +++ b/crates/perry-runtime/src/object/collection_proto_thunks.rs @@ -7,7 +7,7 @@ //! which previously resolved to `global_this_builtin_noop_thunk` and silently //! did nothing. Per spec these methods must perform a `this` brand check and //! throw a `TypeError` when called on an incompatible receiver. The thunks -//! below read the `IMPLICIT_THIS` receiver (set by the `.call`/`.apply` +//! below take the receiver as `this` (set by the `.call`/`.apply` //! dispatch), brand-check it, throw on mismatch, and otherwise dispatch to the //! real runtime helper — so reflective collection calls now also *work*. //! @@ -414,8 +414,8 @@ fn throw_incompatible_receiver(proto: &str, method: &str, receiver_bits: u64) -> } #[inline] -fn set_receiver_or_throw(method: &str) -> *mut crate::set::SetHeader { - let bits = IMPLICIT_THIS.with(|c| c.get()); +fn set_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> *mut crate::set::SetHeader { + let bits = this.bits(); let set = crate::set::set_ptr_from_receiver_bits(bits).or_else(|| { match super::map_set_subclass::subclass_backing_of(f64::from_bits(bits)) { Some(super::map_set_subclass::CollectionBacking::Set(set)) => Some(set), @@ -429,8 +429,8 @@ fn set_receiver_or_throw(method: &str) -> *mut crate::set::SetHeader { } #[inline] -fn map_receiver_or_throw(method: &str) -> *mut crate::map::MapHeader { - let bits = IMPLICIT_THIS.with(|c| c.get()); +fn map_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> *mut crate::map::MapHeader { + let bits = this.bits(); let map = crate::map::map_ptr_from_receiver_bits(bits).or_else(|| { match super::map_set_subclass::subclass_backing_of(f64::from_bits(bits)) { Some(super::map_set_subclass::CollectionBacking::Map(map)) => Some(map), @@ -444,10 +444,10 @@ fn map_receiver_or_throw(method: &str) -> *mut crate::map::MapHeader { } /// Read the original receiver after its brand has been checked. Some native -/// callers publish a raw pointer in IMPLICIT_THIS; normalize it before rooting +/// callers pass a raw pointer as `this`; normalize it before rooting /// or exposing it to JavaScript. The backing collection is not the receiver. -fn collection_this_value() -> f64 { - let bits = IMPLICIT_THIS.with(|c| c.get()); +fn collection_this_value(this: crate::closure::JsThis) -> f64 { + let bits = this.bits(); if bits >> 48 == 0 { crate::value::js_nanbox_pointer(bits as i64) } else { @@ -456,8 +456,13 @@ fn collection_this_value() -> f64 { } #[inline] -fn weak_receiver_or_throw(expected: u32, proto: &str, method: &str) -> f64 { - let receiver = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn weak_receiver_or_throw( + this: crate::closure::JsThis, + expected: u32, + proto: &str, + method: &str, +) -> f64 { + let receiver = f64::from_bits(this.bits()); match super::weak_class_id_from_receiver(receiver) { Some(cid) if cid == expected => receiver, _ => throw_incompatible_receiver(proto, method, receiver.to_bits()), @@ -466,62 +471,69 @@ fn weak_receiver_or_throw(expected: u32, proto: &str, method: &str) -> f64 { pub(super) extern "C" fn set_proto_add_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, v: f64, ) -> f64 { - let set = set_receiver_or_throw("add"); + let set = set_receiver_or_throw(this, "add"); let scope = crate::gc::RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(collection_this_value()); + let receiver = scope.root_nanbox_f64(collection_this_value(this)); crate::set::js_set_add(set, v); receiver.get_nanbox_f64() } pub(super) extern "C" fn set_proto_has_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, v: f64, ) -> f64 { - let set = set_receiver_or_throw("has"); + let set = set_receiver_or_throw(this, "has"); f64::from_bits(crate::value::JSValue::bool(crate::set::js_set_has(set, v) != 0).bits()) } pub(super) extern "C" fn set_proto_size_getter_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let set = set_receiver_or_throw("size"); + let set = set_receiver_or_throw(this, "size"); crate::set::js_set_size(set) as f64 } pub(super) extern "C" fn set_proto_delete_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, v: f64, ) -> f64 { - let set = set_receiver_or_throw("delete"); + let set = set_receiver_or_throw(this, "delete"); f64::from_bits(crate::value::JSValue::bool(crate::set::js_set_delete(set, v) != 0).bits()) } pub(super) extern "C" fn set_proto_clear_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let set = set_receiver_or_throw("clear"); + let set = set_receiver_or_throw(this, "clear"); crate::set::js_set_clear(set); f64::from_bits(crate::value::TAG_UNDEFINED) } pub(super) extern "C" fn set_proto_foreach_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, cb: f64, this_arg: f64, ) -> f64 { - let set = set_receiver_or_throw("forEach"); - crate::set::js_set_foreach_with_collection(set, cb, this_arg, collection_this_value()); + let set = set_receiver_or_throw(this, "forEach"); + crate::set::js_set_foreach_with_collection(set, cb, this_arg, collection_this_value(this)); f64::from_bits(crate::value::TAG_UNDEFINED) } pub(super) extern "C" fn set_proto_values_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let set = set_receiver_or_throw("values"); + let set = set_receiver_or_throw(this, "values"); f64::from_bits( crate::value::JSValue::pointer( crate::collection_iter_object::js_set_values_iter_obj(set) as *mut u8 @@ -532,9 +544,10 @@ pub(super) extern "C" fn set_proto_values_thunk( pub(super) extern "C" fn set_proto_keys_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let set = set_receiver_or_throw("keys"); + let set = set_receiver_or_throw(this, "keys"); f64::from_bits( crate::value::JSValue::pointer( crate::collection_iter_object::js_set_values_iter_obj(set) as *mut u8 @@ -545,9 +558,10 @@ pub(super) extern "C" fn set_proto_keys_thunk( pub(super) extern "C" fn set_proto_entries_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let set = set_receiver_or_throw("entries"); + let set = set_receiver_or_throw(this, "entries"); f64::from_bits( crate::value::JSValue::pointer( crate::collection_iter_object::js_set_entries_iter_obj(set) as *mut u8 @@ -558,71 +572,79 @@ pub(super) extern "C" fn set_proto_entries_thunk( pub(super) extern "C" fn map_proto_get_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, ) -> f64 { - let map = map_receiver_or_throw("get"); + let map = map_receiver_or_throw(this, "get"); crate::map::js_map_get(map, k) } pub(super) extern "C" fn map_proto_set_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, v: f64, ) -> f64 { - let map = map_receiver_or_throw("set"); + let map = map_receiver_or_throw(this, "set"); let scope = crate::gc::RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(collection_this_value()); + let receiver = scope.root_nanbox_f64(collection_this_value(this)); crate::map::js_map_set(map, k, v); receiver.get_nanbox_f64() } pub(super) extern "C" fn map_proto_has_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, ) -> f64 { - let map = map_receiver_or_throw("has"); + let map = map_receiver_or_throw(this, "has"); f64::from_bits(crate::value::JSValue::bool(crate::map::js_map_has(map, k) != 0).bits()) } pub(super) extern "C" fn map_proto_size_getter_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let map = map_receiver_or_throw("size"); + let map = map_receiver_or_throw(this, "size"); crate::map::js_map_size(map) as f64 } pub(super) extern "C" fn map_proto_delete_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, ) -> f64 { - let map = map_receiver_or_throw("delete"); + let map = map_receiver_or_throw(this, "delete"); f64::from_bits(crate::value::JSValue::bool(crate::map::js_map_delete(map, k) != 0).bits()) } pub(super) extern "C" fn map_proto_clear_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let map = map_receiver_or_throw("clear"); + let map = map_receiver_or_throw(this, "clear"); crate::map::js_map_clear(map); f64::from_bits(crate::value::TAG_UNDEFINED) } pub(super) extern "C" fn map_proto_foreach_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, cb: f64, this_arg: f64, ) -> f64 { - let map = map_receiver_or_throw("forEach"); - crate::map::js_map_foreach_with_collection(map, cb, this_arg, collection_this_value()); + let map = map_receiver_or_throw(this, "forEach"); + crate::map::js_map_foreach_with_collection(map, cb, this_arg, collection_this_value(this)); f64::from_bits(crate::value::TAG_UNDEFINED) } pub(super) extern "C" fn map_proto_keys_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let map = map_receiver_or_throw("keys"); + let map = map_receiver_or_throw(this, "keys"); f64::from_bits( crate::value::JSValue::pointer( crate::collection_iter_object::js_map_keys_iter_obj(map) as *mut u8 @@ -633,9 +655,10 @@ pub(super) extern "C" fn map_proto_keys_thunk( pub(super) extern "C" fn map_proto_values_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let map = map_receiver_or_throw("values"); + let map = map_receiver_or_throw(this, "values"); f64::from_bits( crate::value::JSValue::pointer( crate::collection_iter_object::js_map_values_iter_obj(map) as *mut u8 @@ -646,9 +669,10 @@ pub(super) extern "C" fn map_proto_values_thunk( pub(super) extern "C" fn map_proto_entries_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _v: f64, ) -> f64 { - let map = map_receiver_or_throw("entries"); + let map = map_receiver_or_throw(this, "entries"); f64::from_bits( crate::value::JSValue::pointer( crate::collection_iter_object::js_map_entries_iter_obj(map) as *mut u8 @@ -659,25 +683,39 @@ pub(super) extern "C" fn map_proto_entries_thunk( pub(super) extern "C" fn weakset_proto_add_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, v: f64, ) -> f64 { - let r = weak_receiver_or_throw(crate::weakref::CLASS_ID_WEAKSET, "WeakSet.prototype", "add"); + let r = weak_receiver_or_throw( + this, + crate::weakref::CLASS_ID_WEAKSET, + "WeakSet.prototype", + "add", + ); crate::weakref::js_weakset_add(r, v) } pub(super) extern "C" fn weakset_proto_has_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, v: f64, ) -> f64 { - let r = weak_receiver_or_throw(crate::weakref::CLASS_ID_WEAKSET, "WeakSet.prototype", "has"); + let r = weak_receiver_or_throw( + this, + crate::weakref::CLASS_ID_WEAKSET, + "WeakSet.prototype", + "has", + ); crate::weakref::js_weakmap_has(r, v) } pub(super) extern "C" fn weakset_proto_delete_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, v: f64, ) -> f64 { let r = weak_receiver_or_throw( + this, crate::weakref::CLASS_ID_WEAKSET, "WeakSet.prototype", "delete", @@ -687,34 +725,54 @@ pub(super) extern "C" fn weakset_proto_delete_thunk( pub(super) extern "C" fn weakmap_proto_get_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, ) -> f64 { - let r = weak_receiver_or_throw(crate::weakref::CLASS_ID_WEAKMAP, "WeakMap.prototype", "get"); + let r = weak_receiver_or_throw( + this, + crate::weakref::CLASS_ID_WEAKMAP, + "WeakMap.prototype", + "get", + ); crate::weakref::js_weakmap_get(r, k) } pub(super) extern "C" fn weakmap_proto_set_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, v: f64, ) -> f64 { - let r = weak_receiver_or_throw(crate::weakref::CLASS_ID_WEAKMAP, "WeakMap.prototype", "set"); + let r = weak_receiver_or_throw( + this, + crate::weakref::CLASS_ID_WEAKMAP, + "WeakMap.prototype", + "set", + ); crate::weakref::js_weakmap_set(r, k, v) } pub(super) extern "C" fn weakmap_proto_has_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, ) -> f64 { - let r = weak_receiver_or_throw(crate::weakref::CLASS_ID_WEAKMAP, "WeakMap.prototype", "has"); + let r = weak_receiver_or_throw( + this, + crate::weakref::CLASS_ID_WEAKMAP, + "WeakMap.prototype", + "has", + ); crate::weakref::js_weakmap_has(r, k) } pub(super) extern "C" fn weakmap_proto_delete_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, k: f64, ) -> f64 { let r = weak_receiver_or_throw( + this, crate::weakref::CLASS_ID_WEAKMAP, "WeakMap.prototype", "delete", diff --git a/crates/perry-runtime/src/object/collection_proto_thunks/subclass_tests.rs b/crates/perry-runtime/src/object/collection_proto_thunks/subclass_tests.rs index 37b803dab5..68b288b4b1 100644 --- a/crates/perry-runtime/src/object/collection_proto_thunks/subclass_tests.rs +++ b/crates/perry-runtime/src/object/collection_proto_thunks/subclass_tests.rs @@ -16,27 +16,67 @@ fn reflective_map_mutators_keep_subclass_identity() { let _lock = crate::gc::global_side_table_test_lock(); let scope = crate::gc::RuntimeHandleScope::new(); let receiver = subclass(&scope, 0); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - receiver.get_nanbox_f64(), - )); assert_eq!( - map_proto_set_thunk(std::ptr::null(), 1.0, 10.0).to_bits(), + map_proto_set_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0, + 10.0 + ) + .to_bits(), receiver.get_nanbox_f64().to_bits() ); - assert_eq!(map_proto_get_thunk(std::ptr::null(), 1.0), 10.0); - assert_eq!(map_proto_size_getter_thunk(std::ptr::null()), 1.0); assert_eq!( - map_proto_delete_thunk(std::ptr::null(), 1.0).to_bits(), + map_proto_get_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ), + 10.0 + ); + assert_eq!( + map_proto_size_getter_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()) + ), + 1.0 + ); + assert_eq!( + map_proto_delete_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ) + .to_bits(), crate::value::TAG_TRUE ); assert_eq!( - map_proto_has_thunk(std::ptr::null(), 1.0).to_bits(), + map_proto_has_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ) + .to_bits(), crate::value::TAG_FALSE ); - map_proto_set_thunk(std::ptr::null(), 2.0, 20.0); - map_proto_clear_thunk(std::ptr::null(), 0.0); - assert_eq!(map_proto_size_getter_thunk(std::ptr::null()), 0.0); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); + map_proto_set_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 2.0, + 20.0, + ); + map_proto_clear_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 0.0, + ); + assert_eq!( + map_proto_size_getter_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()) + ), + 0.0 + ); } #[test] @@ -44,30 +84,66 @@ fn reflective_set_mutators_keep_subclass_identity() { let _lock = crate::gc::global_side_table_test_lock(); let scope = crate::gc::RuntimeHandleScope::new(); let receiver = subclass(&scope, 1); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - receiver.get_nanbox_f64(), - )); assert_eq!( - set_proto_add_thunk(std::ptr::null(), 1.0).to_bits(), + set_proto_add_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ) + .to_bits(), receiver.get_nanbox_f64().to_bits() ); assert_eq!( - set_proto_has_thunk(std::ptr::null(), 1.0).to_bits(), + set_proto_has_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ) + .to_bits(), crate::value::TAG_TRUE ); - assert_eq!(set_proto_size_getter_thunk(std::ptr::null()), 1.0); assert_eq!( - set_proto_delete_thunk(std::ptr::null(), 1.0).to_bits(), + set_proto_size_getter_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()) + ), + 1.0 + ); + assert_eq!( + set_proto_delete_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ) + .to_bits(), crate::value::TAG_TRUE ); assert_eq!( - set_proto_has_thunk(std::ptr::null(), 1.0).to_bits(), + set_proto_has_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 1.0 + ) + .to_bits(), crate::value::TAG_FALSE ); - set_proto_add_thunk(std::ptr::null(), 2.0); - set_proto_clear_thunk(std::ptr::null(), 0.0); - assert_eq!(set_proto_size_getter_thunk(std::ptr::null()), 0.0); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); + set_proto_add_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 2.0, + ); + set_proto_clear_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + 0.0, + ); + assert_eq!( + set_proto_size_getter_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()) + ), + 0.0 + ); } #[test] diff --git a/crates/perry-runtime/src/object/dataview_proto_thunks.rs b/crates/perry-runtime/src/object/dataview_proto_thunks.rs index 6b01c74c3a..797faa9949 100644 --- a/crates/perry-runtime/src/object/dataview_proto_thunks.rs +++ b/crates/perry-runtime/src/object/dataview_proto_thunks.rs @@ -13,8 +13,8 @@ //! DataView *instances* already worked: `dv.getInt32(0)`, `dv.byteLength`, etc. //! are routed through codegen / `buffer_dispatch::dispatch_buffer_method` on a //! `BufferHeader` marked as a DataView. The thunks here only add the -//! *reflectable* own properties on the prototype; they read the receiver from -//! `IMPLICIT_THIS` (set by the `.call`/`.apply` dispatch), brand-check that it +//! *reflectable* own properties on the prototype; they take the receiver as +//! their `this` argument (supplied by the `.call`/`.apply` dispatch), brand-check that it //! is a DataView (throwing `TypeError` otherwise, per spec — covering test262's //! `this-has-no-*` / `this-is-not-object` cases), then dispatch to the SAME //! runtime helpers the instance path uses. @@ -24,12 +24,12 @@ use super::*; -/// Resolve the `IMPLICIT_THIS` receiver to a DataView `BufferHeader` address, +/// Resolve the `this` receiver to a DataView `BufferHeader` address, /// or `None` if the receiver is not a DataView. Mirrors `typed_array_receiver` /// / `array_buffer_receiver_addr` in `global_this.rs` (NaN-boxed pointer or /// raw-i64 form), then brand-checks `is_data_view`. -fn dataview_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); +fn dataview_receiver_addr(this: crate::closure::JsThis) -> Option { + let this_bits = this.bits(); let this_jsv = crate::value::JSValue::from_bits(this_bits); let raw = if this_jsv.is_pointer() { (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize @@ -47,8 +47,8 @@ fn dataview_receiver_addr() -> Option { /// Brand-check helper: returns the DataView address or throws a `TypeError` /// for an incompatible receiver. Mirrors `typed_array_brand_error`. -fn require_dataview_receiver() -> usize { - match dataview_receiver_addr() { +fn require_dataview_receiver(this: crate::closure::JsThis) -> usize { + match dataview_receiver_addr(this) { Some(addr) => addr, None => super::object_ops::throw_object_type_error( b"Method DataView.prototype called on incompatible receiver", @@ -67,8 +67,9 @@ fn undef() -> f64 { extern "C" fn dataview_byte_length_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let addr = require_dataview_receiver(); + let addr = require_dataview_receiver(this); let buf = addr as *const crate::buffer::BufferHeader; f64::from_bits( crate::value::JSValue::number(crate::buffer::js_buffer_length(buf) as f64).bits(), @@ -77,21 +78,25 @@ extern "C" fn dataview_byte_length_getter_thunk( extern "C" fn dataview_byte_offset_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let addr = require_dataview_receiver(); + let addr = require_dataview_receiver(this); let offset = crate::buffer::buffer_byte_offset(addr); f64::from_bits(crate::value::JSValue::number(offset as f64).bits()) } -extern "C" fn dataview_buffer_getter_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - let addr = require_dataview_receiver(); +extern "C" fn dataview_buffer_getter_thunk( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let addr = require_dataview_receiver(this); let backing = crate::buffer::buffer_backing_array_buffer(addr); f64::from_bits(crate::value::js_nanbox_pointer(backing as i64).to_bits()) } // --------------------------------------------------------------------------- // get* methods (spec length 1): getInt8 … getFloat64. Signature -// `(closure, byteOffset, rest)` where `rest` bundles the optional +// `(closure, this, byteOffset, rest)` where `rest` bundles the optional // `littleEndian` flag (big-endian default). // --------------------------------------------------------------------------- @@ -107,8 +112,8 @@ fn rest_first_arg(rest: f64) -> f64 { crate::array::js_array_get_f64(arr, 0) } -fn dataview_get(suffix: &str, offset: f64, rest: f64) -> f64 { - let addr = require_dataview_receiver(); +fn dataview_get(this: crate::closure::JsThis, suffix: &str, offset: f64, rest: f64) -> f64 { + let addr = require_dataview_receiver(this); let kind = match crate::buffer::DataViewKind::from_method_suffix(suffix) { Some(k) => k, None => return undef(), @@ -118,8 +123,14 @@ fn dataview_get(suffix: &str, offset: f64, rest: f64) -> f64 { crate::buffer::js_data_view_get(buf_f64, offset, kind, little) } -fn dataview_set(suffix: &str, offset: f64, value: f64, rest: f64) -> f64 { - let addr = require_dataview_receiver(); +fn dataview_set( + this: crate::closure::JsThis, + suffix: &str, + offset: f64, + value: f64, + rest: f64, +) -> f64 { + let addr = require_dataview_receiver(this); let kind = match crate::buffer::DataViewKind::from_method_suffix(suffix) { Some(k) => k, None => return undef(), @@ -133,10 +144,11 @@ macro_rules! dataview_get_thunk { ($name:ident, $suffix:literal) => { extern "C" fn $name( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, offset: f64, rest: f64, ) -> f64 { - dataview_get($suffix, offset, rest) + dataview_get(this, $suffix, offset, rest) } }; } @@ -145,11 +157,12 @@ macro_rules! dataview_set_thunk { ($name:ident, $suffix:literal) => { extern "C" fn $name( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, offset: f64, value: f64, rest: f64, ) -> f64 { - dataview_set($suffix, offset, value, rest) + dataview_set(this, $suffix, offset, value, rest) } }; } diff --git a/crates/perry-runtime/src/object/date_proto_thunks.rs b/crates/perry-runtime/src/object/date_proto_thunks.rs index 267e1c987d..f746764445 100644 --- a/crates/perry-runtime/src/object/date_proto_thunks.rs +++ b/crates/perry-runtime/src/object/date_proto_thunks.rs @@ -10,7 +10,7 @@ //! //! Per spec each `Date.prototype` getter performs `thisTimeValue(this)`, which //! throws a `TypeError` when `this` is not an Object with a `[[DateValue]]` -//! slot. These thunks read the `IMPLICIT_THIS` receiver (set by the +//! slot. These thunks take the receiver as their `this` argument (set by the //! `.call`/`.apply` dispatch), brand-check it via `is_date_value`, throw on //! mismatch, and otherwise dispatch to the SAME `js_date_get_*` helper the //! instance path uses — so reflective Date getter calls now also *work*. @@ -22,10 +22,10 @@ use super::*; -/// Resolve the `IMPLICIT_THIS` receiver to a Date time value, or throw a +/// Resolve the `this` receiver to a Date time value, or throw a /// `TypeError` (`thisTimeValue` brand check) when it is not a Date. -fn require_date_timestamp() -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn require_date_timestamp(this: crate::closure::JsThis) -> f64 { + let this = this.as_f64(); if crate::date::is_date_value(this) { crate::date::date_cell_timestamp(this) } else { @@ -35,8 +35,11 @@ fn require_date_timestamp() -> f64 { macro_rules! date_getter_thunk { ($name:ident, $rt:path) => { - extern "C" fn $name(_closure: *const crate::closure::ClosureHeader) -> f64 { - $rt(require_date_timestamp()) + extern "C" fn $name( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { + $rt(require_date_timestamp(this)) } }; } @@ -75,8 +78,11 @@ date_getter_thunk!( /// (`Date.prototype.toISOString.call(x)`) reaches here: brand-check `this` /// (TypeError on a non-Date receiver), then format-or-throw (RangeError when /// the time value is `NaN`). -extern "C" fn date_to_iso_string(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +extern "C" fn date_to_iso_string( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = f64::from_bits(this.bits()); if !crate::date::is_date_value(this) { super::object_ops::throw_object_type_error(b"this is not a Date object."); } @@ -87,8 +93,11 @@ extern "C" fn date_to_iso_string(_closure: *const crate::closure::ClosureHeader) /// `Date.prototype.toUTCString` (and its legacy `toGMTString` alias) /// reflective thunk: brand-check `this` (TypeError on a non-Date receiver), /// then format as "Sun, 23 Mar 2014 00:00:00 GMT" (or "Invalid Date"). -extern "C" fn date_to_utc_string(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +extern "C" fn date_to_utc_string( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = f64::from_bits(this.bits()); if !crate::date::is_date_value(this) { super::object_ops::throw_object_type_error(b"this is not a Date object."); } @@ -97,13 +106,19 @@ extern "C" fn date_to_utc_string(_closure: *const crate::closure::ClosureHeader) } #[cfg(feature = "temporal")] -extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureHeader) -> f64 { - let timestamp = require_date_timestamp(); +extern "C" fn date_to_temporal_instant( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let timestamp = require_date_timestamp(this); crate::temporal::instant::from_epoch_milliseconds_static(&[timestamp]) } #[cfg(not(feature = "temporal"))] -extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn date_to_temporal_instant( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -114,14 +129,17 @@ extern "C" fn date_to_temporal_instant(_closure: *const crate::closure::ClosureH /// otherwise return `Invoke(this, "toISOString")`. So it works for a real Date /// (Invalid → `null`), a plain object carrying its own `toISOString`, and a /// `Number(-Infinity)` wrapper (→ `null`). -extern "C" fn date_to_json(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +extern "C" fn date_to_json( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = f64::from_bits(this.bits()); date_to_json_value(this) } /// Apply the `Date.prototype.toJSON` algorithm to an explicit receiver. /// -/// The closure thunk above uses the implicit-this slot for normal JavaScript +/// The closure thunk above takes its receiver as the `this` argument for normal JavaScript /// calls. JSON.stringify already has the Date value in hand, so it calls this /// shared implementation directly instead of bypassing the observable /// `Invoke(O, "toISOString")` step with `date::js_date_to_json`. @@ -200,21 +218,19 @@ pub(crate) fn date_to_json_value(this: f64) -> f64 { { // `Call(func, O, «»)` — toJSON's `key` argument is intentionally not // forwarded (Invoke passes an empty argument list). - let prev = invoke_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - receiver.get_nanbox_f64(), - )); let closure = crate::value::js_nanbox_get_pointer(func.get_nanbox_f64()) as *const crate::closure::ClosureHeader; - let r = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return r; + return crate::closure::js_closure_call0( + closure, + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + ); } super::object_ops::throw_object_type_error(b"toISOString is not a function") } #[cfg(test)] -pub(crate) fn test_date_to_json_current_this() -> f64 { - date_to_json(std::ptr::null()) +pub(crate) fn test_date_to_json_with_this(this: f64) -> f64 { + date_to_json(std::ptr::null(), crate::closure::JsThis::from_f64(this)) } /// True iff `value` is an ECMAScript Object (`Type(O) is Object`). Objects are @@ -245,8 +261,12 @@ fn value_is_object(value: f64) -> bool { /// The `hint` comparison is against the primitive String value only: a /// `new String("number")` wrapper or any non-string is an invalid hint /// (TypeError), matching test262 `hint-invalid.js`. -extern "C" fn date_to_primitive(_closure: *const crate::closure::ClosureHeader, hint: f64) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +extern "C" fn date_to_primitive( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + hint: f64, +) -> f64 { + let this = f64::from_bits(this.bits()); // Step 1: brand-check `Type(O) is Object` (throws for undefined/null/86/''/true). if !value_is_object(this) { super::object_ops::throw_object_type_error( @@ -375,18 +395,29 @@ pub(crate) fn install_date_proto_to_primitive(proto_obj: *mut ObjectHeader) { // `getOwnPropertyDescriptor(Date, "UTC")`). /// `Date.now()` — current time in milliseconds. -extern "C" fn date_now_static(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn date_now_static( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::date::js_date_now() } /// `Date.parse(string)` — ToString the argument, then parse to a ms timestamp. -extern "C" fn date_parse_static(_closure: *const crate::closure::ClosureHeader, arg: f64) -> f64 { +extern "C" fn date_parse_static( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { let s = crate::value::js_jsvalue_to_string(arg); crate::date::js_date_parse(s as *const crate::string::StringHeader) } /// `Date.UTC(year, month?, day?, …)` — variadic; forwards to `js_date_utc`. -extern "C" fn date_utc_static(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { +extern "C" fn date_utc_static( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { let vals = super::global_this::global_this_rest_array_values(rest); crate::date::js_date_utc(vals.as_ptr(), vals.len() as i32) } @@ -428,8 +459,11 @@ pub(crate) fn install_date_constructor_statics(ctor: *mut crate::closure::Closur } /// Legacy `Date.prototype.getYear` — `getFullYear() - 1900` (NaN-preserving). -extern "C" fn date_get_year(_closure: *const crate::closure::ClosureHeader) -> f64 { - let fy = crate::date::js_date_get_full_year(require_date_timestamp()); +extern "C" fn date_get_year( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let fy = crate::date::js_date_get_full_year(require_date_timestamp(this)); if fy.is_nan() { fy } else { @@ -508,18 +542,18 @@ pub(crate) fn install_date_proto_getters(proto_obj: *mut ObjectHeader) { // `setDate.call(nonDate, 1)` silently produced garbage instead of throwing. // // Each setter performs `thisTimeValue(this)` (TypeError if `this` is not a -// Date) and is variadic, so these thunks read the `IMPLICIT_THIS` receiver, +// Date) and is variadic, so these thunks take the `this` receiver, // brand-check it, collect the `rest` arguments, and dispatch to the same // `js_date_apply_setter` the instance path uses. `js_date_apply_setter` reads // `[[DateValue]]` BEFORE coercing the arguments, so the read-before-ToNumber // ordering holds on the reflective path too. -/// Resolve the `IMPLICIT_THIS` receiver to a Date value, or throw a `TypeError` +/// Resolve the `this` receiver to a Date value, or throw a `TypeError` /// (`thisTimeValue` brand check) when it is not a Date. Returns the NaN-boxed /// `DateCell` value (the setter dispatch needs the receiver itself, not just /// its timestamp, so it can mutate the cell in place). -fn require_date_this() -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn require_date_this(this: crate::closure::JsThis) -> f64 { + let this = this.as_f64(); if crate::date::is_date_value(this) { this } else { @@ -533,8 +567,12 @@ fn require_date_this() -> f64 { /// rebuild. macro_rules! date_setter_thunk { ($name:ident, $is_utc:expr, $field:expr) => { - extern "C" fn $name(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = require_date_this(); + extern "C" fn $name( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + rest: f64, + ) -> f64 { + let this = require_date_this(this); let args = super::global_this::global_this_rest_array_values(rest); crate::date::js_date_apply_setter( this, @@ -585,15 +623,23 @@ date_setter_thunk!(date_set_utc_milliseconds, 1, 6); /// fallback simply defers to the non-locale formatter instead of statically /// pinning the Intl formatting web from the always-installed Date prototype. #[cfg(not(feature = "intl-namespace"))] -fn date_to_locale_opts_impl(_rest: f64, _ctx: crate::intl::TemporalLocaleCtx) -> f64 { - let this = require_date_this(); +fn date_to_locale_opts_impl( + this: crate::closure::JsThis, + _rest: f64, + _ctx: crate::intl::TemporalLocaleCtx, +) -> f64 { + let this = require_date_this(this); let s = crate::date::js_date_to_locale_string(this); crate::value::js_nanbox_string(s as i64) } #[cfg(feature = "intl-namespace")] -fn date_to_locale_opts_impl(rest: f64, ctx: crate::intl::TemporalLocaleCtx) -> f64 { - let this = require_date_this(); +fn date_to_locale_opts_impl( + this: crate::closure::JsThis, + rest: f64, + ctx: crate::intl::TemporalLocaleCtx, +) -> f64 { + let this = require_date_this(this); let epoch_ms = crate::date::date_cell_timestamp(this); if epoch_ms.is_nan() { // Invalid Date → "Invalid Date" for all three methods. @@ -613,23 +659,26 @@ fn date_to_locale_opts_impl(rest: f64, ctx: crate::intl::TemporalLocaleCtx) -> f extern "C" fn date_to_locale_string_opts( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - date_to_locale_opts_impl(rest, crate::intl::TemporalLocaleCtx::PlainDateTime) + date_to_locale_opts_impl(this, rest, crate::intl::TemporalLocaleCtx::PlainDateTime) } extern "C" fn date_to_locale_date_string_opts( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - date_to_locale_opts_impl(rest, crate::intl::TemporalLocaleCtx::PlainDate) + date_to_locale_opts_impl(this, rest, crate::intl::TemporalLocaleCtx::PlainDate) } extern "C" fn date_to_locale_time_string_opts( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - date_to_locale_opts_impl(rest, crate::intl::TemporalLocaleCtx::PlainTime) + date_to_locale_opts_impl(this, rest, crate::intl::TemporalLocaleCtx::PlainTime) } /// Install the brand-checked `Date.prototype` setter thunks. Called from diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index 000896b7c5..b58124d957 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -1493,11 +1493,10 @@ pub(crate) unsafe fn json_object_getter_value( return Some(f64::from_bits(TAG_UNDEFINED)); } let receiver = crate::value::js_nanbox_pointer(obj as i64); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(closure); - js_implicit_this_set(prev.get_nanbox_f64()); - Some(result) + Some(crate::closure::js_closure_call0( + closure, + crate::closure::JsThis::from_f64(receiver), + )) } /// Monotonic (#6386): has an accessor descriptor keyed `"constructor"` ever diff --git a/crates/perry-runtime/src/object/disposable_proto_thunks.rs b/crates/perry-runtime/src/object/disposable_proto_thunks.rs index 14b6869976..91b533c2c9 100644 --- a/crates/perry-runtime/src/object/disposable_proto_thunks.rs +++ b/crates/perry-runtime/src/object/disposable_proto_thunks.rs @@ -5,7 +5,7 @@ //! runtime helpers and never touch these thunks. The thunks exist for the //! *reflective* path — `DisposableStack.prototype.use`, method extraction //! (`const use = stack.use`), `.call`/`.apply`, and Test262's `verifyProperty` -//! descriptor checks. Each reads the `IMPLICIT_THIS` receiver, brand-checks it +//! descriptor checks. Each takes the receiver as `this`, brand-checks it //! against the stack class id, throws a `TypeError` on an incompatible //! receiver, and otherwise dispatches to the shared runtime helper. //! @@ -29,10 +29,14 @@ fn throw_incompatible(proto: &str, method: &str) -> ! { )) } -/// Resolve `IMPLICIT_THIS` to a stack `ObjectHeader` of the expected class id, +/// Resolve the `this` receiver to a stack `ObjectHeader` of the expected class id, /// or throw a `TypeError`. -fn stack_receiver_or_throw(want_async: bool, method: &str) -> *mut ObjectHeader { - let bits = IMPLICIT_THIS.with(|c| c.get()); +fn stack_receiver_or_throw( + this: crate::closure::JsThis, + want_async: bool, + method: &str, +) -> *mut ObjectHeader { + let bits = this.bits(); let value = f64::from_bits(bits); let proto = if want_async { "AsyncDisposableStack.prototype" @@ -55,43 +59,53 @@ fn stack_receiver_or_throw(want_async: bool, method: &str) -> *mut ObjectHeader pub(super) extern "C" fn ds_proto_use_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, resource: f64, ) -> f64 { - let stack = stack_receiver_or_throw(false, "use"); + let stack = stack_receiver_or_throw(this, false, "use"); js_disposable_stack_use(stack, resource) } pub(super) extern "C" fn ds_proto_adopt_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, on_dispose: f64, ) -> f64 { - let stack = stack_receiver_or_throw(false, "adopt"); + let stack = stack_receiver_or_throw(this, false, "adopt"); js_disposable_stack_adopt(stack, value, on_dispose) } pub(super) extern "C" fn ds_proto_defer_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, on_dispose: f64, ) -> f64 { - let stack = stack_receiver_or_throw(false, "defer"); + let stack = stack_receiver_or_throw(this, false, "defer"); js_disposable_stack_defer(stack, on_dispose) } -pub(super) extern "C" fn ds_proto_dispose_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let stack = stack_receiver_or_throw(false, "dispose"); +pub(super) extern "C" fn ds_proto_dispose_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let stack = stack_receiver_or_throw(this, false, "dispose"); js_disposable_stack_dispose(stack) } -pub(super) extern "C" fn ds_proto_move_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let stack = stack_receiver_or_throw(false, "move"); +pub(super) extern "C" fn ds_proto_move_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let stack = stack_receiver_or_throw(this, false, "move"); js_disposable_stack_move(stack) } pub(super) extern "C" fn ds_proto_disposed_getter_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let stack = stack_receiver_or_throw(false, "disposed"); + let stack = stack_receiver_or_throw(this, false, "disposed"); js_disposable_stack_disposed(stack) } @@ -99,45 +113,53 @@ pub(super) extern "C" fn ds_proto_disposed_getter_thunk( pub(super) extern "C" fn ads_proto_use_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, resource: f64, ) -> f64 { - let stack = stack_receiver_or_throw(true, "use"); + let stack = stack_receiver_or_throw(this, true, "use"); js_async_disposable_stack_use(stack, resource) } pub(super) extern "C" fn ads_proto_adopt_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, on_dispose: f64, ) -> f64 { - let stack = stack_receiver_or_throw(true, "adopt"); + let stack = stack_receiver_or_throw(this, true, "adopt"); js_disposable_stack_adopt(stack, value, on_dispose) } pub(super) extern "C" fn ads_proto_defer_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, on_dispose: f64, ) -> f64 { - let stack = stack_receiver_or_throw(true, "defer"); + let stack = stack_receiver_or_throw(this, true, "defer"); js_disposable_stack_defer(stack, on_dispose) } pub(super) extern "C" fn ads_proto_dispose_async_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let stack = stack_receiver_or_throw(true, "disposeAsync"); + let stack = stack_receiver_or_throw(this, true, "disposeAsync"); js_async_disposable_stack_dispose_async(stack) } -pub(super) extern "C" fn ads_proto_move_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let stack = stack_receiver_or_throw(true, "move"); +pub(super) extern "C" fn ads_proto_move_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let stack = stack_receiver_or_throw(this, true, "move"); js_disposable_stack_move(stack) } pub(super) extern "C" fn ads_proto_disposed_getter_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let stack = stack_receiver_or_throw(true, "disposed"); + let stack = stack_receiver_or_throw(this, true, "disposed"); js_disposable_stack_disposed(stack) } diff --git a/crates/perry-runtime/src/object/field_get_set/accessors.rs b/crates/perry-runtime/src/object/field_get_set/accessors.rs index 27d84f4df0..e3fec79a26 100644 --- a/crates/perry-runtime/src/object/field_get_set/accessors.rs +++ b/crates/perry-runtime/src/object/field_get_set/accessors.rs @@ -285,18 +285,14 @@ unsafe fn prototype_property_value_with_guard( return None; } let receiver = crate::value::js_nanbox_pointer(receiver_addr() as i64); - let previous_this = super::super::js_implicit_this_set(receiver); - // The DISPLACED `this` and the displaced accessor receiver both ride - // through `js_object_get_field_by_name` — which can run a getter — before - // being republished. Rooting the ACCESSOR_RECEIVER_OVERRIDE cell (#7231) - // protects the armed value, not these saved ones; that residual is what - // these two handles close. - let previous_this_h = scope.root_nanbox_f64(previous_this); + // The displaced accessor receiver rides through `js_object_get_field_by_name` + // — which can run a getter — before being republished. Rooting the + // ACCESSOR_RECEIVER_OVERRIDE cell (#7231) protects the armed value, not the + // saved one; that residual is what this handle closes. let prev_override = accessor_receiver_override_begin(receiver); let prev_override_h = prev_override.map(|v| scope.root_nanbox_f64(v)); let property = js_object_get_field_by_name(proto_ptr(), key()); accessor_receiver_override_end(prev_override_h.map(|h| h.get_nanbox_f64())); - super::super::js_implicit_this_set(previous_this_h.get_nanbox_f64()); if property.is_undefined() { None } else { @@ -486,14 +482,12 @@ pub(crate) unsafe fn invoke_accessor_getter(get_bits: u64, receiver: f64) -> JSV // // #9417: every value below is GC-managed and lives across an allocation. // `coerce_call_this` boxes a primitive receiver, `clone_closure_rebind_this` - // allocates a fresh `ClosureHeader`, and `js_closure_call0` runs USER CODE — + // allocates a fresh `ClosureHeader`, and the getter call runs USER CODE — // any of the three can drive an evacuating young-gen minor. A bare Rust // local is exactly the slot the collector cannot see or rewrite (#7249, // #7498), and the receiver here is not merely read afterwards: it is - // PUBLISHED into the GC-rooted `IMPLICIT_THIS` cell, and `prev` is written - // back into that same cell after the getter body has had a full turn to - // allocate. Either store installs a pre-collection address as some frame's - // `this`, after which every ordinary property read off it answers + // passed to the getter as its `this`. A stale copy would install a + // pre-collection address as the getter's `this`, after which every ordinary property read off it answers // `undefined` (`js_object_get_own_field_or_undef` fails its // `obj_type == GC_TYPE_OBJECT` check on the retired cell and returns // TAG_UNDEFINED rather than faulting) — the silent-wrong-answer shape @@ -515,14 +509,15 @@ pub(crate) unsafe fn invoke_accessor_getter(get_bits: u64, receiver: f64) -> JSV if (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) == 0 { return JSValue::undefined(); } - let prev_h = scope.root_nanbox_f64(super::super::js_implicit_this_set(recv_h.get_nanbox_f64())); let closure = (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; let result_f64 = { let _boundary = crate::object::prototype_chain::UserCodeResolutionBoundary::enter(); - crate::closure::js_closure_call0(closure) + crate::closure::js_closure_call0( + closure, + crate::closure::JsThis::from_f64(recv_h.get_nanbox_f64()), + ) }; - super::super::js_implicit_this_set(prev_h.get_nanbox_f64()); JSValue::from_bits(result_f64.to_bits()) } @@ -552,11 +547,13 @@ pub(crate) unsafe fn invoke_accessor_setter(set_bits: u64, receiver: f64, value: if (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) == 0 { return; } - let prev_h = scope.root_nanbox_f64(super::super::js_implicit_this_set(recv_h.get_nanbox_f64())); let closure = (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; - let _ = crate::closure::js_closure_call1(closure, value_h.get_nanbox_f64()); - super::super::js_implicit_this_set(prev_h.get_nanbox_f64()); + let _ = crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(recv_h.get_nanbox_f64()), + value_h.get_nanbox_f64(), + ); } /// Invoke an accessor owned by a descriptor-marked object before its empty diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs index f913510351..6ee17f3d33 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs @@ -363,7 +363,7 @@ pub(super) unsafe fn instance_constructor_value( // The prototype's OWN `constructor` data field answers the common // shapes directly — `Object.prototype`, a declared `C.prototype`, a // materialized `F.prototype`, a `{ constructor: F }` literal — so take - // it without the general chain walk, whose implicit-`this` juggling, + // it without the general chain walk, whose receiver juggling, // accessor-receiver override and registry probes cost ~3000 // instructions per read. Skipped when an accessor owns the key, which // must run through the walk to fire with the right receiver. diff --git a/crates/perry-runtime/src/object/field_get_set/field_ops.rs b/crates/perry-runtime/src/object/field_get_set/field_ops.rs index 9a2a664eca..8502632e88 100644 --- a/crates/perry-runtime/src/object/field_get_set/field_ops.rs +++ b/crates/perry-runtime/src/object/field_get_set/field_ops.rs @@ -317,7 +317,11 @@ pub extern "C" fn js_object_set_field_by_index( let closure = (acc.set & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !closure.is_null() { - crate::closure::js_closure_call1(closure, value); + crate::closure::js_closure_call1( + closure, + crate::closure::plain_call_receiver(), + value, + ); } } return; diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 28d2e02863..2832ae8929 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -1006,8 +1006,8 @@ fn get_field_by_name_past_data_probe( // which reads the property first) must resolve the reified Promise prototype // method. The generic prototype walk does not surface these builtin // `Promise.prototype` methods for a subclass instance, so hook them here when - // no own key shadows them. The method thunks unwrap the backing cell from the - // implicit-this receiver (see `promise_prototype_receiver`). + // no own key shadows them. The method thunks unwrap the backing cell from + // their `this` receiver (see `promise_prototype_receiver`). if !key.is_null() && ((obj as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(obj as usize) diff --git a/crates/perry-runtime/src/object/field_get_set/has_property/function_presence_tests.rs b/crates/perry-runtime/src/object/field_get_set/has_property/function_presence_tests.rs index a8fed503e9..3e7d47f542 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property/function_presence_tests.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property/function_presence_tests.rs @@ -1,6 +1,9 @@ use super::*; -extern "C" fn noop(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn noop( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -47,7 +50,10 @@ fn function_presence_follows_prototypes_without_reading_values() { #[test] fn generator_presence_initializes_function_parents() { let _lock = crate::gc::global_side_table_test_lock(); - extern "C" fn generator_body(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn generator_body( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } crate::closure::js_register_closure_generator_function(generator_body as *const u8); diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index 94bdcb66f3..07fecb493b 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -125,11 +125,11 @@ pub(crate) fn set_field_by_name_object_tail( as *const crate::closure::ClosureHeader; if !closure.is_null() { let receiver = crate::value::js_nanbox_pointer(obj as i64); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous_this = - this_scope.root_nanbox_f64(super::js_implicit_this_set(receiver)); - crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(previous_this.get_nanbox_f64()); + crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(receiver), + value, + ); } } else { crate::error::throw_immutable_write(0, name); @@ -767,11 +767,11 @@ pub(crate) fn set_field_by_name_object_tail( as *const crate::closure::ClosureHeader; if !closure.is_null() { let receiver = crate::value::js_nanbox_pointer(obj as i64); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous_this = - this_scope.root_nanbox_f64(super::js_implicit_this_set(receiver)); - crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(previous_this.get_nanbox_f64()); + crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(receiver), + value, + ); } } else { crate::error::throw_immutable_write(0, k); diff --git a/crates/perry-runtime/src/object/global_fetch.rs b/crates/perry-runtime/src/object/global_fetch.rs index f741efbfb5..3748116a23 100644 --- a/crates/perry-runtime/src/object/global_fetch.rs +++ b/crates/perry-runtime/src/object/global_fetch.rs @@ -393,6 +393,7 @@ pub(super) fn call_global_blob_new(parts: f64, type_value: f64) -> f64 { pub(super) fn call_global_blob_new(parts: f64, type_value: f64) -> f64 { let f = GLOBAL_FETCH_BLOB_NEW.load(Ordering::Acquire); if !f.is_null() { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: FetchBlobNewFn = unsafe { std::mem::transmute(f) }; return unsafe { func(parts, type_value) }; } @@ -418,6 +419,7 @@ pub(super) fn call_global_file_new( ) -> f64 { let f = GLOBAL_FETCH_FILE_NEW.load(Ordering::Acquire); if !f.is_null() { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: FetchFileNewFn = unsafe { std::mem::transmute(f) }; return unsafe { func(parts, name, type_value, last_modified) }; } @@ -433,6 +435,7 @@ pub(super) fn call_global_headers_new() -> f64 { pub(super) fn call_global_headers_new() -> f64 { let f = GLOBAL_FETCH_HEADERS_NEW.load(Ordering::Acquire); if !f.is_null() { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: FetchHeadersNewFn = unsafe { std::mem::transmute(f) }; return func(); } @@ -448,6 +451,7 @@ pub(super) fn call_global_headers_init_from_value(handle: f64, init: f64) -> f64 pub(super) fn call_global_headers_init_from_value(handle: f64, init: f64) -> f64 { let f = GLOBAL_FETCH_HEADERS_INIT_FROM_VALUE.load(Ordering::Acquire); if !f.is_null() { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: FetchHeadersInitFromValueFn = unsafe { std::mem::transmute(f) }; return unsafe { func(handle, init) }; } @@ -573,6 +577,7 @@ pub(super) fn call_global_request_new(input: f64, init: f64) -> f64 { pub(super) fn call_global_request_new(input: f64, init: f64) -> f64 { let f = GLOBAL_FETCH_REQUEST_NEW.load(Ordering::Acquire); if !f.is_null() { + // NOT-A-JS-BODY: perry-ext-fetch registers its Rust `Request` constructor. let func: FetchRequestNewFn = unsafe { std::mem::transmute(f) }; return unsafe { func(input, init) }; } @@ -659,6 +664,7 @@ pub(super) fn call_global_response_static_error() -> f64 { pub(super) fn call_global_response_static_error() -> f64 { let f = GLOBAL_FETCH_RESPONSE_STATIC_ERROR.load(Ordering::Acquire); if !f.is_null() { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: FetchResponseStaticErrorFn = unsafe { std::mem::transmute(f) }; return func(); } @@ -667,6 +673,7 @@ pub(super) fn call_global_response_static_error() -> f64 { pub(super) extern "C" fn global_this_fetch_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, rest: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index 23b2afa573..d91927fd30 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -17,10 +17,11 @@ pub(crate) fn global_this_rest_array_values(rest: f64) -> Vec { pub(crate) extern "C" fn function_prototype_call_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, this_arg: f64, rest: f64, ) -> f64 { - let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let target = f64::from_bits(this.bits()); // The generic value-call bridge treats a proxy invocation as a bare // call. Preserve the explicit receiver of Function.prototype.call. if crate::proxy::js_proxy_is_proxy(target) == 1 { @@ -37,21 +38,20 @@ pub(crate) extern "C" fn function_prototype_call_thunk( }; let this_arg = crate::closure::coerce_call_this(target, this_arg); // Concise/object-literal methods read `this` from a baked capture slot, not - // IMPLICIT_THIS; rebind so the explicit `.call(thisArg)` receiver is honored. + // the `this` argument; rebind so the explicit `.call(thisArg)` receiver is honored. let target = crate::closure::rebind_explicit_this(target, this_arg); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = - prev_this_scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits()))); - let result = unsafe { crate::closure::js_native_call_value(target, args_ptr, args_len) }; - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - result + unsafe { + crate::closure::native_call_value_this( + target, + crate::closure::JsThis::from_f64(this_arg), + args_ptr, + args_len, + ) + } } /// `Function.prototype.bind` as a real callable thunk. Reads the target -/// function from `IMPLICIT_THIS` (set by `.call`/`.apply`/`Reflect.apply`), +/// function from its `this` argument (set by `.call`/`.apply`/`Reflect.apply`), /// flattens `(thisArg, ...boundArgs)` into one argument list, and delegates to /// `js_function_bind` (which builds the BOUND_FUNCTION closure). /// @@ -63,10 +63,11 @@ pub(crate) extern "C" fn function_prototype_call_thunk( /// hit exactly this: `Reflect.apply(bind, call, [fn])` yielded `undefined`. pub(crate) extern "C" fn function_prototype_bind_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, this_arg: f64, rest: f64, ) -> f64 { - let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let target = f64::from_bits(this.bits()); let mut args: Vec = Vec::with_capacity(1); args.push(this_arg); args.extend(global_this_rest_array_values(rest)); @@ -75,6 +76,7 @@ pub(crate) extern "C" fn function_prototype_bind_thunk( pub(crate) extern "C" fn global_this_set_timeout_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, callback: f64, delay: f64, rest: f64, @@ -97,6 +99,7 @@ pub(crate) extern "C" fn global_this_set_timeout_thunk( pub(crate) extern "C" fn global_this_clear_timeout_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, arg: f64, ) -> f64 { crate::timer::js_clear_timeout_value(arg); @@ -105,6 +108,7 @@ pub(crate) extern "C" fn global_this_clear_timeout_thunk( pub(crate) extern "C" fn global_this_set_interval_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, callback: f64, delay: f64, rest: f64, @@ -127,6 +131,7 @@ pub(crate) extern "C" fn global_this_set_interval_thunk( pub(crate) extern "C" fn global_this_clear_interval_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, arg: f64, ) -> f64 { crate::timer::js_clear_interval_value(arg); @@ -135,6 +140,7 @@ pub(crate) extern "C" fn global_this_clear_interval_thunk( pub(crate) extern "C" fn global_this_set_immediate_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, callback: f64, rest: f64, ) -> f64 { @@ -151,6 +157,7 @@ pub(crate) extern "C" fn global_this_set_immediate_thunk( pub(crate) extern "C" fn global_this_clear_immediate_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, arg: f64, ) -> f64 { crate::timer::js_clear_immediate_value(arg); @@ -159,6 +166,7 @@ pub(crate) extern "C" fn global_this_clear_immediate_thunk( pub(crate) extern "C" fn global_this_queue_microtask_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, callback: f64, ) -> f64 { let callback = unsafe { crate::timer::js_timer_validate_callback(callback, 3) }; @@ -168,7 +176,7 @@ pub(crate) extern "C" fn global_this_queue_microtask_thunk( /// Thunk for `Object.prototype.toString` exposed as a callable closure /// value. Mirrors `Object.prototype.toString.call(x)` — returns the -/// `"[object Tag]"` string for the receiver in IMPLICIT_THIS. +/// `"[object Tag]"` string for the `this` receiver. /// /// Tag detection uses the same coarse NaN-box / GC-type discrimination /// the rest of the runtime relies on: arrays → `"[object Array]"`, @@ -182,6 +190,7 @@ pub(crate) extern "C" fn global_this_queue_microtask_thunk( /// `undefined`, so the `.call` access threw before the IIFE body ran. pub(crate) extern "C" fn object_prototype_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { // Delegate to the canonical `js_object_to_string` so this callable form // (`const f = Object.prototype.toString; f.call(x)`) shares the full brand @@ -189,15 +198,16 @@ pub(crate) extern "C" fn object_prototype_to_string_thunk( // buffers/…). Previously this thunk duplicated a coarse discrimination that // mis-tagged typed arrays as `[object Number]` and everything beyond // Array/Error/Date as `[object Object]`. - let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_bits = this.bits(); unsafe { crate::object::js_object_to_string(f64::from_bits(this_bits)) } } pub(crate) extern "C" fn object_prototype_is_prototype_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); // Spec 20.1.3.3 step order: if V is not an Object, return false FIRST — // `Object.prototype.isPrototypeOf.call(undefined, 1)` is `false`, not a // TypeError. Symbols are POINTER_TAG'd in Perry but are primitives. @@ -248,8 +258,9 @@ pub(crate) fn is_native_error_subclass_constructor(name: &str) -> bool { pub(crate) extern "C" fn date_prototype_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); // ECMA-262 21.4.4.41 Date.prototype.toString does `thisTimeValue(this)`, // which throws a TypeError when the receiver has no [[DateValue]] slot // (`Date.prototype.toString.call(0)`, `.call({})`, `Date.prototype.toString()` @@ -263,17 +274,19 @@ pub(crate) extern "C" fn date_prototype_to_string_thunk( pub(crate) extern "C" fn object_prototype_has_own_property_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, key: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); super::super::object_ops::js_object_has_own(this_value, key) } pub(crate) extern "C" fn object_prototype_property_is_enumerable_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, key: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); super::super::js_object_property_is_enumerable(this_value, key) } @@ -282,42 +295,47 @@ pub(crate) extern "C" fn object_prototype_property_is_enumerable_thunk( // not just the direct `o.__defineGetter__(...)` native-dispatch path. pub(crate) extern "C" fn object_prototype_define_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, key: f64, getter: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); super::super::js_object_define_getter(this_value, key, getter) } pub(crate) extern "C" fn object_prototype_define_setter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, key: f64, setter: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); super::super::js_object_define_setter(this_value, key, setter) } pub(crate) extern "C" fn object_prototype_lookup_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, key: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); super::super::js_object_lookup_getter(this_value, key) } pub(crate) extern "C" fn object_prototype_lookup_setter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, key: f64, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); super::super::js_object_lookup_setter(this_value, key) } pub(crate) extern "C" fn error_prototype_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); let this_jsv = crate::value::JSValue::from_bits(this_value.to_bits()); // ECMA-262 20.5.3.4 step 2: `If Type(O) is not Object, throw a TypeError`. // A Symbol is a POINTER_TAG value (registered-symbol id), so it slips past @@ -381,15 +399,17 @@ unsafe fn string_header_to_owned(ptr: *const crate::StringHeader) -> String { pub(crate) extern "C" fn object_prototype_value_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); unsafe { super::super::js_object_default_value_of(this_value) } } pub(crate) extern "C" fn object_prototype_to_locale_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this_value = f64::from_bits(this.bits()); unsafe { super::super::js_object_prototype_to_locale_string(this_value) } } @@ -570,11 +590,12 @@ pub(crate) unsafe fn function_apply_proxy(target: f64, receiver: f64, args_array pub(crate) extern "C" fn function_prototype_apply_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, this_arg: f64, args_array: f64, ) -> f64 { unsafe { - let target = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let target = f64::from_bits(this.bits()); if crate::proxy::js_proxy_is_proxy(target) == 1 { return function_apply_proxy(target, this_arg, args_array); } @@ -583,20 +604,17 @@ pub(crate) extern "C" fn function_prototype_apply_thunk( // Rebind a concise/object-literal method's baked `this` slot to the // explicit `.apply(thisArg)` receiver (no-op for arrows / plain fns). let target = crate::closure::rebind_explicit_this(target, this_arg); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = - prev_this_scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits()))); - let result = crate::closure::js_native_call_value(target, args.as_ptr(), args.len()); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - result + crate::closure::native_call_value_this( + target, + crate::closure::JsThis::from_f64(this_arg), + args.as_ptr(), + args.len(), + ) } } -/// #4101: `Function.prototype.toString` as a real callable thunk. Reads the -/// receiver from `IMPLICIT_THIS` (set by `.call`/`.apply`'s runtime arm), then: +/// #4101: `Function.prototype.toString` as a real callable thunk. Takes the +/// receiver as its `this` argument (set by `.call`/`.apply`'s runtime arm), then: /// • throws a `TypeError` when `this` is not callable (the spec brand check /// deferred from #4098 — `Function.prototype.toString.call({})`), and /// • otherwise returns the function's reconstructed source text. @@ -605,8 +623,9 @@ pub(crate) extern "C" fn function_prototype_apply_thunk( /// `Object.prototype.toString` (which keeps its own real thunk). pub(crate) extern "C" fn function_prototype_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_bits = this.bits(); let this_jsv = JSValue::from_bits(this_bits); let raw = if this_jsv.is_pointer() { (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize @@ -644,7 +663,7 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( } /// Thunk for `Array.prototype.slice` exposed as a real callable closure -/// value. Reads the array receiver from `IMPLICIT_THIS` (set by +/// value. Takes the array receiver as its `this` argument (set by /// `Function.prototype.call`/`.apply`'s runtime arm in /// `js_native_call_method`) and forwards ordinary array-like objects to the /// generic engine or real arrays to the shared dense slice-value helper. @@ -662,17 +681,18 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( /// `Cannot read properties of undefined (reading 'call')` at module init. pub(crate) extern "C" fn array_prototype_slice_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, start_val: f64, end_val: f64, ) -> f64 { use crate::value::JSValue; - let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_bits = this.bits(); let this_jsv = JSValue::from_bits(this_bits); let arr_ptr = if this_jsv.is_pointer() { this_jsv.as_pointer::() } else { // Tolerate raw-i64-encoded array receivers (some module-init - // call sites stash array pointers in IMPLICIT_THIS without + // call sites pass array pointers as `this` without // NaN-boxing). The clean_arr_ptr check inside js_array_slice // re-validates. let raw = this_bits as *const crate::array::ArrayHeader; @@ -709,7 +729,7 @@ pub(crate) extern "C" fn array_prototype_slice_thunk( /// Real callable thunks for the generic `Array.prototype` mutators /// (`pop`/`shift`/`reverse` — no positional args; `push`/`unshift`/`splice` — -/// variadic). Each reads the call-site receiver from `IMPLICIT_THIS` (set by +/// variadic). Each takes the call-site receiver as its `this` argument (set by /// the own-field dispatch and `Function.prototype.call`/`.apply`) and forwards /// to the shared engine, which mutates a real array via the dense helpers or a /// plain array-like object via live `Get`/`Set`/`Delete`. Without these, the @@ -738,69 +758,78 @@ pub(crate) extern "C" fn array_prototype_slice_thunk( /// iterate the receiver they are given. pub(crate) extern "C" fn array_prototype_values_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _a: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::array_values_iter(this) } pub(crate) extern "C" fn array_prototype_flat_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, depth: f64, ) -> f64 { - crate::array::js_arraylike_flat(crate::object::js_implicit_this_get(), depth) + crate::array::js_arraylike_flat(this.as_f64(), depth) } pub(crate) extern "C" fn array_prototype_pop_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _a: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::array_proto_mutator(this, "pop", std::ptr::null(), 0) } pub(crate) extern "C" fn array_prototype_shift_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _a: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::array_proto_mutator(this, "shift", std::ptr::null(), 0) } pub(crate) extern "C" fn array_prototype_reverse_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _a: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::array_proto_mutator(this, "reverse", std::ptr::null(), 0) } pub(crate) extern "C" fn array_prototype_push_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let args = global_this_rest_array_values(rest); crate::array::array_proto_mutator(this, "push", args.as_ptr(), args.len()) } pub(crate) extern "C" fn array_prototype_unshift_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let args = global_this_rest_array_values(rest); crate::array::array_proto_mutator(this, "unshift", args.as_ptr(), args.len()) } pub(crate) extern "C" fn array_prototype_splice_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let args = global_this_rest_array_values(rest); crate::array::array_proto_mutator(this, "splice", args.as_ptr(), args.len()) } pub(crate) extern "C" fn array_prototype_sort_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, comparator: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::js_arraylike_sort(this, comparator) } /// `fill` / `copyWithin` were noop-backed until #6908, so a Proxy receiver @@ -814,9 +843,10 @@ pub(crate) extern "C" fn array_prototype_sort_thunk( /// the `end` params the spec resolves `undefined` to `len`). pub(crate) extern "C" fn array_prototype_fill_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let args = global_this_rest_array_values(rest); let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); let value = args.first().copied().unwrap_or(undefined); @@ -832,9 +862,10 @@ pub(crate) extern "C" fn array_prototype_fill_thunk( } pub(crate) extern "C" fn array_prototype_copy_within_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let args = global_this_rest_array_values(rest); let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); let target = args.first().copied().unwrap_or(undefined); @@ -847,7 +878,7 @@ pub(crate) extern "C" fn array_prototype_copy_within_thunk( } /// Real thunks for the generic `Array.prototype` iteration / search methods, -/// each routing the call-site receiver (IMPLICIT_THIS) through the +/// each routing the call-site receiver (`this`) through the /// `js_arraylike_*` engine. These replace the previous noop thunks so a /// reflective resolution — `Array.prototype.map.call(x, …)` through a stored /// reference, or a method reached through an object whose [[Prototype]] chain @@ -858,8 +889,12 @@ pub(crate) extern "C" fn array_prototype_copy_within_thunk( macro_rules! array_proto_arraylike_cb_thunk { ($name:ident, $engine:path) => { #[allow(non_snake_case)] // thunk name mirrors JS API surface - pub(crate) extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); + pub(crate) extern "C" fn $name( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + rest: f64, + ) -> f64 { + let this = this.as_f64(); let args = global_this_rest_array_values(rest); let a = |i: usize| { args.get(i) @@ -895,8 +930,12 @@ array_proto_arraylike_cb_thunk!( macro_rules! array_proto_arraylike_optarg_thunk { ($name:ident, $engine:path) => { #[allow(non_snake_case)] // thunk name mirrors JS API surface - pub(crate) extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); + pub(crate) extern "C" fn $name( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + rest: f64, + ) -> f64 { + let this = this.as_f64(); let args = global_this_rest_array_values(rest); let a = |i: usize| { args.get(i) @@ -921,8 +960,12 @@ fn reduce_right_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { macro_rules! array_proto_arraylike_search_thunk { ($name:ident, $engine:path) => { #[allow(non_snake_case)] // thunk name mirrors JS API surface - pub(crate) extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); + pub(crate) extern "C" fn $name( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + rest: f64, + ) -> f64 { + let this = this.as_f64(); let args = global_this_rest_array_values(rest); let a = |i: usize| { args.get(i) @@ -948,16 +991,18 @@ array_proto_arraylike_search_thunk!( pub(crate) extern "C" fn array_proto_at_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, idx: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::js_arraylike_at(this, idx) } pub(crate) extern "C" fn array_proto_join_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, sep: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::array::js_arraylike_join(this, sep) } @@ -968,16 +1013,18 @@ pub(crate) extern "C" fn array_proto_join_thunk( /// receiver semantics; rewriting it to `x.toString()` loses the Array method. pub(crate) extern "C" fn array_prototype_to_string_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); crate::value::array_prototype_to_string(this) } pub(crate) extern "C" fn array_prototype_concat_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let args = global_this_rest_array_values(rest); crate::array::js_arraylike_concat(this, args.as_ptr(), args.len() as i32) } diff --git a/crates/perry-runtime/src/object/global_this/array_error/apply_args_tests.rs b/crates/perry-runtime/src/object/global_this/array_error/apply_args_tests.rs index aabf57db76..50cc7e2211 100644 --- a/crates/perry-runtime/src/object/global_this/array_error/apply_args_tests.rs +++ b/crates/perry-runtime/src/object/global_this/array_error/apply_args_tests.rs @@ -1,4 +1,9 @@ -extern "C" fn add(_closure: *const crate::closure::ClosureHeader, a: f64, b: f64) -> f64 { +extern "C" fn add( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + a: f64, + b: f64, +) -> f64 { a + b } diff --git a/crates/perry-runtime/src/object/global_this/bigint_promise.rs b/crates/perry-runtime/src/object/global_this/bigint_promise.rs index 43441930fe..560cf22221 100644 --- a/crates/perry-runtime/src/object/global_this/bigint_promise.rs +++ b/crates/perry-runtime/src/object/global_this/bigint_promise.rs @@ -10,6 +10,7 @@ fn nanbox_array_or_undef(arr: *mut crate::array::ArrayHeader) -> f64 { pub(crate) extern "C" fn object_keys_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { nanbox_array_or_undef(super::super::js_object_keys_value(value)) @@ -17,6 +18,7 @@ pub(crate) extern "C" fn object_keys_thunk( pub(crate) extern "C" fn object_values_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { nanbox_array_or_undef(super::super::js_object_values_value(value)) @@ -24,6 +26,7 @@ pub(crate) extern "C" fn object_values_thunk( pub(crate) extern "C" fn object_entries_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { nanbox_array_or_undef(super::super::js_object_entries_value(value)) @@ -31,6 +34,7 @@ pub(crate) extern "C" fn object_entries_thunk( pub(crate) extern "C" fn object_freeze_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_freeze(value) @@ -38,6 +42,7 @@ pub(crate) extern "C" fn object_freeze_thunk( pub(crate) extern "C" fn object_create_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, props: f64, ) -> f64 { @@ -50,6 +55,7 @@ pub(crate) extern "C" fn object_create_thunk( pub(crate) extern "C" fn object_seal_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_seal(value) @@ -57,6 +63,7 @@ pub(crate) extern "C" fn object_seal_thunk( pub(crate) extern "C" fn object_is_sealed_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_is_sealed(value) @@ -64,6 +71,7 @@ pub(crate) extern "C" fn object_is_sealed_thunk( pub(crate) extern "C" fn object_is_frozen_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_is_frozen(value) @@ -71,6 +79,7 @@ pub(crate) extern "C" fn object_is_frozen_thunk( pub(crate) extern "C" fn object_is_extensible_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_is_extensible(value) @@ -78,6 +87,7 @@ pub(crate) extern "C" fn object_is_extensible_thunk( pub(crate) extern "C" fn object_prevent_extensions_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_prevent_extensions(value) @@ -85,6 +95,7 @@ pub(crate) extern "C" fn object_prevent_extensions_thunk( pub(crate) extern "C" fn object_is_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a: f64, b: f64, ) -> f64 { @@ -93,6 +104,7 @@ pub(crate) extern "C" fn object_is_thunk( pub(crate) extern "C" fn object_set_prototype_of_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, proto: f64, ) -> f64 { @@ -101,6 +113,7 @@ pub(crate) extern "C" fn object_set_prototype_of_thunk( pub(crate) extern "C" fn object_get_own_property_symbols_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let arr = unsafe { crate::symbol::js_object_get_own_property_symbols(value) }; @@ -109,6 +122,7 @@ pub(crate) extern "C" fn object_get_own_property_symbols_thunk( pub(crate) extern "C" fn object_get_own_property_descriptors_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_get_own_property_descriptors(value) @@ -116,6 +130,7 @@ pub(crate) extern "C" fn object_get_own_property_descriptors_thunk( pub(crate) extern "C" fn object_define_properties_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, descriptors: f64, ) -> f64 { @@ -124,6 +139,7 @@ pub(crate) extern "C" fn object_define_properties_thunk( pub(crate) extern "C" fn object_group_by_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, items: f64, callback: f64, ) -> f64 { @@ -132,6 +148,7 @@ pub(crate) extern "C" fn object_group_by_thunk( pub(crate) extern "C" fn object_get_prototype_of_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_get_prototype_of(value) @@ -139,6 +156,7 @@ pub(crate) extern "C" fn object_get_prototype_of_thunk( pub(crate) extern "C" fn object_get_own_property_names_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_get_own_property_names(value) @@ -146,6 +164,7 @@ pub(crate) extern "C" fn object_get_own_property_names_thunk( pub(crate) extern "C" fn object_get_own_property_descriptor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, key: f64, ) -> f64 { @@ -154,6 +173,7 @@ pub(crate) extern "C" fn object_get_own_property_descriptor_thunk( pub(crate) extern "C" fn object_define_property_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, key: f64, descriptor: f64, @@ -163,6 +183,7 @@ pub(crate) extern "C" fn object_define_property_thunk( pub(crate) extern "C" fn object_from_entries_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { super::super::js_object_from_entries(value) @@ -170,6 +191,7 @@ pub(crate) extern "C" fn object_from_entries_thunk( pub(crate) extern "C" fn object_assign_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, rest: f64, ) -> f64 { @@ -187,6 +209,7 @@ pub(crate) extern "C" fn object_assign_thunk( /// `Object.prototype.hasOwnProperty.call(obj, key)`. pub(crate) extern "C" fn object_hasown_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, key: f64, ) -> f64 { @@ -195,6 +218,7 @@ pub(crate) extern "C" fn object_hasown_thunk( pub(crate) extern "C" fn array_is_array_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::array::js_array_is_array(value) @@ -202,34 +226,35 @@ pub(crate) extern "C" fn array_is_array_thunk( pub(crate) extern "C" fn array_from_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { // Reflective `Array.from.call(C, items)` / `Array.from.apply(C, [items])` - // binds `C` as the implicit `this`. Read it FIRST (before any nested call - // can overwrite it) and run the spec algorithm — when `C IsConstructor`, + // passes `C` as `this`. Run the spec algorithm — when `C IsConstructor`, // the result is built via `Construct(C)`. A plain reflective call (no // explicit receiver) leaves `this` as undefined / a non-constructor, so // the default `%Array%` path is taken. - let c = crate::object::js_implicit_this_get(); + let c = this.as_f64(); let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); crate::array::array_from_full(c, value, undefined, undefined) } pub(crate) extern "C" fn array_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - // Reflective `Array.of.call(C, ...items)` binds `C` as the implicit `this`. - // Read it FIRST (before any nested call can overwrite it); when `C + // Reflective `Array.of.call(C, ...items)` passes `C` as `this`; when `C // IsConstructor` the result is built via `Construct(C, «len»)`, otherwise the // default `%Array%` path is taken. See `array_of_full` (ECMA-262 §23.1.2.3). - let c = crate::object::js_implicit_this_get(); + let c = this.as_f64(); let vals = global_this_rest_array_values(rest); crate::array::array_of_full(c, &vals) } pub(crate) extern "C" fn number_is_nan_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::builtins::js_number_is_nan(value) @@ -237,6 +262,7 @@ pub(crate) extern "C" fn number_is_nan_thunk( pub(crate) extern "C" fn number_is_finite_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::builtins::js_number_is_finite(value) @@ -244,6 +270,7 @@ pub(crate) extern "C" fn number_is_finite_thunk( pub(crate) extern "C" fn number_is_integer_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::builtins::js_number_is_integer(value) @@ -340,6 +367,7 @@ pub extern "C" fn js_bigint_as_uint_n_call(bits: f64, value: f64) -> f64 { pub(crate) extern "C" fn bigint_as_int_n_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, bits: f64, value: f64, ) -> f64 { @@ -348,6 +376,7 @@ pub(crate) extern "C" fn bigint_as_int_n_thunk( pub(crate) extern "C" fn bigint_as_uint_n_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, bits: f64, value: f64, ) -> f64 { @@ -356,6 +385,7 @@ pub(crate) extern "C" fn bigint_as_uint_n_thunk( pub(crate) extern "C" fn json_parse_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, text: f64, reviver: f64, ) -> f64 { @@ -378,6 +408,7 @@ pub(crate) extern "C" fn json_parse_thunk( pub(crate) extern "C" fn json_stringify_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, replacer: f64, space: f64, @@ -387,6 +418,7 @@ pub(crate) extern "C" fn json_stringify_thunk( pub(crate) extern "C" fn json_raw_json_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, text: f64, ) -> f64 { unsafe { crate::json::js_json_raw_json(text) } @@ -394,6 +426,7 @@ pub(crate) extern "C" fn json_raw_json_thunk( pub(crate) extern "C" fn json_is_raw_json_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { unsafe { crate::json::js_json_is_raw_json(value) } @@ -401,6 +434,7 @@ pub(crate) extern "C" fn json_is_raw_json_thunk( pub(crate) extern "C" fn reflect_apply_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, this_arg: f64, args: f64, @@ -419,6 +453,7 @@ pub(crate) extern "C" fn reflect_apply_thunk( /// `undefined`, which it already resolves to `target` per spec. pub(crate) extern "C" fn reflect_construct_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, args_like: f64, new_target: f64, @@ -434,6 +469,7 @@ pub(crate) extern "C" fn reflect_construct_thunk( /// class-static-init time and threw on destructuring the `undefined`). pub(crate) extern "C" fn reflect_own_keys_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, ) -> f64 { crate::proxy::js_reflect_own_keys(target) @@ -441,6 +477,7 @@ pub(crate) extern "C" fn reflect_own_keys_thunk( pub(crate) extern "C" fn reflect_get_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, receiver: f64, @@ -450,6 +487,7 @@ pub(crate) extern "C" fn reflect_get_thunk( pub(crate) extern "C" fn reflect_set_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, value: f64, @@ -460,6 +498,7 @@ pub(crate) extern "C" fn reflect_set_thunk( pub(crate) extern "C" fn reflect_has_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, ) -> f64 { @@ -468,6 +507,7 @@ pub(crate) extern "C" fn reflect_has_thunk( pub(crate) extern "C" fn reflect_delete_property_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, ) -> f64 { @@ -476,6 +516,7 @@ pub(crate) extern "C" fn reflect_delete_property_thunk( pub(crate) extern "C" fn reflect_define_property_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, descriptor: f64, @@ -485,6 +526,7 @@ pub(crate) extern "C" fn reflect_define_property_thunk( pub(crate) extern "C" fn reflect_get_own_property_descriptor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, key: f64, ) -> f64 { @@ -493,6 +535,7 @@ pub(crate) extern "C" fn reflect_get_own_property_descriptor_thunk( pub(crate) extern "C" fn reflect_get_prototype_of_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, ) -> f64 { crate::proxy::js_reflect_get_prototype_of(target) @@ -500,6 +543,7 @@ pub(crate) extern "C" fn reflect_get_prototype_of_thunk( pub(crate) extern "C" fn reflect_set_prototype_of_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, proto: f64, ) -> f64 { @@ -508,6 +552,7 @@ pub(crate) extern "C" fn reflect_set_prototype_of_thunk( pub(crate) extern "C" fn reflect_is_extensible_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, ) -> f64 { crate::proxy::js_reflect_is_extensible(target) @@ -515,6 +560,7 @@ pub(crate) extern "C" fn reflect_is_extensible_thunk( pub(crate) extern "C" fn reflect_prevent_extensions_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, ) -> f64 { crate::proxy::js_reflect_prevent_extensions(target) @@ -522,6 +568,7 @@ pub(crate) extern "C" fn reflect_prevent_extensions_thunk( pub(crate) extern "C" fn symbol_for_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, key: f64, ) -> f64 { unsafe { crate::symbol::js_symbol_for(key) } @@ -529,6 +576,7 @@ pub(crate) extern "C" fn symbol_for_thunk( pub(crate) extern "C" fn symbol_key_for_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, symbol: f64, ) -> f64 { unsafe { crate::symbol::js_symbol_key_for(symbol) } @@ -536,6 +584,7 @@ pub(crate) extern "C" fn symbol_key_for_thunk( pub(crate) extern "C" fn number_is_safe_integer_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::builtins::js_number_is_safe_integer(value) @@ -546,6 +595,7 @@ pub(crate) extern "C" fn number_is_safe_integer_thunk( // the array-like the array-form runtime helpers expect. pub(crate) extern "C" fn string_from_char_code_static( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let s = crate::string::js_string_from_char_code_array(rest); @@ -554,6 +604,7 @@ pub(crate) extern "C" fn string_from_char_code_static( pub(crate) extern "C" fn string_from_code_point_static( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let s = crate::string::js_string_from_code_point_array(rest); @@ -571,64 +622,74 @@ pub(crate) extern "C" fn string_from_code_point_static( // thunks intentionally use the native Promise machinery regardless of `this`. extern "C" fn promise_resolve_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_resolve_spec(this_ctor, value) } extern "C" fn promise_reject_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, reason: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_reject_spec(this_ctor, reason) } extern "C" fn promise_all_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, iterable: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_all_spec(this_ctor, iterable) } extern "C" fn promise_race_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, iterable: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_race_spec(this_ctor, iterable) } extern "C" fn promise_all_settled_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, iterable: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_all_settled_spec(this_ctor, iterable) } extern "C" fn promise_any_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, iterable: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_any_spec(this_ctor, iterable) } -extern "C" fn promise_with_resolvers_static(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); +extern "C" fn promise_with_resolvers_static( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this_ctor = this.as_f64(); crate::promise::js_promise_with_resolvers_spec(this_ctor) } // `Promise.try(fn, ...args)`: call-arity 1 (callback) + rest (forwarded args). extern "C" fn promise_try_static( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, callback: f64, rest: f64, ) -> f64 { - let this_ctor = crate::object::js_implicit_this_get(); + let this_ctor = this.as_f64(); crate::promise::js_promise_try_spec(this_ctor, callback, rest) } @@ -638,6 +699,7 @@ extern "C" fn promise_try_static( // passed straight through as the substitutions array-like. pub(crate) extern "C" fn string_raw_static( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, call_site: f64, rest: f64, ) -> f64 { @@ -647,21 +709,24 @@ pub(crate) extern "C" fn string_raw_static( pub(crate) extern "C" fn number_parse_float_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - global_this_parse_float_thunk(closure, value) + global_this_parse_float_thunk(closure, this, value) } pub(crate) extern "C" fn number_parse_int_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, radix: f64, ) -> f64 { - global_this_parse_int_thunk(closure, value, radix) + global_this_parse_int_thunk(closure, this, value, radix) } pub(crate) extern "C" fn typed_array_from_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, source: f64, map_fn: f64, this_arg: f64, @@ -672,9 +737,9 @@ pub(crate) extern "C" fn typed_array_from_thunk( // (not a constructor), so this must fire even though a source is supplied // (test262 `from/invoked-as-func`). A concrete TA `this` (kind known) is a // constructor by definition. - let kind_opt = typed_array_constructor_this_kind(); + let kind_opt = typed_array_constructor_this_kind(this); if kind_opt.is_none() { - require_typed_array_from_of_constructor(); + require_typed_array_from_of_constructor(this); } // Spec order: validate the map callback BEFORE the source is read. let mapped = map_fn.to_bits() != crate::value::TAG_UNDEFINED; @@ -693,16 +758,19 @@ pub(crate) extern "C" fn typed_array_from_thunk( // coercion at element k means the map callback never ran for k+1 // (test262 from/set-value-abrupt-completion). let this_scope = crate::gc::RuntimeHandleScope::new(); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + // #9445: `thisArg` is rooted ONCE here, not once per callback, and re-read + // at each call because every callback can move it. + let this_arg_h = this_scope.root_nanbox_f64(this_arg); let map_at = |k: usize, v: f64| -> f64 { if map_closure.is_null() { return v; } - crate::object::js_implicit_this_set(this_arg); - let r = crate::closure::js_closure_call2(map_closure, v, k as f64); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - r + crate::closure::js_closure_call2( + map_closure, + crate::closure::JsThis::from_f64(this_arg_h.get_nanbox_f64()), + v, + k as f64, + ) }; if let Some(kind) = kind_opt { let out = crate::typedarray::typed_array_alloc(kind, raw.len() as u32); @@ -718,7 +786,7 @@ pub(crate) extern "C" fn typed_array_from_thunk( let len_arg = [f64::from_bits( crate::value::JSValue::number(len as f64).bits(), )]; - let ctor = crate::object::js_implicit_this_get(); + let ctor = this.as_f64(); let target = unsafe { super::super::js_new_function_construct(ctor, len_arg.as_ptr(), 1) }; let addr = crate::typedarray_props::typed_array_addr_from_value(target).unwrap_or_else(|| { super::super::object_ops::throw_object_type_error( @@ -742,8 +810,8 @@ pub(crate) extern "C" fn typed_array_from_thunk( /// `%TypedArray%.from`/`.of` step "If IsConstructor(`this`) is false, throw a /// TypeError". Only called when the `this` value is not a concrete typed-array /// constructor (kind unknown); a user constructor passes, anything else throws. -fn require_typed_array_from_of_constructor() { - let this_ctor = crate::object::js_implicit_this_get(); +fn require_typed_array_from_of_constructor(this: crate::closure::JsThis) { + let this_ctor = this.as_f64(); if !value_is_constructor(this_ctor) { super::super::object_ops::throw_object_type_error( b"TypedArray.from/of called with a `this` that is not a constructor", @@ -785,6 +853,7 @@ fn value_is_constructor(value: f64) -> bool { /// that returns an arbitrary (sufficiently long) typed array is used verbatim /// (test262 `from/of` `custom-ctor*`). fn typed_array_create_from_values( + this: crate::closure::JsThis, kind_opt: Option, arr: *mut crate::array::ArrayHeader, ) -> f64 { @@ -792,7 +861,7 @@ fn typed_array_create_from_values( let ta = crate::typedarray::js_typed_array_new_from_array(kind as i32, arr); return crate::value::js_nanbox_pointer(ta as i64); } - let ctor = crate::object::js_implicit_this_get(); + let ctor = this.as_f64(); let len = crate::array::js_array_length(arr) as usize; let len_arg = [f64::from_bits( crate::value::JSValue::number(len as f64).bits(), @@ -824,11 +893,12 @@ fn typed_array_create_from_values( pub(crate) extern "C" fn typed_array_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let kind_opt = typed_array_constructor_this_kind(); + let kind_opt = typed_array_constructor_this_kind(this); if kind_opt.is_none() { - require_typed_array_from_of_constructor(); + require_typed_array_from_of_constructor(this); } let vals = global_this_rest_array_values(rest); let len = vals.len() as u32; @@ -839,7 +909,7 @@ pub(crate) extern "C" fn typed_array_of_thunk( crate::array::js_array_set_f64(arr, i as u32, v); } } - typed_array_create_from_values(kind_opt, arr) + typed_array_create_from_values(this, kind_opt, arr) } pub(crate) fn promise_static_function_spec(name: &str) -> Option<(*const u8, u32, u32, bool)> { diff --git a/crates/perry-runtime/src/object/global_this/builtin_thunks.rs b/crates/perry-runtime/src/object/global_this/builtin_thunks.rs index 0f5cfc3eac..37e4448def 100644 --- a/crates/perry-runtime/src/object/global_this/builtin_thunks.rs +++ b/crates/perry-runtime/src/object/global_this/builtin_thunks.rs @@ -2,6 +2,7 @@ use super::*; pub(crate) extern "C" fn global_this_array_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let rest_value = crate::value::JSValue::from_bits(rest.to_bits()); @@ -29,6 +30,7 @@ pub(crate) extern "C" fn global_this_array_thunk( pub(crate) extern "C" fn global_this_string_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let string_ptr = crate::builtins::js_string_coerce(value); @@ -37,6 +39,7 @@ pub(crate) extern "C" fn global_this_string_thunk( pub(crate) extern "C" fn global_this_object_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::object::js_object_coerce(value) @@ -44,6 +47,7 @@ pub(crate) extern "C" fn global_this_object_thunk( pub(crate) extern "C" fn global_this_structured_clone_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, _options: f64, ) -> f64 { @@ -52,6 +56,7 @@ pub(crate) extern "C" fn global_this_structured_clone_thunk( pub(crate) extern "C" fn global_this_atob_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let decoded = crate::string::js_atob(value); @@ -60,6 +65,7 @@ pub(crate) extern "C" fn global_this_atob_thunk( pub(crate) extern "C" fn global_this_btoa_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let encoded = crate::string::js_btoa(value); @@ -68,12 +74,16 @@ pub(crate) extern "C" fn global_this_btoa_thunk( pub(crate) extern "C" fn math_f16round_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::math::js_math_f16round(value) } -pub(crate) extern "C" fn math_random_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { +pub(crate) extern "C" fn math_random_thunk( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::math::js_math_random() } @@ -98,6 +108,7 @@ macro_rules! math_unary_thunk { ($name:ident, $body:expr) => { pub(crate) extern "C" fn $name( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let x = math_number_arg(value); @@ -134,6 +145,7 @@ math_unary_thunk!(math_trunc_thunk, |x: f64| x.trunc()); pub(crate) extern "C" fn math_round_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let x = math_number_arg(value); @@ -164,6 +176,7 @@ pub(crate) fn js_math_round_value(x: f64) -> f64 { pub(crate) extern "C" fn math_sign_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::math::js_math_sign(value) @@ -171,6 +184,7 @@ pub(crate) extern "C" fn math_sign_thunk( pub(crate) extern "C" fn math_clz32_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { math_to_uint32(value).leading_zeros() as f64 @@ -178,6 +192,7 @@ pub(crate) extern "C" fn math_clz32_thunk( pub(crate) extern "C" fn math_atan2_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, y: f64, x: f64, ) -> f64 { @@ -186,6 +201,7 @@ pub(crate) extern "C" fn math_atan2_thunk( pub(crate) extern "C" fn math_imul_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a: f64, b: f64, ) -> f64 { @@ -194,6 +210,7 @@ pub(crate) extern "C" fn math_imul_thunk( pub(crate) extern "C" fn math_pow_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, base: f64, exp: f64, ) -> f64 { @@ -202,6 +219,7 @@ pub(crate) extern "C" fn math_pow_thunk( pub(crate) extern "C" fn math_min_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let values = global_this_rest_array_values(rest); @@ -227,6 +245,7 @@ pub(crate) extern "C" fn math_min_thunk( pub(crate) extern "C" fn math_max_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let values = global_this_rest_array_values(rest); @@ -252,6 +271,7 @@ pub(crate) extern "C" fn math_max_thunk( pub(crate) extern "C" fn math_hypot_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let mut result = 0.0; @@ -267,6 +287,7 @@ pub(crate) extern "C" fn math_hypot_thunk( pub(crate) extern "C" fn global_this_parse_int_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, radix: f64, ) -> f64 { @@ -276,6 +297,7 @@ pub(crate) extern "C" fn global_this_parse_int_thunk( pub(crate) extern "C" fn global_this_parse_float_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let s = crate::builtins::js_string_coerce(value); @@ -284,6 +306,7 @@ pub(crate) extern "C" fn global_this_parse_float_thunk( pub(crate) extern "C" fn global_this_is_nan_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::builtins::js_is_nan(value) @@ -297,6 +320,7 @@ pub(crate) extern "C" fn global_this_is_nan_thunk( /// but ignored. Returns `undefined`. pub(crate) extern "C" fn global_this_gc_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _force: f64, ) -> f64 { crate::gc::js_gc_collect(); @@ -305,6 +329,7 @@ pub(crate) extern "C" fn global_this_gc_thunk( pub(crate) extern "C" fn global_this_is_finite_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::builtins::js_is_finite(value) @@ -312,6 +337,7 @@ pub(crate) extern "C" fn global_this_is_finite_thunk( pub(crate) extern "C" fn global_this_encode_uri_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::value::js_nanbox_string(crate::builtins::js_encode_uri(value)) @@ -319,6 +345,7 @@ pub(crate) extern "C" fn global_this_encode_uri_thunk( pub(crate) extern "C" fn global_this_decode_uri_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::value::js_nanbox_string(crate::builtins::js_decode_uri(value)) @@ -326,6 +353,7 @@ pub(crate) extern "C" fn global_this_decode_uri_thunk( pub(crate) extern "C" fn global_this_encode_uri_component_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::value::js_nanbox_string(crate::builtins::js_encode_uri_component(value)) @@ -333,6 +361,7 @@ pub(crate) extern "C" fn global_this_encode_uri_component_thunk( pub(crate) extern "C" fn global_this_decode_uri_component_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::value::js_nanbox_string(crate::builtins::js_decode_uri_component(value)) @@ -343,6 +372,7 @@ pub(crate) extern "C" fn global_this_decode_uri_component_thunk( // needs them as real callable globalThis function values. pub(crate) extern "C" fn global_this_escape_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::value::js_nanbox_string(crate::builtins::js_escape(value)) @@ -350,6 +380,7 @@ pub(crate) extern "C" fn global_this_escape_thunk( pub(crate) extern "C" fn global_this_unescape_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::value::js_nanbox_string(crate::builtins::js_unescape(value)) @@ -361,6 +392,7 @@ pub(crate) extern "C" fn global_this_unescape_thunk( // `const N = Number; N("42")` and `const B = Boolean; B(0)` match Node. pub(crate) extern "C" fn global_this_number_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let jsv = crate::value::JSValue::from_bits(value.to_bits()); @@ -375,6 +407,7 @@ pub(crate) extern "C" fn global_this_number_thunk( pub(crate) extern "C" fn global_this_boolean_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let b = crate::value::js_is_truthy(value) != 0; @@ -383,6 +416,7 @@ pub(crate) extern "C" fn global_this_boolean_thunk( pub(crate) extern "C" fn global_this_error_capture_stack_trace_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, constructor_opt: f64, ) -> f64 { @@ -392,6 +426,7 @@ pub(crate) extern "C" fn global_this_error_capture_stack_trace_thunk( /// #2904: `Error.isError(value)` thunk — delegates to the runtime duck-check. pub(crate) extern "C" fn global_this_error_is_error_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { crate::error::js_error_is_error(value) @@ -546,6 +581,7 @@ fn js_function_ctor_from_strings_impl(args_ptr: *const f64, args_len: usize) -> #[cfg(not(panic = "abort"))] pub(crate) extern "C-unwind" fn global_this_function_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { function_call_thunk_impl(rest) @@ -554,6 +590,7 @@ pub(crate) extern "C-unwind" fn global_this_function_call_thunk( #[cfg(panic = "abort")] pub(crate) extern "C" fn global_this_function_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { function_call_thunk_impl(rest) @@ -585,6 +622,7 @@ fn refuse_dynamic_function(args_len: usize, body: &str) -> ! { /// real one with identical `this`/arguments. extern "C" fn depd_wrapfunction_outer_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, fn_v: f64, _log: f64, _deprecate: f64, @@ -621,6 +659,7 @@ const _: extern "C" fn(*const f64, usize) -> f64 = js_function_ctor_from_strings /// callers that invoke it get a usable string rather than a crash. pub(crate) extern "C" fn global_this_error_prepare_stack_trace_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, error: f64, _structured_stack: f64, ) -> f64 { @@ -642,6 +681,7 @@ pub(crate) extern "C" fn global_this_error_prepare_stack_trace_thunk( /// existing `js_proxy_revocable` implementation in `crate::proxy`. pub(crate) extern "C" fn proxy_revocable_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, target: f64, handler: f64, ) -> f64 { @@ -712,12 +752,23 @@ mod tests { array_value(&[string_value("a"), string_value("b")]), string_value("return a + b"), ]); - let f = global_this_function_call_thunk(std::ptr::null(), rest); + let f = global_this_function_call_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + rest, + ); let args = [ f64::from_bits(crate::value::JSValue::number(2.0).bits()), f64::from_bits(crate::value::JSValue::number(3.0).bits()), ]; - let result = unsafe { crate::closure::js_native_call_value(f, args.as_ptr(), args.len()) }; + let result = unsafe { + crate::closure::js_native_call_value( + f, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + }; assert_eq!(crate::builtins::js_number_coerce(result), 5.0); } } diff --git a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs index 5fae385daf..68701e1531 100644 --- a/crates/perry-runtime/src/object/global_this/ctor_thunks.rs +++ b/crates/perry-runtime/src/object/global_this/ctor_thunks.rs @@ -19,6 +19,7 @@ pub(crate) fn normalize_eval_this_body(body: &str) -> Option { pub(crate) extern "C" fn typed_array_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor %TypedArray% requires 'new'") @@ -26,6 +27,7 @@ pub(crate) extern "C" fn typed_array_constructor_call_thunk( pub(crate) extern "C" fn construct_only_builtin_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor requires 'new'") } @@ -44,6 +46,7 @@ pub(crate) extern "C" fn construct_only_builtin_call_thunk( #[cfg(feature = "regex-engine")] pub(crate) extern "C" fn regexp_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, pattern: f64, flags: f64, ) -> f64 { @@ -55,6 +58,7 @@ pub(crate) extern "C" fn regexp_constructor_call_thunk( #[cfg(not(feature = "regex-engine"))] pub(crate) extern "C" fn regexp_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _pattern: f64, _flags: f64, ) -> f64 { @@ -68,6 +72,7 @@ pub(crate) extern "C" fn regexp_constructor_call_thunk( // construct-expression path and is unaffected.) pub(crate) extern "C" fn map_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor Map requires 'new'") @@ -75,6 +80,7 @@ pub(crate) extern "C" fn map_constructor_call_thunk( pub(crate) extern "C" fn set_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor Set requires 'new'") @@ -82,6 +88,7 @@ pub(crate) extern "C" fn set_constructor_call_thunk( pub(crate) extern "C" fn weak_map_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor WeakMap requires 'new'") @@ -89,6 +96,7 @@ pub(crate) extern "C" fn weak_map_constructor_call_thunk( pub(crate) extern "C" fn weak_set_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor WeakSet requires 'new'") @@ -96,6 +104,7 @@ pub(crate) extern "C" fn weak_set_constructor_call_thunk( pub(crate) extern "C" fn weak_ref_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor WeakRef requires 'new'") @@ -103,6 +112,7 @@ pub(crate) extern "C" fn weak_ref_constructor_call_thunk( pub(crate) extern "C" fn promise_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { super::super::object_ops::throw_object_type_error(b"Constructor Promise requires 'new'") @@ -110,6 +120,7 @@ pub(crate) extern "C" fn promise_constructor_call_thunk( pub(crate) extern "C" fn global_this_url_pattern_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, base: f64, ) -> f64 { @@ -123,6 +134,7 @@ fn error_constructor_call(kind: u32, message: f64) -> f64 { pub(crate) extern "C" fn error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_ERROR, message) @@ -130,6 +142,7 @@ pub(crate) extern "C" fn error_constructor_call_thunk( pub(crate) extern "C" fn type_error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_TYPE_ERROR, message) @@ -137,6 +150,7 @@ pub(crate) extern "C" fn type_error_constructor_call_thunk( pub(crate) extern "C" fn range_error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_RANGE_ERROR, message) @@ -144,6 +158,7 @@ pub(crate) extern "C" fn range_error_constructor_call_thunk( pub(crate) extern "C" fn reference_error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_REFERENCE_ERROR, message) @@ -151,6 +166,7 @@ pub(crate) extern "C" fn reference_error_constructor_call_thunk( pub(crate) extern "C" fn syntax_error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_SYNTAX_ERROR, message) @@ -158,6 +174,7 @@ pub(crate) extern "C" fn syntax_error_constructor_call_thunk( pub(crate) extern "C" fn eval_error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_EVAL_ERROR, message) @@ -165,6 +182,7 @@ pub(crate) extern "C" fn eval_error_constructor_call_thunk( pub(crate) extern "C" fn uri_error_constructor_call_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { error_constructor_call(crate::error::ERROR_KIND_URI_ERROR, message) @@ -278,6 +296,7 @@ pub(crate) fn is_array_prototype_method_value(value: f64, method: &str) -> bool pub(crate) extern "C" fn webcrypto_illegal_constructor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { crate::fs::validate::throw_type_error_with_code( "Illegal constructor", @@ -295,12 +314,13 @@ pub extern "C" fn js_webcrypto_illegal_constructor() -> f64 { pub(crate) extern "C" fn global_this_crypto_getter_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { super::super::native_module::webcrypto_namespace() } -fn require_webcrypto_this() -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn require_webcrypto_this(this: crate::closure::JsThis) -> f64 { + let this_value = f64::from_bits(this.bits()); let jv = crate::value::JSValue::from_bits(this_value.to_bits()); if jv.is_pointer() { let obj = jv.as_pointer::(); @@ -320,9 +340,10 @@ fn require_webcrypto_this() -> f64 { pub(crate) extern "C" fn webcrypto_get_random_values_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, array: f64, ) -> f64 { - let this_value = require_webcrypto_this(); + let this_value = require_webcrypto_this(this); unsafe { js_native_call_method( this_value, @@ -340,8 +361,9 @@ pub(crate) extern "C" fn webcrypto_get_random_values_thunk( /// name through the whole native-call tower on every UUID. pub(crate) extern "C" fn webcrypto_random_uuid_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - require_webcrypto_this(); + require_webcrypto_this(this); let ptr = crate::value::JS_NATIVE_CRYPTO_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); if ptr.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); @@ -359,13 +381,14 @@ pub(crate) extern "C" fn webcrypto_random_uuid_thunk( pub(crate) extern "C" fn webcrypto_subtle_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - require_webcrypto_this(); + require_webcrypto_this(this); super::super::native_module::subtle_crypto_namespace() } -fn cryptokey_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); +fn cryptokey_receiver_addr(this: crate::closure::JsThis) -> Option { + let this_bits = this.bits(); let this_jsv = crate::value::JSValue::from_bits(this_bits); let raw = if this_jsv.is_pointer() { (this_bits & crate::value::POINTER_MASK) as usize @@ -383,8 +406,8 @@ fn cryptokey_brand_error() -> ! { ) } -fn cryptokey_property_getter(key: &[u8]) -> f64 { - let addr = cryptokey_receiver_addr().unwrap_or_else(|| cryptokey_brand_error()); +fn cryptokey_property_getter(this: crate::closure::JsThis, key: &[u8]) -> f64 { + let addr = cryptokey_receiver_addr(this).unwrap_or_else(|| cryptokey_brand_error()); unsafe { super::super::crypto_key_property_value(addr, key) .map(|value| f64::from_bits(value.bits())) @@ -394,26 +417,30 @@ fn cryptokey_property_getter(key: &[u8]) -> f64 { pub(crate) extern "C" fn cryptokey_algorithm_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - cryptokey_property_getter(b"algorithm") + cryptokey_property_getter(this, b"algorithm") } pub(crate) extern "C" fn cryptokey_extractable_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - cryptokey_property_getter(b"extractable") + cryptokey_property_getter(this, b"extractable") } pub(crate) extern "C" fn cryptokey_type_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - cryptokey_property_getter(b"type") + cryptokey_property_getter(this, b"type") } pub(crate) extern "C" fn cryptokey_usages_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - cryptokey_property_getter(b"usages") + cryptokey_property_getter(this, b"usages") } /// #10427: `globalThis.crypto.` is a property READ, resolved fresh diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index 7aaa9cfbb8..0aefd49f8c 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -193,6 +193,7 @@ pub unsafe extern "C" fn js_global_or_console_property_by_name( #[inline(never)] pub(crate) extern "C" fn global_this_builtin_noop_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { std::hint::black_box(global_this_builtin_noop_thunk as *const u8); @@ -201,6 +202,7 @@ pub(crate) extern "C" fn global_this_builtin_noop_thunk( pub(crate) extern "C" fn global_this_date_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { let string = crate::date::js_date_to_string(crate::date::js_date_new()); @@ -268,6 +270,7 @@ fn global_this_init_headers_handle(init: f64) -> f64 { pub(crate) extern "C" fn global_this_blob_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, parts: f64, options: f64, ) -> f64 { @@ -277,6 +280,7 @@ pub(crate) extern "C" fn global_this_blob_thunk( pub(crate) extern "C" fn global_this_file_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, parts: f64, name: f64, options: f64, @@ -293,6 +297,7 @@ pub(crate) extern "C" fn global_this_file_thunk( pub(crate) extern "C" fn global_this_headers_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, init: f64, ) -> f64 { let headers = super::super::global_fetch::call_global_headers_new(); @@ -307,6 +312,7 @@ pub(crate) extern "C" fn global_this_headers_thunk( pub(crate) extern "C" fn global_this_response_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, body: f64, init: f64, ) -> f64 { @@ -341,6 +347,7 @@ pub(crate) extern "C" fn global_this_response_thunk( pub(crate) extern "C" fn global_this_request_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, init: f64, ) -> f64 { @@ -406,7 +413,7 @@ unsafe fn attach_temporal_cell_to_this(this_box: f64, cell_box: f64) { /// (`js_fetch_or_value_super`, the non-spread `super(a, b)` path) and the /// args-array `js_super_construct_apply` (the `super(...spread)` path). When /// `parent_val` is a Temporal constructor, run it (Temporal ctors return a -/// fresh cell and never mutate the implicit `this`) and stash the returned cell +/// fresh cell and never mutate their `this`) and stash the returned cell /// on `this_box` so method / getter / instanceof dispatch can recover the /// Temporal brand. Returns `true` when handled. (#5587) #[cfg(feature = "temporal")] @@ -425,16 +432,19 @@ pub(crate) unsafe fn temporal_subclass_super( // to the parent ctor for the duration of the call (the cell it returns is // re-homed onto the subclass `this`; the exact new.target identity is not // observable to these native ctors beyond being defined). Restore after. - // #9445: root the displaced receiver and `this_box` (consumed again below) - // across the parent constructor call. + // #9445: root `this_box` (consumed again below) across the parent + // constructor call. let this_scope = crate::gc::RuntimeHandleScope::new(); let this_h = this_scope.root_nanbox_f64(this_box); - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); // #10490: the displaced `new.target` crosses the same call. let prev_nt = this_scope.root_nanbox_f64(crate::object::js_new_target_set(parent_val)); - let cell = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); + let cell = crate::closure::native_call_value_this( + parent_val, + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), + args_ptr, + args_len, + ); crate::object::js_new_target_set(prev_nt.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if crate::temporal::is_temporal_value(cell) { attach_temporal_cell_to_this(this_h.get_nanbox_f64(), cell); } @@ -464,9 +474,19 @@ pub(crate) unsafe fn attach_fetch_handle_for_construction( undef }; let handle = if kind == 1 { - global_this_request_thunk(std::ptr::null(), arg0, arg1) + global_this_request_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + arg0, + arg1, + ) } else { - global_this_response_thunk(std::ptr::null(), arg0, arg1) + global_this_response_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + arg0, + arg1, + ) }; let this_box = crate::value::js_nanbox_pointer(inst as i64); attach_fetch_handle_to_this(this_box, handle); @@ -478,7 +498,12 @@ pub(crate) unsafe fn attach_fetch_handle_for_construction( /// `undefined` (the super-call value). #[no_mangle] pub extern "C" fn js_request_subclass_init(this_box: f64, input: f64, init: f64) -> f64 { - let handle = global_this_request_thunk(std::ptr::null(), input, init); + let handle = global_this_request_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + input, + init, + ); unsafe { attach_fetch_handle_to_this(this_box, handle) }; f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -487,7 +512,12 @@ pub extern "C" fn js_request_subclass_init(this_box: f64, input: f64, init: f64) /// `js_request_subclass_init` for the Response handle. #[no_mangle] pub extern "C" fn js_response_subclass_init(this_box: f64, body: f64, init: f64) -> f64 { - let handle = global_this_response_thunk(std::ptr::null(), body, init); + let handle = global_this_response_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + body, + init, + ); unsafe { attach_fetch_handle_to_this(this_box, handle) }; f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -605,8 +635,8 @@ fn is_uncallable_builtin_super_parent_class_id(class_id: u32) -> bool { /// instead every runtime-value `super()` dispatches through here. When /// `parent_val` resolves to the Request/Response constructor we allocate the /// native handle and stash it on `this` (so inherited body methods work); -/// otherwise we fall back to the ordinary implicit-`this`-bound -/// `js_native_call_value`, preserving the prior behavior for every other +/// otherwise we fall back to an ordinary value call with `this` passed, +/// preserving the prior behavior for every other /// runtime-value parent (Effect's `Data.Class`, etc.). #[no_mangle] pub unsafe extern "C" fn js_fetch_or_value_super( @@ -800,8 +830,8 @@ pub unsafe extern "C" fn js_fetch_or_value_super( } } // `class X extends Temporal.` (non-spread `super(a, b)`): a Temporal - // constructor returns a fresh NaN-boxed cell and does NOT mutate the - // implicit `this`, so the ordinary dispatch below would drop that cell and + // constructor returns a fresh NaN-boxed cell and does NOT mutate its + // `this`, so the ordinary dispatch below would drop that cell and // leave the subclass instance an empty object with no Temporal brand. Stash // the cell on `this` instead. The native ctor never calls the subclass // constructor, so `called`-counter invariants hold. (#5587) @@ -1003,9 +1033,19 @@ pub unsafe extern "C" fn js_fetch_or_value_super( undef }; let handle = if kind == Some("Request") { - global_this_request_thunk(std::ptr::null(), arg0, arg1) + global_this_request_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + arg0, + arg1, + ) } else { - global_this_response_thunk(std::ptr::null(), arg0, arg1) + global_this_response_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + arg0, + arg1, + ) }; attach_fetch_handle_to_this(this_box, handle); undef @@ -1118,11 +1158,12 @@ pub unsafe extern "C" fn js_fetch_or_value_super( } } } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); - let r = crate::closure::js_native_call_value(callee, args_ptr, args_len); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - r + crate::closure::native_call_value_this( + callee, + crate::closure::JsThis::from_f64(this_box), + args_ptr, + args_len, + ) } } } @@ -1134,12 +1175,14 @@ static KEEP_JS_FETCH_OR_VALUE_SUPER: unsafe extern "C" fn(f64, f64, *const f64, pub(crate) extern "C" fn global_this_response_error_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { super::super::global_fetch::call_global_response_static_error() } pub(crate) extern "C" fn global_this_response_json_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, init: f64, ) -> f64 { @@ -1161,6 +1204,7 @@ pub(crate) extern "C" fn global_this_response_json_thunk( pub(crate) extern "C" fn global_this_response_redirect_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, url: f64, status: f64, ) -> f64 { @@ -1175,6 +1219,7 @@ pub(crate) extern "C" fn global_this_response_redirect_thunk( pub(crate) extern "C" fn global_this_eval_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, source: f64, ) -> f64 { // PerformEval step: "If Type(x) is not String, return x." A non-string @@ -1226,7 +1271,10 @@ pub(crate) extern "C" fn global_this_eval_thunk( mod dynamic_super_new_target_tests { use super::*; - extern "C" fn read_new_target(_closure: *const crate::ClosureHeader) -> f64 { + extern "C" fn read_new_target( + _closure: *const crate::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { crate::object::js_new_target_get() } diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs index d5134d42aa..c5996a0b93 100644 --- a/crates/perry-runtime/src/object/global_this/generator.rs +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -336,7 +336,12 @@ fn generator_receiver_type_error_value(method: &[u8]) -> f64 { /// `is_async` selects the spec's incompatible-receiver behaviour: sync /// generators throw a `TypeError` synchronously, async generators return a /// rejected promise (their methods always return promises). (#3664) -fn generator_proto_method(method: &[u8], arg: f64, is_async: bool) -> f64 { +fn generator_proto_method( + this: crate::closure::JsThis, + method: &[u8], + arg: f64, + is_async: bool, +) -> f64 { let bad_receiver = |method: &[u8]| -> f64 { let errv = generator_receiver_type_error_value(method); if is_async { @@ -346,7 +351,7 @@ fn generator_proto_method(method: &[u8], arg: f64, is_async: bool) -> f64 { crate::exception::js_throw(errv) } }; - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let jv = JSValue::from_bits(this.to_bits()); if !jv.is_pointer() { return bad_receiver(method); @@ -390,46 +395,56 @@ fn generator_proto_method(method: &[u8], arg: f64, is_async: bool) -> f64 { return bad_receiver(method); } match own_method(method) { - Some(own_closure) => crate::closure::js_closure_call1(own_closure, arg), + Some(own_closure) => crate::closure::js_closure_call1( + own_closure, + crate::closure::plain_call_receiver(), + arg, + ), None => bad_receiver(method), } } extern "C" fn generator_proto_next_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - generator_proto_method(b"next", arg, false) + generator_proto_method(this, b"next", arg, false) } extern "C" fn generator_proto_return_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - generator_proto_method(b"return", arg, false) + generator_proto_method(this, b"return", arg, false) } extern "C" fn generator_proto_throw_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - generator_proto_method(b"throw", arg, false) + generator_proto_method(this, b"throw", arg, false) } extern "C" fn async_generator_proto_next_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - generator_proto_method(b"next", arg, true) + generator_proto_method(this, b"next", arg, true) } extern "C" fn async_generator_proto_return_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - generator_proto_method(b"return", arg, true) + generator_proto_method(this, b"return", arg, true) } extern "C" fn async_generator_proto_throw_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - generator_proto_method(b"throw", arg, true) + generator_proto_method(this, b"throw", arg, true) } /// `%AsyncGenerator.prototype%[Symbol.asyncIterator]()` returns `this` (spec @@ -438,9 +453,10 @@ extern "C" fn async_generator_proto_throw_thunk( /// iterator and either throws or silently produces nothing. extern "C" fn async_generator_proto_async_iterator_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { - crate::object::js_implicit_this_get() + this.as_f64() } /// `%Generator.prototype%[Symbol.iterator]()` returns `this` (spec inherits this @@ -448,9 +464,10 @@ extern "C" fn async_generator_proto_async_iterator_thunk( /// note in `build_generator_tower` for why the sync prototype now carries this. extern "C" fn generator_proto_iterator_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { - crate::object::js_implicit_this_get() + this.as_f64() } /// Install a well-known-symbol-keyed method (returning `this`) on a @@ -828,8 +845,8 @@ fn build_generator_tower( // Without the async one, `for await (x of gen())` over an async-generator // *method instance* can't resolve the async iterator and hangs/yields nothing // (the instance carries no own iterator symbol). The async-iterator- - // acquisition path (`js_get_async_iterator`) sets the implicit-this before - // invoking this thunk, so it returns the generator instance. + // acquisition path (`js_get_async_iterator`) passes the receiver as `this` + // when invoking this thunk, so it returns the generator instance. // // The SYNC `%Generator.prototype%` carries `[Symbol.iterator]` for the same // reason (#6696): a *computed* read `gen[Symbol.iterator]` walks the @@ -839,8 +856,8 @@ fn build_generator_tower( // `value[Symbol.iterator]()` on the delegate generator. `for (x of gen())` // is unaffected: it drives the generator's own `.next()` directly (the // builtin-iterator recognizers), and where the sync iterator-acquisition - // path (`js_get_iterator`) does read `[Symbol.iterator]`, it binds - // implicit-this before invoking the method, so the thunk returns the + // path (`js_get_iterator`) does read `[Symbol.iterator]`, it passes the + // receiver as `this` when invoking the method, so the thunk returns the // generator instance. let (symbol_name, display_name, thunk) = if is_async { ( diff --git a/crates/perry-runtime/src/object/global_this/install_static.rs b/crates/perry-runtime/src/object/global_this/install_static.rs index 87fc702462..844ab0f115 100644 --- a/crates/perry-runtime/src/object/global_this/install_static.rs +++ b/crates/perry-runtime/src/object/global_this/install_static.rs @@ -51,6 +51,7 @@ pub extern "C" fn js_promise_static_function_value(name_ptr: *const u8, name_len extern "C" fn url_can_parse_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, base: f64, ) -> f64 { @@ -66,6 +67,7 @@ extern "C" fn url_can_parse_thunk( extern "C" fn url_parse_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, base: f64, ) -> f64 { @@ -91,6 +93,7 @@ extern "C" fn url_parse_thunk( // compile-time `Uint8Array.fromBase64(str)` call path produces. extern "C" fn uint8array_from_base64_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, opts: f64, ) -> f64 { @@ -100,6 +103,7 @@ extern "C" fn uint8array_from_base64_thunk( extern "C" fn uint8array_from_hex_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, input: f64, ) -> f64 { let buf = crate::buffer::js_u8_from_hex(input.to_bits() as i64); @@ -108,6 +112,7 @@ extern "C" fn uint8array_from_hex_thunk( extern "C" fn subtle_crypto_supports_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { let args = global_this_rest_array_values(rest); @@ -158,8 +163,8 @@ fn rejected_type_error_with_code_promise(message: &str, code: &'static str) -> f crate::value::js_nanbox_pointer(promise as i64) } -fn subtle_crypto_dispatch_rest(method_name: &str, rest: f64) -> f64 { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn subtle_crypto_dispatch_rest(this: crate::closure::JsThis, method_name: &str, rest: f64) -> f64 { + let this_value = f64::from_bits(this.bits()); if !is_subtle_crypto_this(this_value) { return rejected_type_error_with_code_promise( "Value of \"this\" must be of type SubtleCrypto", @@ -186,30 +191,34 @@ fn subtle_crypto_dispatch_rest(method_name: &str, rest: f64) -> f64 { pub(crate) extern "C" fn subtle_crypto_encapsulate_bits_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - subtle_crypto_dispatch_rest("encapsulateBits", rest) + subtle_crypto_dispatch_rest(this, "encapsulateBits", rest) } pub(crate) extern "C" fn subtle_crypto_decapsulate_bits_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - subtle_crypto_dispatch_rest("decapsulateBits", rest) + subtle_crypto_dispatch_rest(this, "decapsulateBits", rest) } pub(crate) extern "C" fn subtle_crypto_encapsulate_key_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - subtle_crypto_dispatch_rest("encapsulateKey", rest) + subtle_crypto_dispatch_rest(this, "encapsulateKey", rest) } pub(crate) extern "C" fn subtle_crypto_decapsulate_key_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - subtle_crypto_dispatch_rest("decapsulateKey", rest) + subtle_crypto_dispatch_rest(this, "decapsulateKey", rest) } /// Install a single callable static method on a constructor closure as a @@ -986,22 +995,24 @@ pub(crate) fn install_noop_proto_methods(proto_obj: *mut ObjectHeader, methods: pub(crate) extern "C" fn url_pattern_test_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, input: f64, rest: f64, ) -> f64 { let base = rest_first_arg(rest); - let this_value = crate::object::js_implicit_this_get(); + let this_value = this.as_f64(); let pattern = crate::value::js_nanbox_get_pointer(this_value) as *mut ObjectHeader; crate::url::js_url_pattern_test(pattern, input, base) } pub(crate) extern "C" fn url_pattern_exec_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, input: f64, rest: f64, ) -> f64 { let base = rest_first_arg(rest); - let this_value = crate::object::js_implicit_this_get(); + let this_value = this.as_f64(); let pattern = crate::value::js_nanbox_get_pointer(this_value) as *mut ObjectHeader; crate::url::js_url_pattern_exec(pattern, input, base) } @@ -1024,8 +1035,9 @@ fn rest_first_arg(rest: f64) -> f64 { /// 27.2.4.8, 22.2.5.2, 23.2.2.4). pub(crate) extern "C" fn builtin_species_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - f64::from_bits(IMPLICIT_THIS.with(|c| c.get())) + f64::from_bits(this.bits()) } /// Install the standard own `get [Symbol.species]` accessor diff --git a/crates/perry-runtime/src/object/global_this/math_temporal.rs b/crates/perry-runtime/src/object/global_this/math_temporal.rs index 1ade09495d..305f132f5b 100644 --- a/crates/perry-runtime/src/object/global_this/math_temporal.rs +++ b/crates/perry-runtime/src/object/global_this/math_temporal.rs @@ -86,6 +86,7 @@ pub(crate) fn install_math_namespace(ns_obj: *mut ObjectHeader) { #[cfg(feature = "temporal")] extern "C" fn temporal_duration_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::duration::construct(&global_this_rest_array_values(rest)) @@ -94,6 +95,7 @@ extern "C" fn temporal_duration_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_duration_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::duration::from_static(&global_this_rest_array_values(rest)) @@ -102,6 +104,7 @@ extern "C" fn temporal_duration_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_duration_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::duration::compare_static(&global_this_rest_array_values(rest)) @@ -110,6 +113,7 @@ extern "C" fn temporal_duration_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_instant_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::instant::construct(&global_this_rest_array_values(rest)) @@ -118,6 +122,7 @@ extern "C" fn temporal_instant_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_instant_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::instant::from_static(&global_this_rest_array_values(rest)) @@ -126,6 +131,7 @@ extern "C" fn temporal_instant_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_instant_from_epoch_ms_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::instant::from_epoch_milliseconds_static(&global_this_rest_array_values(rest)) @@ -134,6 +140,7 @@ extern "C" fn temporal_instant_from_epoch_ms_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_instant_from_epoch_ns_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::instant::from_epoch_nanoseconds_static(&global_this_rest_array_values(rest)) @@ -142,6 +149,7 @@ extern "C" fn temporal_instant_from_epoch_ns_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_instant_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::instant::compare_static(&global_this_rest_array_values(rest)) @@ -150,6 +158,7 @@ extern "C" fn temporal_instant_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_date_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_date::construct(&global_this_rest_array_values(rest)) @@ -158,6 +167,7 @@ extern "C" fn temporal_plain_date_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_date_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_date::from_static(&global_this_rest_array_values(rest)) @@ -166,6 +176,7 @@ extern "C" fn temporal_plain_date_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_date_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_date::compare_static(&global_this_rest_array_values(rest)) @@ -174,6 +185,7 @@ extern "C" fn temporal_plain_date_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_time_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_time::construct(&global_this_rest_array_values(rest)) @@ -182,6 +194,7 @@ extern "C" fn temporal_plain_time_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_time_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_time::from_static(&global_this_rest_array_values(rest)) @@ -190,6 +203,7 @@ extern "C" fn temporal_plain_time_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_time_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_time::compare_static(&global_this_rest_array_values(rest)) @@ -198,6 +212,7 @@ extern "C" fn temporal_plain_time_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_date_time_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_date_time::construct(&global_this_rest_array_values(rest)) @@ -206,6 +221,7 @@ extern "C" fn temporal_plain_date_time_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_date_time_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_date_time::from_static(&global_this_rest_array_values(rest)) @@ -214,6 +230,7 @@ extern "C" fn temporal_plain_date_time_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_date_time_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_date_time::compare_static(&global_this_rest_array_values(rest)) @@ -222,6 +239,7 @@ extern "C" fn temporal_plain_date_time_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_year_month_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_year_month::construct(&global_this_rest_array_values(rest)) @@ -230,6 +248,7 @@ extern "C" fn temporal_plain_year_month_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_year_month_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_year_month::from_static(&global_this_rest_array_values(rest)) @@ -238,6 +257,7 @@ extern "C" fn temporal_plain_year_month_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_year_month_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_year_month::compare_static(&global_this_rest_array_values(rest)) @@ -246,6 +266,7 @@ extern "C" fn temporal_plain_year_month_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_month_day_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_month_day::construct(&global_this_rest_array_values(rest)) @@ -254,6 +275,7 @@ extern "C" fn temporal_plain_month_day_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_plain_month_day_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::plain_month_day::from_static(&global_this_rest_array_values(rest)) @@ -262,6 +284,7 @@ extern "C" fn temporal_plain_month_day_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_zoned_date_time_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::zoned_date_time::construct(&global_this_rest_array_values(rest)) @@ -270,6 +293,7 @@ extern "C" fn temporal_zoned_date_time_ctor_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_zoned_date_time_from_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::zoned_date_time::from_static(&global_this_rest_array_values(rest)) @@ -278,6 +302,7 @@ extern "C" fn temporal_zoned_date_time_from_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_zoned_date_time_compare_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::zoned_date_time::compare_static(&global_this_rest_array_values(rest)) @@ -288,6 +313,7 @@ extern "C" fn temporal_zoned_date_time_compare_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_now_instant_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::now::instant(&global_this_rest_array_values(rest)) @@ -296,6 +322,7 @@ extern "C" fn temporal_now_instant_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_now_timezone_id_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::now::time_zone_id(&global_this_rest_array_values(rest)) @@ -304,6 +331,7 @@ extern "C" fn temporal_now_timezone_id_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_now_plain_date_time_iso_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::now::plain_date_time_iso(&global_this_rest_array_values(rest)) @@ -312,6 +340,7 @@ extern "C" fn temporal_now_plain_date_time_iso_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_now_plain_date_iso_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::now::plain_date_iso(&global_this_rest_array_values(rest)) @@ -320,6 +349,7 @@ extern "C" fn temporal_now_plain_date_iso_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_now_plain_time_iso_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::now::plain_time_iso(&global_this_rest_array_values(rest)) @@ -328,6 +358,7 @@ extern "C" fn temporal_now_plain_time_iso_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_now_zoned_date_time_iso_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { crate::temporal::now::zoned_date_time_iso(&global_this_rest_array_values(rest)) @@ -378,12 +409,15 @@ fn build_temporal_now_namespace() -> f64 { /// path and use the returned cell. /// Generic accessor-getter thunk shared by every `Temporal..prototype` /// getter. The property name and expected brand kind are stored on the closure -/// instance (`__tname` / `__tkind`); the receiver comes from `IMPLICIT_THIS`. +/// instance (`__tname` / `__tkind`); the receiver is the `this` argument. /// Throws `TypeError` on a non-Temporal or wrong-brand receiver (the getter /// `branding.js` tests: `blank.call(undefined)`, `years.call({})`, …). #[cfg(feature = "temporal")] -extern "C" fn temporal_proto_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { - let recv = super::super::js_implicit_this_get(); +extern "C" fn temporal_proto_getter_thunk( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let recv = this.as_f64(); let cl = closure as usize; let kind = crate::closure::closure_get_dynamic_prop(cl, "__tkind"); let expected = crate::value::JSValue::from_bits(kind.to_bits()).to_number() as u8; @@ -401,16 +435,17 @@ extern "C" fn temporal_proto_getter_thunk(closure: *const crate::closure::Closur /// Generic method thunk shared by every `Temporal..prototype` method. /// Rest-ABI (fixed arity 0): all args arrive in `rest`. Brand-checks the -/// `IMPLICIT_THIS` receiver, then forwards to the per-type dispatch router — +/// `this` receiver, then forwards to the per-type dispatch router — /// used when a prototype method is invoked through indirection /// (`Temporal.Duration.prototype.add.call(d, x)`); the normal `d.add(x)` path /// is the brand arm in `js_native_call_method`. #[cfg(feature = "temporal")] extern "C" fn temporal_proto_method_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let recv = super::super::js_implicit_this_get(); + let recv = this.as_f64(); let cl = closure as usize; let kind = crate::closure::closure_get_dynamic_prop(cl, "__tkind"); let expected = crate::value::JSValue::from_bits(kind.to_bits()).to_number() as u8; @@ -601,13 +636,14 @@ fn temporal_brand_type_error(type_name: &str, member: &str) -> ! { } /// Shared body for a `Temporal.ZonedDateTime.prototype` accessor getter invoked -/// reflectively. Resolves `this` from `IMPLICIT_THIS`, brand-checks it is a +/// reflectively. Takes `this` as an argument, brand-checks it is a /// `ZonedDateTime`, and returns the getter's value. #[cfg(feature = "temporal")] extern "C" fn temporal_zdt_proto_getter_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this = f64::from_bits(this.bits()); // The accessor's name is `"get "`; recover the bare property. let name = temporal_closure_name(closure); let prop = name.strip_prefix("get ").unwrap_or(&name); @@ -624,9 +660,10 @@ extern "C" fn temporal_zdt_proto_getter_thunk( #[cfg(feature = "temporal")] extern "C" fn temporal_zdt_proto_method_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this = f64::from_bits(this.bits()); let name = temporal_closure_name(closure); if crate::temporal::temporal_kind(this) != Some(crate::temporal::TemporalKind::ZonedDateTime) { temporal_brand_type_error("Temporal.ZonedDateTime", &name); diff --git a/crates/perry-runtime/src/object/global_this/populate.rs b/crates/perry-runtime/src/object/global_this/populate.rs index 67bd848b03..589466b205 100644 --- a/crates/perry-runtime/src/object/global_this/populate.rs +++ b/crates/perry-runtime/src/object/global_this/populate.rs @@ -401,10 +401,10 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade } // Populate well-known method properties on the prototype // (currently just `Array.prototype.slice`). Methods are - // ClosureHeader-backed thunks that read their receiver from - // `IMPLICIT_THIS` and dispatch to the corresponding native - // entry point — works in tandem with `.call`/`.apply` since - // those arms (#970) rebind IMPLICIT_THIS before forwarding. + // ClosureHeader-backed thunks that take their receiver as + // `this` and dispatch to the corresponding native entry + // point — works in tandem with `.call`/`.apply` since those + // arms (#970) pass the explicit receiver when forwarding. populate_builtin_prototype_methods(name, proto_obj); if name == "Function" { // SAFETY: `proto_obj` is the live, just-populated prototype. diff --git a/crates/perry-runtime/src/object/global_this/proto_methods.rs b/crates/perry-runtime/src/object/global_this/proto_methods.rs index 0d21242c22..975420c954 100644 --- a/crates/perry-runtime/src/object/global_this/proto_methods.rs +++ b/crates/perry-runtime/src/object/global_this/proto_methods.rs @@ -4,8 +4,8 @@ use super::*; // `array_proto_*_thunk` without routing through the trunk re-exports. use super::array_error::*; -fn web_method_receiver(name: &str) -> *mut ObjectHeader { - let receiver = crate::object::js_implicit_this_get(); +fn web_method_receiver(this: crate::closure::JsThis, name: &str) -> *mut ObjectHeader { + let receiver = this.as_f64(); if crate::object::web_builtin_to_string_tag(receiver) == Some(name) { return crate::value::js_nanbox_get_pointer(receiver) as *mut ObjectHeader; } @@ -15,22 +15,30 @@ fn web_method_receiver(name: &str) -> *mut ObjectHeader { crate::exception::js_throw(crate::value::js_nanbox_pointer(error as i64)) } -extern "C" fn url_prototype_href_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - crate::url::js_url_get_href(web_method_receiver("URL")) +extern "C" fn url_prototype_href_thunk( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + crate::url::js_url_get_href(web_method_receiver(this, "URL")) } extern "C" fn abort_controller_prototype_abort_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, reason: f64, ) -> f64 { - crate::url::js_abort_controller_abort_reason(web_method_receiver("AbortController"), reason); + crate::url::js_abort_controller_abort_reason( + web_method_receiver(this, "AbortController"), + reason, + ); f64::from_bits(crate::value::TAG_UNDEFINED) } extern "C" fn abort_signal_prototype_throw_if_aborted_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - crate::url::js_abort_signal_throw_if_aborted(web_method_receiver("AbortSignal")) + crate::url::js_abort_signal_throw_if_aborted(web_method_receiver(this, "AbortSignal")) } fn web_method_enumerable(proto_obj: *mut ObjectHeader, name: &str) { @@ -227,27 +235,29 @@ fn install_object_prototype_dunder_proto(proto_obj: *mut ObjectHeader) { extern "C" fn object_prototype_dunder_proto_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { // Spec (Annex B §B.3.1 `get __proto__`): `ToObject(this).[[GetPrototypeOf]]()`. // `js_object_get_prototype_of` already implements exactly this shape — // wrapper-prototype resolution for primitives, Proxy/Temporal/handle // receivers, and a throw on `null`/`undefined` (the `ToObject` failure // case) — so the getter is a direct delegation, not a reimplementation. - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); crate::object::js_object_get_prototype_of(receiver) } extern "C" fn object_prototype_dunder_proto_setter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); crate::proxy::legacy_dunder_proto_set(receiver, value); f64::from_bits(crate::value::TAG_UNDEFINED) } /// Install a native accessor (getter only) on a builtin prototype. The getter -/// is an ordinary `ClosureHeader` that reads its receiver from `IMPLICIT_THIS` +/// is an ordinary `ClosureHeader` that takes its receiver as `this` /// and brand-checks it, so `Object.getOwnPropertyDescriptor(P, k).get.call({})` /// throws like node's. /// @@ -385,7 +395,7 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: ); // Generic mutators get REAL thunks (vs the noop above) so a borrowed // reference works: `obj.pop = Array.prototype.pop; obj.pop()` and - // `Array.prototype.splice.call(obj, …)`. Each reads IMPLICIT_THIS and + // `Array.prototype.splice.call(obj, …)`. Each reads its `this` and // runs the array algorithm on a real array or array-like object. // #7760: `values` was a NOOP in the list above, and // `Array.prototype[Symbol.iterator]` was not an own property at all @@ -466,7 +476,7 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: ); // Iteration / search methods: real generic-engine thunks (rest // shape — spec `.length` recorded separately below). - type RestThunk = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; + type RestThunk = crate::closure::body_call::js_body_fn_ty!(a); let arraylike_thunks: [(&str, RestThunk, u32); 14] = [ ("forEach", array_proto_forEach_thunk, 1), ("map", array_proto_map_thunk, 1), diff --git a/crates/perry-runtime/src/object/global_this/typed_array.rs b/crates/perry-runtime/src/object/global_this/typed_array.rs index 44b7d88a61..284c5d5b06 100644 --- a/crates/perry-runtime/src/object/global_this/typed_array.rs +++ b/crates/perry-runtime/src/object/global_this/typed_array.rs @@ -1,7 +1,7 @@ use super::*; -fn array_buffer_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); +fn array_buffer_receiver_addr(this: crate::closure::JsThis) -> Option { + let this_bits = this.bits(); let this_jsv = JSValue::from_bits(this_bits); let raw = if this_jsv.is_pointer() { (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize @@ -25,8 +25,9 @@ fn array_buffer_brand_error() -> ! { pub(crate) extern "C" fn array_buffer_byte_length_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match array_buffer_receiver_addr() { + match array_buffer_receiver_addr(this) { Some(addr) => { let buf = addr as *const crate::buffer::BufferHeader; f64::from_bits( @@ -41,8 +42,8 @@ pub(crate) extern "C" fn array_buffer_byte_length_getter_thunk( /// getter. Mirrors `array_buffer_receiver_addr` but accepts only buffers in the /// shared registry, so the getter rejects a plain `ArrayBuffer` `this` /// (test262 SharedArrayBuffer/prototype/byteLength/this-is-arraybuffer). -fn shared_array_buffer_receiver_addr() -> Option { - let this_bits = IMPLICIT_THIS.with(|c| c.get()); +fn shared_array_buffer_receiver_addr(this: crate::closure::JsThis) -> Option { + let this_bits = this.bits(); let this_jsv = JSValue::from_bits(this_bits); let raw = if this_jsv.is_pointer() { (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize @@ -60,8 +61,9 @@ fn shared_array_buffer_receiver_addr() -> Option { pub(crate) extern "C" fn shared_array_buffer_byte_length_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match shared_array_buffer_receiver_addr() { + match shared_array_buffer_receiver_addr(this) { Some(addr) => { let buf = addr as *const crate::buffer::BufferHeader; f64::from_bits( @@ -81,10 +83,11 @@ pub(crate) extern "C" fn shared_array_buffer_byte_length_getter_thunk( /// is shared with the instance dispatch in `buffer_dispatch`. pub(crate) extern "C" fn shared_array_buffer_slice_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - match shared_array_buffer_receiver_addr() { + match shared_array_buffer_receiver_addr(this) { Some(addr) => unsafe { let args = [start, end]; super::super::buffer_dispatch::dispatch_buffer_method(addr, "slice", args.as_ptr(), 2) @@ -97,10 +100,11 @@ pub(crate) extern "C" fn shared_array_buffer_slice_thunk( pub(crate) extern "C" fn array_buffer_slice_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, start: f64, end: f64, ) -> f64 { - match array_buffer_receiver_addr() { + match array_buffer_receiver_addr(this) { Some(addr) => unsafe { let args = [start, end]; super::super::buffer_dispatch::dispatch_buffer_method(addr, "slice", args.as_ptr(), 2) @@ -115,8 +119,13 @@ pub(crate) extern "C" fn array_buffer_slice_thunk( /// reflectively (`ArrayBuffer.prototype.resize.call(ab, n)`). Instances /// dispatch through `buffer_dispatch` directly; these only brand-check the /// receiver and forward to the same arm, so the two cannot disagree (#10873). -fn array_buffer_method_via_dispatch(method: &'static str, arg: f64, brand_error: &[u8]) -> f64 { - match array_buffer_receiver_addr() { +fn array_buffer_method_via_dispatch( + this: crate::closure::JsThis, + method: &'static str, + arg: f64, + brand_error: &[u8], +) -> f64 { + match array_buffer_receiver_addr(this) { Some(addr) if !crate::buffer::is_shared_array_buffer(addr) && !crate::buffer::is_data_view(addr) => @@ -130,9 +139,11 @@ fn array_buffer_method_via_dispatch(method: &'static str, arg: f64, brand_error: pub(crate) extern "C" fn array_buffer_resize_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, new_length: f64, ) -> f64 { array_buffer_method_via_dispatch( + this, "resize", new_length, b"Method ArrayBuffer.prototype.resize called on incompatible receiver", @@ -141,9 +152,11 @@ pub(crate) extern "C" fn array_buffer_resize_thunk( pub(crate) extern "C" fn array_buffer_transfer_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, new_length: f64, ) -> f64 { array_buffer_method_via_dispatch( + this, "transfer", new_length, b"Method ArrayBuffer.prototype.transfer called on incompatible receiver", @@ -152,9 +165,11 @@ pub(crate) extern "C" fn array_buffer_transfer_thunk( pub(crate) extern "C" fn array_buffer_transfer_to_fixed_length_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, new_length: f64, ) -> f64 { array_buffer_method_via_dispatch( + this, "transferToFixedLength", new_length, b"Method ArrayBuffer.prototype.transferToFixedLength called on incompatible receiver", @@ -165,8 +180,8 @@ pub(crate) extern "C" fn array_buffer_transfer_to_fixed_length_thunk( /// accessors on `ArrayBuffer.prototype`. Instance reads are answered by /// `get_field_by_name_tail`; this is the `Object.getOwnPropertyDescriptor( /// ArrayBuffer.prototype, "resizable").get.call(ab)` route. -fn array_buffer_flag_getter(key: &[u8]) -> f64 { - let Some(addr) = array_buffer_receiver_addr() else { +fn array_buffer_flag_getter(this: crate::closure::JsThis, key: &[u8]) -> f64 { + let Some(addr) = array_buffer_receiver_addr(this) else { super::super::object_ops::throw_object_type_error( b"Method ArrayBuffer.prototype getter called on incompatible receiver", ) @@ -191,20 +206,23 @@ fn array_buffer_flag_getter(key: &[u8]) -> f64 { pub(crate) extern "C" fn array_buffer_resizable_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - array_buffer_flag_getter(b"resizable") + array_buffer_flag_getter(this, b"resizable") } pub(crate) extern "C" fn array_buffer_max_byte_length_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - array_buffer_flag_getter(b"maxByteLength") + array_buffer_flag_getter(this, b"maxByteLength") } pub(crate) extern "C" fn array_buffer_detached_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - array_buffer_flag_getter(b"detached") + array_buffer_flag_getter(this, b"detached") } pub(crate) unsafe fn validate_array_buffer_species_constructor(addr: usize) { @@ -292,6 +310,7 @@ pub(crate) unsafe fn validate_array_buffer_species_constructor(addr: usize) { pub(crate) extern "C" fn array_buffer_is_view_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { // #11239: the same classifier `util.types.isArrayBufferView` and the @@ -344,14 +363,14 @@ impl TypedArrayAccessorReceiver { } } -/// Resolve the `IMPLICIT_THIS` receiver for the reflected +/// Resolve the `this` receiver for the reflected /// `%TypedArray%.prototype` accessors installed by #2060. A Perry /// `Uint8Array` (and Node's `Buffer` subclass) uses `BufferHeader`, so a miss /// in `lookup_typed_array_kind` must continue through the strict buffer brand /// check instead of becoming a `TypeError` (#9347). -fn typed_array_receiver() -> Option { +fn typed_array_receiver(this: crate::closure::JsThis) -> Option { use crate::value::JSValue; - let this_bits = IMPLICIT_THIS.with(|c| c.get()); + let this_bits = this.bits(); let this_jsv = JSValue::from_bits(this_bits); let raw = if this_jsv.is_pointer() { (this_bits & 0x0000_FFFF_FFFF_FFFF) as usize @@ -394,8 +413,8 @@ fn string_value_to_owned(value: f64) -> Option { } } -pub(crate) fn typed_array_constructor_this_kind() -> Option { - let this_value = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +pub(crate) fn typed_array_constructor_this_kind(this: crate::closure::JsThis) -> Option { + let this_value = f64::from_bits(this.bits()); let ptr = crate::value::js_nanbox_get_pointer(this_value) as usize; if ptr == 0 || !crate::closure::is_closure_ptr(ptr) { return None; @@ -416,8 +435,9 @@ fn typed_array_buffer_value(receiver: TypedArrayAccessorReceiver) -> f64 { /// `%TypedArray%.prototype.length` getter — element count of the receiver. extern "C" fn typed_array_length_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match typed_array_receiver() { + match typed_array_receiver(this) { Some(receiver) => { f64::from_bits(crate::value::JSValue::number(receiver.length() as f64).bits()) } @@ -428,8 +448,9 @@ extern "C" fn typed_array_length_getter_thunk( /// `%TypedArray%.prototype.byteLength` getter — `length * BYTES_PER_ELEMENT`. extern "C" fn typed_array_byte_length_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match typed_array_receiver() { + match typed_array_receiver(this) { Some(receiver) => { f64::from_bits(crate::value::JSValue::number(receiver.byte_length() as f64).bits()) } @@ -440,8 +461,9 @@ extern "C" fn typed_array_byte_length_getter_thunk( /// `%TypedArray%.prototype.byteOffset` getter. extern "C" fn typed_array_byte_offset_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match typed_array_receiver() { + match typed_array_receiver(this) { Some(receiver) => { f64::from_bits(crate::value::JSValue::number(receiver.byte_offset() as f64).bits()) } @@ -453,8 +475,9 @@ extern "C" fn typed_array_byte_offset_getter_thunk( /// identity as the direct property path. extern "C" fn typed_array_buffer_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match typed_array_receiver() { + match typed_array_receiver(this) { Some(receiver) => typed_array_buffer_value(receiver), None => typed_array_brand_error(), } @@ -470,8 +493,9 @@ extern "C" fn typed_array_buffer_getter_thunk( /// `Cannot read properties of undefined (reading 'get')`. extern "C" fn typed_array_to_string_tag_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this = f64::from_bits(this.bits()); match crate::object::typed_array_to_string_tag_name(this) { Some(name) => { let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); diff --git a/crates/perry-runtime/src/object/global_this_tables.rs b/crates/perry-runtime/src/object/global_this_tables.rs index e666063ecb..b8398e49de 100644 --- a/crates/perry-runtime/src/object/global_this_tables.rs +++ b/crates/perry-runtime/src/object/global_this_tables.rs @@ -105,8 +105,8 @@ pub(crate) const GLOBAL_THIS_BUILTIN_CONSTRUCTORS: &[&str] = &[ /// /// #7518: `try_dispatch_value_called_proto_method` needs this to tell a built-in /// *prototype method* invoked as a value — the #3716 uncurry-this idiom, which it -/// must re-dispatch by name on `IMPLICIT_THIS` — from a global *constructor* -/// invoked as a value, which it must not: `IMPLICIT_THIS.EventTarget(…)` resolves +/// must re-dispatch by name on the call's `this` — from a global *constructor* +/// invoked as a value, which it must not: `this.EventTarget(…)` resolves /// to nothing and the by-name tower's catch-all throws /// `TypeError: EventTarget is not a function`. /// diff --git a/crates/perry-runtime/src/object/global_this_webassembly.rs b/crates/perry-runtime/src/object/global_this_webassembly.rs index e618f2cfa5..c25c0b1ad3 100644 --- a/crates/perry-runtime/src/object/global_this_webassembly.rs +++ b/crates/perry-runtime/src/object/global_this_webassembly.rs @@ -492,6 +492,7 @@ fn throw_requires_new(what: &str) -> ! { #[cfg(feature = "wasm-host")] extern "C" fn webassembly_compile_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, bytes: f64, ) -> f64 { crate::webassembly::js_webassembly_compile(bytes) @@ -500,6 +501,7 @@ extern "C" fn webassembly_compile_thunk( #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_compile_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _bytes: f64, ) -> f64 { wasm_unsupported_rejection("WebAssembly.compile") @@ -508,6 +510,7 @@ extern "C" fn webassembly_compile_thunk( #[cfg(feature = "wasm-host")] extern "C" fn webassembly_instantiate_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, bytes: f64, imports: f64, ) -> f64 { @@ -517,6 +520,7 @@ extern "C" fn webassembly_instantiate_thunk( #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_instantiate_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _bytes: f64, _imports: f64, ) -> f64 { @@ -526,6 +530,7 @@ extern "C" fn webassembly_instantiate_thunk( #[cfg(feature = "wasm-host")] extern "C" fn webassembly_validate_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, bytes: f64, ) -> f64 { crate::webassembly::js_webassembly_validate(bytes) @@ -534,6 +539,7 @@ extern "C" fn webassembly_validate_thunk( #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_validate_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _bytes: f64, ) -> f64 { f64::from_bits(crate::value::JSValue::bool(false).bits()) @@ -543,6 +549,7 @@ extern "C" fn webassembly_validate_thunk( /// has no streaming path either, so these reject under both cfgs. extern "C" fn webassembly_compile_streaming_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _source: f64, ) -> f64 { wasm_unsupported_rejection("WebAssembly.compileStreaming") @@ -550,6 +557,7 @@ extern "C" fn webassembly_compile_streaming_thunk( extern "C" fn webassembly_instantiate_streaming_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _source: f64, ) -> f64 { wasm_unsupported_rejection("WebAssembly.instantiateStreaming") @@ -558,6 +566,7 @@ extern "C" fn webassembly_instantiate_streaming_thunk( /// `WebAssembly.promising` (JSPI) — kept as an existing-but-inert function. extern "C" fn webassembly_unsupported_static_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { undefined() @@ -569,6 +578,7 @@ extern "C" fn webassembly_unsupported_static_thunk( extern "C" fn webassembly_module_ctor_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, bytes: f64, ) -> f64 { if !invoked_as_constructor(closure) { @@ -591,6 +601,7 @@ extern "C" fn webassembly_module_ctor_thunk( extern "C" fn webassembly_instance_ctor_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, module: f64, imports: f64, ) -> f64 { @@ -599,21 +610,18 @@ extern "C" fn webassembly_instance_ctor_thunk( } #[cfg(feature = "wasm-host")] { - crate::webassembly::js_webassembly_instance_new( - module, - imports, - crate::object::js_implicit_this_get(), - ) + crate::webassembly::js_webassembly_instance_new(module, imports, this.as_f64()) } #[cfg(not(feature = "wasm-host"))] { - let _ = (module, imports); + let _ = (this, module, imports); crate::exception::js_throw(wasm_unsupported_error(b"LinkError", "WebAssembly.Instance")); } } extern "C" fn webassembly_table_ctor_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, descriptor: f64, ) -> f64 { if !invoked_as_constructor(closure) { @@ -621,20 +629,18 @@ extern "C" fn webassembly_table_ctor_thunk( } #[cfg(feature = "wasm-host")] { - crate::webassembly::js_webassembly_table_new( - descriptor, - crate::object::js_implicit_this_get(), - ) + crate::webassembly::js_webassembly_table_new(descriptor, this.as_f64()) } #[cfg(not(feature = "wasm-host"))] { - let _ = descriptor; + let _ = (this, descriptor); crate::exception::js_throw(wasm_unsupported_error(b"RuntimeError", "WebAssembly.Table")); } } extern "C" fn webassembly_global_ctor_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, descriptor: f64, initial: f64, ) -> f64 { @@ -643,15 +649,11 @@ extern "C" fn webassembly_global_ctor_thunk( } #[cfg(feature = "wasm-host")] { - crate::webassembly::js_webassembly_global_new( - descriptor, - initial, - crate::object::js_implicit_this_get(), - ) + crate::webassembly::js_webassembly_global_new(descriptor, initial, this.as_f64()) } #[cfg(not(feature = "wasm-host"))] { - let _ = (descriptor, initial); + let _ = (this, descriptor, initial); crate::exception::js_throw(wasm_unsupported_error( b"RuntimeError", "WebAssembly.Global", @@ -732,6 +734,7 @@ fn wasm_memory_new_buffer(pages: u32) -> f64 { extern "C" fn webassembly_memory_ctor_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, descriptor: f64, ) -> f64 { if !invoked_as_constructor(closure) { @@ -750,7 +753,7 @@ extern "C" fn webassembly_memory_ctor_thunk( }; // The dynamic construct path pre-allocated the receiver with // `Memory.prototype` linked (so `instanceof` works); fill it in place. - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this = f64::from_bits(this.bits()); #[cfg(feature = "wasm-host")] { crate::webassembly::js_webassembly_memory_new( @@ -859,9 +862,10 @@ fn wasm_memory_grow_on(this: f64, delta: f64) -> Result { extern "C" fn webassembly_memory_grow_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, delta: f64, ) -> f64 { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let this = f64::from_bits(this.bits()); match wasm_memory_grow_on(this, delta) { Ok(old_pages) => old_pages as f64, Err(MemoryCtorError::Type(msg)) => { @@ -879,6 +883,7 @@ extern "C" fn webassembly_memory_grow_thunk( extern "C" fn webassembly_compile_error_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { wasm_error_from_message_value(b"CompileError", message) @@ -886,6 +891,7 @@ extern "C" fn webassembly_compile_error_ctor_thunk( extern "C" fn webassembly_link_error_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { wasm_error_from_message_value(b"LinkError", message) @@ -893,6 +899,7 @@ extern "C" fn webassembly_link_error_ctor_thunk( extern "C" fn webassembly_runtime_error_ctor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, message: f64, ) -> f64 { wasm_error_from_message_value(b"RuntimeError", message) @@ -903,6 +910,7 @@ extern "C" fn webassembly_runtime_error_ctor_thunk( #[cfg(feature = "wasm-host")] extern "C" fn webassembly_module_exports_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, module: f64, ) -> f64 { crate::webassembly::js_webassembly_module_exports(module) @@ -911,6 +919,7 @@ extern "C" fn webassembly_module_exports_thunk( #[cfg(feature = "wasm-host")] extern "C" fn webassembly_module_imports_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, module: f64, ) -> f64 { crate::webassembly::js_webassembly_module_imports(module) @@ -919,6 +928,7 @@ extern "C" fn webassembly_module_imports_thunk( #[cfg(feature = "wasm-host")] extern "C" fn webassembly_module_custom_sections_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, module: f64, name: f64, ) -> f64 { @@ -930,6 +940,7 @@ extern "C" fn webassembly_module_custom_sections_thunk( #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_module_exports_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _module: f64, ) -> f64 { super::super::object_ops::throw_object_type_error( @@ -940,6 +951,7 @@ extern "C" fn webassembly_module_exports_thunk( #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_module_imports_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _module: f64, ) -> f64 { super::super::object_ops::throw_object_type_error( @@ -950,6 +962,7 @@ extern "C" fn webassembly_module_imports_thunk( #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_module_custom_sections_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _module: f64, _name: f64, ) -> f64 { @@ -1475,19 +1488,32 @@ mod tests { fn async_members_reject_with_compile_error() { let closure = std::ptr::null(); assert_rejected_with_compile_error( - webassembly_compile_thunk(closure, undefined()), + webassembly_compile_thunk(closure, crate::closure::JsThis::UNDEFINED, undefined()), "WebAssembly.compile", ); assert_rejected_with_compile_error( - webassembly_instantiate_thunk(closure, undefined(), undefined()), + webassembly_instantiate_thunk( + closure, + crate::closure::JsThis::UNDEFINED, + undefined(), + undefined(), + ), "WebAssembly.instantiate", ); assert_rejected_with_compile_error( - webassembly_compile_streaming_thunk(closure, undefined()), + webassembly_compile_streaming_thunk( + closure, + crate::closure::JsThis::UNDEFINED, + undefined(), + ), "WebAssembly.compileStreaming", ); assert_rejected_with_compile_error( - webassembly_instantiate_streaming_thunk(closure, undefined()), + webassembly_instantiate_streaming_thunk( + closure, + crate::closure::JsThis::UNDEFINED, + undefined(), + ), "WebAssembly.instantiateStreaming", ); } @@ -1495,7 +1521,11 @@ mod tests { #[cfg(not(feature = "wasm-host"))] #[test] fn validate_reports_false() { - let result = webassembly_validate_thunk(std::ptr::null(), undefined()); + let result = webassembly_validate_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + undefined(), + ); assert_eq!( result.to_bits(), crate::value::JSValue::bool(false).bits(), @@ -1506,17 +1536,29 @@ mod tests { #[test] fn error_constructors_build_branded_error_objects() { let message = string_value("boom"); - let compile_err = webassembly_compile_error_ctor_thunk(std::ptr::null(), message); + let compile_err = webassembly_compile_error_ctor_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + message, + ); assert_eq!(error_name_bytes(compile_err), b"CompileError".to_vec()); assert_eq!(error_message_string(compile_err), "boom"); assert_eq!(value_gc_type(compile_err), Some(crate::gc::GC_TYPE_ERROR)); // Message coercion mirrors `new Error(v)`. - let numbered = webassembly_link_error_ctor_thunk(std::ptr::null(), 42.0); + let numbered = webassembly_link_error_ctor_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + 42.0, + ); assert_eq!(error_name_bytes(numbered), b"LinkError".to_vec()); assert_eq!(error_message_string(numbered), "42"); - let bare = webassembly_runtime_error_ctor_thunk(std::ptr::null(), undefined()); + let bare = webassembly_runtime_error_ctor_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + undefined(), + ); assert_eq!(error_name_bytes(bare), b"RuntimeError".to_vec()); assert_eq!(error_message_string(bare), ""); } @@ -1528,8 +1570,16 @@ mod tests { let link_ctor = ns_field(ns, b"LinkError"); let runtime_ctor = ns_field(ns, b"RuntimeError"); - let compile_err = webassembly_compile_error_ctor_thunk(std::ptr::null(), string_value("x")); - let link_err = webassembly_link_error_ctor_thunk(std::ptr::null(), string_value("x")); + let compile_err = webassembly_compile_error_ctor_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + string_value("x"), + ); + let link_err = webassembly_link_error_ctor_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + string_value("x"), + ); assert_eq!( webassembly_error_ctor_instanceof(compile_err, compile_ctor), diff --git a/crates/perry-runtime/src/object/groupby.rs b/crates/perry-runtime/src/object/groupby.rs index 5cfdd22cd5..d93447135e 100644 --- a/crates/perry-runtime/src/object/groupby.rs +++ b/crates/perry-runtime/src/object/groupby.rs @@ -298,7 +298,12 @@ unsafe fn group_by_collect<'scope>( items.push(crate::array::js_array_get_f64(raw, i as u32)); let cb_ptr = crate::value::js_nanbox_get_pointer(callback_handle.get_nanbox_f64()) as *const crate::closure::ClosureHeader; - let key_val = crate::closure::js_closure_call2(cb_ptr, items.get(i), i as f64); + let key_val = crate::closure::js_closure_call2( + cb_ptr, + crate::closure::plain_call_receiver(), + items.get(i), + i as f64, + ); keys.push(key_val); } (keys, items) diff --git a/crates/perry-runtime/src/object/handle_expando.rs b/crates/perry-runtime/src/object/handle_expando.rs index 534eb97687..0e172a2c27 100644 --- a/crates/perry-runtime/src/object/handle_expando.rs +++ b/crates/perry-runtime/src/object/handle_expando.rs @@ -127,7 +127,10 @@ pub fn handle_expando_get(handle: i64, name: &str) -> Option { if closure.is_null() { return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); } - return Some(crate::closure::js_closure_call0(closure)); + return Some(crate::closure::js_closure_call0( + closure, + crate::closure::plain_call_receiver(), + )); } handle_expando_data_get(handle, name) } diff --git a/crates/perry-runtime/src/object/inherited_read_cache.rs b/crates/perry-runtime/src/object/inherited_read_cache.rs index 1190fb0be6..3a222712b8 100644 --- a/crates/perry-runtime/src/object/inherited_read_cache.rs +++ b/crates/perry-runtime/src/object/inherited_read_cache.rs @@ -692,7 +692,7 @@ unsafe fn accessor_hit(obj: *const ObjectHeader, pair_bits: u64) -> Lookup { if crate::object::prototype_chain::resolution_stack_savepoint() != 0 { return Lookup::Unknown; } - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(acc.raw_get); + let f = crate::closure::body_call::js_method_body_fn!(acc.raw_get as *const u8;); return Lookup::Hit(JSValue::from_bits(f(this).to_bits())); } if acc.get != 0 { @@ -890,7 +890,7 @@ unsafe fn accessor_set(obj: *const ObjectHeader, pair_bits: u64, value: f64) -> // A compiled class setter is called directly with the receiver as its // `this` parameter, exactly as the class-setter arm of the generic // `[[Set]]` calls it (that arm opens no resolution boundary either). - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(acc.raw_set); + let f = crate::closure::body_call::js_method_body_fn!(acc.raw_set as *const u8; value); let _ = f(this, value); return true; } diff --git a/crates/perry-runtime/src/object/inherited_read_cache_tests.rs b/crates/perry-runtime/src/object/inherited_read_cache_tests.rs index 96d6b2d1d9..8fdcf37f50 100644 --- a/crates/perry-runtime/src/object/inherited_read_cache_tests.rs +++ b/crates/perry-runtime/src/object/inherited_read_cache_tests.rs @@ -815,7 +815,10 @@ fn a_null_prototype_receiver_never_primes() { } } -extern "C" fn forty_two_getter(_c: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn forty_two_getter( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 42.0 } diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 4c4c13bb45..5ca1850f7d 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -458,12 +458,13 @@ fn throw_invalid_instanceof_rhs(type_ref: f64) -> ! { /// returns `false` when `this` is not callable, so `Function.prototype[Symbol /// .hasInstance].call(undefined, {})` is `false` (not a throw). Installed on /// `Function.prototype` under the `@@hasInstance` key; the receiver flows in -/// through `IMPLICIT_THIS` set by the `.call`/member dispatch. +/// as the `this` argument supplied by the `.call`/member dispatch. pub(crate) extern "C" fn function_prototype_has_instance_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let constructor = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let constructor = f64::from_bits(this.bits()); let result = ordinary_has_instance(constructor, value); f64::from_bits(if result { crate::value::TAG_TRUE @@ -574,7 +575,14 @@ fn dispatch_own_has_instance(cb: f64, value: f64) -> HasInstanceOutcome { throw_type_error(b"Symbol(Symbol.hasInstance) is not a function"); } let args = [value]; - let r = unsafe { crate::closure::js_native_call_value(cb, args.as_ptr(), 1) }; + let r = unsafe { + crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 1, + ) + }; HasInstanceOutcome::Result(if crate::value::js_is_truthy(r) != 0 { f64::from_bits(crate::value::TAG_TRUE) } else { diff --git a/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs b/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs index f0cc429c6e..629551586a 100644 --- a/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs +++ b/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs @@ -21,8 +21,6 @@ pub(super) fn proxy_instanceof(value: f64, constructor: f64) -> f64 { if !crate::proxy::is_callable_function(method.get_nanbox_f64()) { throw_type_error(b"Symbol(Symbol.hasInstance) is not a function"); } - let previous_this = - scope.root_nanbox_f64(js_implicit_this_set(constructor.get_nanbox_f64())); let called = crate::exception::catch_js_throw(|| unsafe { let args = [value.get_nanbox_f64()]; if crate::proxy::js_proxy_is_proxy(method.get_nanbox_f64()) != 0 { @@ -32,10 +30,14 @@ pub(super) fn proxy_instanceof(value: f64, constructor: f64) -> f64 { &args, ) } else { - crate::closure::js_native_call_value(method.get_nanbox_f64(), args.as_ptr(), 1) + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(constructor.get_nanbox_f64()), + args.as_ptr(), + 1, + ) } }); - js_implicit_this_set(previous_this.get_nanbox_f64()); match called { Ok(result) => crate::value::js_is_truthy(result) != 0, Err(error) => crate::exception::js_throw(error), diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs index 3f0ead8b1b..5d45725c28 100644 --- a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -66,7 +66,8 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { // function `__perry_wk_hasinstance_` and the LLVM backend registers a // pointer to it against the class id at module init. if let Some(func_ptr) = lookup_has_instance_hook(class_id) { - let hook: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(func_ptr as *const u8) }; + let hook = + unsafe { crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0) }; let result = hook(value); // Normalize: any truthy NaN-boxed bool stays as the TAG_TRUE/FALSE // sentinel. User-written `return typeof v === "number" && ...` diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index 6c197b35a1..0cfc095299 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -25,7 +25,7 @@ //! - `getOwnPropertyDescriptor(proto, "next")` reads the recorded builtin //! attrs off the prototype object (it's a regular `GC_TYPE_OBJECT` field). //! -//! The `next` closures are thin thunks: they read `js_implicit_this_get()` and +//! The `next` closures are thin thunks: they take the receiver as `this` and //! route by the receiver's class id to the existing //! `dispatch_{array,map,set,string}_iterator_method`. This is the ONLY behaviour //! addition for `proto.next.call(it)` / value-read `it.next()`; the class-id CALL @@ -164,11 +164,11 @@ pub(crate) fn note_iterator_prototype_exposed(value: f64) { } } -/// Resolve and validate the implicit-`this` object shared by the family +/// Resolve and validate the `this` object shared by the family /// prototype thunks. Keeping the raw-address probe here gives both the generic /// family dispatcher and the helper-specific brand check one audited path. -unsafe fn implicit_this_iterator_object() -> Option<*mut ObjectHeader> { - let this = super::js_implicit_this_get(); +unsafe fn this_iterator_object(this: crate::closure::JsThis) -> Option<*mut ObjectHeader> { + let this = this.as_f64(); let jv = JSValue::from_bits(this.to_bits()); if !jv.is_pointer() { return None; @@ -180,14 +180,14 @@ unsafe fn implicit_this_iterator_object() -> Option<*mut ObjectHeader> { Some(obj) } -/// Dispatch `method` on the implicit-`this` iterator instance, routing by class +/// Dispatch `method` on the `this` iterator instance, routing by class /// id to the matching existing iterator dispatcher. Shared by the per-family /// `next` thunks (read as a value or invoked via `.call`) and the parent /// `[Symbol.iterator]` thunk. Returns a `{ value:undefined, done:true }`-ish /// throw when `this` is not a recognized iterator (test262 `this-not-object` / /// `does-not-have-...-internal-slots` brand checks). -unsafe fn dispatch_on_implicit_this(method: &str) -> f64 { - let Some(obj) = implicit_this_iterator_object() else { +unsafe fn dispatch_on_this(this: crate::closure::JsThis, method: &str) -> f64 { + let Some(obj) = this_iterator_object(this) else { return brand_type_error(method); }; let class_id = (*obj).class_id; @@ -228,38 +228,49 @@ fn brand_type_error(method: &str) -> f64 { crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } -// --- `next` thunks, one per family (all read implicit-this, dispatch by id) --- +// --- `next` thunks, one per family (all take `this`, dispatch by id) --- extern "C" fn array_iterator_next_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { - unsafe { dispatch_on_implicit_this("next") } + unsafe { dispatch_on_this(this, "next") } } -extern "C" fn map_iterator_next_thunk(_c: *const crate::closure::ClosureHeader, _arg: f64) -> f64 { - unsafe { dispatch_on_implicit_this("next") } +extern "C" fn map_iterator_next_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _arg: f64, +) -> f64 { + unsafe { dispatch_on_this(this, "next") } } -extern "C" fn set_iterator_next_thunk(_c: *const crate::closure::ClosureHeader, _arg: f64) -> f64 { - unsafe { dispatch_on_implicit_this("next") } +extern "C" fn set_iterator_next_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + _arg: f64, +) -> f64 { + unsafe { dispatch_on_this(this, "next") } } extern "C" fn string_iterator_next_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { - unsafe { dispatch_on_implicit_this("next") } + unsafe { dispatch_on_this(this, "next") } } extern "C" fn regexp_string_iterator_next_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { unsafe { - let Some(obj) = implicit_this_iterator_object() else { + let Some(obj) = this_iterator_object(this) else { return brand_type_error("next"); }; if (*obj).class_id != crate::regex::REGEXP_STRING_ITERATOR_CLASS_ID { return brand_type_error("next"); } - dispatch_on_implicit_this("next") + dispatch_on_this(this, "next") } } @@ -268,10 +279,11 @@ extern "C" fn regexp_string_iterator_next_thunk( /// a saved/bound canonical method must not re-enter a later `next` override. extern "C" fn iterator_helper_next_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { unsafe { - let Some(obj) = implicit_this_iterator_object() else { + let Some(obj) = this_iterator_object(this) else { return brand_type_error("next"); }; if (*obj).class_id != crate::iterator_helpers::ITERATOR_HELPER_CLASS_ID { @@ -284,9 +296,10 @@ extern "C" fn iterator_helper_next_thunk( /// `%IteratorPrototype%[Symbol.iterator]()` returns `this` (the iterator). extern "C" fn iterator_proto_symbol_iterator_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { - super::js_implicit_this_get() + this.as_f64() } /// Set `obj[Symbol.toStringTag] = tag` with the spec descriptor @@ -317,7 +330,7 @@ fn set_to_string_tag(obj: *mut ObjectHeader, tag: &str) { /// appeared user-reparented. A caller treating that as "the per-instance chain /// is authoritative, resolve methods by ordinary inheriting lookup" then /// reached the `%…IteratorPrototype%` `next` THUNK, -/// which resolves its receiver from `js_implicit_this_get()` rather than the +/// which resolves its receiver from the call-site `this` rather than the /// bound `this` (#7576) — producing `Method %IteratorPrototype%.next called on /// incompatible receiver`. The prototype itself is still recorded either way; /// the class-default variant also avoids the divergence bit and cache flushes. @@ -404,7 +417,7 @@ fn install_symbol_iterator(shared: *mut ObjectHeader) { /// Allocate one family prototype with an own `next` method (spec descriptor), /// a `[Symbol.toStringTag]`, and `[[Prototype]] === shared %IteratorPrototype%`. fn build_family_proto( - next_thunk: extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64, + next_thunk: crate::closure::body_call::js_body_fn_ty!(a), tag: &str, shared: *mut ObjectHeader, ) -> *mut ObjectHeader { @@ -517,7 +530,6 @@ pub(crate) unsafe fn call_overridden_iterator_next( ) -> Option { let scope = crate::gc::RuntimeHandleScope::new(); let iter = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(iter_obj as i64)); - let previous = scope.root_nanbox_f64(super::js_implicit_this_get()); // #9019: an OWN `next` (`it.next = fn`, stored past the reserved floor // by `object/reserved_floor.rs`) shadows the prototype thunk and exists // independently of the tower, so probe it BEFORE the tower-null @@ -551,11 +563,14 @@ pub(crate) unsafe fn call_overridden_iterator_next( crate::closure::throw_not_callable(); } let method = scope.root_nanbox_f64(own); - super::js_implicit_this_set(iter.get_nanbox_f64()); let result = crate::exception::js_call_catching(|| { - crate::closure::js_native_call_value(method.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(iter.get_nanbox_f64()), + std::ptr::null(), + 0, + ) }); - super::js_implicit_this_set(previous.get_nanbox_f64()); return match result { Ok(value) => Some(value), Err(error) => crate::exception::js_throw(error), @@ -634,11 +649,14 @@ pub(crate) unsafe fn call_overridden_iterator_next( } let method = scope.root_nanbox_f64(method); - super::js_implicit_this_set(iter.get_nanbox_f64()); let result = crate::exception::js_call_catching(|| { - crate::closure::js_native_call_value(method.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(iter.get_nanbox_f64()), + std::ptr::null(), + 0, + ) }); - super::js_implicit_this_set(previous.get_nanbox_f64()); match result { Ok(value) => Some(value), Err(error) => crate::exception::js_throw(error), @@ -682,11 +700,17 @@ mod override_probe_allocation_tests { const PATCHED_SENTINEL: f64 = 4242.0; - extern "C" fn patched_next_thunk(_closure: *const ClosureHeader) -> f64 { + extern "C" fn patched_next_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { PATCHED_SENTINEL } - extern "C" fn accessor_getter_thunk(_closure: *const ClosureHeader) -> f64 { + extern "C" fn accessor_getter_thunk( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { f64::from_bits(TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/object/literal_constructor.rs b/crates/perry-runtime/src/object/literal_constructor.rs index 81a9fd10ad..3af5cffeb5 100644 --- a/crates/perry-runtime/src/object/literal_constructor.rs +++ b/crates/perry-runtime/src/object/literal_constructor.rs @@ -43,7 +43,11 @@ pub extern "C" fn js_literal_shape_initialize( mod tests { use super::*; - extern "C" fn collecting_setter(_closure: *const crate::ClosureHeader, _value: f64) -> f64 { + extern "C" fn collecting_setter( + _closure: *const crate::ClosureHeader, + _this: crate::closure::JsThis, + _value: f64, + ) -> f64 { crate::gc::gc_collect_minor(); f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 17a5ab35e5..47396062c7 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -370,16 +370,14 @@ pub(crate) use this_binding::js_derived_super_scope_push; pub(crate) use this_binding::SuperNewTargetScope; pub(crate) use this_binding::{ derived_super_binding_stack_restore, derived_super_binding_stack_savepoint, - implicit_this_trap_restore, implicit_this_trap_savepoint, new_target_trap_restore, - new_target_trap_savepoint, scan_implicit_this_roots_mut, static_private_owner_current, - static_private_owner_pop, static_private_owner_push, static_private_owner_stack_restore, - static_private_owner_stack_savepoint, static_this_arm, static_this_arm_if_unarmed, - static_this_disarm, IMPLICIT_THIS, + new_target_trap_restore, new_target_trap_savepoint, scan_dispatch_binding_roots_mut, + static_private_owner_current, static_private_owner_pop, static_private_owner_push, + static_private_owner_stack_restore, static_private_owner_stack_savepoint, static_this_arm, + static_this_arm_if_unarmed, static_this_disarm, }; pub use this_binding::{ - js_implicit_this_get, js_implicit_this_get_sloppy, js_implicit_this_set, js_new_target_get, - js_new_target_set, js_static_this_arm_classref, js_static_this_arm_value, - js_static_this_resolve, js_static_this_resolve_class, ImplicitThisScope, + js_new_target_get, js_new_target_set, js_static_this_arm_classref, js_static_this_arm_value, + js_static_this_resolve, js_static_this_resolve_class, js_this_coerce_sloppy, }; pub use to_string_tag::js_object_to_string; pub(crate) use to_string_tag::typed_array_to_string_tag_name; diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 2bb9a53831..d51fe6c8b5 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -361,10 +361,12 @@ unsafe fn call_primitive_closure_value( this_h.get_nanbox_f64(), ); let bound_h = scope.root_nanbox_u64(bound); - let prev_this = crate::object::js_implicit_this_set(this_h.get_nanbox_f64()); - let prev_this_h = scope.root_nanbox_f64(prev_this); - let result = crate::closure::js_native_call_value(bound_h.get_nanbox_f64(), args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this_h.get_nanbox_f64()); + let result = crate::closure::native_call_value_this( + bound_h.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), + args_ptr, + args_len, + ); Some(result) } @@ -738,18 +740,13 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if let Some((func_ptr, param_count, has_rest)) = lookup_class_symbol_method_in_chain(class_id, sym_key, true) { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); - let result = call_registered_static_method( + return call_registered_static_method( func_ptr, args_ptr, args_len, param_count, has_rest, ); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - return result; } } } else if is_class_object_value(object) { @@ -758,18 +755,13 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if let Some((func_ptr, param_count, has_rest)) = lookup_class_symbol_method_in_chain(class_id, sym_key, true) { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); - let result = call_registered_static_method( + return call_registered_static_method( func_ptr, args_ptr, args_len, param_count, has_rest, ); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - return result; } } else if key_jsval.is_pointer() || JSValue::from_bits(bits).is_pointer() { let obj_val = JSValue::from_bits(bits); @@ -826,15 +818,12 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( crate::object::js_object_get_index_polymorphic(object.to_bits() as i64, index); let fv = JSValue::from_bits(field.to_bits()); if !fv.is_undefined() && !fv.is_null() { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object.to_bits()))); - let result = crate::closure::js_native_call_value(field, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - return result; + return crate::closure::native_call_value_this( + field, + crate::closure::JsThis::from_f64(object), + args_ptr, + args_len, + ); } } } @@ -915,9 +904,8 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( } } - // Non-string key: read the property value, then invoke it with `this` - // bound to the receiver (the codegen `Expr::This` fallback reads - // `IMPLICIT_THIS` when there's no lexical `this`). + // Non-string key: read the property value, then invoke it with the + // receiver as its `this`. let field = if is_symbol_key { crate::symbol::js_object_get_symbol_property(object, key) } else { @@ -935,7 +923,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( // `symbol.rs::js_object_set_symbol_method` / // `dynamic_props.rs::clone_closure_rebind_this`). So when `o = Object.create(P)` // resolves `o[SYM]()`, the closure we get back carries `this === P`, not - // `this === o`, and `IMPLICIT_THIS` alone can't override the baked-in slot. + // `this === o`, and the receiver argument alone can't override the baked-in slot. // When the symbol method is NOT an OWN property of the receiver (i.e. it was // inherited through the prototype chain), rebind its `this` slot to the // receiver before invoking. `clone_closure_rebind_this` is a no-op for @@ -952,16 +940,12 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( field }; - // #8495: the displaced receiver must be ROOTED across the call. The - // `replace` has already overwritten the cell, so this is the only copy the - // frame holds; an evacuating collection inside the callee moves it and the - // restore would publish a pre-move address back INTO the scanned cell. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = - prev_this_scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object.to_bits()))); - let result = crate::closure::js_native_call_value(field, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - result + crate::closure::native_call_value_this( + field, + crate::closure::JsThis::from_f64(object), + args_ptr, + args_len, + ) } /// every regular + spread arg already concatenated by codegen), materialises @@ -1411,20 +1395,17 @@ pub unsafe extern "C-unwind" fn js_native_call_method( // `clone_closure_rebind_this` only rewrites a closure that // carries CAPTURES_THIS_FLAG; it returns a native builtin // (and an arrow, and a generator step) UNCHANGED. Native - // method bodies read their receiver from - // `js_implicit_this_get()` — the same #7576 property that - // made the `%IteratorPrototype%.next` THUNK throw — so - // without binding it here `Object.prototype.isPrototypeOf` - // saw no `this` ("called on null or undefined") and + // method bodies take their receiver as the `this` + // parameter — the same #7576 property that made the + // `%IteratorPrototype%.next` THUNK throw — so without + // passing it here `Object.prototype.isPrototypeOf` saw no + // `this` ("called on null or undefined") and // `Object(true).valueOf()` saw the wrong one ("called on - // incompatible receiver"). #9244. Restored on the way out, - // including when the callee throws — from a ROOT: the - // displaced `this` is the caller's receiver and the callee - // is user code that can move it (#10490). The receiver is - // re-read here, after the clone above allocated. - let _this_scope = crate::object::ImplicitThisScope::bind(&root_scope, object()); - return crate::closure::js_native_call_value( + // incompatible receiver"). #9244. The receiver is re-read + // from its root here, after the clone above allocated. + return crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object()), args.as_ptr(), args.len(), ); @@ -1774,17 +1755,11 @@ pub unsafe extern "C-unwind" fn js_native_call_method( // `closure_get_dynamic_prop` may return a method read off the // closure's `Object.setPrototypeOf` proto, whose bound `this` // (an object-literal method binds the literal) would otherwise - // win over IMPLICIT_THIS and leave `this` as the PROTO. + // win over the receiver argument and leave `this` as the PROTO. let bound = crate::closure::clone_closure_rebind_this( dyn_val.get_nanbox_u64(), f64::from_bits(object().to_bits()), ); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object().to_bits()))); // #7803: `clone_closure_rebind_this` above ALLOCATES, so the // caller's raw `args_ptr` buffer holds pre-move addresses from // here on. `arg_handles` is what the collector rewrites; the @@ -1799,13 +1774,13 @@ pub unsafe extern "C-unwind" fn js_native_call_method( &call_args, ) } else { - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object()), call_args.as_ptr(), call_args.len(), ) }; - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return result; } // #10423: `fn.constructor(p, body)` with no own `constructor` calls @@ -1824,6 +1799,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method( let call_args = refreshed_args(); return crate::closure::js_native_call_value( ctor.get_nanbox_f64(), + crate::closure::plain_call_receiver(), call_args.as_ptr(), call_args.len(), ); @@ -1861,28 +1837,25 @@ pub unsafe extern "C-unwind" fn js_native_call_method( let getter = (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !getter.is_null() { - // #8495: root the displaced receiver across the call below. - let prev_getter_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_getter_this_h = prev_getter_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object().to_bits()))); - let method_fn = crate::closure::js_closure_call0(getter); + let method_fn = crate::closure::js_closure_call0( + getter, + crate::closure::JsThis::from_f64(object()), + ); let bound = crate::closure::clone_closure_rebind_this( method_fn.to_bits(), object(), ); - IMPLICIT_THIS.with(|c| c.set(object().to_bits())); // #7803: two collection points above this line — the - // getter is USER CODE (`js_closure_call0`) and the - // rebind allocates — so the caller's raw buffer is - // stale. Re-read the rooted arguments. + // getter is USER CODE and the rebind allocates — so + // the caller's raw buffer is stale. Re-read the rooted + // arguments. let call_args = refreshed_args(); - let result = crate::closure::js_native_call_value( + return crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object()), call_args.as_ptr(), call_args.len(), ); - IMPLICIT_THIS.with(|c| c.set(prev_getter_this_h.get_nanbox_u64())); - return result; } } } @@ -1957,18 +1930,10 @@ pub unsafe extern "C-unwind" fn js_native_call_method( crate::proxy::js_proxy_get(object_handle.get_nanbox_f64(), key_handle.get_nanbox_f64()); let method_handle = root_scope.root_nanbox_f64(method_value); let args = refreshed_args(); - // Bind `this` to the proxy for the duration of the call, matching the - // receiver semantics of a normal `obj.method(args)` invocation. A - // canonical class-method value reads its receiver from IMPLICIT_THIS via - // `canonical_bound_method_receiver` (#6699 routes a proxy receiver - // through there). - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope.root_nanbox_u64( - IMPLICIT_THIS.with(|c| c.replace(object_handle.get_nanbox_f64().to_bits())), - ); + // The proxy is the call's receiver, matching the receiver semantics + // of a normal `obj.method(args)` invocation. A canonical class-method + // value takes it via `canonical_bound_method_receiver` (#6699 routes a + // proxy receiver through there). let proxy_class_id = crate::proxy::proxy_target_class_id(object_handle.get_nanbox_f64()); let is_static_class_method = proxy_class_id.is_some_and(|class_id| { crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) @@ -1977,15 +1942,15 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if is_static_class_method { crate::object::static_this_arm(object_handle.get_nanbox_f64()); } - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( method_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args.as_ptr(), args.len(), ); if is_static_class_method { crate::object::static_this_disarm(); } - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return result; } @@ -2146,10 +2111,10 @@ pub unsafe extern "C-unwind" fn js_native_call_method( // `Object.setPrototypeOf` chain, so `dyn_val` may be a method // read off the PROTO object — and an object-literal method // carries a bound `this` (the literal). A bound `this` wins over - // IMPLICIT_THIS, so setting IMPLICIT_THIS alone left `this` as - // the PROTO instead of the receiver. Rebind to the receiver, - // exactly as the ObjectHeader arm does for its inherited-field - // dispatch (`clone_closure_rebind_this` + IMPLICIT_THIS) — that + // the receiver argument, so passing the receiver alone left + // `this` as the PROTO. Rebind to the receiver, exactly as the + // ObjectHeader arm does for its inherited-field dispatch + // (`clone_closure_rebind_this` + the receiver argument) — that // asymmetry is why a plain-object receiver worked and a FUNCTION // receiver did not. // @@ -2170,16 +2135,13 @@ pub unsafe extern "C-unwind" fn js_native_call_method( dyn_val.to_bits(), f64::from_bits(recv_bits), ); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = - prev_this_scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(recv_bits))); - let result = - crate::closure::js_native_call_value(f64::from_bits(bound), args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - return result; + // The clone above allocated: re-read the receiver from its root. + return crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(object()), + args_ptr, + args_len, + ); } // Nothing on the function's own properties or its prototype chain // answers `method_name` (the Function.prototype and Object.prototype @@ -2343,18 +2305,12 @@ pub unsafe extern "C-unwind" fn js_native_call_method( field_val.bits(), f64::from_bits(jsval().bits()), ); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(jsval().bits()))); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object()), args_ptr, args_len, ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return result; } } @@ -2401,18 +2357,12 @@ pub unsafe extern "C-unwind" fn js_native_call_method( field_val.bits(), f64::from_bits(jsval().bits()), ); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(jsval().bits()))); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object()), args_ptr, args_len, ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return result; } } @@ -2658,7 +2608,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method( // `class X extends Promise`: inherited `then`/`catch`/`finally` dispatch // against the hidden backing Promise cell. A subclass override (own field / // vtable / prototype method) has already been consulted above, so only a - // genuinely inherited builtin reaches here. Bind `this` to the instance so + // genuinely inherited builtin reaches here. Pass the instance as `this` so // the reified thunk unwraps the backing cell and species-chains via // `receiver.constructor`. (Covers the `X.resolve().finally().then()` chains // that codegen dispatches straight through `js_native_call_method`.) @@ -2666,11 +2616,12 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if crate::promise::subclass_backing_promise(object()).is_some() { if let Some(m) = crate::promise::promise_proto_method(method_name) { let args = refreshed_args(); - let prev_this = - root_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); - let result = crate::closure::js_native_call_value(m, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - return result; + return crate::closure::native_call_value_this( + m, + crate::closure::JsThis::from_f64(object()), + args.as_ptr(), + args.len(), + ); } } } @@ -2731,22 +2682,19 @@ pub unsafe extern "C-unwind" fn js_native_call_method( // objects store dynamic props in the exotic-expando side table, not the // ObjectHeader field map, so the field/vtable/prototype dispatch above // never sees them. Look the name up there; if it is a callable, invoke it - // with the receiver bound as `this` (via IMPLICIT_THIS, matching the - // closure-field dispatch path above). + // with the receiver as `this` (matching the closure-field dispatch path + // above). if jsval().is_pointer() { if let Some((addr, kind)) = super::exotic_expando::exotic_expando_kind_of_value(object()) { if let Some(bits) = super::exotic_expando::value_lookup(kind, addr, method_name) { let candidate = f64::from_bits(bits); if crate::collection_iter::is_callable(candidate) { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object().to_bits()))); - let result = - crate::closure::js_native_call_value(candidate, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + let result = crate::closure::native_call_value_this( + candidate, + crate::closure::JsThis::from_f64(object()), + args_ptr, + args_len, + ); return result; } } @@ -2775,7 +2723,12 @@ pub unsafe extern "C-unwind" fn js_native_call_method( crate::event_target::event_target_method_bind(recv, method_name.as_bytes()) { let args = refreshed_args(); - return crate::closure::js_native_call_value(bound, args.as_ptr(), args.len()); + return crate::closure::js_native_call_value( + bound, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } } } @@ -2820,16 +2773,15 @@ pub unsafe extern "C-unwind" fn js_native_call_method( let got = super::field_get_set::js_object_get_field_by_name(recv, accessor_key); let candidate = f64::from_bits(got.bits()); if crate::collection_iter::is_callable(candidate) { - // Root the displaced receiver across the call: the replace has - // already overwritten the cell, so this is the frame's only copy - // (#8495, same as the exotic-expando arm above). - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(receiver.to_bits()))); let args = refreshed_args(); - let result = - crate::closure::js_native_call_value(candidate, args.as_ptr(), args.len()); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + let result = crate::closure::native_call_value_this( + candidate, + // Re-read: the key allocation and the getter can + // move the receiver. + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ); return result; } } diff --git a/crates/perry-runtime/src/object/native_call_method/closure_override_tests.rs b/crates/perry-runtime/src/object/native_call_method/closure_override_tests.rs index c499c69119..85b26c8cc2 100644 --- a/crates/perry-runtime/src/object/native_call_method/closure_override_tests.rs +++ b/crates/perry-runtime/src/object/native_call_method/closure_override_tests.rs @@ -1,12 +1,20 @@ //! #10045: own callable properties must beat Function.prototype fast paths. use crate::{closure, gc::RuntimeHandleScope, value}; -extern "C" fn original(_closure: *const closure::ClosureHeader, _arg: f64) -> f64 { +extern "C" fn original( + _closure: *const closure::ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { -1.0 } -extern "C" fn own_method(_closure: *const closure::ClosureHeader, _arg: f64) -> f64 { - crate::object::js_implicit_this_get() +extern "C" fn own_method( + _closure: *const closure::ClosureHeader, + this: crate::closure::JsThis, + _arg: f64, +) -> f64 { + this.as_f64() } fn check_own_method_dispatch(proxy: bool) { diff --git a/crates/perry-runtime/src/object/native_call_method/code_point_at_dispatch_tests.rs b/crates/perry-runtime/src/object/native_call_method/code_point_at_dispatch_tests.rs index 76eb6d9251..e499930d35 100644 --- a/crates/perry-runtime/src/object/native_call_method/code_point_at_dispatch_tests.rs +++ b/crates/perry-runtime/src/object/native_call_method/code_point_at_dispatch_tests.rs @@ -121,7 +121,10 @@ fn builtin_callee_gets_the_primitive_receiver_and_a_user_callee_the_wrapper() { "a built-in callee must receive the primitive, not a ToObject wrapper" ); - extern "C" fn sloppy_user_body(_c: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn sloppy_user_body( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { 0.0 } let user = crate::closure::js_closure_alloc(sloppy_user_body as *const u8, 0); diff --git a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs index fca8c802f2..85c214e4bb 100644 --- a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs @@ -454,6 +454,7 @@ pub(super) unsafe fn dispatch_raw_pointer( if field_val.is_pointer() { return Some(crate::closure::js_native_call_value( f64::from_bits(field_val.bits()), + crate::closure::plain_call_receiver(), args_ptr, args_len, )); diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs index 3dbf6baaee..eec74a29b6 100644 --- a/crates/perry-runtime/src/object/native_call_method/common_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -775,16 +775,14 @@ pub(crate) unsafe fn dispatch_function_proto_method( ) { return Some(result); } - // The callee, the explicit `this`, and the saved previous - // implicit-`this` all cross the invocation — a moving + // The callee and the explicit `this` both cross the + // invocation — a moving // collection inside the callee relocates them (#8082: the // forced gate faulted reading the stale callee closure in // `maybe_alias_explicit_this_construction` after the call). let scope = crate::gc::RuntimeHandleScope::new(); let callee_h = scope.root_nanbox_f64(object); let this_h = scope.root_nanbox_f64(this_arg); - let prev_this_h = - scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits()))); // Static bound-method value (`C.m.call(x)`): arm the one-shot // static-`this` override so the method body sees `x` instead // of the lexical class-ref (static private brand checks). @@ -793,17 +791,21 @@ pub(crate) unsafe fn dispatch_function_proto_method( super::static_this_arm(this_arg); } // A concise/object-literal method reads `this` from a baked - // capture slot, not IMPLICIT_THIS; rebind to the explicit + // capture slot, not the `this` argument; rebind to the explicit // `.call(thisArg)` receiver (no-op for arrows / plain fns). let call_target = crate::closure::rebind_explicit_this( callee_h.get_nanbox_f64(), this_h.get_nanbox_f64(), ); - let result = crate::closure::js_native_call_value(call_target, rest_ptr, rest_len); + let result = crate::closure::native_call_value_this( + call_target, + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), + rest_ptr, + rest_len, + ); if static_target { super::static_this_disarm(); } - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); // #4973: `http.Server.call(this, handler)` — the inherits // pattern. Alias the explicit `this` object to the handle the // native class export constructed. @@ -941,14 +943,11 @@ pub(crate) unsafe fn dispatch_function_proto_method( ) { return Some(result); } - // Same rooting discipline as the `call` arm (#8082): callee, - // explicit `this`, and the saved implicit-`this` cross the - // invocation and must survive a moving collection inside it. + // Same rooting discipline as the `call` arm (#8082): callee + // and explicit `this` cross the invocation and must survive a moving collection inside it. let scope = crate::gc::RuntimeHandleScope::new(); let callee_h = scope.root_nanbox_f64(object); let this_h = scope.root_nanbox_f64(this_arg); - let prev_this_h = - scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(this_arg.to_bits()))); // Static bound-method value — see the matching `call` arm. let static_target = super::native_module::is_static_bound_method_value(object); if static_target { @@ -961,15 +960,15 @@ pub(crate) unsafe fn dispatch_function_proto_method( callee_h.get_nanbox_f64(), this_h.get_nanbox_f64(), ); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( apply_target, + crate::closure::JsThis::from_f64(this_h.get_nanbox_f64()), call_args_ptr, call_args_len, ); if static_target { super::static_this_disarm(); } - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); // #4973: `http.Server.apply(this, args)` — same inherits // pattern as the `call` arm above. super::native_this_alias::maybe_alias_explicit_this_construction( diff --git a/crates/perry-runtime/src/object/native_call_method/dispatch_arg_coercion_tests.rs b/crates/perry-runtime/src/object/native_call_method/dispatch_arg_coercion_tests.rs index e827e90e19..b18754450b 100644 --- a/crates/perry-runtime/src/object/native_call_method/dispatch_arg_coercion_tests.rs +++ b/crates/perry-runtime/src/object/native_call_method/dispatch_arg_coercion_tests.rs @@ -103,6 +103,7 @@ fn pad_start_coerces_target_and_defaults_the_fill() { extern "C" fn undef_replacer( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _matched: f64, _offset: f64, _whole: f64, diff --git a/crates/perry-runtime/src/object/native_call_method/disposal.rs b/crates/perry-runtime/src/object/native_call_method/disposal.rs index b4ea3c8526..29649803da 100644 --- a/crates/perry-runtime/src/object/native_call_method/disposal.rs +++ b/crates/perry-runtime/src/object/native_call_method/disposal.rs @@ -14,6 +14,9 @@ pub(super) unsafe fn try_symbol_dispose_dispatch( // Only real heap objects store symbol-keyed methods. Native handles and // primitives return None here and fall through to the existing dispatch. let _obj = object_ptr_from_value(object)?; + // The symbol read can run a getter; the receiver is re-read from this root. + let scope = crate::gc::RuntimeHandleScope::new(); + let object_handle = scope.root_nanbox_f64(object); let want_async = method_name == "__perry_async_dispose__"; let shorts: &[&str] = if want_async { &["asyncDispose", "dispose"] @@ -26,18 +29,16 @@ pub(super) unsafe fn try_symbol_dispose_dispatch( continue; } let sym_f64 = f64::from_bits(JSValue::pointer(sym as *const u8).bits()); - let method = crate::symbol::js_object_get_symbol_property(object, sym_f64); + let method = + crate::symbol::js_object_get_symbol_property(object_handle.get_nanbox_f64(), sym_f64); let mjsv = JSValue::from_bits(method.to_bits()); if method.to_bits() != crate::value::TAG_UNDEFINED && !mjsv.is_null() && mjsv.is_pointer() { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_scope = crate::gc::RuntimeHandleScope::new(); - let prev_h = - prev_scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object.to_bits()))); - let result = crate::closure::js_native_call_value(method, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_h.get_nanbox_u64())); - return Some(result); + return Some(crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args_ptr, + args_len, + )); } } None diff --git a/crates/perry-runtime/src/object/native_call_method/function_shape.rs b/crates/perry-runtime/src/object/native_call_method/function_shape.rs index 10ac520e5f..38c3c9186f 100644 --- a/crates/perry-runtime/src/object/native_call_method/function_shape.rs +++ b/crates/perry-runtime/src/object/native_call_method/function_shape.rs @@ -152,14 +152,14 @@ unsafe fn dictionary_function_proto_method_call( } // A user callable inherited from the recorded prototype: an ordinary // method call with the function as `this`. - let prev_this_h = scope.root_nanbox_u64( - super::IMPLICIT_THIS.with(|c| c.replace(receiver_h.get_nanbox_f64().to_bits())), - ); let callee = crate::closure::rebind_explicit_this(value_h.get_nanbox_f64(), receiver_h.get_nanbox_f64()); - let result = crate::closure::js_native_call_value(callee, args_ptr, args_len); - super::IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - Some(result) + Some(crate::closure::native_call_value_this( + callee, + crate::closure::JsThis::from_f64(receiver_h.get_nanbox_f64()), + args_ptr, + args_len, + )) } #[cfg(test)] @@ -174,7 +174,7 @@ mod tests { use super::*; use std::sync::atomic::Ordering; - extern "C" fn target_body(_c: *const ClosureHeader) -> f64 { + extern "C" fn target_body(_c: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { 42.0 } @@ -219,7 +219,10 @@ mod tests { (*bound_ptr).func_ptr, crate::closure::BOUND_FUNCTION_FUNC_PTR ); - assert_eq!(crate::closure::js_closure_call0(bound_ptr), 42.0); + assert_eq!( + crate::closure::js_closure_call0(bound_ptr, crate::closure::plain_call_receiver()), + 42.0 + ); crate::closure::shape::FUNCTION_PROTOTYPE_PTR.store(saved, Ordering::Release); } } diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index c59e7f2dea..11479c3988 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -39,17 +39,18 @@ unsafe fn dispatch_handle_proto_method( } // An inherited prototype method may be a closure that BAKED `this` into a // capture slot at definition time (object-literal methods are lowered with - // `captures_this`). Setting IMPLICIT_THIS alone can't override that slot, so + // `captures_this`). Passing `this` alone can't override that slot, so // rebind the closure's `this` to the handle receiver first — // `clone_closure_rebind_this` is a no-op for closures that don't capture // `this` and for non-closure values. Mirrors the class-prototype fallback. let _ = closure_ptr; let bound = crate::closure::clone_closure_rebind_this(resolved_bits, object); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); - let result = crate::closure::js_native_call_value(f64::from_bits(bound), args_ptr, args_len); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - Some(result) + Some(crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(object), + args_ptr, + args_len, + )) } pub(super) unsafe fn dispatch_handle( @@ -215,16 +216,12 @@ pub(super) unsafe fn dispatch_handle( { let stored_ptr = crate::value::js_nanbox_get_pointer(stored) as usize; if crate::closure::is_closure_ptr(stored_ptr) { - let recv_bits = jsval.bits(); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(recv_bits))); - let result = - crate::closure::js_native_call_value(stored, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + let result = crate::closure::native_call_value_this( + stored, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); return Some(result); } } @@ -972,30 +969,24 @@ pub(super) unsafe fn dispatch_handle( // (issue #87). // // Issue #519: bind `this` to the receiver - // for the duration of the call. Non-arrow - // function bodies read `this` from - // IMPLICIT_THIS (codegen Expr::This - // fallback when this_stack is empty); - // without this save/set/restore, the - // body sees `this = undefined` and any + // for the call. Non-arrow function bodies + // read `this` from their `this` argument + // (codegen Expr::This fallback when + // this_stack is empty); without passing + // the receiver, the body sees `this = undefined` and any // `this.foo()` call falls through to the // issue #510 catch-all "(undefined).foo // is not a function" TypeError. Hono's // RegExpRouter.match (imported function // assigned as a class field) hit this. - let recv_bits = jsval.bits(); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(recv_bits))); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( f64::from_bits(field_val.bits()), + crate::closure::JsThis::from_f64( + object_handle.get_nanbox_f64(), + ), args_ptr, args_len, ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } } @@ -1152,18 +1143,14 @@ pub(super) unsafe fn dispatch_handle( // Mirrors `resolve_proto_chain_field_with_receiver` // (the winston `get transports()` fix). let receiver_f64 = f64::from_bits(jsval.bits()); - // #8495: root the displaced receiver across the call below. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope.root_nanbox_u64( - IMPLICIT_THIS.with(|c| c.replace(receiver_f64.to_bits())), - ); // #10490: the displaced accessor receiver rides - // through the same getter call — root it too. + // through the getter call — root it. + let override_scope = crate::gc::RuntimeHandleScope::new(); let prev_override = super::super::field_get_set::accessor_receiver_override_begin( receiver_f64, ) - .map(|value| prev_this_scope.root_nanbox_f64(value)); + .map(|value| override_scope.root_nanbox_f64(value)); let field_val = js_object_get_field_by_name( proto_obj as *const _, method_key as *const crate::StringHeader, @@ -1171,7 +1158,6 @@ pub(super) unsafe fn dispatch_handle( super::super::field_get_set::accessor_receiver_override_end( prev_override.map(|handle| handle.get_nanbox_f64()), ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); if !field_val.is_undefined() && !field_val.is_null() { resolved_method = Some(ResolvedMethod::ProtoClosure { field_bits: field_val.bits(), @@ -1218,18 +1204,12 @@ pub(super) unsafe fn dispatch_handle( field_bits, f64::from_bits(jsval.bits()), ); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(jsval.bits()))); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args_ptr, args_len, ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } None => {} @@ -1273,7 +1253,7 @@ pub(super) unsafe fn dispatch_handle( // `symbol.rs::js_object_set_symbol_method`). So when // `o = Object.create(P)` resolves `o.method()`, the // closure carries `this === P`, not `this === o`, and - // setting `IMPLICIT_THIS = o` can't override the + // passing `this = o` can't override the // baked-in slot that the body reads. Rebind the slot // to the receiver before invoking. This mirrors the // symbol-keyed fix (#1969) for the string-keyed @@ -1285,18 +1265,12 @@ pub(super) unsafe fn dispatch_handle( field_val.bits(), f64::from_bits(jsval.bits()), ); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(jsval.bits()))); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args_ptr, args_len, ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } } @@ -1308,15 +1282,12 @@ pub(super) unsafe fn dispatch_handle( // only exists in `CLASS_PROTOTYPE_METHODS`. Bind `this` // to the receiver and call the stored closure. if let Some(method_value) = lookup_prototype_method(class_id, method_name) { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(jsval.bits()))); - let result = - crate::closure::js_native_call_value(method_value, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + let result = crate::closure::native_call_value_this( + method_value, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); return Some(result); } } diff --git a/crates/perry-runtime/src/object/native_call_method/namespace_override.rs b/crates/perry-runtime/src/object/native_call_method/namespace_override.rs index 9626968393..dba91b5b72 100644 --- a/crates/perry-runtime/src/object/native_call_method/namespace_override.rs +++ b/crates/perry-runtime/src/object/native_call_method/namespace_override.rs @@ -80,11 +80,10 @@ pub(super) unsafe fn call_native_namespace_override( } RUNNING.with(|r| r.borrow_mut().push((module, method_name.to_string()))); let _running = Running; - let _this_scope = - crate::object::ImplicitThisScope::bind(root_scope, namespace_handle.get_nanbox_f64()); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&args_handles); - Some(crate::closure::js_native_call_value( + Some(crate::closure::native_call_value_this( bound_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(namespace_handle.get_nanbox_f64()), args.as_ptr(), args.len(), )) diff --git a/crates/perry-runtime/src/object/native_call_method/object_proto.rs b/crates/perry-runtime/src/object/native_call_method/object_proto.rs index 8a88e9f98c..6da2cfdbe7 100644 --- a/crates/perry-runtime/src/object/native_call_method/object_proto.rs +++ b/crates/perry-runtime/src/object/native_call_method/object_proto.rs @@ -32,12 +32,15 @@ pub(super) unsafe fn call_object_to_string_method(object: f64) -> Option { if !crate::closure::is_closure_ptr(method_ptr) { throw_object_to_string_not_function(); } + // Re-read: the key allocation and the method read can move the receiver. + let receiver = object_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - Some(result) + Some(crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + std::ptr::null(), + 0, + )) } pub(crate) unsafe fn js_object_default_value_of(receiver: f64) -> f64 { diff --git a/crates/perry-runtime/src/object/native_call_method/patched_proto.rs b/crates/perry-runtime/src/object/native_call_method/patched_proto.rs index 85f623fea9..5dafc7745a 100644 --- a/crates/perry-runtime/src/object/native_call_method/patched_proto.rs +++ b/crates/perry-runtime/src/object/native_call_method/patched_proto.rs @@ -114,11 +114,14 @@ pub unsafe extern "C-unwind" fn js_native_call_method_patched_proto( ); let bound = scope.root_nanbox_u64(bound); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - // A non-arrow function body reads its receiver from `IMPLICIT_THIS`; + // A non-arrow function body reads its receiver from its `this` argument; // `clone_closure_rebind_this` only covers a closure that captured `this`. - // Restored on the way out, including when the callee throws (#10490). - let _this_scope = crate::object::ImplicitThisScope::bind(&scope, receiver.get_nanbox_f64()); - crate::closure::js_native_call_value(bound.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::native_call_value_this( + bound.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ) } /// Spread form of [`js_native_call_method_patched_proto`]: `args_array_handle` diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index 878ba10073..9448498744 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -142,7 +142,7 @@ unsafe fn class_receiver_arm( // Rebind the closure's reserved `this` slot to the class ref, as // the prototype/field method-dispatch arms above do. A static // data property holding an object-literal method (`captures_this`) - // bakes `this` into a capture slot that `IMPLICIT_THIS` alone + // bakes `this` into a capture slot that the `this` argument alone // can't override; `clone_closure_rebind_this` is a no-op for // closures that don't capture `this`, so plain functions and // arrows are unaffected. @@ -152,17 +152,12 @@ unsafe fn class_receiver_arm( ); let prop_handle = root_scope.root_nanbox_f64(f64::from_bits(bound)); let args = refreshed_args(); - // #8495: root the displaced receiver across the call below. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope.root_nanbox_u64( - IMPLICIT_THIS.with(|c| c.replace(object_handle.get_nanbox_f64().to_bits())), - ); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( prop_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args.as_ptr(), args.len(), ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } } @@ -398,7 +393,7 @@ pub(super) unsafe fn dispatch_primitive( // `.annotations` receiver lowers to a generic Call result), or any // `(expr-returning-a-class-object).staticMethod()`. The compile-time // static-dispatch tower (property_get.rs) binds `this` via - // IMPLICIT_THIS; the generic field-scan path below does NOT, so + // the `this` argument; the generic field-scan path below does NOT, so // `this.` (effect's `annotations() { make(this.ast, ...) }`) // read `undefined`. Route to `js_class_static_method_call`, which binds // `this` to the receiver and walks the class_id parent chain — but only @@ -441,13 +436,10 @@ pub(super) unsafe fn dispatch_primitive( let receiver = object_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method.get_nanbox_u64(), receiver); - let _this = crate::object::ImplicitThisScope::bind( - root_scope, - object_handle.get_nanbox_f64(), - ); let args = refreshed_args(); - return Some(crate::closure::js_native_call_value( + return Some(crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args.as_ptr(), args.len(), )); @@ -484,13 +476,10 @@ pub(super) unsafe fn dispatch_primitive( let bound = crate::closure::clone_closure_rebind_this(method.get_nanbox_u64(), receiver); // Re-read: the clone above allocated (#10490). - let _this = crate::object::ImplicitThisScope::bind( - root_scope, - object_handle.get_nanbox_f64(), - ); let args = refreshed_args(); - return Some(crate::closure::js_native_call_value( + return Some(crate::closure::native_call_value_this( f64::from_bits(bound), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args.as_ptr(), args.len(), )); @@ -528,14 +517,13 @@ pub(super) unsafe fn dispatch_primitive( if (v.to_bits() & crate::value::TAG_MASK) == crate::value::POINTER_TAG && crate::closure::is_closure_ptr(cand) { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(recv.to_bits()))); - let result = crate::closure::js_native_call_value(v, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + let result = crate::closure::native_call_value_this( + v, + // Re-read: the own-property read above can run a getter. + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); return Some(result); } } @@ -567,8 +555,8 @@ pub(super) unsafe fn dispatch_primitive( let has_own_ctor = crate::promise::promise_has_own_constructor(promise_addr); if has_own_then || has_own_ctor { - // Spec path: look up the prototype method and call it with - // IMPLICIT_THIS set to the promise value so the thunk can read it. + // Spec path: look up the prototype method and call it with the + // promise value as `this` so the thunk can read it. if has_own_then && method_name == "then" { // For "then" with own "then": Invoke(promise, "then", args) directly. // exotic_get_own_property invokes accessor getters (propagating throws) @@ -592,33 +580,30 @@ pub(super) unsafe fn dispatch_primitive( )); } let args = refreshed_args(); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(promise_val.to_bits()))); let result = unsafe { - crate::closure::js_native_call_value(own_then, args.as_ptr(), args.len()) + crate::closure::native_call_value_this( + own_then, + // Re-read: the own `then` read above can run a getter. + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ) }; - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } // For "catch"/"finally" with own "then", or "then" with own "constructor": - // call the prototype thunk with IMPLICIT_THIS = promise_val so it can + // call the prototype thunk with `this` = the promise so it can // read the receiver and invoke call_receiver_then / SpeciesConstructor. if let Some(proto_method) = crate::promise::promise_proto_method(method_name) { let args = refreshed_args(); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(promise_val.to_bits()))); let result = unsafe { - crate::closure::js_native_call_value(proto_method, args.as_ptr(), args.len()) + crate::closure::native_call_value_this( + proto_method, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ) }; - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } // Fallthrough to fast path if prototype method lookup fails. @@ -733,17 +718,12 @@ pub(super) unsafe fn dispatch_primitive( ); let prop_handle = root_scope.root_nanbox_f64(f64::from_bits(bound)); let args = refreshed_args(); - // #8495: root the displaced receiver across the call below. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope.root_nanbox_u64( - IMPLICIT_THIS.with(|c| c.replace(object_handle.get_nanbox_f64().to_bits())), - ); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( prop_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), args.as_ptr(), args.len(), ); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); return Some(result); } // Own key present but NOT callable (`re.exec = 5; re.exec()`). @@ -794,7 +774,7 @@ pub(super) unsafe fn dispatch_primitive( // reinterprets the cell's bytes as an object and returns garbage. Every // `Date.prototype` method (getters, setters, `toISOString`, `toJSON`, // `toString`, …) is installed on `Date.prototype` and reads the - // `IMPLICIT_THIS` receiver, so resolve the method there and dispatch with + // `this` receiver, so resolve the method there and dispatch with // `this` bound to the cell. Previously only `toString` was routed this way; // every other dynamic/computed call (`date[m](...)`, `Reflect.apply`) fell // through and silently dropped setter mutations — e.g. dayjs's @@ -819,14 +799,13 @@ pub(super) unsafe fn dispatch_primitive( if (v.to_bits() & crate::value::TAG_MASK) == crate::value::POINTER_TAG && crate::closure::is_closure_ptr((v.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize) { - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = - prev_this_scope.root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(recv_bits))); - let result = crate::closure::js_native_call_value(v, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + let result = crate::closure::native_call_value_this( + v, + // Re-read: the own-property read above can run a getter. + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); return Some(result); } } @@ -842,15 +821,14 @@ pub(super) unsafe fn dispatch_primitive( let value = crate::object::js_object_get_field_by_name(proto_ptr, key); if !value.is_undefined() { let value_f64 = f64::from_bits(value.bits()); - // #8495: root the displaced receiver across the call below — the - // replace has already overwritten the cell, so this is the frame's only - // copy and the restore would otherwise publish a pre-move address. - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope - .root_nanbox_u64(IMPLICIT_THIS.with(|c| c.replace(object.to_bits()))); - let result = - crate::closure::js_native_call_value(value_f64, args_ptr, args_len); - IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); + // Re-read: the key allocation and the prototype read can + // both move the cell. + let result = crate::closure::native_call_value_this( + value_f64, + crate::closure::JsThis::from_f64(object_handle.get_nanbox_f64()), + args_ptr, + args_len, + ); return Some(result); } } diff --git a/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs b/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs index 2345646d70..29da6b5d27 100644 --- a/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs +++ b/crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs @@ -5,12 +5,12 @@ use super::*; /// `js_native_call_value`, which would invoke the shared no-op thunk /// (`global_this_builtin_noop_thunk`) and return `undefined`. This is the final /// link in the "uncurry-this" idiom `Function.prototype.call.bind(method)`: the -/// `Function.prototype.call` thunk stashes the intended receiver in -/// `IMPLICIT_THIS`, then calls the bound `method` value — which until now no-op'd. +/// `Function.prototype.call` thunk passes the intended receiver as `this` +/// to the bound `method` value — which until now no-op'd. /// /// When the invoked closure is a no-op-backed built-in proto method, recover its /// recorded method name and re-dispatch through the real `js_native_call_method` -/// tower using the current `IMPLICIT_THIS` as the receiver. Returns `None` for +/// tower using the call's `this` as the receiver. Returns `None` for /// any other closure so normal dispatch proceeds untouched. /// /// Gated on a recorded built-in `.length` (a proto method always has one) AND on @@ -20,6 +20,7 @@ use super::*; /// invariant, and it stopped holding (#7518). See the exclusion below. pub(crate) unsafe fn try_dispatch_value_called_proto_method( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, args_ptr: *const f64, args_len: usize, ) -> Option { @@ -40,7 +41,7 @@ pub(crate) unsafe fn try_dispatch_value_called_proto_method( let name_hdr = crate::builtins::js_string_coerce(name_val); let name = super::has_own_helpers::str_from_string_header(name_hdr)?; // #7518: a global CONSTRUCTOR reached as a VALUE is never a prototype-method - // uncurry, so re-dispatching it as `IMPLICIT_THIS.(…)` is always wrong: + // uncurry, so re-dispatching it as `this.(…)` is always wrong: // the by-name tower has no such method and its catch-all throws // `TypeError: is not a function`. Constructors share the no-op thunk // with the proto methods this helper serves, and the `.length` gate above @@ -50,7 +51,7 @@ pub(crate) unsafe fn try_dispatch_value_called_proto_method( // a recorded length and opened the gate — re-breaking #6301: a // `class Bus extends EventTarget {}` has no static parent class id, so its // `super()` runs the parent VALUE through `js_fetch_or_value_super`, which - // binds `IMPLICIT_THIS` to the new instance before the value call. That then + // passes the new instance as `this` to the value call. That then // re-dispatched `bus.EventTarget()` and threw. Make the exclusion explicit // and table-driven so growing the spec-length table cannot re-open it. // @@ -69,7 +70,7 @@ pub(crate) unsafe fn try_dispatch_value_called_proto_method( { return None; } - let receiver = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let receiver = this.as_f64(); Some(js_native_call_method( receiver, name.as_ptr() as *const i8, diff --git a/crates/perry-runtime/src/object/native_call_method/to_locale_string_tests.rs b/crates/perry-runtime/src/object/native_call_method/to_locale_string_tests.rs index c5b257c303..e817378b67 100644 --- a/crates/perry-runtime/src/object/native_call_method/to_locale_string_tests.rs +++ b/crates/perry-runtime/src/object/native_call_method/to_locale_string_tests.rs @@ -162,10 +162,10 @@ fn object_prototype_builtin_invokes_to_string_without_self_redispatch() { // the source-level `toLocaleString` dispatcher, which found the same own // thunk again and overflowed the native stack before reaching `toString`. let receiver = crate::object::builtin_prototype_value("Object"); - let previous = crate::object::js_implicit_this_set(receiver); - let result = - crate::object::global_this::object_prototype_to_locale_string_thunk(std::ptr::null()); - crate::object::js_implicit_this_set(previous); + let result = crate::object::global_this::object_prototype_to_locale_string_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(receiver), + ); let ptr = crate::value::js_get_string_pointer_unified(result) as *const crate::StringHeader; assert!(!ptr.is_null()); diff --git a/crates/perry-runtime/src/object/native_get/tests.rs b/crates/perry-runtime/src/object/native_get/tests.rs index 141c68210d..7a041ac2eb 100644 --- a/crates/perry-runtime/src/object/native_get/tests.rs +++ b/crates/perry-runtime/src/object/native_get/tests.rs @@ -95,7 +95,10 @@ fn prototype_data_mutation_shadow_delete_and_freeze_match_forced_slow() { static GETTER_CALLS: AtomicUsize = AtomicUsize::new(0); -extern "C" fn getter(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn getter( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { GETTER_CALLS.fetch_add(1, Ordering::Relaxed); 47.0 } diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 821fccecbb..0d4a52c371 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -439,6 +439,7 @@ pub(crate) fn call_worker_threads_getter( if ptr.is_null() { return fallback(); } + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let getter: WorkerThreadsValueGetter = unsafe { std::mem::transmute(ptr) }; getter() } @@ -511,6 +512,7 @@ pub(crate) fn install_nm_ee_ops() { /// `closure::dispatch::value_call`; see `NmEeOps::ee_dynamic_super`). unsafe fn nm_ee_dynamic_super( func_value: f64, + this: crate::closure::JsThis, args_ptr: *const f64, args_len: usize, ) -> Option { @@ -520,7 +522,7 @@ unsafe fn nm_ee_dynamic_super( if (module == "events" && (method == "EventEmitter" || method == "EventEmitterAsyncResource")) || (module == "stream" && method == "Stream") { - let this_val = super::js_implicit_this_get(); + let this_val = this.as_f64(); if crate::value::JSValue::from_bits(this_val.to_bits()).is_pointer() { if method == "EventEmitterAsyncResource" { let options = if !args_ptr.is_null() && args_len > 0 { @@ -1348,7 +1350,7 @@ pub(crate) fn build_symbol_bound_method_closure( /// Resolve the effective receiver for a BOUND_METHOD dispatch. When the /// captured receiver is a canonical class-method marker (a class prototype-ref, /// produced by `class_prototype_method_value_for_name`), substitute the -/// call-site `this` (IMPLICIT_THIS) provided it is itself a dispatchable class +/// call-site `this` argument provided it is itself a dispatchable class /// receiver (an instance or class ref). Otherwise the captured value is the real /// receiver and is returned unchanged. See `dispatch_bound_method`. /// Is `value` a bound STATIC-method value — a BOUND_METHOD closure whose @@ -1379,13 +1381,16 @@ pub(crate) fn is_static_bound_method_value(value: f64) -> bool { || class_registry::is_class_object_value(captured) } -pub(crate) fn canonical_bound_method_receiver(captured: f64) -> f64 { +pub(crate) fn canonical_bound_method_receiver( + captured: f64, + call_this: crate::closure::JsThis, +) -> f64 { if class_prototype_ref_id(captured).is_some() { // The captured prototype identifies the method's owner, not its receiver. // Class methods are strict: every call-site value, including null, // undefined, primitives and arrays, must reach the body unchanged. // Dispatch resolves the body from the captured owner independently. - super::js_implicit_this_get() + call_this.as_f64() } else { captured } @@ -1418,7 +1423,7 @@ fn class_id_from_method_receiver_known(instance: f64, class_ref: Option) -> // this guard, a free call to a `C.prototype.method` bound-method // value made from inside a function-object method body (e.g. // test262's `assert.throws(…, function(){ m(...) })`, where - // `IMPLICIT_THIS` is the `assert` function) would mis-substitute the + // the call-site `this` was the `assert` function) would mis-substitute the // function object as the receiver and dispatch `assert.method(...)` // instead of `C.prototype.method`, bypassing the generator wrapper's // param prologue. See `canonical_bound_method_receiver`. @@ -1679,19 +1684,4 @@ pub(crate) use cjs_default::{ }; #[cfg(test)] -mod buffer_pool_size_tests { - // #11471: `Buffer.poolSize` is per realm, so a worker's write (possibly of - // a heap value) never reaches another thread. - #[test] - fn buffer_pool_size_writes_stay_on_their_thread() { - let written = std::thread::spawn(|| { - super::set_buffer_pool_size(1.0); - super::buffer_pool_size() - }) - .join() - .unwrap(); - assert_eq!(written, 1.0); - let elsewhere = std::thread::spawn(super::buffer_pool_size).join().unwrap(); - assert_eq!(elsewhere, 65536.0); - } -} +mod buffer_pool_size_tests; diff --git a/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs b/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs index 171cd7f209..ab4d0fd093 100644 --- a/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs +++ b/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs @@ -28,12 +28,13 @@ const ASYNC_RESOURCE_METHODS: &[(&str, u32)] = &[ /// `run`, `exit`, and `runInAsyncScope`. extern "C" fn async_hooks_prototype_method_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { unsafe { let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; let name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let name = std::slice::from_raw_parts(name_ptr as *const u8, name_len); // Node's enterWith/disable implementations do not brand-check an @@ -190,7 +191,7 @@ fn attach_prototype(constructor_value: f64, methods: &[(&str, u32)]) -> f64 { /// Materialize an unbound `AsyncResource.prototype` method for native-handle /// property reads whose static type was erased. Invocation observes the -/// call-site receiver through `IMPLICIT_THIS`, just like the real prototype. +/// call-site receiver as its `this` argument, just like the real prototype. pub(crate) fn async_resource_prototype_method_value(name: &'static str, length: u32) -> f64 { let thunk = async_hooks_prototype_method_thunk as *const u8; crate::closure::js_register_closure_rest(thunk, 0); diff --git a/crates/perry-runtime/src/object/native_module/buffer_pool_size_tests.rs b/crates/perry-runtime/src/object/native_module/buffer_pool_size_tests.rs new file mode 100644 index 0000000000..237669bfb6 --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/buffer_pool_size_tests.rs @@ -0,0 +1,14 @@ +// #11471: `Buffer.poolSize` is per realm, so a worker's write (possibly of +// a heap value) never reaches another thread. +#[test] +fn buffer_pool_size_writes_stay_on_their_thread() { + let written = std::thread::spawn(|| { + super::set_buffer_pool_size(1.0); + super::buffer_pool_size() + }) + .join() + .unwrap(); + assert_eq!(written, 1.0); + let elsewhere = std::thread::spawn(super::buffer_pool_size).join().unwrap(); + assert_eq!(elsewhere, 65536.0); +} diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 2a4418c3c9..854f263314 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -220,6 +220,7 @@ fn async_hooks_static_method_value( extern "C" fn fs_namespace_descriptor_getter_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { unsafe { let property_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const u8; @@ -230,6 +231,7 @@ extern "C" fn fs_namespace_descriptor_getter_thunk( extern "C" fn fs_namespace_descriptor_setter_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) @@ -306,12 +308,14 @@ pub(crate) fn is_cluster_emitter_method(prop: &str) -> bool { extern "C" fn sqlite_statement_sync_constructor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") } extern "C" fn sqlite_session_constructor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") } @@ -371,6 +375,7 @@ fn native_callable_export_display_name<'a>(module: &str, prop: &'a str) -> &'a s extern "C" fn buffer_constructor_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, encoding_or_offset: f64, length: f64, @@ -416,12 +421,15 @@ extern "C" fn buffer_constructor_thunk( crate::value::js_nanbox_pointer(buf as i64) } -extern "C" fn buffer_prototype_method_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn buffer_prototype_method_thunk( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } /// `this` for a `Buffer.prototype.offset`/`.parent` accessor read, resolved -/// through `IMPLICIT_THIS` (the same mechanism `require_webcrypto_this` in +/// from the getter's `this` argument (the same mechanism `require_webcrypto_this` in /// `ctor_thunks.rs` uses for Web Crypto getters). `None` for a non-buffer /// receiver — Node's real getters answer `undefined` rather than throwing /// (`isInstance(this, Buffer) ? … : undefined`), and ordinary Buffer/typed- @@ -429,8 +437,8 @@ extern "C" fn buffer_prototype_method_thunk(_closure: *const crate::closure::Clo /// `.offset`/`.parent` directly — see `get_field_by_name_tail.rs`); this only /// matters for reflection (`Object.getOwnPropertyDescriptor(Buffer.prototype, /// "offset").get.call(x)`) and enumeration. -fn buffer_prototype_this_addr() -> Option { - let this_bits = crate::object::IMPLICIT_THIS.with(|c| c.get()); +fn buffer_prototype_this_addr(this: crate::closure::JsThis) -> Option { + let this_bits = this.bits(); let jv = crate::value::JSValue::from_bits(this_bits); if !jv.is_pointer() { return None; @@ -447,8 +455,9 @@ fn buffer_prototype_this_addr() -> Option { /// Node's `Buffer.prototype`. extern "C" fn buffer_prototype_parent_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match buffer_prototype_this_addr() { + match buffer_prototype_this_addr(this) { Some(addr) => { crate::value::js_nanbox_pointer(crate::buffer::buffer_backing_array_buffer(addr) as i64) } @@ -460,8 +469,9 @@ extern "C" fn buffer_prototype_parent_getter_thunk( /// `.byteOffset`, still a real own accessor on Node's `Buffer.prototype`. extern "C" fn buffer_prototype_offset_getter_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match buffer_prototype_this_addr() { + match buffer_prototype_this_addr(this) { Some(addr) => crate::buffer::buffer_byte_offset(addr) as f64, None => f64::from_bits(crate::value::TAG_UNDEFINED), } @@ -757,6 +767,7 @@ fn attach_assert_prototype(constructor_value: f64) { extern "C" fn sqlite_database_sync_prototype_method_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg0: f64, arg1: f64, arg2: f64, @@ -764,7 +775,7 @@ extern "C" fn sqlite_database_sync_prototype_method_thunk( unsafe { let method_name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; let method_name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let args = [arg0, arg1, arg2]; crate::object::js_native_call_method( receiver, @@ -1104,6 +1115,7 @@ fn attach_crypto_key_object_shape(closure_addr: usize, constructor_value: f64) { extern "C" fn x509_issuer_certificate_getter_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -1342,12 +1354,13 @@ pub(crate) fn scan_tls_derived_prototype_roots_mut( extern "C" fn tls_prototype_method_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { unsafe { let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; let name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let args_array = crate::value::js_nanbox_get_pointer(rest); crate::object::js_native_call_method_apply(receiver, name_ptr, name_len, args_array) } diff --git a/crates/perry-runtime/src/object/native_module/callable_exports/buffer_species.rs b/crates/perry-runtime/src/object/native_module/callable_exports/buffer_species.rs index 026efdf05b..2a6cdd1eff 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports/buffer_species.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports/buffer_species.rs @@ -29,6 +29,7 @@ use super::*; /// memory-sharing `ArrayBuffer` view. extern "C" fn fast_buffer_constructor_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, byte_offset: f64, length: f64, @@ -43,12 +44,15 @@ extern "C" fn fast_buffer_constructor_thunk( let buf = crate::buffer::js_buffer_alloc(size, 0); return crate::value::js_nanbox_pointer(buf as i64); } - super::buffer_constructor_thunk(closure, value, byte_offset, length) + super::buffer_constructor_thunk(closure, this, value, byte_offset, length) } /// `get [Symbol.species]` on `Buffer`: answers the `FastBuffer` held in the /// getter's own capture slot 0. -extern "C" fn buffer_species_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn buffer_species_getter_thunk( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::closure::js_closure_get_capture_f64(closure, 0) } diff --git a/crates/perry-runtime/src/object/native_module/callable_exports/module_cjs.rs b/crates/perry-runtime/src/object/native_module/callable_exports/module_cjs.rs index b1ab0c2b47..f37b0aa091 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports/module_cjs.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports/module_cjs.rs @@ -3,6 +3,7 @@ use std::cell::Cell; extern "C" fn module_cjs_extension_noop_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _module: f64, _filename: f64, ) -> f64 { @@ -187,6 +188,7 @@ pub(crate) fn module_constants_value() -> f64 { extern "C" fn module_wrap_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, source: f64, ) -> f64 { let value = JSValue::from_bits(source.to_bits()); @@ -243,6 +245,7 @@ fn module_wrapper_value() -> f64 { extern "C" fn module_prototype_method_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _a: f64, _b: f64, _c: f64, @@ -252,20 +255,22 @@ extern "C" fn module_prototype_method_thunk( extern "C" fn module_prototype_load_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, filename: f64, _b: f64, _c: f64, ) -> f64 { - crate::process::js_module_instance_load(filename) + crate::process::js_module_instance_load(this, filename) } extern "C" fn module_prototype_require_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, specifier: f64, _b: f64, _c: f64, ) -> f64 { - crate::process::js_module_instance_require(specifier) + crate::process::js_module_instance_require(this, specifier) } fn module_prototype_method(name: &str, length: u32) -> f64 { @@ -291,6 +296,7 @@ fn module_prototype_method(name: &str, length: u32) -> f64 { extern "C" fn module_prototype_constructor_getter( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { // Resolve through the canonical callable cache at access time. Capturing // the constructor while its own attach was still in progress preserved a @@ -301,18 +307,23 @@ extern "C" fn module_prototype_constructor_getter( bound_native_callable_export_value("module", "Module") } -extern "C" fn module_prototype_false_getter(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn module_prototype_false_getter( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { native_bool_value(false) } extern "C" fn module_prototype_parent_getter( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } extern "C" fn module_prototype_parent_setter( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) diff --git a/crates/perry-runtime/src/object/native_module/class_method_bind.rs b/crates/perry-runtime/src/object/native_module/class_method_bind.rs index 6b7c3a58af..a44e322f0b 100644 --- a/crates/perry-runtime/src/object/native_module/class_method_bind.rs +++ b/crates/perry-runtime/src/object/native_module/class_method_bind.rs @@ -78,7 +78,7 @@ pub extern "C" fn js_class_method_bind( // `class_prototype_method_value_for_name` instead of minting a fresh // per-receiver closure here. The canonical captures the OWNER class's // prototype-ref (capture 0); `dispatch_bound_method` recognises that marker - // and supplies the call-site `this` (IMPLICIT_THIS) so invocations still see + // and supplies the call-site `this` argument so invocations still see // the right receiver — e.g. the `this.m = this.m.bind(this)` idiom rebinds // correctly, and a bare `const f = c.m; f()` runs with the spec `this`. // diff --git a/crates/perry-runtime/src/object/native_module/namespace_builders.rs b/crates/perry-runtime/src/object/native_module/namespace_builders.rs index 2ea2051b3e..3746f55e92 100644 --- a/crates/perry-runtime/src/object/native_module/namespace_builders.rs +++ b/crates/perry-runtime/src/object/native_module/namespace_builders.rs @@ -327,6 +327,7 @@ unsafe fn global_agent_build_name(options: f64, is_https: bool) -> String { extern "C" fn global_agent_get_name_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, ) -> f64 { @@ -339,6 +340,7 @@ extern "C" fn global_agent_get_name_thunk( extern "C" fn global_agent_keep_socket_alive_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _socket: f64, ) -> f64 { f64::from_bits(JSValue::bool(true).bits()) @@ -346,6 +348,7 @@ extern "C" fn global_agent_keep_socket_alive_thunk( extern "C" fn global_agent_reuse_socket_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _socket: f64, _request: f64, ) -> f64 { @@ -354,6 +357,7 @@ extern "C" fn global_agent_reuse_socket_thunk( extern "C" fn global_agent_listener_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -395,12 +399,16 @@ extern "C" fn global_agent_listener_thunk( } } -extern "C" fn global_agent_destroy_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn global_agent_destroy_thunk( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { f64::from_bits(JSValue::undefined().bits()) } extern "C" fn global_agent_add_request_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, first: f64, second: f64, third: f64, @@ -665,8 +673,12 @@ pub unsafe extern "C" fn js_https_global_agent_emit( let ptr = (callback_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if crate::closure::is_closure_ptr(ptr as usize) { - let _ = - crate::closure::js_closure_call2(ptr, arg0.get_nanbox_f64(), arg1.get_nanbox_f64()); + let _ = crate::closure::js_closure_call2( + ptr, + crate::closure::plain_call_receiver(), + arg0.get_nanbox_f64(), + arg1.get_nanbox_f64(), + ); } } } diff --git a/crates/perry-runtime/src/object/native_module/web_locks.rs b/crates/perry-runtime/src/object/native_module/web_locks.rs index 3a99e677db..94dd89a28c 100644 --- a/crates/perry-runtime/src/object/native_module/web_locks.rs +++ b/crates/perry-runtime/src/object/native_module/web_locks.rs @@ -542,6 +542,7 @@ fn web_locks_release(id: u64) { extern "C" fn worker_threads_locks_release_fulfill( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let id = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; @@ -554,6 +555,7 @@ extern "C" fn worker_threads_locks_release_fulfill( extern "C" fn worker_threads_locks_release_reject( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let id = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; @@ -566,6 +568,7 @@ extern "C" fn worker_threads_locks_release_reject( extern "C" fn worker_threads_locks_request( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, name_value: f64, options_or_callback: f64, maybe_callback: f64, @@ -657,18 +660,22 @@ pub extern "C" fn js_worker_threads_locks_request( ) -> f64 { worker_threads_locks_request( std::ptr::null(), + crate::closure::JsThis::UNDEFINED, name_value, options_or_callback, maybe_callback, ) } -extern "C" fn worker_threads_locks_query(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn worker_threads_locks_query( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let snapshot = web_locks_query_snapshot(); web_locks_object_value(crate::promise::js_promise_resolved(snapshot)) } #[no_mangle] pub extern "C" fn js_worker_threads_locks_query() -> f64 { - worker_threads_locks_query(std::ptr::null()) + worker_threads_locks_query(std::ptr::null(), crate::closure::JsThis::UNDEFINED) } diff --git a/crates/perry-runtime/src/object/native_module_registry.rs b/crates/perry-runtime/src/object/native_module_registry.rs index 318855c689..82a6062830 100644 --- a/crates/perry-runtime/src/object/native_module_registry.rs +++ b/crates/perry-runtime/src/object/native_module_registry.rs @@ -508,6 +508,14 @@ pub extern "C" fn js_nm_install_stream() { Ordering::Relaxed, ); nm_register_ctor(NmBucket::Stream, nm_ctor_stream); + // `black_box` for the reason `js_nm_enable_install_all` gives: this is the + // only value ever stored to the single-pointer slot, and without it + // whole-program optimization devirtualizes the load in + // `nm_stream_subclass_init` into a direct call, re-pinning node_stream. + let init = std::hint::black_box( + super::native_this_alias::node_stream_subclass_init as NmStreamSubclassInitFn, + ); + NM_STREAM_SUBCLASS_INIT.store(init as *mut (), Ordering::Relaxed); } #[no_mangle] pub extern "C" fn js_nm_install_timers() { @@ -722,6 +730,34 @@ fn nm_register_ctor(b: NmBucket, f: NmCtorFn) { NM_CTOR_REGISTRY[b as usize].store(f as *mut (), Ordering::Relaxed); } +type NmStreamSubclassInitFn = fn(&str, f64, f64) -> bool; + +/// The stream bucket's subclass-init shim (`Readable.call(this, opts)`, a +/// dynamic `super(opts)` onto a stream base). Registered by +/// `js_nm_install_stream` so the always-linked call paths that reach it do not +/// pin node_stream into programs that never import `stream`. +static NM_STREAM_SUBCLASS_INIT: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); + +/// Run the installed stream subclass-init shim for stream base `method` on +/// `this`. False when `method` is no stream base or `stream` was never +/// installed (then no stream base callee can exist either). +pub(crate) fn nm_stream_subclass_init(method: &str, this: f64, opts: f64) -> bool { + let p = NM_STREAM_SUBCLASS_INIT.load(Ordering::Relaxed); + // See nm_dispatch_lookup: unit tests run without the codegen install. + #[cfg(test)] + let p = if p.is_null() && nm_lazy_install_enabled() { + js_nm_install_all(); + NM_STREAM_SUBCLASS_INIT.load(Ordering::Relaxed) + } else { + p + }; + if p.is_null() { + return false; + } + let f = unsafe { std::mem::transmute::<*mut (), NmStreamSubclassInitFn>(p) }; + f(method, this, opts) +} + /// Per-module callable-export "attach" handlers: prototype/statics decoration /// for bound module exports (`stream.Readable`'s prototype tower, /// `sqlite.DatabaseSync`'s methods, `AsyncLocalStorage.bind`, …). Formerly a diff --git a/crates/perry-runtime/src/object/native_module_stream.rs b/crates/perry-runtime/src/object/native_module_stream.rs index fe5b2dfbed..3c55cb71c0 100644 --- a/crates/perry-runtime/src/object/native_module_stream.rs +++ b/crates/perry-runtime/src/object/native_module_stream.rs @@ -215,6 +215,7 @@ pub(crate) fn is_stream_event_emitter_prototype_value(value: f64) -> bool { extern "C" fn stream_static_method_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, arg0: f64, arg1: f64, ) -> f64 { @@ -490,7 +491,12 @@ mod tests { )); let readable_to_web = static_method_value(readable, "toWeb"); let web_readable = unsafe { - crate::closure::js_native_call_value(readable_to_web, [node_readable].as_ptr(), 1) + crate::closure::js_native_call_value( + readable_to_web, + crate::closure::plain_call_receiver(), + [node_readable].as_ptr(), + 1, + ) }; assert_object_property_function(web_readable, b"getReader"); @@ -499,7 +505,12 @@ mod tests { )); let writable_to_web = static_method_value(writable, "toWeb"); let web_writable = unsafe { - crate::closure::js_native_call_value(writable_to_web, [node_writable].as_ptr(), 1) + crate::closure::js_native_call_value( + writable_to_web, + crate::closure::plain_call_receiver(), + [node_writable].as_ptr(), + 1, + ) }; assert_object_property_function(web_writable, b"getWriter"); @@ -508,7 +519,12 @@ mod tests { )); let duplex_to_web = static_method_value(duplex, "toWeb"); let web_pair = unsafe { - crate::closure::js_native_call_value(duplex_to_web, [node_duplex].as_ptr(), 1) + crate::closure::js_native_call_value( + duplex_to_web, + crate::closure::plain_call_receiver(), + [node_duplex].as_ptr(), + 1, + ) }; let web_pair_readable = unsafe { property(web_pair, b"readable") }; let web_pair_writable = unsafe { property(web_pair, b"writable") }; diff --git a/crates/perry-runtime/src/object/native_this_alias.rs b/crates/perry-runtime/src/object/native_this_alias.rs index 3b7314f896..6daee7f016 100644 --- a/crates/perry-runtime/src/object/native_this_alias.rs +++ b/crates/perry-runtime/src/object/native_this_alias.rs @@ -252,7 +252,18 @@ pub(crate) unsafe fn maybe_construct_http_class_with_this( /// (`js_fetch_or_value_super`, #10448/#10798) and the explicit-`this` /// `Base.call(this, opts)` path below (#10454), so every construction shape /// installs the identical surface. False when `method` is no stream base. +/// +/// The node_stream shims are reached through the stream bucket's install +/// (`js_nm_install_stream`), never directly: this runs on the always-linked +/// `Function.prototype.call`/`.apply` path, and a direct call would link all of +/// node_stream into every program. A stream base callee only exists in a +/// program that imports `stream`, which emits that install. pub(crate) fn run_node_stream_subclass_init(method: &str, this: f64, opts: f64) -> bool { + super::native_module_registry::nm_stream_subclass_init(method, this, opts) +} + +/// The stream bucket's subclass-init entry, installed by `js_nm_install_stream`. +pub(crate) fn node_stream_subclass_init(method: &str, this: f64, opts: f64) -> bool { use crate::node_stream as ns; match method { "Readable" => ns::js_node_stream_readable_subclass_init(this, opts), @@ -483,6 +494,7 @@ crate::perry_thread_local! { /// args)` (light-my-request's override shape) calls. extern "C" fn server_response_prototype_method_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg0: f64, arg1: f64, arg2: f64, @@ -490,7 +502,7 @@ extern "C" fn server_response_prototype_method_thunk( let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const u8; let name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let receiver_jv = JSValue::from_bits(receiver.to_bits()); let receiver_is_handle = receiver_jv.is_pointer() && crate::value::addr_class::is_small_handle( diff --git a/crates/perry-runtime/src/object/nm_namespace_hooks.rs b/crates/perry-runtime/src/object/nm_namespace_hooks.rs index 78289b9355..0da6bf0a15 100644 --- a/crates/perry-runtime/src/object/nm_namespace_hooks.rs +++ b/crates/perry-runtime/src/object/nm_namespace_hooks.rs @@ -75,7 +75,8 @@ pub(crate) struct NmEeOps { /// Dynamic `super()` for `class X extends `: /// installs the EE methods on the fresh instance. `None` when the callee /// is not the bound events export (fall through to normal call dispatch). - pub ee_dynamic_super: unsafe fn(f64, *const f64, usize) -> Option, + /// The second argument is the call's `this` (the fresh instance). + pub ee_dynamic_super: unsafe fn(f64, crate::closure::JsThis, *const f64, usize) -> Option, } static NM_EE_OPS: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); diff --git a/crates/perry-runtime/src/object/own_override.rs b/crates/perry-runtime/src/object/own_override.rs index f97de1bbf0..e4d63b9c6b 100644 --- a/crates/perry-runtime/src/object/own_override.rs +++ b/crates/perry-runtime/src/object/own_override.rs @@ -354,8 +354,7 @@ unsafe fn resolve_own_user_method(recv: f64, name: &str) -> Option { /// would be a wrong `this` or an unrooted method value, neither of which a /// fixture reliably catches. /// -/// `IMPLICIT_THIS` is bound across the call and restored after it, because the -/// callee reads its receiver from there when it has no lexical `this`. +/// The receiver is passed to the callee as its `this` argument. /// /// `recv` and `args` are READERS, not values: resolving the method can run an /// own accessor's getter and allocate, so the receiver and arguments handed to @@ -391,18 +390,12 @@ unsafe fn invoke_own_user_method(own: f64, recv: f64, args: &[f64]) -> f64 { let arg_handles = root_scope.root_nanbox_f64_slice(args); // Re-read AFTER rooting: resolving the method can move the heap. let refreshed = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - let prev_this_scope = crate::gc::RuntimeHandleScope::new(); - let prev_this_h = prev_this_scope.root_nanbox_u64( - crate::object::this_binding::IMPLICIT_THIS - .with(|c| c.replace(recv_handle.get_nanbox_f64().to_bits())), - ); - let result = crate::closure::js_native_call_value( + crate::closure::native_call_value_this( method_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(recv_handle.get_nanbox_f64()), refreshed.as_ptr(), refreshed.len(), - ); - crate::object::this_binding::IMPLICIT_THIS.with(|c| c.set(prev_this_h.get_nanbox_u64())); - result + ) } /// Does the array behind `arr` OWN a property named `push`? Returns the LIVE diff --git a/crates/perry-runtime/src/object/own_override_push_tests.rs b/crates/perry-runtime/src/object/own_override_push_tests.rs index ef6915bbd8..25eb7e0fb9 100644 --- a/crates/perry-runtime/src/object/own_override_push_tests.rs +++ b/crates/perry-runtime/src/object/own_override_push_tests.rs @@ -28,7 +28,11 @@ unsafe fn push_or_own(arr: *mut ArrayHeader, value: f64) -> (u64, u32) { (bits, own) } -extern "C" fn own_push_fixture(_closure: *const crate::closure::ClosureHeader, x: f64) -> f64 { +extern "C" fn own_push_fixture( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + x: f64, +) -> f64 { x * 10.0 } diff --git a/crates/perry-runtime/src/object/primitive_proto_thunks.rs b/crates/perry-runtime/src/object/primitive_proto_thunks.rs index b7ffdddcc0..76535331fb 100644 --- a/crates/perry-runtime/src/object/primitive_proto_thunks.rs +++ b/crates/perry-runtime/src/object/primitive_proto_thunks.rs @@ -186,8 +186,8 @@ fn primitive_proto_method_closure_value(method_name: &str, func_ptr: *const u8, crate::value::js_nanbox_pointer(closure as i64) } -fn receiver_value() -> f64 { - f64::from_bits(IMPLICIT_THIS.with(|c| c.get())) +fn receiver_value(this: crate::closure::JsThis) -> f64 { + f64::from_bits(this.bits()) } fn throw_incompatible_receiver(proto: &str, method: &str) -> ! { @@ -202,8 +202,8 @@ fn boxed_payload(receiver: f64, expected_class_id: u32) -> Option { (class_id == expected_class_id).then_some(payload) } -fn number_receiver_or_throw(method: &str) -> f64 { - let receiver = receiver_value(); +fn number_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> f64 { + let receiver = receiver_value(this); if let Some(payload) = boxed_payload(receiver, CLASS_ID_BOXED_NUMBER) { return payload; } @@ -226,8 +226,8 @@ fn number_receiver_or_throw(method: &str) -> f64 { throw_incompatible_receiver("Number.prototype", method) } -fn boolean_receiver_or_throw(method: &str) -> f64 { - let receiver = receiver_value(); +fn boolean_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> f64 { + let receiver = receiver_value(this); if let Some(payload) = boxed_payload(receiver, CLASS_ID_BOXED_BOOLEAN) { return payload; } @@ -245,8 +245,8 @@ fn boolean_receiver_or_throw(method: &str) -> f64 { throw_incompatible_receiver("Boolean.prototype", method) } -fn symbol_receiver_or_throw(method: &str) -> f64 { - let receiver = receiver_value(); +fn symbol_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> f64 { + let receiver = receiver_value(this); if let Some(payload) = boxed_payload(receiver, CLASS_ID_BOXED_SYMBOL) { return payload; } @@ -256,8 +256,8 @@ fn symbol_receiver_or_throw(method: &str) -> f64 { throw_incompatible_receiver("Symbol.prototype", method) } -fn bigint_receiver_or_throw(method: &str) -> f64 { - let receiver = receiver_value(); +fn bigint_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> f64 { + let receiver = receiver_value(this); if let Some(payload) = boxed_payload(receiver, CLASS_ID_BOXED_BIGINT) { return payload; } @@ -270,8 +270,8 @@ fn bigint_receiver_or_throw(method: &str) -> f64 { /// ECMA-262 `thisStringValue(value)`: accept a String primitive or a boxed /// `String` object; throw `TypeError` for anything else, including `null`, /// `undefined`, numbers, booleans, symbols, and plain objects. -fn string_receiver_or_throw(method: &str) -> f64 { - let receiver = receiver_value(); +fn string_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> f64 { + let receiver = receiver_value(this); let jv = crate::value::JSValue::from_bits(receiver.to_bits()); // String primitive (heap or SSO inline). if jv.is_string() || jv.is_short_string() { @@ -295,46 +295,51 @@ fn string_value(ptr: *mut crate::string::StringHeader) -> f64 { pub(super) extern "C" fn number_proto_value_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - number_receiver_or_throw("valueOf") + number_receiver_or_throw(this, "valueOf") } pub(super) extern "C" fn number_proto_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, radix: f64, ) -> f64 { string_value(crate::value::js_jsvalue_to_string_radix( - number_receiver_or_throw("toString"), + number_receiver_or_throw(this, "toString"), radix, )) } pub(super) extern "C" fn number_proto_to_fixed_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, decimals: f64, ) -> f64 { string_value(crate::string::js_number_to_fixed( - number_receiver_or_throw("toFixed"), + number_receiver_or_throw(this, "toFixed"), decimals, )) } pub(super) extern "C" fn number_proto_to_precision_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, precision: f64, ) -> f64 { string_value(crate::string::js_number_to_precision( - number_receiver_or_throw("toPrecision"), + number_receiver_or_throw(this, "toPrecision"), precision, )) } pub(super) extern "C" fn number_proto_to_exponential_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, decimals: f64, ) -> f64 { string_value(crate::string::js_number_to_exponential( - number_receiver_or_throw("toExponential"), + number_receiver_or_throw(this, "toExponential"), decimals, )) } @@ -357,9 +362,10 @@ pub(super) extern "C" fn number_proto_to_exponential_thunk( /// (Intl builds a fresh instance per call; there is no formatter cache). pub(super) extern "C" fn number_proto_to_locale_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let n = number_receiver_or_throw("toLocaleString"); + let n = number_receiver_or_throw(this, "toLocaleString"); #[cfg(feature = "intl-namespace")] { let args = super::global_this::global_this_rest_array_values(rest); @@ -379,14 +385,16 @@ pub(super) extern "C" fn number_proto_to_locale_string_thunk( pub(super) extern "C" fn boolean_proto_value_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - boolean_receiver_or_throw("valueOf") + boolean_receiver_or_throw(this, "valueOf") } pub(super) extern "C" fn boolean_proto_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let value = boolean_receiver_or_throw("toString"); + let value = boolean_receiver_or_throw(this, "toString"); let bytes = if crate::value::JSValue::from_bits(value.to_bits()).as_bool() { b"true".as_slice() } else { @@ -398,14 +406,16 @@ pub(super) extern "C" fn boolean_proto_to_string_thunk( pub(super) extern "C" fn symbol_proto_value_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - symbol_receiver_or_throw("valueOf") + symbol_receiver_or_throw(this, "valueOf") } pub(super) extern "C" fn symbol_proto_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let symbol = symbol_receiver_or_throw("toString"); + let symbol = symbol_receiver_or_throw(this, "toString"); let s = unsafe { crate::symbol::js_symbol_to_string(symbol) } as *mut crate::string::StringHeader; string_value(s) @@ -413,25 +423,28 @@ pub(super) extern "C" fn symbol_proto_to_string_thunk( pub(super) extern "C" fn bigint_proto_value_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - bigint_receiver_or_throw("valueOf") + bigint_receiver_or_throw(this, "valueOf") } pub(super) extern "C" fn bigint_proto_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, radix: f64, ) -> f64 { string_value(crate::value::js_jsvalue_to_string_radix( - bigint_receiver_or_throw("toString"), + bigint_receiver_or_throw(this, "toString"), radix, )) } pub(super) extern "C" fn bigint_proto_to_locale_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let value = bigint_receiver_or_throw("toLocaleString"); + let value = bigint_receiver_or_throw(this, "toLocaleString"); let args = super::global_this::global_this_rest_array_values(rest); let undef = f64::from_bits(crate::value::TAG_UNDEFINED); let _locales = args.first().copied().unwrap_or(undef); @@ -464,14 +477,16 @@ pub(super) extern "C" fn bigint_proto_to_locale_string_thunk( /// `TypeError` for any other receiver. pub(super) extern "C" fn string_proto_to_string_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - string_receiver_or_throw("toString") + string_receiver_or_throw(this, "toString") } /// `String.prototype.valueOf()` — brand-checked, identical semantics to /// `toString()`: returns the [[StringData]] of the receiver. pub(super) extern "C" fn string_proto_value_of_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - string_receiver_or_throw("valueOf") + string_receiver_or_throw(this, "valueOf") } diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index 63036a0e4a..171f853b05 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -129,10 +129,14 @@ unsafe fn ordinary_to_primitive_string_key(value: f64) -> Option { continue; } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = - crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + // Re-read: the key allocation, the method read and the clone can all + // move the receiver. + let result = crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), + std::ptr::null(), + 0, + ); if js_value_is_not_object(result) { return Some(result); } @@ -540,21 +544,22 @@ pub unsafe extern "C" fn js_object_super_call( let bound = crate::closure::clone_closure_rebind_this(callee_handle.get_nanbox_u64(), receiver); let bound_handle = scope.root_nanbox_u64(bound); let receiver = f64::from_bits(receiver_handle.get_heap_word_u64()); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value( + crate::closure::native_call_value_this( f64::from_bits(bound_handle.get_nanbox_u64()), + crate::closure::JsThis::from_f64(receiver), args_ptr, args_len, - ); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + ) } #[cfg(test)] mod property_key_tests { use super::*; - extern "C" fn accessor_getter(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn accessor_getter( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { 123.0 } diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 4ffc11b31f..f457330246 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -1165,14 +1165,9 @@ pub(crate) fn resolve_inherited_field_from_prototype( return None; } // `js_object_get_field_by_name` handles its own further prototype hops - // (recorded protos on the proto object), so this is the full walk. Bind - // accessor getters to the original receiver while walking inherited - // properties; otherwise prototype accessors would observe the prototype - // object instead of the instance. + // (recorded protos on the proto object), so this is the full walk. let receiver = f64::from_bits(crate::value::js_nanbox_pointer(obj_ptr as i64).to_bits()); let scope = crate::gc::RuntimeHandleScope::new(); - let previous_this = super::js_implicit_this_set(receiver); - let previous_this_handle = scope.root_nanbox_f64(previous_this); // The recursive `get_field(proto, key)` re-derives the accessor receiver // from `proto`; stash the real instance so an inherited getter binds `this` // to it, not to the prototype. @@ -1182,7 +1177,6 @@ pub(crate) fn resolve_inherited_field_from_prototype( super::field_get_set::accessor_receiver_override_end( prev_override_handle.map(|handle| handle.get_nanbox_f64()), ); - super::js_implicit_this_set(previous_this_handle.get_nanbox_f64()); if v.bits() == 0x7FFC_0000_0000_0001 { // undefined — treat as "not present" so callers fall back cleanly. None diff --git a/crates/perry-runtime/src/object/regex_proto_thunks.rs b/crates/perry-runtime/src/object/regex_proto_thunks.rs index be0ccac2c5..d710fa33ab 100644 --- a/crates/perry-runtime/src/object/regex_proto_thunks.rs +++ b/crates/perry-runtime/src/object/regex_proto_thunks.rs @@ -33,11 +33,11 @@ enum RegexReceiver { Prototype, } -/// Resolve `IMPLICIT_THIS` to a RegExp instance or `RegExp.prototype`, throwing +/// Resolve the `this` receiver to a RegExp instance or `RegExp.prototype`, throwing /// `TypeError` otherwise (matching the spec brand check shared by every /// flag/`source` getter). -fn regex_receiver_or_throw(getter: &str) -> RegexReceiver { - let receiver = crate::value::JSValue::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn regex_receiver_or_throw(this: crate::closure::JsThis, getter: &str) -> RegexReceiver { + let receiver = crate::value::JSValue::from_bits(this.bits()); if receiver.is_pointer() { let ptr = receiver.as_pointer::() as usize; if crate::regex::is_registered_regex(ptr) { @@ -78,8 +78,8 @@ fn regex_has_flag(re: *const crate::regex::RegExpHeader, flag: char) -> bool { /// Shared body for the boolean flag getters: regex → boolean, prototype → /// undefined, else TypeError. -fn flag_getter(getter: &str, flag: char) -> f64 { - match regex_receiver_or_throw(getter) { +fn flag_getter(this: crate::closure::JsThis, getter: &str, flag: char) -> f64 { + match regex_receiver_or_throw(this, getter) { RegexReceiver::Regex(re) => { f64::from_bits(crate::value::JSValue::bool(regex_has_flag(re, flag)).bits()) } @@ -89,49 +89,58 @@ fn flag_getter(getter: &str, flag: char) -> f64 { pub(super) extern "C" fn regex_proto_global_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("global", 'g') + flag_getter(this, "global", 'g') } pub(super) extern "C" fn regex_proto_ignore_case_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("ignoreCase", 'i') + flag_getter(this, "ignoreCase", 'i') } pub(super) extern "C" fn regex_proto_multiline_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("multiline", 'm') + flag_getter(this, "multiline", 'm') } pub(super) extern "C" fn regex_proto_dot_all_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("dotAll", 's') + flag_getter(this, "dotAll", 's') } pub(super) extern "C" fn regex_proto_sticky_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("sticky", 'y') + flag_getter(this, "sticky", 'y') } pub(super) extern "C" fn regex_proto_unicode_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("unicode", 'u') + flag_getter(this, "unicode", 'u') } pub(super) extern "C" fn regex_proto_unicode_sets_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("unicodeSets", 'v') + flag_getter(this, "unicodeSets", 'v') } pub(super) extern "C" fn regex_proto_has_indices_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - flag_getter("hasIndices", 'd') + flag_getter(this, "hasIndices", 'd') } pub(super) extern "C" fn regex_proto_source_getter( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - match regex_receiver_or_throw("source") { + match regex_receiver_or_throw(this, "source") { RegexReceiver::Regex(re) => { // `js_regexp_get_source` returns the *escaped* source string. let s = crate::regex::js_regexp_get_source(re); @@ -147,17 +156,19 @@ pub(super) extern "C" fn regex_proto_source_getter( /// `get RegExp.prototype.flags` — spec 22.2.6.4. Reads each flag property off /// the (generic) receiver via `Get` + `ToBoolean` and assembles in canonical /// order `d g i m s u v y`. Throws `TypeError` only if `this` is not an Object. -pub(super) extern "C" fn regex_proto_flags_getter(_c: *const crate::closure::ClosureHeader) -> f64 { +pub(super) extern "C" fn regex_proto_flags_getter( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { #[cfg(feature = "regex-engine")] { - let value = crate::regex::perex_api::finish(crate::regex::perex_match_search::flags( - crate::object::js_implicit_this_get(), - )); + let value = + crate::regex::perex_api::finish(crate::regex::perex_match_search::flags(this.as_f64())); crate::value::js_nanbox_string(value as i64) } #[cfg(not(feature = "regex-engine"))] { - let receiver = crate::value::JSValue::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let receiver = crate::value::JSValue::from_bits(this.bits()); // Type(R) must be Object. Pointer-tagged values are objects EXCEPT Symbols // (which are also pointer-tagged via the symbol side-table); a Symbol `this` // must throw a TypeError, not silently assemble "". @@ -230,9 +241,10 @@ fn install_getter(proto_obj: *mut ObjectHeader, name: &str, func_ptr: *const u8) #[cfg(feature = "regex-engine")] pub(super) extern "C" fn regex_proto_exec_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - let re = regex_instance_or_throw("exec"); + let re = regex_instance_or_throw(this, "exec"); let scope = crate::gc::RuntimeHandleScope::new(); let re = scope.root_raw_const_ptr(re); let s = crate::value::js_jsvalue_to_string_coerce(arg); @@ -263,10 +275,12 @@ pub(crate) fn is_builtin_regexp_exec(value: f64) -> bool { #[cfg(feature = "regex-engine")] pub(super) extern "C" fn regex_proto_test_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { let matched = crate::regex::perex_api::finish(crate::regex::perex_dispatch::test_value( - crate::object::js_implicit_this_get(), + this, + this.as_f64(), arg, )); f64::from_bits(crate::value::JSValue::bool(matched).bits()) @@ -282,10 +296,11 @@ pub(super) extern "C" fn regex_proto_test_thunk( #[cfg(feature = "regex-engine")] pub(super) extern "C" fn regex_proto_compile_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, pattern: f64, flags: f64, ) -> f64 { - let re = regex_instance_or_throw("compile"); + let re = regex_instance_or_throw(this, "compile"); crate::regex::js_regexp_compile_value(re as *mut crate::regex::RegExpHeader, pattern, flags) } @@ -295,8 +310,9 @@ pub(super) extern "C" fn regex_proto_compile_thunk( /// `RegExp.prototype.toString.call({ source: "x", flags: "g" })` works. pub(super) extern "C" fn regex_proto_to_string_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let receiver = crate::value::JSValue::from_bits(IMPLICIT_THIS.with(|c| c.get())); + let receiver = crate::value::JSValue::from_bits(this.bits()); if !receiver.is_pointer() || crate::symbol::is_registered_symbol(receiver.as_pointer::() as usize) { @@ -323,12 +339,15 @@ pub(super) extern "C" fn regex_proto_to_string_thunk( f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()) } -/// Resolve `IMPLICIT_THIS` to a live RegExp instance (with `[[RegExpMatcher]]`), +/// Resolve the `this` receiver to a live RegExp instance (with `[[RegExpMatcher]]`), /// throwing `TypeError` otherwise. Unlike the flag/`source` getters, this does /// NOT treat `RegExp.prototype` specially — builtin exec requires a matcher. #[cfg(feature = "regex-engine")] -fn regex_instance_or_throw(method: &str) -> *const crate::regex::RegExpHeader { - let receiver = crate::value::JSValue::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn regex_instance_or_throw( + this: crate::closure::JsThis, + method: &str, +) -> *const crate::regex::RegExpHeader { + let receiver = crate::value::JSValue::from_bits(this.bits()); if receiver.is_pointer() { let ptr = receiver.as_pointer::() as usize; if crate::regex::is_registered_regex(ptr) { @@ -864,7 +883,6 @@ pub(crate) fn regexp_get_property( as *const crate::regex::RegExpHeader; return crate::JSValue::from_bits(crate::regex::js_regexp_get_last_index(re).to_bits()); } - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let result = crate::exception::catch_js_throw(|| { if let Some(name) = &name { let addr = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as usize; @@ -892,7 +910,6 @@ pub(crate) fn regexp_get_property( }); crate::proxy::js_reflect_get(proto.get_nanbox_f64(), key, receiver.get_nanbox_f64()) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); drop(name); match result { Ok(value) => crate::JSValue::from_bits(value.to_bits()), diff --git a/crates/perry-runtime/src/object/reserved_floor.rs b/crates/perry-runtime/src/object/reserved_floor.rs index 20516eda4f..92afc5443e 100644 --- a/crates/perry-runtime/src/object/reserved_floor.rs +++ b/crates/perry-runtime/src/object/reserved_floor.rs @@ -242,7 +242,11 @@ mod tests { /// path `for…of`'s fused `js_for_of_next` takes). #[test] fn own_next_shadows_the_builtin_advance() { - extern "C" fn patched_next(_c: *const crate::closure::ClosureHeader, _arg: f64) -> f64 { + extern "C" fn patched_next( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, + ) -> f64 { unsafe { crate::iter_result::make_iter_result(JSValue::number(777.0), false) } } unsafe { diff --git a/crates/perry-runtime/src/object/restricted_function_store_tests.rs b/crates/perry-runtime/src/object/restricted_function_store_tests.rs index 01814225bd..625efd6c1d 100644 --- a/crates/perry-runtime/src/object/restricted_function_store_tests.rs +++ b/crates/perry-runtime/src/object/restricted_function_store_tests.rs @@ -2,18 +2,21 @@ extern "C" fn non_strict_restricted_store_test_body( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { 9525.0 } extern "C" fn strict_restricted_store_test_body( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { 9526.0 } extern "C" fn method_restricted_store_test_body( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { 9527.0 } diff --git a/crates/perry-runtime/src/object/string_proto_thunks.rs b/crates/perry-runtime/src/object/string_proto_thunks.rs index 593d2d12e7..7a46e0754d 100644 --- a/crates/perry-runtime/src/object/string_proto_thunks.rs +++ b/crates/perry-runtime/src/object/string_proto_thunks.rs @@ -163,6 +163,7 @@ fn install_generic_string_proto_methods(proto_obj: *mut ObjectHeader) { /// own method name off the closure. Args arrive as a rest array. pub(super) extern "C" fn string_proto_generic_thunk( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { // Copy the method name to an owned String — `string_this_or_throw` may run @@ -173,7 +174,7 @@ pub(super) extern "C" fn string_proto_generic_thunk( let name: String = unsafe { super::has_own_helpers::str_from_string_header(name_hdr) } .map(|s| s.to_string()) .unwrap_or_default(); - let s = string_this_or_throw(&name); + let s = string_this_or_throw(this, &name); let s_val = f64::from_bits(crate::value::JSValue::string_ptr(s).bits()); let args = super::global_this::global_this_rest_array_values(rest); let (args_ptr, args_len) = if args.is_empty() { @@ -247,13 +248,16 @@ fn throw_string_proto_nullish(method: &str) -> ! { } /// `RequireObjectCoercible(this)` + `ToString(this)` for the generic-`this` -/// String.prototype methods. `this` is the IMPLICIT_THIS receiver bound by +/// String.prototype methods. `this` is the receiver argument passed by /// `.call`/`.apply`/property dispatch. `null`/`undefined` throw `TypeError`; /// everything else (a primitive string, a boxed `String`/`Boolean`/`Number` /// object, a `{ toString }` object) coerces via the shared `js_string_coerce`, /// which runs user `toString`/`valueOf`. -fn string_this_or_throw(method: &str) -> *mut crate::string::StringHeader { - let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn string_this_or_throw( + this: crate::closure::JsThis, + method: &str, +) -> *mut crate::string::StringHeader { + let this = f64::from_bits(this.bits()); let jv = crate::value::JSValue::from_bits(this.to_bits()); if jv.is_undefined() || jv.is_null() { throw_string_proto_nullish(method); @@ -270,9 +274,10 @@ fn string_this_or_throw(method: &str) -> *mut crate::string::StringHeader { pub(super) extern "C" fn string_proto_char_at_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, pos: f64, ) -> f64 { - let s = string_this_or_throw("charAt"); + let s = string_this_or_throw(this, "charAt"); let idx = crate::string::js_string_index_to_i32(pos); let r = crate::string::js_string_char_at(s, idx); f64::from_bits(crate::value::JSValue::string_ptr(r).bits()) @@ -280,81 +285,93 @@ pub(super) extern "C" fn string_proto_char_at_thunk( pub(super) extern "C" fn string_proto_char_code_at_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, pos: f64, ) -> f64 { - let s = string_this_or_throw("charCodeAt"); + let s = string_this_or_throw(this, "charCodeAt"); let idx = crate::string::js_string_index_to_i32(pos); crate::string::js_string_char_code_at(s, idx) } pub(super) extern "C" fn string_proto_code_point_at_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, pos: f64, ) -> f64 { - let s = string_this_or_throw("codePointAt"); + let s = string_this_or_throw(this, "codePointAt"); let idx = crate::string::js_string_index_to_i32(pos); crate::string::js_string_code_point_at(s, idx) } pub(super) extern "C" fn string_proto_at_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, index: f64, ) -> f64 { - let s = string_this_or_throw("at"); + let s = string_this_or_throw(this, "at"); let idx = crate::string::js_string_index_to_i32(index); crate::string::js_string_at(s, idx) } pub(super) extern "C" fn string_proto_symbol_iterator_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let s = string_this_or_throw("[Symbol.iterator]"); + let s = string_this_or_throw(this, "[Symbol.iterator]"); crate::string::string_values_iter(s) } #[cfg(feature = "regex-engine")] -extern "C" fn string_proto_match_thunk(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { - crate::regex::js_string_match_js(crate::object::js_implicit_this_get(), arg) +extern "C" fn string_proto_match_thunk( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { + crate::regex::js_string_match_js(this.as_f64(), arg) } #[cfg(feature = "regex-engine")] -extern "C" fn string_proto_search_thunk(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { - crate::regex::js_string_search_js(crate::object::js_implicit_this_get(), arg) +extern "C" fn string_proto_search_thunk( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { + crate::regex::js_string_search_js(this.as_f64(), arg) } #[cfg(feature = "regex-engine")] extern "C" fn string_proto_match_all_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, arg: f64, ) -> f64 { - crate::regex::js_string_match_all_js(crate::object::js_implicit_this_get(), arg) + crate::regex::js_string_match_all_js(this.as_f64(), arg) } #[cfg(feature = "regex-engine")] extern "C" fn string_proto_replace_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, search: f64, replacement: f64, ) -> f64 { - crate::regex::js_string_replace_js(crate::object::js_implicit_this_get(), search, replacement) + crate::regex::js_string_replace_js(this.as_f64(), search, replacement) } #[cfg(feature = "regex-engine")] extern "C" fn string_proto_replace_all_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, search: f64, replacement: f64, ) -> f64 { - crate::regex::js_string_replace_all_js( - crate::object::js_implicit_this_get(), - search, - replacement, - ) + crate::regex::js_string_replace_all_js(this.as_f64(), search, replacement) } #[cfg(feature = "regex-engine")] extern "C" fn string_proto_split_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, separator: f64, limit: f64, ) -> f64 { - crate::regex::js_string_split_js(crate::object::js_implicit_this_get(), separator, limit) + crate::regex::js_string_split_js(this.as_f64(), separator, limit) } diff --git a/crates/perry-runtime/src/object/temporal_proto.rs b/crates/perry-runtime/src/object/temporal_proto.rs index 9a6acecbea..c3fc98909f 100644 --- a/crates/perry-runtime/src/object/temporal_proto.rs +++ b/crates/perry-runtime/src/object/temporal_proto.rs @@ -44,12 +44,13 @@ fn throw_brand(member: &str) -> ! { } /// Shared accessor getter for every `Temporal..prototype` field. Reads -/// `IMPLICIT_THIS`, derives the property name from the closure (`"get hour"` → +/// its `this` argument, derives the property name from the closure (`"get hour"` → /// `"hour"`), and routes to the brand router; a non-Temporal `this` throws. pub(super) extern "C" fn temporal_proto_getter_thunk( c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let recv = f64::from_bits(IMPLICIT_THIS.with(|x| x.get())); + let recv = f64::from_bits(this.bits()); let full = closure_name(c); let prop = full.strip_prefix("get ").unwrap_or(&full); match tdispatch::get_property(recv, prop) { @@ -59,13 +60,14 @@ pub(super) extern "C" fn temporal_proto_getter_thunk( } /// Shared method thunk for every `Temporal..prototype` method. Reads -/// `IMPLICIT_THIS`, derives the method name from the closure, brand-checks the +/// its `this` argument, derives the method name from the closure, brand-checks the /// receiver, and routes to the brand router with the rest-array args. pub(super) extern "C" fn temporal_proto_method_thunk( c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, rest: f64, ) -> f64 { - let recv = f64::from_bits(IMPLICIT_THIS.with(|x| x.get())); + let recv = f64::from_bits(this.bits()); let name = closure_name(c); if crate::temporal::temporal_kind(recv).is_none() { throw_brand(&name); diff --git a/crates/perry-runtime/src/object/tests.rs b/crates/perry-runtime/src/object/tests.rs index 77b9a3688a..c4ca9130fc 100644 --- a/crates/perry-runtime/src/object/tests.rs +++ b/crates/perry-runtime/src/object/tests.rs @@ -95,6 +95,7 @@ unsafe fn installed_builtin_method(ctor_name: &str, method_name: &str) -> f64 { extern "C" fn symbol_to_primitive_nan( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, hint: f64, ) -> f64 { let hint_value = JSValue::from_bits(hint.to_bits()); @@ -102,20 +103,27 @@ extern "C" fn symbol_to_primitive_nan( f64::NAN } -extern "C" fn value_of_finite(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn value_of_finite( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 1.0 } extern "C" fn symbol_to_primitive_this_object( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, hint: f64, ) -> f64 { let hint_value = JSValue::from_bits(hint.to_bits()); assert_eq!(js_string_to_rust(hint_value), "number"); - crate::object::js_implicit_this_get() + this.as_f64() } -extern "C" fn to_iso_string_sentinel(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn to_iso_string_sentinel( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let string = crate::string::js_string_from_bytes(b"iso".as_ptr(), 3); crate::value::js_nanbox_string(string as i64) } @@ -151,9 +159,9 @@ fn date_to_json_number_hint_honors_symbol_to_primitive() { crate::value::js_nanbox_pointer(value_of as i64), ); - let prev_this = js_implicit_this_set(receiver_value); - let result = catch_js(crate::object::date_proto_thunks::test_date_to_json_current_this); - js_implicit_this_set(prev_this); + let result = catch_js(|| { + crate::object::date_proto_thunks::test_date_to_json_with_this(receiver_value) + }); let result = result.expect("Date.prototype.toJSON should not throw"); assert!( @@ -192,9 +200,9 @@ fn date_to_json_symbol_to_primitive_object_result_throws() { crate::value::js_nanbox_pointer(to_iso as i64), ); - let prev_this = js_implicit_this_set(receiver_value); - let result = catch_js(crate::object::date_proto_thunks::test_date_to_json_current_this); - js_implicit_this_set(prev_this); + let result = catch_js(|| { + crate::object::date_proto_thunks::test_date_to_json_with_this(receiver_value) + }); assert!( result.is_err(), @@ -411,7 +419,10 @@ fn closure_name_can_be_redefined_with_define_property() { } } -extern "C" fn closure_accessor_getter(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn closure_accessor_getter( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { 4.0 } @@ -1309,12 +1320,12 @@ fn map_size_by_name_does_not_oob_read_keys_array() { } /// #7518: a `globalThis` built-in CONSTRUCTOR reached as a VALUE must never be -/// re-dispatched as a method name on `IMPLICIT_THIS`. +/// re-dispatched as a method name on the call's `this`. /// /// `try_dispatch_value_called_proto_method` exists for the #3716 uncurry-this /// idiom: a built-in *prototype method* invoked as a value arrives backed by the /// shared `global_this_builtin_noop_thunk`, so the helper recovers its recorded -/// `name` and re-dispatches `IMPLICIT_THIS.(…)` through the real by-name +/// `name` and re-dispatches `this.(…)` through the real by-name /// tower. Global constructors share that same no-op thunk, and the only thing /// keeping them out was incidental — they recorded no builtin `.length`. /// @@ -1322,8 +1333,8 @@ fn map_size_by_name_does_not_oob_read_keys_array() { /// `EventTarget.length` reads `0` like Node. That gave the EventTarget global a /// recorded length, opened the gate, and re-broke #6301: `class Bus extends /// EventTarget {}` has no static parent class id, so its `super()` runs the -/// parent VALUE through `js_fetch_or_value_super` — which binds `IMPLICIT_THIS` -/// to the new instance before the value call — and the helper turned that into +/// parent VALUE through `js_fetch_or_value_super` — which passes the new +/// instance as `this` to the value call — and the helper turned that into /// `bus.EventTarget()`, whose miss throws `TypeError: EventTarget is not a /// function`. `parity` is tag-gated, so the gap test that covers this sat red on /// `main` for a week unnoticed; this assertion lives in the per-PR `cargo-test` @@ -1342,10 +1353,11 @@ fn global_builtin_constructor_values_are_not_redispatched_by_name() { // PROCESS-global CLOSURE_PROPS table (#6965). let _global = crate::gc::global_side_table_test_lock(); // Give the pre-fix failure mode a real receiver to miss on, so a regression - // surfaces as a clean catchable throw rather than a dispatch on whatever - // `IMPLICIT_THIS` happened to hold. - let receiver = crate::value::js_nanbox_pointer(js_object_alloc(0, 0) as i64); - let prev_this = crate::object::js_implicit_this_set(receiver); + // surfaces as a clean catchable throw rather than a dispatch on an + // arbitrary receiver. + let root_scope = crate::gc::RuntimeHandleScope::new(); + let receiver = + root_scope.root_nanbox_f64(crate::value::js_nanbox_pointer(js_object_alloc(0, 0) as i64)); let mut with_recorded_length = 0usize; let mut offenders: Vec = Vec::new(); @@ -1362,7 +1374,12 @@ fn global_builtin_constructor_values_are_not_redispatched_by_name() { } let verdict = catch_js(|| { match unsafe { - crate::object::try_dispatch_value_called_proto_method(closure, std::ptr::null(), 0) + crate::object::try_dispatch_value_called_proto_method( + closure, + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + std::ptr::null(), + 0, + ) } { None => 1.0, Some(_) => 0.0, @@ -1375,12 +1392,10 @@ fn global_builtin_constructor_values_are_not_redispatched_by_name() { } } - crate::object::js_implicit_this_set(prev_this); - assert!( offenders.is_empty(), "globalThis built-in constructors must not be value-dispatched as \ - `IMPLICIT_THIS.(…)`; offenders: {offenders:?}" + `this.(…)`; offenders: {offenders:?}" ); // Non-vacuity: the bug needs the no-op thunk PLUS a recorded spec `.length`. // If nothing in the table carries a length, every entry above declined at the @@ -1566,7 +1581,12 @@ fn this_method_snapshot_survives_own_property_replacement() { }); let result = unsafe { - crate::closure::js_native_call_value(captured.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::js_native_call_value( + captured.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) }; assert_eq!( result.to_bits(), @@ -1632,7 +1652,14 @@ fn constructor_ref_method_value_resolves_static_over_instance_method() { let class_ref = super::native_module::class_constructor_ref_value(LEX_METHOD_TEST_CLASS_ID); let bound = super::native_module::js_class_method_bind(class_ref, NAME.as_ptr(), NAME.len()); - let result = unsafe { crate::closure::js_native_call_value(bound, std::ptr::null(), 0) }; + let result = unsafe { + crate::closure::js_native_call_value( + bound, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!( result, 42.0, "a method value extracted off the CONSTRUCTOR ref must dispatch the \ @@ -1644,8 +1671,14 @@ fn constructor_ref_method_value_resolves_static_over_instance_method() { let proto_ref = super::native_module::class_prototype_ref_value(LEX_METHOD_TEST_CLASS_ID); let bound_proto = super::native_module::js_class_method_bind(proto_ref, NAME.as_ptr(), NAME.len()); - let result_proto = - unsafe { crate::closure::js_native_call_value(bound_proto, std::ptr::null(), 0) }; + let result_proto = unsafe { + crate::closure::js_native_call_value( + bound_proto, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; assert_eq!( result_proto, 7.0, "a method value extracted off the PROTOTYPE ref must still dispatch \ diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index 15f47fdbf6..49c8bf60ff 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -5,51 +5,6 @@ use super::*; use std::cell::{Cell, RefCell}; -// Implicit `this` for closure-typed class fields invoked method-style. -// -// Issue #519: when `obj.fn(args)` calls a closure stored as a class field, -// the field-scan dispatch in `js_native_call_method` can't bind `this` -// through the closure ABI (closures take `(closure_ptr, arg0, …)` — no -// `this` slot). Hono's RegExpRouter does this with `match = match` (the -// imported function from matcher.js), and the function body's -// `this.buildAllMatchers()` reads `this = 0` and TypeErrors out. -// -// Codegen for `Expr::This` (perry-codegen/src/expr.rs) reads from this -// thread-local when the lexical `this_stack` is empty (i.e. inside a -// non-arrow function body or top-level closure body). The field-scan -// dispatch saves the previous value, sets it to the receiver, calls the -// closure, then restores. Direct function calls (`fn(args)`) don't touch -// this slot, so non-method invocations don't pollute it across calls. -// -// Defaults to `TAG_UNDEFINED`. JS spec says top-level `this` is undefined -// in strict mode, which matches. -// The implicit-`this` cell itself lives INLINE in this thread's -// `tls_hot::HotTls` (`implicit_this`), so the save/restore pair around every -// dynamically-dispatched call — two calls per call — can be performed by -// generated code on Apple aarch64 without entering the runtime at all; the -// accessors below are the portable path and the fallback. -#[inline(always)] -fn implicit_this_cell() -> &'static Cell { - &crate::tls_hot::hot().implicit_this -} - -/// The `IMPLICIT_THIS.with(|cell| …)` shape the runtime's dispatch paths and -/// prototype thunks already use, re-backed by the inline `HotTls` field above -/// instead of a `perry_thread_local!` slot. Preserving the shape keeps the -/// move invisible at those ~120 call sites while making every one of them one -/// hop shorter — and it is the same cell generated code now reads and writes -/// directly on Apple aarch64. -pub(crate) struct ImplicitThisSlot; - -impl ImplicitThisSlot { - #[inline(always)] - pub(crate) fn with(&self, f: impl FnOnce(&Cell) -> R) -> R { - f(implicit_this_cell()) - } -} - -pub(crate) static IMPLICIT_THIS: ImplicitThisSlot = ImplicitThisSlot; - crate::perry_thread_local! { pub(crate) static NEW_TARGET: Cell = const { Cell::new(crate::value::TAG_UNDEFINED) }; // One-shot receiver override for STATIC method bodies. A compiled static @@ -64,7 +19,7 @@ crate::perry_thread_local! { static STATIC_THIS_OVERRIDE: Cell<(bool, u64)> = const { Cell::new((false, crate::value::TAG_UNDEFINED)) }; /// Lexical ClassDefinitionEvaluation owner for static method/accessor - /// dispatch. Unlike IMPLICIT_THIS, this is not replaced by `.call`'s + /// dispatch. Unlike the `this` argument, this is not replaced by `.call`'s /// visible receiver. A stack makes nested dispatch frame-local. static STATIC_PRIVATE_OWNER_STACK: RefCell> = const { RefCell::new(crate::exception::CatchStack::new( @@ -172,6 +127,11 @@ pub extern "C" fn js_static_this_resolve(default_this: f64) -> f64 { }) } +/// Keepalive anchor — `js_this_coerce_sloppy` is called only from generated +/// code (a sloppy body's receiver prologue). +#[cfg(feature = "keepalive-anchors")] +#[used(compiler)] +static KEEP_JS_THIS_COERCE_SLOPPY: extern "C" fn(f64) -> f64 = js_this_coerce_sloppy; /// [`js_static_this_resolve`] for a static method of class `class_id`: the /// armed override if any, else the class's function object, cached in the /// method's own zero-initialised `slot` (the object is pinned, so the cached @@ -208,16 +168,13 @@ pub unsafe extern "C" fn js_static_this_resolve_class(class_id: i32, slot: *mut value } -/// Read the current implicit `this` (issue #519). +/// OrdinaryCallBindThis for a non-strict body: `undefined`/`null` become +/// `globalThis`, a boolean/string/number its wrapper object; objects and +/// class refs are unchanged. Emitted at the entry of a sloppy body that reads +/// its receiver (the `this` parameter, `perry_abi::JS_BODY_THIS_PARAM`) — +/// only on the non-object path. Can allocate: a GC safepoint. #[no_mangle] -pub extern "C" fn js_implicit_this_get() -> f64 { - f64::from_bits(implicit_this_cell().get()) -} - -/// Read implicit `this` using ordinary (non-strict) function binding rules. -#[no_mangle] -pub extern "C" fn js_implicit_this_get_sloppy() -> f64 { - let value = js_implicit_this_get(); +pub extern "C" fn js_this_coerce_sloppy(value: f64) -> f64 { let jv = crate::value::JSValue::from_bits(value.to_bits()); if jv.is_undefined() || jv.is_null() { return js_get_global_this(); @@ -246,94 +203,6 @@ pub extern "C" fn js_implicit_this_get_sloppy() -> f64 { value } -/// Set the implicit `this` and return the previous value. -/// Callers must restore the previous value to scope the binding to the -/// duration of a single method-style call. -/// -/// **The returned previous value is a GC-managed pointer held in a bare Rust -/// local** (#9417, #9445). It is the caller's receiver, and the call this -/// save/restore brackets is user code, so an evacuating young-gen minor inside -/// it moves that object and rewrites every slot the collector can see — a -/// Rust local is not one. Restoring the stale bits then installs a retired -/// from-space address as the caller's `this`, and the failure is silent: -/// `js_object_get_own_field_or_undef` fails its type check on the recycled cell -/// and answers `undefined`, so the caller's next `this.` throws a -/// TypeError naming a property nowhere near the defect. Every runtime site -/// therefore roots the saved value in a `RuntimeHandleScope` and re-reads it at -/// the restore: -/// -/// ```ignore -/// let this_scope = crate::gc::RuntimeHandleScope::new(); -/// let prev = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); -/// … user code … -/// js_implicit_this_set(prev.get_nanbox_f64()); -/// ``` -/// -/// or, when the restore must also run as the callee unwinds, the -/// [`ImplicitThisScope`] guard, which is that idiom with the restore in `Drop`. -/// -/// This is longjmp-safe: `exception.rs` saves and restores the handle stack at -/// trap boundaries, so a throw through the window truncates the scope exactly -/// as a normal drop would. Inside a loop, open the scope PER ITERATION (or -/// reuse an existing per-iteration scope) so the handle stack does not grow by -/// one slot per callback. -/// -/// This is part of every dynamically-dispatched call's save/restore path. -/// Keep the shipped path to one TLS replacement; default-off diagnostics here -/// still impose their mode checks millions of times on closure-heavy programs. -#[no_mangle] -pub extern "C" fn js_implicit_this_set(value: f64) -> f64 { - f64::from_bits(implicit_this_cell().replace(value.to_bits())) -} - -/// Bind `IMPLICIT_THIS` for the lifetime of the guard and restore the -/// displaced value on the way out — including when the callee unwinds, which -/// the runtime's `extern "C-unwind"` dispatch surfaces make an ordinary -/// outcome: a plain set/restore pair leaks the receiver into every later -/// implicit-`this` read once a callback throws (#9244). -/// -/// The displaced value is the CALLER's receiver, held across the user code the -/// guard brackets, so it lives in a slot of the borrowed `RuntimeHandleScope` -/// — marked, and rewritten when an evacuating minor moves it — and `Drop` -/// re-reads that slot. A guard that keeps it in a plain field is the #9445 -/// shape with the restore moved into `Drop`, where a sweep for -/// `let prev = js_implicit_this_set(..)` cannot see it: the private guards this -/// replaced reinstalled a retired from-space address as the caller's `this` -/// after `Object.setPrototypeOf(o, proto); o.run()` ran an allocating method -/// (#10490), and after every `Array.prototype` callback method. -/// -/// The borrow is what makes the order safe: the scope must be declared before -/// the guard, so the guard's `Drop` runs while its slot is still on the handle -/// stack. -pub struct ImplicitThisScope<'scope> { - previous: crate::gc::RuntimeHandle<'scope>, -} - -impl<'scope> ImplicitThisScope<'scope> { - #[inline] - pub fn bind(scope: &'scope crate::gc::RuntimeHandleScope, receiver: f64) -> Self { - Self { - previous: scope.root_nanbox_f64(js_implicit_this_set(receiver)), - } - } - - /// Bind `this` to `undefined`: OrdinaryCallBindThis for a callback the - /// runtime invokes with no receiver (`sort` comparators, `reduce` - /// callbacks, an absent `thisArg`). Without it the callee reads whatever - /// the enclosing method dispatch left in the cell (#11419). - #[inline] - pub fn bind_undefined(scope: &'scope crate::gc::RuntimeHandleScope) -> Self { - Self::bind(scope, f64::from_bits(crate::value::TAG_UNDEFINED)) - } -} - -impl Drop for ImplicitThisScope<'_> { - #[inline] - fn drop(&mut self) { - js_implicit_this_set(self.previous.get_nanbox_f64()); - } -} - /// Read the current `new.target` value for ordinary function bodies. #[no_mangle] pub extern "C" fn js_new_target_get() -> f64 { @@ -384,32 +253,11 @@ impl Drop for SuperNewTargetScope<'_> { } } -/// `catch_savepoints!` capture/restore for `IMPLICIT_THIS` (PR #10564 review -/// finding). Several runtime guards displace `IMPLICIT_THIS` around a call -/// they don't control — a `super()` bridge, a prototype-walk accessor -/// dispatch, a stdlib listener/getter dispatcher — with a bare -/// save/call/restore statement sequence, not `ImplicitThisScope`. Neither a -/// `longjmp` nor a system unwind runs the restore statement that follows the -/// call, so a throw crossing one of those sites leaves the callee's receiver -/// installed for every later implicit-`this` read. This closes that gap the -/// same way `runtime_handles`/`call_method` already do: captured at every `try`, -/// replayed by `js_throw` before the exception transports, regardless of -/// transport. It is an unconditional `set`, so it composes safely with a -/// `ImplicitThisScope::drop` that also fires on the unwind path: whichever -/// runs last for a given frame reproduces the same locally-correct value. -#[inline] -pub(crate) fn implicit_this_trap_savepoint() -> u64 { - implicit_this_cell().get() -} - -pub(crate) fn implicit_this_trap_restore(bits: u64) { - implicit_this_cell().set(bits); -} - -/// `catch_savepoints!` capture/restore for `NEW_TARGET`. Same rationale as -/// [`implicit_this_trap_savepoint`]: the Temporal/Intl subclass `super()` -/// bridges (`fetch_globals.rs`, `intl/subclass.rs`) save/restore `new.target` -/// with a bare statement pair around the parent constructor call. +/// `catch_savepoints!` capture/restore for `NEW_TARGET`: the Temporal/Intl +/// subclass `super()` bridges (`fetch_globals.rs`, `intl/subclass.rs`) +/// save/restore `new.target` with a bare statement pair around the parent +/// constructor call, which neither a `longjmp` nor a system unwind runs, so +/// every `try` captures it and `js_throw` replays it. #[inline] pub(crate) fn new_target_trap_savepoint() -> u64 { NEW_TARGET.with(|c| c.get()) @@ -419,37 +267,15 @@ pub(crate) fn new_target_trap_restore(bits: u64) { NEW_TARGET.with(|c| c.set(bits)); } -/// GC mutable-root scanner for the implicit-`this` cell (issue #1813). -/// -/// `IMPLICIT_THIS` holds the NaN-boxed receiver for the duration of a -/// dynamically-dispatched non-arrow method body — set then restored by -/// `js_native_call_method` and by the codegen `js_implicit_this_set` -/// save/restore around `js_native_call_value`. That receiver is a live -/// heap object for the whole call, but the cell is plain thread-local -/// storage, so before this scanner it was invisible to GC: not a root. -/// -/// When a moving GC runs *during* the method body — e.g. a nested stdlib -/// pump draining network IO for `@perryts/mysql`'s `Pool.acquire` → -/// handshake → `nativeScramble` under concurrent load — the receiver is -/// evacuated/copied. Without a root slot to rewrite, the cell kept the -/// stale pre-move pointer and the body's next `this`-derived dispatch -/// dereferenced freed/relocated memory: the concurrent-load SIGSEGV in -/// `js_native_call_method` reported in #1813. (It only surfaced under -/// memory pressure because nursery copying / old-gen evacuation only move -/// objects then — hence the load-dependent heisenbug.) -/// -/// Marking also keeps `this` reachable when the cell is its only root. -/// Non-pointer tags (the `TAG_UNDEFINED` default, plus null/int/bool) -/// flow through `visit_nanbox_bits` as no-ops, so scanning the idle cell -/// is safe. -pub fn scan_implicit_this_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - { - let c = implicit_this_cell(); - let mut bits = c.get(); - if visitor.visit_nanbox_u64_slot(&mut bits) { - c.set(bits); - } - } +/// GC mutable-root scanner for the dispatch-binding cells: `new.target`, the +/// one-shot static-`this` override and the static private-owner stack. Each +/// holds a NaN-boxed heap value (a constructor, a receiver) for the duration +/// of a call, in plain thread-local storage the collector cannot see, so it +/// marks them and rewrites them when a moving collection relocates the value +/// (the #1813 class: a stale pre-move pointer read after a nested collection). +/// Non-pointer tags flow through `visit_nanbox_u64_slot` as no-ops, so an +/// idle cell is safe to scan. +pub fn scan_dispatch_binding_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { NEW_TARGET.with(|c| { let mut bits = c.get(); if visitor.visit_nanbox_u64_slot(&mut bits) { diff --git a/crates/perry-runtime/src/object/to_string_tag.rs b/crates/perry-runtime/src/object/to_string_tag.rs index 16b18a1df9..3052f57ac9 100644 --- a/crates/perry-runtime/src/object/to_string_tag.rs +++ b/crates/perry-runtime/src/object/to_string_tag.rs @@ -482,7 +482,7 @@ pub unsafe extern "C" fn js_object_to_string(value: f64) -> f64 { tag_str = Some("Error".to_string()); } if let Some(func_ptr) = lookup_to_string_tag_hook(class_id) { - let getter: extern "C" fn(f64) -> f64 = std::mem::transmute(func_ptr as *const u8); + let getter = crate::closure::body_call::js_bare_body_fn!(func_ptr as *const u8; a0); let result_f64 = getter(value); let rbits = result_f64.to_bits(); if (rbits & 0xFFFF_0000_0000_0000) == STRING_TAG { diff --git a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs index 7484816602..026f9ec0ad 100644 --- a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs +++ b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs @@ -17,7 +17,7 @@ //! regular Array helper, which (after the array-like-receiver change) silently //! accepted a plain array and returned a wrong-but-non-throwing result. //! -//! These thunks instead read the `IMPLICIT_THIS` receiver (set by the +//! These thunks instead take the receiver as their `this` argument (set by the //! `.call`/`.apply` dispatch), brand-check it via `lookup_typed_array_kind`, //! throw a `TypeError` on a non-TypedArray receiver, and otherwise delegate to //! the existing `dispatch_typed_array_method` tower (the same code the fast @@ -155,11 +155,14 @@ fn throw_not_typed_array(method: &str) -> ! { )) } -/// Read the `IMPLICIT_THIS` receiver and brand-check it as a real TypedArray. +/// Brand-check the `this` receiver as a real TypedArray. /// Returns the cleaned receiver, or throws a `TypeError`. #[inline] -unsafe fn ta_receiver_or_throw(method: &str) -> TypedArrayProtoReceiver { - let bits = IMPLICIT_THIS.with(|c| c.get()); +unsafe fn ta_receiver_or_throw( + this: crate::closure::JsThis, + method: &str, +) -> TypedArrayProtoReceiver { + let bits = this.bits(); // A TypedArray receiver reaches here in either of two boxings: a NaN-boxed // `POINTER_TAG` value (top16 >= 0x7FF8) or a *raw* heap pointer whose top16 // is 0 (the receiver-typed fast path threads the bare pointer — see the @@ -666,7 +669,7 @@ macro_rules! rooted_callback { fn call(&self, $($arg: f64),+) -> f64 { self.handle .with_const_ptr::(|cb| { - self.site.call(cb, $($arg),+) + self.site.call(cb, crate::closure::plain_call_receiver(), $($arg),+) }) } } @@ -967,8 +970,8 @@ pub(crate) unsafe fn dispatch_uint8_buffer_method( /// `TYPED_ARRAY_PROTO_METHODS`; the `unwrap_or(undefined)` guard never fires in /// practice (the name set is kept in sync) but avoids a panic on drift. #[inline] -unsafe fn brand_then_dispatch(method: &str, args: &[f64]) -> f64 { - let receiver = ta_receiver_or_throw(method); +unsafe fn brand_then_dispatch(this: crate::closure::JsThis, method: &str, args: &[f64]) -> f64 { + let receiver = ta_receiver_or_throw(this, method); let args_ptr = if args.is_empty() { std::ptr::null() } else { @@ -1012,12 +1015,13 @@ macro_rules! ta_thunk { ($name:ident, $method:literal, $argc:literal) => { pub(super) extern "C" fn $name( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, a: f64, b: f64, d: f64, ) -> f64 { let all = [a, b, d]; - unsafe { brand_then_dispatch($method, &all[..$argc]) } + unsafe { brand_then_dispatch(this, $method, &all[..$argc]) } } }; } @@ -1052,34 +1056,37 @@ ta_thunk!(ta_with_thunk, "with", 2); pub(super) extern "C" fn ta_last_index_of_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, search_element: f64, rest: f64, ) -> f64 { unsafe { let args = first_arg_with_rest(search_element, rest); - brand_then_dispatch("lastIndexOf", &args) + brand_then_dispatch(this, "lastIndexOf", &args) } } pub(super) extern "C" fn ta_reduce_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, callback: f64, rest: f64, ) -> f64 { unsafe { let args = first_arg_with_rest(callback, rest); - brand_then_dispatch("reduce", &args) + brand_then_dispatch(this, "reduce", &args) } } pub(super) extern "C" fn ta_reduce_right_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, callback: f64, rest: f64, ) -> f64 { unsafe { let args = first_arg_with_rest(callback, rest); - brand_then_dispatch("reduceRight", &args) + brand_then_dispatch(this, "reduceRight", &args) } } @@ -1089,6 +1096,7 @@ pub(super) extern "C" fn ta_reduce_right_thunk( /// but keeps the install path total. pub(super) extern "C" fn ta_generic_thunk( c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, a: f64, b: f64, d: f64, @@ -1099,7 +1107,7 @@ pub(super) extern "C" fn ta_generic_thunk( let name = super::has_own_helpers::str_from_string_header(name_hdr).unwrap_or("TypedArray method"); let all = [a, b, d]; - brand_then_dispatch(name, &all) + brand_then_dispatch(this, name, &all) } } @@ -1134,13 +1142,13 @@ fn throw_not_uint8_array(method: &str) -> ! { )) } -/// Resolve the `IMPLICIT_THIS` receiver to a Uint8Array-backed buffer address. +/// Resolve the `this` receiver to a Uint8Array-backed buffer address. /// The base64/hex methods only exist on Uint8Array, so a multi-byte typed-array /// receiver (`Uint8Array.prototype.toBase64.call(new Int8Array())`) is rejected /// with a `TypeError` just like Node's `ValidateUint8Array`. #[inline] -unsafe fn uint8_receiver_or_throw(method: &str) -> usize { - match ta_receiver_or_throw(method) { +unsafe fn uint8_receiver_or_throw(this: crate::closure::JsThis, method: &str) -> usize { + match ta_receiver_or_throw(this, method) { TypedArrayProtoReceiver::Uint8Buffer(addr) => addr, TypedArrayProtoReceiver::TypedArray(_) => throw_not_uint8_array(method), } @@ -1148,12 +1156,13 @@ unsafe fn uint8_receiver_or_throw(method: &str) -> usize { pub(super) extern "C" fn u8_to_base64_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, opts: f64, _b: f64, _d: f64, ) -> f64 { unsafe { - let addr = uint8_receiver_or_throw("toBase64"); + let addr = uint8_receiver_or_throw(this, "toBase64"); let args = [opts]; super::dispatch_buffer_method(addr, "toBase64", args.as_ptr(), args.len()) } @@ -1161,24 +1170,26 @@ pub(super) extern "C" fn u8_to_base64_thunk( pub(super) extern "C" fn u8_to_hex_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _a: f64, _b: f64, _d: f64, ) -> f64 { unsafe { - let addr = uint8_receiver_or_throw("toHex"); + let addr = uint8_receiver_or_throw(this, "toHex"); super::dispatch_buffer_method(addr, "toHex", std::ptr::null(), 0) } } pub(super) extern "C" fn u8_set_from_base64_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, str_arg: f64, opts: f64, _d: f64, ) -> f64 { unsafe { - let addr = uint8_receiver_or_throw("setFromBase64"); + let addr = uint8_receiver_or_throw(this, "setFromBase64"); let args = [str_arg, opts]; super::dispatch_buffer_method(addr, "setFromBase64", args.as_ptr(), args.len()) } @@ -1186,12 +1197,13 @@ pub(super) extern "C" fn u8_set_from_base64_thunk( pub(super) extern "C" fn u8_set_from_hex_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, str_arg: f64, _b: f64, _d: f64, ) -> f64 { unsafe { - let addr = uint8_receiver_or_throw("setFromHex"); + let addr = uint8_receiver_or_throw(this, "setFromHex"); let args = [str_arg]; super::dispatch_buffer_method(addr, "setFromHex", args.as_ptr(), args.len()) } diff --git a/crates/perry-runtime/src/object/util_types.rs b/crates/perry-runtime/src/object/util_types.rs index 0674bff749..14f64b05e3 100644 --- a/crates/perry-runtime/src/object/util_types.rs +++ b/crates/perry-runtime/src/object/util_types.rs @@ -433,8 +433,12 @@ mod buffer_view_tests { expected_view ); assert_eq!( - super::super::global_this::array_buffer_is_view_thunk(std::ptr::null(), value) - .to_bits(), + super::super::global_this::array_buffer_is_view_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + value + ) + .to_bits(), expected_view, ); assert_eq!( diff --git a/crates/perry-runtime/src/object/view_brand_tests.rs b/crates/perry-runtime/src/object/view_brand_tests.rs index 8e738fc933..79e28d8d01 100644 --- a/crates/perry-runtime/src/object/view_brand_tests.rs +++ b/crates/perry-runtime/src/object/view_brand_tests.rs @@ -32,7 +32,11 @@ fn row(value: f64) -> Row { use crate::object::js_instanceof; Row { is_view_direct: is_true(crate::object::js_util_types_is_array_buffer_view(value)), - is_view_value: is_true(array_buffer_is_view_thunk(std::ptr::null(), value)), + is_view_value: is_true(array_buffer_is_view_thunk( + std::ptr::null(), + crate::closure::JsThis::UNDEFINED, + value, + )), typed: is_true(crate::object::js_util_types_is_typed_array(value)), uint8: is_true(crate::object::js_util_types_is_uint8_array(value)), int8: is_true(crate::object::js_util_types_is_int8_array(value)), diff --git a/crates/perry-runtime/src/object/weakref_proto_thunks.rs b/crates/perry-runtime/src/object/weakref_proto_thunks.rs index 05629d1b73..6b3f213803 100644 --- a/crates/perry-runtime/src/object/weakref_proto_thunks.rs +++ b/crates/perry-runtime/src/object/weakref_proto_thunks.rs @@ -242,11 +242,16 @@ fn throw_incompatible(proto: &str, method: &str) -> ! { )) } -/// Resolve `IMPLICIT_THIS` to a receiver of the expected weak-wrapper class id, +/// Resolve the `this` receiver to a receiver of the expected weak-wrapper class id, /// or throw a `TypeError`. Mirrors `collection_proto_thunks`' /// `weak_receiver_or_throw` for the WeakRef/FinalizationRegistry pair. -fn wrapper_receiver_or_throw(expected: u32, proto: &str, method: &str) -> f64 { - let receiver = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); +fn wrapper_receiver_or_throw( + this: crate::closure::JsThis, + expected: u32, + proto: &str, + method: &str, +) -> f64 { + let receiver = f64::from_bits(this.bits()); if is_weak_wrapper(receiver, expected) { receiver } else { @@ -256,18 +261,21 @@ fn wrapper_receiver_or_throw(expected: u32, proto: &str, method: &str) -> f64 { pub(super) extern "C" fn weakref_proto_deref_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let r = wrapper_receiver_or_throw(CLASS_ID_WEAKREF, "WeakRef.prototype", "deref"); + let r = wrapper_receiver_or_throw(this, CLASS_ID_WEAKREF, "WeakRef.prototype", "deref"); js_weakref_deref(r) } pub(super) extern "C" fn finreg_proto_register_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, target: f64, held: f64, token: f64, ) -> f64 { let r = wrapper_receiver_or_throw( + this, CLASS_ID_FINALIZATION_REGISTRY, "FinalizationRegistry.prototype", "register", @@ -277,9 +285,11 @@ pub(super) extern "C" fn finreg_proto_register_thunk( pub(super) extern "C" fn finreg_proto_unregister_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, token: f64, ) -> f64 { let r = wrapper_receiver_or_throw( + this, CLASS_ID_FINALIZATION_REGISTRY, "FinalizationRegistry.prototype", "unregister", @@ -366,11 +376,18 @@ mod tests { /// answer, so the assertions below cannot pass by accident. const FOREIGN_SENTINEL: i32 = 7947; - extern "C" fn foreign_method_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn foreign_method_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { f64::from_bits(JSValue::int32(FOREIGN_SENTINEL).bits()) } - extern "C" fn foreign_method_thunk_1(_c: *const crate::closure::ClosureHeader, _a: f64) -> f64 { + extern "C" fn foreign_method_thunk_1( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _a: f64, + ) -> f64 { f64::from_bits(JSValue::int32(FOREIGN_SENTINEL).bits()) } diff --git a/crates/perry-runtime/src/os/os_process_emitter.rs b/crates/perry-runtime/src/os/os_process_emitter.rs index 7d91e5a191..a3f747065d 100644 --- a/crates/perry-runtime/src/os/os_process_emitter.rs +++ b/crates/perry-runtime/src/os/os_process_emitter.rs @@ -148,6 +148,7 @@ fn listener_lookup_ptr(listener_bits: i64) -> *const crate::closure::ClosureHead extern "C" fn process_once_raw_wrapper( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest_args: f64, ) -> f64 { if closure.is_null() { @@ -165,13 +166,23 @@ extern "C" fn process_once_raw_wrapper( let rest_jv = crate::value::JSValue::from_bits(rest_args.to_bits()); if !rest_jv.is_pointer() { return unsafe { - crate::closure::js_closure_call_array(target_ptr as i64, std::ptr::null(), 0) + crate::closure::js_closure_call_array( + target_ptr as i64, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) }; } let arr = rest_jv.as_pointer::(); if arr.is_null() { return unsafe { - crate::closure::js_closure_call_array(target_ptr as i64, std::ptr::null(), 0) + crate::closure::js_closure_call_array( + target_ptr as i64, + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) }; } let len = crate::array::js_array_length(arr) as usize; @@ -180,7 +191,12 @@ extern "C" fn process_once_raw_wrapper( args.push(crate::array::js_array_get_f64(arr, i as u32)); } unsafe { - crate::closure::js_closure_call_array(target_ptr as i64, args.as_ptr(), args.len() as i64) + crate::closure::js_closure_call_array( + target_ptr as i64, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len() as i64, + ) } } @@ -378,6 +394,7 @@ pub(crate) fn emit_process_event(event: &str, args: &[f64]) -> bool { unsafe { crate::closure::js_closure_call_array( listener.callback as i64, + crate::closure::plain_call_receiver(), args.as_ptr(), args.len() as i64, ); diff --git a/crates/perry-runtime/src/os_process_streams.rs b/crates/perry-runtime/src/os_process_streams.rs index a784def14c..d5fa0857eb 100644 --- a/crates/perry-runtime/src/os_process_streams.rs +++ b/crates/perry-runtime/src/os_process_streams.rs @@ -50,6 +50,7 @@ fn callable_closure_ptr(value: f64) -> usize { /// fires the optional completion callback (see [`schedule_write_callback`]). extern "C" fn process_stdout_write_stub( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, arg2: f64, arg3: f64, @@ -62,6 +63,7 @@ extern "C" fn process_stdout_write_stub( /// `write` impl for process.stderr. Same as stdout, targeting fd 2. extern "C" fn process_stderr_write_stub( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, chunk: f64, arg2: f64, arg3: f64, @@ -76,6 +78,7 @@ extern "C" fn process_stderr_write_stub( /// optional completion callback so an awaited `stdin.write(x, cb)` resolves. extern "C" fn process_stdin_write_noop_stub( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _chunk: f64, arg2: f64, arg3: f64, @@ -86,6 +89,7 @@ extern "C" fn process_stdin_write_noop_stub( extern "C" fn process_stream_emit_stub( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { f64::from_bits(crate::value::TAG_TRUE) @@ -93,6 +97,7 @@ extern "C" fn process_stream_emit_stub( extern "C" fn process_stream_on_once_stub( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) @@ -143,6 +148,7 @@ fn stdin_reader_should_stop() -> bool { /// hold on the event loop and stops the reader. No-op return (`undefined`). extern "C" fn process_stdin_detach_stub( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { STDIN_DETACHED.store(true, std::sync::atomic::Ordering::Release); @@ -159,10 +165,11 @@ extern "C" fn process_stdin_detach_stub( /// delivery (#9676). Node's contract: an unref'd stdin still emits `'data'`. extern "C" fn process_stdin_unref_stub( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { STDIN_UNREFED.store(true, std::sync::atomic::Ordering::Release); - crate::object::js_implicit_this_get() + this.as_f64() } /// `process.stdin.ref()` — restore the event-loop hold (#9676). Was a no-op @@ -173,10 +180,11 @@ extern "C" fn process_stdin_unref_stub( /// stream. `resume()` remains the one call that restarts a stopped reader. extern "C" fn process_stdin_ref_stub( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { STDIN_UNREFED.store(false, std::sync::atomic::Ordering::Release); - crate::object::js_implicit_this_get() + this.as_f64() } thread_local! { @@ -674,6 +682,7 @@ pub fn stdin_chunk_jsvalue(chunk: &[u8]) -> f64 { /// the reader can decide. extern "C" fn process_stdin_set_encoding( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, encoding: f64, ) -> f64 { let name = stdin_event_name(encoding).unwrap_or_default(); @@ -827,6 +836,7 @@ pub fn enable_process_stdin_keypress_events(callback: i64) { /// `process.stdin.addListener(event, cb)` / `.on(...)` reached as an object method. extern "C" fn process_stdin_add_listener( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -838,12 +848,13 @@ extern "C" fn process_stdin_add_listener( } return stdin_this_value(); } - process_stdin_on(closure, event, callback) + process_stdin_on(closure, this, event, callback) } /// `process.stdin.once(event, cb)` reached as an object method. extern "C" fn process_stdin_add_listener_once( closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -855,12 +866,13 @@ extern "C" fn process_stdin_add_listener_once( } return stdin_this_value(); } - process_stdin_once(closure, event, callback) + process_stdin_once(closure, this, event, callback) } /// `process.stdin.removeListener(event, cb)` / `.off(...)`. extern "C" fn process_stdin_remove_listener( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -926,6 +938,7 @@ fn clear_stdin_listener_list(list: &std::sync::Mutex>) { /// call that Node treats as authoritative. extern "C" fn process_stdin_remove_all_listeners( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event: f64, ) -> f64 { let name = stdin_event_name(event); @@ -987,6 +1000,7 @@ extern "C" fn process_stdin_remove_all_listeners( /// as a real array (empty when there are none), like Node's EventEmitter. extern "C" fn process_stdin_listeners( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event: f64, ) -> f64 { let name = stdin_event_name(event).unwrap_or_default(); @@ -1181,6 +1195,7 @@ fn register_generic_stdin_keypress_listener(event: f64, callback: f64, once: boo /// listener and starts the reader. Returns `this` so callers can chain. extern "C" fn process_stdin_on( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -1214,12 +1229,13 @@ extern "C" fn process_stdin_on( _ => {} } } - crate::object::js_implicit_this_get() + this.as_f64() } /// `process.stdin.once(event, cb)` — fires the listener exactly once. extern "C" fn process_stdin_once( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -1251,13 +1267,17 @@ extern "C" fn process_stdin_once( _ => {} } } - crate::object::js_implicit_this_get() + this.as_f64() } /// `process.stdin.read([size])` — returns buffered input as a string (stdin is /// `setEncoding("utf8")` in practice) or `null` when nothing is buffered, per /// Node's `Readable.read()` contract. -extern "C" fn process_stdin_read(_closure: *const crate::closure::ClosureHeader, _arg: f64) -> f64 { +extern "C" fn process_stdin_read( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, +) -> f64 { let bytes = match STDIN_BUFFER.lock() { Ok(mut b) => std::mem::take(&mut *b), Err(_) => return f64::from_bits(crate::value::TAG_NULL), @@ -1304,6 +1324,7 @@ pub fn reset_process_stdin_liveness_for_tests() { /// paused stdin can resume. extern "C" fn process_stdin_resume( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _arg: f64, ) -> f64 { STDIN_DETACHED.store(false, std::sync::atomic::Ordering::Release); @@ -1317,7 +1338,7 @@ extern "C" fn process_stdin_resume( resume(); } ensure_stdin_reader(); - crate::object::js_implicit_this_get() + this.as_f64() } /// Drain buffered stdin and fire `data`/`readable` listeners. Called once per @@ -1366,86 +1387,8 @@ mod empty_checkpoint_tests { } #[cfg(test)] -mod reader_lifecycle_tests { - use super::{reader_slot_claim_start, reader_slot_claim_stop}; - use std::sync::atomic::{AtomicBool, Ordering}; - - /// #10895: replays the interleaving that stranded fd 0 without a reader — - /// the reader decides to stop, and `resume()` asks for a restart BEFORE - /// the dying reader has finished leaving. The stop decision must already - /// have released the reader slot, or the restart's claim fails and nobody - /// ever reads stdin again. - /// - /// Runs on local flags: no fd-0 reader is spawned and no process-global - /// stdin state is touched, so it cannot disturb the liveness tests. - #[test] - fn a_restart_requested_while_the_reader_is_leaving_is_not_lost() { - // A reader is running and `pause()` has latched the detach. - let started = AtomicBool::new(true); - let detached = AtomicBool::new(true); - assert!( - reader_slot_claim_stop(&started, || detached.load(Ordering::Acquire)), - "a detached reader must decide to stop" - ); - // `resume()`: clear the latch, then ask for a reader. The old reader - // has not run another instruction since its stop decision. - detached.store(false, Ordering::Release); - assert!( - reader_slot_claim_start(&started), - "restart lost: the stopping reader still held the reader slot" - ); - // The respawned reader owns the slot; a second request is a no-op. - assert!(!reader_slot_claim_start(&started)); - } - - /// The other order: `resume()` clears the latch before the reader looks. - /// The reader keeps running and no second reader may be started on fd 0. - #[test] - fn a_resume_that_beats_the_stop_check_keeps_the_one_reader() { - let started = AtomicBool::new(true); - let detached = AtomicBool::new(true); - detached.store(false, Ordering::Release); - assert!(!reader_slot_claim_start(&started)); - assert!(!reader_slot_claim_stop(&started, || detached.load(Ordering::Acquire))); - assert!(started.load(Ordering::Acquire)); - } - - /// Hammer the handshake from two threads: a "reader" that stops whenever - /// it sees the latch and a "main" that pauses/resumes. After every - /// resume the slot must be owned — by the surviving reader or by the - /// restart — never stranded. - #[test] - fn pause_resume_storm_never_strands_the_slot() { - use std::sync::Arc; - let started = Arc::new(AtomicBool::new(true)); - let detached = Arc::new(AtomicBool::new(false)); - let done = Arc::new(AtomicBool::new(false)); - let reader = { - let (started, detached, done) = (started.clone(), detached.clone(), done.clone()); - std::thread::spawn(move || { - while !done.load(Ordering::Acquire) { - // A live reader polls the latch between reads; one that - // stopped waits to be "respawned" by main's claim. - if started.load(Ordering::Acquire) { - reader_slot_claim_stop(&started, || detached.load(Ordering::Acquire)); - } - std::hint::spin_loop(); - } - }) - }; - for _ in 0..200_000 { - detached.store(true, Ordering::Release); // pause() - detached.store(false, Ordering::Release); // resume(): clear … - reader_slot_claim_start(&started); // … then ensure a reader - assert!( - started.load(Ordering::Acquire), - "resume() returned with no reader owning fd 0" - ); - } - done.store(true, Ordering::Release); - reader.join().unwrap(); - } -} +#[path = "os_process_streams/reader_lifecycle_tests.rs"] +mod reader_lifecycle_tests; fn pump_stdin_data_chunks() { let has_bytes = STDIN_BUFFER.lock().map(|b| !b.is_empty()).unwrap_or(false); @@ -1491,14 +1434,16 @@ fn pump_stdin_data_chunks() { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); // Node calls stream listeners with `this === stream`. Re-read the - // singleton per listener and root the displaced receiver: the previous - // listener was user code, so either may have moved (#9445). + // singleton per listener: the previous listener was user code, so it + // may have moved (#9445). let this = stdin_this_value(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { - crate::closure::js_closure_call1(closure, arg_handle.get_nanbox_f64()); + crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(this), + arg_handle.get_nanbox_f64(), + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } return; } @@ -1514,13 +1459,11 @@ fn pump_stdin_data_chunks() { for cb in readable_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - // Per-listener re-read + rooted save/restore (#9445), as for `data`. + // Per-listener re-read of the receiver (#9445), as for `data`. let this = stdin_this_value(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { - crate::closure::js_closure_call0(closure); + crate::closure::js_closure_call0(closure, crate::closure::JsThis::from_f64(this)); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -1562,13 +1505,15 @@ fn maybe_fire_stdin_end() { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - // Per-listener re-read + rooted save/restore (#9445), as for `data`. + // Per-listener re-read of the receiver (#9445), as for `data`. let this = stdin_this_value(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { - crate::closure::js_closure_call1(closure, flush_handle.get_nanbox_f64()); + crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(this), + flush_handle.get_nanbox_f64(), + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } } @@ -1599,13 +1544,11 @@ fn maybe_fire_stdin_end() { for cb in end_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); - // Per-listener re-read + rooted save/restore (#9445), as for `data`. + // Per-listener re-read of the receiver (#9445), as for `data`. let this = stdin_this_value(); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); cb_handle.with_const_ptr::(|closure| { - crate::closure::js_closure_call0(closure); + crate::closure::js_closure_call0(closure, crate::closure::JsThis::from_f64(this)); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -1661,7 +1604,7 @@ pub fn scan_process_stream_singleton_roots_mut(visitor: &mut crate::gc::RuntimeR /// Build a stream object with a `write` field bound to the given stub. fn build_stream_object_with_write( - write_stub: extern "C" fn(*const crate::closure::ClosureHeader, f64, f64, f64) -> f64, + write_stub: crate::closure::body_call::js_body_fn_ty!(a, a, a), fd: f64, writable: f64, ) -> *mut crate::object::ObjectHeader { @@ -1835,13 +1778,11 @@ fn build_stream_object_with_write( // replaces the stream stubs below with its real listener/flow operations; // stdout and stderr retain the stubs. if let Some(start) = teardown_start { - let set_field_with_stub = - |idx: u32, stub: extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64| { - let c = js_closure_alloc(stub as *const u8, 0); - js_object_set_field(obj, idx, JSValue::pointer(c as *const u8)); - }; - let lifecycle: extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64 = if is_stdin - { + let set_field_with_stub = |idx: u32, stub: crate::closure::body_call::js_body_fn_ty!(a)| { + let c = js_closure_alloc(stub as *const u8, 0); + js_object_set_field(obj, idx, JSValue::pointer(c as *const u8)); + }; + let lifecycle: crate::closure::body_call::js_body_fn_ty!(a) = if is_stdin { process_stdin_detach_stub } else { process_stream_on_once_stub diff --git a/crates/perry-runtime/src/os_process_streams/reader_lifecycle_tests.rs b/crates/perry-runtime/src/os_process_streams/reader_lifecycle_tests.rs new file mode 100644 index 0000000000..af8faae47f --- /dev/null +++ b/crates/perry-runtime/src/os_process_streams/reader_lifecycle_tests.rs @@ -0,0 +1,78 @@ +use super::{reader_slot_claim_start, reader_slot_claim_stop}; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// #10895: replays the interleaving that stranded fd 0 without a reader — +/// the reader decides to stop, and `resume()` asks for a restart BEFORE +/// the dying reader has finished leaving. The stop decision must already +/// have released the reader slot, or the restart's claim fails and nobody +/// ever reads stdin again. +/// +/// Runs on local flags: no fd-0 reader is spawned and no process-global +/// stdin state is touched, so it cannot disturb the liveness tests. +#[test] +fn a_restart_requested_while_the_reader_is_leaving_is_not_lost() { + // A reader is running and `pause()` has latched the detach. + let started = AtomicBool::new(true); + let detached = AtomicBool::new(true); + assert!( + reader_slot_claim_stop(&started, || detached.load(Ordering::Acquire)), + "a detached reader must decide to stop" + ); + // `resume()`: clear the latch, then ask for a reader. The old reader + // has not run another instruction since its stop decision. + detached.store(false, Ordering::Release); + assert!( + reader_slot_claim_start(&started), + "restart lost: the stopping reader still held the reader slot" + ); + // The respawned reader owns the slot; a second request is a no-op. + assert!(!reader_slot_claim_start(&started)); +} + +/// The other order: `resume()` clears the latch before the reader looks. +/// The reader keeps running and no second reader may be started on fd 0. +#[test] +fn a_resume_that_beats_the_stop_check_keeps_the_one_reader() { + let started = AtomicBool::new(true); + let detached = AtomicBool::new(true); + detached.store(false, Ordering::Release); + assert!(!reader_slot_claim_start(&started)); + assert!(!reader_slot_claim_stop(&started, || detached.load(Ordering::Acquire))); + assert!(started.load(Ordering::Acquire)); +} + +/// Hammer the handshake from two threads: a "reader" that stops whenever +/// it sees the latch and a "main" that pauses/resumes. After every +/// resume the slot must be owned — by the surviving reader or by the +/// restart — never stranded. +#[test] +fn pause_resume_storm_never_strands_the_slot() { + use std::sync::Arc; + let started = Arc::new(AtomicBool::new(true)); + let detached = Arc::new(AtomicBool::new(false)); + let done = Arc::new(AtomicBool::new(false)); + let reader = { + let (started, detached, done) = (started.clone(), detached.clone(), done.clone()); + std::thread::spawn(move || { + while !done.load(Ordering::Acquire) { + // A live reader polls the latch between reads; one that + // stopped waits to be "respawned" by main's claim. + if started.load(Ordering::Acquire) { + reader_slot_claim_stop(&started, || detached.load(Ordering::Acquire)); + } + std::hint::spin_loop(); + } + }) + }; + for _ in 0..200_000 { + detached.store(true, Ordering::Release); // pause() + detached.store(false, Ordering::Release); // resume(): clear … + reader_slot_claim_start(&started); // … then ensure a reader + assert!( + started.load(Ordering::Acquire), + "resume() returned with no reader owning fd 0" + ); + } + done.store(true, Ordering::Release); + reader.join().unwrap(); +} diff --git a/crates/perry-runtime/src/perf_hooks.rs b/crates/perry-runtime/src/perf_hooks.rs index 3ec25f3069..d5eb74eaad 100644 --- a/crates/perry-runtime/src/perf_hooks.rs +++ b/crates/perry-runtime/src/perf_hooks.rs @@ -1690,6 +1690,7 @@ fn schedule_flush() { #[no_mangle] pub extern "C" fn js_perf_observer_flush_all( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { FLUSH_SCHEDULED.with(|f| f.set(false)); let work: Vec<(u64, u64, Vec)> = OBSERVERS.with(|o| { diff --git a/crates/perry-runtime/src/perf_hooks/prototypes.rs b/crates/perry-runtime/src/perf_hooks/prototypes.rs index 72889e853d..b2a3f3d430 100644 --- a/crates/perry-runtime/src/perf_hooks/prototypes.rs +++ b/crates/perry-runtime/src/perf_hooks/prototypes.rs @@ -28,9 +28,12 @@ fn invalid_perf_receiver(class_name: &str) -> ! { ) } -extern "C" fn perf_entry_field_getter_thunk(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn perf_entry_field_getter_thunk( + closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { unsafe { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let Some(obj) = as_object_ptr(this) else { invalid_perf_receiver("PerformanceEntry"); }; @@ -52,9 +55,12 @@ extern "C" fn perf_entry_field_getter_thunk(closure: *const crate::closure::Clos } } -extern "C" fn perf_entry_to_json_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn perf_entry_to_json_thunk( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { unsafe { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); let Some(obj) = as_object_ptr(this) else { invalid_perf_receiver("PerformanceEntry"); }; @@ -65,12 +71,16 @@ extern "C" fn perf_entry_to_json_thunk(_closure: *const crate::closure::ClosureH } } -extern "C" fn perf_time_origin_getter_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn perf_time_origin_getter_thunk( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { time_origin_ms() } extern "C" fn perf_supported_entry_types_getter_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { perf_supported_entry_types_value() } @@ -83,9 +93,10 @@ pub(crate) fn perf_supported_entry_types_value() -> f64 { extern "C" fn perf_observer_observe_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, options: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); if !is_perf_observer_value(this) { invalid_perf_receiver("PerformanceObserver"); } @@ -94,8 +105,9 @@ extern "C" fn perf_observer_observe_thunk( extern "C" fn perf_observer_disconnect_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); if !is_perf_observer_value(this) { invalid_perf_receiver("PerformanceObserver"); } @@ -104,16 +116,20 @@ extern "C" fn perf_observer_disconnect_thunk( extern "C" fn perf_observer_take_records_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); if !is_perf_observer_value(this) { invalid_perf_receiver("PerformanceObserver"); } js_perf_observer_take_records(this) } -extern "C" fn perf_list_get_entries_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn perf_list_get_entries_thunk( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let this = this.as_f64(); if !is_perf_observer_list_value(this) { invalid_perf_receiver("PerformanceObserverEntryList"); } @@ -122,9 +138,10 @@ extern "C" fn perf_list_get_entries_thunk(_closure: *const crate::closure::Closu extern "C" fn perf_list_get_by_type_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, entry_type: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); if !is_perf_observer_list_value(this) { invalid_perf_receiver("PerformanceObserverEntryList"); } @@ -133,9 +150,10 @@ extern "C" fn perf_list_get_by_type_thunk( extern "C" fn perf_list_get_by_name_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, name: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); if !is_perf_observer_list_value(this) { invalid_perf_receiver("PerformanceObserverEntryList"); } diff --git a/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs b/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs index a1d6ad18a9..9ade00bac3 100644 --- a/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs +++ b/crates/perry-runtime/src/perf_hooks/prototypes/performance_methods.rs @@ -2,105 +2,124 @@ use super::*; -fn require_performance_receiver() { - if !is_performance_namespace_value(crate::object::js_implicit_this_get()) { +fn require_performance_receiver(this: crate::closure::JsThis) { + if !is_performance_namespace_value(this.as_f64()) { invalid_perf_receiver("Performance"); } } pub(super) extern "C" fn clear_marks( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, name: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_clear_marks(name) } pub(super) extern "C" fn clear_measures( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, name: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_clear_measures(name) } pub(super) extern "C" fn clear_resource_timings( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_clear_resource_timings() } -pub(super) extern "C" fn get_entries(_closure: *const crate::closure::ClosureHeader) -> f64 { - require_performance_receiver(); +pub(super) extern "C" fn get_entries( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + require_performance_receiver(this); js_perf_get_entries() } pub(super) extern "C" fn get_entries_by_name( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, name: f64, entry_type: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_get_entries_by_name(name, entry_type) } pub(super) extern "C" fn get_entries_by_type( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, entry_type: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_get_entries_by_type(entry_type) } pub(super) extern "C" fn mark( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, name: f64, options: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_mark(name, options) } pub(super) extern "C" fn measure( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, name: f64, start_or_options: f64, end: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_measure(name, start_or_options, end) } -pub(super) extern "C" fn now(_closure: *const crate::closure::ClosureHeader) -> f64 { - require_performance_receiver(); +pub(super) extern "C" fn now( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + require_performance_receiver(this); crate::date::js_performance_now() } pub(super) extern "C" fn set_resource_timing_buffer_size( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, size: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_set_resource_timing_buffer_size(size) } -pub(super) extern "C" fn to_json(_closure: *const crate::closure::ClosureHeader) -> f64 { - require_performance_receiver(); +pub(super) extern "C" fn to_json( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + require_performance_receiver(this); js_perf_to_json() } pub(super) extern "C" fn event_loop_utilization( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, utilization1: f64, utilization2: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_event_loop_utilization(utilization1, utilization2) } pub(super) extern "C" fn mark_resource_timing( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, timing_info: f64, requested_url: f64, initiator_type: f64, @@ -110,7 +129,7 @@ pub(super) extern "C" fn mark_resource_timing( response_status: f64, delivery_type: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_mark_resource_timing( timing_info, requested_url, @@ -125,9 +144,10 @@ pub(super) extern "C" fn mark_resource_timing( pub(super) extern "C" fn timerify( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, function: f64, options: f64, ) -> f64 { - require_performance_receiver(); + require_performance_receiver(this); js_perf_timerify(function, options) } diff --git a/crates/perry-runtime/src/perf_hooks/timerify.rs b/crates/perry-runtime/src/perf_hooks/timerify.rs index 1f442dc465..9e810fa125 100644 --- a/crates/perry-runtime/src/perf_hooks/timerify.rs +++ b/crates/perry-runtime/src/perf_hooks/timerify.rs @@ -48,6 +48,7 @@ unsafe fn finish_timerify_entry(name_value: f64, start_time: f64, histogram: f64 extern "C" fn perf_timerify_settle( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, outcome: f64, ) -> f64 { unsafe { @@ -64,6 +65,7 @@ extern "C" fn perf_timerify_settle( extern "C" fn perf_timerify_wrapper( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, rest: f64, ) -> f64 { unsafe { @@ -111,7 +113,12 @@ extern "C" fn perf_timerify_wrapper( if crate::object::class_ref_id(target).is_some() { throw_type_error("Class constructor cannot be invoked without 'new'"); } - crate::closure::js_native_call_value(target, call_args.as_ptr(), call_args.len()) + crate::closure::js_native_call_value( + target, + crate::closure::plain_call_receiver(), + call_args.as_ptr(), + call_args.len(), + ) }; let result_handle = scope.root_nanbox_f64(result); diff --git a/crates/perry-runtime/src/plugin.rs b/crates/perry-runtime/src/plugin.rs index b36b8c2d2c..3ccc9e803e 100644 --- a/crates/perry-runtime/src/plugin.rs +++ b/crates/perry-runtime/src/plugin.rs @@ -458,7 +458,7 @@ unsafe fn call_closure_1(handler_bits: u64, arg: f64) -> f64 { if closure_ptr.is_null() { return f64::from_bits(JSValue::undefined().bits()); } - crate::closure::js_closure_call1(closure_ptr, arg) + crate::closure::js_closure_call1(closure_ptr, crate::closure::plain_call_receiver(), arg) } // ============================================================================ @@ -694,7 +694,7 @@ pub extern "C" fn perry_plugin_unregister_service(api_handle: i64, name: f64) -> reg.services.remove(idx); } else { drop(reg); - crate::closure::js_closure_call0(stop_ptr); + crate::closure::js_closure_call0(stop_ptr, crate::closure::plain_call_receiver()); let mut reg = REGISTRY.lock().unwrap(); if let Some(idx2) = reg .services diff --git a/crates/perry-runtime/src/process.rs b/crates/perry-runtime/src/process.rs index b6e2027528..ad1c06e3bf 100644 --- a/crates/perry-runtime/src/process.rs +++ b/crates/perry-runtime/src/process.rs @@ -299,9 +299,8 @@ pub(crate) fn module_set_field_rooted( handle.with_mut_ptr(|obj: *mut crate::object::ObjectHeader| module_set_field(obj, name, value)); } -pub(crate) type ModuleFunction1 = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; -pub(crate) type ModuleFunction2 = - extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64; +pub(crate) type ModuleFunction1 = crate::closure::body_call::js_body_fn_ty!(a); +pub(crate) type ModuleFunction2 = crate::closure::body_call::js_body_fn_ty!(a, a); #[derive(Clone, Copy)] pub(crate) struct ModuleLoaderHookEntry { diff --git a/crates/perry-runtime/src/process/env_misc.rs b/crates/perry-runtime/src/process/env_misc.rs index cfba2ee481..88a93b3e7b 100644 --- a/crates/perry-runtime/src/process/env_misc.rs +++ b/crates/perry-runtime/src/process/env_misc.rs @@ -648,7 +648,10 @@ fn set_error_string_prop(error: *mut crate::error::ErrorHeader, name: &str, valu static WARNED_PROCESS_WARNING_TRACE_HINT: AtomicBool = AtomicBool::new(false); -extern "C" fn process_warning_callback(closure: *const ClosureHeader) -> f64 { +extern "C" fn process_warning_callback( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { use std::io::Write; if closure.is_null() { diff --git a/crates/perry-runtime/src/process/finalization.rs b/crates/perry-runtime/src/process/finalization.rs index 08c8dc93d7..93b7acf947 100644 --- a/crates/perry-runtime/src/process/finalization.rs +++ b/crates/perry-runtime/src/process/finalization.rs @@ -6,6 +6,7 @@ use super::*; extern "C" fn process_finalization_before_exit_listener( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _code: f64, ) -> f64 { js_process_run_finalization_before_exit(); @@ -163,7 +164,12 @@ fn run_process_finalization_callbacks(kind: ProcessFinalizationKind) { } let args = [obj_handle.get_nanbox_f64(), event_handle.get_nanbox_f64()]; unsafe { - crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); + crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } } } @@ -191,6 +197,7 @@ pub fn scan_process_finalization_roots_mut(visitor: &mut crate::gc::RuntimeRootV extern "C" fn process_finalization_register_function( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, callback: f64, ) -> f64 { @@ -199,6 +206,7 @@ extern "C" fn process_finalization_register_function( extern "C" fn process_finalization_register_before_exit_function( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, callback: f64, ) -> f64 { @@ -207,6 +215,7 @@ extern "C" fn process_finalization_register_before_exit_function( extern "C" fn process_finalization_unregister_function( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, obj: f64, ) -> f64 { process_finalization_unregister(obj) diff --git a/crates/perry-runtime/src/process/ipc.rs b/crates/perry-runtime/src/process/ipc.rs index 5b278d0f18..2afe84db9e 100644 --- a/crates/perry-runtime/src/process/ipc.rs +++ b/crates/perry-runtime/src/process/ipc.rs @@ -98,8 +98,8 @@ fn set_field(obj: *mut crate::object::ObjectHeader, name: &str, value: f64) { crate::object::js_object_set_field_by_name(obj, key, value); } -type IpcFunction0 = extern "C" fn(*const ClosureHeader) -> f64; -type IpcFunction4 = extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64; +type IpcFunction0 = crate::closure::body_call::js_body_fn_ty!(); +type IpcFunction4 = crate::closure::body_call::js_body_fn_ty!(a, a, a, a); fn ipc_function0(name: &str, thunk: IpcFunction0, length: u32) -> f64 { let func_ptr = thunk as *const u8; @@ -390,7 +390,10 @@ fn process_channel_value() -> f64 { object_value(obj) } -extern "C" fn process_channel_ref_fn(_closure: *const ClosureHeader) -> f64 { +extern "C" fn process_channel_ref_fn( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { process_ipc_ensure_initialized(); let mut state = ipc_lock(); if state.available && state.connected { @@ -399,7 +402,10 @@ extern "C" fn process_channel_ref_fn(_closure: *const ClosureHeader) -> f64 { undefined_value() } -extern "C" fn process_channel_unref_fn(_closure: *const ClosureHeader) -> f64 { +extern "C" fn process_channel_unref_fn( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { process_ipc_ensure_initialized(); let mut state = ipc_lock(); if state.available && state.connected { @@ -408,12 +414,16 @@ extern "C" fn process_channel_unref_fn(_closure: *const ClosureHeader) -> f64 { undefined_value() } -extern "C" fn process_ipc_disconnect_fn(_closure: *const ClosureHeader) -> f64 { +extern "C" fn process_ipc_disconnect_fn( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { process_ipc_disconnect_call() } extern "C" fn process_ipc_send_fn( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, message: f64, a2: f64, a3: f64, @@ -562,7 +572,10 @@ fn defer_send_callback(cb: f64, ok: bool) { crate::timer::js_set_immediate_callback(deferred as i64); } -extern "C" fn process_ipc_send_callback_thunk(closure: *const ClosureHeader) -> f64 { +extern "C" fn process_ipc_send_callback_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let cb = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); if crate::fs::extract_closure_ptr(cb).is_null() { return undefined_value(); @@ -574,7 +587,14 @@ extern "C" fn process_ipc_send_callback_thunk(closure: *const ClosureHeader) -> channel_closed_error() }; let args = [arg]; - unsafe { js_native_call_value(cb, args.as_ptr(), args.len()) }; + unsafe { + js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + }; undefined_value() } diff --git a/crates/perry-runtime/src/process/node_module.rs b/crates/perry-runtime/src/process/node_module.rs index c105355597..65819a941d 100644 --- a/crates/perry-runtime/src/process/node_module.rs +++ b/crates/perry-runtime/src/process/node_module.rs @@ -57,14 +57,15 @@ pub extern "C" fn js_module_constants() -> f64 { extern "C" fn module_require_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, specifier: f64, ) -> f64 { - js_module_instance_require(specifier) + js_module_instance_require(this, specifier) } -pub(crate) fn js_module_instance_require(specifier: f64) -> f64 { +pub(crate) fn js_module_instance_require(this: crate::closure::JsThis, specifier: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let receiver = scope.root_nanbox_f64(this.as_f64()); let specifier = scope.root_nanbox_f64(specifier); let Some(_) = module_object_ptr(receiver.get_nanbox_f64()) else { module_throw_plain_type_error("Module.prototype.require called on incompatible receiver"); @@ -84,6 +85,7 @@ pub(crate) fn js_module_instance_require(specifier: f64) -> f64 { crate::closure::js_closure_call1( crate::value::js_nanbox_get_pointer(require.get_nanbox_f64()) as *const crate::closure::ClosureHeader, + crate::closure::plain_call_receiver(), specifier.get_nanbox_f64(), ) } @@ -444,8 +446,8 @@ pub extern "C" fn js_module_load(request: f64, _parent: f64, _is_main: f64) -> f module_undefined() } -pub extern "C" fn js_module_instance_load(filename: f64) -> f64 { - let receiver = crate::object::js_implicit_this_get(); +pub extern "C" fn js_module_instance_load(this: crate::closure::JsThis, filename: f64) -> f64 { + let receiver = this.as_f64(); let scope = crate::gc::RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let Some(_) = module_object_ptr(receiver.get_nanbox_f64()) else { @@ -874,7 +876,10 @@ fn module_hook_member(value: f64, name: &str) -> f64 { crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); } -extern "C" fn module_hooks_deregister(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn module_hooks_deregister( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let id = js_closure_get_capture_f64(closure, 0) as u64; // 2026-07-09 GC audit wave 2: deregister used to only flip `active`, // leaving the resolve/load closures strongly rooted by @@ -964,6 +969,7 @@ pub extern "C" fn js_module_register_hooks(hooks: f64) -> f64 { extern "C" fn module_loader_next_resolve( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, specifier: f64, _context: f64, ) -> f64 { @@ -975,6 +981,7 @@ extern "C" fn module_loader_next_resolve( extern "C" fn module_loader_next_load( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _url: f64, _context: f64, ) -> f64 { @@ -987,7 +994,7 @@ extern "C" fn module_loader_next_load( fn module_loader_callback( slot: &'static std::thread::LocalKey>, name: &str, - func: extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64, + func: crate::closure::body_call::js_body_fn_ty!(a, a), ) -> f64 { let ptr = slot.with(|cell| { let existing = cell.get(); @@ -1032,13 +1039,21 @@ fn module_loader_result_url(result: f64, fallback: f64) -> f64 { extern "C" fn module_loader_hook_chain( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, context: f64, ) -> f64 { let callback = js_closure_get_capture_f64(closure, 0); let next = js_closure_get_capture_f64(closure, 1); let args = [value, context, next]; - unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) } + unsafe { + crate::closure::js_native_call_value( + callback, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) + } } fn module_loader_hook_chain_function(callback: f64, next: f64, name: &str) -> f64 { @@ -1125,6 +1140,7 @@ pub extern "C" fn js_module_dynamic_import_apply_hooks(specifier: f64) -> f64 { let resolved = unsafe { crate::closure::js_native_call_value( resolve_chain.get_nanbox_f64(), + crate::closure::plain_call_receiver(), resolve_args.as_ptr(), resolve_args.len(), ) @@ -1149,6 +1165,7 @@ pub extern "C" fn js_module_dynamic_import_apply_hooks(specifier: f64) -> f64 { unsafe { crate::closure::js_native_call_value( load_chain.get_nanbox_f64(), + crate::closure::plain_call_receiver(), load_args.as_ptr(), load_args.len(), ); diff --git a/crates/perry-runtime/src/process/node_module/source_map.rs b/crates/perry-runtime/src/process/node_module/source_map.rs index 4bc09d197d..79903ac512 100644 --- a/crates/perry-runtime/src/process/node_module/source_map.rs +++ b/crates/perry-runtime/src/process/node_module/source_map.rs @@ -85,7 +85,7 @@ pub extern "C" fn js_module_source_map_new(payload: f64, options: f64) -> f64 { obj.with_mut_ptr(|obj: *mut crate::object::ObjectHeader| module_object_value(obj)) } -type SourceMapThunk = extern "C" fn(*const ClosureHeader, f64) -> f64; +type SourceMapThunk = crate::closure::body_call::js_body_fn_ty!(a); fn source_map_method(name: &str, thunk: SourceMapThunk) -> f64 { let func_ptr = thunk as *const u8; @@ -97,8 +97,11 @@ fn source_map_method(name: &str, thunk: SourceMapThunk) -> f64 { crate::value::js_nanbox_pointer(closure as i64) } -extern "C" fn source_map_payload_getter(_closure: *const ClosureHeader) -> f64 { - let obj = source_map_receiver(); +extern "C" fn source_map_payload_getter( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = source_map_receiver(this); let scope = crate::gc::RuntimeHandleScope::new(); let payload = scope.root_nanbox_f64(f64::from_bits( crate::object::js_object_get_field(obj, 0).bits(), @@ -106,8 +109,11 @@ extern "C" fn source_map_payload_getter(_closure: *const ClosureHeader) -> f64 { crate::builtins::js_structured_clone(payload.get_nanbox_f64()) } -extern "C" fn source_map_line_lengths_getter(_closure: *const ClosureHeader) -> f64 { - let obj = source_map_receiver(); +extern "C" fn source_map_line_lengths_getter( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let obj = source_map_receiver(this); let scope = crate::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(f64::from_bits( crate::object::js_object_get_field(obj, 1).bits(), @@ -128,7 +134,7 @@ extern "C" fn source_map_line_lengths_getter(_closure: *const ClosureHeader) -> f64::from_bits(JSValue::array_ptr(cloned).bits()) } -fn source_map_getter(name: &str, thunk: extern "C" fn(*const ClosureHeader) -> f64) -> f64 { +fn source_map_getter(name: &str, thunk: crate::closure::body_call::js_body_fn_ty!()) -> f64 { let func_ptr = thunk as *const u8; crate::closure::js_register_closure_arity(func_ptr, 0); let closure = js_closure_alloc(func_ptr, 0); @@ -138,8 +144,8 @@ fn source_map_getter(name: &str, thunk: extern "C" fn(*const ClosureHeader) -> f crate::value::js_nanbox_pointer(closure as i64) } -fn source_map_receiver() -> *const crate::object::ObjectHeader { - let receiver = crate::object::js_implicit_this_get(); +fn source_map_receiver(this: crate::closure::JsThis) -> *const crate::object::ObjectHeader { + let receiver = this.as_f64(); let Some(obj) = module_object_ptr(receiver) else { module_throw_plain_type_error("Receiver must be an instance of SourceMap"); }; @@ -196,12 +202,12 @@ fn source_map_prototype() -> f64 { ( 4, "payload", - source_map_payload_getter as extern "C" fn(*const ClosureHeader) -> f64, + source_map_payload_getter as crate::closure::body_call::js_body_fn_ty!(), ), ( 3, "lineLengths", - source_map_line_lengths_getter as extern "C" fn(*const ClosureHeader) -> f64, + source_map_line_lengths_getter as crate::closure::body_call::js_body_fn_ty!(), ), ] { let getter = source_map_getter(name, thunk); @@ -599,9 +605,13 @@ fn source_map_origin_object( /// decoded entry whose generated position is `<=` the query, shaped like /// Node's `{ generatedLine, generatedColumn, originalSource, originalLine, /// originalColumn, name? }`. Returns `{}` when no entry precedes the query. -extern "C" fn source_map_find_entry_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn source_map_find_entry_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { let _ = closure; - let receiver = source_map_receiver(); + let receiver = source_map_receiver(this); let scope = crate::gc::RuntimeHandleScope::new(); let payload = scope.root_nanbox_f64(f64::from_bits( crate::object::js_object_get_field(receiver, 0).bits(), @@ -647,9 +657,13 @@ extern "C" fn source_map_find_entry_thunk(closure: *const ClosureHeader, rest: f /// not a finite number) and tags on the `name`/`fileName` of the entry at that /// generated position. The lone special case is a numeric `(0, 0)` query, for /// which Node returns an empty object. -extern "C" fn source_map_find_origin_thunk(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn source_map_find_origin_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest: f64, +) -> f64 { let _ = closure; - let receiver = source_map_receiver(); + let receiver = source_map_receiver(this); let scope = crate::gc::RuntimeHandleScope::new(); let payload = scope.root_nanbox_f64(f64::from_bits( crate::object::js_object_get_field(receiver, 0).bits(), diff --git a/crates/perry-runtime/src/process/permission.rs b/crates/perry-runtime/src/process/permission.rs index e8b2fb5eb6..4ec02294d2 100644 --- a/crates/perry-runtime/src/process/permission.rs +++ b/crates/perry-runtime/src/process/permission.rs @@ -146,6 +146,7 @@ fn throw_permission_arg_type(name: &str, value: f64) -> ! { extern "C" fn process_permission_has_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, scope_value: f64, reference_value: f64, ) -> f64 { @@ -168,6 +169,7 @@ extern "C" fn process_permission_has_thunk( extern "C" fn process_permission_drop_thunk( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, scope_value: f64, reference_value: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/process/report.rs b/crates/perry-runtime/src/process/report.rs index e0e995b59c..1c5421c675 100644 --- a/crates/perry-runtime/src/process/report.rs +++ b/crates/perry-runtime/src/process/report.rs @@ -34,6 +34,7 @@ pub(crate) fn process_features_value() -> f64 { extern "C" fn process_report_function_get_report( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, err: f64, ) -> f64 { validate_report_error_arg(err); @@ -42,6 +43,7 @@ extern "C" fn process_report_function_get_report( extern "C" fn process_report_function_write_report( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, file: f64, err: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/promise/assimilate.rs b/crates/perry-runtime/src/promise/assimilate.rs index 7f28ad80a9..91438909e1 100644 --- a/crates/perry-runtime/src/promise/assimilate.rs +++ b/crates/perry-runtime/src/promise/assimilate.rs @@ -251,7 +251,10 @@ pub(super) fn enqueue_native_adoption_job(outer: *mut Promise, inner: *mut Promi /// a synchronous copy, matching V8's reaction-job. A still-pending inner /// falls back to the existing chain wiring: its settlement path already /// delivers through the microtask runner. -extern "C" fn native_promise_adoption_job(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn native_promise_adoption_job( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { use crate::closure::js_closure_get_capture_ptr; let outer = js_closure_get_capture_ptr(closure, 0) as *mut Promise; @@ -317,6 +320,7 @@ fn thenable_job_take_guard(guard_arr: *mut crate::array::ArrayHeader) -> bool { pub(super) extern "C" fn thenable_job_resolve_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -331,6 +335,7 @@ pub(super) extern "C" fn thenable_job_resolve_fn( pub(super) extern "C" fn thenable_job_reject_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -343,7 +348,10 @@ pub(super) extern "C" fn thenable_job_reject_fn( 0.0 } -extern "C" fn promise_resolve_thenable_job(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn promise_resolve_thenable_job( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { use crate::array::{js_array_alloc, js_array_set_f64}; use crate::closure::{ js_closure_alloc, js_closure_get_capture_f64, js_closure_get_capture_ptr, @@ -382,12 +390,14 @@ extern "C" fn promise_resolve_thenable_job(closure: *const crate::closure::Closu let reject_value = crate::value::js_nanbox_pointer(reject_closure as i64); let args = [resolve_value, reject_value]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(thenable)); let result = combinator_catch_js(|| unsafe { - crate::closure::js_native_call_value(then_action, args.as_ptr(), args.len()) + crate::closure::native_call_value_this( + then_action, + crate::closure::JsThis::from_f64(thenable), + args.as_ptr(), + args.len(), + ) }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if let Err(reason) = result { if thenable_job_take_guard(guard_arr) { js_promise_reject(promise, reason); @@ -479,19 +489,16 @@ pub(super) fn assimilate_via_then_property(value: f64) -> f64 { .with_mut_ptr::(|reject| crate::value::js_nanbox_pointer(reject as i64)); let args = [resolve_f64, reject_f64]; - // Bind `this` to the thenable so a non-arrow `then` body reads the right - // receiver, then call `Get(value, "then")` as a value (own data property). - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - value_handle.get_nanbox_f64(), - )); // #9445 + // Call `Get(value, "then")` as a value (own data property) with the + // thenable as its receiver, so a non-arrow `then` body reads the right `this`. unsafe { - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( then_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), args.as_ptr(), args.len(), ); } - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); // Re-read the wrapper through its handle: the user `then` just ran and may // have relocated it (#9539). Returning `new_promise`'s pre-call address is diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index a709b4592e..90e890fd11 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -1026,6 +1026,7 @@ fn build_async_step_thunks( // Capture layout: [step_closure_ptr] extern "C" fn async_step_fulfill_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let step = crate::closure::js_closure_get_capture_ptr(closure, 0) @@ -1087,7 +1088,12 @@ fn call_async_step_body( // `step` is needed only by this call and is never used afterward. Any // future post-call use must root it and re-read its relocated address. - let result = crate::closure::js_closure_call2(step, value, is_error); + let result = crate::closure::js_closure_call2( + step, + crate::closure::plain_call_receiver(), + value, + is_error, + ); let result_h = scope.root_nanbox_f64(result); INLINE_TRAP.with(|c| { c.set(InlineTrap { @@ -1148,6 +1154,7 @@ fn forward_swallowed_rejection(result: f64, trap_next: *mut Promise) { extern "C" fn async_step_reject_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let step = crate::closure::js_closure_get_capture_ptr(closure, 0) @@ -1372,11 +1379,16 @@ fn array_from_async_call_next( } // Synchronous iterator path: invoke the handler directly with the // result so the iteration loop continues without going through .then. - array_from_async_step(chain_closure as *const _, next_result); + array_from_async_step( + chain_closure as *const _, + crate::closure::plain_call_receiver(), + next_result, + ); } extern "C" fn array_from_async_reject( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let result_promise = crate::closure::js_closure_get_capture_ptr(closure, 0) as *mut Promise; @@ -1392,6 +1404,7 @@ extern "C" fn array_from_async_reject( /// another `.next()` call. extern "C" fn array_from_async_step( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, iter_result: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -1480,6 +1493,7 @@ fn array_from_async_await_value(closure: *const crate::closure::ClosureHeader, v extern "C" fn array_from_async_value_step( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let state = crate::closure::js_closure_get_capture_ptr(closure, 0) @@ -1501,10 +1515,11 @@ fn array_from_async_map_or_push(closure: *const crate::closure::ClosureHeader, v let this_arg = crate::closure::js_closure_get_capture_f64(closure, AFA_THIS_ARG); let args = [value, index]; let args_arr = crate::array::js_array_from_f64(args.as_ptr(), args.len() as u32); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); - let mapped_promise = js_promise_try(map_fn, args_arr); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let mapped_promise = super::combinators::promise_try_this( + map_fn, + crate::closure::JsThis::from_f64(this_arg), + args_arr, + ); let mapped_closure = crate::closure::js_closure_get_capture_ptr(closure, AFA_MAPPED_CLOSURE) as *const crate::closure::ClosureHeader; @@ -1515,6 +1530,7 @@ fn array_from_async_map_or_push(closure: *const crate::closure::ClosureHeader, v extern "C" fn array_from_async_mapped_step( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let state = crate::closure::js_closure_get_capture_ptr(closure, 0) @@ -1603,6 +1619,7 @@ mod tests { extern "C" fn relocating_step( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _value: f64, _is_error: f64, ) -> f64 { @@ -1702,7 +1719,7 @@ mod tests { ]) }); - async_step_fulfill_thunk(thunk, 41.0); + async_step_fulfill_thunk(thunk, crate::closure::plain_call_receiver(), 41.0); assert_eq!( (*captured_to).state, diff --git a/crates/perry-runtime/src/promise/checked_dispatch.rs b/crates/perry-runtime/src/promise/checked_dispatch.rs index c8bcb5f9e1..ee5e7576bf 100644 --- a/crates/perry-runtime/src/promise/checked_dispatch.rs +++ b/crates/perry-runtime/src/promise/checked_dispatch.rs @@ -50,12 +50,14 @@ pub extern "C" fn js_promise_then_checked( )); } let args = [on_fulfilled, on_rejected]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); - let result = - unsafe { crate::closure::js_native_call_value(own_then, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return unsafe { + crate::closure::native_call_value_this( + own_then, + crate::closure::JsThis::from_f64(promise_val), + args.as_ptr(), + args.len(), + ) + }; } if promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() @@ -63,11 +65,12 @@ pub extern "C" fn js_promise_then_checked( // Own `constructor` override OR a `class X extends Promise` instance: // route through the SpeciesConstructor-aware thunk, which unwraps the // backing cell and reads `this.constructor` for species chaining. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); - let result = promise_prototype_then_thunk(std::ptr::null(), on_fulfilled, on_rejected); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return promise_prototype_then_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(promise_val), + on_fulfilled, + on_rejected, + ); } let promise = promise_addr as *mut Promise; box_promise_ptr(js_promise_then( @@ -84,11 +87,11 @@ pub extern "C" fn js_promise_catch_checked(promise_val: f64, on_rejected: f64) - || promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); - let result = promise_prototype_catch_thunk(std::ptr::null(), on_rejected); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return promise_prototype_catch_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(promise_val), + on_rejected, + ); } let promise = promise_addr as *mut Promise; box_promise_ptr(js_promise_catch(promise, arg_to_closure(on_rejected))) @@ -101,11 +104,11 @@ pub extern "C" fn js_promise_finally_checked(promise_val: f64, on_finally: f64) || promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); - let result = promise_prototype_finally_thunk(std::ptr::null(), on_finally); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return promise_prototype_finally_thunk( + std::ptr::null(), + crate::closure::JsThis::from_f64(promise_val), + on_finally, + ); } let promise = promise_addr as *mut Promise; box_promise_ptr(js_promise_finally(promise, arg_to_closure(on_finally))) diff --git a/crates/perry-runtime/src/promise/combinators.rs b/crates/perry-runtime/src/promise/combinators.rs index 3a610abeda..cf2a9d8c49 100644 --- a/crates/perry-runtime/src/promise/combinators.rs +++ b/crates/perry-runtime/src/promise/combinators.rs @@ -217,13 +217,18 @@ fn promise_try_closure_ptr(callback: f64) -> Option<*const crate::closure::Closu crate::closure::is_closure_ptr(ptr).then_some(ptr as *const crate::closure::ClosureHeader) } -fn promise_try_call(callback: f64, args_ptr: *const f64, args_len: usize) -> Result { +fn promise_try_call( + callback: f64, + this: crate::closure::JsThis, + args_ptr: *const f64, + args_len: usize, +) -> Result { let Some(closure) = promise_try_closure_ptr(callback) else { return Err(promise_try_type_error_value(callback)); }; crate::exception::catch_js_throw(|| unsafe { - crate::closure::js_closure_call_array(closure as i64, args_ptr, args_len as i64) + crate::closure::js_closure_call_array(closure as i64, this, args_ptr, args_len as i64) }) } @@ -233,6 +238,16 @@ fn promise_try_call(callback: f64, args_ptr: *const f64, args_len: usize) -> Res pub extern "C" fn js_promise_try( callback: f64, args: *const crate::array::ArrayHeader, +) -> *mut Promise { + promise_try_this(callback, crate::closure::plain_call_receiver(), args) +} + +/// `js_promise_try` calling `callback` with `this` as its receiver +/// (`Array.fromAsync`'s `thisArg` for the map function). +pub(crate) fn promise_try_this( + callback: f64, + this: crate::closure::JsThis, + args: *const crate::array::ArrayHeader, ) -> *mut Promise { let (args_ptr, args_len) = if args.is_null() { (std::ptr::null(), 0) @@ -244,7 +259,7 @@ pub extern "C" fn js_promise_try( (data, len) }; - match promise_try_call(callback, args_ptr, args_len) { + match promise_try_call(callback, this, args_ptr, args_len) { Ok(value) => js_promise_resolved(value), Err(reason) => js_promise_rejected(reason), } @@ -693,12 +708,14 @@ pub extern "C" fn js_promise_new_with_executor( if let Err(reason) = combinator_catch_js(|| { js_closure_call2( rooted_executor(), + crate::closure::plain_call_receiver(), resolve_h.get_nanbox_f64(), reject_h.get_nanbox_f64(), ) }) { crate::closure::js_closure_call1( ptr_of(&reject_h) as *mut crate::closure::ClosureHeader, + crate::closure::plain_call_receiver(), reason, ); } @@ -706,6 +723,7 @@ pub extern "C" fn js_promise_new_with_executor( // Non-callable: preserve the prior synchronous TypeError (uncaught). js_closure_call2( rooted_executor(), + crate::closure::plain_call_receiver(), resolve_h.get_nanbox_f64(), reject_h.get_nanbox_f64(), ); @@ -859,6 +877,7 @@ pub(super) fn make_resolving_functions( /// rather than fulfilling with the thenable as a plain value. pub(super) extern "C" fn promise_resolve_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -896,6 +915,7 @@ pub(super) extern "C" fn promise_resolve_fn( /// Called when user calls reject(reason) inside the executor. pub(super) extern "C" fn promise_reject_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -1079,6 +1099,7 @@ fn attach_promise_all_after_prior_reaction(promise: *mut Promise, state: Promise extern "C" fn promise_all_ordered_fulfill_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::closure::{js_closure_get_capture_f64, js_closure_get_capture_ptr}; @@ -1096,6 +1117,7 @@ extern "C" fn promise_all_ordered_fulfill_handler( extern "C" fn promise_all_ordered_reject_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -1195,6 +1217,7 @@ pub extern "C" fn js_promise_race(promises_arr: *const crate::array::ArrayHeader /// Handler for Promise.race fulfill — resolves the race promise with the first value extern "C" fn promise_race_resolve_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -1212,6 +1235,7 @@ extern "C" fn promise_race_resolve_handler( /// Handler for Promise.race reject — rejects the race promise with the first reason extern "C" fn promise_race_reject_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -1385,17 +1409,26 @@ pub extern "C" fn js_assimilate_thenable(value: f64) -> f64 { unsafe { match then_param_count { 0 => { - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(then_func_ptr); - f(this_f64); + crate::closure::body_call::js_method_body_call!( + then_func_ptr as *const u8, + this_f64 + ); } 1 => { - let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(then_func_ptr); - f(this_f64, resolve_f64); + crate::closure::body_call::js_method_body_call!( + then_func_ptr as *const u8, + this_f64, + resolve_f64 + ); } _ => { // 2+ params: pass resolve/reject; any extra slots arrive as NaN. - let f: extern "C" fn(f64, f64, f64) -> f64 = std::mem::transmute(then_func_ptr); - f(this_f64, resolve_f64, reject_f64); + crate::closure::body_call::js_method_body_call!( + then_func_ptr as *const u8, + this_f64, + resolve_f64, + reject_f64 + ); } } } @@ -1528,6 +1561,7 @@ pub extern "C" fn js_promise_all_settled( extern "C" fn promise_all_settled_fulfill_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::array::{js_array_get_f64, js_array_set_f64, ArrayHeader}; @@ -1556,6 +1590,7 @@ extern "C" fn promise_all_settled_fulfill_handler( extern "C" fn promise_all_settled_reject_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::array::{js_array_get_f64, js_array_set_f64, ArrayHeader}; @@ -1662,6 +1697,7 @@ pub extern "C" fn js_promise_any(promises_arr: *const crate::array::ArrayHeader) extern "C" fn promise_any_fulfill_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::array::{js_array_get_f64, js_array_set_f64, ArrayHeader}; @@ -1685,6 +1721,7 @@ extern "C" fn promise_any_fulfill_handler( extern "C" fn promise_any_reject_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::array::{js_array_get_f64, js_array_set_f64, ArrayHeader}; @@ -1721,255 +1758,5 @@ extern "C" fn promise_any_reject_handler( } #[cfg(test)] -mod tests { - use super::*; - use crate::array::{js_array_alloc, js_array_get_f64, js_array_set_f64}; - use crate::closure::{ - js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64, - }; - use crate::object::{js_object_alloc, js_object_get_field, js_object_set_field_by_name}; - use crate::value::js_nanbox_pointer; - - fn reset_promise_test_state() { - TASK_QUEUE.with(|q| q.borrow_mut().clear()); - PROMISE_ALL_STATES.with(|s| s.borrow_mut().clear()); - } - - fn thenable_value(func: *const u8, captured: f64) -> f64 { - let obj = js_object_alloc(0, 0); - let then = js_closure_alloc(func, 1); - js_closure_set_capture_f64(then, 0, captured); - let key = crate::string::js_string_from_bytes(b"then".as_ptr(), 4); - js_object_set_field_by_name(obj, key, js_nanbox_pointer(then as i64)); - js_nanbox_pointer(obj as i64) - } - - #[test] - fn promise_probe_rejects_pointer_tagged_native_handles() { - let fetch_family_handle = js_nanbox_pointer(0x40001); - assert_eq!(js_value_is_promise(fetch_family_handle), 0); - } - - // #5226: small typed arrays / buffers are system-`alloc`'d off the GC heap - // with NO 8-byte GcHeader prefix and are tracked only in side tables. The - // type-dispatch probes (`js_value_is_promise`, `is_date_cell_addr`, …) must - // recognize them via the side table and must NOT back-read - // `ptr - GC_HEADER_SIZE` — a read on a block at the start of a freshly - // mapped region crosses into the (unmapped) preceding page and segfaults. - // Reproduce that worst case with a guarded mapping: without the side-table - // skip these probes SIGSEGV; with it, they classify the block correctly. - #[cfg(unix)] - #[test] - fn type_probes_skip_offheap_typed_array_with_unmapped_preceding_page() { - unsafe { - let page = libc::sysconf(libc::_SC_PAGESIZE) as usize; - let total = page * 2; - let base = libc::mmap( - std::ptr::null_mut(), - total, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, - -1, - 0, - ); - assert_ne!(base, libc::MAP_FAILED); - // Guard the first page so any `ptr - GC_HEADER_SIZE` back-read faults. - assert_eq!(libc::mprotect(base, page, libc::PROT_NONE), 0); - let ta = (base as *mut u8).add(page) as *const crate::typedarray::TypedArrayHeader; - crate::typedarray::register_typed_array(ta, 0); - - let value = js_nanbox_pointer(ta as i64); - assert_eq!(js_value_is_promise(value), 0); - assert!(!crate::date::is_date_cell_addr(ta as usize)); - - crate::typedarray::unregister_typed_array(ta); - assert_eq!(libc::munmap(base, total), 0); - } - } - - extern "C" fn test_thenable_resolve_twice( - closure: *const crate::closure::ClosureHeader, - on_fulfilled: f64, - _on_rejected: f64, - ) -> f64 { - let value = js_closure_get_capture_f64(closure, 0); - let unexpected = value + 1000.0; - unsafe { - crate::closure::js_native_call_value(on_fulfilled, [value].as_ptr(), 1); - crate::closure::js_native_call_value(on_fulfilled, [unexpected].as_ptr(), 1); - } - 0.0 - } - - extern "C" fn test_thenable_reject( - closure: *const crate::closure::ClosureHeader, - _on_fulfilled: f64, - on_rejected: f64, - ) -> f64 { - let reason = js_closure_get_capture_f64(closure, 0); - unsafe { - crate::closure::js_native_call_value(on_rejected, [reason].as_ptr(), 1); - } - 0.0 - } - - #[test] - fn promise_all_assimilates_thenable_and_guards_double_resolve() { - unsafe { - reset_promise_test_state(); - let arr = js_array_alloc(1); - (*arr).length = 1; - js_array_set_f64( - arr, - 0, - thenable_value(test_thenable_resolve_twice as *const u8, 7.0), - ); - - let all = js_promise_all(arr); - assert_eq!((*all).state, PromiseState::Pending); - - crate::promise::js_promise_run_microtasks(); - - assert_eq!((*all).state, PromiseState::Fulfilled); - let results = crate::value::js_nanbox_get_pointer((*all).value) - as *const crate::array::ArrayHeader; - assert_eq!(js_array_get_f64(results, 0), 7.0); - } - } - - #[test] - fn promise_all_rejects_from_thenable_job() { - unsafe { - reset_promise_test_state(); - let arr = js_array_alloc(1); - (*arr).length = 1; - js_array_set_f64( - arr, - 0, - thenable_value(test_thenable_reject as *const u8, 13.0), - ); - - let all = js_promise_all(arr); - assert_eq!((*all).state, PromiseState::Pending); - - crate::promise::js_promise_run_microtasks(); - - assert_eq!((*all).state, PromiseState::Rejected); - assert_eq!((*all).reason, 13.0); - } - } - - #[test] - fn promise_all_settled_assimilates_thenables_in_input_order() { - unsafe { - reset_promise_test_state(); - let arr = js_array_alloc(2); - (*arr).length = 2; - js_array_set_f64( - arr, - 0, - thenable_value(test_thenable_reject as *const u8, 1.0), - ); - js_array_set_f64( - arr, - 1, - thenable_value(test_thenable_resolve_twice as *const u8, 2.0), - ); - - let settled = js_promise_all_settled(arr); - assert_eq!((*settled).state, PromiseState::Pending); - - crate::promise::js_promise_run_microtasks(); - - assert_eq!((*settled).state, PromiseState::Fulfilled); - let results = crate::value::js_nanbox_get_pointer((*settled).value) - as *const crate::array::ArrayHeader; - let first = crate::value::js_nanbox_get_pointer(js_array_get_f64(results, 0)) - as *const crate::object::ObjectHeader; - let second = crate::value::js_nanbox_get_pointer(js_array_get_f64(results, 1)) - as *const crate::object::ObjectHeader; - assert_eq!(js_object_get_field(first, 1).bits(), 1.0f64.to_bits()); - assert_eq!(js_object_get_field(second, 1).bits(), 2.0f64.to_bits()); - } - } - - #[test] - fn promise_result_resolution_assimilates_thenable_objects() { - unsafe { - reset_promise_test_state(); - let promise = js_promise_new(); - promise_resolve_assimilating( - promise, - thenable_value(test_thenable_resolve_twice as *const u8, 21.0), - ); - assert_eq!((*promise).state, PromiseState::Pending); - - crate::promise::js_promise_run_microtasks(); - - assert_eq!((*promise).state, PromiseState::Fulfilled); - assert_eq!((*promise).value, 21.0); - } - } - - /// Regression: a pending promise reused as input to two `Promise.all` - /// calls must settle BOTH all-promises when it resolves. Pre-fix, - /// `promise_all_take_handler` only popped the first matching state - /// (via `swap_remove`), so the second `Promise.all` hung forever. - #[test] - fn promise_all_with_shared_pending_input_resolves_both() { - unsafe { - // Pending promise that will be shared across two Promise.all calls. - let shared = js_promise_new(); - - // Second input for each all() — a pre-resolved promise so the - // remaining counter only needs `shared` to settle. - let other_a = js_promise_new(); - js_promise_resolve(other_a, 100.0); - let other_b = js_promise_new(); - js_promise_resolve(other_b, 200.0); - - // Build [shared, other_a] - let arr_a = js_array_alloc(2); - (*arr_a).length = 2; - js_array_set_f64(arr_a, 0, js_nanbox_pointer(shared as i64)); - js_array_set_f64(arr_a, 1, js_nanbox_pointer(other_a as i64)); - let all_a = js_promise_all(arr_a); - - // Build [shared, other_b] - let arr_b = js_array_alloc(2); - (*arr_b).length = 2; - js_array_set_f64(arr_b, 0, js_nanbox_pointer(shared as i64)); - js_array_set_f64(arr_b, 1, js_nanbox_pointer(other_b as i64)); - let all_b = js_promise_all(arr_b); - - // Both all() results should still be pending. - assert_eq!((*all_a).state, PromiseState::Pending); - assert_eq!((*all_b).state, PromiseState::Pending); - - // PROMISE_ALL_STATES must hold TWO entries keyed on `shared`. - let registered = - PROMISE_ALL_STATES.with(|s| s.borrow_mut().count_for_key(shared as usize)); - assert_eq!( - registered, 2, - "expected two Promise.all states keyed on the shared pending promise" - ); - - // Settle the shared promise; drain microtasks so PromiseAll tasks - // run and update both result arrays. - js_promise_resolve(shared, 42.0); - crate::promise::js_promise_run_microtasks(); - - // Both Promise.all results must now be Fulfilled. - assert_eq!( - (*all_a).state, - PromiseState::Fulfilled, - "first Promise.all should have settled" - ); - assert_eq!( - (*all_b).state, - PromiseState::Fulfilled, - "second Promise.all should have settled (was hanging pre-fix)" - ); - } - } -} +#[path = "combinators_tests.rs"] +mod tests; diff --git a/crates/perry-runtime/src/promise/combinators_tests.rs b/crates/perry-runtime/src/promise/combinators_tests.rs new file mode 100644 index 0000000000..718755fa76 --- /dev/null +++ b/crates/perry-runtime/src/promise/combinators_tests.rs @@ -0,0 +1,265 @@ +//! Unit tests for `combinators.rs` (split out for the file-size gate). +use super::*; +use crate::array::{js_array_alloc, js_array_get_f64, js_array_set_f64}; +use crate::closure::{js_closure_alloc, js_closure_get_capture_f64, js_closure_set_capture_f64}; +use crate::object::{js_object_alloc, js_object_get_field, js_object_set_field_by_name}; +use crate::value::js_nanbox_pointer; + +fn reset_promise_test_state() { + TASK_QUEUE.with(|q| q.borrow_mut().clear()); + PROMISE_ALL_STATES.with(|s| s.borrow_mut().clear()); +} + +fn thenable_value(func: *const u8, captured: f64) -> f64 { + let obj = js_object_alloc(0, 0); + let then = js_closure_alloc(func, 1); + js_closure_set_capture_f64(then, 0, captured); + let key = crate::string::js_string_from_bytes(b"then".as_ptr(), 4); + js_object_set_field_by_name(obj, key, js_nanbox_pointer(then as i64)); + js_nanbox_pointer(obj as i64) +} + +#[test] +fn promise_probe_rejects_pointer_tagged_native_handles() { + let fetch_family_handle = js_nanbox_pointer(0x40001); + assert_eq!(js_value_is_promise(fetch_family_handle), 0); +} + +// #5226: small typed arrays / buffers are system-`alloc`'d off the GC heap +// with NO 8-byte GcHeader prefix and are tracked only in side tables. The +// type-dispatch probes (`js_value_is_promise`, `is_date_cell_addr`, …) must +// recognize them via the side table and must NOT back-read +// `ptr - GC_HEADER_SIZE` — a read on a block at the start of a freshly +// mapped region crosses into the (unmapped) preceding page and segfaults. +// Reproduce that worst case with a guarded mapping: without the side-table +// skip these probes SIGSEGV; with it, they classify the block correctly. +#[cfg(unix)] +#[test] +fn type_probes_skip_offheap_typed_array_with_unmapped_preceding_page() { + unsafe { + let page = libc::sysconf(libc::_SC_PAGESIZE) as usize; + let total = page * 2; + let base = libc::mmap( + std::ptr::null_mut(), + total, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ); + assert_ne!(base, libc::MAP_FAILED); + // Guard the first page so any `ptr - GC_HEADER_SIZE` back-read faults. + assert_eq!(libc::mprotect(base, page, libc::PROT_NONE), 0); + let ta = (base as *mut u8).add(page) as *const crate::typedarray::TypedArrayHeader; + crate::typedarray::register_typed_array(ta, 0); + + let value = js_nanbox_pointer(ta as i64); + assert_eq!(js_value_is_promise(value), 0); + assert!(!crate::date::is_date_cell_addr(ta as usize)); + + crate::typedarray::unregister_typed_array(ta); + assert_eq!(libc::munmap(base, total), 0); + } +} + +extern "C" fn test_thenable_resolve_twice( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + on_fulfilled: f64, + _on_rejected: f64, +) -> f64 { + let value = js_closure_get_capture_f64(closure, 0); + let unexpected = value + 1000.0; + unsafe { + crate::closure::js_native_call_value( + on_fulfilled, + crate::closure::plain_call_receiver(), + [value].as_ptr(), + 1, + ); + crate::closure::js_native_call_value( + on_fulfilled, + crate::closure::plain_call_receiver(), + [unexpected].as_ptr(), + 1, + ); + } + 0.0 +} + +extern "C" fn test_thenable_reject( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _on_fulfilled: f64, + on_rejected: f64, +) -> f64 { + let reason = js_closure_get_capture_f64(closure, 0); + unsafe { + crate::closure::js_native_call_value( + on_rejected, + crate::closure::plain_call_receiver(), + [reason].as_ptr(), + 1, + ); + } + 0.0 +} + +#[test] +fn promise_all_assimilates_thenable_and_guards_double_resolve() { + unsafe { + reset_promise_test_state(); + let arr = js_array_alloc(1); + (*arr).length = 1; + js_array_set_f64( + arr, + 0, + thenable_value(test_thenable_resolve_twice as *const u8, 7.0), + ); + + let all = js_promise_all(arr); + assert_eq!((*all).state, PromiseState::Pending); + + crate::promise::js_promise_run_microtasks(); + + assert_eq!((*all).state, PromiseState::Fulfilled); + let results = + crate::value::js_nanbox_get_pointer((*all).value) as *const crate::array::ArrayHeader; + assert_eq!(js_array_get_f64(results, 0), 7.0); + } +} + +#[test] +fn promise_all_rejects_from_thenable_job() { + unsafe { + reset_promise_test_state(); + let arr = js_array_alloc(1); + (*arr).length = 1; + js_array_set_f64( + arr, + 0, + thenable_value(test_thenable_reject as *const u8, 13.0), + ); + + let all = js_promise_all(arr); + assert_eq!((*all).state, PromiseState::Pending); + + crate::promise::js_promise_run_microtasks(); + + assert_eq!((*all).state, PromiseState::Rejected); + assert_eq!((*all).reason, 13.0); + } +} + +#[test] +fn promise_all_settled_assimilates_thenables_in_input_order() { + unsafe { + reset_promise_test_state(); + let arr = js_array_alloc(2); + (*arr).length = 2; + js_array_set_f64( + arr, + 0, + thenable_value(test_thenable_reject as *const u8, 1.0), + ); + js_array_set_f64( + arr, + 1, + thenable_value(test_thenable_resolve_twice as *const u8, 2.0), + ); + + let settled = js_promise_all_settled(arr); + assert_eq!((*settled).state, PromiseState::Pending); + + crate::promise::js_promise_run_microtasks(); + + assert_eq!((*settled).state, PromiseState::Fulfilled); + let results = crate::value::js_nanbox_get_pointer((*settled).value) + as *const crate::array::ArrayHeader; + let first = crate::value::js_nanbox_get_pointer(js_array_get_f64(results, 0)) + as *const crate::object::ObjectHeader; + let second = crate::value::js_nanbox_get_pointer(js_array_get_f64(results, 1)) + as *const crate::object::ObjectHeader; + assert_eq!(js_object_get_field(first, 1).bits(), 1.0f64.to_bits()); + assert_eq!(js_object_get_field(second, 1).bits(), 2.0f64.to_bits()); + } +} + +#[test] +fn promise_result_resolution_assimilates_thenable_objects() { + unsafe { + reset_promise_test_state(); + let promise = js_promise_new(); + promise_resolve_assimilating( + promise, + thenable_value(test_thenable_resolve_twice as *const u8, 21.0), + ); + assert_eq!((*promise).state, PromiseState::Pending); + + crate::promise::js_promise_run_microtasks(); + + assert_eq!((*promise).state, PromiseState::Fulfilled); + assert_eq!((*promise).value, 21.0); + } +} + +/// Regression: a pending promise reused as input to two `Promise.all` +/// calls must settle BOTH all-promises when it resolves. Pre-fix, +/// `promise_all_take_handler` only popped the first matching state +/// (via `swap_remove`), so the second `Promise.all` hung forever. +#[test] +fn promise_all_with_shared_pending_input_resolves_both() { + unsafe { + // Pending promise that will be shared across two Promise.all calls. + let shared = js_promise_new(); + + // Second input for each all() — a pre-resolved promise so the + // remaining counter only needs `shared` to settle. + let other_a = js_promise_new(); + js_promise_resolve(other_a, 100.0); + let other_b = js_promise_new(); + js_promise_resolve(other_b, 200.0); + + // Build [shared, other_a] + let arr_a = js_array_alloc(2); + (*arr_a).length = 2; + js_array_set_f64(arr_a, 0, js_nanbox_pointer(shared as i64)); + js_array_set_f64(arr_a, 1, js_nanbox_pointer(other_a as i64)); + let all_a = js_promise_all(arr_a); + + // Build [shared, other_b] + let arr_b = js_array_alloc(2); + (*arr_b).length = 2; + js_array_set_f64(arr_b, 0, js_nanbox_pointer(shared as i64)); + js_array_set_f64(arr_b, 1, js_nanbox_pointer(other_b as i64)); + let all_b = js_promise_all(arr_b); + + // Both all() results should still be pending. + assert_eq!((*all_a).state, PromiseState::Pending); + assert_eq!((*all_b).state, PromiseState::Pending); + + // PROMISE_ALL_STATES must hold TWO entries keyed on `shared`. + let registered = PROMISE_ALL_STATES.with(|s| s.borrow_mut().count_for_key(shared as usize)); + assert_eq!( + registered, 2, + "expected two Promise.all states keyed on the shared pending promise" + ); + + // Settle the shared promise; drain microtasks so PromiseAll tasks + // run and update both result arrays. + js_promise_resolve(shared, 42.0); + crate::promise::js_promise_run_microtasks(); + + // Both Promise.all results must now be Fulfilled. + assert_eq!( + (*all_a).state, + PromiseState::Fulfilled, + "first Promise.all should have settled" + ); + assert_eq!( + (*all_b).state, + PromiseState::Fulfilled, + "second Promise.all should have settled (was hanging pre-fix)" + ); + } +} diff --git a/crates/perry-runtime/src/promise/microtasks.rs b/crates/perry-runtime/src/promise/microtasks.rs index 6d5eab92c2..ad6dc7291e 100644 --- a/crates/perry-runtime/src/promise/microtasks.rs +++ b/crates/perry-runtime/src/promise/microtasks.rs @@ -655,6 +655,7 @@ fn pump_protected(mode: MicrotaskDrainMode, reentrant: bool, landed: bool, ran: // above allocate. let result = crate::closure::js_closure_call1( rooted_closure(&callback_handle), + crate::closure::plain_call_receiver(), value_handle.get_nanbox_f64(), ); // Keep the callback result rooted across `after()` (which @@ -810,8 +811,11 @@ fn pump_protected(mode: MicrotaskDrainMode, reentrant: bool, landed: bool, ran: }; crate::v8::promise_hook_before(rooted_promise(&next_handle)); let callback = rooted_closure(&callback_handle); - let result = - crate::closure::js_closure_call1(callback, value_handle.get_nanbox_f64()); + let result = crate::closure::js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + value_handle.get_nanbox_f64(), + ); CURRENT_MICROTASK_VALUE.with(|c| c.set(result)); let next_for_after = CURRENT_MICROTASK_NEXT.with(|c| c.get()); crate::v8::promise_hook_after(next_for_after); @@ -875,7 +879,10 @@ fn pump_protected(mode: MicrotaskDrainMode, reentrant: bool, landed: bool, ran: CURRENT_MICROTASK_VALUE.with(|c| c.set(0.0)); CURRENT_MICROTASK_NEXT.with(|c| c.set(std::ptr::null_mut())); crate::async_hooks::before(async_id, trigger_async_id); - crate::closure::js_closure_call0(rooted_closure(&callback_handle)); + crate::closure::js_closure_call0( + rooted_closure(&callback_handle), + crate::closure::plain_call_receiver(), + ); crate::async_hooks::after(async_id); crate::async_hooks::destroy(async_id); CURRENT_MICROTASK_PROMISE @@ -1189,9 +1196,13 @@ fn call_async_step_direct( // the stored function pointer instead. unsafe { let func_ptr = (*step_closure).func_ptr; - let func: extern "C" fn(*const crate::closure::ClosureHeader, f64, f64) -> f64 = - std::mem::transmute(func_ptr); - func(step_closure, value, is_error_bits) + crate::closure::body_call::js_body_call!( + func_ptr, + step_closure, + crate::closure::plain_call_receiver(), + value, + is_error_bits + ) } } diff --git a/crates/perry-runtime/src/promise/microtasks/empty.rs b/crates/perry-runtime/src/promise/microtasks/empty.rs index ab19a55f4e..0072a167a9 100644 --- a/crates/perry-runtime/src/promise/microtasks/empty.rs +++ b/crates/perry-runtime/src/promise/microtasks/empty.rs @@ -79,7 +79,10 @@ mod tests { fn queued_microtask_runs_and_restores_quiescence() { isolated(|| { static CALLED: AtomicU64 = AtomicU64::new(0); - extern "C" fn callback(_: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn callback( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { CALLED.fetch_add(1, Ordering::Relaxed); 0.0 } @@ -181,7 +184,11 @@ mod tests { fn buffered_stdin_is_delivered_without_any_timer() { isolated(|| { static CALLED: AtomicU64 = AtomicU64::new(0); - extern "C" fn callback(_: *const crate::closure::ClosureHeader, _: f64) -> f64 { + extern "C" fn callback( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _: f64, + ) -> f64 { CALLED.fetch_add(1, Ordering::Relaxed); 0.0 } @@ -237,7 +244,10 @@ mod tests { fn exit_promise_checkpoint_leaves_ticks_forbidden() { isolated(|| { static CALLED: AtomicU64 = AtomicU64::new(0); - extern "C" fn callback(_: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn callback( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { CALLED.fetch_add(1, Ordering::Relaxed); 0.0 } diff --git a/crates/perry-runtime/src/promise/mod.rs b/crates/perry-runtime/src/promise/mod.rs index 3b799dc472..7e0f416680 100644 --- a/crates/perry-runtime/src/promise/mod.rs +++ b/crates/perry-runtime/src/promise/mod.rs @@ -206,6 +206,7 @@ pub(crate) fn adapt_foreign_promise_value(value: f64) -> f64 { } unsafe { + // NOT-A-JS-BODY: a native Rust helper registered by another crate. let func: ForeignPromiseAdapterFn = std::mem::transmute(f); func(value) } diff --git a/crates/perry-runtime/src/promise/rejection_loop_tests.rs b/crates/perry-runtime/src/promise/rejection_loop_tests.rs index 5bccbf643a..b3401d7ddc 100644 --- a/crates/perry-runtime/src/promise/rejection_loop_tests.rs +++ b/crates/perry-runtime/src/promise/rejection_loop_tests.rs @@ -3,7 +3,12 @@ use super::*; // Models `for (...) { try { await Promise.reject(remaining) } catch {} }`. // Every resumption advances the loop even though no fulfilled await occurs. -extern "C" fn catching_step(step: ClosurePtr, remaining: f64, is_error: f64) -> f64 { +extern "C" fn catching_step( + step: ClosurePtr, + _this: crate::closure::JsThis, + remaining: f64, + is_error: f64, +) -> f64 { if is_error.to_bits() != crate::value::TAG_TRUE { return crate::value::js_nanbox_pointer(js_async_step_done(-1.0, step) as i64); } diff --git a/crates/perry-runtime/src/promise/resolving_function_kind_tests.rs b/crates/perry-runtime/src/promise/resolving_function_kind_tests.rs index 9e509a9983..537b58fbbf 100644 --- a/crates/perry-runtime/src/promise/resolving_function_kind_tests.rs +++ b/crates/perry-runtime/src/promise/resolving_function_kind_tests.rs @@ -64,7 +64,11 @@ fn resolving_functions_answer_reflection_from_their_kind() { } } -extern "C" fn unrelated_native_body(_closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn unrelated_native_body( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { value } diff --git a/crates/perry-runtime/src/promise/scanners.rs b/crates/perry-runtime/src/promise/scanners.rs index 71f4417f8c..ea74f91fe7 100644 --- a/crates/perry-runtime/src/promise/scanners.rs +++ b/crates/perry-runtime/src/promise/scanners.rs @@ -1056,6 +1056,7 @@ pub(crate) unsafe fn test_store_with_resolvers_result_fields( extern "C" fn with_resolvers_resolve_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let promise_box = crate::closure::js_closure_get_capture_f64(closure, 0); @@ -1066,6 +1067,7 @@ extern "C" fn with_resolvers_resolve_handler( extern "C" fn with_resolvers_reject_handler( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let promise_box = crate::closure::js_closure_get_capture_f64(closure, 0); diff --git a/crates/perry-runtime/src/promise/spec_combinators.rs b/crates/perry-runtime/src/promise/spec_combinators.rs index 21d99fe3e7..a1712ae1c7 100644 --- a/crates/perry-runtime/src/promise/spec_combinators.rs +++ b/crates/perry-runtime/src/promise/spec_combinators.rs @@ -167,6 +167,7 @@ fn is_callable(value: f64) -> bool { /// TypeError if either slot is already set (executor called twice). extern "C" fn capability_executor_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, resolve: f64, reject: f64, ) -> f64 { @@ -299,12 +300,8 @@ fn call_with_this(func: f64, this_arg: f64, args: &[f64]) -> Result { } else { (args.as_ptr(), args.len()) }; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); - let result = - combinator_catch_js(|| unsafe { crate::closure::js_native_call_value(func, ptr, len) }); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + let this = crate::closure::JsThis::from_f64(this_arg); + combinator_catch_js(|| unsafe { crate::closure::native_call_value_this(func, this, ptr, len) }) } /// `Invoke(obj, "then", args)` catching exceptions into `Err`. @@ -392,6 +389,7 @@ fn dec_remaining(state: *mut crate::array::ArrayHeader) -> bool { /// Promise.all Resolve Element Function. extern "C" fn all_resolve_element_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let guard = js_closure_get_capture_ptr(closure, 0) as *mut crate::array::ArrayHeader; @@ -414,6 +412,7 @@ extern "C" fn all_resolve_element_fn( /// Promise.allSettled Resolve Element Function → `{status:"fulfilled", value}`. extern "C" fn settled_fulfill_element_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { settled_element(closure, value, true) @@ -422,6 +421,7 @@ extern "C" fn settled_fulfill_element_fn( /// Promise.allSettled Reject Element Function → `{status:"rejected", reason}`. extern "C" fn settled_reject_element_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { settled_element(closure, reason, false) @@ -471,6 +471,7 @@ fn settled_element( /// AggregateError once all reject. extern "C" fn any_reject_element_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let guard = js_closure_get_capture_ptr(closure, 0) as *mut crate::array::ArrayHeader; @@ -1331,12 +1332,18 @@ mod fast_arm_tests { extern "C" fn own_then_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, on_fulfilled: f64, _on_rejected: f64, ) -> f64 { let v = crate::closure::js_closure_get_capture_f64(closure, 0); unsafe { - crate::closure::js_native_call_value(on_fulfilled, [v].as_ptr(), 1); + crate::closure::js_native_call_value( + on_fulfilled, + crate::closure::plain_call_receiver(), + [v].as_ptr(), + 1, + ); } undef() } diff --git a/crates/perry-runtime/src/promise/subclass.rs b/crates/perry-runtime/src/promise/subclass.rs index 5ad228b020..490c4b7400 100644 --- a/crates/perry-runtime/src/promise/subclass.rs +++ b/crates/perry-runtime/src/promise/subclass.rs @@ -152,11 +152,16 @@ pub extern "C" fn js_promise_subclass_init(this: f64, executor: f64) -> f64 { // closures and raw-pointer-bits closures, so `executor` is passed as-is. let args = [resolve.get_nanbox_f64(), reject.get_nanbox_f64()]; if let Err(reason) = super::combinators::combinator_catch_js(|| unsafe { - crate::closure::js_native_call_value(executor.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + executor.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) }) { let reject = crate::value::js_nanbox_get_pointer(reject.get_nanbox_f64()) as *const crate::closure::ClosureHeader; - crate::closure::js_closure_call1(reject, reason); + crate::closure::js_closure_call1(reject, crate::closure::plain_call_receiver(), reason); } // #7795: arm the probe gate before the field exists, so no reader can diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index c1a550b085..dbbc284cbb 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -436,6 +436,7 @@ pub extern "C" fn js_promise_resolve_with_promise(outer: *mut Promise, inner: *m /// Internal callback for forwarding resolve from inner to outer promise extern "C" fn promise_forward_resolve( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { let outer_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *mut Promise; @@ -446,6 +447,7 @@ extern "C" fn promise_forward_resolve( /// Internal callback for forwarding reject from inner to outer promise extern "C" fn promise_forward_reject( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let outer_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *mut Promise; @@ -1026,12 +1028,14 @@ fn call_receiver_then(receiver: f64, args: &[f64]) -> f64 { let err_val = crate::value::JSValue::pointer(err_ptr as *const u8).bits(); crate::exception::js_throw(f64::from_bits(err_val)); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = - unsafe { crate::closure::js_native_call_value(then_fn, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + then_fn, + crate::closure::JsThis::from_f64(receiver), + args.as_ptr(), + args.len(), + ) + } } fn throw_promise_finally_non_object() -> ! { @@ -1204,6 +1208,7 @@ fn promise_species_constructor(receiver: f64) -> f64 { // Captures: [on_fulfilled_f64, cap_resolve_f64, cap_reject_f64] extern "C" fn then_cap_fulfill_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::closure::{js_closure_get_capture_f64, js_native_call_value}; @@ -1221,7 +1226,11 @@ extern "C" fn then_cap_fulfill_fn( (value, false) } else { match crate::exception::catch_js_throw(|| { - crate::closure::js_closure_call1(on_ful_cl, value) + crate::closure::js_closure_call1( + on_ful_cl, + crate::closure::plain_call_receiver(), + value, + ) }) { Ok(ret) => (ret, false), Err(exc) => (exc, true), @@ -1230,7 +1239,14 @@ extern "C" fn then_cap_fulfill_fn( let func = if threw { cap_reject } else { cap_resolve }; let args = [result]; - let _ = unsafe { js_native_call_value(func, args.as_ptr(), 1) }; + let _ = unsafe { + js_native_call_value( + func, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 1, + ) + }; undef } @@ -1238,6 +1254,7 @@ extern "C" fn then_cap_fulfill_fn( // Captures: [on_rejected_f64, cap_resolve_f64, cap_reject_f64] extern "C" fn then_cap_reject_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::closure::{js_closure_get_capture_f64, js_native_call_value}; @@ -1255,7 +1272,11 @@ extern "C" fn then_cap_reject_fn( (reason, true) // passthrough rejection } else { match crate::exception::catch_js_throw(|| { - crate::closure::js_closure_call1(on_rej_cl, reason) + crate::closure::js_closure_call1( + on_rej_cl, + crate::closure::plain_call_receiver(), + reason, + ) }) { Ok(ret) => (ret, false), Err(exc) => (exc, true), @@ -1264,7 +1285,14 @@ extern "C" fn then_cap_reject_fn( let func = if threw { cap_reject } else { cap_resolve }; let args = [result]; - let _ = unsafe { js_native_call_value(func, args.as_ptr(), 1) }; + let _ = unsafe { + js_native_call_value( + func, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 1, + ) + }; undef } @@ -1335,6 +1363,7 @@ fn perform_promise_then_with_cap( // Captures [value_f64]: ignores its argument and returns the captured value. extern "C" fn spec_value_return_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { crate::closure::js_closure_get_capture_f64(closure, 0) @@ -1343,6 +1372,7 @@ extern "C" fn spec_value_return_fn( // Captures [reason_f64]: ignores its argument and throws the captured reason. extern "C" fn spec_reason_thrower_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { let reason = crate::closure::js_closure_get_capture_f64(closure, 0); @@ -1352,6 +1382,7 @@ extern "C" fn spec_reason_thrower_fn( // thenFinally: captures [C_f64, on_finally_f64], called with fulfilled value. extern "C" fn spec_then_finally_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { use crate::closure::{ @@ -1370,7 +1401,7 @@ extern "C" fn spec_then_finally_fn( let result = if on_finally_cl.is_null() { undef } else { - crate::closure::js_closure_call0(on_finally_cl) + crate::closure::js_closure_call0(on_finally_cl, crate::closure::plain_call_receiver()) }; // Build valueThunk = () => value @@ -1388,6 +1419,7 @@ extern "C" fn spec_then_finally_fn( // catchFinally: captures [C_f64, on_finally_f64], called with rejection reason. extern "C" fn spec_catch_finally_fn( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { use crate::closure::{ @@ -1406,7 +1438,7 @@ extern "C" fn spec_catch_finally_fn( let result = if on_finally_cl.is_null() { undef } else { - crate::closure::js_closure_call0(on_finally_cl) + crate::closure::js_closure_call0(on_finally_cl, crate::closure::plain_call_receiver()) }; // Build thrower = () => { throw reason; } @@ -1445,11 +1477,12 @@ fn ensure_spec_finally_arities_registered() { pub(crate) extern "C" fn promise_prototype_then_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, on_fulfilled: f64, on_rejected: f64, ) -> f64 { ensure_spec_finally_arities_registered(); - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let promise = if js_value_is_promise(receiver) != 0 { crate::value::js_nanbox_get_pointer(receiver) as *mut Promise } else if let Some(backing) = super::subclass::subclass_backing_promise(receiver) { @@ -1501,9 +1534,10 @@ pub(crate) extern "C" fn promise_prototype_then_thunk( pub(crate) extern "C" fn promise_prototype_catch_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, on_rejected: f64, ) -> f64 { - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); let args = [f64::from_bits(crate::value::TAG_UNDEFINED), on_rejected]; call_receiver_then(receiver, &args) } @@ -1514,12 +1548,13 @@ pub(crate) extern "C" fn promise_prototype_catch_thunk( pub(crate) extern "C" fn promise_prototype_finally_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, on_finally: f64, ) -> f64 { use crate::closure::{js_closure_alloc, js_closure_set_capture_f64}; ensure_spec_finally_arities_registered(); - let receiver = crate::object::js_implicit_this_get(); + let receiver = this.as_f64(); // receiver must be a JS Object — pointer-tagged, not a registered symbol, not a handle. if !is_promise_species_object(receiver) { @@ -1556,7 +1591,7 @@ pub(crate) extern "C" fn promise_prototype_finally_thunk( /// `then`/`catch`/`finally` value-read, or `None` for any other property. /// Returns the shared prototype method so `p.then === Promise.prototype.then` /// (spec-required identity) and `.call(receiver)` properly receives the caller's -/// `this` via IMPLICIT_THIS rather than a captured pointer. +/// `this` as its receiver parameter rather than a captured pointer. pub unsafe fn js_promise_bound_method(_promise: *mut Promise, property: &str) -> Option { if !matches!(property, "then" | "catch" | "finally") { return None; @@ -1590,6 +1625,7 @@ pub unsafe fn js_promise_bound_method(_promise: *mut Promise, property: &str) -> /// Called with the upstream fulfilled `value`. extern "C" fn finally_fulfill_wrapper( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { finally_wrapper_common(closure, value, true) @@ -1600,6 +1636,7 @@ extern "C" fn finally_fulfill_wrapper( /// Called with the upstream rejection `reason`. extern "C" fn finally_reject_wrapper( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { finally_wrapper_common(closure, reason, false) @@ -1653,17 +1690,18 @@ fn finally_wrapper_common( // report 1, failing every finally test that asserts a zero-arg invocation. // (Armed in a C trampoline frame, #9305; the rejection below runs after // the trap is popped, as before.) - let ret = - match crate::exception::catch_js_throw(|| crate::closure::js_closure_call0(on_finally)) { - Ok(ret) => ret, - Err(exc) => { - // onFinally threw — reject `next` with the thrown value. - if !next.is_null() { - js_promise_reject(next, exc); - } - return undef; + let ret = match crate::exception::catch_js_throw(|| { + crate::closure::js_closure_call0(on_finally, crate::closure::plain_call_receiver()) + }) { + Ok(ret) => ret, + Err(exc) => { + // onFinally threw — reject `next` with the thrown value. + if !next.is_null() { + js_promise_reject(next, exc); } - }; + return undef; + } + }; // If onFinally returned a Promise/thenable, adopt it: wait for it before // settling `next`. `js_assimilate_thenable` returns a native Promise for @@ -1742,6 +1780,7 @@ fn finally_settle_next_with_extra_hop(next: *mut Promise, orig: f64, is_fulfille /// Captures [next_promise_ptr (i64), orig (f64), is_fulfilled (TAG_TRUE/FALSE)]. extern "C" fn finally_cleanup_fulfill( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _cleanup_value: f64, ) -> f64 { use crate::closure::{js_closure_get_capture_f64, js_closure_get_capture_ptr}; @@ -1762,6 +1801,7 @@ extern "C" fn finally_cleanup_fulfill( /// the upstream outcome). Captures [next_promise_ptr (i64)]. extern "C" fn finally_cleanup_reject( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, cleanup_reason: f64, ) -> f64 { use crate::closure::js_closure_get_capture_ptr; @@ -1776,6 +1816,7 @@ extern "C" fn finally_cleanup_reject( /// Captures [next_promise_ptr (i64), value (f64)]. extern "C" fn finally_passthrough_fulfill( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _: f64, ) -> f64 { use crate::closure::{js_closure_get_capture_f64, js_closure_get_capture_ptr}; @@ -1818,6 +1859,7 @@ extern "C" fn finally_passthrough_fulfill( /// Captures [next_promise_ptr (i64), reason (f64)]. extern "C" fn finally_passthrough_reject( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _: f64, ) -> f64 { use crate::closure::{js_closure_get_capture_f64, js_closure_get_capture_ptr}; diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 2b6a7c039a..f0f71f701b 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -20,8 +20,6 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; -use crate::closure::{js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call3}; - mod apply_construct; pub use apply_construct::{call_proxy_value_with_this, js_proxy_apply, js_proxy_construct}; pub(crate) use apply_construct::{is_callable_function, is_constructor_function}; @@ -701,31 +699,31 @@ fn coerce_trap_bool(value: f64) -> f64 { /// Invoke a present (already-confirmed-callable) handler trap with the handler /// bound as the trap's `this` (ECMA-262: traps are called as /// `Call(trap, handler, args)`). Object-literal/method traps read `this` from a -/// reserved closure slot, while free-function traps fall back to -/// `IMPLICIT_THIS`; we set both so either style observes the handler. Mirrors +/// reserved closure slot, while free-function traps read the `this` +/// parameter; we supply both so either style observes the handler. Mirrors /// the apply/construct/getOwnPropertyDescriptor trap-call dance, which the /// per-trap paths (get/set/has/deleteProperty/defineProperty/…) previously /// skipped — they called the trap with the wrong `this` and, for get/set, /// dropped the trailing `receiver` argument. fn call_trap(handler: f64, trap: f64, args: &[f64]) -> f64 { - let rebound = crate::closure::clone_closure_rebind_this(trap.to_bits(), handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let handler = this_scope.root_nanbox_f64(handler); + let rebound = + crate::closure::clone_closure_rebind_this(trap.to_bits(), handler.get_nanbox_f64()); let closure = closure_from(f64::from_bits(rebound)); if closure.is_null() { return throw_type_error("proxy trap is not a function"); } let undef = f64::from_bits(TAG_UNDEFINED); let a = |i: usize| -> f64 { args.get(i).copied().unwrap_or(undef) }; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); - let result = match args.len() { - 0 => js_closure_call0(closure), - 1 => js_closure_call1(closure, a(0)), - 2 => js_closure_call2(closure, a(0), a(1)), - 3 => js_closure_call3(closure, a(0), a(1), a(2)), - _ => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), - }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + let this = crate::closure::JsThis::from_f64(handler.get_nanbox_f64()); + match args.len() { + 0 => crate::closure::js_closure_call0(closure, this), + 1 => crate::closure::js_closure_call1(closure, this, a(0)), + 2 => crate::closure::js_closure_call2(closure, this, a(0), a(1)), + 3 => crate::closure::js_closure_call3(closure, this, a(0), a(1), a(2)), + _ => crate::closure::js_closure_call4(closure, this, a(0), a(1), a(2), a(3)), + } } /// Throw `TypeError: Reflect. called on non-object`. Does not return. @@ -992,11 +990,11 @@ enum MovedElement<'a> { /// Invoke a callable `f64` value with the supplied positional args and an /// explicit `thisArg` binding, throwing `TypeError` if `f` is not callable. -/// Used by `Reflect.apply`. `thisArg` flows through `IMPLICIT_THIS` so free +/// Used by `Reflect.apply`. `thisArg` is passed as the `this` parameter so free /// functions reading `this` observe it. fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { // A concise/object-literal method reads `this` from a baked capture slot, - // not IMPLICIT_THIS; rebind to the explicit `Reflect.apply` receiver so it + // not only the `this` parameter; rebind to the explicit `Reflect.apply` receiver so it // is honored (no-op for arrows / plain fns / bound fns). // // That rebind is also the one thing on this path that ALLOCATES, and the @@ -1036,29 +1034,26 @@ fn dispatch_with_explicit_this(f: f64, this_arg: f64, args: &[f64]) -> f64 { if closure.is_null() { return throw_type_error("Reflect.apply target is not a function"); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); + let this = crate::closure::JsThis::from_f64(this_arg); let a = |i: usize| -> f64 { args.get(i) .copied() .unwrap_or(f64::from_bits(TAG_UNDEFINED)) }; - let result = match args.len() { - 0 => js_closure_call0(closure), - 1 => js_closure_call1(closure, a(0)), - 2 => js_closure_call2(closure, a(0), a(1)), - 3 => js_closure_call3(closure, a(0), a(1), a(2)), - 4 => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), + match args.len() { + 0 => crate::closure::js_closure_call0(closure, this), + 1 => crate::closure::js_closure_call1(closure, this, a(0)), + 2 => crate::closure::js_closure_call2(closure, this, a(0), a(1)), + 3 => crate::closure::js_closure_call3(closure, this, a(0), a(1), a(2)), + 4 => crate::closure::js_closure_call4(closure, this, a(0), a(1), a(2), a(3)), // #10425: this arm was `_ => js_closure_call4(…)`, so every argument // after the fourth was dropped. The variadic entry point owns // arbitrary-arity dispatch, rest bundling included. (`call_trap` // above keeps its catch-all: a proxy trap receives at most four.) n => unsafe { - crate::closure::js_closure_call_array(closure as i64, args.as_ptr(), n as i64) + crate::closure::js_closure_call_array(closure as i64, this, args.as_ptr(), n as i64) }, - }; - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + } } /// Resolve the ultimate target when a Proxy wraps a class constructor. Used @@ -1723,15 +1718,19 @@ fn call_setter_with_receiver(setter_bits: u64, receiver: f64, value: f64) -> boo if setter_bits == 0 { return false; } - let rebound = crate::closure::clone_closure_rebind_this(setter_bits, receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let receiver = this_scope.root_nanbox_f64(receiver); + let value = this_scope.root_nanbox_f64(value); + let rebound = crate::closure::clone_closure_rebind_this(setter_bits, receiver.get_nanbox_f64()); let closure = closure_from(f64::from_bits(rebound)); if closure.is_null() { return false; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let _ = js_closure_call1(closure, value); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); + let _ = crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + value.get_nanbox_f64(), + ); true } @@ -2956,7 +2955,10 @@ mod tests { /// including through a nested proxy whose inner proxy gets revoked. #[test] fn revoked_proxy_keeps_creation_callability() { - extern "C" fn dummy_fn(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn dummy_fn( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { f64::from_bits(TAG_UNDEFINED) } let f = crate::closure::js_closure_alloc(dummy_fn as *const u8, 0); diff --git a/crates/perry-runtime/src/proxy/apply_construct.rs b/crates/perry-runtime/src/proxy/apply_construct.rs index 8eea0862a2..0072991dec 100644 --- a/crates/perry-runtime/src/proxy/apply_construct.rs +++ b/crates/perry-runtime/src/proxy/apply_construct.rs @@ -92,10 +92,10 @@ pub(crate) fn is_constructor_function(value: f64) -> bool { /// Forward a `[[Call]]` to `target` (the default behavior when a proxy has no /// `apply` trap). If `target` is itself a proxy, recurse so its own trap chain /// runs; otherwise invoke the target through the canonical value-call path with -/// `this_arg` bound via `IMPLICIT_THIS`. Routing through `js_native_call_value` -/// (rather than calling the closure directly) also recovers built-in prototype -/// methods invoked as values — e.g. forwarding to `Object.prototype.hasOwnProperty` -/// re-dispatches by name with the receiver taken from `IMPLICIT_THIS`. +/// `this_arg` as its receiver. Routing through the value-call path (rather than +/// calling the closure directly) also recovers built-in prototype methods +/// invoked as values — e.g. forwarding to `Object.prototype.hasOwnProperty` +/// re-dispatches by name with `this_arg` as the receiver. fn forward_apply(target: f64, this_arg: f64, args_array: f64) -> f64 { if lookup(target).is_some() { return js_proxy_apply(target, this_arg, args_array); @@ -121,15 +121,16 @@ fn forward_apply(target: f64, this_arg: f64, args_array: f64) -> f64 { } else { (buf.as_ptr(), buf.len()) }; - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - this_arg.get_nanbox_f64(), - )); // Keep the native argument owner above the trap. A JS longjmp would // otherwise bypass Vec::drop on every throwing forwarded proxy call. let result = crate::exception::catch_js_throw(|| unsafe { - crate::closure::js_native_call_value(target.get_nanbox_f64(), ptr, n) + crate::closure::native_call_value_this( + target.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_arg.get_nanbox_f64()), + ptr, + n, + ) }); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); drop(buf); match result { Ok(value) => value, @@ -150,7 +151,6 @@ pub fn call_proxy_value_with_this(proxy: f64, this_arg: f64, args: &[f64]) -> f6 let proxy_h = scope.root_nanbox_f64(proxy); let this_h = scope.root_nanbox_f64(this_arg); let arg_handles: Vec<_> = args.iter().map(|a| scope.root_nanbox_f64(*a)).collect(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let result = crate::exception::catch_js_throw(|| { let arr_h = scope .root_nanbox_u64(POINTER_TAG | (crate::array::js_array_alloc(0) as u64 & POINTER_MASK)); @@ -165,7 +165,6 @@ pub fn call_proxy_value_with_this(proxy: f64, this_arg: f64, args: &[f64]) -> f6 f64::from_bits(arr_h.get_nanbox_u64()), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); drop(arg_handles); match result { Ok(value) => value, @@ -238,25 +237,22 @@ pub extern "C" fn js_proxy_apply(proxy_boxed: f64, this_arg: f64, args_array: f6 } // Invoke the trap with the handler bound as `this` and the spec argument // list (target, thisArgument, argArray). Object-literal/free-function traps - // read `this` from a closure slot and/or the IMPLICIT_THIS fallback, so we - // set both — mirroring the `Reflect.get` accessor path. + // read `this` from a closure slot and/or the `this` parameter, so we + // supply both — mirroring the `Reflect.get` accessor path. let rebound = crate::closure::clone_closure_rebind_this(trap_bits, handler.get_nanbox_f64()); let closure = closure_from(f64::from_bits(rebound)); if closure.is_null() { return throw_type_error("proxy apply trap is not a function"); } - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - handler.get_nanbox_f64(), - )); let result = crate::exception::catch_js_throw(|| { js_closure_call3( closure, + crate::closure::JsThis::from_f64(handler.get_nanbox_f64()), target.get_nanbox_f64(), this_arg.get_nanbox_f64(), args_array.get_nanbox_f64(), ) }); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); match result { Ok(value) => value, Err(error) => crate::exception::js_throw(error), @@ -366,18 +362,15 @@ pub extern "C" fn js_proxy_construct(proxy_boxed: f64, args_array: f64, new_targ if closure.is_null() { return throw_type_error("proxy construct trap is not a function"); } - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - handler.get_nanbox_f64(), - )); let result = crate::exception::catch_js_throw(|| { js_closure_call3( closure, + crate::closure::JsThis::from_f64(handler.get_nanbox_f64()), target.get_nanbox_f64(), args_array.get_nanbox_f64(), nt.get_nanbox_f64(), ) }); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); match result { Ok(value) => value, Err(error) => crate::exception::js_throw(error), diff --git a/crates/perry-runtime/src/proxy/get.rs b/crates/perry-runtime/src/proxy/get.rs index 08c1ab2e3c..05f565455a 100644 --- a/crates/perry-runtime/src/proxy/get.rs +++ b/crates/perry-runtime/src/proxy/get.rs @@ -111,9 +111,6 @@ pub(crate) fn proxy_get_with_receiver(proxy_boxed: f64, key: f64, receiver: f64) // Ordinary target getters and further prototype hops must keep Receiver. // Clear/restore the override around the operation; getters and Proxy hops // consume it before entering user code so nested reads bind independently. - let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - receiver.get_nanbox_f64(), - )); let previous_override = crate::object::accessor_receiver_override_begin(receiver.get_nanbox_f64()) .map(|value| scope.root_nanbox_f64(value)); @@ -121,6 +118,5 @@ pub(crate) fn proxy_get_with_receiver(proxy_boxed: f64, key: f64, receiver: f64) crate::object::accessor_receiver_override_end( previous_override.map(|value| value.get_nanbox_f64()), ); - crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/proxy/json.rs b/crates/perry-runtime/src/proxy/json.rs index ea29a912bc..eec74e1cb9 100644 --- a/crates/perry-runtime/src/proxy/json.rs +++ b/crates/perry-runtime/src/proxy/json.rs @@ -64,7 +64,11 @@ pub(crate) fn js_proxy_own_keys_for_json(proxy_boxed: f64) -> f64 { } let trap = handler_trap(handler, "ownKeys"); if is_callable(trap) { - return crate::closure::js_closure_call1(closure_from(trap), target); + return crate::closure::js_closure_call1( + closure_from(trap), + crate::closure::plain_call_receiver(), + target, + ); } if lookup(target).is_some() { return js_proxy_own_keys_for_json(target); diff --git a/crates/perry-runtime/src/proxy/reflect.rs b/crates/perry-runtime/src/proxy/reflect.rs index c61a1724ec..efc8f866ee 100644 --- a/crates/perry-runtime/src/proxy/reflect.rs +++ b/crates/perry-runtime/src/proxy/reflect.rs @@ -1,9 +1,9 @@ use super::{ closure_from, coerce_trap_bool, extract_pointer, handler_trap, is_callable_function, - js_closure_call0, js_closure_call2, js_proxy_delete, js_proxy_get, js_proxy_has, lookup, - nanbox_bool, reflect_non_object_typeerror, reflect_ordinary_delete_property_key, - reflect_ordinary_set_with_receiver, reflect_value_is_object, revoked_return, - target_get_property_key, throw_type_error, PROXIES, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, + js_proxy_delete, js_proxy_get, js_proxy_has, lookup, nanbox_bool, reflect_non_object_typeerror, + reflect_ordinary_delete_property_key, reflect_ordinary_set_with_receiver, + reflect_value_is_object, revoked_return, target_get_property_key, throw_type_error, PROXIES, + TAG_NULL, TAG_TRUE, TAG_UNDEFINED, }; /// `Reflect.get(target, key, receiver)` (#2766). @@ -51,7 +51,7 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { } // #2766: if `key` resolves to an accessor *getter* on `target`, rebind its // `this` to the receiver and invoke it - object-literal getters capture - // `this` in a reserved closure slot (not `IMPLICIT_THIS`), so plain + // `this` in a reserved closure slot (not only the `this` parameter), so plain // forwarding would read the target's fields, not the receiver's. When the // receiver equals the target we can skip the clone and use the ordinary // read. @@ -76,22 +76,20 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { crate::closure::clone_closure_rebind_this(getter.get_nanbox_f64().to_bits(), recv); let closure = closure_from(f64::from_bits(rebound)); if !closure.is_null() { - // Also set IMPLICIT_THIS for free-function getters that read - // `this` from the implicit-this fallback rather than a slot. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + // Also pass the receiver as the `this` parameter for + // free-function getters that read it rather than a slot. let recv = if receiver_handle.get_nanbox_f64().to_bits() == TAG_UNDEFINED { target_handle.get_nanbox_f64() } else { receiver_handle.get_nanbox_f64() }; - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); - let result = js_closure_call0(closure); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return result; + return crate::closure::js_closure_call0( + closure, + crate::closure::JsThis::from_f64(recv), + ); } } } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let recv = if receiver_handle.get_nanbox_f64().to_bits() == TAG_UNDEFINED { target_handle.get_nanbox_f64() } else { @@ -109,13 +107,10 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { ) }; } - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); - let result = target_get_property_key( + target_get_property_key( target_handle.get_nanbox_f64(), property_key_handle.get_nanbox_f64(), - ); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + ) } /// `Reflect.set(target, key, value, receiver?)` - returns the boolean result @@ -444,16 +439,12 @@ pub extern "C" fn js_reflect_get_own_property_descriptor(target: f64, key: f64) if closure.is_null() { return throw_type_error("proxy getOwnPropertyDescriptor trap is not a function"); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - handler_handle.get_nanbox_f64(), - )); - let result = js_closure_call2( + let result = crate::closure::js_closure_call2( closure, + crate::closure::JsThis::from_f64(handler_handle.get_nanbox_f64()), inner_handle.get_nanbox_f64(), property_key_handle.get_nanbox_f64(), ); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); let result_handle = scope.root_nanbox_f64(result); let target_desc = crate::object::js_object_get_own_property_descriptor( diff --git a/crates/perry-runtime/src/proxy/reflect_misc.rs b/crates/perry-runtime/src/proxy/reflect_misc.rs index 2ae7545d1f..ebc1a867ea 100644 --- a/crates/perry-runtime/src/proxy/reflect_misc.rs +++ b/crates/perry-runtime/src/proxy/reflect_misc.rs @@ -366,6 +366,7 @@ pub extern "C" fn js_reflect_prevent_extensions(target: f64) -> f64 { /// already-revoked proxy is a no-op (Node's `revoke()` is idempotent). (#2846) pub(super) extern "C" fn proxy_revoke_trampoline( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let proxy = crate::closure::js_closure_get_capture_f64(closure, 0); js_proxy_revoke(proxy); @@ -378,6 +379,7 @@ mod tests { extern "C" fn weighted_six( _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, a0: f64, a1: f64, a2: f64, diff --git a/crates/perry-runtime/src/pty/mod.rs b/crates/perry-runtime/src/pty/mod.rs index 5f120e0518..84aeb2b3b5 100644 --- a/crates/perry-runtime/src/pty/mod.rs +++ b/crates/perry-runtime/src/pty/mod.rs @@ -51,8 +51,7 @@ mod platform_impl { cp_get_field, cp_make_error, cp_object_ptr, cp_set_field, cp_this, cp_undefined, cp_value_to_bytes, cp_value_to_string, CpFn, }; - use crate::closure::{js_native_call_value, ClosureHeader}; - use crate::object::js_implicit_this_set; + use crate::closure::{native_call_value_this, ClosureHeader, JsThis}; use crate::value::JSValue; // Shape-id band kept clear of cluster (0x7FFF_FC80) and child_process @@ -83,7 +82,6 @@ mod platform_impl { let scope = crate::gc::RuntimeHandleScope::new(); let target = scope.root_nanbox_f64(target); let args = scope.root_nanbox_f64_slice(args); - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let key = pty_listener_key(event); let Some(arr) = cp_array_ptr(cp_get_field(target.get_nanbox_f64(), &key)) else { return; @@ -95,16 +93,15 @@ mod platform_impl { .collect(); let callbacks = scope.root_nanbox_f64_slice(&callbacks); for cb in callbacks { - js_implicit_this_set(target.get_nanbox_f64()); let current_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&args); unsafe { - let _ = js_native_call_value( + let _ = native_call_value_this( cb.get_nanbox_f64(), + JsThis::from_f64(target.get_nanbox_f64()), current_args.as_ptr(), current_args.len(), ); } - js_implicit_this_set(prev.get_nanbox_f64()); } } @@ -139,8 +136,11 @@ mod platform_impl { val } - extern "C" fn pty_disposable_dispose(closure: *const ClosureHeader) -> f64 { - let this = cp_this(closure); + extern "C" fn pty_disposable_dispose( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { + let this = cp_this(_this, closure); let target = cp_get_field(this, b"__ptyTarget"); let cb = cp_get_field(this, b"__ptyCb"); if let Some(event) = cp_value_to_string(cp_get_field(this, b"__ptyEvent")) { @@ -165,20 +165,32 @@ mod platform_impl { // ----- IPty method bodies (slot 0 of each closure = the IPty object) ---- - extern "C" fn pty_method_on_data(closure: *const ClosureHeader, cb: f64) -> f64 { - let this = cp_this(closure); + extern "C" fn pty_method_on_data( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + cb: f64, + ) -> f64 { + let this = cp_this(_this, closure); pty_register(this, "data", cb); pty_make_disposable(this, "data", cb) } - extern "C" fn pty_method_on_exit(closure: *const ClosureHeader, cb: f64) -> f64 { - let this = cp_this(closure); + extern "C" fn pty_method_on_exit( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + cb: f64, + ) -> f64 { + let this = cp_this(_this, closure); pty_register(this, "exit", cb); pty_make_disposable(this, "exit", cb) } - extern "C" fn pty_method_write(closure: *const ClosureHeader, data: f64) -> f64 { - let this = cp_this(closure); + extern "C" fn pty_method_write( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + data: f64, + ) -> f64 { + let this = cp_this(_this, closure); if let Some(handle) = pty_handle_of(this) { let bytes = cp_value_to_bytes(data); if !bytes.is_empty() { @@ -188,8 +200,13 @@ mod platform_impl { cp_undefined() } - extern "C" fn pty_method_resize(closure: *const ClosureHeader, cols: f64, rows: f64) -> f64 { - let this = cp_this(closure); + extern "C" fn pty_method_resize( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + cols: f64, + rows: f64, + ) -> f64 { + let this = cp_this(_this, closure); let cols_i = pty_arg_i32(cols); let rows_i = pty_arg_i32(rows); if cols_i <= 0 || rows_i <= 0 || cols_i > u16::MAX as i32 || rows_i > u16::MAX as i32 { @@ -208,23 +225,33 @@ mod platform_impl { cp_undefined() } - extern "C" fn pty_method_kill(closure: *const ClosureHeader, signal: f64) -> f64 { - let this = cp_this(closure); + extern "C" fn pty_method_kill( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + signal: f64, + ) -> f64 { + let this = cp_this(_this, closure); if let Some(handle) = pty_handle_of(this) { reactor::pty_live_kill(handle, pty_parse_kill_signal(signal)); } cp_undefined() } - extern "C" fn pty_method_pause(closure: *const ClosureHeader) -> f64 { - if let Some(handle) = pty_handle_of(cp_this(closure)) { + extern "C" fn pty_method_pause( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { + if let Some(handle) = pty_handle_of(cp_this(this, closure)) { reactor::pty_live_set_paused(handle, true); } cp_undefined() } - extern "C" fn pty_method_resume(closure: *const ClosureHeader) -> f64 { - if let Some(handle) = pty_handle_of(cp_this(closure)) { + extern "C" fn pty_method_resume( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { + if let Some(handle) = pty_handle_of(cp_this(this, closure)) { reactor::pty_live_set_paused(handle, false); } cp_undefined() @@ -233,9 +260,12 @@ mod platform_impl { /// opencode's `Pty` wrapper calls `dispose()`; node-pty's UnixTerminal /// exposes the equivalent `destroy()`. Both are "hang up the terminal": /// kill with the default SIGHUP. - extern "C" fn pty_method_dispose(closure: *const ClosureHeader) -> f64 { - pty_method_resume(closure); - pty_method_kill(closure, cp_undefined()) + extern "C" fn pty_method_dispose( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { + pty_method_resume(closure, this); + pty_method_kill(closure, this, cp_undefined()) } /// `kill([signal])` — node-pty defaults to `SIGHUP` (a hangup is how a @@ -430,21 +460,29 @@ mod platform_impl { /// runner can't interleave their pumps. static TEST_LOCK: Mutex<()> = Mutex::new(()); - extern "C" fn test_data_listener(_closure: *const ClosureHeader, chunk: f64) -> f64 { + extern "C" fn test_data_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, + ) -> f64 { if let Some(s) = cp_value_to_string(chunk) { DATA_SINK.lock().unwrap().push_str(&s); } cp_undefined() } - extern "C" fn test_exit_listener(_closure: *const ClosureHeader, payload: f64) -> f64 { + extern "C" fn test_exit_listener( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + payload: f64, + ) -> f64 { let code = cp_get_field(payload, b"exitCode"); let signal = cp_get_field(payload, b"signal"); *EXIT_SINK.lock().unwrap() = Some((code, signal)); cp_undefined() } - fn listener_value(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> f64 { + fn listener_value(f: crate::closure::body_call::js_body_fn_ty!(a)) -> f64 { crate::closure::js_register_closure_arity(f as *const u8, 1); let closure = crate::closure::js_closure_alloc(f as *const u8, 0); cp_box_ptr(closure as *const u8) diff --git a/crates/perry-runtime/src/pty/tests.rs b/crates/perry-runtime/src/pty/tests.rs index 39fdccd902..fa6f6c9360 100644 --- a/crates/perry-runtime/src/pty/tests.rs +++ b/crates/perry-runtime/src/pty/tests.rs @@ -13,29 +13,46 @@ thread_local! { static REMOVED_CALLS: Cell = const { Cell::new(0) }; } -extern "C" fn removed(_closure: *const ClosureHeader, _chunk: f64) -> f64 { +extern "C" fn removed( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _chunk: f64, +) -> f64 { REMOVED_CALLS.with(|n| n.set(n.get() + 1)); cp_undefined() } -extern "C" fn data(_closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn data( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { OUTPUT.with(|o| o.borrow_mut().push_str(&cp_value_to_string(chunk).unwrap())); cp_undefined() } -extern "C" fn exited(_closure: *const ClosureHeader, event: f64) -> f64 { +extern "C" fn exited( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + event: f64, +) -> f64 { EXITS.with(|e| e.borrow_mut().push(cp_get_field(event, b"exitCode"))); cp_undefined() } -fn callback(f: extern "C" fn(*const ClosureHeader, f64) -> f64) -> f64 { +fn callback(f: crate::closure::body_call::js_body_fn_ty!(a)) -> f64 { crate::closure::js_register_closure_arity(f as *const u8, 1); cp_box_ptr(crate::closure::js_closure_alloc(f as *const u8, 0).cast()) } fn call(target: f64, name: &[u8], args: &[f64]) -> f64 { unsafe { - crate::closure::js_native_call_value(cp_get_field(target, name), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + cp_get_field(target, name), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) } } @@ -87,7 +104,12 @@ fn aliases_share_native_spawn_and_paused_output_precedes_exit() { opts.get_nanbox_f64(), ]; let term = scope.root_nanbox_f64(unsafe { - crate::closure::js_native_call_value(spawn.get_nanbox_f64(), args.as_ptr(), args.len()) + crate::closure::js_native_call_value( + spawn.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) }); assert!(cp_get_field(term.get_nanbox_f64(), b"pid") > 0.0); let handle = pty_handle_of(term.get_nanbox_f64()).unwrap(); diff --git a/crates/perry-runtime/src/readline_helpers.rs b/crates/perry-runtime/src/readline_helpers.rs index e7c5e8a6ae..88d3dd5ca0 100644 --- a/crates/perry-runtime/src/readline_helpers.rs +++ b/crates/perry-runtime/src/readline_helpers.rs @@ -246,7 +246,11 @@ pub fn split_keypress_chunks(bytes: &[u8]) -> Vec> { chunks } -extern "C" fn emit_keypress_data(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn emit_keypress_data( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + chunk: f64, +) -> f64 { let stream_scope = crate::gc::RuntimeHandleScope::new(); let stream = stream_scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 0)); let chunk = stream_scope.root_nanbox_f64(chunk); diff --git a/crates/perry-runtime/src/regex/match_all.rs b/crates/perry-runtime/src/regex/match_all.rs index ff57fa7980..f9aa3e2a55 100644 --- a/crates/perry-runtime/src/regex/match_all.rs +++ b/crates/perry-runtime/src/regex/match_all.rs @@ -141,9 +141,10 @@ pub(crate) fn regexp(receiver: f64, argument: f64) -> Result { pub(crate) extern "C" fn regexp_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, argument: f64, ) -> f64 { - api::finish(regexp(crate::object::js_implicit_this_get(), argument)) + api::finish(regexp(this.as_f64(), argument)) } fn string(receiver: f64, pattern: f64) -> Result { diff --git a/crates/perry-runtime/src/regex/perex_dispatch.rs b/crates/perry-runtime/src/regex/perex_dispatch.rs index f22949f98c..f3435f0159 100644 --- a/crates/perry-runtime/src/regex/perex_dispatch.rs +++ b/crates/perry-runtime/src/regex/perex_dispatch.rs @@ -50,16 +50,11 @@ pub(super) fn is_regexp(value: &RuntimeHandle<'_>) -> Result } pub(crate) fn get(owner: &RuntimeHandle<'_>, name: &[u8]) -> Result { - let scope = RuntimeHandleScope::new(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - let result = api::caught(|| { + api::caught(|| { let key = crate::string::canonical_key(name); let value = owner.get_nanbox_f64(); crate::proxy::js_reflect_get(value, js_nanbox_string(key as i64), value) - }); - // Getter dispatch can throw through its own normal this-restoration path. - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - result + }) } pub(crate) fn call_one( @@ -68,9 +63,7 @@ pub(crate) fn call_one( argument: &RuntimeHandle<'_>, ) -> Result { let scope = RuntimeHandleScope::new(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - let result = api::caught(|| { - crate::object::js_implicit_this_set(receiver.get_nanbox_f64()); + api::caught(|| { if crate::proxy::js_proxy_is_proxy(method.get_nanbox_f64()) == 1 { // The generic value-call bridge drops this for proxies. Supply // the actual receiver and an exact one-element GC argument array. @@ -86,12 +79,15 @@ pub(crate) fn call_one( } else { let args = [argument.get_nanbox_f64()]; unsafe { - crate::closure::js_native_call_value(method.get_nanbox_f64(), args.as_ptr(), 1) + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + args.as_ptr(), + 1, + ) } } - }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - result + }) } #[cfg(test)] @@ -203,25 +199,18 @@ pub(crate) fn to_string(value: &RuntimeHandle<'_>) -> Result<*mut StringHeader, "Cannot convert a Symbol value to a string", )); } - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - let result = - api::caught(|| crate::value::js_jsvalue_to_string_coerce(primitive.get_nanbox_f64())); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - let string = result?; + let string = + api::caught(|| crate::value::js_jsvalue_to_string_coerce(primitive.get_nanbox_f64()))?; crate::string::js_string_addref(string); Ok(string) } pub(crate) fn get_symbol(owner: &RuntimeHandle<'_>, name: &str) -> Result { - let scope = RuntimeHandleScope::new(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - let result = api::caught(|| { + api::caught(|| { let key = crate::symbol::well_known_symbol(name); let value = owner.get_nanbox_f64(); crate::proxy::js_reflect_get(value, js_nanbox_pointer(key as i64), value) - }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - result + }) } pub(crate) fn set_last_index(owner: &RuntimeHandle<'_>, value: f64) -> Result<(), EngineError> { @@ -239,7 +228,6 @@ pub(crate) fn set_last_index(owner: &RuntimeHandle<'_>, value: f64) -> Result<() super::js_regexp_set_last_index(re, value.get_nanbox_f64()); return Ok(()); } - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let result = api::caught(|| { let key = crate::string::canonical_key(b"lastIndex"); crate::proxy::js_reflect_set( @@ -249,7 +237,6 @@ pub(crate) fn set_last_index(owner: &RuntimeHandle<'_>, value: f64) -> Result<() owner.get_nanbox_f64(), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); if crate::value::js_is_truthy(result?) != 0 { Ok(()) } else { @@ -272,10 +259,7 @@ pub(crate) fn to_number(value: &RuntimeHandle<'_>) -> Result { "Cannot convert a BigInt value to a number", )); } - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); - let result = api::caught(|| crate::builtins::js_number_coerce(primitive.get_nanbox_f64())); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - result + api::caught(|| crate::builtins::js_number_coerce(primitive.get_nanbox_f64())) } fn to_primitive(value: &RuntimeHandle<'_>, string_hint: bool) -> Result { @@ -370,14 +354,16 @@ pub(crate) fn test_string(receiver: f64, input: *const StringHeader) -> Result Result { +pub(crate) fn test_value( + _this: crate::closure::JsThis, + receiver: f64, + argument: f64, +) -> Result { require_object(receiver)?; let scope = RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let argument = scope.root_nanbox_f64(argument); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let input = api::caught(|| crate::value::js_jsvalue_to_string_coerce(argument.get_nanbox_f64())); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); test_string(receiver.get_nanbox_f64(), input?) } diff --git a/crates/perry-runtime/src/regex/perex_match_search.rs b/crates/perry-runtime/src/regex/perex_match_search.rs index f6dc3cebad..133ddda4b2 100644 --- a/crates/perry-runtime/src/regex/perex_match_search.rs +++ b/crates/perry-runtime/src/regex/perex_match_search.rs @@ -354,17 +354,17 @@ pub(crate) fn string( dispatch::call_one(&method, &re, &argument) } -pub(crate) extern "C" fn match_thunk(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { - api::finish(regexp( - Operation::Match, - crate::object::js_implicit_this_get(), - arg, - )) +pub(crate) extern "C" fn match_thunk( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { + api::finish(regexp(Operation::Match, this.as_f64(), arg)) } -pub(crate) extern "C" fn search_thunk(_: *const crate::closure::ClosureHeader, arg: f64) -> f64 { - api::finish(regexp( - Operation::Search, - crate::object::js_implicit_this_get(), - arg, - )) +pub(crate) extern "C" fn search_thunk( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + arg: f64, +) -> f64 { + api::finish(regexp(Operation::Search, this.as_f64(), arg)) } diff --git a/crates/perry-runtime/src/regex/perex_replace.rs b/crates/perry-runtime/src/regex/perex_replace.rs index 2d7f66ebcc..911dfabcd4 100644 --- a/crates/perry-runtime/src/regex/perex_replace.rs +++ b/crates/perry-runtime/src/regex/perex_replace.rs @@ -328,14 +328,11 @@ pub(crate) fn string( pub(crate) extern "C" fn regexp_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, input: f64, replacement: f64, ) -> f64 { - api::finish(regexp( - crate::object::js_implicit_this_get(), - input, - replacement, - )) + api::finish(regexp(this.as_f64(), input, replacement)) } #[no_mangle] diff --git a/crates/perry-runtime/src/regex/perex_replace_storage.rs b/crates/perry-runtime/src/regex/perex_replace_storage.rs index 6b466c0344..c377550db7 100644 --- a/crates/perry-runtime/src/regex/perex_replace_storage.rs +++ b/crates/perry-runtime/src/regex/perex_replace_storage.rs @@ -111,18 +111,14 @@ pub(super) fn call( args: &List<'_>, memory: &MemoryBudget, ) -> Result { - let scope = RuntimeHandleScope::new(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); if crate::proxy::js_proxy_is_proxy(method.get_nanbox_f64()) == 1 { - let result = api::caught(|| { + return api::caught(|| { crate::proxy::js_proxy_apply( method.get_nanbox_f64(), receiver.get_nanbox_f64(), args.value(), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); - return result; } let mut slots = Vec::>::new(); slots @@ -146,14 +142,13 @@ pub(super) fn call( slots.capacity().checked_mul(8).ok_or(StorageError::Limit)?, )?; let result = api::caught(|| unsafe { - crate::object::js_implicit_this_set(receiver.get_nanbox_f64()); - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), slots.as_ptr().cast(), slots.len(), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); drop(reservation); drop(frame); drop(slots); @@ -211,8 +206,6 @@ pub(super) fn call_native( crate::gc::js_shadow_frame_pop(self.0); } } - let scope = RuntimeHandleScope::new(); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let slots = &args.slots; for slot in slots { // SAFETY: nothing else holds a reference to these cells, and the @@ -231,14 +224,13 @@ pub(super) fn call_native( slots.len().checked_mul(8).ok_or(StorageError::Limit)?, )?; let result = api::caught(|| unsafe { - crate::object::js_implicit_this_set(receiver.get_nanbox_f64()); - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), slots.as_ptr().cast(), slots.len(), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); drop(reservation); drop(frame); result diff --git a/crates/perry-runtime/src/regex/perex_split.rs b/crates/perry-runtime/src/regex/perex_split.rs index 42d43d04c9..3289e8ba3c 100644 --- a/crates/perry-runtime/src/regex/perex_split.rs +++ b/crates/perry-runtime/src/regex/perex_split.rs @@ -608,10 +608,11 @@ fn string_via_engine(receiver: f64, separator: f64, limit_value: f64) -> Result< pub(crate) extern "C" fn regexp_thunk( _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, input: f64, limit: f64, ) -> f64 { - api::finish(regexp(crate::object::js_implicit_this_get(), input, limit)) + api::finish(regexp(this.as_f64(), input, limit)) } #[no_mangle] diff --git a/crates/perry-runtime/src/regex/perex_substitution.rs b/crates/perry-runtime/src/regex/perex_substitution.rs index 062a0119fb..3a2b976a72 100644 --- a/crates/perry-runtime/src/regex/perex_substitution.rs +++ b/crates/perry-runtime/src/regex/perex_substitution.rs @@ -96,7 +96,6 @@ impl Substitution<'_, '_> { ) })??; let key = scope.root_string_ptr(key); - let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let groups = self.groups.unwrap(); let value = api::caught(|| { let key = key @@ -107,7 +106,6 @@ impl Substitution<'_, '_> { groups.get_nanbox_f64(), ) }); - crate::object::js_implicit_this_set(previous.get_nanbox_f64()); let value = scope.root_nanbox_f64(value?); if value.get_nanbox_f64().to_bits() != TAG_UNDEFINED { selected = Some(text(&scope, &value)?); diff --git a/crates/perry-runtime/src/regex/replace_fn.rs b/crates/perry-runtime/src/regex/replace_fn.rs index 786e93c0db..d25e50d2c4 100644 --- a/crates/perry-runtime/src/regex/replace_fn.rs +++ b/crates/perry-runtime/src/regex/replace_fn.rs @@ -5,12 +5,12 @@ use super::*; use crate::value::js_nanbox_string; pub(super) unsafe fn call_replace_callback(callback: f64, args: &[f64]) -> String { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(f64::from_bits( - crate::value::TAG_UNDEFINED, - ))); - let ret = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); + let ret = crate::closure::native_call_value_this( + callback, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); // §22.1.3.19 step "Let replacement be ? ToString(? Call(replaceValue, …))": // the callback result is ToString-coerced — `undefined` renders as // "undefined", a number stringifies, an object runs its `toString`, and a diff --git a/crates/perry-runtime/src/regex/site_test.rs b/crates/perry-runtime/src/regex/site_test.rs index af6e75aea8..3e379b4238 100644 --- a/crates/perry-runtime/src/regex/site_test.rs +++ b/crates/perry-runtime/src/regex/site_test.rs @@ -318,12 +318,14 @@ fn call_value_at_site(site_key: usize, callee: f64, this_value: Option) -> 0 }; let this_value = this_value.map(|value| scope.root_nanbox_f64(value)); - let this_guard = this_value - .as_ref() - .map(|value| crate::object::ImplicitThisScope::bind(&scope, value.get_nanbox_f64())); let active = ActiveFactoryGuard::push(site_key, expected_identity); + let this = this_value + .as_ref() + .map_or(crate::closure::plain_call_receiver(), |value| { + crate::closure::JsThis::from_f64(value.get_nanbox_f64()) + }); let result = unsafe { - crate::closure::js_native_call_value(callee.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::native_call_value_this(callee.get_nanbox_f64(), this, std::ptr::null(), 0) }; if !active.handled() { let reason = if lookup(site_key).is_some() { @@ -334,7 +336,6 @@ fn call_value_at_site(site_key: usize, callee: f64, this_value: Option) -> note_declined(reason); } drop(active); - drop(this_guard); result } @@ -474,9 +475,15 @@ fn site_test_dispatch_impl(receiver: f64, method: f64, argument: f64) -> f64 { } let method = scope.root_nanbox_f64(method); - let _this_guard = crate::object::ImplicitThisScope::bind(&scope, receiver.get_nanbox_f64()); let args = [argument.get_nanbox_f64()]; - unsafe { crate::closure::js_native_call_value(method.get_nanbox_f64(), args.as_ptr(), 1) } + unsafe { + crate::closure::native_call_value_this( + method.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + args.as_ptr(), + 1, + ) + } } /// Strong root for every cached header. The visitor rewrites entries in @@ -641,7 +648,10 @@ mod tests { ); } - extern "C" fn exact_factory(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn exact_factory( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { let re = js_regexp_new_factory_site( string("x"), string("g"), @@ -651,7 +661,10 @@ mod tests { crate::value::js_nanbox_pointer(re as i64) } - extern "C" fn replacement_factory(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn replacement_factory( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { let object = crate::object::js_object_alloc(0, 0); crate::object::js_object_set_field_by_name( object, @@ -662,11 +675,14 @@ mod tests { } #[inline(never)] - extern "C" fn nested_factory_wrapper(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn nested_factory_wrapper( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { // Keep this observably distinct from `exact_factory` under release // function merging while modeling a non-literal wrapper with effects. NESTED_WRAPPER_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - exact_factory(std::ptr::null()) + exact_factory(std::ptr::null(), this) } fn closure(function: *const u8) -> f64 { @@ -802,7 +818,11 @@ mod tests { assert_eq!(TEST_DECLINED_CALLEE.with(std::cell::Cell::get), 1); } - extern "C" fn patched_test(_closure: *const crate::closure::ClosureHeader, _arg: f64) -> f64 { + extern "C" fn patched_test( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _arg: f64, + ) -> f64 { f64::from_bits(crate::value::TAG_TRUE) } diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index cd26fbd539..fdef4a877a 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -272,7 +272,7 @@ pub fn is_registered_set(addr: usize) -> bool { /// (`TypeError` on a non-`Set` receiver) before dispatching. Mirrors the /// receiver extraction used by the `Array.prototype.slice` thunk: a /// NaN-boxed pointer, or a bare raw-i64 pointer some module-init call sites -/// stash in `IMPLICIT_THIS`. Primitives (undefined/null/number/string/bool) +/// pass as the receiver. Primitives (undefined/null/number/string/bool) /// carry a non-zero tag in the top 16 bits and resolve to `None`. pub fn set_ptr_from_receiver_bits(bits: u64) -> Option<*mut SetHeader> { let jsv = crate::value::JSValue::from_bits(bits); @@ -2116,8 +2116,6 @@ fn js_set_foreach_impl( // during the callback are visited too. Bounding this by the live count // surfaced holes and stopped before later live values (#9072). let mut i = 0usize; - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let set = set_handle.get_raw_const_ptr::(); if i >= (*set).used as usize { @@ -2139,10 +2137,8 @@ fn js_set_foreach_impl( } let args = [value, value, set_value]; let cb = callback_handle.get_nanbox_f64(); - let this_v = this_handle.get_nanbox_f64(); - crate::object::js_implicit_this_set(this_v); - let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let this_v = crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()); + let _ = crate::closure::native_call_value_this(cb, this_v, args.as_ptr(), args.len()); } } let set = set_handle.get_raw_const_ptr::(); diff --git a/crates/perry-runtime/src/set_tombstone_tests.rs b/crates/perry-runtime/src/set_tombstone_tests.rs index 7f3adfbcd4..c75dfa17cc 100644 --- a/crates/perry-runtime/src/set_tombstone_tests.rs +++ b/crates/perry-runtime/src/set_tombstone_tests.rs @@ -9,6 +9,7 @@ crate::perry_thread_local! { extern "C" fn delete_current_set_value( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, _value_again: f64, collection: f64, @@ -21,6 +22,7 @@ extern "C" fn delete_current_set_value( extern "C" fn delete_earlier_set_value( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, _value_again: f64, collection: f64, @@ -397,6 +399,7 @@ fn clear_truncates_the_squeeze_history() { extern "C" fn delete_earlier_then_live_read( _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, value: f64, _value_again: f64, collection: f64, diff --git a/crates/perry-runtime/src/symbol/accessors.rs b/crates/perry-runtime/src/symbol/accessors.rs index 6fe61d1690..831a538eea 100644 --- a/crates/perry-runtime/src/symbol/accessors.rs +++ b/crates/perry-runtime/src/symbol/accessors.rs @@ -281,9 +281,11 @@ pub(super) unsafe fn invoke_symbol_accessor_setter( } let scope = crate::gc::RuntimeHandleScope::new(); let value_h = scope.root_nanbox_f64(value); - let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - crate::closure::js_closure_call1(closure, value_h.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); + crate::closure::js_closure_call1( + closure, + crate::closure::JsThis::from_f64(receiver), + value_h.get_nanbox_f64(), + ); value_h.get_nanbox_f64() } @@ -295,11 +297,7 @@ pub(super) unsafe fn invoke_symbol_accessor_getter(get_bits: u64, receiver: f64) if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + crate::closure::js_closure_call0(closure, crate::closure::JsThis::from_f64(receiver)) } pub(super) fn scan_symbol_accessor_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { diff --git a/crates/perry-runtime/src/symbol/inherited_accessor_tests.rs b/crates/perry-runtime/src/symbol/inherited_accessor_tests.rs index 94b504233f..72f709d2b8 100644 --- a/crates/perry-runtime/src/symbol/inherited_accessor_tests.rs +++ b/crates/perry-runtime/src/symbol/inherited_accessor_tests.rs @@ -10,13 +10,17 @@ thread_local! { } /// Getter body: answers the `this` it was invoked with. -extern "C" fn this_getter(_closure: *const ClosureHeader) -> f64 { - crate::object::js_implicit_this_get() +extern "C" fn this_getter(_closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + this.as_f64() } /// Setter body: records `(this, value)`. -extern "C" fn recording_setter(_closure: *const ClosureHeader, value: f64) -> f64 { - let this = crate::object::js_implicit_this_get(); +extern "C" fn recording_setter( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + let this = this.as_f64(); SETTER_CALL.with(|c| c.set(Some((this.to_bits(), value.to_bits())))); f64::from_bits(TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 0ca5f2d9a1..7dbb2399b6 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -262,13 +262,14 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { throw_value_not_iterable(val_f64); } let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(val_f64)); + let val_h = this_scope.root_nanbox_f64(val_f64); let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); let rebound_ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(rebound)) as *const crate::closure::ClosureHeader; - let iter = crate::closure::js_closure_call0(rebound_ptr); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let iter = crate::closure::js_closure_call0( + rebound_ptr, + crate::closure::JsThis::from_f64(val_h.get_nanbox_f64()), + ); if !is_object_value(iter) { throw_iterator_result_not_object(); } @@ -443,6 +444,8 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { // and wraps the wrong value if `this` stays the prototype. Rebind // `this` to the original value; a no-op for closures that don't // capture `this`. + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let val_h = this_scope.root_nanbox_f64(val_f64); let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); let call_target = f64::from_bits(rebound); let fn_ptr = crate::value::js_nanbox_get_pointer(call_target) @@ -452,14 +455,13 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { // `[Symbol.iterator]()` factory runs with `this === obj`. The // `clone_closure_rebind_this` above covers a closure that // *captures* `this` (effect's prototype method); a plain - // `function(){ …this… }` factory reads `this` dynamically off - // IMPLICIT_THIS, so set it here too (test262 yield-star-sync-* - // asserts the `[Symbol.iterator]` call's thisValue === obj). - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(val_f64)); - let iter = crate::closure::js_closure_call0(fn_ptr); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + // `function(){ …this… }` factory reads its `this` parameter, + // so pass `obj` there too (test262 yield-star-sync-* asserts + // the `[Symbol.iterator]` call's thisValue === obj). + let iter = crate::closure::js_closure_call0( + fn_ptr, + crate::closure::JsThis::from_f64(val_h.get_nanbox_f64()), + ); // Several Perry host-backed collections expose iterator // helpers as eager arrays for direct `.entries()` parity. When // the same function is reached through `Symbol.iterator`, wrap @@ -650,19 +652,18 @@ pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { // Bind `this` to the object for the call. A class-instance // `[Symbol.toPrimitive]` lives on the prototype and reads `this.field` // (`class Temperature { [Symbol.toPrimitive](h){ return this.celsius } }`), - // reading `this` dynamically off IMPLICIT_THIS; without an explicit receiver + // reading its `this` parameter; without an explicit receiver // `this.celsius` resolved to `undefined`, so `+t` was `NaN` and `` `${t}` `` // was `undefined°C` (test_gap_symbols). The proxy arm above already binds // `this`; mirror it for the closure method. - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - value_handle.get_nanbox_f64(), - )); // Spec says the return value must be a primitive; if it's still an // object pointer, that's a TypeError in JS, but we just return it // as-is and let the caller fall back. - let result = crate::closure::js_closure_call1(closure_ptr, hint_f64); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + crate::closure::js_closure_call1( + closure_ptr, + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), + hint_f64, + ) } #[cfg(test)] diff --git a/crates/perry-runtime/src/text.rs b/crates/perry-runtime/src/text.rs index d6ff922eaf..19cb635e5f 100644 --- a/crates/perry-runtime/src/text.rs +++ b/crates/perry-runtime/src/text.rs @@ -787,8 +787,8 @@ static KEEP_TEXT_DECODER_IGNORE_BOM: extern "C" fn(f64) -> f64 = js_text_decoder // Prototype thunks. // // A native method or accessor installed on a shared prototype is a -// `ClosureHeader` whose ABI carries no receiver; it reads one from -// `js_implicit_this_get()`. `temporal_proto_getter_thunk` is the template. +// `ClosureHeader` body; it reads its receiver from its `this` parameter. +// `temporal_proto_getter_thunk` is the template. // Each thunk brand-checks its receiver against the family class id, so // `TextDecoder.prototype.decode.call({})` throws a TypeError exactly as node // does, instead of silently decoding as utf-8. @@ -804,9 +804,10 @@ fn require_text_brand(value: f64, class_id: u32, message: &[u8]) { #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_decoder_decode_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, input: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_DECODER_CLASS_ID, @@ -819,8 +820,9 @@ pub(crate) extern "C" fn text_decoder_decode_thunk( #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_decoder_encoding_getter( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_DECODER_CLASS_ID, @@ -833,8 +835,9 @@ pub(crate) extern "C" fn text_decoder_encoding_getter( #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_decoder_fatal_getter( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_DECODER_CLASS_ID, @@ -846,8 +849,9 @@ pub(crate) extern "C" fn text_decoder_fatal_getter( #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_decoder_ignore_bom_getter( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_DECODER_CLASS_ID, @@ -859,8 +863,9 @@ pub(crate) extern "C" fn text_decoder_ignore_bom_getter( #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_encoder_encoding_getter( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_ENCODER_CLASS_ID, @@ -873,9 +878,10 @@ pub(crate) extern "C" fn text_encoder_encoding_getter( #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_encoder_encode_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, input: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_ENCODER_CLASS_ID, @@ -887,10 +893,11 @@ pub(crate) extern "C" fn text_encoder_encode_thunk( #[cfg(feature = "global-text")] pub(crate) extern "C" fn text_encoder_encode_into_thunk( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, source: f64, dest: f64, ) -> f64 { - let this = crate::object::js_implicit_this_get(); + let this = this.as_f64(); require_text_brand( this, TEXT_ENCODER_CLASS_ID, diff --git a/crates/perry-runtime/src/thread.rs b/crates/perry-runtime/src/thread.rs index d97129dfaf..ed71594593 100644 --- a/crates/perry-runtime/src/thread.rs +++ b/crates/perry-runtime/src/thread.rs @@ -1013,7 +1013,7 @@ pub(crate) unsafe fn test_deserialize_bigint_limbs(limbs: [u64; BIGINT_LIMBS]) - /// This matches Perry's closure calling convention where the first parameter /// is a pointer to the ClosureHeader (for accessing captures) and the second /// is the f64 argument. -type ClosureCallFn = unsafe extern "C" fn(*const ClosureHeader, f64) -> f64; +type ClosureCallFn = crate::closure::body_call::js_body_fn_ty!(argument); /// Process an array in parallel across multiple OS threads. /// @@ -1216,7 +1216,8 @@ unsafe fn parallel_map_impl(array_val: f64, closure_val: f64) -> i64 { None }; - let call_fn: ClosureCallFn = std::mem::transmute(func_usize); + let call_fn: ClosureCallFn = + crate::closure::body_call::js_body_fn!(func_usize as *const u8; argument); for elem_sv in &chunk { let arg = f64::from_bits(deserialize_nanbox_on_current_thread(elem_sv)); @@ -1224,7 +1225,7 @@ unsafe fn parallel_map_impl(array_val: f64, closure_val: f64) -> i64 { .as_ref() .map(|h| h.get_raw_mut_ptr::() as *const ClosureHeader) .unwrap_or(ptr::null()); - let result = call_fn(local_closure, arg); + let result = call_fn(local_closure, crate::closure::plain_call_receiver(), arg); results.push(serialize_nanbox_for_thread(result.to_bits())); } @@ -1296,7 +1297,8 @@ unsafe fn single_thread_map( let result_arr = crate::array::js_array_alloc(len as u32); let result_handle = scope.root_raw_mut_ptr(result_arr); - let call_fn: ClosureCallFn = std::mem::transmute(func as usize); + let call_fn: ClosureCallFn = + crate::closure::body_call::js_body_fn!(func as *const u8; argument); for i in 0..len { // Sparse-safe element read (see `parallel_map_impl`); re-derived from @@ -1308,7 +1310,7 @@ unsafe fn single_thread_map( let closure = closure_handle .as_ref() .map_or(ptr::null(), |h| h.get_raw_const_ptr::()); - let result = call_fn(closure, arg); + let result = call_fn(closure, crate::closure::plain_call_receiver(), arg); let result_arr = result_handle.get_raw_mut_ptr::(); // GC_STORE_AUDIT(BARRIERED): single-thread map result slot uses the shared array slot-store helper. store_thread_array_slot(result_arr, i, result.to_bits()); @@ -1467,7 +1469,8 @@ unsafe fn parallel_filter_impl(array_val: f64, closure_val: f64) -> i64 { None }; - let call_fn: ClosureCallFn = std::mem::transmute(func_usize); + let call_fn: ClosureCallFn = + crate::closure::body_call::js_body_fn!(func_usize as *const u8; argument); for elem_sv in &chunk { let arg = f64::from_bits(deserialize_nanbox_on_current_thread(elem_sv)); @@ -1475,7 +1478,7 @@ unsafe fn parallel_filter_impl(array_val: f64, closure_val: f64) -> i64 { .as_ref() .map(|h| h.get_raw_mut_ptr::() as *const ClosureHeader) .unwrap_or(ptr::null()); - let result = call_fn(local_closure, arg); + let result = call_fn(local_closure, crate::closure::plain_call_receiver(), arg); let keep = is_truthy_bits(result.to_bits()); if keep { kept.push(serialize_nanbox_for_thread(arg.to_bits())); @@ -1541,7 +1544,8 @@ unsafe fn single_thread_filter( let result_arr = crate::array::js_array_alloc(len as u32); let result_handle = scope.root_raw_mut_ptr(result_arr); - let call_fn: ClosureCallFn = std::mem::transmute(func as usize); + let call_fn: ClosureCallFn = + crate::closure::body_call::js_body_fn!(func as *const u8; argument); let mut count = 0u32; for i in 0..len { @@ -1554,7 +1558,7 @@ unsafe fn single_thread_filter( let closure = closure_handle .as_ref() .map_or(ptr::null(), |h| h.get_raw_const_ptr::()); - let result = call_fn(closure, arg); + let result = call_fn(closure, crate::closure::plain_call_receiver(), arg); let keep = is_truthy_bits(result.to_bits()); if keep { let result_arr = result_handle.get_raw_mut_ptr::(); @@ -1577,7 +1581,7 @@ static ACTIVE_THREAD_JOBS: AtomicUsize = AtomicUsize::new(0); /// The compiled closure function signature for zero-argument closures. /// Takes only the closure header pointer, returns f64 result. -type ClosureCall0Fn = unsafe extern "C" fn(*const ClosureHeader) -> f64; +type ClosureCall0Fn = crate::closure::body_call::js_body_fn_ty!(); /// FFI entry point for `spawn(closure)`. /// @@ -1691,10 +1695,11 @@ unsafe fn spawn_impl(closure_val: f64) -> *mut crate::promise::Promise { // Call the function — catch panics to avoid aborting across FFI boundary let call_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let call_fn: ClosureCall0Fn = unsafe { std::mem::transmute(func_usize) }; + let call_fn: ClosureCall0Fn = + unsafe { crate::closure::body_call::js_body_fn!(func_usize as *const u8;) }; let local_closure = closure_handle.get_raw_mut_ptr::() as *const ClosureHeader; - unsafe { call_fn(local_closure) } + call_fn(local_closure, crate::closure::plain_call_receiver()) })); match call_result { diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 6f364cd3f3..e8b60f205c 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -465,14 +465,17 @@ fn call_timer_callback( let previous = crate::async_context::enter_context(context); let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&scope, &previous); + let this_handle = scope.root_nanbox_f64(timer_handle_value(id)); let a = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let cb = callback_handle.get_raw_const_ptr::(); - let prev_this = - scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); with_timer_uncaught_trap(|| unsafe { - crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), a.len() as i64); + crate::closure::js_closure_call_array( + cb as i64, + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + a.as_ptr(), + a.len() as i64, + ); }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); crate::async_context::restore_context(previous); } @@ -510,15 +513,19 @@ fn call_timer_callback_entry( handle } }; - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); + let this_handle = scope.root_nanbox_f64(receiver); with_timer_uncaught_trap(|| { let a = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let cb = callback_handle.get_raw_const_ptr::(); unsafe { - crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), a.len() as i64); + crate::closure::js_closure_call_array( + cb as i64, + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + a.as_ptr(), + a.len() as i64, + ); } }); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } fn timer_delay_text(delay_ms: f64) -> String { diff --git a/crates/perry-runtime/src/timer/handle_object.rs b/crates/perry-runtime/src/timer/handle_object.rs index 41ffb79740..990a7637a0 100644 --- a/crates/perry-runtime/src/timer/handle_object.rs +++ b/crates/perry-runtime/src/timer/handle_object.rs @@ -142,30 +142,39 @@ fn throw_timer_type_error(message: &[u8]) -> ! { /// So a foreign receiver is answered, not refused — the opposite of the text /// family, whose WebIDL accessors throw. Each thunk below returns node's answer /// for `None` and never touches timer state in that case. -fn timer_receiver() -> (f64, Option) { - let this = crate::object::js_implicit_this_get(); +fn timer_receiver(this: crate::closure::JsThis) -> (f64, Option) { + let this = this.as_f64(); let id = timer_handle_id(this); (this, id) } -extern "C" fn timer_proto_ref_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let (this, id) = timer_receiver(); +extern "C" fn timer_proto_ref_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let (this, id) = timer_receiver(_this); if let Some(id) = id { js_timer_ref(id); } this } -extern "C" fn timer_proto_unref_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let (this, id) = timer_receiver(); +extern "C" fn timer_proto_unref_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let (this, id) = timer_receiver(_this); if let Some(id) = id { js_timer_unref(id); } this } -extern "C" fn timer_proto_has_ref_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let (_, id) = timer_receiver(); +extern "C" fn timer_proto_has_ref_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let (_, id) = timer_receiver(this); match id { Some(id) if js_timer_has_ref(id) != 0 => { f64::from_bits(crate::value::JSValue::bool(true).bits()) @@ -176,8 +185,11 @@ extern "C" fn timer_proto_has_ref_thunk(_c: *const crate::closure::ClosureHeader } } -extern "C" fn timer_proto_refresh_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let (this, id) = timer_receiver(); +extern "C" fn timer_proto_refresh_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let (this, id) = timer_receiver(_this); if let Some(id) = id { js_timer_refresh(id); } @@ -293,8 +305,11 @@ fn clear_every_kind(id: i64) { clearImmediate(id); } -extern "C" fn timer_proto_close_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let (this, id) = timer_receiver(); +extern "C" fn timer_proto_close_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { + let (this, id) = timer_receiver(_this); if let Some(id) = id { clear_every_kind(id); } @@ -302,8 +317,11 @@ extern "C" fn timer_proto_close_thunk(_c: *const crate::closure::ClosureHeader) } /// `t[Symbol.dispose]()` — `using t = setTimeout(...)` clears the timer (#1213). -extern "C" fn timer_proto_dispose_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - let (_, id) = timer_receiver(); +extern "C" fn timer_proto_dispose_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let (_, id) = timer_receiver(this); if let Some(id) = id { clear_every_kind(id); } @@ -315,9 +333,10 @@ extern "C" fn timer_proto_dispose_thunk(_c: *const crate::closure::ClosureHeader /// conversion, so `+setImmediate(...)` must stay `NaN` (#10542). extern "C" fn timer_proto_to_primitive_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, _hint: f64, ) -> f64 { - let (_, id) = timer_receiver(); + let (_, id) = timer_receiver(this); match id { Some(id) => id as f64, None => f64::from_bits(crate::value::TAG_UNDEFINED), @@ -328,7 +347,11 @@ extern "C" fn timer_proto_to_primitive_thunk( /// is not usable directly. This stands in for it so `t.constructor.name` is /// answered by an ordinary prototype property instead of the fabricated /// `{ name }` object the handle path had to synthesize per read. -extern "C" fn timer_ctor_thunk(_c: *const crate::closure::ClosureHeader, _a: f64) -> f64 { +extern "C" fn timer_ctor_thunk( + _c: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + _a: f64, +) -> f64 { throw_timer_type_error(b"Timeout is not a constructor") } diff --git a/crates/perry-runtime/src/timer/tests_inline.rs b/crates/perry-runtime/src/timer/tests_inline.rs index d858cefea5..a631d49dbf 100644 --- a/crates/perry-runtime/src/timer/tests_inline.rs +++ b/crates/perry-runtime/src/timer/tests_inline.rs @@ -147,7 +147,10 @@ mod mock_dispatch_own_pin_tests { /// the shared registry when the churn starts, so it is the very first one /// evicted once the churn passes the cap — and this callback observes its /// own eviction while it is still running. - extern "C" fn churn_then_check_self(_closure: *const crate::closure::ClosureHeader) -> f64 { + extern "C" fn churn_then_check_self( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + ) -> f64 { let id = SELF_ID.load(Ordering::SeqCst); for _ in 0..(ref_states::TIMER_REF_STATES_CAP + 2_000) { // #340/#341: the producer returns the handle object; `clearTimeout` diff --git a/crates/perry-runtime/src/tls.rs b/crates/perry-runtime/src/tls.rs index 0afc743a77..44a739eeae 100644 --- a/crates/perry-runtime/src/tls.rs +++ b/crates/perry-runtime/src/tls.rs @@ -588,6 +588,7 @@ pub extern "C" fn js_tls_client_check_identity(handle: i64, certificate: f64) -> let host = string_value(metadata.servername.as_deref().unwrap_or_default()); crate::closure::js_closure_call2( metadata.check_server_identity as *const crate::ClosureHeader, + crate::closure::plain_call_receiver(), host, certificate, ) @@ -1096,7 +1097,11 @@ pub extern "C" fn js_tls_prepare_connect() { let callback = (create_context.to_bits() & crate::value::POINTER_MASK) as *const crate::ClosureHeader; if !callback.is_null() { - crate::closure::js_closure_call1(callback, ptr_value(options)); + crate::closure::js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + ptr_value(options), + ); } } diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index b9442a22d7..bca6a7f9e3 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -170,17 +170,11 @@ pub(crate) struct HotTls { // measurable part. Only small `Copy` values with a `const` initial state // belong here; anything needing `Drop` stays a slot. // ------------------------------------------------------------------ - /// `object::this_binding` — the implicit `this` of the current - /// dynamically-dispatched method call, NaN-boxed (`TAG_UNDEFINED` when - /// none). FIRST inline value on purpose: generated code on Apple - /// aarch64 reads and writes it at the fixed byte offset - /// [`HOT_TLS_IMPLICIT_THIS_OFFSET`] (see `hot_tls_layout_is_what_codegen_assumes`). - pub(crate) implicit_this: Cell, /// `agent_ptrs::PERRY_AGENT_PTRS` — this thread's per-agent pointer block. /// Generated code on Apple aarch64 reads it at the fixed byte offset /// [`HOT_TLS_AGENT_PTRS_OFFSET`] (`perry-codegen/src/expr/agent_ptr.rs`), - /// so it sits right after `implicit_this`, where only fixed-size fields - /// precede it: an array sized by a tunable constant (the prototype rows, + /// so it is the FIRST inline value, where only fixed-size fields precede + /// it: an array sized by a tunable constant (the prototype rows, /// the box caches, the generic slots) must never come before a field /// whose offset generated code bakes in. pub(crate) agent_ptrs: Cell<*mut u8>, @@ -222,14 +216,9 @@ pub(crate) struct HotTls { /// not emit the inline path there (ILP32 codegen is refused until #11378). #[cfg(target_pointer_width = "64")] pub const HOT_TLS_INLINE_STATE_OFFSET: usize = 8; -#[cfg(target_pointer_width = "64")] -pub const HOT_TLS_IMPLICIT_THIS_OFFSET: usize = 128; #[cfg(target_pointer_width = "32")] pub const HOT_TLS_INLINE_STATE_OFFSET: usize = 4; -#[cfg(target_pointer_width = "32")] -pub const HOT_TLS_IMPLICIT_THIS_OFFSET: usize = 64; const _: () = assert!(std::mem::offset_of!(HotTls, inline_state) == HOT_TLS_INLINE_STATE_OFFSET); -const _: () = assert!(std::mem::offset_of!(HotTls, implicit_this) == HOT_TLS_IMPLICIT_THIS_OFFSET); /// `HotTls::agent_ptrs` (`perry-abi`), read by generated code on Apple aarch64. pub use crate::codegen_abi::HOT_TLS_AGENT_PTRS_OFFSET; #[cfg(target_pointer_width = "64")] @@ -278,7 +267,6 @@ impl HotTls { shape_install_memo: std::ptr::null_mut(), learned_inline_fields: std::ptr::null_mut(), temp_roots: std::ptr::null_mut(), - implicit_this: Cell::new(crate::value::TAG_UNDEFINED), dirty_old_pages: [const { Cell::new(usize::MAX) }; 16], last_external_dirty_page: Cell::new(usize::MAX), last_external_dirty_header: Cell::new(usize::MAX), diff --git a/crates/perry-runtime/src/tty.rs b/crates/perry-runtime/src/tty.rs index be057a9968..95ce31c1ea 100644 --- a/crates/perry-runtime/src/tty.rs +++ b/crates/perry-runtime/src/tty.rs @@ -221,12 +221,12 @@ fn heap_object_ptr(value: f64) -> Option<*mut ObjectHeader> { } } -fn this_value() -> f64 { - crate::object::js_implicit_this_get() +fn this_value(this: crate::closure::JsThis) -> f64 { + this.as_f64() } -fn this_object() -> Option<*mut ObjectHeader> { - heap_object_ptr(this_value()) +fn this_object(this: crate::closure::JsThis) -> Option<*mut ObjectHeader> { + heap_object_ptr(this_value(this)) } fn fd_from_object(obj: *mut ObjectHeader) -> Option { @@ -241,12 +241,14 @@ fn fd_from_object(obj: *mut ObjectHeader) -> Option { None } -fn current_fd(default_fd: i32) -> i32 { - this_object().and_then(fd_from_object).unwrap_or(default_fd) +fn current_fd(default_fd: i32, this: crate::closure::JsThis) -> i32 { + this_object(this) + .and_then(fd_from_object) + .unwrap_or(default_fd) } -fn this_has_fd() -> bool { - this_object().and_then(fd_from_object).is_some() +fn this_has_fd(this: crate::closure::JsThis) -> bool { + this_object(this).and_then(fd_from_object).is_some() } fn value_to_string(value: f64) -> Option { @@ -620,6 +622,7 @@ fn validate_color_count(count: f64) -> f64 { #[no_mangle] pub extern "C" fn js_tty_write_stream_get_color_depth( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, env: f64, ) -> f64 { let jsval = JSValue::from_bits(env.to_bits()); @@ -635,6 +638,7 @@ pub extern "C" fn js_tty_write_stream_get_color_depth( #[no_mangle] pub extern "C" fn js_tty_write_stream_has_colors( _closure: *const ClosureHeader, + _this: crate::closure::JsThis, count: f64, env: f64, ) -> f64 { @@ -908,15 +912,16 @@ pub(crate) fn is_tty_stream_instance(value: f64, class_name: &str) -> bool { #[no_mangle] pub extern "C" fn js_tty_read_stream_set_raw_mode( _closure: *const ClosureHeader, + this: crate::closure::JsThis, mode: f64, ) -> f64 { let enabled = crate::value::js_is_truthy(mode) != 0; - let fd = current_fd(0); + let fd = current_fd(0, this); let _ = set_fd_raw_mode(fd, enabled); - if let Some(obj) = this_object() { + if let Some(obj) = this_object(this) { crate::object::js_object_set_field_by_name(obj, named_key(b"isRaw"), js_bool(enabled)); } - this_value() + this_value(this) } fn write_all_fd(fd: i32, bytes: &[u8]) -> bool { @@ -962,13 +967,14 @@ fn numeric_arg(value: f64, default: i32) -> i32 { default } -fn write_control_sequence(sequence: String) -> f64 { - js_bool(write_all_fd(current_fd(1), sequence.as_bytes())) +fn write_control_sequence(sequence: String, this: crate::closure::JsThis) -> f64 { + js_bool(write_all_fd(current_fd(1, this), sequence.as_bytes())) } #[no_mangle] pub extern "C" fn js_tty_write_stream_cursor_to( _closure: *const ClosureHeader, + this: crate::closure::JsThis, x: f64, y: f64, _callback: f64, @@ -981,12 +987,13 @@ pub extern "C" fn js_tty_write_stream_cursor_to( let y = numeric_arg(y, 0).max(0); format!("\x1b[{};{}H", y + 1, x + 1) }; - write_control_sequence(sequence) + write_control_sequence(sequence, this) } #[no_mangle] pub extern "C" fn js_tty_write_stream_move_cursor( _closure: *const ClosureHeader, + this: crate::closure::JsThis, dx: f64, dy: f64, _callback: f64, @@ -1004,12 +1011,13 @@ pub extern "C" fn js_tty_write_stream_move_cursor( } else if dy > 0 { sequence.push_str(&format!("\x1b[{}B", dy)); } - write_control_sequence(sequence) + write_control_sequence(sequence, this) } #[no_mangle] pub extern "C" fn js_tty_write_stream_clear_line( _closure: *const ClosureHeader, + this: crate::closure::JsThis, dir: f64, _callback: f64, ) -> f64 { @@ -1018,15 +1026,16 @@ pub extern "C" fn js_tty_write_stream_clear_line( 1 => 0, _ => 2, }; - write_control_sequence(format!("\x1b[{}K", mode)) + write_control_sequence(format!("\x1b[{}K", mode), this) } #[no_mangle] pub extern "C" fn js_tty_write_stream_clear_screen_down( _closure: *const ClosureHeader, + this: crate::closure::JsThis, _callback: f64, ) -> f64 { - write_control_sequence("\x1b[0J".to_string()) + write_control_sequence("\x1b[0J".to_string(), this) } fn winsize_value(fd: i32) -> Option<(i32, i32)> { @@ -1046,8 +1055,11 @@ pub(crate) fn tty_write_stream_dimension(property: &str) -> Option { } #[no_mangle] -pub extern "C" fn js_tty_write_stream_get_window_size(_closure: *const ClosureHeader) -> f64 { - let fd = current_fd(1); +pub extern "C" fn js_tty_write_stream_get_window_size( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let fd = current_fd(1, this); let arr = crate::array::js_array_alloc(2); let (cols, rows) = winsize_value(fd).unwrap_or((0, 0)); let col_value = if cols > 0 { @@ -1066,8 +1078,11 @@ pub extern "C" fn js_tty_write_stream_get_window_size(_closure: *const ClosureHe } #[no_mangle] -pub extern "C" fn js_tty_write_stream_refresh_size(_closure: *const ClosureHeader) -> f64 { - let _ = winsize_value(current_fd(1)); +pub extern "C" fn js_tty_write_stream_refresh_size( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let _ = winsize_value(current_fd(1, this)); TAG_UNDEFINED_F64 } @@ -1083,42 +1098,45 @@ fn register_resize_callback(callback: i64) { #[no_mangle] pub extern "C" fn js_tty_write_stream_on( _closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, callback: f64, ) -> f64 { - if event_name_from_value(event).as_deref() == Some("resize") && this_has_fd() { + if event_name_from_value(event).as_deref() == Some("resize") && this_has_fd(this) { let callback = callback_ptr_from_value(callback); if callback != 0 { register_resize_callback(callback); } } - this_value() + this_value(this) } #[no_mangle] pub extern "C" fn js_tty_write_stream_remove_listener( _closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, _callback: f64, ) -> f64 { - if event_name_from_value(event).as_deref() == Some("resize") && this_has_fd() { + if event_name_from_value(event).as_deref() == Some("resize") && this_has_fd(this) { RESIZE_CALLBACK.with(|cb| *cb.borrow_mut() = None); } - this_value() + this_value(this) } #[no_mangle] pub extern "C" fn js_tty_write_stream_remove_all_listeners( _closure: *const ClosureHeader, + this: crate::closure::JsThis, event: f64, ) -> f64 { let event_js = JSValue::from_bits(event.to_bits()); if (event_js.is_undefined() || event_name_from_value(event).as_deref() == Some("resize")) - && this_has_fd() + && this_has_fd(this) { RESIZE_CALLBACK.with(|cb| *cb.borrow_mut() = None); } - this_value() + this_value(this) } pub fn throw_invalid_fd(fd: f64) -> ! { @@ -1246,7 +1264,7 @@ pub extern "C" fn js_tty_resize_drain() -> i32 { let cb = RESIZE_CALLBACK.with(|c| *c.borrow()); if let Some(cb_i64) = cb { let closure = cb_i64 as *const ClosureHeader; - js_closure_call0(closure); + js_closure_call0(closure, crate::closure::plain_call_receiver()); return 1; } 0 diff --git a/crates/perry-runtime/src/tui/handle_object.rs b/crates/perry-runtime/src/tui/handle_object.rs index 4855f0ab49..35cd7bbbf9 100644 --- a/crates/perry-runtime/src/tui/handle_object.rs +++ b/crates/perry-runtime/src/tui/handle_object.rs @@ -209,8 +209,7 @@ pub(crate) fn tui_widget_id_from_bits(bits: u64) -> i64 { } /// `(kind, id)` for a NaN-boxed JS value. The prototype thunks resolve their -/// receiver through this, since `js_implicit_this_get` hands back a boxed -/// value. +/// receiver (their `this` parameter, a boxed value) through this. fn tui_handle_parts_value(value: f64) -> Option<(TuiKind, i64)> { let bits = value.to_bits(); if (bits & crate::value::TAG_MASK) != crate::value::POINTER_TAG { @@ -227,8 +226,8 @@ fn tui_handle_parts_value(value: f64) -> Option<(TuiKind, i64)> { /// timer methods, and the one that cannot turn a working program into a /// throwing one. It is never read as an id of this kind: that is what the /// class-id brand prevents. -fn receiver_id(kind: TuiKind) -> Option { - let this = crate::object::js_implicit_this_get(); +fn receiver_id(this: crate::closure::JsThis, kind: TuiKind) -> Option { + let this = this.as_f64(); match tui_handle_parts_value(this) { Some((k, id)) if k == kind => Some(id), _ => None, @@ -237,43 +236,63 @@ fn receiver_id(kind: TuiKind) -> Option { const UNDEFINED: u64 = crate::value::TAG_UNDEFINED; -extern "C" fn state_proto_get_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - match receiver_id(TuiKind::State) { +extern "C" fn state_proto_get_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + match receiver_id(this, TuiKind::State) { Some(id) => super::state::state_get_by_id(id), None => f64::from_bits(UNDEFINED), } } -extern "C" fn state_proto_set_thunk(_c: *const crate::closure::ClosureHeader, value: f64) -> f64 { - if let Some(id) = receiver_id(TuiKind::State) { +extern "C" fn state_proto_set_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + if let Some(id) = receiver_id(this, TuiKind::State) { super::state::state_set_by_id(id, value); } f64::from_bits(UNDEFINED) } -extern "C" fn ref_proto_get_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - match receiver_id(TuiKind::RefBox) { +extern "C" fn ref_proto_get_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + match receiver_id(this, TuiKind::RefBox) { Some(id) => super::hooks::ref_get_by_id(id), None => f64::from_bits(UNDEFINED), } } -extern "C" fn ref_proto_set_thunk(_c: *const crate::closure::ClosureHeader, value: f64) -> f64 { - if let Some(id) = receiver_id(TuiKind::RefBox) { +extern "C" fn ref_proto_set_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + value: f64, +) -> f64 { + if let Some(id) = receiver_id(this, TuiKind::RefBox) { super::hooks::ref_set_by_id(id, value); } f64::from_bits(UNDEFINED) } -extern "C" fn app_proto_exit_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - if receiver_id(TuiKind::App).is_some() { +extern "C" fn app_proto_exit_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + if receiver_id(this, TuiKind::App).is_some() { super::input::EXIT_FLAG.store(true, Ordering::Release); } f64::from_bits(UNDEFINED) } -extern "C" fn app_proto_wait_until_exit_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - if receiver_id(TuiKind::App).is_some() { +extern "C" fn app_proto_wait_until_exit_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + if receiver_id(this, TuiKind::App).is_some() { super::hooks::wait_until_exit_blocking(); } f64::from_bits(UNDEFINED) @@ -281,45 +300,62 @@ extern "C" fn app_proto_wait_until_exit_thunk(_c: *const crate::closure::Closure extern "C" fn stdout_proto_write_thunk( _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - if receiver_id(TuiKind::Stdout).is_some() { + if receiver_id(this, TuiKind::Stdout).is_some() { let s = crate::value::js_jsvalue_to_string_coerce(value); super::hooks::stdout_write_string_ptr(s); } f64::from_bits(UNDEFINED) } -extern "C" fn stdout_proto_columns_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - match receiver_id(TuiKind::Stdout) { +extern "C" fn stdout_proto_columns_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + match receiver_id(this, TuiKind::Stdout) { Some(_) => super::hooks::js_perry_tui_stdout_columns(0), None => f64::from_bits(UNDEFINED), } } -extern "C" fn stdout_proto_rows_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - match receiver_id(TuiKind::Stdout) { +extern "C" fn stdout_proto_rows_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + match receiver_id(this, TuiKind::Stdout) { Some(_) => super::hooks::js_perry_tui_stdout_rows(0), None => f64::from_bits(UNDEFINED), } } -extern "C" fn focus_proto_next_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - if receiver_id(TuiKind::FocusManager).is_some() { +extern "C" fn focus_proto_next_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + if receiver_id(this, TuiKind::FocusManager).is_some() { super::hooks::js_perry_tui_focus_next(); } f64::from_bits(UNDEFINED) } -extern "C" fn focus_proto_previous_thunk(_c: *const crate::closure::ClosureHeader) -> f64 { - if receiver_id(TuiKind::FocusManager).is_some() { +extern "C" fn focus_proto_previous_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + if receiver_id(this, TuiKind::FocusManager).is_some() { super::hooks::js_perry_tui_focus_previous(); } f64::from_bits(UNDEFINED) } -extern "C" fn focus_proto_focus_thunk(_c: *const crate::closure::ClosureHeader, id: f64) -> f64 { - if receiver_id(TuiKind::FocusManager).is_some() { +extern "C" fn focus_proto_focus_thunk( + _c: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + id: f64, +) -> f64 { + if receiver_id(this, TuiKind::FocusManager).is_some() { super::hooks::js_perry_tui_focus(id); } f64::from_bits(UNDEFINED) diff --git a/crates/perry-runtime/src/tui/hooks.rs b/crates/perry-runtime/src/tui/hooks.rs index 472f360b89..3b9d6987fb 100644 --- a/crates/perry-runtime/src/tui/hooks.rs +++ b/crates/perry-runtime/src/tui/hooks.rs @@ -404,6 +404,7 @@ pub extern "C" fn js_perry_tui_use_state_set(slot_idx: f64, value: f64) -> f64 { #[no_mangle] pub extern "C" fn perry_tui_state_setter_trampoline( closure: *const ClosureHeader, + _this: crate::closure::JsThis, value: f64, ) -> f64 { if closure.is_null() { @@ -542,7 +543,10 @@ pub extern "C" fn js_perry_tui_use_effect(fn_closure: i64, deps_array: i64) -> f // but possible) doesn't deadlock. drop(s); if fn_closure != 0 { - js_closure_call0(fn_closure as *const ClosureHeader); + js_closure_call0( + fn_closure as *const ClosureHeader, + crate::closure::plain_call_receiver(), + ); } } f64::from_bits(TAG_UNDEFINED) @@ -614,7 +618,10 @@ pub extern "C" fn js_perry_tui_use_memo(fn_closure: i64, deps_array: i64) -> f64 if should_compute { drop(s); let value = if fn_closure != 0 { - js_closure_call0(fn_closure as *const ClosureHeader) + js_closure_call0( + fn_closure as *const ClosureHeader, + crate::closure::plain_call_receiver(), + ) } else { f64::from_bits(TAG_UNDEFINED) }; diff --git a/crates/perry-runtime/src/tui/input.rs b/crates/perry-runtime/src/tui/input.rs index 509294a494..9c970eac1a 100644 --- a/crates/perry-runtime/src/tui/input.rs +++ b/crates/perry-runtime/src/tui/input.rs @@ -282,7 +282,7 @@ pub fn drain_input() -> i32 { let s_ptr = js_string_from_bytes(chunk.as_ptr(), chunk.len() as u32); let arg = f64::from_bits(JSValue::string_ptr(s_ptr).bits()); let closure = handler as *const ClosureHeader; - js_closure_call1(closure, arg); + js_closure_call1(closure, crate::closure::plain_call_receiver(), arg); } count += 1; i = chunk_end; diff --git a/crates/perry-runtime/src/tui/run.rs b/crates/perry-runtime/src/tui/run.rs index 1a5efcc52d..6b6cf9a173 100644 --- a/crates/perry-runtime/src/tui/run.rs +++ b/crates/perry-runtime/src/tui/run.rs @@ -62,7 +62,7 @@ pub extern "C" fn js_perry_tui_run(component: i64) -> f64 { super::hooks::reset_hook_index(); // Call the component to get a fresh widget tree. - let widget_v = js_closure_call0(component_closure); + let widget_v = js_closure_call0(component_closure, crate::closure::plain_call_receiver()); // #340/#341: the component returns a widget handle OBJECT, so resolve // it through the brand instead of masking the tag off whatever came // back. A component that returns a number or a string now paints diff --git a/crates/perry-runtime/src/typed_feedback/tests.rs b/crates/perry-runtime/src/typed_feedback/tests.rs index fdec89b20e..ec959d1790 100644 --- a/crates/perry-runtime/src/typed_feedback/tests.rs +++ b/crates/perry-runtime/src/typed_feedback/tests.rs @@ -23,7 +23,11 @@ extern "C" fn test_direct_method(_this: f64, value: f64) -> f64 { value } -extern "C" fn test_direct_closure(_closure: *const crate::closure::ClosureHeader, arg: f64) -> f64 { +extern "C" fn test_direct_closure( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + arg: f64, +) -> f64 { TEST_DIRECT_CLOSURE_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); arg } @@ -2997,7 +3001,14 @@ fn function_source_array_literal_keeps_the_array_index_fast_path_armed() { .map(|s| s.to_string()) .collect(); let f = crate::dyn_eval::dyn_function_from_strings(&source); - let result = unsafe { crate::closure::js_native_call_value(f, [].as_ptr(), 0) }; + let result = unsafe { + crate::closure::js_native_call_value( + f, + crate::closure::plain_call_receiver(), + [].as_ptr(), + 0, + ) + }; let result = crate::value::JSValue::from_bits(result.to_bits()); assert_eq!( if result.is_int32() { diff --git a/crates/perry-runtime/src/typedarray/construct.rs b/crates/perry-runtime/src/typedarray/construct.rs index 7492afcfa6..68dc086bd8 100644 --- a/crates/perry-runtime/src/typedarray/construct.rs +++ b/crates/perry-runtime/src/typedarray/construct.rs @@ -224,8 +224,12 @@ unsafe fn typed_array_plain_object_values(val: f64) -> Vec { rooted.get_nanbox_f64(), ); let bound_rooted = scope.root_nanbox_u64(bound); - let iter = - crate::closure::js_native_call_value(bound_rooted.get_nanbox_f64(), ptr::null(), 0); + let iter = crate::closure::js_native_call_value( + bound_rooted.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + ptr::null(), + 0, + ); let iter_rooted = scope.root_nanbox_f64(iter); let mut raw = Vec::new(); while let Some(v) = diff --git a/crates/perry-runtime/src/typedarray/iterate.rs b/crates/perry-runtime/src/typedarray/iterate.rs index af0ab949d6..6157f1d002 100644 --- a/crates/perry-runtime/src/typedarray/iterate.rs +++ b/crates/perry-runtime/src/typedarray/iterate.rs @@ -43,7 +43,13 @@ pub extern "C" fn js_typed_array_map( }; for i in 0..len { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); crate::typedarray_props::species_result_store(result_addr, i, r); } species::result_as_ptr(result) @@ -75,7 +81,13 @@ pub extern "C" fn js_typed_array_filter( let mut kept: Vec = Vec::new(); for i in 0..len { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) != 0 { kept.push(v); } @@ -111,7 +123,13 @@ pub extern "C" fn js_typed_array_every( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in 0..len { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) == 0 { return f64::from_bits(crate::value::TAG_FALSE); } @@ -139,7 +157,13 @@ pub extern "C" fn js_typed_array_some( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in 0..len { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) != 0 { return f64::from_bits(crate::value::TAG_TRUE); } @@ -165,7 +189,13 @@ pub extern "C" fn js_typed_array_for_each( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in 0..len { let v = load_at(ta, i); - let _ = cb_site.call(callback, v, i as f64, recv); + let _ = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); } } } @@ -191,7 +221,13 @@ pub extern "C" fn js_typed_array_find( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in 0..len { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) != 0 { return v; } @@ -219,7 +255,13 @@ pub extern "C" fn js_typed_array_find_index( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in 0..len { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) != 0 { return i as f64; } @@ -240,10 +282,8 @@ pub extern "C" fn js_typed_array_reduce( has_initial: i32, initial: f64, ) -> f64 { - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback is a plain call, so it runs with `this` undefined, + // not the receiver of whatever method dispatch encloses this call. let ta = clean_ta_ptr(ta); if ta.is_null() { if has_initial != 0 { @@ -284,7 +324,14 @@ pub extern "C" fn js_typed_array_reduce( let acc = scope.root_nanbox_f64(accumulator); for i in start_idx..len { let v = load_at(ta, i); - let next = cb_site.call(callback, acc.get_nanbox_f64(), v, i as f64, recv); + let next = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + acc.get_nanbox_f64(), + v, + i as f64, + recv, + ); acc.set_nanbox_f64(next); } acc.get_nanbox_f64() @@ -300,10 +347,8 @@ pub extern "C" fn js_typed_array_reduce_right( has_initial: i32, initial: f64, ) -> f64 { - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback is a plain call, so it runs with `this` undefined, + // not the receiver of whatever method dispatch encloses this call. let ta = clean_ta_ptr(ta); if ta.is_null() { if has_initial != 0 { @@ -345,7 +390,14 @@ pub extern "C" fn js_typed_array_reduce_right( if start_idx > 0 { for i in (0..start_idx).rev() { let v = load_at(ta, i); - let next = cb_site.call(callback, acc.get_nanbox_f64(), v, i as f64, recv); + let next = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + acc.get_nanbox_f64(), + v, + i as f64, + recv, + ); acc.set_nanbox_f64(next); } } diff --git a/crates/perry-runtime/src/typedarray/species.rs b/crates/perry-runtime/src/typedarray/species.rs index f52723635a..2e687fa42d 100644 --- a/crates/perry-runtime/src/typedarray/species.rs +++ b/crates/perry-runtime/src/typedarray/species.rs @@ -112,6 +112,7 @@ pub(crate) unsafe fn prototype_constructor_patch(kind: u8, owner: usize) -> Opti let bound = crate::closure::clone_closure_rebind_this(desc.get, owner_value); return Some(crate::closure::js_native_call_value( f64::from_bits(bound), + crate::closure::plain_call_receiver(), std::ptr::null(), 0, )); diff --git a/crates/perry-runtime/src/typedarray/transform.rs b/crates/perry-runtime/src/typedarray/transform.rs index 6c6f168dfe..4a2757034e 100644 --- a/crates/perry-runtime/src/typedarray/transform.rs +++ b/crates/perry-runtime/src/typedarray/transform.rs @@ -134,6 +134,7 @@ unsafe fn bigint_lane_compare( let b_handle = scope.root_nanbox_f64(box_lane(b_bits)); let r = site.call( comparator, + crate::closure::plain_call_receiver(), a_handle.get_nanbox_f64(), b_handle.get_nanbox_f64(), ); @@ -187,10 +188,8 @@ pub extern "C" fn js_typed_array_sort_with_comparator( if comparator.is_null() { return js_typed_array_sort_default(ta); } - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback is a plain call, so it runs with `this` undefined, + // not the receiver of whatever method dispatch encloses this call. let ta_clean = clean_ta_ptr(ta as *const TypedArrayHeader) as *mut TypedArrayHeader; if ta_clean.is_null() { return ta_clean; @@ -233,7 +232,9 @@ pub extern "C" fn js_typed_array_sort_with_comparator( // `with_const_ptr`: an argument-position read, which `across_*` // cannot express. The handle is re-read per comparison, so a // relocation during comparison `i` is reflected in `i + 1`. - let r = cmp_handle.with_const_ptr::(|cmp| cmp_site.call(cmp, *a, *b)); + let r = cmp_handle.with_const_ptr::(|cmp| { + cmp_site.call(cmp, crate::closure::plain_call_receiver(), *a, *b) + }); if r < 0.0 { std::cmp::Ordering::Less } else if r > 0.0 { @@ -281,10 +282,8 @@ pub extern "C" fn js_typed_array_to_sorted_with_comparator( if comparator.is_null() { return js_typed_array_to_sorted_default(ta); } - // #11419: the callback runs with `this` undefined, not the receiver of - // whatever method dispatch encloses this call. - let this_scope = crate::gc::RuntimeHandleScope::new(); - let _this = crate::object::ImplicitThisScope::bind_undefined(&this_scope); + // #11419: the callback is a plain call, so it runs with `this` undefined, + // not the receiver of whatever method dispatch encloses this call. let ta = clean_ta_ptr(ta); if ta.is_null() { return typed_array_alloc(KIND_FLOAT64, 0); @@ -322,7 +321,9 @@ pub extern "C" fn js_typed_array_to_sorted_with_comparator( // `with_const_ptr`: an argument-position read, which `across_*` // cannot express. The handle is re-read per comparison, so a // relocation during comparison `i` is reflected in `i + 1`. - let r = cmp_handle.with_const_ptr::(|cmp| cmp_site.call(cmp, *a, *b)); + let r = cmp_handle.with_const_ptr::(|cmp| { + cmp_site.call(cmp, crate::closure::plain_call_receiver(), *a, *b) + }); if r < 0.0 { std::cmp::Ordering::Less } else if r > 0.0 { @@ -397,7 +398,13 @@ pub extern "C" fn js_typed_array_find_last( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in (0..len).rev() { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) != 0 { return v; } @@ -425,7 +432,13 @@ pub extern "C" fn js_typed_array_find_last_index( let cb_site = crate::closure::DirectCall3::resolve(callback); for i in (0..len).rev() { let v = load_at(ta, i); - let r = cb_site.call(callback, v, i as f64, recv); + let r = cb_site.call( + callback, + crate::closure::plain_call_receiver(), + v, + i as f64, + recv, + ); if crate::value::js_is_truthy(r) != 0 { return i as f64; } diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 45d9638740..3cc8d16d71 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -236,11 +236,7 @@ fn invoke_typed_array_accessor_getter(get_bits: u64, receiver: f64) -> f64 { if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - result + crate::closure::js_closure_call0(closure, crate::closure::JsThis::from_f64(receiver)) } fn invoke_typed_array_accessor_setter(set_bits: u64, receiver: f64, value: f64) { @@ -248,10 +244,7 @@ fn invoke_typed_array_accessor_setter(set_bits: u64, receiver: f64, value: f64) if closure.is_null() { return; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - crate::closure::js_closure_call1(closure, value); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); + crate::closure::js_closure_call1(closure, crate::closure::JsThis::from_f64(receiver), value); } fn barrier_typed_array_own_props(owner: usize, props: &mut [TypedArrayOwnProp]) { @@ -1139,7 +1132,12 @@ pub(crate) unsafe fn typed_array_own_to_primitive_number(owner: usize, value: f6 continue; } let bound = crate::closure::clone_closure_rebind_this(mbits, value); - let r = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); + let r = crate::closure::js_native_call_value( + f64::from_bits(bound), + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); let rb = r.to_bits(); let is_object = (rb >> 48) == 0x7FFD && crate::symbol::js_is_symbol(r) == 0 diff --git a/crates/perry-runtime/src/url/abort.rs b/crates/perry-runtime/src/url/abort.rs index e08bebf187..25c4465148 100644 --- a/crates/perry-runtime/src/url/abort.rs +++ b/crates/perry-runtime/src/url/abort.rs @@ -68,6 +68,7 @@ fn alloc_abort_signal() -> *mut ObjectHeader { extern "C" fn abort_signal_add_event_listener_method( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event_type: f64, listener: f64, ) -> f64 { @@ -78,6 +79,7 @@ extern "C" fn abort_signal_add_event_listener_method( extern "C" fn abort_signal_remove_event_listener_method( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event_type: f64, listener: f64, ) -> f64 { @@ -100,6 +102,7 @@ fn abort_signal_listener_method_value(signal: *mut ObjectHeader, add: bool) -> f extern "C" fn abort_controller_abort_method( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, reason: f64, ) -> f64 { let controller_bits = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; @@ -199,7 +202,7 @@ fn fire_abort_listeners(signal: *mut ObjectHeader) { continue; }; if !cb_ptr.is_null() { - crate::closure::js_closure_call0(cb_ptr); + crate::closure::js_closure_call0(cb_ptr, crate::closure::plain_call_receiver()); } } } @@ -273,7 +276,10 @@ fn notify_fetch_abort(signal_ptr: i64) { } } -extern "C" fn abort_error_constructor_thunk(_closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn abort_error_constructor_thunk( + _closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { crate::error::js_throw_illegal_constructor_type_error() } @@ -420,6 +426,7 @@ pub extern "C" fn js_abort_signal_remove_listener( /// "abort" once, so Node's `{ once: true }` is behaviorally implied. extern "C" fn abort_signal_add_event_listener_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event_type: f64, listener: f64, _options: f64, @@ -434,6 +441,7 @@ extern "C" fn abort_signal_add_event_listener_thunk( /// Bound-method thunk: `signal.removeEventListener(type, listener[, options])`. extern "C" fn abort_signal_remove_event_listener_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, event_type: f64, listener: f64, _options: f64, @@ -448,6 +456,7 @@ extern "C" fn abort_signal_remove_event_listener_thunk( /// Bound-method thunk: `signal.throwIfAborted()`. extern "C" fn abort_signal_throw_if_aborted_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, ) -> f64 { let signal_bits = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; let signal = @@ -545,7 +554,10 @@ fn timeout_dom_exception_value() -> f64 { /// signal with a `TimeoutError` and firing its `abort` listeners (which is how /// a pending `fetch` bound by the signal learns to reject — see /// `js_fetch_with_options`). -extern "C" fn abort_signal_timeout_fire(closure: *const crate::closure::ClosureHeader) -> f64 { +extern "C" fn abort_signal_timeout_fire( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let signal_bits = crate::closure::js_closure_get_capture_ptr(closure, 0) as u64; let signal = crate::value::js_nanbox_get_pointer(f64::from_bits(signal_bits)) as *mut ObjectHeader; @@ -622,6 +634,7 @@ pub extern "C" fn js_abort_signal_throw_if_aborted(signal: *mut ObjectHeader) -> extern "C" fn abort_any_propagate_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, _arg: f64, ) -> f64 { // capture 0 = combined signal pointer (NaN-boxed), capture 1 = source signal. diff --git a/crates/perry-runtime/src/url/prototype.rs b/crates/perry-runtime/src/url/prototype.rs index 6ee8b26701..f633578ad9 100644 --- a/crates/perry-runtime/src/url/prototype.rs +++ b/crates/perry-runtime/src/url/prototype.rs @@ -9,8 +9,8 @@ use super::parse::*; use super::url_class::*; use super::*; -fn require_url_receiver(name: &str) -> *mut ObjectHeader { - let this = crate::object::js_implicit_this_get(); +fn require_url_receiver(name: &str, this: crate::closure::JsThis) -> *mut ObjectHeader { + let this = this.as_f64(); if let Some(obj) = object_from_f64(this) { if is_url_object_shape(obj) { return obj; @@ -24,8 +24,11 @@ fn require_url_receiver(name: &str) -> *mut ObjectHeader { macro_rules! url_getter { ($fn_name:ident, $name:literal, $slot:expr) => { - extern "C" fn $fn_name(_closure: *const crate::closure::ClosureHeader) -> f64 { - let obj = require_url_receiver($name); + extern "C" fn $fn_name( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + ) -> f64 { + let obj = require_url_receiver($name, this); crate::object::js_object_get_field_f64(obj, $slot) } }; @@ -33,8 +36,12 @@ macro_rules! url_getter { macro_rules! url_setter { ($fn_name:ident, $name:literal, $setter:path) => { - extern "C" fn $fn_name(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { - let obj = require_url_receiver($name); + extern "C" fn $fn_name( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, + value: f64, + ) -> f64 { + let obj = require_url_receiver($name, this); $setter(obj, value); f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index 12fb116992..a34aa602d9 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -929,8 +929,9 @@ pub extern "C" fn js_url_search_params_for_each( let entries = get_url_search_params_entries(params); let this_value = crate::value::js_nanbox_pointer(params as i64); let this_scope = crate::gc::RuntimeHandleScope::new(); - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + // A callback can collect, so `thisArg` is rooted ONCE here and re-read at + // each call. + let this_arg_handle = this_scope.root_nanbox_f64(this_arg); for (key, value) in entries { let args = [ create_string_f64(&value), @@ -938,9 +939,12 @@ pub extern "C" fn js_url_search_params_for_each( this_value, ]; unsafe { - crate::object::js_implicit_this_set(this_arg); - let _ = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let _ = crate::closure::native_call_value_this( + callback, + crate::closure::JsThis::from_f64(this_arg_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ); } } } @@ -1038,6 +1042,7 @@ fn usp_thunk_receiver(closure: *const crate::closure::ClosureHeader) -> *mut Obj extern "C" fn usp_append_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, name: f64, value: f64, ) -> f64 { @@ -1047,6 +1052,7 @@ extern "C" fn usp_append_thunk( extern "C" fn usp_set_thunk( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, name: f64, value: f64, ) -> f64 { @@ -1054,7 +1060,11 @@ extern "C" fn usp_set_thunk( f64::from_bits(crate::value::TAG_UNDEFINED) } -extern "C" fn usp_get_thunk(closure: *const crate::closure::ClosureHeader, name: f64) -> f64 { +extern "C" fn usp_get_thunk( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + name: f64, +) -> f64 { // js_url_search_params_get returns a raw `*mut StringHeader` for the // static-lowering path; rebuild the boxed value here instead. let wanted = coerce_search_param_arg(name); @@ -1065,7 +1075,11 @@ extern "C" fn usp_get_thunk(closure: *const crate::closure::ClosureHeader, name: } } -extern "C" fn usp_has_thunk(closure: *const crate::closure::ClosureHeader, name: f64) -> f64 { +extern "C" fn usp_has_thunk( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + name: f64, +) -> f64 { if js_url_search_params_has(usp_thunk_receiver(closure), name) != 0.0 { f64::from_bits(crate::value::TAG_TRUE) } else { @@ -1073,7 +1087,11 @@ extern "C" fn usp_has_thunk(closure: *const crate::closure::ClosureHeader, name: } } -extern "C" fn usp_delete_thunk(closure: *const crate::closure::ClosureHeader, name: f64) -> f64 { +extern "C" fn usp_delete_thunk( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + name: f64, +) -> f64 { js_url_search_params_delete(usp_thunk_receiver(closure), name); f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/util_abort.rs b/crates/perry-runtime/src/util_abort.rs index 7dd1d558d6..e24a8aa4d6 100644 --- a/crates/perry-runtime/src/util_abort.rs +++ b/crates/perry-runtime/src/util_abort.rs @@ -77,7 +77,10 @@ fn promise_from_capture(closure: *const ClosureHeader, index: u32) -> *mut crate js_nanbox_get_pointer(f64::from_bits(bits)) as *mut crate::promise::Promise } -extern "C" fn aborted_resolve_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn aborted_resolve_listener( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { let promise = promise_from_capture(closure, 0); let signal = f64::from_bits(js_closure_get_capture_ptr(closure, 1) as u64); if !promise.is_null() { diff --git a/crates/perry-runtime/src/util_debuglog.rs b/crates/perry-runtime/src/util_debuglog.rs index e47cc83093..fc1f082bb1 100644 --- a/crates/perry-runtime/src/util_debuglog.rs +++ b/crates/perry-runtime/src/util_debuglog.rs @@ -165,7 +165,11 @@ fn call_enabled_callback_once(scope: &crate::gc::RuntimeHandleScope, closure: *m let logger_value = nanbox_closure(closure); let logger_handle = scope.root_nanbox_f64(logger_value); - js_closure_call1(callback_ptr, logger_handle.get_nanbox_f64()); + js_closure_call1( + callback_ptr, + crate::closure::plain_call_receiver(), + logger_handle.get_nanbox_f64(), + ); } fn write_stderr_line(line: &str) { @@ -186,7 +190,11 @@ fn write_stderr_line(line: &str) { ); let write_f64 = f64::from_bits(write_value.bits()); if let Some(write_ptr) = closure_ptr(write_f64) { - js_closure_call1(write_ptr, line_handle.get_nanbox_f64()); + js_closure_call1( + write_ptr, + crate::closure::plain_call_receiver(), + line_handle.get_nanbox_f64(), + ); return; } } @@ -194,7 +202,11 @@ fn write_stderr_line(line: &str) { eprint!("{line}"); } -extern "C" fn logger_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 { +extern "C" fn logger_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest_value: f64, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index 37e80998d9..e0d1dc6745 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -319,7 +319,11 @@ pub extern "C" fn js_util_callbackify(fn_value: f64) -> f64 { /// Receives the rest array of forwarded args (NaN-boxed pointer to an /// `ArrayHeader`). Builds an args list `[…rest, inner_cb]`, runs the /// original under a setjmp trap so a sync `throw` rejects the promise. -extern "C" fn outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 { +extern "C" fn outer_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest_value: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let fn_value = if closure.is_null() { @@ -328,6 +332,9 @@ extern "C" fn outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 js_closure_get_capture_f64(closure, 0) }; let fn_handle = scope.root_nanbox_f64(fn_value); + // Node calls `original` with the wrapper's own receiver + // (`ReflectApply(original, this, args)`). + let this_handle = scope.root_nanbox_f64(this.as_f64()); let promise_ptr = js_promise_new(); if promise_ptr.is_null() { @@ -400,7 +407,12 @@ extern "C" fn outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 }; let n = js_array_length(arr) as usize; unsafe { - crate::closure::js_native_call_value(fn_handle.get_nanbox_f64(), data, n); + crate::closure::js_native_call_value( + fn_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + data, + n, + ); } } else { // #7341: `js_get_exception` can allocate, so pair it with the @@ -426,7 +438,12 @@ extern "C" fn outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 /// /// Standard Node convention: a null/undefined `err` means the call /// succeeded with `value`. Anything else is a rejection. -extern "C" fn inner_callback_thunk(closure: *const ClosureHeader, err: f64, value: f64) -> f64 { +extern "C" fn inner_callback_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + err: f64, + value: f64, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } @@ -446,7 +463,11 @@ extern "C" fn inner_callback_thunk(closure: *const ClosureHeader, err: f64, valu /// [`outer_thunk`] but installs a 3-arg inner callback so the resolved /// value is Node's `{ publicKey, privateKey }` object (via /// `customPromisifyArgs`) rather than just the public key. -extern "C" fn gkp_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 { +extern "C" fn gkp_outer_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest_value: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let fn_value = if closure.is_null() { @@ -455,6 +476,9 @@ extern "C" fn gkp_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> js_closure_get_capture_f64(closure, 0) }; let fn_handle = scope.root_nanbox_f64(fn_value); + // Node calls `original` with the wrapper's own receiver + // (`ReflectApply(original, this, args)`). + let this_handle = scope.root_nanbox_f64(this.as_f64()); let promise_ptr = js_promise_new(); if promise_ptr.is_null() { @@ -520,7 +544,12 @@ extern "C" fn gkp_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> }; let n = js_array_length(arr) as usize; unsafe { - crate::closure::js_native_call_value(fn_handle.get_nanbox_f64(), data, n); + crate::closure::js_native_call_value( + fn_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + data, + n, + ); } } else { // #7341: `js_get_exception` can allocate, so pair it with the @@ -548,6 +577,7 @@ extern "C" fn gkp_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> /// `customPromisifyArgs`); otherwise rejects with `err`. extern "C" fn gkp_inner_callback_thunk( closure: *const ClosureHeader, + _this: crate::closure::JsThis, err: f64, public_key: f64, private_key: f64, @@ -602,7 +632,11 @@ extern "C" fn gkp_inner_callback_thunk( /// `util.deprecate()` wrapper body. Receives all user arguments bundled in /// `rest_value`, emits one warning per wrapper, then forwards the call. -extern "C" fn deprecate_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 { +extern "C" fn deprecate_outer_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest_value: f64, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } @@ -612,6 +646,9 @@ extern "C" fn deprecate_outer_thunk(closure: *const ClosureHeader, rest_value: f let msg = js_closure_get_capture_f64(closure, 1); let code = js_closure_get_capture_f64(closure, 2); let fn_handle = scope.root_nanbox_f64(fn_value); + // Node calls `original` with the wrapper's own receiver + // (`ReflectApply(original, this, args)`). + let this_handle = scope.root_nanbox_f64(this.as_f64()); let msg_handle = scope.root_nanbox_f64(msg); let code_handle = scope.root_nanbox_f64(code); let rest_handle = scope.root_nanbox_f64(rest_value); @@ -647,14 +684,25 @@ extern "C" fn deprecate_outer_thunk(closure: *const ClosureHeader, rest_value: f } }; - unsafe { crate::closure::js_native_call_value(fn_handle.get_nanbox_f64(), rest_data, rest_len) } + unsafe { + crate::closure::js_native_call_value( + fn_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + rest_data, + rest_len, + ) + } } /// Outer callbackify body: `(closure, rest_array_value) -> undefined`. /// /// The last incoming argument is the Node-style callback. Every preceding /// argument is forwarded to the original promise-returning function. -extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 { +extern "C" fn callbackify_outer_thunk( + closure: *const ClosureHeader, + this: crate::closure::JsThis, + rest_value: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let fn_value = if closure.is_null() { @@ -663,6 +711,9 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: js_closure_get_capture_f64(closure, 0) }; let fn_handle = scope.root_nanbox_f64(fn_value); + // Node calls `original` with the wrapper's own receiver + // (`ReflectApply(original, this, args)`). + let this_handle = scope.root_nanbox_f64(this.as_f64()); let rest_bits = rest_value.to_bits(); let rest_arr_ptr = if (rest_bits & TAG_MASK) == POINTER_TAG { @@ -702,7 +753,12 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: let returned = unsafe { let arr = original_args_handle.get_raw_const_ptr::(); let data = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; - crate::closure::js_native_call_value(fn_handle.get_nanbox_f64(), data, original_arg_len) + crate::closure::js_native_call_value( + fn_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + data, + original_arg_len, + ) }; // #9539: `returned` is a heap value that outlives two closure allocations, // an interned-key lookup and `js_assimilate_thenable` (which runs the @@ -772,17 +828,14 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: let on_rejected = nanbox_pointer(rejected_handle.get_raw_const_ptr::() as *const u8); let args = [on_fulfilled, on_rejected]; - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( - returned_handle.get_nanbox_f64(), - )); unsafe { - crate::closure::js_native_call_value( + crate::closure::native_call_value_this( then_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(returned_handle.get_nanbox_f64()), args.as_ptr(), args.len(), ); } - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return TAG_UNDEFINED_F64; } @@ -841,19 +894,32 @@ fn throw_callbackify_not_thenable(value: f64) -> ! { throw_plain_type_error("The \"original\" function did not return a Promise"); } -extern "C" fn callbackify_fulfilled_thunk(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn callbackify_fulfilled_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + value: f64, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } let callback_value = js_closure_get_capture_f64(closure, 0); let args = [TAG_NULL_F64, value]; unsafe { - crate::closure::js_native_call_value(callback_value, args.as_ptr(), args.len()); + crate::closure::js_native_call_value( + callback_value, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } TAG_UNDEFINED_F64 } -extern "C" fn callbackify_rejected_thunk(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn callbackify_rejected_thunk( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } @@ -870,7 +936,12 @@ fn call_callback_rejected(callback_value: f64, reason: f64) { }; let args = [err]; unsafe { - crate::closure::js_native_call_value(callback_value, args.as_ptr(), args.len()); + crate::closure::js_native_call_value( + callback_value, + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } } diff --git a/crates/perry-runtime/src/v8.rs b/crates/perry-runtime/src/v8.rs index b80d9b7ee0..4f8b6ae8dd 100644 --- a/crates/perry-runtime/src/v8.rs +++ b/crates/perry-runtime/src/v8.rs @@ -168,7 +168,10 @@ fn register_stop_trampoline_once() { }); } -extern "C" fn promise_hook_stop_trampoline(closure: *const ClosureHeader) -> f64 { +extern "C" fn promise_hook_stop_trampoline( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { if closure.is_null() { return TAG_UNDEFINED_F64; } @@ -305,7 +308,12 @@ pub(crate) fn promise_hook_init(promise: *mut Promise, parent: *mut Promise) { } else { promise_value(parent_ptr) }; - js_closure_call2(callback_handle.get_raw_const_ptr(), promise_arg, parent_arg); + js_closure_call2( + callback_handle.get_raw_const_ptr(), + crate::closure::plain_call_receiver(), + promise_arg, + parent_arg, + ); }); } @@ -322,6 +330,7 @@ pub(crate) fn promise_hook_before(promise: *mut Promise) { let callback_handle = scope.root_raw_const_ptr(callbacks.before); js_closure_call1( callback_handle.get_raw_const_ptr(), + crate::closure::plain_call_receiver(), promise_value(promise_handle.get_raw_mut_ptr::()), ); }); @@ -340,6 +349,7 @@ pub(crate) fn promise_hook_after(promise: *mut Promise) { let callback_handle = scope.root_raw_const_ptr(callbacks.after); js_closure_call1( callback_handle.get_raw_const_ptr(), + crate::closure::plain_call_receiver(), promise_value(promise_handle.get_raw_mut_ptr::()), ); }); @@ -358,6 +368,7 @@ pub(crate) fn promise_hook_settled(promise: *mut Promise) { let callback_handle = scope.root_raw_const_ptr(callbacks.settled); js_closure_call1( callback_handle.get_raw_const_ptr(), + crate::closure::plain_call_receiver(), promise_value(promise_handle.get_raw_mut_ptr::()), ); }); diff --git a/crates/perry-runtime/src/value/dynamic_array.rs b/crates/perry-runtime/src/value/dynamic_array.rs index 68009c7351..1392300def 100644 --- a/crates/perry-runtime/src/value/dynamic_array.rs +++ b/crates/perry-runtime/src/value/dynamic_array.rs @@ -113,7 +113,11 @@ pub extern "C" fn js_dynamic_array_find( let length = js_dynamic_array_length(arr_value); for i in 0..length { let element = js_dynamic_array_get(arr_value, i); - let result = crate::closure::js_closure_call1(callback, element); + let result = crate::closure::js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + element, + ); // Proper truthy check: handles NaN-boxed booleans if js_is_truthy(result) != 0 { return element; @@ -148,7 +152,11 @@ pub extern "C" fn js_dynamic_array_findIndex( let length = js_dynamic_array_length(arr_value); for i in 0..length { let element = js_dynamic_array_get(arr_value, i); - let result = crate::closure::js_closure_call1(callback, element); + let result = crate::closure::js_closure_call1( + callback, + crate::closure::plain_call_receiver(), + element, + ); // Proper truthy check: handles NaN-boxed booleans if js_is_truthy(result) != 0 { return i as f64; diff --git a/crates/perry-runtime/src/value/to_string_array.rs b/crates/perry-runtime/src/value/to_string_array.rs index 3e40ba6e98..2031c95e14 100644 --- a/crates/perry-runtime/src/value/to_string_array.rs +++ b/crates/perry-runtime/src/value/to_string_array.rs @@ -83,11 +83,12 @@ pub(crate) unsafe fn array_prototype_to_string_override(value: f64) -> ArrayToSt let receiver = value_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); let bound_handle = scope.root_nanbox_u64(bound); - let prev_this = crate::object::js_implicit_this_set(receiver); - let prev_this_handle = scope.root_nanbox_f64(prev_this); - let ret = - crate::closure::js_native_call_value(bound_handle.get_nanbox_f64(), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this_handle.get_nanbox_f64()); + let ret = crate::closure::native_call_value_this( + bound_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), + std::ptr::null(), + 0, + ); if is_primitive_value(ret) { ArrayToStringOutcome::Primitive(ret) } else { @@ -146,16 +147,13 @@ pub(crate) fn call_array_prototype_to_string_method( let rebound = crate::closure::rebind_explicit_this(method_handle.get_nanbox_f64(), receiver); let rebound_handle = scope.root_nanbox_f64(rebound); - let previous = crate::object::js_implicit_this_set(receiver); - let previous_handle = scope.root_nanbox_f64(previous); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); - let result = crate::closure::js_native_call_value( + crate::closure::native_call_value_this( rebound_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver_handle.get_nanbox_f64()), args.as_ptr(), args.len(), - ); - crate::object::js_implicit_this_set(previous_handle.get_nanbox_f64()); - result + ) } } @@ -186,11 +184,12 @@ pub(crate) fn array_prototype_to_string(value: f64) -> f64 { let receiver = receiver_handle.get_nanbox_f64(); let rebound = crate::closure::rebind_explicit_this(join_handle.get_nanbox_f64(), receiver); let rebound_handle = scope.root_nanbox_f64(rebound); - let previous = crate::object::js_implicit_this_set(receiver); - let previous_handle = scope.root_nanbox_f64(previous); - let result = unsafe { - crate::closure::js_native_call_value(rebound_handle.get_nanbox_f64(), std::ptr::null(), 0) - }; - crate::object::js_implicit_this_set(previous_handle.get_nanbox_f64()); - result + unsafe { + crate::closure::native_call_value_this( + rebound_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(receiver_handle.get_nanbox_f64()), + std::ptr::null(), + 0, + ) + } } diff --git a/crates/perry-runtime/src/value/to_string_class_ref.rs b/crates/perry-runtime/src/value/to_string_class_ref.rs index 005bc8336f..de92fb21dc 100644 --- a/crates/perry-runtime/src/value/to_string_class_ref.rs +++ b/crates/perry-runtime/src/value/to_string_class_ref.rs @@ -63,11 +63,12 @@ pub(crate) unsafe fn custom_to_primitive(value: f64, hint: &[u8]) -> CustomToPri if !crate::closure::is_closure_ptr(method_ptr) { return CustomToPrimitiveOutcome::TypeError; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let result = crate::closure::js_native_call_value(method, &hint, 1); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); - result + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(receiver), + &hint, + 1, + ) }; if is_primitive_value(result) { diff --git a/crates/perry-runtime/src/value/to_string_primitive.rs b/crates/perry-runtime/src/value/to_string_primitive.rs index 88ba593ef0..88d96b5702 100644 --- a/crates/perry-runtime/src/value/to_string_primitive.rs +++ b/crates/perry-runtime/src/value/to_string_primitive.rs @@ -175,8 +175,6 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } else { [b"valueOf", b"toString"] }; - // #9445: the displaced receiver is rooted ONCE here, not once per callback. - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for name in order { let recv = value_handle.get_nanbox_f64(); // #10510: a runtime-spelled literal — reuse the interned key instead @@ -199,14 +197,12 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( return time; } let method_handle = scope.root_nanbox_f64(method); - let recv = value_handle.get_nanbox_f64(); - crate::object::js_implicit_this_set(recv); - let result = crate::closure::js_native_call_value( + let result = crate::closure::native_call_value_this( method_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), std::ptr::null(), 0, ); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if is_primitive_value(result) { return result; } @@ -458,11 +454,15 @@ pub(crate) unsafe fn call_own_method(method: f64, receiver: f64) -> Option // Rebind `this` to the receiver — an assigned closure may have baked a // different value into its reserved `this` slot (an inherited or bound // method), exactly as the method-dispatch tower does (#1982). - let bound = crate::closure::clone_closure_rebind_this(bits, receiver); let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let receiver = this_scope.root_nanbox_f64(receiver); + let bound = crate::closure::clone_closure_rebind_this(bits, receiver.get_nanbox_f64()); + let ret = crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(receiver.get_nanbox_f64()), + std::ptr::null(), + 0, + ); Some(ret) } @@ -633,9 +633,12 @@ unsafe fn call_method_for_primitive( // receiver, so rebinding is a correct no-op. Mirrors #1982. let recv = value_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, recv); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); - let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let ret = crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), + std::ptr::null(), + 0, + ); let ret_jsv = JSValue::from_bits(ret.to_bits()); let is_primitive = ret_jsv.is_any_string() || ret_jsv.is_number() @@ -688,9 +691,12 @@ unsafe fn call_function_method( let method_handle = scope.root_nanbox_f64(method); let bound = crate::closure::clone_closure_rebind_this(method_handle.get_nanbox_u64(), recv); - let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); - let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + let ret = crate::closure::native_call_value_this( + f64::from_bits(bound), + crate::closure::JsThis::from_f64(value_handle.get_nanbox_f64()), + std::ptr::null(), + 0, + ); FunctionMethodOutcome::Value(ret) } diff --git a/crates/perry-runtime/src/wasi.rs b/crates/perry-runtime/src/wasi.rs index d73d85fef5..8827ffc69a 100644 --- a/crates/perry-runtime/src/wasi.rs +++ b/crates/perry-runtime/src/wasi.rs @@ -740,9 +740,12 @@ pub(crate) unsafe fn js_wasi_init_subclass(this_box: f64, options: f64) { } #[no_mangle] -pub extern "C" fn js_wasi_get_import_object(_closure: *const ClosureHeader) -> f64 { +pub extern "C" fn js_wasi_get_import_object( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); + let this = scope.root_nanbox_f64(this.as_f64()); if heap_object_ptr(this.get_nanbox_f64()).is_none() || !is_wasi_instance(this.get_nanbox_f64()) { let wrapper = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); @@ -777,10 +780,14 @@ pub extern "C" fn js_wasi_get_import_object(_closure: *const ClosureHeader) -> f } #[no_mangle] -pub extern "C" fn js_wasi_start(_closure: *const ClosureHeader, instance: f64) -> f64 { +pub extern "C" fn js_wasi_start( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + instance: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let instance = scope.root_nanbox_f64(instance); - let wasi = scope.root_nanbox_f64(wasi_receiver_or_throw()); + let wasi = scope.root_nanbox_f64(wasi_receiver_or_throw(this)); ensure_wasi_not_started(wasi.get_nanbox_f64()); let import = scope.root_nanbox_f64(wasi_import_or_throw(wasi.get_nanbox_f64())); let memory = scope.root_nanbox_f64(instance_export(instance.get_nanbox_f64(), b"memory")); @@ -800,7 +807,14 @@ pub extern "C" fn js_wasi_start(_closure: *const ClosureHeader, instance: f64) - invalid_undefined_property("instance.exports._initialize", initialize) } WASI_EXIT_CODE.with(|slot| slot.set(None)); - unsafe { crate::closure::js_native_call_value(start.get_nanbox_f64(), std::ptr::null(), 0) }; + unsafe { + crate::closure::js_native_call_value( + start.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) + }; if let Some(code) = WASI_EXIT_CODE.with(|slot| slot.take()) { return code as f64; } @@ -817,10 +831,14 @@ pub extern "C" fn js_wasi_start(_closure: *const ClosureHeader, instance: f64) - } #[no_mangle] -pub extern "C" fn js_wasi_initialize(_closure: *const ClosureHeader, instance: f64) -> f64 { +pub extern "C" fn js_wasi_initialize( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + instance: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let instance = scope.root_nanbox_f64(instance); - let wasi = scope.root_nanbox_f64(wasi_receiver_or_throw()); + let wasi = scope.root_nanbox_f64(wasi_receiver_or_throw(this)); ensure_wasi_not_started(wasi.get_nanbox_f64()); let import = scope.root_nanbox_f64(wasi_import_or_throw(wasi.get_nanbox_f64())); let memory = scope.root_nanbox_f64(instance_export(instance.get_nanbox_f64(), b"memory")); @@ -843,7 +861,12 @@ pub extern "C" fn js_wasi_initialize(_closure: *const ClosureHeader, instance: f } if !is_undefined(initialize.get_nanbox_f64()) { unsafe { - crate::closure::js_native_call_value(initialize.get_nanbox_f64(), std::ptr::null(), 0) + crate::closure::js_native_call_value( + initialize.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ) }; } undefined() @@ -852,6 +875,7 @@ pub extern "C" fn js_wasi_initialize(_closure: *const ClosureHeader, instance: f #[no_mangle] pub extern "C" fn js_wasi_finalize_bindings( _closure: *const ClosureHeader, + this: crate::closure::JsThis, instance: f64, rest: f64, ) -> f64 { @@ -862,7 +886,10 @@ pub extern "C" fn js_wasi_finalize_bindings( // accepting the optional second argument. let options = rest_argument(rest, 0); let override_memory = scope.root_nanbox_f64(finalize_memory_option(options)); - let wasi = scope.root_nanbox_f64(wasi_receiver_or_throw_with_code("ERR_INVALID_ARG_TYPE")); + let wasi = scope.root_nanbox_f64(wasi_receiver_or_throw_with_code( + "ERR_INVALID_ARG_TYPE", + this, + )); ensure_wasi_not_started(wasi.get_nanbox_f64()); let import = scope.root_nanbox_f64(wasi_import_or_throw(wasi.get_nanbox_f64())); let exported_memory = @@ -879,12 +906,12 @@ pub extern "C" fn js_wasi_finalize_bindings( undefined() } -fn wasi_receiver_or_throw() -> f64 { - wasi_receiver_or_throw_with_code("") +fn wasi_receiver_or_throw(this: crate::closure::JsThis) -> f64 { + wasi_receiver_or_throw_with_code("", this) } -fn wasi_receiver_or_throw_with_code(code: &'static str) -> f64 { - let this = crate::object::js_implicit_this_get(); +fn wasi_receiver_or_throw_with_code(code: &'static str, this: crate::closure::JsThis) -> f64 { + let this = this.as_f64(); if heap_object_ptr(this).is_none() { type_error_with_code("Value of \"this\" must be of type WASI", code); } @@ -1002,8 +1029,8 @@ fn finalize_memory_option(options: f64) -> f64 { object_field(options, b"memory") } -fn import_from_closure(closure: *const ClosureHeader) -> f64 { - let this = crate::object::js_implicit_this_get(); +fn import_from_closure(this: crate::closure::JsThis, closure: *const ClosureHeader) -> f64 { + let this = this.as_f64(); if let Some(obj) = heap_object_ptr(this) { if is_wasi_import_object(obj) { return this; @@ -1140,13 +1167,14 @@ fn import_function_name(closure: *const ClosureHeader) -> &'static str { #[no_mangle] pub extern "C" fn js_wasi_import_stub( closure: *const ClosureHeader, + this: crate::closure::JsThis, arg0: f64, arg1: f64, arg2: f64, arg3: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let import = scope.root_nanbox_f64(import_from_closure(closure)); + let import = scope.root_nanbox_f64(import_from_closure(this, closure)); match import_function_name(closure).trim_start_matches("bound ") { "args_sizes_get" => { if !is_undefined(arg2) || argument(arg0).is_none() || argument(arg1).is_none() { diff --git a/crates/perry-runtime/src/weakref/test_support.rs b/crates/perry-runtime/src/weakref/test_support.rs index b61e18a886..a4f0022044 100644 --- a/crates/perry-runtime/src/weakref/test_support.rs +++ b/crates/perry-runtime/src/weakref/test_support.rs @@ -82,7 +82,11 @@ fn empty_checkpoint_delivers_recorded_finalization_job() { use super::*; use std::sync::atomic::{AtomicU64, Ordering}; static DELIVERED: AtomicU64 = AtomicU64::new(0); - extern "C" fn cleanup(_: *const crate::closure::ClosureHeader, held: f64) -> f64 { + extern "C" fn cleanup( + _: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + held: f64, + ) -> f64 { DELIVERED.store(held.to_bits(), Ordering::Relaxed); 0.0 } diff --git a/crates/perry-runtime/src/web_storage.rs b/crates/perry-runtime/src/web_storage.rs index cd8bd861b9..3d0eaaf541 100644 --- a/crates/perry-runtime/src/web_storage.rs +++ b/crates/perry-runtime/src/web_storage.rs @@ -26,59 +26,92 @@ thread_local! { static SESSION_STORE: RefCell> = const { RefCell::new(BTreeMap::new()) }; } -pub extern "C" fn storage_constructor_illegal(_closure: *const ClosureHeader) -> f64 { +pub extern "C" fn storage_constructor_illegal( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { throw_error("Illegal constructor") } -extern "C" fn storage_global_setter(_closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn storage_global_setter( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, + _value: f64, +) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn storage_local_global_getter(_closure: *const ClosureHeader) -> f64 { +extern "C" fn storage_local_global_getter( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { storage_global_value(StorageKind::Local) } -extern "C" fn storage_session_global_getter(_closure: *const ClosureHeader) -> f64 { +extern "C" fn storage_session_global_getter( + _closure: *const ClosureHeader, + _this: crate::closure::JsThis, +) -> f64 { storage_global_value(StorageKind::Session) } -extern "C" fn storage_length_getter(_closure: *const ClosureHeader) -> f64 { - let Some(kind) = receiver_kind() else { +extern "C" fn storage_length_getter( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let Some(kind) = receiver_kind(this) else { return throw_type_error("Illegal invocation"); }; with_store(kind, |store| store.len() as f64) } -extern "C" fn storage_get_item(_closure: *const ClosureHeader, key: f64) -> f64 { - let Some(kind) = receiver_kind() else { +extern "C" fn storage_get_item( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + key: f64, +) -> f64 { + let Some(kind) = receiver_kind(this) else { return throw_type_error("Illegal invocation"); }; storage_get_item_impl(kind, key) } -extern "C" fn storage_set_item(_closure: *const ClosureHeader, key: f64, value: f64) -> f64 { - let Some(kind) = receiver_kind() else { +extern "C" fn storage_set_item( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + key: f64, + value: f64, +) -> f64 { + let Some(kind) = receiver_kind(this) else { return throw_type_error("Illegal invocation"); }; storage_set_item_impl(kind, key, value) } -extern "C" fn storage_remove_item(_closure: *const ClosureHeader, key: f64) -> f64 { - let Some(kind) = receiver_kind() else { +extern "C" fn storage_remove_item( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + key: f64, +) -> f64 { + let Some(kind) = receiver_kind(this) else { return throw_type_error("Illegal invocation"); }; storage_remove_item_impl(kind, key) } -extern "C" fn storage_key(_closure: *const ClosureHeader, index: f64) -> f64 { - let Some(kind) = receiver_kind() else { +extern "C" fn storage_key( + _closure: *const ClosureHeader, + this: crate::closure::JsThis, + index: f64, +) -> f64 { + let Some(kind) = receiver_kind(this) else { return throw_type_error("Illegal invocation"); }; storage_key_impl(kind, index) } -extern "C" fn storage_clear(_closure: *const ClosureHeader) -> f64 { - let Some(kind) = receiver_kind() else { +extern "C" fn storage_clear(_closure: *const ClosureHeader, this: crate::closure::JsThis) -> f64 { + let Some(kind) = receiver_kind(this) else { return throw_type_error("Illegal invocation"); }; storage_clear_impl(kind) @@ -355,8 +388,8 @@ fn set_global_storage_property(global: *mut ObjectHeader, name: &str, value: *mu } } -fn receiver_kind() -> Option { - let this = crate::object::js_implicit_this_get(); +fn receiver_kind(this: crate::closure::JsThis) -> Option { + let this = this.as_f64(); storage_kind_from_value(this) } diff --git a/crates/perry-runtime/src/webassembly.rs b/crates/perry-runtime/src/webassembly.rs index 44a4a8dbbd..71ec7f104c 100644 --- a/crates/perry-runtime/src/webassembly.rs +++ b/crates/perry-runtime/src/webassembly.rs @@ -961,8 +961,12 @@ unsafe extern "C" fn call_wasm_import( _ => f64::from_bits(TAG_UNDEFINED), }) .collect(); - let result = - crate::closure::js_native_call_value(callback.get_nanbox_f64(), args.as_ptr(), args.len()); + let result = crate::closure::js_native_call_value( + callback.get_nanbox_f64(), + crate::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); let result_kinds = if result_count == 0 { &[] @@ -1076,8 +1080,8 @@ fn make_export_function( }) } -fn global_handle_from_receiver() -> Option<*mut c_void> { - let receiver = JSValue::from_bits(crate::object::js_implicit_this_get().to_bits()); +fn global_handle_from_receiver(this: crate::closure::JsThis) -> Option<*mut c_void> { + let receiver = JSValue::from_bits(this.as_f64().to_bits()); if !receiver.is_pointer() { return None; } @@ -1088,8 +1092,11 @@ fn global_handle_from_receiver() -> Option<*mut c_void> { .map(|handle| handle as *mut c_void) } -extern "C" fn js_wasm_global_get(_closure: *const crate::closure::ClosureHeader) -> f64 { - let Some(handle) = global_handle_from_receiver() else { +extern "C" fn js_wasm_global_get( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let Some(handle) = global_handle_from_receiver(this) else { return nanbox_undefined(); }; let mut kind = WASM_VAL_KIND_NONE; @@ -1102,9 +1109,10 @@ extern "C" fn js_wasm_global_get(_closure: *const crate::closure::ClosureHeader) extern "C" fn js_wasm_global_set( _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, value: f64, ) -> f64 { - let Some(handle) = global_handle_from_receiver() else { + let Some(handle) = global_handle_from_receiver(this) else { return nanbox_undefined(); }; let mut kind = WASM_VAL_KIND_NONE; @@ -1567,7 +1575,7 @@ pub extern "C" fn js_webassembly_instantiate(bytes_jsval: f64, imports_jsval: f6 /// `WebAssembly.callExport(handle, name, ...args)` — invoke an exported /// function by name with numeric arguments. Currently supports up to 4 -/// numeric args, mirroring the closure-call ABI in `closure.rs`. All +/// numeric args, mirroring the `js_closure_call{N}` entries. All /// arguments and the return value are passed as f64; the runtime infers /// the wasm signature from the export type and widens/narrows as needed. /// diff --git a/crates/perry-runtime/src/webassembly_calls.rs b/crates/perry-runtime/src/webassembly_calls.rs index 07d1a5e376..3d23bf1f5d 100644 --- a/crates/perry-runtime/src/webassembly_calls.rs +++ b/crates/perry-runtime/src/webassembly_calls.rs @@ -4,6 +4,7 @@ use super::*; extern "C" fn js_wasm_instance_result_then( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, on_fulfilled: f64, _on_rejected: f64, ) -> f64 { @@ -13,6 +14,7 @@ extern "C" fn js_wasm_instance_result_then( let outcome = crate::exception::catch_js_throw(|| unsafe { crate::closure::js_native_call_value( on_fulfilled.get_nanbox_f64(), + crate::closure::plain_call_receiver(), [value.get_nanbox_f64()].as_ptr(), 1, ) @@ -45,6 +47,7 @@ macro_rules! wasm_export_call_shim { ($name:ident $(, $arg:ident)*) => { pub(super) extern "C" fn $name( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, $($arg: f64),* ) -> f64 { call_captured_wasm_export(closure, &[$($arg),*]) diff --git a/crates/perry-runtime/src/webassembly_table.rs b/crates/perry-runtime/src/webassembly_table.rs index 4cbfab0f6a..f7157f5adc 100644 --- a/crates/perry-runtime/src/webassembly_table.rs +++ b/crates/perry-runtime/src/webassembly_table.rs @@ -57,7 +57,11 @@ pub(super) fn wasm_function_external(value: f64) -> *mut c_void { crate::closure::js_closure_get_capture_f64(closure, 6) as usize as *mut c_void } -extern "C" fn js_wasm_table_get(closure: *const crate::closure::ClosureHeader, index: f64) -> f64 { +extern "C" fn js_wasm_table_get( + closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, + index: f64, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let (external, inst, name, table) = table_method_context(&scope, closure); let values = scope.root_nanbox_f64(table_values(table.get_nanbox_f64())); @@ -120,6 +124,7 @@ extern "C" fn js_wasm_table_get(closure: *const crate::closure::ClosureHeader, i extern "C" fn js_wasm_table_set( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, index: f64, value: f64, ) -> f64 { @@ -163,6 +168,7 @@ extern "C" fn js_wasm_table_set( extern "C" fn js_wasm_table_grow( closure: *const crate::closure::ClosureHeader, + _this: crate::closure::JsThis, delta: f64, value: f64, ) -> f64 { diff --git a/crates/perry-runtime/src/yoga.rs b/crates/perry-runtime/src/yoga.rs index f47e097b51..7f0edf7f2b 100644 --- a/crates/perry-runtime/src/yoga.rs +++ b/crates/perry-runtime/src/yoga.rs @@ -615,7 +615,14 @@ fn measure_leaf( let (h, hmode) = measure_axis(known_dims.height, available_space.height); let args = [w, wmode, h, hmode]; - let result = unsafe { crate::closure::js_native_call_value(cb, args.as_ptr(), 4) }; + let result = unsafe { + crate::closure::js_native_call_value( + cb, + crate::closure::plain_call_receiver(), + args.as_ptr(), + 4, + ) + }; // result is a NaN-boxed object {width, height}; read both numbers. let bits = result.to_bits(); diff --git a/crates/perry-stdlib/src/async_local_storage.rs b/crates/perry-stdlib/src/async_local_storage.rs index 2c482b0c51..c077e6241c 100644 --- a/crates/perry-stdlib/src/async_local_storage.rs +++ b/crates/perry-stdlib/src/async_local_storage.rs @@ -56,7 +56,12 @@ unsafe fn validate_callback(callback: f64) -> *const ClosureHeader { unsafe fn call_with_forwarded_args(cb: *const ClosureHeader, args_array: i64) -> f64 { let closure_env = cb as i64; if args_array == 0 { - return js_closure_call_array(closure_env, std::ptr::null(), 0); + return js_closure_call_array( + closure_env, + perry_runtime::closure::plain_call_receiver(), + std::ptr::null(), + 0, + ); } let arr = args_array as *const ArrayHeader; let len = js_array_length(arr) as i64; @@ -65,7 +70,12 @@ unsafe fn call_with_forwarded_args(cb: *const ClosureHeader, args_array: i64) -> } else { perry_runtime::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 }; - js_closure_call_array(closure_env, data, len) + js_closure_call_array( + closure_env, + perry_runtime::closure::plain_call_receiver(), + data, + len, + ) } /// AsyncLocalStorage handle. Store stacks live in perry-runtime's active diff --git a/crates/perry-stdlib/src/bun_sql.rs b/crates/perry-stdlib/src/bun_sql.rs index 58ff4c0c56..bb2289b2f9 100644 --- a/crates/perry-stdlib/src/bun_sql.rs +++ b/crates/perry-stdlib/src/bun_sql.rs @@ -122,6 +122,7 @@ unsafe fn execute_sqlite_query(db_handle: Handle, strings_value: f64, params_val extern "C" fn bun_sql_query_tag( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, strings_value: f64, params_value: f64, ) -> f64 { @@ -146,7 +147,11 @@ unsafe fn finish_transaction(db_handle: Handle, nested: bool, success: bool) { }); } -extern "C" fn bun_sql_transaction_fulfilled(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn bun_sql_transaction_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + value: f64, +) -> f64 { unsafe { let handle = captured_handle(closure); let nested = js_closure_get_capture_f64(closure, 1) != 0.0; @@ -155,7 +160,11 @@ extern "C" fn bun_sql_transaction_fulfilled(closure: *const ClosureHeader, value } } -extern "C" fn bun_sql_transaction_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn bun_sql_transaction_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { unsafe { let scope = RuntimeHandleScope::new(); let reason = scope.root_nanbox_f64(reason); @@ -171,7 +180,11 @@ extern "C" fn bun_sql_transaction_rejected(closure: *const ClosureHeader, reason } } -extern "C" fn bun_sql_begin(closure: *const ClosureHeader, callback_value: f64) -> f64 { +extern "C" fn bun_sql_begin( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + callback_value: f64, +) -> f64 { unsafe { if closure_ptr_from_value(callback_value).is_none() { let error = js_call_catching(|| throw_plain_type("SQL.begin expects a callback")) @@ -202,18 +215,23 @@ extern "C" fn bun_sql_begin(closure: *const ClosureHeader, callback_value: f64) let callback_ptr = closure_ptr_from_value(callback.get_nanbox_f64()) .expect("the rooted transaction callback remains callable"); - let callback_result = - match js_call_catching(|| js_closure_call1(callback_ptr, client.get_nanbox_f64())) { - Ok(value) => value, - Err(error) => { - let error = scope.root_nanbox_f64(error); - let _ = js_call_catching(|| { - finish_transaction(db_handle, nested, false); - undefined_f64() - }); - return rejected_promise(error.get_nanbox_f64()); - } - }; + let callback_result = match js_call_catching(|| { + js_closure_call1( + callback_ptr, + perry_runtime::closure::plain_call_receiver(), + client.get_nanbox_f64(), + ) + }) { + Ok(value) => value, + Err(error) => { + let error = scope.root_nanbox_f64(error); + let _ = js_call_catching(|| { + finish_transaction(db_handle, nested, false); + undefined_f64() + }); + return rejected_promise(error.get_nanbox_f64()); + } + }; let callback_scope = RuntimeHandleScope::new(); let callback_result = callback_scope.root_nanbox_f64(callback_result); let callback_promise = match js_promise_resolved_catching(callback_result.get_nanbox_f64()) @@ -259,11 +277,18 @@ extern "C" fn bun_sql_begin(closure: *const ClosureHeader, callback_value: f64) } } -extern "C" fn bun_sql_reserve(closure: *const ClosureHeader, _options: f64) -> f64 { +extern "C" fn bun_sql_reserve( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _options: f64, +) -> f64 { unsafe { resolved_promise(captured_client(closure)) } } -extern "C" fn bun_sql_release(_closure: *const ClosureHeader) -> f64 { +extern "C" fn bun_sql_release( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { undefined_f64() } @@ -275,11 +300,22 @@ unsafe fn call_onclose(callback_value: f64, client: f64, error: f64) -> Result<( let Some(callback) = closure_ptr_from_value(callback_value.get_nanbox_f64()) else { return Ok(()); }; - js_call_catching(|| js_closure_call2(callback, client.get_nanbox_f64(), error.get_nanbox_f64())) - .map(|_| ()) + js_call_catching(|| { + js_closure_call2( + callback, + perry_runtime::closure::plain_call_receiver(), + client.get_nanbox_f64(), + error.get_nanbox_f64(), + ) + }) + .map(|_| ()) } -extern "C" fn bun_sql_close(closure: *const ClosureHeader, _options: f64) -> f64 { +extern "C" fn bun_sql_close( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _options: f64, +) -> f64 { unsafe { let scope = RuntimeHandleScope::new(); let db_handle = captured_handle(closure); @@ -463,7 +499,11 @@ pub unsafe extern "C" fn js_bun_sql_new(config_value: f64, options_value: f64) - if let Some(onconnect) = onconnect { if let Some(callback) = closure_ptr_from_value(onconnect.get_nanbox_f64()) { - js_closure_call1(callback, client.get_nanbox_f64()); + js_closure_call1( + callback, + perry_runtime::closure::plain_call_receiver(), + client.get_nanbox_f64(), + ); } } client.get_nanbox_f64() diff --git a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs index 94d7681106..d29886b708 100644 --- a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs +++ b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs @@ -48,6 +48,7 @@ fn async_local_storage_method_name_static(property: &str) -> Option<&'static [u8 extern "C" fn async_local_storage_unbound_method_thunk( closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, rest: f64, ) -> f64 { unsafe { @@ -57,7 +58,7 @@ extern "C" fn async_local_storage_unbound_method_thunk( let name_str = std::str::from_utf8(name).unwrap_or(""); let scope = perry_runtime::gc::RuntimeHandleScope::new(); let rest = scope.root_nanbox_f64(rest); - let receiver_handle = scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_get()); + let receiver_handle = scope.root_nanbox_f64(this.as_f64()); let receiver = receiver_handle.get_nanbox_f64(); let receiver_raw = if receiver.to_bits() >> 48 == 0x7FFD { (receiver.to_bits() & POINTER_MASK_BITS) as i64 diff --git a/crates/perry-stdlib/src/common/net_method_values.rs b/crates/perry-stdlib/src/common/net_method_values.rs index 63521f9678..394a1083ce 100644 --- a/crates/perry-stdlib/src/common/net_method_values.rs +++ b/crates/perry-stdlib/src/common/net_method_values.rs @@ -504,6 +504,7 @@ pub(crate) unsafe fn dispatch_external_server_method( if callback >= 0x1000 { perry_runtime::closure::js_closure_call2( callback as *const perry_runtime::ClosureHeader, + perry_runtime::closure::plain_call_receiver(), null(), js_net_server_get_connections(handle), ); diff --git a/crates/perry-stdlib/src/crypto/hash_handles.rs b/crates/perry-stdlib/src/crypto/hash_handles.rs index 1e5fda0346..6a49926bfd 100644 --- a/crates/perry-stdlib/src/crypto/hash_handles.rs +++ b/crates/perry-stdlib/src/crypto/hash_handles.rs @@ -229,13 +229,20 @@ unsafe fn digest_value(bytes: &[u8], encoding: Option<&str>) -> f64 { unsafe fn emit_callback0(cb: i64) { if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + js_closure_call0( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + ); } } unsafe fn emit_callback1(cb: i64, arg: f64) { if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, arg); + js_closure_call1( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + arg, + ); } } diff --git a/crates/perry-stdlib/src/crypto/keys.rs b/crates/perry-stdlib/src/crypto/keys.rs index 8ffa4087e0..176350d2c2 100644 --- a/crates/perry-stdlib/src/crypto/keys.rs +++ b/crates/perry-stdlib/src/crypto/keys.rs @@ -97,6 +97,7 @@ pub(super) unsafe fn call_node_style_callback2(callback_bits: f64, err: f64, val } perry_runtime::closure::js_closure_call2( raw as *const perry_runtime::ClosureHeader, + perry_runtime::closure::plain_call_receiver(), err, value, ); diff --git a/crates/perry-stdlib/src/crypto/random.rs b/crates/perry-stdlib/src/crypto/random.rs index 4dfbc60836..66c200cc71 100644 --- a/crates/perry-stdlib/src/crypto/random.rs +++ b/crates/perry-stdlib/src/crypto/random.rs @@ -964,6 +964,7 @@ mod tests { extern "C" fn record_cb_thunk( _closure: *const perry_runtime::ClosureHeader, + _this: perry_runtime::closure::JsThis, err: f64, value: f64, ) -> f64 { diff --git a/crates/perry-stdlib/src/domain.rs b/crates/perry-stdlib/src/domain.rs index ca20eb4764..89f5474af7 100644 --- a/crates/perry-stdlib/src/domain.rs +++ b/crates/perry-stdlib/src/domain.rs @@ -295,26 +295,19 @@ unsafe fn emit_domain_event(handle: Handle, event: &str, args: &[f64]) -> bool { // moving minor collection; an unrooted copy then holds a retired // from-space address for the NEXT listener in this same loop. Root both // through one handle scope and re-read the current bits before every - // call. - // - // #10490: the displaced `this` is the caller's receiver and every listener - // is user code that can move it too. Root it ONCE, in that same scope, - // before the first `js_implicit_this_set`, and restore it from that root - // rather than from a plain per-iteration local. + // call. Each listener runs with the domain as `this`. let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let previous_this = scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_get()); let listener_handles = scope.root_nanbox_f64_slice(&listeners); let arg_handles = scope.root_nanbox_f64_slice(args); for listener_handle in &listener_handles { let live_args = perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - perry_runtime::object::js_implicit_this_set(receiver); let _ = perry_runtime::closure::js_native_call_value( listener_handle.get_nanbox_f64(), + perry_runtime::closure::JsThis::from_f64(receiver), live_args.as_ptr(), live_args.len(), ); - perry_runtime::object::js_implicit_this_set(previous_this.get_nanbox_f64()); } true } @@ -335,7 +328,12 @@ unsafe fn call_with_domain(handle: Handle, callback: f64, args: &[f64]) -> f64 { // Armed in a C trampoline frame (#9305); the error emit below runs // after the trap is popped, exactly as before. let outcome = perry_runtime::exception::catch_js_throw(|| unsafe { - perry_runtime::closure::js_native_call_value(callback, args.as_ptr(), args.len()) + perry_runtime::closure::js_native_call_value( + callback, + perry_runtime::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ) }); exit_domain(handle); match outcome { @@ -459,7 +457,11 @@ pub extern "C" fn js_domain_exit(handle: Handle) -> f64 { undefined() } -extern "C" fn domain_bound_wrapper(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn domain_bound_wrapper( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + rest: f64, +) -> f64 { unsafe { let handle = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as Handle; let callback = perry_runtime::closure::js_closure_get_capture_f64(closure, 1); @@ -469,7 +471,11 @@ extern "C" fn domain_bound_wrapper(closure: *const ClosureHeader, rest: f64) -> } } -extern "C" fn domain_intercept_wrapper(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn domain_intercept_wrapper( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + rest: f64, +) -> f64 { unsafe { let handle = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as Handle; let callback = perry_runtime::closure::js_closure_get_capture_f64(closure, 1); diff --git a/crates/perry-stdlib/src/events.rs b/crates/perry-stdlib/src/events.rs index 770e0a5f1f..8d5653af33 100644 --- a/crates/perry-stdlib/src/events.rs +++ b/crates/perry-stdlib/src/events.rs @@ -473,6 +473,7 @@ impl EventEmitterHandle { let closure_ptr = handle.get_heap_word_u64() as *const ClosureHeader; js_closure_call2( closure_ptr, + perry_runtime::closure::plain_call_receiver(), event_arg_h.get_nanbox_f64(), listener_arg_h.get_nanbox_f64(), ); @@ -691,7 +692,11 @@ unsafe fn create_once_raw_wrapper(handle: Handle, event_name: &str, callback: i6 wrapper as i64 } -extern "C" fn event_emitter_once_wrapper(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn event_emitter_once_wrapper( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + rest: f64, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; unsafe { @@ -849,9 +854,13 @@ unsafe fn dispatch_error_monitor(emitter: &mut EventEmitterHandle, arg: Option() as i64, ); } - // #10490: root the displaced `this` across the listener (user code). - let this_scope = perry_runtime::gc::RuntimeHandleScope::new(); - let previous_this = - this_scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_set(receiver)); - let result = - perry_runtime::closure::js_native_call_value(callback_value, args.as_ptr(), args.len()); - perry_runtime::object::js_implicit_this_set(previous_this.get_nanbox_f64()); - result + // A listener runs with the emitter as `this`. + perry_runtime::closure::js_native_call_value( + callback_value, + perry_runtime::closure::JsThis::from_f64(receiver), + args.as_ptr(), + args.len(), + ) } const TAG_UNDEFINED_F64_BITS: u64 = 0x7FFC_0000_0000_0001; -extern "C" fn events_capture_rejection_handler(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn events_capture_rejection_handler( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let handle = js_closure_get_capture_ptr(closure, 0) as Handle; diff --git a/crates/perry-stdlib/src/events/events_on.rs b/crates/perry-stdlib/src/events/events_on.rs index a595265bfd..d842790751 100644 --- a/crates/perry-stdlib/src/events/events_on.rs +++ b/crates/perry-stdlib/src/events/events_on.rs @@ -83,7 +83,11 @@ fn register_events_on_arities() { /// The queue listener fired for each emitted event. Resolves a blocked `next()` /// Promise immediately if one is waiting, otherwise buffers the `[arg]` array. -extern "C" fn events_on_queue_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { +extern "C" fn events_on_queue_listener( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + arg0: f64, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; @@ -115,7 +119,10 @@ extern "C" fn events_on_queue_listener(closure: *const ClosureHeader, arg0: f64) /// `next()` — drain a buffered event, reject on abort, finish when done, or /// return a pending Promise the listener will resolve on the next event. -extern "C" fn events_on_next(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_on_next( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; @@ -151,7 +158,10 @@ extern "C" fn events_on_next(closure: *const ClosureHeader) -> f64 { /// `return()` — end iteration: mark done, detach the listener, settle any /// blocked `next()` with `{ done: true }`. -extern "C" fn events_on_return(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_on_return( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; @@ -191,13 +201,19 @@ extern "C" fn events_on_return(closure: *const ClosureHeader) -> f64 { events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true) } -extern "C" fn events_on_aiter_self(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_on_aiter_self( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { perry_runtime::closure::js_closure_get_capture_f64(closure, 0) } /// `queue[Symbol.asyncIterator]()` — build a fresh `{ next, return }` iterator /// object bound to the shared state. -extern "C" fn events_on_async_iterator(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_on_async_iterator( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; let state = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; @@ -250,7 +266,10 @@ unsafe fn install_events_on_async_iterator(queue: *mut ArrayHeader, state: *mut ); } -extern "C" fn events_on_abort_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_on_abort_listener( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let handle = js_closure_get_capture_ptr(closure, 0) as Handle; diff --git a/crates/perry-stdlib/src/events/module_helpers.rs b/crates/perry-stdlib/src/events/module_helpers.rs index d6fde1d471..1888a17f11 100644 --- a/crates/perry-stdlib/src/events/module_helpers.rs +++ b/crates/perry-stdlib/src/events/module_helpers.rs @@ -33,7 +33,10 @@ pub(super) unsafe fn call_net_socket_method(handle: i64, name: &str, args: &[f64 ) } -extern "C" fn events_abort_listener_dispose(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_abort_listener_dispose( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let signal_ptr = js_closure_get_capture_ptr(closure, 0); diff --git a/crates/perry-stdlib/src/events/once_helpers.rs b/crates/perry-stdlib/src/events/once_helpers.rs index 7866b722fa..7bccf9dd6b 100644 --- a/crates/perry-stdlib/src/events/once_helpers.rs +++ b/crates/perry-stdlib/src/events/once_helpers.rs @@ -38,7 +38,10 @@ unsafe fn remove_stream_or_socket_once_listener( } } -extern "C" fn events_once_abort_listener(closure: *const ClosureHeader) -> f64 { +extern "C" fn events_once_abort_listener( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let handle = js_closure_get_capture_ptr(closure, 0) as Handle; @@ -58,7 +61,11 @@ extern "C" fn events_once_abort_listener(closure: *const ClosureHeader) -> f64 { undefined_value() } -extern "C" fn events_once_stream_resolve_listener(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn events_once_stream_resolve_listener( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + rest: f64, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; @@ -77,7 +84,11 @@ extern "C" fn events_once_stream_resolve_listener(closure: *const ClosureHeader, undefined_value() } -extern "C" fn events_once_stream_reject_listener(closure: *const ClosureHeader, rest: f64) -> f64 { +extern "C" fn events_once_stream_reject_listener( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + rest: f64, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; @@ -115,7 +126,11 @@ fn first_rest_arg_or_undefined(rest: f64) -> f64 { } } -extern "C" fn events_once_event_target_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { +extern "C" fn events_once_event_target_listener( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + arg0: f64, +) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; let promise = js_closure_get_capture_ptr(closure, 0) as *mut Promise; diff --git a/crates/perry-stdlib/src/events/warnings.rs b/crates/perry-stdlib/src/events/warnings.rs index e6f9beaa01..7df3e3229f 100644 --- a/crates/perry-stdlib/src/events/warnings.rs +++ b/crates/perry-stdlib/src/events/warnings.rs @@ -53,25 +53,19 @@ unsafe fn emit_warning(warning: f64) { // allocate in its own dispatch preamble before it reads the // callee/args, which would otherwise leave these pointing at a // retired from-space address by the time they're dereferenced. - // - // #10490: the displaced `this` crosses the (user-replaceable) - // `process.emitWarning`, so root it in that same scope and - // restore it from the root, not from a plain local. + // `process.emitWarning` is called as a method of `process`. let scope = perry_runtime::gc::RuntimeHandleScope::new(); let callback_h = scope.root_nanbox_f64(emit_warning); let process_h = scope.root_nanbox_f64(process); let arg_handles = scope.root_nanbox_f64_slice(&[warning]); - let previous_this = scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_set( - process_h.get_nanbox_f64(), - )); let live_args = perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); perry_runtime::closure::js_native_call_value( callback_h.get_nanbox_f64(), + perry_runtime::closure::JsThis::from_f64(process_h.get_nanbox_f64()), live_args.as_ptr(), live_args.len(), ); - perry_runtime::object::js_implicit_this_set(previous_this.get_nanbox_f64()); return; } } diff --git a/crates/perry-stdlib/src/fetch/body_metadata.rs b/crates/perry-stdlib/src/fetch/body_metadata.rs index 8e904ad377..32cbb9a5c5 100644 --- a/crates/perry-stdlib/src/fetch/body_metadata.rs +++ b/crates/perry-stdlib/src/fetch/body_metadata.rs @@ -816,6 +816,7 @@ pub extern "C" fn js_form_data_for_each(handle: f64, callback: f64) -> f64 { as *const perry_runtime::ClosureHeader; perry_runtime::js_closure_call3( closure, + perry_runtime::closure::plain_call_receiver(), value_handle.get_nanbox_f64(), name_handle.get_nanbox_f64(), handle, diff --git a/crates/perry-stdlib/src/fetch/headers.rs b/crates/perry-stdlib/src/fetch/headers.rs index f46e88dd65..c5845e78a1 100644 --- a/crates/perry-stdlib/src/fetch/headers.rs +++ b/crates/perry-stdlib/src/fetch/headers.rs @@ -662,7 +662,12 @@ pub extern "C" fn js_headers_for_each(handle: f64, callback: f64) -> f64 { let k_nan = JSValue::string_ptr(k_ptr).bits(); let closure = perry_runtime::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const perry_runtime::ClosureHeader; - perry_runtime::js_closure_call2(closure, v_handle.get_nanbox_f64(), f64::from_bits(k_nan)); + perry_runtime::js_closure_call2( + closure, + perry_runtime::closure::plain_call_receiver(), + v_handle.get_nanbox_f64(), + f64::from_bits(k_nan), + ); } f64::from_bits(TAG_UNDEFINED) } diff --git a/crates/perry-stdlib/src/querystring.rs b/crates/perry-stdlib/src/querystring.rs index d363ee4b18..e41617377a 100644 --- a/crates/perry-stdlib/src/querystring.rs +++ b/crates/perry-stdlib/src/querystring.rs @@ -390,7 +390,7 @@ unsafe fn apply_decode_codec(callback: *const ClosureHeader, raw: &str) -> Optio /// Call a user-supplied codec closure with `raw` and return its JS value. unsafe fn call_codec(callback: *const ClosureHeader, raw: &str) -> f64 { let arg = nanbox_string(intern_string(raw)); - js_closure_call1(callback, arg) + js_closure_call1(callback, perry_runtime::closure::plain_call_receiver(), arg) } unsafe fn apply_encode_codec(callback: *const ClosureHeader, raw: &str) -> String { diff --git a/crates/perry-stdlib/src/readline/mod.rs b/crates/perry-stdlib/src/readline/mod.rs index 0456134295..ac2f1f2211 100644 --- a/crates/perry-stdlib/src/readline/mod.rs +++ b/crates/perry-stdlib/src/readline/mod.rs @@ -821,7 +821,12 @@ fn call_write_value(output: f64, text: &str) { if let Some(write) = object_field(output, b"write").filter(|v| is_callable(*v)) { let args = [chunk]; unsafe { - let _ = js_native_call_value(write, args.as_ptr(), args.len()); + let _ = js_native_call_value( + write, + perry_runtime::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } return; } @@ -906,7 +911,10 @@ fn close_custom_interface(handle: i64) { }) .flatten(); if let Some(cb_i64) = cb { - js_closure_call0(cb_i64 as *const ClosureHeader); + js_closure_call0( + cb_i64 as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + ); // Release the slot once the close notification has an observer. If the // custom stream completed before user code could attach `rl.on` // listeners, retain the closed state temporarily; `js_readline_on` @@ -958,7 +966,11 @@ fn append_custom_input(handle: i64, chunk: f64) { }) .flatten(); if let Some(cb_i64) = cb { - js_closure_call1(cb_i64 as *const ClosureHeader, callback_arg(&line)); + js_closure_call1( + cb_i64 as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + callback_arg(&line), + ); } else { let _ = with_interface_mut(handle, |state| { state.buffered_lines.push_back(line); @@ -1033,7 +1045,10 @@ enum NextAction { Pending, } -extern "C" fn readline_aiter_next(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_aiter_next( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let handle = js_closure_get_capture_f64(closure, 0) as i64; // Decide the outcome without holding the interface borrow across the Promise // allocation below (GC must be free to scan READLINE_INTERFACES). @@ -1060,7 +1075,10 @@ extern "C" fn readline_aiter_next(closure: *const ClosureHeader) -> f64 { } } -extern "C" fn readline_aiter_return(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_aiter_return( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let handle = js_closure_get_capture_f64(closure, 0) as i64; let pending = with_interface_mut(handle, |state| { state.ended = true; @@ -1076,7 +1094,10 @@ extern "C" fn readline_aiter_return(closure: *const ClosureHeader) -> f64 { resolved_iter_promise(undefined(), true) } -extern "C" fn readline_aiter_self(closure: *const ClosureHeader) -> f64 { +extern "C" fn readline_aiter_self( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { js_closure_get_capture_f64(closure, 0) } @@ -1145,13 +1166,20 @@ pub extern "C" fn js_readline_iterator(handle: i64) -> i64 { obj_handle.get_raw_mut_ptr::() as i64 } -extern "C" fn custom_input_data(closure: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn custom_input_data( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + chunk: f64, +) -> f64 { let handle = js_closure_get_capture_f64(closure, 0) as i64; append_custom_input(handle, chunk); undefined() } -extern "C" fn custom_input_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn custom_input_close( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let handle = js_closure_get_capture_f64(closure, 0) as i64; close_custom_interface(handle); undefined() @@ -1201,8 +1229,12 @@ fn attach_custom_input(handle: i64, input: f64) { // slice from the handles every iteration. let args = [event.get_nanbox_f64(), cb.get_nanbox_f64()]; unsafe { - let _ = - js_native_call_value(on_handle.get_nanbox_f64(), args.as_ptr(), args.len()); + let _ = js_native_call_value( + on_handle.get_nanbox_f64(), + perry_runtime::closure::plain_call_receiver(), + args.as_ptr(), + args.len(), + ); } } } @@ -1315,153 +1347,12 @@ fn ensure_reader_started() { // --------------------------------------------------------------------------- #[cfg(unix)] -mod termios_impl { - use std::sync::Mutex; - - /// Saved cooked-mode termios so we can restore on disable. Lazy-init - /// on the first enable call; survives toggle cycles. - static SAVED: Mutex> = Mutex::new(None); - - /// Enable raw mode on fd 0 (stdin). Returns true on success. - pub fn enable() -> bool { - unsafe { - let mut current: libc::termios = std::mem::zeroed(); - if libc::tcgetattr(0, &mut current) != 0 { - return false; - } - // Save the original on first enable so disable can restore. - { - let mut saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); - if saved.is_none() { - *saved = Some(current); - } - } - let mut raw = current; - // cfmakeraw equivalent (Node's setRawMode does roughly this). - raw.c_iflag &= !(libc::IGNBRK - | libc::BRKINT - | libc::PARMRK - | libc::ISTRIP - | libc::INLCR - | libc::IGNCR - | libc::ICRNL - | libc::IXON); - raw.c_oflag &= !libc::OPOST; - raw.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); - raw.c_cflag &= !(libc::CSIZE | libc::PARENB); - raw.c_cflag |= libc::CS8; - raw.c_cc[libc::VMIN] = 1; - raw.c_cc[libc::VTIME] = 0; - libc::tcsetattr(0, libc::TCSANOW, &raw) == 0 - } - } - - /// Disable raw mode (restore the saved cooked-mode termios). - pub fn disable() -> bool { - unsafe { - let saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); - if let Some(t) = saved.as_ref() { - libc::tcsetattr(0, libc::TCSANOW, t) == 0 - } else { - // Never enabled — nothing to restore. - true - } - } - } -} +#[path = "termios_unix.rs"] +mod termios_impl; #[cfg(all(windows, not(unix)))] -mod termios_impl { - use std::sync::Mutex; - use windows_sys::Win32::System::Console::{ - GetConsoleMode, GetStdHandle, SetConsoleMode, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, - ENABLE_PROCESSED_INPUT, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, - STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, - }; - - /// Saved console modes for the input + output handles. Set on first - /// `enable()`; restored by `disable()`. Two-tuple so we can leave - /// the output handle's mode untouched if we couldn't read it (e.g. - /// stdout redirected to a file — `GetConsoleMode` fails on - /// non-console handles). - static SAVED: Mutex)>> = Mutex::new(None); - - /// Flip stdin into byte-mode + virtual-terminal-input mode (so - /// arrow keys arrive as ANSI `\x1b[A..D` matching the Unix path's - /// parser) and stdout into virtual-terminal-processing mode (so the - /// renderer's CSI escapes actually move the cursor instead of - /// printing literally). Saves the original modes on first call so - /// `disable()` restores cleanly. (#406.) - pub fn enable() -> bool { - unsafe { - // windows-sys 0.61 (#720) made HANDLE a `*mut c_void` (was `isize` - // in 0.52). Use `.is_null()` + `INVALID_HANDLE_VALUE` constant - // instead of raw integer comparison. (#406 fix updated for - // windows-sys 0.61.) - use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; - let h_in = GetStdHandle(STD_INPUT_HANDLE); - if h_in.is_null() || h_in == INVALID_HANDLE_VALUE { - return false; - } - let mut current_in: u32 = 0; - if GetConsoleMode(h_in, &mut current_in) == 0 { - return false; - } - let h_out = GetStdHandle(STD_OUTPUT_HANDLE); - let current_out = if !h_out.is_null() && h_out != INVALID_HANDLE_VALUE { - let mut m: u32 = 0; - if GetConsoleMode(h_out, &mut m) != 0 { - Some(m) - } else { - None - } - } else { - None - }; - - { - let mut saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); - if saved.is_none() { - *saved = Some((current_in, current_out)); - } - } - - let raw_in = (current_in - & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)) - | ENABLE_VIRTUAL_TERMINAL_INPUT; - if SetConsoleMode(h_in, raw_in) == 0 { - return false; - } - if let Some(out_mode) = current_out { - let raw_out = out_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; - let _ = SetConsoleMode(h_out, raw_out); - } - true - } - } - - pub fn disable() -> bool { - unsafe { - let saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); - if let Some((in_mode, out_mode)) = saved.as_ref() { - use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; - let h_in = GetStdHandle(STD_INPUT_HANDLE); - if !h_in.is_null() && h_in != INVALID_HANDLE_VALUE { - let _ = SetConsoleMode(h_in, *in_mode); - } - if let Some(m) = out_mode { - let h_out = GetStdHandle(STD_OUTPUT_HANDLE); - if !h_out.is_null() && h_out != INVALID_HANDLE_VALUE { - let _ = SetConsoleMode(h_out, *m); - } - } - true - } else { - true - } - } - } -} +#[path = "termios_windows.rs"] +mod termios_impl; #[cfg(not(any(unix, windows)))] mod termios_impl { @@ -1585,14 +1476,21 @@ pub extern "C" fn js_readline_on( }; let cb = with_interface(handle, |state| state.line_callback).flatten(); if let Some(cb_i64) = cb { - js_closure_call1(cb_i64 as *const ClosureHeader, callback_arg(&line)); + js_closure_call1( + cb_i64 as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + callback_arg(&line), + ); } } } if replay_close { let cb = with_interface(handle, |state| state.close_callback).flatten(); if let Some(cb_i64) = cb { - js_closure_call0(cb_i64 as *const ClosureHeader); + js_closure_call0( + cb_i64 as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + ); } READLINE_INTERFACES.with(|interfaces| { if let Some(slot) = interfaces.borrow_mut().get_mut(handle as usize) { @@ -1649,7 +1547,7 @@ pub extern "C" fn js_readline_close(_handle: i64) -> f64 { let cb = CLOSE_CALLBACK.with(|c| c.borrow_mut().take()); if let Some(cb_i64) = cb { let closure = cb_i64 as *const ClosureHeader; - js_closure_call0(closure); + js_closure_call0(closure, perry_runtime::closure::plain_call_receiver()); } } undefined() diff --git a/crates/perry-stdlib/src/readline/pump.rs b/crates/perry-stdlib/src/readline/pump.rs index 3ce52dc444..0c3d9faa70 100644 --- a/crates/perry-stdlib/src/readline/pump.rs +++ b/crates/perry-stdlib/src/readline/pump.rs @@ -433,7 +433,7 @@ pub extern "C" fn js_readline_process_pending() -> i32 { .collect(); for callback in callback_handles { let closure = callback.get_raw_const_ptr::(); - js_closure_call0(closure); + js_closure_call0(closure, perry_runtime::closure::plain_call_receiver()); fired += 1; } } @@ -482,7 +482,11 @@ pub extern "C" fn js_readline_process_pending() -> i32 { let arg_handle = arg_scope.root_nanbox_f64(arg); for callback in &data_callback_handles { let closure = callback.get_raw_const_ptr::(); - js_closure_call1(closure, arg_handle.get_nanbox_f64()); + js_closure_call1( + closure, + perry_runtime::closure::plain_call_receiver(), + arg_handle.get_nanbox_f64(), + ); fired += 1; } } @@ -500,7 +504,12 @@ pub extern "C" fn js_readline_process_pending() -> i32 { scope.root_nanbox_f64(f64::from_bits(JSValue::string_ptr(seq_str).bits())); let arg2 = build_keypress_object(&name, ctrl, shift, meta, &seq); let closure = callback.get_raw_const_ptr::(); - js_closure_call2(closure, arg1.get_nanbox_f64(), arg2); + js_closure_call2( + closure, + perry_runtime::closure::plain_call_receiver(), + arg1.get_nanbox_f64(), + arg2, + ); fired += 1; } } @@ -524,14 +533,14 @@ pub extern "C" fn js_readline_process_pending() -> i32 { let q_cb = QUESTION_CALLBACK.with(|cb| cb.borrow_mut().take()); if let Some(cb_i64) = q_cb { let closure = cb_i64 as *const ClosureHeader; - js_closure_call1(closure, arg); + js_closure_call1(closure, perry_runtime::closure::plain_call_receiver(), arg); fired += 1; continue; } let line_cb = LINE_CALLBACK.with(|cb| *cb.borrow()); if let Some(cb_i64) = line_cb { let closure = cb_i64 as *const ClosureHeader; - js_closure_call1(closure, arg); + js_closure_call1(closure, perry_runtime::closure::plain_call_receiver(), arg); fired += 1; } } @@ -570,7 +579,11 @@ pub extern "C" fn js_readline_process_pending() -> i32 { let arg_handle = flush_scope.root_nanbox_f64(flushed); for callback in &flush_handles { let closure = callback.get_raw_const_ptr::(); - js_closure_call1(closure, arg_handle.get_nanbox_f64()); + js_closure_call1( + closure, + perry_runtime::closure::plain_call_receiver(), + arg_handle.get_nanbox_f64(), + ); fired += 1; } } @@ -579,7 +592,7 @@ pub extern "C" fn js_readline_process_pending() -> i32 { let cb = CLOSE_CALLBACK.with(|c| c.borrow_mut().take()); if let Some(cb_i64) = cb { let closure = cb_i64 as *const ClosureHeader; - js_closure_call0(closure); + js_closure_call0(closure, perry_runtime::closure::plain_call_receiver()); fired += 1; } } @@ -592,7 +605,10 @@ pub extern "C" fn js_readline_process_pending() -> i32 { .map(|mut v| std::mem::take(&mut *v)) .unwrap_or_default(); for cb_i64 in end_cbs { - js_closure_call0(cb_i64 as *const ClosureHeader); + js_closure_call0( + cb_i64 as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + ); fired += 1; } } diff --git a/crates/perry-stdlib/src/readline/termios_unix.rs b/crates/perry-stdlib/src/readline/termios_unix.rs new file mode 100644 index 0000000000..3b24b566c9 --- /dev/null +++ b/crates/perry-stdlib/src/readline/termios_unix.rs @@ -0,0 +1,52 @@ +use std::sync::Mutex; + +/// Saved cooked-mode termios so we can restore on disable. Lazy-init +/// on the first enable call; survives toggle cycles. +static SAVED: Mutex> = Mutex::new(None); + +/// Enable raw mode on fd 0 (stdin). Returns true on success. +pub fn enable() -> bool { + unsafe { + let mut current: libc::termios = std::mem::zeroed(); + if libc::tcgetattr(0, &mut current) != 0 { + return false; + } + // Save the original on first enable so disable can restore. + { + let mut saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); + if saved.is_none() { + *saved = Some(current); + } + } + let mut raw = current; + // cfmakeraw equivalent (Node's setRawMode does roughly this). + raw.c_iflag &= !(libc::IGNBRK + | libc::BRKINT + | libc::PARMRK + | libc::ISTRIP + | libc::INLCR + | libc::IGNCR + | libc::ICRNL + | libc::IXON); + raw.c_oflag &= !libc::OPOST; + raw.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); + raw.c_cflag &= !(libc::CSIZE | libc::PARENB); + raw.c_cflag |= libc::CS8; + raw.c_cc[libc::VMIN] = 1; + raw.c_cc[libc::VTIME] = 0; + libc::tcsetattr(0, libc::TCSANOW, &raw) == 0 + } +} + +/// Disable raw mode (restore the saved cooked-mode termios). +pub fn disable() -> bool { + unsafe { + let saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); + if let Some(t) = saved.as_ref() { + libc::tcsetattr(0, libc::TCSANOW, t) == 0 + } else { + // Never enabled — nothing to restore. + true + } + } +} diff --git a/crates/perry-stdlib/src/readline/termios_windows.rs b/crates/perry-stdlib/src/readline/termios_windows.rs new file mode 100644 index 0000000000..2867cb49aa --- /dev/null +++ b/crates/perry-stdlib/src/readline/termios_windows.rs @@ -0,0 +1,89 @@ +use std::sync::Mutex; +use windows_sys::Win32::System::Console::{ + GetConsoleMode, GetStdHandle, SetConsoleMode, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, + ENABLE_PROCESSED_INPUT, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, + STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, +}; + +/// Saved console modes for the input + output handles. Set on first +/// `enable()`; restored by `disable()`. Two-tuple so we can leave +/// the output handle's mode untouched if we couldn't read it (e.g. +/// stdout redirected to a file — `GetConsoleMode` fails on +/// non-console handles). +static SAVED: Mutex)>> = Mutex::new(None); + +/// Flip stdin into byte-mode + virtual-terminal-input mode (so +/// arrow keys arrive as ANSI `\x1b[A..D` matching the Unix path's +/// parser) and stdout into virtual-terminal-processing mode (so the +/// renderer's CSI escapes actually move the cursor instead of +/// printing literally). Saves the original modes on first call so +/// `disable()` restores cleanly. (#406.) +pub fn enable() -> bool { + unsafe { + // windows-sys 0.61 (#720) made HANDLE a `*mut c_void` (was `isize` + // in 0.52). Use `.is_null()` + `INVALID_HANDLE_VALUE` constant + // instead of raw integer comparison. (#406 fix updated for + // windows-sys 0.61.) + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + let h_in = GetStdHandle(STD_INPUT_HANDLE); + if h_in.is_null() || h_in == INVALID_HANDLE_VALUE { + return false; + } + let mut current_in: u32 = 0; + if GetConsoleMode(h_in, &mut current_in) == 0 { + return false; + } + let h_out = GetStdHandle(STD_OUTPUT_HANDLE); + let current_out = if !h_out.is_null() && h_out != INVALID_HANDLE_VALUE { + let mut m: u32 = 0; + if GetConsoleMode(h_out, &mut m) != 0 { + Some(m) + } else { + None + } + } else { + None + }; + + { + let mut saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); + if saved.is_none() { + *saved = Some((current_in, current_out)); + } + } + + let raw_in = (current_in + & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)) + | ENABLE_VIRTUAL_TERMINAL_INPUT; + if SetConsoleMode(h_in, raw_in) == 0 { + return false; + } + if let Some(out_mode) = current_out { + let raw_out = out_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + let _ = SetConsoleMode(h_out, raw_out); + } + true + } +} + +pub fn disable() -> bool { + unsafe { + let saved = SAVED.lock().unwrap_or_else(|p| p.into_inner()); + if let Some((in_mode, out_mode)) = saved.as_ref() { + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + let h_in = GetStdHandle(STD_INPUT_HANDLE); + if !h_in.is_null() && h_in != INVALID_HANDLE_VALUE { + let _ = SetConsoleMode(h_in, *in_mode); + } + if let Some(m) = out_mode { + let h_out = GetStdHandle(STD_OUTPUT_HANDLE); + if !h_out.is_null() && h_out != INVALID_HANDLE_VALUE { + let _ = SetConsoleMode(h_out, *m); + } + } + true + } else { + true + } + } +} diff --git a/crates/perry-stdlib/src/readline/test_support.rs b/crates/perry-stdlib/src/readline/test_support.rs index 5d6a6e16dd..52a80e0b46 100644 --- a/crates/perry-stdlib/src/readline/test_support.rs +++ b/crates/perry-stdlib/src/readline/test_support.rs @@ -15,7 +15,11 @@ pub(super) fn test_inject_chunk(chunk: &[u8]) { PENDING_DATA.lock().unwrap().push(chunk.to_vec()); } -extern "C" fn count_data_callback(_closure: *const ClosureHeader, _chunk: f64) -> f64 { +extern "C" fn count_data_callback( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _chunk: f64, +) -> f64 { DATA_COUNT.with(|count| *count.borrow_mut() += 1); undefined() } @@ -24,7 +28,10 @@ pub(super) fn data_counter_callback() -> i64 { js_closure_alloc(count_data_callback as *const u8, 0) as i64 } -extern "C" fn count_readable_callback(_closure: *const ClosureHeader) -> f64 { +extern "C" fn count_readable_callback( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { DATA_COUNT.with(|count| *count.borrow_mut() += 1); undefined() } @@ -35,6 +42,7 @@ pub(super) fn readable_counter_callback() -> i64 { extern "C" fn record_keypress_callback( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, _seq: f64, key_obj: f64, ) -> f64 { diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests.rs index 285fb1c514..ebc6b38ff3 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests.rs @@ -11,7 +11,10 @@ //! Only reachable with perry-runtime built as an ordinary dependency: its own //! unit tests keep freed blocks mapped and make these tables per-thread. -extern "C" fn probe_thunk(_closure: *const perry_runtime::ClosureHeader) -> f64 { +extern "C" fn probe_thunk( + _closure: *const perry_runtime::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { 0.0 } diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests/listeners_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests/listeners_tests.rs index 88ce3e9541..e851e51cbf 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests/listeners_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests/listeners_tests.rs @@ -7,11 +7,20 @@ use perry_runtime::gc::RuntimeHandleScope; use perry_runtime::{ClosureHeader, JSValue, ObjectHeader}; -extern "C" fn thunk1(_closure: *const ClosureHeader, _a: f64) -> f64 { +extern "C" fn thunk1( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _a: f64, +) -> f64 { f64::from_bits(JSValue::undefined().bits()) } -extern "C" fn thunk2(_closure: *const ClosureHeader, _a: f64, _b: f64) -> f64 { +extern "C" fn thunk2( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _a: f64, + _b: f64, +) -> f64 { f64::from_bits(JSValue::undefined().bits()) } diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs index ad035a99c8..91b6eab6df 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs @@ -7,7 +7,10 @@ use perry_runtime::ClosureHeader; -extern "C" fn probe_thunk(_closure: *const ClosureHeader) -> f64 { +extern "C" fn probe_thunk( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { 0.0 } diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests/streams_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests/streams_tests.rs index 7de8477db6..2128666c87 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests/streams_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests/streams_tests.rs @@ -6,7 +6,10 @@ //! join is not vacuous), and checks the exiting thread's `Arena::drop` //! released it. -extern "C" fn probe_thunk(_closure: *const perry_runtime::ClosureHeader) -> f64 { +extern "C" fn probe_thunk( + _closure: *const perry_runtime::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { 0.0 } diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests/symbols_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests/symbols_tests.rs index 22d0feb648..1bb12f28bf 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests/symbols_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests/symbols_tests.rs @@ -28,7 +28,10 @@ extern "C" { fn js_u8_buffer_read_f64(target: *const u8, index: i32) -> f64; } -extern "C" fn probe_thunk(_closure: *const perry_runtime::ClosureHeader) -> f64 { +extern "C" fn probe_thunk( + _closure: *const perry_runtime::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { 0.0 } diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests/ui_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests/ui_tests.rs index 6099eb6e4a..92f6084ca5 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests/ui_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests/ui_tests.rs @@ -13,11 +13,16 @@ use perry_runtime::closure::js_closure_alloc; use perry_runtime::gc::RuntimeHandleScope; use perry_runtime::{ArrayHeader, ClosureHeader, JSValue}; -extern "C" fn thunk0(_closure: *const ClosureHeader) -> f64 { +extern "C" fn thunk0(_closure: *const ClosureHeader, _this: perry_runtime::closure::JsThis) -> f64 { 0.0 } -extern "C" fn thunk2(_closure: *const ClosureHeader, _a: f64, _b: f64) -> f64 { +extern "C" fn thunk2( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _a: f64, + _b: f64, +) -> f64 { 0.0 } diff --git a/crates/perry-stdlib/src/sqlite/backup.rs b/crates/perry-stdlib/src/sqlite/backup.rs index 9e1985090e..c6fdebb8db 100644 --- a/crates/perry-stdlib/src/sqlite/backup.rs +++ b/crates/perry-stdlib/src/sqlite/backup.rs @@ -326,6 +326,7 @@ pub(crate) unsafe fn call_backup_progress( ); js_closure_call1( progress, + perry_runtime::closure::plain_call_receiver(), f64::from_bits(JSValue::object_ptr(info as *mut u8).bits()), ); } diff --git a/crates/perry-stdlib/src/sqlite/better.rs b/crates/perry-stdlib/src/sqlite/better.rs index 6680e3a711..c5057c2918 100644 --- a/crates/perry-stdlib/src/sqlite/better.rs +++ b/crates/perry-stdlib/src/sqlite/better.rs @@ -413,6 +413,7 @@ pub unsafe extern "C" fn js_sqlite_pragma( /// Captures: [0] = db_handle (as f64), [1] = original closure ptr (as i64) pub(crate) unsafe extern "C" fn sqlite_tx_wrapper( wrapper_closure: *const perry_runtime::ClosureHeader, + _this: perry_runtime::closure::JsThis, arg0: f64, ) -> f64 { use perry_runtime::closure::{ @@ -428,7 +429,11 @@ pub(crate) unsafe extern "C" fn sqlite_tx_wrapper( js_sqlite_begin_transaction(db_handle); // Call original closure with argument - let result = js_closure_call1(original_closure, arg0); + let result = js_closure_call1( + original_closure, + perry_runtime::closure::plain_call_receiver(), + arg0, + ); // COMMIT js_sqlite_commit(db_handle); diff --git a/crates/perry-stdlib/src/sqlite/bind.rs b/crates/perry-stdlib/src/sqlite/bind.rs index e472b12fa1..c7167cf732 100644 --- a/crates/perry-stdlib/src/sqlite/bind.rs +++ b/crates/perry-stdlib/src/sqlite/bind.rs @@ -510,6 +510,7 @@ pub(crate) unsafe fn node_sqlite_call_closure(callback: f64, args: &[f64]) -> f6 }; js_closure_call_array( closure as i64, + perry_runtime::closure::plain_call_receiver(), if args.is_empty() { std::ptr::null() } else { diff --git a/crates/perry-stdlib/src/sqlite/bun.rs b/crates/perry-stdlib/src/sqlite/bun.rs index e2f2dea61c..17c80825a1 100644 --- a/crates/perry-stdlib/src/sqlite/bun.rs +++ b/crates/perry-stdlib/src/sqlite/bun.rs @@ -209,6 +209,7 @@ pub unsafe extern "C" fn js_bun_sqlite_statement_finalize(stmt_handle: Handle) { unsafe extern "C" fn bun_sqlite_transaction_wrapper( wrapper: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, rest_value: f64, ) -> f64 { let db_handle = js_closure_get_capture_f64(wrapper, 0) as Handle; @@ -233,6 +234,7 @@ unsafe extern "C" fn bun_sqlite_transaction_wrapper( match perry_runtime::exception::js_call_catching(|| { js_closure_call_array( callback as i64, + perry_runtime::closure::plain_call_receiver(), if args.is_empty() { std::ptr::null() } else { diff --git a/crates/perry-stdlib/src/sqlite/dispatch.rs b/crates/perry-stdlib/src/sqlite/dispatch.rs index 4235c182d8..8ecc085b05 100644 --- a/crates/perry-stdlib/src/sqlite/dispatch.rs +++ b/crates/perry-stdlib/src/sqlite/dispatch.rs @@ -200,7 +200,10 @@ pub unsafe fn dispatch_node_sqlite_database_property( } } -pub(crate) extern "C" fn sql_tag_store_constructor_thunk(_closure: *const ClosureHeader) -> f64 { +pub(crate) extern "C" fn sql_tag_store_constructor_thunk( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { throw_illegal_constructor() } diff --git a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs index b454d6f7e5..b6a41619cb 100644 --- a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs +++ b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs @@ -485,7 +485,11 @@ pub(crate) unsafe extern "C" fn node_sqlite_changeset_filter( CStr::from_ptr(table).to_str().unwrap_or("") }; let table_value = JSValue::string_ptr(js_string_from_bytes(table.as_ptr(), table.len() as u32)); - let result = js_closure_call1(filter, f64::from_bits(table_value.bits())); + let result = js_closure_call1( + filter, + perry_runtime::closure::plain_call_receiver(), + f64::from_bits(table_value.bits()), + ); (perry_runtime::value::js_is_truthy(result) != 0) as c_int } @@ -500,6 +504,7 @@ pub(crate) unsafe extern "C" fn node_sqlite_changeset_conflict( }; let result = js_closure_call1( on_conflict, + perry_runtime::closure::plain_call_receiver(), f64::from_bits(JSValue::number(conflict as f64).bits()), ); let result = value_from_f64(result); diff --git a/crates/perry-stdlib/src/streams.rs b/crates/perry-stdlib/src/streams.rs index 9be233d55b..0e4479a9e5 100644 --- a/crates/perry-stdlib/src/streams.rs +++ b/crates/perry-stdlib/src/streams.rs @@ -25,6 +25,7 @@ //! into `desiredSize`) live in `streams/byob.rs` and the queue helpers on //! `ReadableStreamData` (#4915). +use perry_runtime::closure::JsThis; use perry_runtime::{ArrayHeader, ClosureHeader, JSValue, ObjectHeader, Promise, StringHeader}; use std::collections::{HashMap, VecDeque}; use std::sync::Mutex; @@ -53,11 +54,16 @@ extern "C" { #[link_name = "js_closure_alloc"] fn provider_js_closure_alloc(function: *const u8, capture_count: u32) -> *mut ClosureHeader; #[link_name = "js_closure_call0"] - fn provider_js_closure_call0(closure: *const ClosureHeader) -> f64; + fn provider_js_closure_call0(closure: *const ClosureHeader, this: JsThis) -> f64; #[link_name = "js_closure_call1"] - fn provider_js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64; + fn provider_js_closure_call1(closure: *const ClosureHeader, this: JsThis, arg0: f64) -> f64; #[link_name = "js_closure_call2"] - fn provider_js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64; + fn provider_js_closure_call2( + closure: *const ClosureHeader, + this: JsThis, + arg0: f64, + arg1: f64, + ) -> f64; #[link_name = "js_closure_get_capture_ptr"] fn provider_js_closure_get_capture_ptr(closure: *const ClosureHeader, index: u32) -> i64; #[link_name = "js_closure_set_capture_ptr"] @@ -86,11 +92,10 @@ extern "C" { #[link_name = "js_native_call_value"] fn provider_js_native_call_value( function: f64, + this: JsThis, arguments: *const f64, argument_count: usize, ) -> f64; - #[link_name = "js_implicit_this_set"] - fn provider_js_implicit_this_set(value: f64) -> f64; #[link_name = "js_ffi_root_scope_enter"] fn provider_js_ffi_root_scope_enter() -> usize; #[link_name = "js_ffi_root_push_nanbox"] @@ -173,16 +178,16 @@ fn js_closure_alloc(function: *const u8, capture_count: u32) -> *mut ClosureHead provider_call!(provider_js_closure_alloc(function, capture_count)) } -fn js_closure_call0(closure: *const ClosureHeader) -> f64 { - provider_call!(provider_js_closure_call0(closure)) +fn js_closure_call0(closure: *const ClosureHeader, this: JsThis) -> f64 { + provider_call!(provider_js_closure_call0(closure, this)) } -fn js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64 { - provider_call!(provider_js_closure_call1(closure, arg0)) +fn js_closure_call1(closure: *const ClosureHeader, this: JsThis, arg0: f64) -> f64 { + provider_call!(provider_js_closure_call1(closure, this, arg0)) } -fn js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64 { - provider_call!(provider_js_closure_call2(closure, arg0, arg1)) +fn js_closure_call2(closure: *const ClosureHeader, this: JsThis, arg0: f64, arg1: f64) -> f64 { + provider_call!(provider_js_closure_call2(closure, this, arg0, arg1)) } fn js_closure_get_capture_ptr(closure: *const ClosureHeader, index: u32) -> i64 { @@ -221,35 +226,22 @@ fn js_promise_mark_internally_handled(promise: *mut Promise) { provider_call!(provider_js_promise_mark_internally_handled(promise)) } -fn js_native_call_value(function: f64, arguments: *const f64, argument_count: usize) -> f64 { +/// Call `function` with `this` bound to `receiver` (a method call of a +/// function value, `function.call(receiver)`). +fn js_native_call_value( + function: f64, + receiver: f64, + arguments: *const f64, + argument_count: usize, +) -> f64 { provider_call!(provider_js_native_call_value( function, + perry_runtime::closure::JsThis::from_f64(receiver), arguments, argument_count )) } -fn js_implicit_this_set(value: f64) -> f64 { - provider_call!(provider_js_implicit_this_set(value)) -} - -/// Call `f` with `IMPLICIT_THIS` bound to `receiver`, restoring the displaced -/// value from a transient ROOT afterwards (#10490): it is the caller's -/// receiver, and `f` runs user code that an evacuating minor can move it -/// across. Uses the provider's root stack like every other runtime-owned -/// operation in this file. -fn with_implicit_this(receiver: f64, f: impl FnOnce() -> f64) -> f64 { - let base = provider_call!(provider_js_ffi_root_scope_enter()); - let previous = js_implicit_this_set(receiver); - let slot = provider_call!(provider_js_ffi_root_push_nanbox(previous.to_bits())); - let result = f(); - js_implicit_this_set(f64::from_bits(provider_call!( - provider_js_ffi_root_get_nanbox(slot) - ))); - provider_call!(provider_js_ffi_root_scope_exit(base)); - result -} - fn js_promise_new() -> *mut Promise { provider_call!(provider_js_promise_new()) } @@ -318,7 +310,11 @@ pub(crate) fn internal_promise() -> *mut Promise { unsafe fn try_call_stream_action(callback: i64, reason: f64) -> Result { perry_runtime::exception::catch_js_throw(|| { - js_closure_call1(callback as *const ClosureHeader, reason) + js_closure_call1( + callback as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + reason, + ) }) .map_err(f64::to_bits) } @@ -1082,11 +1078,18 @@ unsafe fn invoke_start(stream_id: usize) { } }; if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, controller); + js_closure_call1( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + controller, + ); } } -extern "C" fn readable_pull_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn readable_pull_microtask( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let stream_bits = js_closure_get_capture_ptr(closure, 0) as u64; let stream_id = f64::from_bits(stream_bits) as usize; @@ -1117,7 +1120,11 @@ extern "C" fn readable_pull_microtask(closure: *const ClosureHeader) -> f64 { pull_deferred_byte_chunk(stream_id, cb); f64::from_bits(TAG_UNDEFINED) } else { - js_closure_call1(cb as *const ClosureHeader, stream_id as f64) + js_closure_call1( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + stream_id as f64, + ) } }); match pull_outcome { @@ -1161,7 +1168,11 @@ fn readable_pull_settled_closure(func: *const u8, stream_id: usize) -> *mut Clos closure } -extern "C" fn readable_pull_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn readable_pull_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _value: f64, +) -> f64 { unsafe { let stream_id = js_closure_get_capture_ptr(closure, 0) as usize; if let Some(s) = READABLE_STREAMS.lock().unwrap().get_mut(&stream_id) { @@ -1172,7 +1183,11 @@ extern "C" fn readable_pull_fulfilled(closure: *const ClosureHeader, _value: f64 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn readable_pull_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn readable_pull_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { unsafe { let stream_id = js_closure_get_capture_ptr(closure, 0) as usize; let should_error = { @@ -1257,7 +1272,10 @@ unsafe fn maybe_pull_inner(stream_id: usize, force: bool) { } unsafe fn pull_deferred_byte_chunk(stream_id: usize, cb: i64) { - let chunk = js_closure_call0(cb as *const ClosureHeader); + let chunk = js_closure_call0( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + ); if chunk.to_bits() == TAG_UNDEFINED { let _ = js_readable_stream_controller_close(stream_id as f64); } else { @@ -1733,7 +1751,7 @@ pub(crate) unsafe fn has_async_iterator(value: f64) -> bool { unsafe fn call_symbol_async_iterator(value: f64) -> Option { let method = async_iterator_method(value)?; - let iterator = with_implicit_this(value, || js_native_call_value(method, std::ptr::null(), 0)); + let iterator = js_native_call_value(method, value, std::ptr::null(), 0); if iterator.to_bits() == TAG_UNDEFINED { None } else { @@ -1778,18 +1796,21 @@ unsafe fn await_maybe_promise(value: f64) -> SettledValue { } unsafe fn call_iterator_next(iterator: f64) -> Option { - let iter_ptr = js_nanbox_get_pointer(iterator); - if iter_ptr == 0 { + if js_nanbox_get_pointer(iterator) == 0 { return None; } - let iter_obj = iter_ptr as *const ObjectHeader; + // The key allocation can collect: the iterator is the receiver of the + // call below, so it is rooted across it and re-read. + let base = provider_call!(provider_js_ffi_root_scope_enter()); + let slot = provider_call!(provider_js_ffi_root_push_nanbox(iterator.to_bits())); let next_key = js_string_from_bytes(b"next".as_ptr(), 4); + let iterator = f64::from_bits(provider_call!(provider_js_ffi_root_get_nanbox(slot))); + provider_call!(provider_js_ffi_root_scope_exit(base)); + let iter_obj = js_nanbox_get_pointer(iterator) as *const ObjectHeader; let next_val = js_object_get_field_by_name(iter_obj, next_key); let next = f64::from_bits(next_val.bits()); if is_callable_value(next) { - let result = - with_implicit_this(iterator, || js_native_call_value(next, std::ptr::null(), 0)); - Some(result) + Some(js_native_call_value(next, iterator, std::ptr::null(), 0)) } else { Some(perry_runtime::object::js_native_call_method( iterator, @@ -2060,6 +2081,7 @@ pub unsafe extern "C" fn js_readable_stream_controller_enqueue( let size = if strategy_size_cb != 0 { let size = readable_strategy_size_to_number(js_closure_call1( strategy_size_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), chunk, )); if size.is_nan() || size < 0.0 || size.is_infinite() { @@ -2166,7 +2188,11 @@ pub unsafe extern "C" fn js_readable_stream_controller_desired_size(stream_handl // ReadableStreamDefaultReader FFI // ───────────────────────────────────────────────────────────────────── -extern "C" fn readable_from_chunk_fulfilled(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn readable_from_chunk_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + value: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -2178,7 +2204,11 @@ extern "C" fn readable_from_chunk_fulfilled(closure: *const ClosureHeader, value f64::from_bits(TAG_UNDEFINED) } -extern "C" fn readable_from_chunk_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn readable_from_chunk_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -2337,7 +2367,7 @@ fn resolved_done_promise() -> f64 { } fn closure_capture_value( - func: extern "C" fn(*const ClosureHeader) -> f64, + func: extern "C" fn(*const ClosureHeader, perry_runtime::closure::JsThis) -> f64, value: f64, ) -> *mut ClosureHeader { let fn_ptr = func as *const u8; @@ -2355,7 +2385,10 @@ fn closure_capture_value_get(closure: *const ClosureHeader) -> f64 { f64::from_bits(bits) } -extern "C" fn readable_stream_iterator_next(closure: *const ClosureHeader) -> f64 { +extern "C" fn readable_stream_iterator_next( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let reader = closure_capture_value_get(closure); if reader.to_bits() == TAG_UNDEFINED { return resolved_done_promise(); @@ -2363,7 +2396,10 @@ extern "C" fn readable_stream_iterator_next(closure: *const ClosureHeader) -> f6 unsafe { box_promise(js_reader_read(reader)) } } -extern "C" fn readable_stream_iterator_return(closure: *const ClosureHeader) -> f64 { +extern "C" fn readable_stream_iterator_return( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let reader = closure_capture_value_get(closure); if reader.to_bits() != TAG_UNDEFINED { unsafe { @@ -2373,7 +2409,10 @@ extern "C" fn readable_stream_iterator_return(closure: *const ClosureHeader) -> resolved_done_promise() } -extern "C" fn readable_stream_iterator_self(closure: *const ClosureHeader) -> f64 { +extern "C" fn readable_stream_iterator_self( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { closure_capture_value_get(closure) } diff --git a/crates/perry-stdlib/src/streams/byob.rs b/crates/perry-stdlib/src/streams/byob.rs index 14bc0713e8..b5209da248 100644 --- a/crates/perry-stdlib/src/streams/byob.rs +++ b/crates/perry-stdlib/src/streams/byob.rs @@ -432,7 +432,11 @@ pub unsafe extern "C" fn js_readable_stream_controller_byob_request(stream_handl f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()) } -extern "C" fn byob_request_respond(closure: *const ClosureHeader, bytes_written: f64) -> f64 { +extern "C" fn byob_request_respond( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + bytes_written: f64, +) -> f64 { unsafe { let stream_id = perry_runtime::closure::js_closure_get_capture_f64(closure, 0) as usize; let n = JSValue::from_bits(bytes_written.to_bits()).to_number(); @@ -503,7 +507,11 @@ extern "C" fn byob_request_respond(closure: *const ClosureHeader, bytes_written: f64::from_bits(TAG_UNDEFINED) } -extern "C" fn byob_request_respond_with_new_view(closure: *const ClosureHeader, view: f64) -> f64 { +extern "C" fn byob_request_respond_with_new_view( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + view: f64, +) -> f64 { unsafe { let stream_id = perry_runtime::closure::js_closure_get_capture_f64(closure, 0) as usize; let view_bits = view.to_bits(); diff --git a/crates/perry-stdlib/src/streams/pipe.rs b/crates/perry-stdlib/src/streams/pipe.rs index a9deef471e..1217e9addf 100644 --- a/crates/perry-stdlib/src/streams/pipe.rs +++ b/crates/perry-stdlib/src/streams/pipe.rs @@ -138,7 +138,10 @@ fn pipe_state_from_capture(closure: *const ClosureHeader) -> PipeState { } } -extern "C" fn readable_stream_pipe_to_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn readable_stream_pipe_to_microtask( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let r_id = capture_f64(closure, 0) as usize; let w_id = capture_f64(closure, 1) as usize; @@ -150,6 +153,7 @@ extern "C" fn readable_stream_pipe_to_microtask(closure: *const ClosureHeader) - extern "C" fn readable_stream_pipe_to_read_fulfilled( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, result: f64, ) -> f64 { unsafe { @@ -171,6 +175,7 @@ extern "C" fn readable_stream_pipe_to_read_fulfilled( extern "C" fn readable_stream_pipe_to_write_fulfilled( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, _value: f64, ) -> f64 { unsafe { @@ -187,6 +192,7 @@ extern "C" fn readable_stream_pipe_to_write_fulfilled( extern "C" fn readable_stream_pipe_to_close_fulfilled( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, _value: f64, ) -> f64 { unsafe { @@ -206,6 +212,7 @@ extern "C" fn readable_stream_pipe_to_close_fulfilled( extern "C" fn readable_stream_pipe_to_read_rejected( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, reason: f64, ) -> f64 { unsafe { @@ -228,6 +235,7 @@ extern "C" fn readable_stream_pipe_to_read_rejected( extern "C" fn readable_stream_pipe_to_write_rejected( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, reason: f64, ) -> f64 { unsafe { @@ -248,7 +256,10 @@ extern "C" fn readable_stream_pipe_to_write_rejected( f64::from_bits(TAG_UNDEFINED) } -extern "C" fn readable_stream_pipe_to_aborted(closure: *const ClosureHeader) -> f64 { +extern "C" fn readable_stream_pipe_to_aborted( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let promise = promise_from_capture(closure, 2); if perry_runtime::promise::js_promise_state(promise) != 0 { @@ -427,6 +438,7 @@ unsafe fn cancel_source_and_reject( extern "C" fn readable_stream_pipe_to_shutdown_fulfilled( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, _value: f64, ) -> f64 { unsafe { @@ -447,6 +459,7 @@ extern "C" fn readable_stream_pipe_to_shutdown_fulfilled( extern "C" fn readable_stream_pipe_to_shutdown_rejected( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, reason: f64, ) -> f64 { unsafe { @@ -869,7 +882,7 @@ pub unsafe extern "C" fn js_readable_stream_pipe_to( state.abort_listener, ); if perry_runtime::url::js_abort_signal_is_aborted(signal_ptr) != 0 { - readable_stream_pipe_to_aborted(listener); + readable_stream_pipe_to_aborted(listener, perry_runtime::closure::JsThis::UNDEFINED); return promise; } } @@ -934,7 +947,11 @@ mod tests { use super::*; use crate::streams::{alloc_readable, alloc_writable}; - extern "C" fn pending_abort_action(closure: *const ClosureHeader, _reason: f64) -> f64 { + extern "C" fn pending_abort_action( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _reason: f64, + ) -> f64 { let promise = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *mut Promise; box_promise(promise) diff --git a/crates/perry-stdlib/src/streams/strategy.rs b/crates/perry-stdlib/src/streams/strategy.rs index 3ad9b92e21..b8284fe6ba 100644 --- a/crates/perry-stdlib/src/streams/strategy.rs +++ b/crates/perry-stdlib/src/streams/strategy.rs @@ -15,12 +15,20 @@ use super::*; // node_submodules. /// `CountQueuingStrategy.prototype.size` — every chunk counts as 1. -extern "C" fn count_queuing_strategy_size(_c: *const ClosureHeader, _chunk: f64) -> f64 { +extern "C" fn count_queuing_strategy_size( + _c: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _chunk: f64, +) -> f64 { 1.0 } /// `ByteLengthQueuingStrategy.prototype.size` — `chunk.byteLength`. -extern "C" fn byte_length_queuing_strategy_size(_c: *const ClosureHeader, chunk: f64) -> f64 { +extern "C" fn byte_length_queuing_strategy_size( + _c: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + chunk: f64, +) -> f64 { // Mirror Node's `return chunk.byteLength`: the generic property getter // resolves `.byteLength` for both registered buffers/typed arrays and // plain `{ byteLength }` objects. @@ -31,7 +39,7 @@ extern "C" fn byte_length_queuing_strategy_size(_c: *const ClosureHeader, chunk: /// is the raw JSValue bits read from the caller's options object. unsafe fn build_queuing_strategy( hwm_bits: u64, - size_fn: extern "C" fn(*const ClosureHeader, f64) -> f64, + size_fn: extern "C" fn(*const ClosureHeader, perry_runtime::closure::JsThis, f64) -> f64, ) -> f64 { let obj = js_object_alloc(0, 2); let keys = js_array_alloc(2); diff --git a/crates/perry-stdlib/src/streams/tee.rs b/crates/perry-stdlib/src/streams/tee.rs index bbf1f7a034..b8bc0353af 100644 --- a/crates/perry-stdlib/src/streams/tee.rs +++ b/crates/perry-stdlib/src/streams/tee.rs @@ -235,9 +235,12 @@ pub(super) unsafe fn tee_source_enqueue( g.get(&id).map(|s| s.strategy_size_cb).unwrap_or(0) }; let size = if size_cb != 0 { - let size = super::readable_strategy_size_to_number( - perry_runtime::closure::js_closure_call1(size_cb as *const ClosureHeader, chunk), - ); + let size = + super::readable_strategy_size_to_number(perry_runtime::closure::js_closure_call1( + size_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + chunk, + )); if size.is_nan() || size < 0.0 || size.is_infinite() { super::throw_invalid_readable_strategy_size(id, size); } @@ -324,7 +327,10 @@ pub(super) unsafe fn tee_schedule_pull_demand(source: usize) { /// later. `TEE_PULLING` stays held across the hop (single-threaded microtask /// dispatch — the remove+insert below has no interleaving window), so /// coalescing against enqueue/close reroutes keeps working. -extern "C" fn tee_demand_hop(closure: *const ClosureHeader) -> f64 { +extern "C" fn tee_demand_hop( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let source = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; TEE_PULLING.lock().unwrap().remove(&source); @@ -335,7 +341,10 @@ extern "C" fn tee_demand_hop(closure: *const ClosureHeader) -> f64 { /// The extra tick a byte-stream tee's CHAINED pull pays before the next /// cycle (see the chain decision in `tee_pull_microtask`). -extern "C" fn tee_byte_chain_hop(closure: *const ClosureHeader) -> f64 { +extern "C" fn tee_byte_chain_hop( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let source = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; tee_schedule_pull(source); @@ -343,7 +352,10 @@ extern "C" fn tee_byte_chain_hop(closure: *const ClosureHeader) -> f64 { f64::from_bits(0x7FFC_0000_0000_0001) // TAG_UNDEFINED } -extern "C" fn tee_close_tick(closure: *const ClosureHeader) -> f64 { +extern "C" fn tee_close_tick( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let source = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; tee_close_branches(source); @@ -351,7 +363,10 @@ extern "C" fn tee_close_tick(closure: *const ClosureHeader) -> f64 { f64::from_bits(0x7FFC_0000_0000_0001) // TAG_UNDEFINED } -extern "C" fn tee_pull_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn tee_pull_microtask( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let undef = f64::from_bits(0x7FFC_0000_0000_0001); // TAG_UNDEFINED unsafe { let source = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; diff --git a/crates/perry-stdlib/src/streams/tests.rs b/crates/perry-stdlib/src/streams/tests.rs index 3a2889320d..a75a121d35 100644 --- a/crates/perry-stdlib/src/streams/tests.rs +++ b/crates/perry-stdlib/src/streams/tests.rs @@ -260,7 +260,6 @@ fn stream_runtime_owned_calls_cannot_bypass_provider_abi() { for forbidden in [ "perry_runtime::array::js_", "perry_runtime::closure::js_", - "perry_runtime::object::js_implicit_this_set", "perry_runtime::promise::js_", ] { assert!( @@ -423,3 +422,62 @@ fn buffered_tee_demand_skips_only_the_cold_source_hop() { streams.remove(&buffered); streams.remove(&empty); } + +/// Not a NaN-boxed value any call passes: "`next()` never ran". +const NEXT_NOT_CALLED: f64 = 0.5; + +/// Records the receiver it was called with in its own capture slot 0 (a slot +/// the collector sees), so no static holds a heap value. +extern "C" fn next_recording_this( + closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, +) -> f64 { + perry_runtime::closure::js_closure_set_capture_f64( + closure as *mut ClosureHeader, + 0, + this.as_f64(), + ); + f64::from_bits(TAG_UNDEFINED) +} + +/// An async-iterable source's `next()` is a METHOD call: the iterator is its +/// receiver. The body reads `this` only from its receiver parameter, so this +/// fails if `call_iterator_next` makes a plain call (the body would see +/// `undefined`). +#[test] +fn iterator_next_is_called_with_the_iterator_as_its_receiver() { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let fn_ptr = next_recording_this as *const u8; + js_register_closure_arity(fn_ptr, 0); + let next = scope.root_raw_mut_ptr(js_closure_alloc(fn_ptr, 1)); + perry_runtime::closure::js_closure_set_capture_f64( + next.get_raw_mut_ptr::(), + 0, + NEXT_NOT_CALLED, + ); + let iterator_obj = scope.root_raw_mut_ptr(js_object_alloc(0, 1)); + let key = js_string_from_bytes(b"next".as_ptr(), 4); + let next_value = f64::from_bits( + JSValue::pointer(next.get_raw_mut_ptr::() as *const u8).bits(), + ); + perry_runtime::object::js_object_set_field_by_name( + iterator_obj.get_raw_mut_ptr::(), + key, + next_value, + ); + let iterator = scope.root_nanbox_f64(f64::from_bits( + JSValue::object_ptr(iterator_obj.get_raw_mut_ptr::() as *mut u8).bits(), + )); + + let result = unsafe { call_iterator_next(iterator.get_nanbox_f64()) }; + assert!(result.is_some()); + let seen = perry_runtime::closure::js_closure_get_capture_f64( + next.get_raw_mut_ptr::(), + 0, + ); + assert_eq!( + seen.to_bits(), + iterator.get_nanbox_f64().to_bits(), + "next() must see the iterator as `this`" + ); +} diff --git a/crates/perry-stdlib/src/streams/transform.rs b/crates/perry-stdlib/src/streams/transform.rs index 5f82b200d9..dace897148 100644 --- a/crates/perry-stdlib/src/streams/transform.rs +++ b/crates/perry-stdlib/src/streams/transform.rs @@ -131,7 +131,11 @@ unsafe fn alloc_transform_stream_with_strategies( // transform/flush callbacks receive), so `controller.enqueue(c)` / // `controller.terminate()` / `controller.error(e)` act on the readable. if start_cb != 0 { - js_closure_call1(start_cb as *const ClosureHeader, readable_id as f64); + js_closure_call1( + start_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + readable_id as f64, + ); } id as f64 } @@ -281,7 +285,10 @@ pub(super) unsafe fn transform_write(writable_id: usize, chunk: f64) -> *mut Pro /// before the transformer runs (sink write algorithm → PerformTransform); /// teepipe2.js shows the transform output landing one tick later than a /// single-job deferral produces. Re-queue once, then run the transform. -extern "C" fn transform_write_job(closure: *const ClosureHeader) -> f64 { +extern "C" fn transform_write_job( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { { let job_fn = transform_write_job2 as *const u8; perry_runtime::closure::js_register_closure_arity(job_fn, 0); @@ -298,7 +305,10 @@ extern "C" fn transform_write_job(closure: *const ClosureHeader) -> f64 { f64::from_bits(TAG_UNDEFINED) } -extern "C" fn transform_write_job2(closure: *const ClosureHeader) -> f64 { +extern "C" fn transform_write_job2( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let transform_cb = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0); let readable_id = perry_runtime::closure::js_closure_get_capture_ptr(closure, 1) as usize; @@ -310,6 +320,7 @@ extern "C" fn transform_write_job2(closure: *const ClosureHeader) -> f64 { if transform_cb != 0 && readable_id != 0 { let ret = js_closure_call2( transform_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), chunk, readable_id as f64, ); @@ -523,7 +534,11 @@ unsafe fn transform_write_job_done(writable_id: usize) { /// The async transformer's returned promise fulfilled — settle the write with /// the same backpressure gate as the synchronous path. -extern "C" fn transform_write_settle_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn transform_write_settle_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _value: f64, +) -> f64 { unsafe { let readable_id = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; let promise = @@ -538,7 +553,11 @@ extern "C" fn transform_write_settle_fulfilled(closure: *const ClosureHeader, _v /// The async transformer's returned promise rejected — error the readable side /// and reject the write (spec: transformResultPromise rejection errors the /// TransformStream). -extern "C" fn transform_write_settle_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn transform_write_settle_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { unsafe { let readable_id = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as usize; let promise = @@ -657,6 +676,7 @@ mod backpressure_tests { extern "C" fn capture_transform( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, _chunk: f64, _controller: f64, ) -> f64 { @@ -816,6 +836,7 @@ unsafe fn perform_transform_close(writable_id: usize, promise: *mut Promise) { let flush_result = if !handled_native && flush_cb != 0 && readable_id != 0 { Some(js_closure_call1( flush_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), readable_id as f64, )) } else { @@ -931,7 +952,11 @@ unsafe fn error_transform_close( js_promise_reject(close_promise, f64::from_bits(reason_bits)); } -extern "C" fn transform_flush_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn transform_flush_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _value: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } @@ -945,7 +970,11 @@ extern "C" fn transform_flush_fulfilled(closure: *const ClosureHeader, _value: f f64::from_bits(TAG_UNDEFINED) } -extern "C" fn transform_flush_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn transform_flush_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } diff --git a/crates/perry-stdlib/src/streams/writable.rs b/crates/perry-stdlib/src/streams/writable.rs index 3d564e438f..459e54c894 100644 --- a/crates/perry-stdlib/src/streams/writable.rs +++ b/crates/perry-stdlib/src/streams/writable.rs @@ -53,7 +53,11 @@ pub unsafe extern "C" fn js_writable_stream_new_with_sink_type( // start → write → close. The controller arg is the stream handle. let start_cb = closure_from_bits(start_bits.to_bits()); if start_cb != 0 { - js_closure_call1(start_cb as *const ClosureHeader, id as f64); + js_closure_call1( + start_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + id as f64, + ); } id as f64 } @@ -79,7 +83,11 @@ pub unsafe extern "C" fn js_writable_stream_new_from_sink_object(sink: f64, hwm: ); let start_cb = stream_object_closure(sink, b"start"); if start_cb != 0 { - js_closure_call1(start_cb as *const ClosureHeader, id as f64); + js_closure_call1( + start_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + id as f64, + ); } id as f64 } @@ -273,7 +281,10 @@ fn writable_capture_promise(closure: *const ClosureHeader, idx: u32) -> *mut Pro perry_runtime::closure::js_closure_get_capture_ptr(closure, idx) as *mut Promise } -extern "C" fn writable_write_start_microtask(closure: *const ClosureHeader) -> f64 { +extern "C" fn writable_write_start_microtask( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { unsafe { let stream_id = writable_capture_usize(closure, 0); let writer_id = writable_capture_usize(closure, 1); @@ -291,7 +302,11 @@ extern "C" fn writable_write_start_microtask(closure: *const ClosureHeader) -> f f64::from_bits(TAG_UNDEFINED) } -extern "C" fn writable_write_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { +extern "C" fn writable_write_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _value: f64, +) -> f64 { unsafe { let stream_id = writable_capture_usize(closure, 0); let writer_id = writable_capture_usize(closure, 1); @@ -301,7 +316,11 @@ extern "C" fn writable_write_fulfilled(closure: *const ClosureHeader, _value: f6 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn writable_write_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn writable_write_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { unsafe { let stream_id = writable_capture_usize(closure, 0); let write_promise = writable_capture_promise(closure, 2); @@ -351,16 +370,31 @@ unsafe fn attach_writable_write_handlers( } unsafe fn try_call_writable_write(cb: i64, chunk: f64) -> Result { - perry_runtime::exception::catch_js_throw(|| js_closure_call1(cb as *const ClosureHeader, chunk)) - .map_err(f64::to_bits) + perry_runtime::exception::catch_js_throw(|| { + js_closure_call1( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + chunk, + ) + }) + .map_err(f64::to_bits) } unsafe fn try_call_writable_close(cb: i64) -> Result { - perry_runtime::exception::catch_js_throw(|| js_closure_call0(cb as *const ClosureHeader)) - .map_err(f64::to_bits) -} - -extern "C" fn writable_close_fulfilled(closure: *const ClosureHeader, _value: f64) -> f64 { + perry_runtime::exception::catch_js_throw(|| { + js_closure_call0( + cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + ) + }) + .map_err(f64::to_bits) +} + +extern "C" fn writable_close_fulfilled( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _value: f64, +) -> f64 { unsafe { let stream_id = writable_capture_usize(closure, 0); let close_promise = writable_capture_promise(closure, 1); @@ -369,7 +403,11 @@ extern "C" fn writable_close_fulfilled(closure: *const ClosureHeader, _value: f6 f64::from_bits(TAG_UNDEFINED) } -extern "C" fn writable_close_rejected(closure: *const ClosureHeader, reason: f64) -> f64 { +extern "C" fn writable_close_rejected( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + reason: f64, +) -> f64 { unsafe { let stream_id = writable_capture_usize(closure, 0); let close_promise = writable_capture_promise(closure, 1); @@ -540,9 +578,15 @@ pub(super) unsafe fn writable_stream_write( .map(|s| s.strategy_size_cb) .unwrap_or(0); let chunk_size = if size_cb != 0 { - let size = - JSValue::from_bits(js_closure_call1(size_cb as *const ClosureHeader, chunk).to_bits()) - .to_number(); + let size = JSValue::from_bits( + js_closure_call1( + size_cb as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), + chunk, + ) + .to_bits(), + ) + .to_number(); if size.is_nan() || size < 0.0 || size.is_infinite() { let message = invalid_size_message(size); throw_range_error_with_code(&message, "ERR_INVALID_ARG_VALUE"); diff --git a/crates/perry-stdlib/src/string_decoder.rs b/crates/perry-stdlib/src/string_decoder.rs index 8b2214aa52..7bcfc41a9f 100644 --- a/crates/perry-stdlib/src/string_decoder.rs +++ b/crates/perry-stdlib/src/string_decoder.rs @@ -192,8 +192,8 @@ unsafe fn string_decoder_constructor_value() -> f64 { ) } -unsafe fn this_string_decoder_handle() -> i64 { - let this_value = perry_runtime::object::js_implicit_this_get(); +unsafe fn this_string_decoder_handle(this: perry_runtime::closure::JsThis) -> i64 { + let this_value = this.as_f64(); let bits = this_value.to_bits(); let handle = if bits >> 48 == 0x7FFD { (bits & 0x0000_FFFF_FFFF_FFFF) as i64 @@ -213,42 +213,48 @@ unsafe fn this_string_decoder_handle() -> i64 { extern "C" fn string_decoder_proto_write( _closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, buf: f64, ) -> f64 { - unsafe { dispatch_string_decoder(this_string_decoder_handle(), "write", &[buf]) } + unsafe { dispatch_string_decoder(this_string_decoder_handle(this), "write", &[buf]) } } extern "C" fn string_decoder_proto_end( _closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, buf: f64, ) -> f64 { - unsafe { dispatch_string_decoder(this_string_decoder_handle(), "end", &[buf]) } + unsafe { dispatch_string_decoder(this_string_decoder_handle(this), "end", &[buf]) } } extern "C" fn string_decoder_proto_text( _closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, buf: f64, _offset: f64, ) -> f64 { - unsafe { dispatch_string_decoder(this_string_decoder_handle(), "write", &[buf]) } + unsafe { dispatch_string_decoder(this_string_decoder_handle(this), "write", &[buf]) } } extern "C" fn string_decoder_last_char_getter( _closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, ) -> f64 { - unsafe { dispatch_string_decoder_property(this_string_decoder_handle(), "lastChar") } + unsafe { dispatch_string_decoder_property(this_string_decoder_handle(this), "lastChar") } } extern "C" fn string_decoder_last_need_getter( _closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, ) -> f64 { - unsafe { dispatch_string_decoder_property(this_string_decoder_handle(), "lastNeed") } + unsafe { dispatch_string_decoder_property(this_string_decoder_handle(this), "lastNeed") } } extern "C" fn string_decoder_last_total_getter( _closure: *const perry_runtime::closure::ClosureHeader, + this: perry_runtime::closure::JsThis, ) -> f64 { - unsafe { dispatch_string_decoder_property(this_string_decoder_handle(), "lastTotal") } + unsafe { dispatch_string_decoder_property(this_string_decoder_handle(this), "lastTotal") } } pub unsafe fn string_decoder_prototype_value() -> f64 { diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index d0627dd4ce..681b8c0592 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -1165,7 +1165,12 @@ unsafe fn failed_server_socket(server_handle: i64, servername: Option) - socket_id } -extern "C" fn tls_sni_completion(closure: *const ClosureHeader, error: f64, context: f64) -> f64 { +extern "C" fn tls_sni_completion( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + error: f64, + context: f64, +) -> f64 { unsafe { let server_handle = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as i64; let hostname = value_to_string(perry_runtime::closure::js_closure_get_capture_f64( @@ -1251,6 +1256,7 @@ pub unsafe extern "C" fn js_tls_client_preflight( perry_runtime::closure::js_closure_set_capture_f64(completion, 1, nanbox_str(&servername)); js_closure_call2( sni_callback as *const ClosureHeader, + perry_runtime::closure::plain_call_receiver(), nanbox_str(&servername), js_nanbox_pointer(completion as i64), ); @@ -1286,21 +1292,22 @@ pub unsafe extern "C" fn js_tls_client_preflight( js_nanbox_pointer(string_array(&protocols) as i64), ); let callback_socket = nanbox_handle(socket_id); + // The rebind below can allocate a replacement closure: root the + // argument object across it and re-read it. + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let argument_h = scope.root_nanbox_f64(js_nanbox_pointer(argument as i64)); // Node invokes an object-literal ALPNCallback with the pending - // TLSSocket, not the closure's original options-object receiver. + // TLSSocket as `this`, not the closure's original options-object + // receiver. let rebound_callback = perry_runtime::closure::js_closure_unbox_callee_checked_rebind( nanbox_handle(alpn_callback), callback_socket, ); - // #10490: root the displaced `this` across the ALPNCallback (user code). - let this_scope = perry_runtime::gc::RuntimeHandleScope::new(); - let previous_this = this_scope - .root_nanbox_f64(perry_runtime::object::js_implicit_this_set(callback_socket)); - let selected = js_closure_call1( + let selected = perry_runtime::closure::js_closure_call1( rebound_callback as *const ClosureHeader, - js_nanbox_pointer(argument as i64), + perry_runtime::closure::JsThis::from_f64(callback_socket), + argument_h.get_nanbox_f64(), ); - perry_runtime::object::js_implicit_this_set(previous_this.get_nanbox_f64()); let selected = value_to_string(selected).map(String::into_bytes); if let Some(socket) = sockets().lock().unwrap().remove(&socket_id) { liveness::step(liveness::socket_keeps_alive(&socket), false); diff --git a/crates/perry-stdlib/src/tls/event_pump.rs b/crates/perry-stdlib/src/tls/event_pump.rs index c6998ef151..2a94ab441a 100644 --- a/crates/perry-stdlib/src/tls/event_pump.rs +++ b/crates/perry-stdlib/src/tls/event_pump.rs @@ -53,7 +53,7 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call0(cb); + js_closure_call0(cb, perry_runtime::closure::plain_call_receiver()); } } drain_once_listeners(server_id, "listening"); @@ -69,7 +69,11 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call1(cb, socket); + js_closure_call1( + cb, + perry_runtime::closure::plain_call_receiver(), + socket, + ); } } drain_once_listeners(server_id, event_name); @@ -90,7 +94,7 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call0(cb); + js_closure_call0(cb, perry_runtime::closure::plain_call_receiver()); } } if let Some(server) = servers().lock().unwrap().remove(&server_id) { @@ -109,7 +113,11 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call1(cb, err.get_nanbox_f64()); + js_closure_call1( + cb, + perry_runtime::closure::plain_call_receiver(), + err.get_nanbox_f64(), + ); } } drain_once_listeners(server_id, "error"); @@ -126,7 +134,12 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call2(cb, err.get_nanbox_f64(), socket.get_nanbox_f64()); + js_closure_call2( + cb, + perry_runtime::closure::plain_call_receiver(), + err.get_nanbox_f64(), + socket.get_nanbox_f64(), + ); } } drain_once_listeners(server_id, "tlsClientError"); @@ -141,7 +154,11 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call1(cb, data.get_nanbox_f64()); + js_closure_call1( + cb, + perry_runtime::closure::plain_call_receiver(), + data.get_nanbox_f64(), + ); } } drain_once_listeners(socket_id, "data"); @@ -155,7 +172,7 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call0(cb); + js_closure_call0(cb, perry_runtime::closure::plain_call_receiver()); } } drain_once_listeners(socket_id, "end"); @@ -169,7 +186,7 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call0(cb); + js_closure_call0(cb, perry_runtime::closure::plain_call_receiver()); } } if let Some(socket) = sockets().lock().unwrap().remove(&socket_id) { @@ -188,7 +205,11 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { for cb in callbacks { let cb = cb.get_raw_const_ptr::(); if !cb.is_null() { - js_closure_call1(cb, err.get_nanbox_f64()); + js_closure_call1( + cb, + perry_runtime::closure::plain_call_receiver(), + err.get_nanbox_f64(), + ); } } drain_once_listeners(socket_id, "error"); diff --git a/crates/perry-stdlib/src/webcrypto/digest.rs b/crates/perry-stdlib/src/webcrypto/digest.rs index d464638195..2674a9cf50 100644 --- a/crates/perry-stdlib/src/webcrypto/digest.rs +++ b/crates/perry-stdlib/src/webcrypto/digest.rs @@ -8,7 +8,10 @@ use super::*; /// continue an event-loop iteration early: Auth.js hashes its CSRF token with /// `subtle.digest`, so a Next.js Server Component's `await auth()` completed /// ahead of Node and reordered the React Flight (RSC) rows of the response. -extern "C" fn webcrypto_digest_settle(closure: *const perry_runtime::ClosureHeader) -> f64 { +extern "C" fn webcrypto_digest_settle( + closure: *const perry_runtime::ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let promise_bits = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as u64; let value_bits = perry_runtime::closure::js_closure_get_capture_ptr(closure, 1) as u64; // Slot 2 holds the remaining macrotask hops (raw i64). Node's threadpool diff --git a/crates/perry-stdlib/src/worker_threads.rs b/crates/perry-stdlib/src/worker_threads.rs index e3d56bca3e..57d67544fb 100644 --- a/crates/perry-stdlib/src/worker_threads.rs +++ b/crates/perry-stdlib/src/worker_threads.rs @@ -641,7 +641,10 @@ fn listener_once(options: f64) -> bool { perry_runtime::value::js_is_truthy(get_object_field(options, "once")) != 0 } -extern "C" fn worker_threads_noop0(_closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_threads_noop0( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { js_undefined() } @@ -754,7 +757,10 @@ fn queue_worker_threads_microtask() { perry_runtime::event_pump::js_notify_main_thread(); } -extern "C" fn worker_threads_channels_microtask(_closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_threads_channels_microtask( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { js_worker_threads_channels_process_pending(); js_undefined() } @@ -925,13 +931,11 @@ fn call_callback1(callback_bits: u64, this_bits: u64, arg: f64) { if closure.is_null() { return; } - // #10490: root the displaced `this` across the callback (user code). - let this_scope = perry_runtime::gc::RuntimeHandleScope::new(); - let prev_this = this_scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_set( - f64::from_bits(this_bits), - )); - perry_runtime::closure::js_closure_call1(closure, arg); - perry_runtime::object::js_implicit_this_set(prev_this.get_nanbox_f64()); + perry_runtime::closure::js_closure_call1( + closure, + perry_runtime::closure::JsThis(this_bits), + arg, + ); } fn object_event_handler(target_bits: u64, name: &str) -> Option { @@ -1023,11 +1027,21 @@ fn push_parent_event(parent: AgentId, event: WorkerEvent) { perry_runtime::event_pump::js_notify_main_thread(); } -extern "C" fn worker_on(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { +extern "C" fn worker_on( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { worker_add_listener(captured_worker_id(closure), event, callback, false, false) } -extern "C" fn worker_once(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { +extern "C" fn worker_once( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { worker_add_listener(captured_worker_id(closure), event, callback, true, false) } @@ -1036,6 +1050,7 @@ extern "C" fn worker_once(closure: *const ClosureHeader, event: f64, callback: f /// `MessageEvent` (with `.data`) for "message" events. extern "C" fn worker_add_event_listener( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -1045,10 +1060,11 @@ extern "C" fn worker_add_event_listener( /// `worker.removeEventListener(type, listener)`. extern "C" fn worker_remove_event_listener( closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { - worker_off(closure, event, callback) + worker_off(closure, this, event, callback) } fn worker_add_listener( @@ -1077,7 +1093,12 @@ fn worker_add_listener( js_undefined() } -extern "C" fn worker_off(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { +extern "C" fn worker_off( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { let worker_id = captured_worker_id(closure); let Some(event) = event_name(event) else { return js_undefined(); @@ -1092,7 +1113,11 @@ extern "C" fn worker_off(closure: *const ClosureHeader, event: f64, callback: f6 js_undefined() } -extern "C" fn worker_post_message(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn worker_post_message( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + value: f64, +) -> f64 { let worker_id = captured_worker_id(closure); let message = unsafe { serialize_nanbox_for_thread(value.to_bits()) }; let sender = WORKERS @@ -1106,11 +1131,17 @@ extern "C" fn worker_post_message(closure: *const ClosureHeader, value: f64) -> js_undefined() } -extern "C" fn worker_terminate(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_terminate( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_terminate_by_id(captured_worker_id(closure)) } -extern "C" fn worker_reload(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_reload( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_reload_by_id(captured_worker_id(closure)) } @@ -1127,7 +1158,10 @@ fn worker_reload_by_id(worker_id: u64) -> f64 { js_undefined() } -extern "C" fn worker_ref(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_ref( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_ref_by_id(captured_worker_id(closure)) } @@ -1139,7 +1173,10 @@ fn worker_ref_by_id(worker_id: u64) -> f64 { js_undefined() } -extern "C" fn worker_unref(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_unref( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_unref_by_id(captured_worker_id(closure)) } @@ -1151,7 +1188,10 @@ fn worker_unref_by_id(worker_id: u64) -> f64 { js_undefined() } -extern "C" fn worker_get_heap_statistics(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_get_heap_statistics( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_get_heap_statistics_by_id(captured_worker_id(closure)) } @@ -1159,7 +1199,11 @@ fn worker_get_heap_statistics_by_id(_worker_id: u64) -> f64 { resolved_promise_value(unsafe { js_v8_get_heap_statistics() }) } -extern "C" fn worker_cpu_usage(closure: *const ClosureHeader, prior: f64) -> f64 { +extern "C" fn worker_cpu_usage( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + prior: f64, +) -> f64 { worker_cpu_usage_by_id(captured_worker_id(closure), prior) } @@ -1167,7 +1211,11 @@ fn worker_cpu_usage_by_id(_worker_id: u64, prior: f64) -> f64 { resolved_promise_value(unsafe { js_process_cpu_usage(prior) }) } -extern "C" fn worker_get_heap_snapshot(closure: *const ClosureHeader, options: f64) -> f64 { +extern "C" fn worker_get_heap_snapshot( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + options: f64, +) -> f64 { worker_get_heap_snapshot_by_id(captured_worker_id(closure), options) } @@ -1175,7 +1223,10 @@ fn worker_get_heap_snapshot_by_id(_worker_id: u64, _options: f64) -> f64 { resolved_promise_value(worker_readable_stream_object()) } -extern "C" fn worker_start_cpu_profile(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_start_cpu_profile( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_start_cpu_profile_by_id(captured_worker_id(closure)) } @@ -1183,7 +1234,10 @@ fn worker_start_cpu_profile_by_id(_worker_id: u64) -> f64 { resolved_promise_value(worker_profile_handle(0)) } -extern "C" fn worker_start_heap_profile(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_start_heap_profile( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { worker_start_heap_profile_by_id(captured_worker_id(closure)) } diff --git a/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs b/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs index a6cd495871..d42b588904 100644 --- a/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs +++ b/crates/perry-stdlib/src/worker_threads/broadcast_channel.rs @@ -8,7 +8,11 @@ use super::*; -extern "C" fn broadcast_post_message(closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn broadcast_post_message( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + value: f64, +) -> f64 { let channel_id = port_id_from_closure(closure); let channel_name = BROADCAST_CHANNELS.with(|channels| { channels @@ -34,7 +38,10 @@ extern "C" fn broadcast_post_message(closure: *const ClosureHeader, value: f64) js_undefined() } -extern "C" fn broadcast_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn broadcast_close( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let channel_id = port_id_from_closure(closure); BROADCAST_CHANNELS.with(|channels| { if let Some(state) = channels.borrow_mut().get_mut(&channel_id) { @@ -46,7 +53,10 @@ extern "C" fn broadcast_close(closure: *const ClosureHeader) -> f64 { js_undefined() } -extern "C" fn broadcast_ref_or_unref(closure: *const ClosureHeader) -> f64 { +extern "C" fn broadcast_ref_or_unref( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let channel_id = port_id_from_closure(closure); BROADCAST_CHANNELS.with(|channels| match channels.borrow().get(&channel_id) { Some(state) => f64::from_bits(state.object_bits), @@ -56,6 +66,7 @@ extern "C" fn broadcast_ref_or_unref(closure: *const ClosureHeader) -> f64 { extern "C" fn broadcast_add_event_listener( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, options: f64, @@ -86,6 +97,7 @@ extern "C" fn broadcast_add_event_listener( extern "C" fn broadcast_remove_event_listener( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { diff --git a/crates/perry-stdlib/src/worker_threads/message_port.rs b/crates/perry-stdlib/src/worker_threads/message_port.rs index d0332b0b83..ea3afd3a40 100644 --- a/crates/perry-stdlib/src/worker_threads/message_port.rs +++ b/crates/perry-stdlib/src/worker_threads/message_port.rs @@ -97,7 +97,12 @@ pub(super) fn message_port_object(port_id: u64) -> *mut perry_runtime::object::O } /// port.postMessage(value) — deliver to the peer port's inbox (#3157). -extern "C" fn port_post_message(closure: *const ClosureHeader, value: f64, _transfer: f64) -> f64 { +extern "C" fn port_post_message( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + value: f64, + _transfer: f64, +) -> f64 { let port_id = port_id_from_closure(closure); if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { return js_worker_threads_post_message(value); @@ -127,12 +132,22 @@ extern "C" fn port_post_message(closure: *const ClosureHeader, value: f64, _tran } /// port.on(event, callback) / addListener (#3157). -extern "C" fn port_on(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { +extern "C" fn port_on( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { port_add_node_listener(closure, event, callback, false) } /// port.once(event, callback) (#6763). -extern "C" fn port_once(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { +extern "C" fn port_once( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { port_add_node_listener(closure, event, callback, true) } @@ -199,7 +214,12 @@ fn port_add_node_listener( } /// port.off(event) / removeListener (#3157). -extern "C" fn port_off(closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { +extern "C" fn port_off( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { let port_id = port_id_from_closure(closure); let event_name = string_value_to_string(event).unwrap_or_default(); if port_id == PARENT_PORT_HANDLE as u64 && CURRENT_WORKER_ID.with(|id| id.get()) != 0 { @@ -237,7 +257,11 @@ extern "C" fn port_off(closure: *const ClosureHeader, event: f64, callback: f64) js_undefined() } -extern "C" fn port_listener_count(closure: *const ClosureHeader, event: f64) -> f64 { +extern "C" fn port_listener_count( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + event: f64, +) -> f64 { let port_id = port_id_from_closure(closure); let event_name = string_value_to_string(event).unwrap_or_default(); MESSAGE_PORTS.with(|ports| { @@ -264,6 +288,7 @@ extern "C" fn port_listener_count(closure: *const ClosureHeader, event: f64) -> /// port.addEventListener(event, callback) (#3598). extern "C" fn port_add_event_listener( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, options: f64, @@ -318,6 +343,7 @@ extern "C" fn port_add_event_listener( /// port.removeEventListener(event, callback) (#3598). extern "C" fn port_remove_event_listener( closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -351,7 +377,10 @@ extern "C" fn port_remove_event_listener( } /// port.start() — enable delivery of queued messages to the listener (#3157). -extern "C" fn port_start(closure: *const ClosureHeader) -> f64 { +extern "C" fn port_start( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let port_id = port_id_from_closure(closure); MESSAGE_PORTS.with(|ports| { if let Some(state) = ports.borrow_mut().get_mut(&port_id) { @@ -361,7 +390,10 @@ extern "C" fn port_start(closure: *const ClosureHeader) -> f64 { js_undefined() } -extern "C" fn port_ref(closure: *const ClosureHeader) -> f64 { +extern "C" fn port_ref( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let port_id = port_id_from_closure(closure); let has_handler = MESSAGE_PORTS.with(|ports| { ports @@ -380,7 +412,10 @@ extern "C" fn port_ref(closure: *const ClosureHeader) -> f64 { js_undefined() } -extern "C" fn port_unref(closure: *const ClosureHeader) -> f64 { +extern "C" fn port_unref( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let port_id = port_id_from_closure(closure); let has_handler = MESSAGE_PORTS.with(|ports| { ports @@ -399,7 +434,10 @@ extern "C" fn port_unref(closure: *const ClosureHeader) -> f64 { js_undefined() } -extern "C" fn port_has_ref(closure: *const ClosureHeader) -> f64 { +extern "C" fn port_has_ref( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let port_id = port_id_from_closure(closure); let has_handler = MESSAGE_PORTS.with(|ports| { let ports = ports.borrow(); @@ -422,7 +460,10 @@ extern "C" fn port_has_ref(closure: *const ClosureHeader) -> f64 { } /// port.close() — mark closed and queue `close` events on both ends (#3157). -extern "C" fn port_close(closure: *const ClosureHeader) -> f64 { +extern "C" fn port_close( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let port_id = port_id_from_closure(closure); let peer_id = MESSAGE_PORTS.with(|ports| ports.borrow().get(&port_id).map(|state| state.peer)); MESSAGE_PORTS.with(|ports| { diff --git a/crates/perry-stdlib/src/worker_threads/parent_port.rs b/crates/perry-stdlib/src/worker_threads/parent_port.rs index e557031530..b9b98c4dba 100644 --- a/crates/perry-stdlib/src/worker_threads/parent_port.rs +++ b/crates/perry-stdlib/src/worker_threads/parent_port.rs @@ -63,6 +63,7 @@ pub(super) fn worker_parent_port_object() -> *mut perry_runtime::object::ObjectH extern "C" fn worker_parent_port_add_event_listener( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -72,6 +73,7 @@ extern "C" fn worker_parent_port_add_event_listener( extern "C" fn worker_parent_port_remove_event_listener( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -79,12 +81,17 @@ extern "C" fn worker_parent_port_remove_event_listener( js_worker_threads_parent_port_event_remove(event.to_bits() as i64, callback_ptr) } -extern "C" fn worker_parent_port_post_message(_closure: *const ClosureHeader, value: f64) -> f64 { +extern "C" fn worker_parent_port_post_message( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + value: f64, +) -> f64 { js_worker_threads_post_message(value) } extern "C" fn worker_parent_port_on( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -94,6 +101,7 @@ extern "C" fn worker_parent_port_on( extern "C" fn worker_parent_port_off( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, _callback: f64, ) -> f64 { diff --git a/crates/perry-stdlib/src/worker_threads/worker_pump.rs b/crates/perry-stdlib/src/worker_threads/worker_pump.rs index 056b18243a..b4e36b3b1f 100644 --- a/crates/perry-stdlib/src/worker_threads/worker_pump.rs +++ b/crates/perry-stdlib/src/worker_threads/worker_pump.rs @@ -26,7 +26,11 @@ pub(super) fn deliver_parent_port_message(message: &SerializedValue) { if let Some(callback_ptr) = MESSAGE_CALLBACK.with(|cb| *cb.borrow()) { let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call1(closure, value_h.get_nanbox_f64()); + perry_runtime::closure::js_closure_call1( + closure, + perry_runtime::closure::plain_call_receiver(), + value_h.get_nanbox_f64(), + ); } // Root the listener closures BEFORE allocating the `MessageEvent`: that @@ -59,7 +63,11 @@ pub(super) fn deliver_parent_port_message(message: &SerializedValue) { let callback_ptr = perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call1(closure, event_h.get_nanbox_f64()); + perry_runtime::closure::js_closure_call1( + closure, + perry_runtime::closure::plain_call_receiver(), + event_h.get_nanbox_f64(), + ); } } } @@ -180,7 +188,11 @@ pub extern "C" fn js_worker_threads_process_pending() -> i32 { // Call the message callback with the parsed value let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call1(closure, parsed); + perry_runtime::closure::js_closure_call1( + closure, + perry_runtime::closure::plain_call_receiver(), + parsed, + ); processed += 1; } } @@ -191,7 +203,10 @@ pub extern "C" fn js_worker_threads_process_pending() -> i32 { let close_cb = CLOSE_CALLBACK.with(|cb| cb.borrow_mut().take()); if let Some(callback_ptr) = close_cb { let closure = callback_ptr as *const ClosureHeader; - perry_runtime::closure::js_closure_call0(closure); + perry_runtime::closure::js_closure_call0( + closure, + perry_runtime::closure::plain_call_receiver(), + ); } } @@ -342,9 +357,16 @@ fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { arg_handle.as_ref().map(|h| h.get_nanbox_f64()) }; if let Some(arg) = call_arg { - perry_runtime::closure::js_closure_call1(closure, arg); + perry_runtime::closure::js_closure_call1( + closure, + perry_runtime::closure::plain_call_receiver(), + arg, + ); } else { - perry_runtime::closure::js_closure_call0(closure); + perry_runtime::closure::js_closure_call0( + closure, + perry_runtime::closure::plain_call_receiver(), + ); } } js_undefined() diff --git a/crates/perry-stdlib/src/worker_threads/worker_surface.rs b/crates/perry-stdlib/src/worker_threads/worker_surface.rs index e22abbde5b..1bdc334a72 100644 --- a/crates/perry-stdlib/src/worker_threads/worker_surface.rs +++ b/crates/perry-stdlib/src/worker_threads/worker_surface.rs @@ -284,6 +284,7 @@ pub(super) fn web_worker_global_handler(name: &str) -> Option { extern "C" fn web_worker_post_message( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, value: f64, _transfer: f64, ) -> f64 { @@ -292,6 +293,7 @@ extern "C" fn web_worker_post_message( extern "C" fn web_worker_add_event_listener( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, _options: f64, @@ -302,6 +304,7 @@ extern "C" fn web_worker_add_event_listener( extern "C" fn web_worker_remove_event_listener( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, event: f64, callback: f64, ) -> f64 { @@ -309,7 +312,10 @@ extern "C" fn web_worker_remove_event_listener( js_worker_threads_parent_port_event_remove(event.to_bits() as i64, callback_ptr) } -extern "C" fn web_worker_close(_closure: *const ClosureHeader) -> f64 { +extern "C" fn web_worker_close( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { CURRENT_WORKER_CLOSE_REQUESTED.with(|closed| closed.set(true)); js_undefined() } @@ -355,19 +361,21 @@ fn stream_listener_key(event: &str) -> String { format!("__perryWorkerStreamListener:{event}") } -fn stream_this() -> f64 { - perry_runtime::object::js_implicit_this_get() -} - -fn stream_register(event: f64, callback: f64) -> f64 { - let this = stream_this(); +fn stream_register(receiver: perry_runtime::closure::JsThis, event: f64, callback: f64) -> f64 { + let this = receiver.as_f64(); let Some(event) = string_value_to_string(event) else { return this; }; let key = stream_listener_key(&event); + // The listener array allocations can collect: root the receiver (and the + // callback being stored) across them and re-read both. + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let this_h = scope.root_nanbox_f64(this); + let callback_h = scope.root_nanbox_f64(callback); let arr = array_ptr_from_value(get_object_field_from_value(this, &key)) .unwrap_or_else(|| perry_runtime::array::js_array_alloc(0)); - let arr = perry_runtime::array::js_array_push_f64(arr, callback); + let arr = perry_runtime::array::js_array_push_f64(arr, callback_h.get_nanbox_f64()); + let this = this_h.get_nanbox_f64(); if let Some(obj) = object_ptr_from_value(this) { set_object_field( obj, @@ -378,8 +386,8 @@ fn stream_register(event: f64, callback: f64) -> f64 { this } -fn stream_emit_event(event: f64, arg: f64) -> f64 { - let this = stream_this(); +fn stream_emit_event(receiver: perry_runtime::closure::JsThis, event: f64, arg: f64) -> f64 { + let this = receiver.as_f64(); let Some(event) = string_value_to_string(event) else { return js_bool(false); }; @@ -392,33 +400,33 @@ fn stream_emit_event(event: f64, arg: f64) -> f64 { // listener allocates enough to trigger a moving minor collection // corrupts every read for the rest of this loop, not just one listener. // Root all three through one handle scope and re-read the current bits - // before every dispatch. - // - // #10490: the displaced `this` crosses every listener (user code) too. - // Root it ONCE in that same scope, before the first - // `js_implicit_this_set`, and restore it from that root rather than from - // a plain per-iteration local. + // before every dispatch. Each listener runs with the stream as `this`. let scope = perry_runtime::gc::RuntimeHandleScope::new(); let this_h = scope.root_nanbox_f64(this); let arr_h = scope.root_raw_mut_ptr(arr); let arg_h = scope.root_nanbox_f64(arg); - let prev_this = scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_get()); let len = perry_runtime::array::js_array_length(arr_h.get_raw_mut_ptr()); for i in 0..len { let callback = perry_runtime::array::js_array_get_f64(arr_h.get_raw_mut_ptr(), i); - perry_runtime::object::js_implicit_this_set(this_h.get_nanbox_f64()); unsafe { let args = [arg_h.get_nanbox_f64()]; - let _ = - perry_runtime::closure::js_native_call_value(callback, args.as_ptr(), args.len()); + let _ = perry_runtime::closure::js_native_call_value( + callback, + perry_runtime::closure::JsThis::from_f64(this_h.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ); } - perry_runtime::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } js_bool(len > 0) } -fn stream_remove_listener(event: f64, callback: f64) -> f64 { - let this = stream_this(); +fn stream_remove_listener( + receiver: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { + let this = receiver.as_f64(); let Some(event) = string_value_to_string(event) else { return this; }; @@ -426,14 +434,21 @@ fn stream_remove_listener(event: f64, callback: f64) -> f64 { let Some(arr) = array_ptr_from_value(get_object_field_from_value(this, &key)) else { return this; }; - let len = perry_runtime::array::js_array_length(arr); + // The filtered copy allocates: root the receiver, the old array and the + // callback compared against across it and re-read them. + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let this_h = scope.root_nanbox_f64(this); + let arr_h = scope.root_raw_mut_ptr(arr); + let callback_h = scope.root_nanbox_f64(callback); + let len = perry_runtime::array::js_array_length(arr_h.get_raw_mut_ptr()); let mut next = perry_runtime::array::js_array_alloc(len); for i in 0..len { - let value = perry_runtime::array::js_array_get_f64(arr, i); - if value.to_bits() != callback.to_bits() { + let value = perry_runtime::array::js_array_get_f64(arr_h.get_raw_mut_ptr(), i); + if value.to_bits() != callback_h.get_nanbox_f64().to_bits() { next = perry_runtime::array::js_array_push_f64(next, value); } } + let this = this_h.get_nanbox_f64(); if let Some(obj) = object_ptr_from_value(this) { set_object_field( obj, @@ -444,35 +459,70 @@ fn stream_remove_listener(event: f64, callback: f64) -> f64 { this } -extern "C" fn stream_on(_closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { - stream_register(event, callback) +extern "C" fn stream_on( + _closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { + stream_register(this, event, callback) } -extern "C" fn stream_emit(_closure: *const ClosureHeader, event: f64, arg: f64) -> f64 { - stream_emit_event(event, arg) +extern "C" fn stream_emit( + _closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, + event: f64, + arg: f64, +) -> f64 { + stream_emit_event(this, event, arg) } -extern "C" fn stream_off(_closure: *const ClosureHeader, event: f64, callback: f64) -> f64 { - stream_remove_listener(event, callback) +extern "C" fn stream_off( + _closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, + event: f64, + callback: f64, +) -> f64 { + stream_remove_listener(this, event, callback) } -extern "C" fn stream_this0(_closure: *const ClosureHeader) -> f64 { - stream_this() +extern "C" fn stream_this0( + _closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, +) -> f64 { + this.as_f64() } -extern "C" fn stream_this1(_closure: *const ClosureHeader, _arg: f64) -> f64 { - stream_this() +extern "C" fn stream_this1( + _closure: *const ClosureHeader, + this: perry_runtime::closure::JsThis, + _arg: f64, +) -> f64 { + this.as_f64() } -extern "C" fn stream_write(_closure: *const ClosureHeader, _chunk: f64, _encoding: f64) -> f64 { +extern "C" fn stream_write( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _chunk: f64, + _encoding: f64, +) -> f64 { js_bool(true) } -extern "C" fn stream_read(_closure: *const ClosureHeader, _size: f64) -> f64 { +extern "C" fn stream_read( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + _size: f64, +) -> f64 { js_null() } -extern "C" fn stream_pipe(_closure: *const ClosureHeader, dest: f64) -> f64 { +extern "C" fn stream_pipe( + _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, + dest: f64, +) -> f64 { dest } @@ -532,6 +582,7 @@ fn worker_writable_stream_object() -> f64 { extern "C" fn worker_event_loop_utilization( _closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, util1: f64, util2: f64, ) -> f64 { @@ -542,7 +593,10 @@ fn worker_profile_result(kind: &str) -> f64 { string_value(&format!("{{\"perryWorkerProfile\":\"{kind}\"}}")) } -extern "C" fn worker_profile_stop(closure: *const ClosureHeader) -> f64 { +extern "C" fn worker_profile_stop( + closure: *const ClosureHeader, + _this: perry_runtime::closure::JsThis, +) -> f64 { let kind_bits = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as u64; let kind = if kind_bits == 1 { "heap" } else { "cpu" }; resolved_promise_value(worker_profile_result(kind)) diff --git a/crates/perry-stdlib/src/zlib.rs b/crates/perry-stdlib/src/zlib.rs index 43e1b2eef3..e0311abee5 100644 --- a/crates/perry-stdlib/src/zlib.rs +++ b/crates/perry-stdlib/src/zlib.rs @@ -1535,7 +1535,11 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { for callback in callbacks { let callback = callback.get_raw_const_ptr::(); if !callback.is_null() { - js_closure_call1(callback, buffer.get_nanbox_f64()); + js_closure_call1( + callback, + perry_runtime::closure::plain_call_receiver(), + buffer.get_nanbox_f64(), + ); } } } @@ -1570,13 +1574,19 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { for callback in end_callbacks { let callback = callback.get_raw_const_ptr::(); if !callback.is_null() { - js_closure_call0(callback); + js_closure_call0( + callback, + perry_runtime::closure::plain_call_receiver(), + ); } } for callback in finish_callbacks { let callback = callback.get_raw_const_ptr::(); if !callback.is_null() { - js_closure_call0(callback); + js_closure_call0( + callback, + perry_runtime::closure::plain_call_receiver(), + ); } } for destination in destinations { @@ -1585,7 +1595,10 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { for callback in close_callbacks { let callback = callback.get_raw_const_ptr::(); if !callback.is_null() { - js_closure_call0(callback); + js_closure_call0( + callback, + perry_runtime::closure::plain_call_receiver(), + ); } } } @@ -1594,7 +1607,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { let callback = scope.root_raw_const_ptr(cb as *const ClosureHeader); let callback = callback.get_raw_const_ptr::(); if !callback.is_null() { - js_closure_call0(callback); + js_closure_call0(callback, perry_runtime::closure::plain_call_receiver()); } } ZlibEvent::OneShotCallback(cb, result, ids) => { @@ -1623,6 +1636,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { let buffer = scope.root_nanbox_f64(buf_f64); js_closure_call2( callback.get_raw_const_ptr::(), + perry_runtime::closure::plain_call_receiver(), f64::from_bits(JSValue::null().bits()), buffer.get_nanbox_f64(), ); @@ -1632,6 +1646,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { )); js_closure_call2( callback.get_raw_const_ptr::(), + perry_runtime::closure::plain_call_receiver(), error.get_nanbox_f64(), f64::from_bits(JSValue::undefined().bits()), ); @@ -1641,6 +1656,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { let error = scope.root_nanbox_f64(build_zlib_error(&msg)); js_closure_call2( callback.get_raw_const_ptr::(), + perry_runtime::closure::plain_call_receiver(), error.get_nanbox_f64(), f64::from_bits(JSValue::undefined().bits()), ); @@ -1667,7 +1683,11 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { for callback in callbacks { let callback = callback.get_raw_const_ptr::(); if !callback.is_null() { - js_closure_call1(callback, error.get_nanbox_f64()); + js_closure_call1( + callback, + perry_runtime::closure::plain_call_receiver(), + error.get_nanbox_f64(), + ); } } } diff --git a/crates/perry-ui-android/src/app.rs b/crates/perry-ui-android/src/app.rs index 103febeb3a..e792e279df 100644 --- a/crates/perry-ui-android/src/app.rs +++ b/crates/perry-ui-android/src/app.rs @@ -350,7 +350,7 @@ pub fn add_keyboard_shortcut(_key_ptr: *const u8, _modifiers: f64, _callback: f6 } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -419,7 +419,7 @@ pub fn handle_activate() { return; } unsafe { - js_closure_call1(ptr, 0.0); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, 0.0); } } }); @@ -435,7 +435,7 @@ pub fn handle_terminate() { return; } unsafe { - js_closure_call1(ptr, 0.0); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, 0.0); } } }); diff --git a/crates/perry-ui-android/src/callback.rs b/crates/perry-ui-android/src/callback.rs index e09d0959f8..ca66d257db 100644 --- a/crates/perry-ui-android/src/callback.rs +++ b/crates/perry-ui-android/src/callback.rs @@ -3,10 +3,17 @@ use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Mutex; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; - fn js_closure_call4(closure: *const u8, arg0: f64, arg1: f64, arg2: f64, arg3: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; + fn js_closure_call4( + closure: *const u8, + this: perry_ffi::JsThis, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + ) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -119,7 +126,7 @@ pub fn invoke0(key: i64) { key, closure_ptr, ); - js_closure_call0(closure_ptr); + js_closure_call0(closure_ptr, perry_ffi::JsThis::UNDEFINED); __android_log_print( 3, b"PerryCallback\0".as_ptr(), @@ -149,7 +156,7 @@ pub fn invoke1(key: i64, arg: f64) { // Unbox NaN-boxed closure — see invoke0 for details. let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } @@ -164,7 +171,7 @@ pub fn invoke2(key: i64, arg1: f64, arg2: f64) { // Unbox NaN-boxed closure — see invoke0 for details. let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call2(closure_ptr, arg1, arg2); + js_closure_call2(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg1, arg2); } } } @@ -180,7 +187,14 @@ pub fn invoke4(key: i64, arg0: f64, arg1: f64, arg2: f64, arg3: f64) { // Unbox NaN-boxed closure — see invoke0 for details. let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call4(closure_ptr, arg0, arg1, arg2, arg3); + js_closure_call4( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + arg0, + arg1, + arg2, + arg3, + ); } } } @@ -206,7 +220,7 @@ pub fn invoke_with_string_array(key: i64, paths: &[String]) { arr = js_array_push_f64(arr, nb_str); } let nb_arr = js_nanbox_pointer(arr as i64); - js_closure_call1(closure_ptr, nb_arr); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb_arr); } } } diff --git a/crates/perry-ui-android/src/dialog.rs b/crates/perry-ui-android/src/dialog.rs index 1b7b261d15..abbf2c2614 100644 --- a/crates/perry-ui-android/src/dialog.rs +++ b/crates/perry-ui-android/src/dialog.rs @@ -7,7 +7,7 @@ use jni::JValue; use perry_ffi::copy_string_from_raw as str_from_header; extern "C" { - fn js_closure_call1(closure: f64, arg: f64) -> f64; + fn js_closure_call1(closure: f64, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_string_from_bytes(ptr: *const u8, len: usize) -> *const u8; fn js_nanbox_string(ptr: *const u8) -> f64; } @@ -25,7 +25,7 @@ pub fn save_file_dialog( let s = unsafe { js_string_from_bytes(empty.as_ptr(), 0) }; let val = unsafe { js_nanbox_string(s) }; unsafe { - js_closure_call1(callback, val); + js_closure_call1(callback, perry_ffi::JsThis::UNDEFINED, val); } } } diff --git a/crates/perry-ui-android/src/drag_drop.rs b/crates/perry-ui-android/src/drag_drop.rs index 5e12619f87..c00bd5cb5f 100644 --- a/crates/perry-ui-android/src/drag_drop.rs +++ b/crates/perry-ui-android/src/drag_drop.rs @@ -77,8 +77,8 @@ extern "C" { fn js_array_push_f64(arr: *mut c_void, value: f64) -> *mut c_void; fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; // Converts an arbitrary JS value to a runtime `StringHeader` (typed here as // `*const u8` so it feeds `str_from_header`, the same way `clipboard.rs` @@ -279,7 +279,7 @@ pub extern "C" fn Java_com_perry_app_PerryBridge_nativeInvokeDropCallback( js_object_set_field_by_name(obj, js_key(b"urls"), js_nanbox_pointer(arr as i64)); } let payload = js_nanbox_pointer(obj as i64); - js_closure_call1(closure_ptr, payload); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, payload); } pump_microtasks(); }); @@ -312,7 +312,7 @@ fn drag_provider_payload(key: i64) -> Option { return None; } unsafe { - let ret = js_closure_call0(closure_ptr); + let ret = js_closure_call0(closure_ptr, perry_ffi::JsThis::UNDEFINED); if ret.to_bits() == TAG_UNDEFINED { return None; } diff --git a/crates/perry-ui-android/src/media_playback.rs b/crates/perry-ui-android/src/media_playback.rs index 57a09dba6d..3ec4df8361 100644 --- a/crates/perry-ui-android/src/media_playback.rs +++ b/crates/perry-ui-android/src/media_playback.rs @@ -36,8 +36,8 @@ static MEDIA_SESSION: OnceLock>> = OnceLock::new(); extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; fn js_string_from_bytes(ptr: *const u8, len: i32) -> i64; fn js_string_new_sso(data: *const u8, len: u32) -> f64; fn js_run_stdlib_pump(); @@ -803,7 +803,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -812,7 +816,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-android/src/pointer.rs b/crates/perry-ui-android/src/pointer.rs index db28bc7245..03a1c20a50 100644 --- a/crates/perry-ui-android/src/pointer.rs +++ b/crates/perry-ui-android/src/pointer.rs @@ -19,7 +19,7 @@ use crate::jni_bridge; use jni::JValue; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -49,7 +49,7 @@ pub extern "C" fn Java_com_perry_app_PerryBridge_nativeInvokePointerCallback( } unsafe { let pe = js_pointer_event_new(x, y, button.max(0) as u32, POINTER_TYPE_TOUCH); - js_closure_call1(closure_ptr, pe); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, pe); } } diff --git a/crates/perry-ui-android/src/state.rs b/crates/perry-ui-android/src/state.rs index 31122a4ee2..bb0a94ea43 100644 --- a/crates/perry-ui-android/src/state.rs +++ b/crates/perry-ui-android/src/state.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_get_string_pointer_unified(value: f64) -> *const u8; } @@ -239,7 +239,7 @@ pub fn state_set(handle: i64, value: f64) { // Do not use to_bits() as a raw pointer — leaves the 0x7ffd tag. let closure_ptr = unsafe { js_nanbox_get_pointer(callback_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } @@ -298,7 +298,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { // Closure f64 is raw pointer bits bitcast to f64, not NaN-boxed let closure_ptr = closure.to_bits() as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; // Widget handle returned as i64 bitcast to f64 let child_handle = child_f64.to_bits() as i64; widgets::add_child(container, child_handle); diff --git a/crates/perry-ui-android/src/widgets/lazyvstack.rs b/crates/perry-ui-android/src/widgets/lazyvstack.rs index 8b75bc3391..40dbb213b9 100644 --- a/crates/perry-ui-android/src/widgets/lazyvstack.rs +++ b/crates/perry-ui-android/src/widgets/lazyvstack.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: f64, arg: f64) -> f64; + fn js_closure_call1(closure: f64, this: perry_ffi::JsThis, arg: f64) -> f64; } struct LazyState { @@ -49,7 +49,8 @@ pub fn create(count: f64, render_closure: f64) -> i64 { // Render initial items let n = count as i64; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(render_closure, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(render_closure, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = child_f64.to_bits() as i64; if child_handle > 0 { super::add_child(container_handle, child_handle); @@ -71,7 +72,8 @@ pub fn update(handle: i64, count: i64) { // Re-render all items for i in 0..count { - let child_f64 = unsafe { js_closure_call1(closure, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = child_f64.to_bits() as i64; if child_handle > 0 { super::add_child(container, child_handle); diff --git a/crates/perry-ui-android/src/widgets/table.rs b/crates/perry-ui-android/src/widgets/table.rs index d4b5104947..ead75a9a74 100644 --- a/crates/perry-ui-android/src/widgets/table.rs +++ b/crates/perry-ui-android/src/widgets/table.rs @@ -6,7 +6,7 @@ use jni::{ }; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, row: f64, col: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, row: f64, col: f64) -> f64; fn js_string_from_bytes(bytes: *const u8, len: i64) -> *const u8; } fn with_table(handle: i64, default: R, f: impl FnOnce(&mut Env, &JObject) -> R) -> R { @@ -76,6 +76,7 @@ pub extern "system" fn Java_com_perry_app_PerryBridge_nativeTableRenderCell( let value = unsafe { js_closure_call2( js_nanbox_get_pointer(closure) as *const u8, + perry_ffi::JsThis::UNDEFINED, row as f64, col as f64, ) diff --git a/crates/perry-ui-android/src/widgets/webview.rs b/crates/perry-ui-android/src/widgets/webview.rs index 5c336838ed..c2d1d22f4f 100644 --- a/crates/perry-ui-android/src/widgets/webview.rs +++ b/crates/perry-ui-android/src/widgets/webview.rs @@ -22,7 +22,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; fn js_is_truthy(value: f64) -> i32; @@ -386,7 +386,7 @@ pub extern "system" fn Java_com_perry_app_PerryBridge_nativeWebViewShouldNavigat let result_cell = StdCell::new(f64::from_bits(0x7FFC_0000_0000_0001)); let result_cell_ref = &result_cell; let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let r = unsafe { js_closure_call1(closure_ptr, url_nb) }; + let r = unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb) }; result_cell_ref.set(r); })); let result = result_cell.get(); @@ -420,13 +420,13 @@ pub extern "system" fn Java_com_perry_app_PerryBridge_nativeWebViewLoaded( } let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let nb = nanbox_str(&url_str); - unsafe { js_closure_call1(closure_ptr, nb) }; + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb) }; })); }); } extern "C" { - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; } #[no_mangle] @@ -454,7 +454,14 @@ pub extern "system" fn Java_com_perry_app_PerryBridge_nativeWebViewError( } let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let msg_nb = nanbox_str(&msg); - unsafe { js_closure_call2(closure_ptr, code as f64, msg_nb) }; + unsafe { + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + code as f64, + msg_nb, + ) + }; })); }); } @@ -490,7 +497,7 @@ pub extern "system" fn Java_com_perry_app_PerryBridge_nativeWebViewEvalResult( } let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let nb = nanbox_str(&s); - unsafe { js_closure_call1(closure_ptr, nb) }; + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb) }; })); }); } diff --git a/crates/perry-ui-gtk4/src/app.rs b/crates/perry-ui-gtk4/src/app.rs index 30758956db..6980f1ad04 100644 --- a/crates/perry-ui-gtk4/src/app.rs +++ b/crates/perry-ui-gtk4/src/app.rs @@ -77,7 +77,7 @@ struct AppEntry { } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; @@ -352,7 +352,7 @@ pub fn app_run(_app_handle: i64) { if let Some(callback) = *cb.borrow() { let ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); @@ -385,7 +385,7 @@ pub fn app_run(_app_handle: i64) { if let Some(callback) = *cb.borrow() { let ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); @@ -667,7 +667,7 @@ fn install_shortcuts_on_window(window: &ApplicationWindow) { if let Some(callback) = matched { let closure_ptr = unsafe { js_nanbox_get_pointer(callback) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } glib::Propagation::Stop } else { @@ -718,7 +718,7 @@ fn install_timer(interval_ms: f64, callback: f64) { if let Some(callback) = callback { let ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } #[cfg(feature = "geisterhand")] diff --git a/crates/perry-ui-gtk4/src/audio.rs b/crates/perry-ui-gtk4/src/audio.rs index a755b5f06e..b498632443 100644 --- a/crates/perry-ui-gtk4/src/audio.rs +++ b/crates/perry-ui-gtk4/src/audio.rs @@ -207,7 +207,7 @@ extern "C" { fn js_string_from_bytes(ptr: *const u8, len: i32) -> i64; fn js_array_create() -> i64; fn js_array_push_f64(array_ptr: i64, value: f64); - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64); + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64); fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; } @@ -432,6 +432,11 @@ pub fn invoke_audio_callback(samples_ptr: *const f32, num_samples: usize) { let num_samples_val = num_samples as f64; unsafe { - js_closure_call2(callback_ptr, samples_val, num_samples_val); + js_closure_call2( + callback_ptr, + perry_ffi::JsThis::UNDEFINED, + samples_val, + num_samples_val, + ); } } diff --git a/crates/perry-ui-gtk4/src/camera.rs b/crates/perry-ui-gtk4/src/camera.rs index 74ea2a856c..0ca906e3d0 100644 --- a/crates/perry-ui-gtk4/src/camera.rs +++ b/crates/perry-ui-gtk4/src/camera.rs @@ -14,7 +14,13 @@ thread_local! { } extern "C" { - fn js_closure_call3(closure: *const u8, arg1: f64, arg2: f64, arg3: f64) -> f64; + fn js_closure_call3( + closure: *const u8, + this: perry_ffi::JsThis, + arg1: f64, + arg2: f64, + arg3: f64, + ) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; } @@ -255,6 +261,7 @@ fn schedule_frame_processing(handle: i64) { let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; js_closure_call3( closure_ptr, + perry_ffi::JsThis::UNDEFINED, js_nanbox_pointer(frame_bytes.as_ptr() as i64), w as f64, h as f64, diff --git a/crates/perry-ui-gtk4/src/dialog.rs b/crates/perry-ui-gtk4/src/dialog.rs index dc6b1d4068..d5cf73728a 100644 --- a/crates/perry-ui-gtk4/src/dialog.rs +++ b/crates/perry-ui-gtk4/src/dialog.rs @@ -2,7 +2,7 @@ use gtk4::prelude::*; use gtk4::{FileChooserAction, FileChooserDialog, ResponseType, Window}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -42,7 +42,7 @@ pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_typ unsafe { js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64) }; let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; unsafe { - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); } dialog.close(); return; @@ -50,7 +50,11 @@ pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_typ } } unsafe { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } dialog.close(); }); @@ -109,7 +113,7 @@ pub fn alert(title_ptr: *const u8, message_ptr: *const u8, buttons_ptr: *const u }; let closure_ptr = unsafe { js_nanbox_get_pointer(callback_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, index); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, index); } dialog.close(); }); diff --git a/crates/perry-ui-gtk4/src/drag_drop.rs b/crates/perry-ui-gtk4/src/drag_drop.rs index 7e0b663b4a..81dd546313 100644 --- a/crates/perry-ui-gtk4/src/drag_drop.rs +++ b/crates/perry-ui-gtk4/src/drag_drop.rs @@ -44,8 +44,8 @@ use std::collections::HashMap; use std::ffi::c_void; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; @@ -105,7 +105,7 @@ unsafe fn call_provider(cb: f64) -> Option { if p.is_null() { return None; } - let ret = js_closure_call0(p); + let ret = js_closure_call0(p, perry_ffi::JsThis::UNDEFINED); let sh = js_jsvalue_to_string(ret); if sh.is_null() { None @@ -216,7 +216,7 @@ fn deliver_drop(handle: i64, value: &glib::Value) { let payload = js_nanbox_pointer(obj as i64); let cb_ptr = js_nanbox_get_pointer(cb) as *const u8; - js_closure_call1(cb_ptr, payload); + js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, payload); } } diff --git a/crates/perry-ui-gtk4/src/file_dialog.rs b/crates/perry-ui-gtk4/src/file_dialog.rs index 35f5ceca17..1d2710633f 100644 --- a/crates/perry-ui-gtk4/src/file_dialog.rs +++ b/crates/perry-ui-gtk4/src/file_dialog.rs @@ -2,7 +2,7 @@ use gtk4::prelude::*; use gtk4::{FileChooserAction, FileChooserDialog, ResponseType, Window}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -62,7 +62,7 @@ fn open_dialog_with_action( unsafe { js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64) }; let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; unsafe { - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); } dialog.close(); return; @@ -72,7 +72,11 @@ fn open_dialog_with_action( // User cancelled or no file selected unsafe { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } dialog.close(); }); diff --git a/crates/perry-ui-gtk4/src/issue_552_stub.rs b/crates/perry-ui-gtk4/src/issue_552_stub.rs index 2fc3d23ca6..41951f2989 100644 --- a/crates/perry-ui-gtk4/src/issue_552_stub.rs +++ b/crates/perry-ui-gtk4/src/issue_552_stub.rs @@ -6,7 +6,7 @@ //! picker callback receives an empty array. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -30,7 +30,7 @@ pub extern "C" fn perry_system_geolocation_get_current(_on_success: f64, on_erro } let nb = unsupported_string(); let cb = js_nanbox_get_pointer(on_error) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -47,7 +47,7 @@ pub extern "C" fn perry_system_geolocation_request_permission(callback: f64) { unsafe { let nb = unsupported_string(); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -61,7 +61,7 @@ pub extern "C" fn perry_system_image_picker_pick( let arr = js_array_alloc(0); let nb = js_nanbox_pointer(arr as i64); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } diff --git a/crates/perry-ui-gtk4/src/location.rs b/crates/perry-ui-gtk4/src/location.rs index 38bcfbb42f..f278d25a7f 100644 --- a/crates/perry-ui-gtk4/src/location.rs +++ b/crates/perry-ui-gtk4/src/location.rs @@ -8,7 +8,7 @@ use gtk4::glib; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -27,10 +27,10 @@ pub fn request_location(callback: f64) { let ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; match result { Some((lat, lon)) => unsafe { - js_closure_call2(ptr, lat, lon); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, lat, lon); }, None => unsafe { - js_closure_call2(ptr, f64::NAN, f64::NAN); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, f64::NAN, f64::NAN); }, } }); diff --git a/crates/perry-ui-gtk4/src/media_playback.rs b/crates/perry-ui-gtk4/src/media_playback.rs index 701042a0f5..1e583a5a6d 100644 --- a/crates/perry-ui-gtk4/src/media_playback.rs +++ b/crates/perry-ui-gtk4/src/media_playback.rs @@ -30,8 +30,8 @@ use std::sync::Arc; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; fn js_string_from_bytes(data: *const u8, len: i32) -> i64; fn js_string_new_sso(data: *const u8, len: u32) -> f64; fn js_run_stdlib_pump(); @@ -502,7 +502,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -511,7 +515,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-gtk4/src/menu.rs b/crates/perry-ui-gtk4/src/menu.rs index 838f24b324..56dca9e4c6 100644 --- a/crates/perry-ui-gtk4/src/menu.rs +++ b/crates/perry-ui-gtk4/src/menu.rs @@ -48,7 +48,7 @@ pub(crate) fn scan_gtk4_menu_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_> } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -231,7 +231,10 @@ fn build_gio_menu(menu_handle: i64, app: >k4::Application) -> gio::Menu { if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + ); } } }); @@ -422,7 +425,7 @@ pub fn set_context_menu(widget_handle: i64, menu_handle: i64) { if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-gtk4/src/network_stub.rs b/crates/perry-ui-gtk4/src/network_stub.rs index 0ca620f5a8..a29356026e 100644 --- a/crates/perry-ui-gtk4/src/network_stub.rs +++ b/crates/perry-ui-gtk4/src/network_stub.rs @@ -7,7 +7,7 @@ //! true reachability detection should target iOS / macOS / Android. extern "C" { - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -28,7 +28,12 @@ pub extern "C" fn perry_system_network_get_status(callback: f64) { if cb.is_null() { return; } - js_closure_call2(cb, f64::from_bits(TAG_TRUE), nb_str("unknown")); + js_closure_call2( + cb, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(TAG_TRUE), + nb_str("unknown"), + ); } } diff --git a/crates/perry-ui-gtk4/src/state.rs b/crates/perry-ui-gtk4/src/state.rs index 3035eeefb3..267e6c065c 100644 --- a/crates/perry-ui-gtk4/src/state.rs +++ b/crates/perry-ui-gtk4/src/state.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -260,7 +260,7 @@ pub fn state_set(handle: i64, value: f64) { for closure_f64 in callbacks_snapshot { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } @@ -328,7 +328,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; widgets::add_child(container, child_handle); } diff --git a/crates/perry-ui-gtk4/src/toolbar.rs b/crates/perry-ui-gtk4/src/toolbar.rs index fd22c7d77f..d13db9bde5 100644 --- a/crates/perry-ui-gtk4/src/toolbar.rs +++ b/crates/perry-ui-gtk4/src/toolbar.rs @@ -18,7 +18,7 @@ pub(crate) fn scan_gtk4_toolbar_gc_roots(visitor: &mut perry_ffi::GcRootVisitor< } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -69,7 +69,7 @@ pub fn add_item(toolbar_handle: i64, label_ptr: *const u8, icon_ptr: *const u8, if let Some(closure_f64) = closure_f64 { let ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-gtk4/src/tray.rs b/crates/perry-ui-gtk4/src/tray.rs index 6b7ded95e5..7a6806c206 100644 --- a/crates/perry-ui-gtk4/src/tray.rs +++ b/crates/perry-ui-gtk4/src/tray.rs @@ -54,7 +54,7 @@ use ksni::{Handle, MenuItem, ToolTip, TrayMethods}; use std::sync::{Arc, Mutex, OnceLock}; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -176,7 +176,7 @@ fn invoke_callback_on_main(callback: f64) { let ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; if !ptr.is_null() { unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs b/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs index 4cffe56991..3a725b178d 100644 --- a/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -114,7 +114,7 @@ pub fn add_item(handle: i64, icon_ptr: *const u8, label_ptr: *const u8) { if on_select != 0.0 { unsafe { let ptr = js_nanbox_get_pointer(on_select) as *const u8; - js_closure_call1(ptr, item_index as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, item_index as f64); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/button.rs b/crates/perry-ui-gtk4/src/widgets/button.rs index 0ccb1cdece..2b624a69c4 100644 --- a/crates/perry-ui-gtk4/src/widgets/button.rs +++ b/crates/perry-ui-gtk4/src/widgets/button.rs @@ -18,7 +18,7 @@ pub(crate) fn scan_gtk4_button_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<' } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -47,7 +47,7 @@ pub fn create(label_ptr: *const u8, on_press: f64) -> i64 { if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/calendar.rs b/crates/perry-ui-gtk4/src/widgets/calendar.rs index 10a8dd27e6..36d7cbc915 100644 --- a/crates/perry-ui-gtk4/src/widgets/calendar.rs +++ b/crates/perry-ui-gtk4/src/widgets/calendar.rs @@ -12,7 +12,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -54,7 +54,7 @@ pub fn create(year: i64, month: i64, on_change: f64) -> i64 { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } }); } diff --git a/crates/perry-ui-gtk4/src/widgets/combobox.rs b/crates/perry-ui-gtk4/src/widgets/combobox.rs index 40a417f364..f35b6f7c0d 100644 --- a/crates/perry-ui-gtk4/src/widgets/combobox.rs +++ b/crates/perry-ui-gtk4/src/widgets/combobox.rs @@ -15,7 +15,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -33,7 +33,7 @@ fn fire_change(callback: f64, text: &str) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } diff --git a/crates/perry-ui-gtk4/src/widgets/command_palette.rs b/crates/perry-ui-gtk4/src/widgets/command_palette.rs index a075cfb798..1ec18c82f3 100644 --- a/crates/perry-ui-gtk4/src/widgets/command_palette.rs +++ b/crates/perry-ui-gtk4/src/widgets/command_palette.rs @@ -15,7 +15,7 @@ use std::cell::RefCell; use std::rc::Rc; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -205,7 +205,7 @@ pub fn show() { if on_run != 0.0 { unsafe { let closure_ptr = js_nanbox_get_pointer(on_run) as *const u8; - js_closure_call0(closure_ptr); + js_closure_call0(closure_ptr, perry_ffi::JsThis::UNDEFINED); } } hide(); diff --git a/crates/perry-ui-gtk4/src/widgets/date_picker.rs b/crates/perry-ui-gtk4/src/widgets/date_picker.rs index d68753cb0c..2f599315d6 100644 --- a/crates/perry-ui-gtk4/src/widgets/date_picker.rs +++ b/crates/perry-ui-gtk4/src/widgets/date_picker.rs @@ -13,7 +13,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -55,7 +55,7 @@ pub fn create(year: i64, month: i64, on_change: f64) -> i64 { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } }); } diff --git a/crates/perry-ui-gtk4/src/widgets/image_gallery.rs b/crates/perry-ui-gtk4/src/widgets/image_gallery.rs index ded036f81e..1e9181ac50 100644 --- a/crates/perry-ui-gtk4/src/widgets/image_gallery.rs +++ b/crates/perry-ui-gtk4/src/widgets/image_gallery.rs @@ -8,7 +8,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -70,7 +70,7 @@ pub fn create(on_index_change: f64) -> i64 { if cb != 0.0 { unsafe { let ptr = js_nanbox_get_pointer(cb) as *const u8; - js_closure_call1(ptr, new_index as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, new_index as f64); } } } diff --git a/crates/perry-ui-gtk4/src/widgets/lazyvstack.rs b/crates/perry-ui-gtk4/src/widgets/lazyvstack.rs index e0399716df..12ff08e089 100644 --- a/crates/perry-ui-gtk4/src/widgets/lazyvstack.rs +++ b/crates/perry-ui-gtk4/src/widgets/lazyvstack.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -44,7 +44,8 @@ pub fn create(count: f64, render_closure: f64) -> i64 { let closure_ptr = unsafe { js_nanbox_get_pointer(render_closure) } as *const u8; let n = count as i64; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; if let Some(child) = super::get_widget(child_handle) { if child.parent().is_some() { @@ -79,7 +80,9 @@ pub fn update(handle: i64, count: i64) { // Re-render let closure_ptr = unsafe { js_nanbox_get_pointer(state.render_closure) } as *const u8; for i in 0..count { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = unsafe { + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) + }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; if let Some(child) = super::get_widget(child_handle) { if child.parent().is_some() { diff --git a/crates/perry-ui-gtk4/src/widgets/mod.rs b/crates/perry-ui-gtk4/src/widgets/mod.rs index 02828b4de3..287784e029 100644 --- a/crates/perry-ui-gtk4/src/widgets/mod.rs +++ b/crates/perry-ui-gtk4/src/widgets/mod.rs @@ -458,7 +458,7 @@ pub fn set_background_gradient( /// don't satisfy `=== true` checks. pub fn set_on_hover(handle: i64, callback: f64) { extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } // Perry's NaN-box payload for `true` / `false`. Mirrors @@ -472,14 +472,14 @@ pub fn set_on_hover(handle: i64, callback: f64) { motion.connect_enter(move |_ctrl, _x, _y| { let ptr = unsafe { js_nanbox_get_pointer(cb) } as *const u8; unsafe { - js_closure_call1(ptr, f64::from_bits(TAG_TRUE)); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, f64::from_bits(TAG_TRUE)); } }); let cb2 = callback; motion.connect_leave(move |_ctrl| { let ptr = unsafe { js_nanbox_get_pointer(cb2) } as *const u8; unsafe { - js_closure_call1(ptr, f64::from_bits(TAG_FALSE)); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, f64::from_bits(TAG_FALSE)); } }); widget.add_controller(motion); @@ -489,7 +489,7 @@ pub fn set_on_hover(handle: i64, callback: f64) { /// Set a double-click callback on a widget. pub fn set_on_double_click(handle: i64, callback: f64) { extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } if let Some(widget) = get_widget(handle) { @@ -500,7 +500,7 @@ pub fn set_on_double_click(handle: i64, callback: f64) { if n_press == 2 { let ptr = unsafe { js_nanbox_get_pointer(cb) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); @@ -511,7 +511,7 @@ pub fn set_on_double_click(handle: i64, callback: f64) { /// Set a single-click callback on a widget. pub fn set_on_click(handle: i64, callback: f64) { extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } if let Some(widget) = get_widget(handle) { @@ -522,7 +522,7 @@ pub fn set_on_click(handle: i64, callback: f64) { if n_press == 1 { let ptr = unsafe { js_nanbox_get_pointer(cb) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); @@ -542,7 +542,7 @@ pub fn set_on_click(handle: i64, callback: f64) { /// convention `0`/`1`/`2` (web `MouseEvent.button`). pub fn set_on_mouse_down(handle: i64, callback: f64) { extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -567,7 +567,7 @@ pub fn set_on_mouse_down(handle: i64, callback: f64) { } let pe = unsafe { js_pointer_event_new(x, y, js_button, 0) }; unsafe { - js_closure_call1(ptr, pe); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, pe); } }); widget.add_controller(gesture); @@ -576,7 +576,7 @@ pub fn set_on_mouse_down(handle: i64, callback: f64) { pub fn set_on_mouse_up(handle: i64, callback: f64) { extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -600,7 +600,7 @@ pub fn set_on_mouse_up(handle: i64, callback: f64) { } let pe = unsafe { js_pointer_event_new(x, y, js_button, 0) }; unsafe { - js_closure_call1(ptr, pe); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, pe); } }); widget.add_controller(gesture); @@ -609,7 +609,7 @@ pub fn set_on_mouse_up(handle: i64, callback: f64) { pub fn set_on_mouse_move(handle: i64, callback: f64) { extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -625,7 +625,7 @@ pub fn set_on_mouse_move(handle: i64, callback: f64) { // per the web `MouseEvent.button` spec for mousemove. let pe = unsafe { js_pointer_event_new(x, y, 0, 0) }; unsafe { - js_closure_call1(ptr, pe); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, pe); } }); widget.add_controller(motion); diff --git a/crates/perry-ui-gtk4/src/widgets/picker.rs b/crates/perry-ui-gtk4/src/widgets/picker.rs index b44abce14c..3a643254bc 100644 --- a/crates/perry-ui-gtk4/src/widgets/picker.rs +++ b/crates/perry-ui-gtk4/src/widgets/picker.rs @@ -21,7 +21,7 @@ pub(crate) fn scan_gtk4_picker_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<' } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -51,7 +51,11 @@ pub fn create(_label_ptr: *const u8, on_change: f64, _style: i64) -> i64 { let selected = dd.selected() as f64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, selected); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + selected, + ); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/rich_text.rs b/crates/perry-ui-gtk4/src/widgets/rich_text.rs index ee06f33c5e..da7b7b16f9 100644 --- a/crates/perry-ui-gtk4/src/widgets/rich_text.rs +++ b/crates/perry-ui-gtk4/src/widgets/rich_text.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -80,7 +80,7 @@ pub fn create(width: f64, height: f64, on_change: f64) -> i64 { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } }); } diff --git a/crates/perry-ui-gtk4/src/widgets/scrollview.rs b/crates/perry-ui-gtk4/src/widgets/scrollview.rs index b770639e15..c49ef8ccc5 100644 --- a/crates/perry-ui-gtk4/src/widgets/scrollview.rs +++ b/crates/perry-ui-gtk4/src/widgets/scrollview.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -146,7 +146,7 @@ pub fn set_scroll_end_callback(scroll_handle: i64, callback: f64, threshold_px: if should_fire && closure != 0.0 { unsafe { let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/securefield.rs b/crates/perry-ui-gtk4/src/widgets/securefield.rs index df7abcbdac..ca5bda58de 100644 --- a/crates/perry-ui-gtk4/src/widgets/securefield.rs +++ b/crates/perry-ui-gtk4/src/widgets/securefield.rs @@ -17,7 +17,7 @@ pub(crate) fn scan_gtk4_securefield_gc_roots(visitor: &mut perry_ffi::GcRootVisi } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -53,7 +53,11 @@ pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + nanboxed, + ); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/slider.rs b/crates/perry-ui-gtk4/src/widgets/slider.rs index 9e6bb305ea..83193da2e6 100644 --- a/crates/perry-ui-gtk4/src/widgets/slider.rs +++ b/crates/perry-ui-gtk4/src/widgets/slider.rs @@ -18,7 +18,7 @@ pub(crate) fn scan_gtk4_slider_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<' } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -56,7 +56,11 @@ pub fn create(min: f64, max: f64, initial: f64, on_change: f64) -> i64 { let value = scale.value(); let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + value, + ); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/textarea.rs b/crates/perry-ui-gtk4/src/widgets/textarea.rs index 794a9a133f..aca6da8961 100644 --- a/crates/perry-ui-gtk4/src/widgets/textarea.rs +++ b/crates/perry-ui-gtk4/src/widgets/textarea.rs @@ -18,7 +18,7 @@ pub(crate) fn scan_gtk4_textarea_gc_roots(visitor: &mut perry_ffi::GcRootVisitor } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -72,7 +72,11 @@ pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + nanboxed, + ); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/textfield.rs b/crates/perry-ui-gtk4/src/widgets/textfield.rs index 57b895edba..8666cf0ff8 100644 --- a/crates/perry-ui-gtk4/src/widgets/textfield.rs +++ b/crates/perry-ui-gtk4/src/widgets/textfield.rs @@ -20,8 +20,8 @@ pub(crate) fn scan_gtk4_textfield_gc_roots(visitor: &mut perry_ffi::GcRootVisito } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -60,7 +60,11 @@ pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + nanboxed, + ); } } }); @@ -137,7 +141,11 @@ pub fn set_on_submit(handle: i64, on_submit: f64) { let closure_ptr = unsafe { js_nanbox_get_pointer(on_submit) }; if closure_ptr != 0 { unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + nanboxed, + ); } } }); @@ -155,7 +163,7 @@ pub fn set_on_focus(handle: i64, on_focus: f64) { let closure_ptr = unsafe { js_nanbox_get_pointer(on_focus) }; if closure_ptr != 0 { unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/toggle.rs b/crates/perry-ui-gtk4/src/widgets/toggle.rs index 3275e539c4..1f8b4cb372 100644 --- a/crates/perry-ui-gtk4/src/widgets/toggle.rs +++ b/crates/perry-ui-gtk4/src/widgets/toggle.rs @@ -24,7 +24,7 @@ const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -71,7 +71,11 @@ pub fn create(label_ptr: *const u8, on_change: f64) -> i64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + value, + ); } } }); diff --git a/crates/perry-ui-gtk4/src/widgets/tree_view.rs b/crates/perry-ui-gtk4/src/widgets/tree_view.rs index 8560f6cd89..d513b515d8 100644 --- a/crates/perry-ui-gtk4/src/widgets/tree_view.rs +++ b/crates/perry-ui-gtk4/src/widgets/tree_view.rs @@ -16,7 +16,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -115,7 +115,7 @@ pub fn create(root_node: i64, on_select: f64) -> i64 { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } }); } diff --git a/crates/perry-ui-gtk4/src/widgets/webview.rs b/crates/perry-ui-gtk4/src/widgets/webview.rs index 611f930d22..8313ba8806 100644 --- a/crates/perry-ui-gtk4/src/widgets/webview.rs +++ b/crates/perry-ui-gtk4/src/widgets/webview.rs @@ -14,8 +14,8 @@ use std::collections::HashMap; use webkit6::prelude::*; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; fn js_is_truthy(value: f64) -> i32; @@ -174,7 +174,8 @@ fn install_signal_handlers(handle: i64, webview: &webkit6::WebView) { let url_nb = nanbox_str(&url); let closure_ptr = unsafe { js_nanbox_get_pointer(on_should) } as *const u8; if !closure_ptr.is_null() { - let result = unsafe { js_closure_call1(closure_ptr, url_nb) }; + let result = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb) }; let bits = result.to_bits(); let is_undefined = bits == 0x7FFC_0000_0000_0001; let allow = is_undefined || unsafe { js_is_truthy(result) != 0 }; @@ -206,7 +207,7 @@ fn install_signal_handlers(handle: i64, webview: &webkit6::WebView) { let closure_ptr = unsafe { js_nanbox_get_pointer(on_loaded) } as *const u8; if !closure_ptr.is_null() { unsafe { - js_closure_call1(closure_ptr, url_nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb); } } }); @@ -226,7 +227,7 @@ fn install_signal_handlers(handle: i64, webview: &webkit6::WebView) { let closure_ptr = unsafe { js_nanbox_get_pointer(on_error) } as *const u8; if !closure_ptr.is_null() { unsafe { - js_closure_call2(closure_ptr, code, msg_nb); + js_closure_call2(closure_ptr, perry_ffi::JsThis::UNDEFINED, code, msg_nb); } } false @@ -299,7 +300,7 @@ pub fn evaluate_js(handle: i64, js_ptr: *const u8, callback: f64) { let closure_ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; if !closure_ptr.is_null() { unsafe { - js_closure_call1(closure_ptr, nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb); } } }); diff --git a/crates/perry-ui-gtk4/src/window.rs b/crates/perry-ui-gtk4/src/window.rs index 657b10feb4..a87d03626f 100644 --- a/crates/perry-ui-gtk4/src/window.rs +++ b/crates/perry-ui-gtk4/src/window.rs @@ -86,7 +86,7 @@ pub fn set_size(window_handle: i64, width: f64, height: f64) { pub fn on_focus_lost(window_handle: i64, callback: f64) { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; } WINDOWS.with(|w| { if let Some(window) = w.borrow().get(&window_handle) { @@ -95,7 +95,7 @@ pub fn on_focus_lost(window_handle: i64, callback: f64) { if !window.is_active() { let ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; unsafe { - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } }); diff --git a/crates/perry-ui-ios/src/adaptive_layout.rs b/crates/perry-ui-ios/src/adaptive_layout.rs index 9a5d199b74..cecbaaa9a1 100644 --- a/crates/perry-ui-ios/src/adaptive_layout.rs +++ b/crates/perry-ui-ios/src/adaptive_layout.rs @@ -25,7 +25,7 @@ extern "C" { fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -308,7 +308,11 @@ unsafe fn invoke_listener(callback: f64, snapshot: &LayoutSnapshot) { return; } js_run_stdlib_pump(); - js_closure_call1(closure, js_nanbox_pointer(object)); + js_closure_call1( + closure, + perry_ffi::JsThis::UNDEFINED, + js_nanbox_pointer(object), + ); js_promise_run_microtasks(); } diff --git a/crates/perry-ui-ios/src/app.rs b/crates/perry-ui-ios/src/app.rs index 68444bd995..59d84813b3 100644 --- a/crates/perry-ui-ios/src/app.rs +++ b/crates/perry-ui-ios/src/app.rs @@ -662,7 +662,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_callback_timer_tick() -> i32; fn js_interval_timer_tick() -> i32; fn js_frame_pump_default() -> i32; @@ -982,7 +982,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-ios/src/audio_playback.rs b/crates/perry-ui-ios/src/audio_playback.rs index 2c755519d4..14badc4fbc 100644 --- a/crates/perry-ui-ios/src/audio_playback.rs +++ b/crates/perry-ui-ios/src/audio_playback.rs @@ -16,9 +16,9 @@ use std::cell::RefCell; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; #[allow(dead_code)] - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -653,7 +653,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -712,7 +712,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } if should_clean { diff --git a/crates/perry-ui-ios/src/background.rs b/crates/perry-ui-ios/src/background.rs index 65125b6a2b..8297ed65be 100644 --- a/crates/perry-ui-ios/src/background.rs +++ b/crates/perry-ui-ios/src/background.rs @@ -23,7 +23,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_value_is_promise(value: f64) -> i32; } @@ -76,7 +76,7 @@ unsafe fn invoke_handler(handler: f64, task: *mut AnyObject) { js_run_stdlib_pump(); js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(handler) as *const u8; - let result = js_closure_call0(ptr); + let result = js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); js_promise_run_microtasks(); // If the handler returned a Promise, we'd ideally tie completion to its diff --git a/crates/perry-ui-ios/src/camera.rs b/crates/perry-ui-ios/src/camera.rs index d62f7c0faa..766a29b0f0 100644 --- a/crates/perry-ui-ios/src/camera.rs +++ b/crates/perry-ui-ios/src/camera.rs @@ -15,8 +15,8 @@ use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use crate::widgets; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; @@ -254,7 +254,12 @@ fn register_tap_handler_class() -> *const c_void { js_run_stdlib_pump(); js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call2(closure_ptr as *const u8, norm_x, norm_y); + js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + norm_x, + norm_y, + ); } }); } diff --git a/crates/perry-ui-ios/src/deeplinks.rs b/crates/perry-ui-ios/src/deeplinks.rs index f4d04b4ad2..09c3216e4f 100644 --- a/crates/perry-ui-ios/src/deeplinks.rs +++ b/crates/perry-ui-ios/src/deeplinks.rs @@ -28,7 +28,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; } @@ -69,7 +69,7 @@ unsafe fn invoke_handler(url: &str, source: &str) { if !ptr.is_null() { let url_v = nanbox_str(url); let src_v = nanbox_str(source); - js_closure_call2(ptr, url_v, src_v); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, url_v, src_v); } } } diff --git a/crates/perry-ui-ios/src/drag_drop.rs b/crates/perry-ui-ios/src/drag_drop.rs index 95f7e0f71e..b1105457a6 100644 --- a/crates/perry-ui-ios/src/drag_drop.rs +++ b/crates/perry-ui-ios/src/drag_drop.rs @@ -30,8 +30,8 @@ use std::ffi::c_void; use std::rc::Rc; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; @@ -80,7 +80,7 @@ unsafe fn call_provider(cb: f64) -> Option { if p.is_null() { return None; } - let ret = js_closure_call0(p); + let ret = js_closure_call0(p, perry_ffi::JsThis::UNDEFINED); let sh = js_jsvalue_to_string(ret); if sh.is_null() { None @@ -198,7 +198,7 @@ unsafe fn perform_drop_impl(interaction: *mut AnyObject, session: *mut AnyObject } let payload = js_nanbox_pointer(obj as i64); let cb_ptr = js_nanbox_get_pointer(a.cb) as *const u8; - js_closure_call1(cb_ptr, payload); + js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, payload); } // text diff --git a/crates/perry-ui-ios/src/file_dialog.rs b/crates/perry-ui-ios/src/file_dialog.rs index 1a3bf1fe7f..299693d226 100644 --- a/crates/perry-ui-ios/src/file_dialog.rs +++ b/crates/perry-ui-ios/src/file_dialog.rs @@ -1,5 +1,5 @@ extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -9,6 +9,10 @@ pub fn open_dialog(callback: f64) { unsafe { let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; // Call callback with TAG_UNDEFINED (user "cancelled") - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } diff --git a/crates/perry-ui-ios/src/geolocation.rs b/crates/perry-ui-ios/src/geolocation.rs index 1f93bf9c0b..097af727de 100644 --- a/crates/perry-ui-ios/src/geolocation.rs +++ b/crates/perry-ui-ios/src/geolocation.rs @@ -12,8 +12,15 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call4(closure: *const u8, arg0: f64, arg1: f64, arg2: f64, arg3: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call4( + closure: *const u8, + this: perry_ffi::JsThis, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + ) -> f64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; } @@ -111,7 +118,14 @@ unsafe fn pump() { unsafe fn invoke_success(closure_f64: f64, lat: f64, lng: f64, accuracy: f64, timestamp_ms: f64) { pump(); let ptr = js_nanbox_get_pointer(closure_f64) as *const u8; - js_closure_call4(ptr, lat, lng, accuracy, timestamp_ms); + js_closure_call4( + ptr, + perry_ffi::JsThis::UNDEFINED, + lat, + lng, + accuracy, + timestamp_ms, + ); } unsafe fn invoke_error(closure_f64: f64, message: &str) { @@ -121,14 +135,14 @@ unsafe fn invoke_error(closure_f64: f64, message: &str) { pump(); let ptr = js_nanbox_get_pointer(closure_f64) as *const u8; let msg = nanbox_str(message); - js_closure_call1(ptr, msg); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, msg); } unsafe fn invoke_status(closure_f64: f64, status: &str) { pump(); let ptr = js_nanbox_get_pointer(closure_f64) as *const u8; let msg = nanbox_str(status); - js_closure_call1(ptr, msg); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, msg); } fn auth_status_string(status: i32) -> &'static str { diff --git a/crates/perry-ui-ios/src/image_picker.rs b/crates/perry-ui-ios/src/image_picker.rs index 694b47293e..662fc591b7 100644 --- a/crates/perry-ui-ios/src/image_picker.rs +++ b/crates/perry-ui-ios/src/image_picker.rs @@ -7,7 +7,7 @@ //! sandboxed Photos asset URLs to NSTemporaryDirectory. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_array_alloc(capacity: u32) -> *mut std::ffi::c_void; fn js_nanbox_pointer(ptr: i64) -> f64; @@ -18,6 +18,6 @@ pub fn pick(_max_count: f64, _allow_multiple: f64, callback: f64) { let arr = js_array_alloc(0); let nb = js_nanbox_pointer(arr as i64); let cb_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb_ptr, nb); + js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, nb); } } diff --git a/crates/perry-ui-ios/src/location.rs b/crates/perry-ui-ios/src/location.rs index cf3cae5569..f98a49838f 100644 --- a/crates/perry-ui-ios/src/location.rs +++ b/crates/perry-ui-ios/src/location.rs @@ -7,7 +7,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; } // Raw ObjC runtime FFI for dynamic class registration @@ -51,7 +51,12 @@ unsafe fn invoke_callback(lat: f64, lon: f64) { js_run_stdlib_pump(); js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call2(closure_ptr as *const u8, lat, lon); + js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + lat, + lon, + ); } } diff --git a/crates/perry-ui-ios/src/media_playback.rs b/crates/perry-ui-ios/src/media_playback.rs index 636d95fe06..cb70889b4e 100644 --- a/crates/perry-ui-ios/src/media_playback.rs +++ b/crates/perry-ui-ios/src/media_playback.rs @@ -30,8 +30,8 @@ use std::sync::OnceLock; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; // Signature matches the rest of perry-ui-macos (see audio.rs:133, // lib.rs:113/118/1662). Runtime returns `*mut StringHeader` which is // i64-sized on Apple platforms. @@ -935,7 +935,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -944,7 +948,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-ios/src/menu.rs b/crates/perry-ui-ios/src/menu.rs index c784b41742..3ea8dd17c3 100644 --- a/crates/perry-ui-ios/src/menu.rs +++ b/crates/perry-ui-ios/src/menu.rs @@ -9,7 +9,7 @@ use objc2::runtime::{AnyClass, AnyObject, Sel}; use std::cell::RefCell; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -435,7 +435,7 @@ pub(crate) unsafe fn dispatch_menu_action(sender: *mut AnyObject) { }); if let Some(cb) = callback { let ptr = js_nanbox_get_pointer(cb) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-ios/src/network.rs b/crates/perry-ui-ios/src/network.rs index 5cfa3cd0e9..5208a8bd5f 100644 --- a/crates/perry-ui-ios/src/network.rs +++ b/crates/perry-ui-ios/src/network.rs @@ -22,7 +22,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; } @@ -107,7 +107,7 @@ unsafe fn invoke_callback(closure_f64: f64, status: Status) { } let connected = bool_to_jsvalue(status.connected); let kind = nanbox_str(status.kind); - js_closure_call2(ptr, connected, kind); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, connected, kind); } fn classify(path: *mut c_void) -> Status { diff --git a/crates/perry-ui-ios/src/notifications.rs b/crates/perry-ui-ios/src/notifications.rs index bf62f8e169..5bd0addad2 100644 --- a/crates/perry-ui-ios/src/notifications.rs +++ b/crates/perry-ui-ios/src/notifications.rs @@ -60,8 +60,8 @@ extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; fn js_string_from_bytes(data: *const u8, len: u32) -> *mut perry_runtime::string::StringHeader; - fn js_closure_call1(closure: *const u8, arg0: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg0: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_json_parse(text_ptr: *const perry_runtime::string::StringHeader) -> u64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; @@ -149,7 +149,7 @@ unsafe fn dispatch_tap(response: &AnyObject) { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call2(ptr, id_value, action_value); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, id_value, action_value); } } @@ -263,6 +263,7 @@ pub fn on_background_receive(callback: f64) { #[no_mangle] unsafe extern "C" fn perry_ios_notification_completion_trampoline( closure: *const perry_runtime::closure::ClosureHeader, + _this: perry_runtime::closure::JsThis, _arg: f64, ) -> f64 { let handle = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0); @@ -323,7 +324,7 @@ pub unsafe fn dispatch_device_token(device_token: *mut AnyObject) { let boxed = js_nanbox_string(str_ptr as i64); let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, boxed); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, boxed); } } @@ -405,7 +406,7 @@ pub unsafe fn dispatch_remote_payload_with_completion( } return; } - let result = js_closure_call1(cb_ptr, payload_value); + let result = js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, payload_value); let Some(block) = completion_block else { // Caller passed a null completion (e.g., direct invocation from a @@ -511,7 +512,7 @@ pub unsafe fn dispatch_remote_payload(user_info: *mut AnyObject) { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, parsed_f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, parsed_f64); } } diff --git a/crates/perry-ui-ios/src/pointer.rs b/crates/perry-ui-ios/src/pointer.rs index 26d9cf2f4d..8529d3679f 100644 --- a/crates/perry-ui-ios/src/pointer.rs +++ b/crates/perry-ui-ios/src/pointer.rs @@ -23,7 +23,7 @@ use std::collections::HashMap; use crate::widgets::get_widget; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -142,7 +142,7 @@ fn dispatch_phase(recog: &PerryPointerRecognizer, touches: &AnyObject, phase: u3 return; } let pe = js_pointer_event_new(local.x, local.y, 0, POINTER_TYPE_TOUCH); - js_closure_call1(closure_ptr as *const u8, pe); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, pe); } } diff --git a/crates/perry-ui-ios/src/state.rs b/crates/perry-ui-ios/src/state.rs index 13b70e46e4..eded1088c3 100644 --- a/crates/perry-ui-ios/src/state.rs +++ b/crates/perry-ui-ios/src/state.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -270,7 +270,7 @@ pub fn state_set(handle: i64, value: f64) { for closure_f64 in callbacks_snapshot { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } } @@ -308,7 +308,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; widgets::add_child(container, child_handle); } diff --git a/crates/perry-ui-ios/src/widgets/alert.rs b/crates/perry-ui-ios/src/widgets/alert.rs index a8eea1bb3b..9357fcd3e3 100644 --- a/crates/perry-ui-ios/src/widgets/alert.rs +++ b/crates/perry-ui-ios/src/widgets/alert.rs @@ -5,7 +5,7 @@ use objc2::runtime::{AnyClass, AnyObject}; use objc2_foundation::NSString; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_array_get_length(arr: i64) -> i64; fn js_array_get_element(arr: i64, index: i64) -> f64; @@ -26,7 +26,7 @@ unsafe extern "C" fn alert_callback_trampoline(ctx: *mut std::ffi::c_void) { let (closure, idx) = *pkg; let ptr = js_nanbox_get_pointer(closure) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, idx as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); } }); } diff --git a/crates/perry-ui-ios/src/widgets/bottom_nav.rs b/crates/perry-ui-ios/src/widgets/bottom_nav.rs index 03e0e769ca..9090e3a565 100644 --- a/crates/perry-ui-ios/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-ios/src/widgets/bottom_nav.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -92,7 +92,7 @@ unsafe extern "C" fn trampoline(ctx: *mut std::ffi::c_void) { let pkg = Box::from_raw(ctx as *mut (f64, i64)); let (closure, idx) = *pkg; let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call1(ptr, idx as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); }); } diff --git a/crates/perry-ui-ios/src/widgets/button.rs b/crates/perry-ui-ios/src/widgets/button.rs index 1fd2e04c0b..69df4bbe72 100644 --- a/crates/perry-ui-ios/src/widgets/button.rs +++ b/crates/perry-ui-ios/src/widgets/button.rs @@ -24,7 +24,7 @@ pub(crate) fn scan_ios_button_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_ } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; // dispatch_get_main_queue() is a macro; the actual symbol is _dispatch_main_q static _dispatch_main_q: std::ffi::c_void; @@ -39,7 +39,7 @@ unsafe extern "C" fn button_callback_trampoline(context: *mut std::ffi::c_void) let _ = std::panic::catch_unwind(|| { let closure_f64 = f64::from_bits(context as u64); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); }); } diff --git a/crates/perry-ui-ios/src/widgets/calendar.rs b/crates/perry-ui-ios/src/widgets/calendar.rs index 4e9d325cde..e4fba24015 100644 --- a/crates/perry-ui-ios/src/widgets/calendar.rs +++ b/crates/perry-ui-ios/src/widgets/calendar.rs @@ -17,7 +17,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -63,7 +63,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } diff --git a/crates/perry-ui-ios/src/widgets/combobox.rs b/crates/perry-ui-ios/src/widgets/combobox.rs index 44cb323acd..aeb43b4fb9 100644 --- a/crates/perry-ui-ios/src/widgets/combobox.rs +++ b/crates/perry-ui-ios/src/widgets/combobox.rs @@ -24,7 +24,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -60,7 +60,7 @@ fn fire_callback(handle: i64, value: &str) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); }), ); } diff --git a/crates/perry-ui-ios/src/widgets/date_picker.rs b/crates/perry-ui-ios/src/widgets/date_picker.rs index f894f3bfd4..7395289bfb 100644 --- a/crates/perry-ui-ios/src/widgets/date_picker.rs +++ b/crates/perry-ui-ios/src/widgets/date_picker.rs @@ -18,7 +18,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -64,7 +64,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } diff --git a/crates/perry-ui-ios/src/widgets/image_gallery.rs b/crates/perry-ui-ios/src/widgets/image_gallery.rs index 35362479a2..421a810c15 100644 --- a/crates/perry-ui-ios/src/widgets/image_gallery.rs +++ b/crates/perry-ui-ios/src/widgets/image_gallery.rs @@ -17,7 +17,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -84,7 +84,7 @@ define_class!( }); if closure != 0.0 { let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call1(ptr, new_index as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, new_index as f64); } } } diff --git a/crates/perry-ui-ios/src/widgets/mod.rs b/crates/perry-ui-ios/src/widgets/mod.rs index 6740e21648..fa5ccf8ad9 100644 --- a/crates/perry-ui-ios/src/widgets/mod.rs +++ b/crates/perry-ui-ios/src/widgets/mod.rs @@ -734,7 +734,7 @@ pub fn set_hugging_priority(handle: i64, priority: f64) { use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } diff --git a/crates/perry-ui-ios/src/widgets/picker.rs b/crates/perry-ui-ios/src/widgets/picker.rs index 3df9261023..7b134ebe44 100644 --- a/crates/perry-ui-ios/src/widgets/picker.rs +++ b/crates/perry-ui-ios/src/widgets/picker.rs @@ -21,7 +21,7 @@ pub(crate) fn scan_ios_picker_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_ } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; // dispatch_get_main_queue() is a macro; the actual symbol is _dispatch_main_q static _dispatch_main_q: std::ffi::c_void; @@ -43,7 +43,11 @@ unsafe extern "C" fn picker_callback_trampoline(context: *mut std::ffi::c_void) let _ = std::panic::catch_unwind(|| { let payload = Box::from_raw(context as *mut PickerDispatch); let closure_ptr = js_nanbox_get_pointer(payload.closure_f64); - js_closure_call1(closure_ptr as *const u8, payload.index); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + payload.index, + ); }); } diff --git a/crates/perry-ui-ios/src/widgets/rich_text.rs b/crates/perry-ui-ios/src/widgets/rich_text.rs index fc3db8961b..43b3a750e8 100644 --- a/crates/perry-ui-ios/src/widgets/rich_text.rs +++ b/crates/perry-ui-ios/src/widgets/rich_text.rs @@ -19,7 +19,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -74,7 +74,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } diff --git a/crates/perry-ui-ios/src/widgets/scrollview.rs b/crates/perry-ui-ios/src/widgets/scrollview.rs index da9d5ee2ea..76e956598c 100644 --- a/crates/perry-ui-ios/src/widgets/scrollview.rs +++ b/crates/perry-ui-ios/src/widgets/scrollview.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use super::scroll_mutation_gate::ScrollMutationGate; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -24,7 +24,7 @@ unsafe extern "C" fn refresh_callback_trampoline(context: *mut std::ffi::c_void) let _ = std::panic::catch_unwind(|| { let closure_f64 = f64::from_bits(context as u64); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); }); } diff --git a/crates/perry-ui-ios/src/widgets/securefield.rs b/crates/perry-ui-ios/src/widgets/securefield.rs index 28b4a6daa5..5b8f79a638 100644 --- a/crates/perry-ui-ios/src/widgets/securefield.rs +++ b/crates/perry-ui-ios/src/widgets/securefield.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_ios_securefield_gc_roots(visitor: &mut perry_ffi::GcRootVisit } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -50,7 +50,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-ios/src/widgets/slider.rs b/crates/perry-ui-ios/src/widgets/slider.rs index 4c08d89080..87c3f6be9a 100644 --- a/crates/perry-ui-ios/src/widgets/slider.rs +++ b/crates/perry-ui-ios/src/widgets/slider.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_ios_slider_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_ } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -43,7 +43,7 @@ define_class!( let value: f32 = unsafe { msg_send![sender, value] }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value as f64); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value as f64); } } }); diff --git a/crates/perry-ui-ios/src/widgets/tabbar.rs b/crates/perry-ui-ios/src/widgets/tabbar.rs index 5125bf1543..0284a744a9 100644 --- a/crates/perry-ui-ios/src/widgets/tabbar.rs +++ b/crates/perry-ui-ios/src/widgets/tabbar.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -116,7 +116,11 @@ unsafe extern "C" fn tab_callback_trampoline(context: *mut std::ffi::c_void) { let packed = Box::from_raw(context as *mut (f64, i64)); let (closure_f64, tab_index) = *packed; let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call1(closure_ptr as *const u8, tab_index as f64); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + tab_index as f64, + ); }); } diff --git a/crates/perry-ui-ios/src/widgets/textarea.rs b/crates/perry-ui-ios/src/widgets/textarea.rs index 871396d53c..e16b93b383 100644 --- a/crates/perry-ui-ios/src/widgets/textarea.rs +++ b/crates/perry-ui-ios/src/widgets/textarea.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_ios_textarea_gc_roots(visitor: &mut perry_ffi::GcRootVisitor< } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -51,7 +51,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-ios/src/widgets/textfield.rs b/crates/perry-ui-ios/src/widgets/textfield.rs index 19c03c410f..e773462380 100644 --- a/crates/perry-ui-ios/src/widgets/textfield.rs +++ b/crates/perry-ui-ios/src/widgets/textfield.rs @@ -24,7 +24,7 @@ pub(crate) fn scan_ios_textfield_gc_roots(visitor: &mut perry_ffi::GcRootVisitor } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -55,7 +55,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); @@ -247,7 +247,7 @@ define_class!( let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-ios/src/widgets/toggle.rs b/crates/perry-ui-ios/src/widgets/toggle.rs index 51368f87ab..8fc9033cd7 100644 --- a/crates/perry-ui-ios/src/widgets/toggle.rs +++ b/crates/perry-ui-ios/src/widgets/toggle.rs @@ -23,7 +23,7 @@ const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -53,7 +53,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value); } } }); diff --git a/crates/perry-ui-ios/src/widgets/tree_view.rs b/crates/perry-ui-ios/src/widgets/tree_view.rs index 56fc6593a4..dfb4cc1336 100644 --- a/crates/perry-ui-ios/src/widgets/tree_view.rs +++ b/crates/perry-ui-ios/src/widgets/tree_view.rs @@ -27,7 +27,7 @@ use objc2_ui_kit::UIView; use std::cell::{Cell, RefCell}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -311,7 +311,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on_select) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); }), ); } diff --git a/crates/perry-ui-ios/src/widgets/webview.rs b/crates/perry-ui-ios/src/widgets/webview.rs index fc0ccb74a4..920aa1c238 100644 --- a/crates/perry-ui-ios/src/widgets/webview.rs +++ b/crates/perry-ui-ios/src/widgets/webview.rs @@ -16,8 +16,8 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -153,7 +153,7 @@ define_class!( crate::catch_callback_panic( "webview onShouldNavigate", std::panic::AssertUnwindSafe(|| { - let r = unsafe { js_closure_call1(closure_ptr, url_nb) }; + let r = unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb) }; result_cell_ref.set(r); }), ); @@ -202,7 +202,7 @@ define_class!( let url_nb = nanbox_str(&url_str); let closure_ptr = js_nanbox_get_pointer(on_loaded) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, url_nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb); } }), ); @@ -264,7 +264,12 @@ impl PerryWebViewDelegate { let msg_nb = nanbox_str(&msg); let closure_ptr = js_nanbox_get_pointer(on_error) as *const u8; if !closure_ptr.is_null() { - js_closure_call2(closure_ptr, code as f64, msg_nb); + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + code as f64, + msg_nb, + ); } }), ); @@ -415,7 +420,7 @@ pub fn evaluate_js(handle: i64, js_ptr: *const u8, callback: f64) { let nb = nanbox_str(&s); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb); } }), ); diff --git a/crates/perry-ui-ios/src/widgets/wheel_picker.rs b/crates/perry-ui-ios/src/widgets/wheel_picker.rs index de264cb2a7..8c652ebb7c 100644 --- a/crates/perry-ui-ios/src/widgets/wheel_picker.rs +++ b/crates/perry-ui-ios/src/widgets/wheel_picker.rs @@ -26,7 +26,7 @@ pub(crate) fn scan_ios_wheel_picker_gc_roots(visitor: &mut perry_ffi::GcRootVisi } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -45,7 +45,7 @@ unsafe extern "C" fn callback_trampoline(context: *mut std::ffi::c_void) { let _ = std::panic::catch_unwind(|| { let payload = Box::from_raw(context as *mut WheelPickerDispatch); let closure = js_nanbox_get_pointer(payload.closure) as *const u8; - js_closure_call1(closure, payload.index); + js_closure_call1(closure, perry_ffi::JsThis::UNDEFINED, payload.index); }); } diff --git a/crates/perry-ui-macos/src/app.rs b/crates/perry-ui-macos/src/app.rs index a55843c89a..3918417f91 100644 --- a/crates/perry-ui-macos/src/app.rs +++ b/crates/perry-ui-macos/src/app.rs @@ -786,14 +786,14 @@ fn invoke_global_hotkey_callback(key: usize) { let callback_ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; if !callback_ptr.is_null() { unsafe { - js_closure_call0(callback_ptr); + js_closure_call0(callback_ptr, perry_ffi::JsThis::UNDEFINED); } } } } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -818,7 +818,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); @@ -1306,7 +1306,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); @@ -1535,7 +1535,7 @@ define_class!( fn invoke_terminate_callback() { extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } let cb = ON_TERMINATE_CALLBACK.with(|c| *c.borrow()); @@ -1543,7 +1543,11 @@ fn invoke_terminate_callback() { unsafe { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, f64::from_bits(0x7FFC_0000_0000_0001)); // undefined + js_closure_call1( + ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); // undefined } } } @@ -1551,7 +1555,7 @@ fn invoke_terminate_callback() { fn invoke_activate_callback() { extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } let cb = ON_ACTIVATE_CALLBACK.with(|c| *c.borrow()); @@ -1559,7 +1563,11 @@ fn invoke_activate_callback() { unsafe { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, f64::from_bits(0x7FFC_0000_0000_0001)); // undefined + js_closure_call1( + ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); // undefined } } } @@ -1772,7 +1780,7 @@ thread_local! { pub fn window_on_focus_lost(window_handle: i64, callback: f64) { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; } WINDOWS.with(|w| { let windows = w.borrow(); @@ -1791,7 +1799,7 @@ pub fn window_on_focus_lost(window_handle: i64, callback: f64) { .with(|callbacks| callbacks.borrow().get(&window_handle).copied()); if let Some(callback) = callback { let ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } }); let _: Retained = objc2::msg_send![ diff --git a/crates/perry-ui-macos/src/audio_playback.rs b/crates/perry-ui-macos/src/audio_playback.rs index c980817b32..05892f31b3 100644 --- a/crates/perry-ui-macos/src/audio_playback.rs +++ b/crates/perry-ui-macos/src/audio_playback.rs @@ -18,9 +18,9 @@ use std::cell::RefCell; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; #[allow(dead_code)] - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -650,7 +650,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -709,7 +709,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } if should_clean { diff --git a/crates/perry-ui-macos/src/background.rs b/crates/perry-ui-macos/src/background.rs index 5f727bd419..4855c0502b 100644 --- a/crates/perry-ui-macos/src/background.rs +++ b/crates/perry-ui-macos/src/background.rs @@ -34,7 +34,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_value_is_promise(value: f64) -> i32; } @@ -87,7 +87,7 @@ unsafe fn invoke_handler(handler: f64) { js_run_stdlib_pump(); js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(handler) as *const u8; - let result = js_closure_call0(ptr); + let result = js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); js_promise_run_microtasks(); let _ = js_value_is_promise(result); } diff --git a/crates/perry-ui-macos/src/deeplinks.rs b/crates/perry-ui-macos/src/deeplinks.rs index b113296360..7bee49c088 100644 --- a/crates/perry-ui-macos/src/deeplinks.rs +++ b/crates/perry-ui-macos/src/deeplinks.rs @@ -33,7 +33,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -72,7 +72,7 @@ unsafe fn invoke_handler(url: &str, source: &str) { if !ptr.is_null() { let url_v = nanbox_str(url); let src_v = nanbox_str(source); - js_closure_call2(ptr, url_v, src_v); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, url_v, src_v); } } } diff --git a/crates/perry-ui-macos/src/drag_drop.rs b/crates/perry-ui-macos/src/drag_drop.rs index 261da4b368..29cfdcbaad 100644 --- a/crates/perry-ui-macos/src/drag_drop.rs +++ b/crates/perry-ui-macos/src/drag_drop.rs @@ -36,8 +36,8 @@ use std::collections::HashMap; use std::ffi::{c_void, CString}; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; @@ -97,7 +97,7 @@ unsafe fn call_provider(cb: f64) -> Option { if p.is_null() { return None; } - let ret = js_closure_call0(p); + let ret = js_closure_call0(p, perry_ffi::JsThis::UNDEFINED); let sh = js_jsvalue_to_string(ret); if sh.is_null() { None @@ -241,7 +241,7 @@ extern "C-unwind" fn perform_drag(this: *mut AnyObject, _cmd: Sel, sender: *mut let payload = js_nanbox_pointer(obj as i64); let cb_ptr = js_nanbox_get_pointer(cb) as *const u8; - js_closure_call1(cb_ptr, payload); + js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, payload); } Bool::YES } diff --git a/crates/perry-ui-macos/src/file_dialog.rs b/crates/perry-ui-macos/src/file_dialog.rs index d5191d5b61..76c17cf137 100644 --- a/crates/perry-ui-macos/src/file_dialog.rs +++ b/crates/perry-ui-macos/src/file_dialog.rs @@ -4,7 +4,7 @@ use objc2_app_kit::NSOpenPanel; use objc2_foundation::{MainThreadMarker, NSString}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -36,13 +36,21 @@ pub fn open_dialog(callback: f64) { let bytes = rust_str.as_bytes(); let str_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let nanboxed = js_nanbox_string(str_ptr as i64); - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); } else { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } else { // User cancelled - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } } @@ -72,12 +80,20 @@ pub fn open_folder_dialog(callback: f64) { let bytes = rust_str.as_bytes(); let str_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let nanboxed = js_nanbox_string(str_ptr as i64); - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); } else { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } else { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } } @@ -113,12 +129,20 @@ pub fn save_dialog(callback: f64, default_name_ptr: *const u8, _allowed_types_pt let bytes = rust_str.as_bytes(); let str_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let nanboxed = js_nanbox_string(str_ptr as i64); - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); } else { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } else { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } } diff --git a/crates/perry-ui-macos/src/geolocation.rs b/crates/perry-ui-macos/src/geolocation.rs index d629e5992a..384e263099 100644 --- a/crates/perry-ui-macos/src/geolocation.rs +++ b/crates/perry-ui-macos/src/geolocation.rs @@ -18,8 +18,15 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call4(closure: *const u8, arg0: f64, arg1: f64, arg2: f64, arg3: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call4( + closure: *const u8, + this: perry_ffi::JsThis, + arg0: f64, + arg1: f64, + arg2: f64, + arg3: f64, + ) -> f64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -113,7 +120,14 @@ unsafe fn pump() { unsafe fn invoke_success(closure_f64: f64, lat: f64, lng: f64, accuracy: f64, timestamp_ms: f64) { pump(); let ptr = js_nanbox_get_pointer(closure_f64) as *const u8; - js_closure_call4(ptr, lat, lng, accuracy, timestamp_ms); + js_closure_call4( + ptr, + perry_ffi::JsThis::UNDEFINED, + lat, + lng, + accuracy, + timestamp_ms, + ); } unsafe fn invoke_error(closure_f64: f64, message: &str) { @@ -123,14 +137,14 @@ unsafe fn invoke_error(closure_f64: f64, message: &str) { pump(); let ptr = js_nanbox_get_pointer(closure_f64) as *const u8; let msg = nanbox_str(message); - js_closure_call1(ptr, msg); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, msg); } unsafe fn invoke_status(closure_f64: f64, status: &str) { pump(); let ptr = js_nanbox_get_pointer(closure_f64) as *const u8; let msg = nanbox_str(status); - js_closure_call1(ptr, msg); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, msg); } fn auth_status_string(status: i32) -> &'static str { diff --git a/crates/perry-ui-macos/src/image_picker.rs b/crates/perry-ui-macos/src/image_picker.rs index 9a3b908a05..614038e3f0 100644 --- a/crates/perry-ui-macos/src/image_picker.rs +++ b/crates/perry-ui-macos/src/image_picker.rs @@ -11,7 +11,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; fn js_array_alloc(capacity: u32) -> *mut std::ffi::c_void; fn js_nanbox_pointer(ptr: i64) -> f64; @@ -82,6 +82,6 @@ pub fn pick(max_count: f64, allow_multiple: f64, callback: f64) { let nb_arr = js_nanbox_pointer(cur as i64); let cb_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb_ptr, nb_arr); + js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, nb_arr); } } diff --git a/crates/perry-ui-macos/src/location.rs b/crates/perry-ui-macos/src/location.rs index db4ab121b8..ef3c5940fd 100644 --- a/crates/perry-ui-macos/src/location.rs +++ b/crates/perry-ui-macos/src/location.rs @@ -9,7 +9,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; } // Raw ObjC runtime FFI for dynamic class registration @@ -46,7 +46,12 @@ unsafe fn invoke_callback(lat: f64, lon: f64) { js_run_stdlib_pump(); js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call2(closure_ptr as *const u8, lat, lon); + js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + lat, + lon, + ); } } diff --git a/crates/perry-ui-macos/src/media_playback.rs b/crates/perry-ui-macos/src/media_playback.rs index 23ac2a5106..76e1e0a635 100644 --- a/crates/perry-ui-macos/src/media_playback.rs +++ b/crates/perry-ui-macos/src/media_playback.rs @@ -29,8 +29,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; // Signature matches the rest of perry-ui-macos (see audio.rs:133, // lib.rs:113/118/1662). Runtime returns `*mut StringHeader` which is // i64-sized on Apple platforms. @@ -740,7 +740,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -749,7 +753,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-macos/src/menu.rs b/crates/perry-ui-macos/src/menu.rs index 9bdc0fd1b3..bd15b9a22e 100644 --- a/crates/perry-ui-macos/src/menu.rs +++ b/crates/perry-ui-macos/src/menu.rs @@ -23,7 +23,7 @@ pub(crate) fn scan_macos_menu_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_ } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -51,7 +51,7 @@ define_class!( if let Some(cf) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(cf) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); diff --git a/crates/perry-ui-macos/src/network.rs b/crates/perry-ui-macos/src/network.rs index 127eca7803..f741fd69f9 100644 --- a/crates/perry-ui-macos/src/network.rs +++ b/crates/perry-ui-macos/src/network.rs @@ -18,7 +18,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -98,7 +98,7 @@ unsafe fn invoke_callback(closure_f64: f64, status: Status) { } let connected = bool_to_jsvalue(status.connected); let kind = nanbox_str(status.kind); - js_closure_call2(ptr, connected, kind); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, connected, kind); } fn classify(path: *mut c_void) -> Status { diff --git a/crates/perry-ui-macos/src/notifications.rs b/crates/perry-ui-macos/src/notifications.rs index 5bcea40b28..5cf71d7a79 100644 --- a/crates/perry-ui-macos/src/notifications.rs +++ b/crates/perry-ui-macos/src/notifications.rs @@ -38,8 +38,8 @@ pub(crate) fn scan_macos_notifications_gc_roots(visitor: &mut perry_ffi::GcRootV extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; - fn js_closure_call1(closure: *const u8, arg0: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg0: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_json_parse(text_ptr: *const perry_ffi::StringHeader) -> u64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; @@ -142,7 +142,7 @@ unsafe fn dispatch_tap(response: &AnyObject) { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call2(ptr, id_value, action_value); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, id_value, action_value); } } @@ -301,7 +301,7 @@ pub unsafe fn dispatch_device_token(device_token: *mut AnyObject) { let boxed = js_nanbox_string(str_ptr as i64); let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, boxed); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, boxed); } } @@ -368,7 +368,7 @@ pub unsafe fn dispatch_remote_payload(user_info: *mut AnyObject) { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, parsed_f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, parsed_f64); } } diff --git a/crates/perry-ui-macos/src/pointer.rs b/crates/perry-ui-macos/src/pointer.rs index 6e19ff7392..d610ab1e9e 100644 --- a/crates/perry-ui-macos/src/pointer.rs +++ b/crates/perry-ui-macos/src/pointer.rs @@ -29,7 +29,7 @@ use std::collections::HashMap; use crate::widgets::get_widget; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -296,7 +296,7 @@ unsafe fn handle_event(event: *mut AnyObject) { continue; } let pe = js_pointer_event_new(local_x, local_y, button, POINTER_TYPE_MOUSE); - js_closure_call1(closure_ptr as *const u8, pe); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, pe); } } @@ -340,7 +340,11 @@ unsafe fn dispatch_hover_transitions(event_window: *mut AnyObject, win_loc: CGPo continue; } let bool_bits = if inside { TAG_TRUE } else { TAG_FALSE }; - js_closure_call1(closure_ptr as *const u8, f64::from_bits(bool_bits)); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(bool_bits), + ); } } diff --git a/crates/perry-ui-macos/src/state.rs b/crates/perry-ui-macos/src/state.rs index 92fb232e1e..5b7e5c2afa 100644 --- a/crates/perry-ui-macos/src/state.rs +++ b/crates/perry-ui-macos/src/state.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -276,7 +276,7 @@ pub fn state_set(handle: i64, value: f64) { for closure_f64 in callbacks_snapshot { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } } @@ -317,7 +317,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; widgets::add_child(container, child_handle); } diff --git a/crates/perry-ui-macos/src/tray.rs b/crates/perry-ui-macos/src/tray.rs index e53a58ca32..c345220e95 100644 --- a/crates/perry-ui-macos/src/tray.rs +++ b/crates/perry-ui-macos/src/tray.rs @@ -30,7 +30,7 @@ pub(crate) fn scan_macos_tray_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_ } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -58,7 +58,7 @@ define_class!( if let Some(cf) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(cf) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); diff --git a/crates/perry-ui-macos/src/widgets/alert.rs b/crates/perry-ui-macos/src/widgets/alert.rs index d3a168435d..fba4b6f8c3 100644 --- a/crates/perry-ui-macos/src/widgets/alert.rs +++ b/crates/perry-ui-macos/src/widgets/alert.rs @@ -5,7 +5,7 @@ use objc2::runtime::{AnyClass, AnyObject}; use objc2_foundation::{MainThreadMarker, NSString}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -60,7 +60,7 @@ pub fn show(title_ptr: *const u8, message_ptr: *const u8, arr_ptr: i64, callback let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, button_index); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, button_index); } } } diff --git a/crates/perry-ui-macos/src/widgets/bottom_nav.rs b/crates/perry-ui-macos/src/widgets/bottom_nav.rs index 438ca0f4b0..3cfd8a8a0b 100644 --- a/crates/perry-ui-macos/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-macos/src/widgets/bottom_nav.rs @@ -21,7 +21,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -88,7 +88,7 @@ define_class!( if on_select != 0.0 { unsafe { let closure_ptr = js_nanbox_get_pointer(on_select) as *const u8; - js_closure_call1(closure_ptr, item_index as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, item_index as f64); } } } diff --git a/crates/perry-ui-macos/src/widgets/button.rs b/crates/perry-ui-macos/src/widgets/button.rs index 578d9fdb7d..865b972531 100644 --- a/crates/perry-ui-macos/src/widgets/button.rs +++ b/crates/perry-ui-macos/src/widgets/button.rs @@ -21,7 +21,7 @@ pub(crate) fn scan_macos_button_gc_roots(visitor: &mut perry_ffi::GcRootVisitor< } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -47,7 +47,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); diff --git a/crates/perry-ui-macos/src/widgets/calendar.rs b/crates/perry-ui-macos/src/widgets/calendar.rs index 4f90bd2c1a..c143e6d2bc 100644 --- a/crates/perry-ui-macos/src/widgets/calendar.rs +++ b/crates/perry-ui-macos/src/widgets/calendar.rs @@ -21,7 +21,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -67,7 +67,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } })); } diff --git a/crates/perry-ui-macos/src/widgets/combobox.rs b/crates/perry-ui-macos/src/widgets/combobox.rs index 08a13554a3..a90d0cc7fe 100644 --- a/crates/perry-ui-macos/src/widgets/combobox.rs +++ b/crates/perry-ui-macos/src/widgets/combobox.rs @@ -31,7 +31,7 @@ pub(crate) fn scan_macos_combobox_gc_roots(visitor: &mut perry_ffi::GcRootVisito } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -83,7 +83,7 @@ fn fire_callback(target_addr: usize, handle: i64) { let header_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let arg = js_nanbox_string(header_ptr as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } }), ); diff --git a/crates/perry-ui-macos/src/widgets/command_palette.rs b/crates/perry-ui-macos/src/widgets/command_palette.rs index df3ddb04cc..b425e99826 100644 --- a/crates/perry-ui-macos/src/widgets/command_palette.rs +++ b/crates/perry-ui-macos/src/widgets/command_palette.rs @@ -23,7 +23,7 @@ use objc2_foundation::{NSObject, NSString}; use std::cell::{Cell, RefCell}; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -186,7 +186,7 @@ unsafe fn invoke_row(row: i64) { let on_run = COMMANDS.with(|c| c.borrow().get(cmd_idx).map(|cmd| cmd.on_run).unwrap_or(0.0)); if on_run != 0.0 { let closure_ptr = js_nanbox_get_pointer(on_run) as *const u8; - js_closure_call0(closure_ptr); + js_closure_call0(closure_ptr, perry_ffi::JsThis::UNDEFINED); } hide(); } diff --git a/crates/perry-ui-macos/src/widgets/date_picker.rs b/crates/perry-ui-macos/src/widgets/date_picker.rs index 7890d6ab13..3d2b6edfd8 100644 --- a/crates/perry-ui-macos/src/widgets/date_picker.rs +++ b/crates/perry-ui-macos/src/widgets/date_picker.rs @@ -21,7 +21,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -67,7 +67,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } })); } diff --git a/crates/perry-ui-macos/src/widgets/foreach_registry.rs b/crates/perry-ui-macos/src/widgets/foreach_registry.rs index fd37a49e08..bd64bc0730 100644 --- a/crates/perry-ui-macos/src/widgets/foreach_registry.rs +++ b/crates/perry-ui-macos/src/widgets/foreach_registry.rs @@ -22,7 +22,7 @@ //! and call `widgets::add_child`. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -48,7 +48,8 @@ pub extern "C" fn render_handler(container_handle: i64, render_closure: f64, cou return; } for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; if child_handle != 0 { crate::widgets::add_child(container_handle, child_handle); diff --git a/crates/perry-ui-macos/src/widgets/image_gallery.rs b/crates/perry-ui-macos/src/widgets/image_gallery.rs index 0d255e8331..aa5e6839b3 100644 --- a/crates/perry-ui-macos/src/widgets/image_gallery.rs +++ b/crates/perry-ui-macos/src/widgets/image_gallery.rs @@ -22,7 +22,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -264,7 +264,7 @@ fn recompute_index(handle: i64) { if new_index >= 0 && closure != 0.0 { unsafe { let closure_ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call1(closure_ptr, new_index as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, new_index as f64); } } } diff --git a/crates/perry-ui-macos/src/widgets/lazyvstack.rs b/crates/perry-ui-macos/src/widgets/lazyvstack.rs index 08b187d922..70cff63bc3 100644 --- a/crates/perry-ui-macos/src/widgets/lazyvstack.rs +++ b/crates/perry-ui-macos/src/widgets/lazyvstack.rs @@ -8,8 +8,8 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -69,7 +69,7 @@ define_class!( return std::ptr::null_mut(); } let render_ptr = unsafe { js_nanbox_get_pointer(render_closure) } as *const u8; - let child_f64 = unsafe { js_closure_call1(render_ptr, row as f64) }; + let child_f64 = unsafe { js_closure_call1(render_ptr, perry_ffi::JsThis::UNDEFINED, row as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; if let Some(view) = super::get_widget(child_handle) { Retained::as_ptr(&view) as *mut NSView @@ -334,7 +334,7 @@ fn check_scroll_end(handle: i64) { if should_fire { unsafe { let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-macos/src/widgets/mod.rs b/crates/perry-ui-macos/src/widgets/mod.rs index fa0c0660e8..d753757817 100644 --- a/crates/perry-ui-macos/src/widgets/mod.rs +++ b/crates/perry-ui-macos/src/widgets/mod.rs @@ -1066,7 +1066,7 @@ pub fn match_parent_width(child_handle: i64) { // ============================================================================= extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -1239,7 +1239,7 @@ objc2::define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); diff --git a/crates/perry-ui-macos/src/widgets/picker.rs b/crates/perry-ui-macos/src/widgets/picker.rs index 5df1bf3a5d..5d674f5d6b 100644 --- a/crates/perry-ui-macos/src/widgets/picker.rs +++ b/crates/perry-ui-macos/src/widgets/picker.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_macos_picker_gc_roots(visitor: &mut perry_ffi::GcRootVisitor< } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -45,7 +45,7 @@ define_class!( let index: i64 = unsafe { msg_send![&*view, indexOfSelectedItem] }; let closure_ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; unsafe { - js_closure_call1(closure_ptr, index as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, index as f64); } } } diff --git a/crates/perry-ui-macos/src/widgets/rich_text.rs b/crates/perry-ui-macos/src/widgets/rich_text.rs index da9f19c367..d15d17185a 100644 --- a/crates/perry-ui-macos/src/widgets/rich_text.rs +++ b/crates/perry-ui-macos/src/widgets/rich_text.rs @@ -26,7 +26,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -85,7 +85,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } })); } diff --git a/crates/perry-ui-macos/src/widgets/scrollview.rs b/crates/perry-ui-macos/src/widgets/scrollview.rs index cac77febf2..2534865cd6 100644 --- a/crates/perry-ui-macos/src/widgets/scrollview.rs +++ b/crates/perry-ui-macos/src/widgets/scrollview.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; use std::sync::Once; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -298,7 +298,7 @@ fn check_scroll_end(handle: i64) { if should_fire { unsafe { let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-macos/src/widgets/securefield.rs b/crates/perry-ui-macos/src/widgets/securefield.rs index 589af841cd..ad00e42bef 100644 --- a/crates/perry-ui-macos/src/widgets/securefield.rs +++ b/crates/perry-ui-macos/src/widgets/securefield.rs @@ -22,7 +22,7 @@ pub(crate) fn scan_macos_securefield_gc_roots(visitor: &mut perry_ffi::GcRootVis } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -68,7 +68,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-macos/src/widgets/slider.rs b/crates/perry-ui-macos/src/widgets/slider.rs index 6ae847ad55..1bb71d877c 100644 --- a/crates/perry-ui-macos/src/widgets/slider.rs +++ b/crates/perry-ui-macos/src/widgets/slider.rs @@ -20,7 +20,7 @@ pub(crate) fn scan_macos_slider_gc_roots(visitor: &mut perry_ffi::GcRootVisitor< } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -45,7 +45,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value); } } }); diff --git a/crates/perry-ui-macos/src/widgets/table.rs b/crates/perry-ui-macos/src/widgets/table.rs index dbc77dbf9d..e2c8dc55d0 100644 --- a/crates/perry-ui-macos/src/widgets/table.rs +++ b/crates/perry-ui-macos/src/widgets/table.rs @@ -8,8 +8,8 @@ use objc2_foundation::{MainThreadMarker, NSObject, NSString}; use std::cell::RefCell; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -129,7 +129,7 @@ define_class!( return std::ptr::null_mut(); } let render_ptr = unsafe { js_nanbox_get_pointer(render_closure) } as *const u8; - let child_f64 = unsafe { js_closure_call2(render_ptr, row as f64, col as f64) }; + let child_f64 = unsafe { js_closure_call2(render_ptr, perry_ffi::JsThis::UNDEFINED, row as f64, col as f64) }; // Issue #556: tag-validate before treating the closure // return as a widget handle. The closure SHOULD return a // Widget (POINTER_TAG = 0x7FFD), but if user code @@ -198,7 +198,7 @@ define_class!( let ascending: objc2::runtime::Bool = msg_send![first, ascending]; let asc_f = if ascending.as_bool() { 1.0 } else { 0.0 }; let closure_ptr = js_nanbox_get_pointer(sort_closure) as *const u8; - js_closure_call2(closure_ptr, col_index as f64, asc_f); + js_closure_call2(closure_ptr, perry_ffi::JsThis::UNDEFINED, col_index as f64, asc_f); } })); } @@ -225,7 +225,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(select_closure) } as *const u8; unsafe { - js_closure_call1(closure_ptr, selected_row as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, selected_row as f64); } } })); diff --git a/crates/perry-ui-macos/src/widgets/textarea.rs b/crates/perry-ui-macos/src/widgets/textarea.rs index 24344e5f4c..c55a7aafbf 100644 --- a/crates/perry-ui-macos/src/widgets/textarea.rs +++ b/crates/perry-ui-macos/src/widgets/textarea.rs @@ -22,7 +22,7 @@ pub(crate) fn scan_macos_textarea_gc_roots(visitor: &mut perry_ffi::GcRootVisito } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -65,7 +65,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-macos/src/widgets/textfield.rs b/crates/perry-ui-macos/src/widgets/textfield.rs index 0bd4c51534..f7cba04fd4 100644 --- a/crates/perry-ui-macos/src/widgets/textfield.rs +++ b/crates/perry-ui-macos/src/widgets/textfield.rs @@ -40,7 +40,7 @@ pub(crate) fn scan_macos_textfield_gc_roots(visitor: &mut perry_ffi::GcRootVisit } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -120,7 +120,7 @@ define_class!( b.suppress.set(true); } unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } if let Some(b) = binding { b.suppress.set(false); @@ -184,7 +184,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); @@ -461,7 +461,7 @@ define_class!( } else { return; } let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, 0.0); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, 0.0); } } }); diff --git a/crates/perry-ui-macos/src/widgets/toggle.rs b/crates/perry-ui-macos/src/widgets/toggle.rs index eef55a5378..185a1abc81 100644 --- a/crates/perry-ui-macos/src/widgets/toggle.rs +++ b/crates/perry-ui-macos/src/widgets/toggle.rs @@ -28,7 +28,7 @@ const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -58,7 +58,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value); } } }); diff --git a/crates/perry-ui-macos/src/widgets/toolbar.rs b/crates/perry-ui-macos/src/widgets/toolbar.rs index f7f3776384..71d2ad1368 100644 --- a/crates/perry-ui-macos/src/widgets/toolbar.rs +++ b/crates/perry-ui-macos/src/widgets/toolbar.rs @@ -8,7 +8,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -67,7 +67,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } })); diff --git a/crates/perry-ui-macos/src/widgets/tree_view.rs b/crates/perry-ui-macos/src/widgets/tree_view.rs index ee303662b1..653dfb5209 100644 --- a/crates/perry-ui-macos/src/widgets/tree_view.rs +++ b/crates/perry-ui-macos/src/widgets/tree_view.rs @@ -24,7 +24,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -247,7 +247,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let arg = js_nanbox_string(header as i64); let closure = js_nanbox_get_pointer(on_select) as *const u8; - js_closure_call1(closure, arg); + js_closure_call1(closure, perry_ffi::JsThis::UNDEFINED, arg); } })); } diff --git a/crates/perry-ui-macos/src/widgets/webview.rs b/crates/perry-ui-macos/src/widgets/webview.rs index fcd3fb66da..7b01eb011d 100644 --- a/crates/perry-ui-macos/src/widgets/webview.rs +++ b/crates/perry-ui-macos/src/widgets/webview.rs @@ -32,9 +32,9 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; fn js_is_truthy(value: f64) -> i32; @@ -347,7 +347,7 @@ define_class!( if closure_ptr.is_null() { return; // default ALLOW } - let result = unsafe { js_closure_call1(closure_ptr, url_nb) }; + let result = unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb) }; // Per the API: undefined / no return = allow (matches JS // "implicit allow"); explicit `false` / 0 / null = cancel. @@ -425,7 +425,7 @@ define_class!( let url_nb = nanbox_str(&url_str); let closure_ptr = js_nanbox_get_pointer(on_loaded) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, url_nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb); } }), ); @@ -490,7 +490,12 @@ impl PerryWebViewDelegate { let msg_nb = nanbox_str(&msg); let closure_ptr = js_nanbox_get_pointer(on_error) as *const u8; if !closure_ptr.is_null() { - js_closure_call2(closure_ptr, code as f64, msg_nb); + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + code as f64, + msg_nb, + ); } }), ); @@ -683,7 +688,7 @@ pub fn evaluate_js(handle: i64, js_ptr: *const u8, callback: f64) { let nb = nanbox_str(&s); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb); } }), ); @@ -834,5 +839,5 @@ fn webview_for_handle(handle: i64) -> Option<*mut AnyObject> { /// future event hooks the design names but doesn't ship in Phase 1. #[allow(dead_code)] fn _used_js_closure_call0() { - let _ = js_closure_call0 as unsafe extern "C" fn(_) -> _; + let _ = js_closure_call0 as unsafe extern "C" fn(_, _) -> _; } diff --git a/crates/perry-ui-tvos/src/app.rs b/crates/perry-ui-tvos/src/app.rs index c31b325369..aa27f3d0ca 100644 --- a/crates/perry-ui-tvos/src/app.rs +++ b/crates/perry-ui-tvos/src/app.rs @@ -451,7 +451,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_callback_timer_tick() -> i32; fn js_interval_timer_tick() -> i32; fn js_frame_pump_default() -> i32; @@ -576,7 +576,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-tvos/src/audio_playback.rs b/crates/perry-ui-tvos/src/audio_playback.rs index 8d8f5a12e3..b651533bce 100644 --- a/crates/perry-ui-tvos/src/audio_playback.rs +++ b/crates/perry-ui-tvos/src/audio_playback.rs @@ -16,9 +16,9 @@ use std::cell::RefCell; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; #[allow(dead_code)] - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -653,7 +653,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -712,7 +712,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } if should_clean { diff --git a/crates/perry-ui-tvos/src/background.rs b/crates/perry-ui-tvos/src/background.rs index 0d49ff7daa..090c2a51f9 100644 --- a/crates/perry-ui-tvos/src/background.rs +++ b/crates/perry-ui-tvos/src/background.rs @@ -23,7 +23,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_value_is_promise(value: f64) -> i32; } @@ -67,7 +67,7 @@ unsafe fn invoke_handler(handler: f64, task: *mut AnyObject) { js_run_stdlib_pump(); js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(handler) as *const u8; - let result = js_closure_call0(ptr); + let result = js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); js_promise_run_microtasks(); let _ = js_value_is_promise(result); let _: () = msg_send![task, setTaskCompletedWithSuccess: true]; diff --git a/crates/perry-ui-tvos/src/file_dialog.rs b/crates/perry-ui-tvos/src/file_dialog.rs index 1a3bf1fe7f..299693d226 100644 --- a/crates/perry-ui-tvos/src/file_dialog.rs +++ b/crates/perry-ui-tvos/src/file_dialog.rs @@ -1,5 +1,5 @@ extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -9,6 +9,10 @@ pub fn open_dialog(callback: f64) { unsafe { let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; // Call callback with TAG_UNDEFINED (user "cancelled") - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } diff --git a/crates/perry-ui-tvos/src/issue_552_stub.rs b/crates/perry-ui-tvos/src/issue_552_stub.rs index fdfb77da44..bf54a94c58 100644 --- a/crates/perry-ui-tvos/src/issue_552_stub.rs +++ b/crates/perry-ui-tvos/src/issue_552_stub.rs @@ -6,7 +6,7 @@ //! picker callback receives an empty array. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -30,7 +30,7 @@ pub extern "C" fn perry_system_geolocation_get_current(_on_success: f64, on_erro } let nb = unsupported_string(); let cb = js_nanbox_get_pointer(on_error) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -47,7 +47,7 @@ pub extern "C" fn perry_system_geolocation_request_permission(callback: f64) { unsafe { let nb = unsupported_string(); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -61,6 +61,6 @@ pub extern "C" fn perry_system_image_picker_pick( let arr = js_array_alloc(0); let nb = js_nanbox_pointer(arr as i64); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } diff --git a/crates/perry-ui-tvos/src/location.rs b/crates/perry-ui-tvos/src/location.rs index e6da382ae8..cb7b37dfa2 100644 --- a/crates/perry-ui-tvos/src/location.rs +++ b/crates/perry-ui-tvos/src/location.rs @@ -7,7 +7,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; } // Raw ObjC runtime FFI for dynamic class registration @@ -51,7 +51,12 @@ unsafe fn invoke_callback(lat: f64, lon: f64) { js_run_stdlib_pump(); js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call2(closure_ptr as *const u8, lat, lon); + js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + lat, + lon, + ); } } diff --git a/crates/perry-ui-tvos/src/media_playback.rs b/crates/perry-ui-tvos/src/media_playback.rs index 3bc91c408a..1bfe58dd75 100644 --- a/crates/perry-ui-tvos/src/media_playback.rs +++ b/crates/perry-ui-tvos/src/media_playback.rs @@ -27,8 +27,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; // Signature matches the rest of perry-ui-macos (see audio.rs:133, // lib.rs:113/118/1662). Runtime returns `*mut StringHeader` which is // i64-sized on Apple platforms. @@ -775,7 +775,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -784,7 +788,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-tvos/src/menu.rs b/crates/perry-ui-tvos/src/menu.rs index 8f01a1d04e..002a0d7f01 100644 --- a/crates/perry-ui-tvos/src/menu.rs +++ b/crates/perry-ui-tvos/src/menu.rs @@ -9,7 +9,7 @@ use objc2::runtime::{AnyClass, AnyObject, Sel}; use std::cell::RefCell; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -400,7 +400,7 @@ pub(crate) unsafe fn dispatch_menu_action(sender: *mut AnyObject) { }); if let Some(cb) = callback { let ptr = js_nanbox_get_pointer(cb) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-tvos/src/network_stub.rs b/crates/perry-ui-tvos/src/network_stub.rs index 0ca620f5a8..a29356026e 100644 --- a/crates/perry-ui-tvos/src/network_stub.rs +++ b/crates/perry-ui-tvos/src/network_stub.rs @@ -7,7 +7,7 @@ //! true reachability detection should target iOS / macOS / Android. extern "C" { - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -28,7 +28,12 @@ pub extern "C" fn perry_system_network_get_status(callback: f64) { if cb.is_null() { return; } - js_closure_call2(cb, f64::from_bits(TAG_TRUE), nb_str("unknown")); + js_closure_call2( + cb, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(TAG_TRUE), + nb_str("unknown"), + ); } } diff --git a/crates/perry-ui-tvos/src/pointer.rs b/crates/perry-ui-tvos/src/pointer.rs index 8a05a2ba5a..ca61e96c9a 100644 --- a/crates/perry-ui-tvos/src/pointer.rs +++ b/crates/perry-ui-tvos/src/pointer.rs @@ -23,7 +23,7 @@ use std::collections::HashMap; use crate::widgets::get_widget; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -142,7 +142,7 @@ fn dispatch_phase(recog: &PerryPointerRecognizer, touches: &AnyObject, phase: u3 return; } let pe = js_pointer_event_new(local.x, local.y, 0, POINTER_TYPE_TOUCH); - js_closure_call1(closure_ptr as *const u8, pe); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, pe); } } diff --git a/crates/perry-ui-tvos/src/state.rs b/crates/perry-ui-tvos/src/state.rs index e753c76e85..fd3701c631 100644 --- a/crates/perry-ui-tvos/src/state.rs +++ b/crates/perry-ui-tvos/src/state.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -236,7 +236,7 @@ pub fn state_set(handle: i64, value: f64) { for closure_f64 in callbacks_snapshot { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } } @@ -274,7 +274,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; widgets::add_child(container, child_handle); } diff --git a/crates/perry-ui-tvos/src/widgets/alert.rs b/crates/perry-ui-tvos/src/widgets/alert.rs index a8eea1bb3b..9357fcd3e3 100644 --- a/crates/perry-ui-tvos/src/widgets/alert.rs +++ b/crates/perry-ui-tvos/src/widgets/alert.rs @@ -5,7 +5,7 @@ use objc2::runtime::{AnyClass, AnyObject}; use objc2_foundation::NSString; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_array_get_length(arr: i64) -> i64; fn js_array_get_element(arr: i64, index: i64) -> f64; @@ -26,7 +26,7 @@ unsafe extern "C" fn alert_callback_trampoline(ctx: *mut std::ffi::c_void) { let (closure, idx) = *pkg; let ptr = js_nanbox_get_pointer(closure) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, idx as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); } }); } diff --git a/crates/perry-ui-tvos/src/widgets/bottom_nav.rs b/crates/perry-ui-tvos/src/widgets/bottom_nav.rs index 8673f4ef15..330aea4ac9 100644 --- a/crates/perry-ui-tvos/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-tvos/src/widgets/bottom_nav.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -92,7 +92,7 @@ unsafe extern "C" fn trampoline(ctx: *mut std::ffi::c_void) { let pkg = Box::from_raw(ctx as *mut (f64, i64)); let (closure, idx) = *pkg; let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call1(ptr, idx as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); }); } diff --git a/crates/perry-ui-tvos/src/widgets/button.rs b/crates/perry-ui-tvos/src/widgets/button.rs index c061c76fcc..99cf3e5f52 100644 --- a/crates/perry-ui-tvos/src/widgets/button.rs +++ b/crates/perry-ui-tvos/src/widgets/button.rs @@ -24,7 +24,7 @@ pub(crate) fn scan_tvos_button_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<' } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; // dispatch_get_main_queue() is a macro; the actual symbol is _dispatch_main_q static _dispatch_main_q: std::ffi::c_void; @@ -38,7 +38,7 @@ extern "C" { unsafe extern "C" fn button_callback_trampoline(context: *mut std::ffi::c_void) { let closure_f64 = f64::from_bits(context as u64); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } pub struct PerryButtonTargetIvars { diff --git a/crates/perry-ui-tvos/src/widgets/mod.rs b/crates/perry-ui-tvos/src/widgets/mod.rs index f17811f160..4fa401dafe 100644 --- a/crates/perry-ui-tvos/src/widgets/mod.rs +++ b/crates/perry-ui-tvos/src/widgets/mod.rs @@ -522,7 +522,7 @@ pub fn set_hugging_priority(handle: i64, priority: f64) { use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } diff --git a/crates/perry-ui-tvos/src/widgets/scrollview.rs b/crates/perry-ui-tvos/src/widgets/scrollview.rs index 7e0fcb0e0f..6956364560 100644 --- a/crates/perry-ui-tvos/src/widgets/scrollview.rs +++ b/crates/perry-ui-tvos/src/widgets/scrollview.rs @@ -8,7 +8,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -21,7 +21,7 @@ extern "C" { unsafe extern "C" fn refresh_callback_trampoline(context: *mut std::ffi::c_void) { let closure_f64 = f64::from_bits(context as u64); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } thread_local! { diff --git a/crates/perry-ui-tvos/src/widgets/securefield.rs b/crates/perry-ui-tvos/src/widgets/securefield.rs index b21b18f335..bc23c94c52 100644 --- a/crates/perry-ui-tvos/src/widgets/securefield.rs +++ b/crates/perry-ui-tvos/src/widgets/securefield.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_tvos_securefield_gc_roots(visitor: &mut perry_ffi::GcRootVisi } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -50,7 +50,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-tvos/src/widgets/slider.rs b/crates/perry-ui-tvos/src/widgets/slider.rs index 9c42b0138b..f906192ea3 100644 --- a/crates/perry-ui-tvos/src/widgets/slider.rs +++ b/crates/perry-ui-tvos/src/widgets/slider.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_tvos_slider_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<' } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -43,7 +43,7 @@ define_class!( let value: f32 = unsafe { msg_send![sender, value] }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value as f64); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value as f64); } } }); diff --git a/crates/perry-ui-tvos/src/widgets/tabbar.rs b/crates/perry-ui-tvos/src/widgets/tabbar.rs index cd7f3f12e2..36ad72d261 100644 --- a/crates/perry-ui-tvos/src/widgets/tabbar.rs +++ b/crates/perry-ui-tvos/src/widgets/tabbar.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -115,7 +115,11 @@ unsafe extern "C" fn tab_callback_trampoline(context: *mut std::ffi::c_void) { let packed = Box::from_raw(context as *mut (f64, i64)); let (closure_f64, tab_index) = *packed; let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call1(closure_ptr as *const u8, tab_index as f64); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + tab_index as f64, + ); } // ── Public API ──────────────────────────────────────────────── diff --git a/crates/perry-ui-tvos/src/widgets/textarea.rs b/crates/perry-ui-tvos/src/widgets/textarea.rs index 5b9d37728e..4743d6c7df 100644 --- a/crates/perry-ui-tvos/src/widgets/textarea.rs +++ b/crates/perry-ui-tvos/src/widgets/textarea.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_tvos_textarea_gc_roots(visitor: &mut perry_ffi::GcRootVisitor } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -50,7 +50,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-tvos/src/widgets/textfield.rs b/crates/perry-ui-tvos/src/widgets/textfield.rs index cf395aa6c0..658fd598f2 100644 --- a/crates/perry-ui-tvos/src/widgets/textfield.rs +++ b/crates/perry-ui-tvos/src/widgets/textfield.rs @@ -24,7 +24,7 @@ pub(crate) fn scan_tvos_textfield_gc_roots(visitor: &mut perry_ffi::GcRootVisito } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -55,7 +55,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); @@ -236,7 +236,7 @@ define_class!( let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-tvos/src/widgets/toggle.rs b/crates/perry-ui-tvos/src/widgets/toggle.rs index 15d7075c1a..8bf899f845 100644 --- a/crates/perry-ui-tvos/src/widgets/toggle.rs +++ b/crates/perry-ui-tvos/src/widgets/toggle.rs @@ -23,7 +23,7 @@ const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -53,7 +53,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value); } } }); diff --git a/crates/perry-ui-visionos/src/app.rs b/crates/perry-ui-visionos/src/app.rs index d19a7c8647..83f8115fb3 100644 --- a/crates/perry-ui-visionos/src/app.rs +++ b/crates/perry-ui-visionos/src/app.rs @@ -302,7 +302,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_callback_timer_tick() -> i32; fn js_interval_timer_tick() -> i32; fn js_frame_pump_default() -> i32; @@ -386,7 +386,7 @@ define_class!( if let Some(closure_f64) = closure_f64 { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-visionos/src/audio_playback.rs b/crates/perry-ui-visionos/src/audio_playback.rs index e03194ca2e..312d78de33 100644 --- a/crates/perry-ui-visionos/src/audio_playback.rs +++ b/crates/perry-ui-visionos/src/audio_playback.rs @@ -16,9 +16,9 @@ use std::cell::RefCell; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; #[allow(dead_code)] - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -653,7 +653,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -712,7 +712,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } if should_clean { diff --git a/crates/perry-ui-visionos/src/background.rs b/crates/perry-ui-visionos/src/background.rs index 8789bc8ae4..708c0b802f 100644 --- a/crates/perry-ui-visionos/src/background.rs +++ b/crates/perry-ui-visionos/src/background.rs @@ -24,7 +24,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_value_is_promise(value: f64) -> i32; } @@ -68,7 +68,7 @@ unsafe fn invoke_handler(handler: f64, task: *mut AnyObject) { js_run_stdlib_pump(); js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(handler) as *const u8; - let result = js_closure_call0(ptr); + let result = js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); js_promise_run_microtasks(); let _ = js_value_is_promise(result); let _: () = msg_send![task, setTaskCompletedWithSuccess: true]; diff --git a/crates/perry-ui-visionos/src/camera.rs b/crates/perry-ui-visionos/src/camera.rs index e7bcb1a9d1..a41ef3e2df 100644 --- a/crates/perry-ui-visionos/src/camera.rs +++ b/crates/perry-ui-visionos/src/camera.rs @@ -15,8 +15,8 @@ use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use crate::widgets; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; @@ -254,7 +254,12 @@ fn register_tap_handler_class() -> *const c_void { js_run_stdlib_pump(); js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call2(closure_ptr as *const u8, norm_x, norm_y); + js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + norm_x, + norm_y, + ); } }); } diff --git a/crates/perry-ui-visionos/src/drag_drop.rs b/crates/perry-ui-visionos/src/drag_drop.rs index 674aa1bcbe..252bcce226 100644 --- a/crates/perry-ui-visionos/src/drag_drop.rs +++ b/crates/perry-ui-visionos/src/drag_drop.rs @@ -36,8 +36,8 @@ use std::ffi::c_void; use std::rc::Rc; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; @@ -86,7 +86,7 @@ unsafe fn call_provider(cb: f64) -> Option { if p.is_null() { return None; } - let ret = js_closure_call0(p); + let ret = js_closure_call0(p, perry_ffi::JsThis::UNDEFINED); let sh = js_jsvalue_to_string(ret); if sh.is_null() { None @@ -204,7 +204,7 @@ unsafe fn perform_drop_impl(interaction: *mut AnyObject, session: *mut AnyObject } let payload = js_nanbox_pointer(obj as i64); let cb_ptr = js_nanbox_get_pointer(a.cb) as *const u8; - js_closure_call1(cb_ptr, payload); + js_closure_call1(cb_ptr, perry_ffi::JsThis::UNDEFINED, payload); } // text diff --git a/crates/perry-ui-visionos/src/file_dialog.rs b/crates/perry-ui-visionos/src/file_dialog.rs index 1a3bf1fe7f..299693d226 100644 --- a/crates/perry-ui-visionos/src/file_dialog.rs +++ b/crates/perry-ui-visionos/src/file_dialog.rs @@ -1,5 +1,5 @@ extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -9,6 +9,10 @@ pub fn open_dialog(callback: f64) { unsafe { let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; // Call callback with TAG_UNDEFINED (user "cancelled") - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } diff --git a/crates/perry-ui-visionos/src/issue_552_stub.rs b/crates/perry-ui-visionos/src/issue_552_stub.rs index fdfb77da44..bf54a94c58 100644 --- a/crates/perry-ui-visionos/src/issue_552_stub.rs +++ b/crates/perry-ui-visionos/src/issue_552_stub.rs @@ -6,7 +6,7 @@ //! picker callback receives an empty array. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -30,7 +30,7 @@ pub extern "C" fn perry_system_geolocation_get_current(_on_success: f64, on_erro } let nb = unsupported_string(); let cb = js_nanbox_get_pointer(on_error) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -47,7 +47,7 @@ pub extern "C" fn perry_system_geolocation_request_permission(callback: f64) { unsafe { let nb = unsupported_string(); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -61,6 +61,6 @@ pub extern "C" fn perry_system_image_picker_pick( let arr = js_array_alloc(0); let nb = js_nanbox_pointer(arr as i64); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } diff --git a/crates/perry-ui-visionos/src/location.rs b/crates/perry-ui-visionos/src/location.rs index f8ccab51f5..ff98f6adaf 100644 --- a/crates/perry-ui-visionos/src/location.rs +++ b/crates/perry-ui-visionos/src/location.rs @@ -7,7 +7,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; } // Raw ObjC runtime FFI for dynamic class registration @@ -51,7 +51,12 @@ unsafe fn invoke_callback(lat: f64, lon: f64) { js_run_stdlib_pump(); js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call2(closure_ptr as *const u8, lat, lon); + js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + lat, + lon, + ); } } diff --git a/crates/perry-ui-visionos/src/media_playback.rs b/crates/perry-ui-visionos/src/media_playback.rs index 3236c1bc67..a20d24aa40 100644 --- a/crates/perry-ui-visionos/src/media_playback.rs +++ b/crates/perry-ui-visionos/src/media_playback.rs @@ -27,8 +27,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; // Signature matches the rest of perry-ui-macos (see audio.rs:133, // lib.rs:113/118/1662). Runtime returns `*mut StringHeader` which is // i64-sized on Apple platforms. @@ -775,7 +775,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -784,7 +788,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-visionos/src/menu.rs b/crates/perry-ui-visionos/src/menu.rs index d88b0e08de..cb78c86e1f 100644 --- a/crates/perry-ui-visionos/src/menu.rs +++ b/crates/perry-ui-visionos/src/menu.rs @@ -9,7 +9,7 @@ use objc2::runtime::{AnyClass, AnyObject, Sel}; use std::cell::RefCell; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -435,7 +435,7 @@ pub(crate) unsafe fn dispatch_menu_action(sender: *mut AnyObject) { }); if let Some(cb) = callback { let ptr = js_nanbox_get_pointer(cb) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-visionos/src/network_stub.rs b/crates/perry-ui-visionos/src/network_stub.rs index 0ca620f5a8..a29356026e 100644 --- a/crates/perry-ui-visionos/src/network_stub.rs +++ b/crates/perry-ui-visionos/src/network_stub.rs @@ -7,7 +7,7 @@ //! true reachability detection should target iOS / macOS / Android. extern "C" { - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -28,7 +28,12 @@ pub extern "C" fn perry_system_network_get_status(callback: f64) { if cb.is_null() { return; } - js_closure_call2(cb, f64::from_bits(TAG_TRUE), nb_str("unknown")); + js_closure_call2( + cb, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(TAG_TRUE), + nb_str("unknown"), + ); } } diff --git a/crates/perry-ui-visionos/src/pointer.rs b/crates/perry-ui-visionos/src/pointer.rs index 6f756bd7b8..ed7e3ed743 100644 --- a/crates/perry-ui-visionos/src/pointer.rs +++ b/crates/perry-ui-visionos/src/pointer.rs @@ -23,7 +23,7 @@ use std::collections::HashMap; use crate::widgets::get_widget; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -142,7 +142,7 @@ fn dispatch_phase(recog: &PerryPointerRecognizer, touches: &AnyObject, phase: u3 return; } let pe = js_pointer_event_new(local.x, local.y, 0, POINTER_TYPE_TOUCH); - js_closure_call1(closure_ptr as *const u8, pe); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, pe); } } diff --git a/crates/perry-ui-visionos/src/state.rs b/crates/perry-ui-visionos/src/state.rs index f2511612e2..c59134a08a 100644 --- a/crates/perry-ui-visionos/src/state.rs +++ b/crates/perry-ui-visionos/src/state.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -236,7 +236,7 @@ pub fn state_set(handle: i64, value: f64) { for closure_f64 in callbacks_snapshot { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } } @@ -274,7 +274,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; widgets::add_child(container, child_handle); } diff --git a/crates/perry-ui-visionos/src/widgets/alert.rs b/crates/perry-ui-visionos/src/widgets/alert.rs index a8eea1bb3b..9357fcd3e3 100644 --- a/crates/perry-ui-visionos/src/widgets/alert.rs +++ b/crates/perry-ui-visionos/src/widgets/alert.rs @@ -5,7 +5,7 @@ use objc2::runtime::{AnyClass, AnyObject}; use objc2_foundation::NSString; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_array_get_length(arr: i64) -> i64; fn js_array_get_element(arr: i64, index: i64) -> f64; @@ -26,7 +26,7 @@ unsafe extern "C" fn alert_callback_trampoline(ctx: *mut std::ffi::c_void) { let (closure, idx) = *pkg; let ptr = js_nanbox_get_pointer(closure) as *const u8; if !ptr.is_null() { - js_closure_call1(ptr, idx as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); } }); } diff --git a/crates/perry-ui-visionos/src/widgets/bottom_nav.rs b/crates/perry-ui-visionos/src/widgets/bottom_nav.rs index e44995efc1..282bdbbd09 100644 --- a/crates/perry-ui-visionos/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-visionos/src/widgets/bottom_nav.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -92,7 +92,7 @@ unsafe extern "C" fn trampoline(ctx: *mut std::ffi::c_void) { let pkg = Box::from_raw(ctx as *mut (f64, i64)); let (closure, idx) = *pkg; let ptr = js_nanbox_get_pointer(closure) as *const u8; - js_closure_call1(ptr, idx as f64); + js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); }); } diff --git a/crates/perry-ui-visionos/src/widgets/button.rs b/crates/perry-ui-visionos/src/widgets/button.rs index d9006ba353..6f2f6171d8 100644 --- a/crates/perry-ui-visionos/src/widgets/button.rs +++ b/crates/perry-ui-visionos/src/widgets/button.rs @@ -24,7 +24,7 @@ pub(crate) fn scan_visionos_button_gc_roots(visitor: &mut perry_ffi::GcRootVisit } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; // dispatch_get_main_queue() is a macro; the actual symbol is _dispatch_main_q static _dispatch_main_q: std::ffi::c_void; @@ -39,7 +39,7 @@ unsafe extern "C" fn button_callback_trampoline(context: *mut std::ffi::c_void) let _ = std::panic::catch_unwind(|| { let closure_f64 = f64::from_bits(context as u64); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); }); } diff --git a/crates/perry-ui-visionos/src/widgets/calendar.rs b/crates/perry-ui-visionos/src/widgets/calendar.rs index d6de68a04c..c23720e398 100644 --- a/crates/perry-ui-visionos/src/widgets/calendar.rs +++ b/crates/perry-ui-visionos/src/widgets/calendar.rs @@ -17,7 +17,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -63,7 +63,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } diff --git a/crates/perry-ui-visionos/src/widgets/combobox.rs b/crates/perry-ui-visionos/src/widgets/combobox.rs index 648d4a255e..8a4fa47a37 100644 --- a/crates/perry-ui-visionos/src/widgets/combobox.rs +++ b/crates/perry-ui-visionos/src/widgets/combobox.rs @@ -24,7 +24,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -60,7 +60,7 @@ fn fire_callback(handle: i64, value: &str) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); }), ); } diff --git a/crates/perry-ui-visionos/src/widgets/date_picker.rs b/crates/perry-ui-visionos/src/widgets/date_picker.rs index f98d080e70..0732894774 100644 --- a/crates/perry-ui-visionos/src/widgets/date_picker.rs +++ b/crates/perry-ui-visionos/src/widgets/date_picker.rs @@ -18,7 +18,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -64,7 +64,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } diff --git a/crates/perry-ui-visionos/src/widgets/mod.rs b/crates/perry-ui-visionos/src/widgets/mod.rs index 00c455a7e4..3d242cbf50 100644 --- a/crates/perry-ui-visionos/src/widgets/mod.rs +++ b/crates/perry-ui-visionos/src/widgets/mod.rs @@ -681,7 +681,7 @@ pub fn set_hugging_priority(handle: i64, priority: f64) { use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } diff --git a/crates/perry-ui-visionos/src/widgets/rich_text.rs b/crates/perry-ui-visionos/src/widgets/rich_text.rs index ec25f9b50c..53967dc256 100644 --- a/crates/perry-ui-visionos/src/widgets/rich_text.rs +++ b/crates/perry-ui-visionos/src/widgets/rich_text.rs @@ -19,7 +19,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -74,7 +74,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } } diff --git a/crates/perry-ui-visionos/src/widgets/scrollview.rs b/crates/perry-ui-visionos/src/widgets/scrollview.rs index 02dcea5d7c..712fc02eea 100644 --- a/crates/perry-ui-visionos/src/widgets/scrollview.rs +++ b/crates/perry-ui-visionos/src/widgets/scrollview.rs @@ -8,7 +8,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -22,7 +22,7 @@ unsafe extern "C" fn refresh_callback_trampoline(context: *mut std::ffi::c_void) let _ = std::panic::catch_unwind(|| { let closure_f64 = f64::from_bits(context as u64); let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); }); } diff --git a/crates/perry-ui-visionos/src/widgets/securefield.rs b/crates/perry-ui-visionos/src/widgets/securefield.rs index bba267f864..5a75394c6b 100644 --- a/crates/perry-ui-visionos/src/widgets/securefield.rs +++ b/crates/perry-ui-visionos/src/widgets/securefield.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_visionos_securefield_gc_roots(visitor: &mut perry_ffi::GcRoot } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -50,7 +50,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-visionos/src/widgets/slider.rs b/crates/perry-ui-visionos/src/widgets/slider.rs index bcd3ed880c..92be2f2ba2 100644 --- a/crates/perry-ui-visionos/src/widgets/slider.rs +++ b/crates/perry-ui-visionos/src/widgets/slider.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_visionos_slider_gc_roots(visitor: &mut perry_ffi::GcRootVisit } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -43,7 +43,7 @@ define_class!( let value: f32 = unsafe { msg_send![sender, value] }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, value as f64); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, value as f64); } } }); diff --git a/crates/perry-ui-visionos/src/widgets/tabbar.rs b/crates/perry-ui-visionos/src/widgets/tabbar.rs index 070e02f1d5..bb04515e8d 100644 --- a/crates/perry-ui-visionos/src/widgets/tabbar.rs +++ b/crates/perry-ui-visionos/src/widgets/tabbar.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -116,7 +116,11 @@ unsafe extern "C" fn tab_callback_trampoline(context: *mut std::ffi::c_void) { let packed = Box::from_raw(context as *mut (f64, i64)); let (closure_f64, tab_index) = *packed; let closure_ptr = js_nanbox_get_pointer(closure_f64); - js_closure_call1(closure_ptr as *const u8, tab_index as f64); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + tab_index as f64, + ); }); } diff --git a/crates/perry-ui-visionos/src/widgets/textarea.rs b/crates/perry-ui-visionos/src/widgets/textarea.rs index da1c81e07e..ffba5c2196 100644 --- a/crates/perry-ui-visionos/src/widgets/textarea.rs +++ b/crates/perry-ui-visionos/src/widgets/textarea.rs @@ -19,7 +19,7 @@ pub(crate) fn scan_visionos_textarea_gc_roots(visitor: &mut perry_ffi::GcRootVis } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -51,7 +51,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-visionos/src/widgets/textfield.rs b/crates/perry-ui-visionos/src/widgets/textfield.rs index d3486c1f0d..b859cc507a 100644 --- a/crates/perry-ui-visionos/src/widgets/textfield.rs +++ b/crates/perry-ui-visionos/src/widgets/textfield.rs @@ -24,7 +24,7 @@ pub(crate) fn scan_visionos_textfield_gc_roots(visitor: &mut perry_ffi::GcRootVi } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -55,7 +55,7 @@ define_class!( let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); @@ -247,7 +247,7 @@ define_class!( let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) }; unsafe { - js_closure_call1(closure_ptr as *const u8, nanboxed); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, nanboxed); } } }); diff --git a/crates/perry-ui-visionos/src/widgets/tree_view.rs b/crates/perry-ui-visionos/src/widgets/tree_view.rs index d339ff74aa..1e1c192d2c 100644 --- a/crates/perry-ui-visionos/src/widgets/tree_view.rs +++ b/crates/perry-ui-visionos/src/widgets/tree_view.rs @@ -27,7 +27,7 @@ use objc2_ui_kit::UIView; use std::cell::{Cell, RefCell}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -311,7 +311,7 @@ define_class!( let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on_select) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); }), ); } diff --git a/crates/perry-ui-visionos/src/widgets/webview.rs b/crates/perry-ui-visionos/src/widgets/webview.rs index fedf77d062..8983e730b5 100644 --- a/crates/perry-ui-visionos/src/widgets/webview.rs +++ b/crates/perry-ui-visionos/src/widgets/webview.rs @@ -16,8 +16,8 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -153,7 +153,7 @@ define_class!( crate::catch_callback_panic( "webview onShouldNavigate", std::panic::AssertUnwindSafe(|| { - let r = unsafe { js_closure_call1(closure_ptr, url_nb) }; + let r = unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb) }; result_cell_ref.set(r); }), ); @@ -202,7 +202,7 @@ define_class!( let url_nb = nanbox_str(&url_str); let closure_ptr = js_nanbox_get_pointer(on_loaded) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, url_nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb); } }), ); @@ -264,7 +264,12 @@ impl PerryWebViewDelegate { let msg_nb = nanbox_str(&msg); let closure_ptr = js_nanbox_get_pointer(on_error) as *const u8; if !closure_ptr.is_null() { - js_closure_call2(closure_ptr, code as f64, msg_nb); + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + code as f64, + msg_nb, + ); } }), ); @@ -415,7 +420,7 @@ pub fn evaluate_js(handle: i64, js_ptr: *const u8, callback: f64) { let nb = nanbox_str(&s); let closure_ptr = js_nanbox_get_pointer(callback) as *const u8; if !closure_ptr.is_null() { - js_closure_call1(closure_ptr, nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb); } }), ); diff --git a/crates/perry-ui-visionos/src/widgets/wheel_picker.rs b/crates/perry-ui-visionos/src/widgets/wheel_picker.rs index 6504ad2968..4b7cb65b83 100644 --- a/crates/perry-ui-visionos/src/widgets/wheel_picker.rs +++ b/crates/perry-ui-visionos/src/widgets/wheel_picker.rs @@ -25,7 +25,7 @@ pub(crate) fn scan_visionos_wheel_picker_gc_roots(visitor: &mut perry_ffi::GcRoo } extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; static _dispatch_main_q: std::ffi::c_void; fn dispatch_async_f( @@ -44,7 +44,7 @@ unsafe extern "C" fn callback_trampoline(context: *mut std::ffi::c_void) { let _ = std::panic::catch_unwind(|| { let payload = Box::from_raw(context as *mut WheelPickerDispatch); let closure = js_nanbox_get_pointer(payload.closure) as *const u8; - js_closure_call1(closure, payload.index); + js_closure_call1(closure, perry_ffi::JsThis::UNDEFINED, payload.index); }); } diff --git a/crates/perry-ui-watchos/src/audio_playback.rs b/crates/perry-ui-watchos/src/audio_playback.rs index 5665812756..106acb899f 100644 --- a/crates/perry-ui-watchos/src/audio_playback.rs +++ b/crates/perry-ui-watchos/src/audio_playback.rs @@ -16,9 +16,9 @@ use std::cell::RefCell; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; #[allow(dead_code)] - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; } @@ -653,7 +653,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -712,7 +712,7 @@ fn drain_pending_callbacks() { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call0(ptr as *const u8); + let _ = js_closure_call0(ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } if should_clean { diff --git a/crates/perry-ui-watchos/src/background.rs b/crates/perry-ui-watchos/src/background.rs index 335bfa88a4..113afc2581 100644 --- a/crates/perry-ui-watchos/src/background.rs +++ b/crates/perry-ui-watchos/src/background.rs @@ -29,7 +29,7 @@ extern "C" { fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_value_is_promise(value: f64) -> i32; } @@ -56,7 +56,7 @@ unsafe fn invoke_handler(handler: f64) { js_run_stdlib_pump(); js_promise_run_microtasks(); let ptr = js_nanbox_get_pointer(handler) as *const u8; - let result = js_closure_call0(ptr); + let result = js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); js_promise_run_microtasks(); let _ = js_value_is_promise(result); } diff --git a/crates/perry-ui-watchos/src/issue_552_stub.rs b/crates/perry-ui-watchos/src/issue_552_stub.rs index fdfb77da44..bf54a94c58 100644 --- a/crates/perry-ui-watchos/src/issue_552_stub.rs +++ b/crates/perry-ui-watchos/src/issue_552_stub.rs @@ -6,7 +6,7 @@ //! picker callback receives an empty array. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -30,7 +30,7 @@ pub extern "C" fn perry_system_geolocation_get_current(_on_success: f64, on_erro } let nb = unsupported_string(); let cb = js_nanbox_get_pointer(on_error) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -47,7 +47,7 @@ pub extern "C" fn perry_system_geolocation_request_permission(callback: f64) { unsafe { let nb = unsupported_string(); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -61,6 +61,6 @@ pub extern "C" fn perry_system_image_picker_pick( let arr = js_array_alloc(0); let nb = js_nanbox_pointer(arr as i64); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } diff --git a/crates/perry-ui-watchos/src/media_playback.rs b/crates/perry-ui-watchos/src/media_playback.rs index 35b1a0e330..80e4def65b 100644 --- a/crates/perry-ui-watchos/src/media_playback.rs +++ b/crates/perry-ui-watchos/src/media_playback.rs @@ -55,8 +55,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; // Signature matches the rest of perry-ui-watchos (see audio.rs's // string callers — and the canonical `str_from_header` in lib.rs). // Runtime returns `*mut StringHeader` which is i64-sized on Apple @@ -798,7 +798,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -807,7 +811,12 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-watchos/src/network_stub.rs b/crates/perry-ui-watchos/src/network_stub.rs index 0ca620f5a8..a29356026e 100644 --- a/crates/perry-ui-watchos/src/network_stub.rs +++ b/crates/perry-ui-watchos/src/network_stub.rs @@ -7,7 +7,7 @@ //! true reachability detection should target iOS / macOS / Android. extern "C" { - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -28,7 +28,12 @@ pub extern "C" fn perry_system_network_get_status(callback: f64) { if cb.is_null() { return; } - js_closure_call2(cb, f64::from_bits(TAG_TRUE), nb_str("unknown")); + js_closure_call2( + cb, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(TAG_TRUE), + nb_str("unknown"), + ); } } diff --git a/crates/perry-ui-watchos/src/notifications.rs b/crates/perry-ui-watchos/src/notifications.rs index e56874f92c..5bdeca9b41 100644 --- a/crates/perry-ui-watchos/src/notifications.rs +++ b/crates/perry-ui-watchos/src/notifications.rs @@ -32,7 +32,7 @@ extern "C" { // as `i64` here (32-bit ptr zero-extended on arm64_32). Keeping the same // signature avoids a `clashing_extern_declarations` warning. fn js_string_from_bytes(data: *const u8, len: i32) -> i64; - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_is_truthy(value: f64) -> i32; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; @@ -134,7 +134,7 @@ unsafe fn dispatch_tap(response: &AnyObject) { let ptr = js_nanbox_get_pointer(callback) as *const u8; if !ptr.is_null() { - js_closure_call2(ptr, id_value, action_value); + js_closure_call2(ptr, perry_ffi::JsThis::UNDEFINED, id_value, action_value); } } diff --git a/crates/perry-ui-watchos/src/state.rs b/crates/perry-ui-watchos/src/state.rs index e8575cca72..ea9f6a477b 100644 --- a/crates/perry-ui-watchos/src/state.rs +++ b/crates/perry-ui-watchos/src/state.rs @@ -9,7 +9,7 @@ use std::ffi::CString; use crate::tree; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -144,7 +144,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; tree::add_child(container, child_handle); } @@ -259,7 +260,7 @@ pub fn state_set(handle: i64, value: f64) { for closure_f64 in callbacks_snapshot { let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } } diff --git a/crates/perry-ui-watchos/src/tree.rs b/crates/perry-ui-watchos/src/tree.rs index 48fbf8a341..c04d2a6d01 100644 --- a/crates/perry-ui-watchos/src/tree.rs +++ b/crates/perry-ui-watchos/src/tree.rs @@ -321,11 +321,11 @@ pub extern "C" fn perry_watchos_handle_action(id: i64) { if let Some(closure_f64) = closure { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; } let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, 0.0); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, 0.0); } } } @@ -547,11 +547,11 @@ pub extern "C" fn perry_watchos_slider_changed(id: i64, value: f64) { if let Some(closure_f64) = closure { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; } let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, value); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, value); } } } @@ -567,11 +567,15 @@ pub extern "C" fn perry_watchos_toggle_changed(id: i64, on: bool) { if let Some(closure_f64) = closure { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; } let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, if on { 1.0 } else { 0.0 }); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + if on { 1.0 } else { 0.0 }, + ); } } } @@ -587,11 +591,11 @@ pub extern "C" fn perry_watchos_picker_changed(id: i64, index: i64) { if let Some(closure_f64) = closure { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; } let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, index as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, index as f64); } } } @@ -601,7 +605,7 @@ pub extern "C" fn perry_watchos_picker_changed(id: i64, index: i64) { pub extern "C" fn perry_watchos_textarea_changed(id: i64, text_ptr: *const std::ffi::c_char) { extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; } @@ -624,7 +628,7 @@ pub extern "C" fn perry_watchos_textarea_changed(id: i64, text_ptr: *const std:: let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; let closure_ptr = unsafe { js_nanbox_get_pointer(closure_f64) } as *const u8; unsafe { - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); } } } diff --git a/crates/perry-ui-windows-winui/src/app.rs b/crates/perry-ui-windows-winui/src/app.rs index 8c18fb45f2..25a25e86ac 100644 --- a/crates/perry-ui-windows-winui/src/app.rs +++ b/crates/perry-ui-windows-winui/src/app.rs @@ -12,7 +12,7 @@ use crate::winui::backend::{self, RenderBackend}; extern "C" { fn js_callback_timer_tick() -> i32; - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_frame_pump_default() -> i32; fn js_gc_step_us(budget_us: u64, out: *mut u8) -> u32; fn js_interval_timer_tick() -> i32; @@ -89,7 +89,7 @@ fn invoke_app_callback(slot: &'static std::thread::LocalKey f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; fn js_string_from_bytes(data: *const u8, len: u32) -> *mut u8; @@ -247,7 +247,7 @@ fn invoke_node0(handle: i64, slot: CallbackSlot) { let callback = node_callback(handle, slot); if callback != 0 { unsafe { - js_closure_call0(callback as *const u8); + js_closure_call0(callback as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -256,7 +256,7 @@ fn invoke_node1(handle: i64, slot: CallbackSlot, value: f64) { let callback = node_callback(handle, slot); if callback != 0 { unsafe { - js_closure_call1(callback as *const u8, value); + js_closure_call1(callback as *const u8, perry_ffi::JsThis::UNDEFINED, value); } } } @@ -269,7 +269,7 @@ fn invoke_node_string(handle: i64, slot: CallbackSlot, value: &str) { // so even that collection's rewritten slot is observed. let callback = node_callback(handle, slot); if callback != 0 { - js_closure_call1(callback as *const u8, boxed); + js_closure_call1(callback as *const u8, perry_ffi::JsThis::UNDEFINED, boxed); } } } diff --git a/crates/perry-ui-windows/src/app.rs b/crates/perry-ui-windows/src/app.rs index bc45b50132..ecbe619496 100644 --- a/crates/perry-ui-windows/src/app.rs +++ b/crates/perry-ui-windows/src/app.rs @@ -35,7 +35,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; // `docs/src/platforms/windows-7.md`. extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_callback_timer_tick() -> i32; fn js_interval_timer_tick() -> i32; @@ -293,7 +293,7 @@ unsafe fn dispatch_ui_message(msg: &MSG) { GLOBAL_HOTKEY_CALLBACKS.with(|cbs| { if let Some(cb_ptr) = cbs.borrow().get(&hotkey_id) { unsafe { - js_closure_call0(*cb_ptr); + js_closure_call0(*cb_ptr, perry_ffi::JsThis::UNDEFINED); } } }); @@ -992,7 +992,7 @@ fn try_handle_shortcut(vk: u16) -> bool { let shortcuts = shortcuts.borrow(); for s in shortcuts.iter() { if s.vk == vk && s.ctrl == ctrl_down && s.shift == shift_down && s.alt == alt_down { - unsafe { js_closure_call0(s.callback_ptr) }; + unsafe { js_closure_call0(s.callback_ptr, perry_ffi::JsThis::UNDEFINED) }; return true; } } @@ -1384,7 +1384,7 @@ pub fn handle_timer(hwnd: HWND, timer_id: usize) { let timers = t.borrow(); let idx = timer_id - 1; if idx < timers.len() { - unsafe { js_closure_call0(timers[idx].callback_ptr) }; + unsafe { js_closure_call0(timers[idx].callback_ptr, perry_ffi::JsThis::UNDEFINED) }; } }); } @@ -1395,7 +1395,7 @@ pub fn handle_activate(activating: bool) { if activating { ON_ACTIVATE_CALLBACK.with(|c| { if let Some(ptr) = *c.borrow() { - unsafe { js_closure_call0(ptr) }; + unsafe { js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED) }; } }); } @@ -1406,7 +1406,7 @@ pub fn handle_activate(activating: bool) { pub fn handle_terminate() { ON_TERMINATE_CALLBACK.with(|c| { if let Some(ptr) = *c.borrow() { - unsafe { js_closure_call0(ptr) }; + unsafe { js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED) }; } }); } diff --git a/crates/perry-ui-windows/src/dialog.rs b/crates/perry-ui-windows/src/dialog.rs index 0d17d254f6..5aaa270f35 100644 --- a/crates/perry-ui-windows/src/dialog.rs +++ b/crates/perry-ui-windows/src/dialog.rs @@ -1,7 +1,7 @@ //! Save file dialog and alert dialog (Win32) extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -46,7 +46,7 @@ pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_typ let bytes = path_str.as_bytes(); let str_ptr = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let nanboxed = js_nanbox_string(str_ptr as i64); - js_closure_call1(closure_ptr, nanboxed); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); CoTaskMemFree(Some(path.0 as *const _)); return; } @@ -54,14 +54,22 @@ pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_typ } } // Cancelled or error - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } #[cfg(not(target_os = "windows"))] { unsafe { - js_closure_call1(closure_ptr, f64::from_bits(0x7FFC_0000_0000_0001)); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(0x7FFC_0000_0000_0001), + ); } } } @@ -140,7 +148,7 @@ pub fn alert(title_ptr: *const u8, message_ptr: *const u8, buttons_ptr: *const u IDNO => 1.0, _ => 0.0, }; - js_closure_call1(closure_ptr, index); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, index); } } @@ -148,7 +156,7 @@ pub fn alert(title_ptr: *const u8, message_ptr: *const u8, buttons_ptr: *const u { let _ = (title, message, buttons_ptr); unsafe { - js_closure_call1(closure_ptr, 0.0); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, 0.0); } } } diff --git a/crates/perry-ui-windows/src/drag_drop.rs b/crates/perry-ui-windows/src/drag_drop.rs index d2e69db88c..955887d800 100644 --- a/crates/perry-ui-windows/src/drag_drop.rs +++ b/crates/perry-ui-windows/src/drag_drop.rs @@ -49,8 +49,8 @@ use std::sync::Once; // about unused items. #[cfg(target_os = "windows")] extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_pointer(ptr: i64) -> f64; fn js_nanbox_string(ptr: i64) -> f64; @@ -101,7 +101,7 @@ unsafe fn call_provider(cb: f64) -> Option { if p == 0 { return None; } - let ret = js_closure_call0(p as *const u8); + let ret = js_closure_call0(p as *const u8, perry_ffi::JsThis::UNDEFINED); let sh = js_jsvalue_to_string(ret); if sh.is_null() { None @@ -455,7 +455,7 @@ mod imp { let payload = js_nanbox_pointer(obj as i64); let cb_ptr = js_nanbox_get_pointer(cb); if cb_ptr != 0 { - js_closure_call1(cb_ptr as *const u8, payload); + js_closure_call1(cb_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, payload); } } diff --git a/crates/perry-ui-windows/src/file_dialog.rs b/crates/perry-ui-windows/src/file_dialog.rs index cb5bd63dda..a698c43201 100644 --- a/crates/perry-ui-windows/src/file_dialog.rs +++ b/crates/perry-ui-windows/src/file_dialog.rs @@ -1,7 +1,7 @@ //! File open dialog — COM-based IFileOpenDialog extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -34,7 +34,7 @@ pub fn open_dialog(callback: f64) { bytes.len() as u32, ); let nanboxed = js_nanbox_string(str_ptr as i64); - js_closure_call1(callback_ptr, nanboxed); + js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); CoTaskMemFree(Some(path.0 as *const _)); return; } @@ -44,13 +44,13 @@ pub fn open_dialog(callback: f64) { // Cancelled or error — call with undefined let undefined = f64::from_bits(0x7FFC_0000_0000_0001); - js_closure_call1(callback_ptr, undefined); + js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, undefined); } } #[cfg(not(target_os = "windows"))] { let undefined = f64::from_bits(0x7FFC_0000_0000_0001); - unsafe { js_closure_call1(callback_ptr, undefined) }; + unsafe { js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, undefined) }; } } diff --git a/crates/perry-ui-windows/src/folder_dialog.rs b/crates/perry-ui-windows/src/folder_dialog.rs index df820a642b..1ea439205f 100644 --- a/crates/perry-ui-windows/src/folder_dialog.rs +++ b/crates/perry-ui-windows/src/folder_dialog.rs @@ -1,7 +1,7 @@ //! Folder open dialog — COM-based IFileOpenDialog with FOS_PICKFOLDERS extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -39,7 +39,7 @@ pub fn open_dialog(callback: f64) { bytes.len() as u32, ); let nanboxed = js_nanbox_string(str_ptr as i64); - js_closure_call1(callback_ptr, nanboxed); + js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, nanboxed); CoTaskMemFree(Some(path.0 as *const _)); return; } @@ -49,13 +49,13 @@ pub fn open_dialog(callback: f64) { // Cancelled or error — call with undefined let undefined = f64::from_bits(0x7FFC_0000_0000_0001); - js_closure_call1(callback_ptr, undefined); + js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, undefined); } } #[cfg(not(target_os = "windows"))] { let undefined = f64::from_bits(0x7FFC_0000_0000_0001); - unsafe { js_closure_call1(callback_ptr, undefined) }; + unsafe { js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, undefined) }; } } diff --git a/crates/perry-ui-windows/src/issue_552_stub.rs b/crates/perry-ui-windows/src/issue_552_stub.rs index ee638b0bee..c2bbf09ebf 100644 --- a/crates/perry-ui-windows/src/issue_552_stub.rs +++ b/crates/perry-ui-windows/src/issue_552_stub.rs @@ -6,7 +6,7 @@ //! picker callback receives an empty array. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -30,7 +30,7 @@ pub extern "C" fn perry_system_geolocation_get_current(_on_success: f64, on_erro } let nb = unsupported_string(); let cb = js_nanbox_get_pointer(on_error) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -47,7 +47,7 @@ pub extern "C" fn perry_system_geolocation_request_permission(callback: f64) { unsafe { let nb = unsupported_string(); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } @@ -61,7 +61,7 @@ pub extern "C" fn perry_system_image_picker_pick( let arr = js_array_alloc(0); let nb = js_nanbox_pointer(arr as i64); let cb = js_nanbox_get_pointer(callback) as *const u8; - js_closure_call1(cb, nb); + js_closure_call1(cb, perry_ffi::JsThis::UNDEFINED, nb); } } diff --git a/crates/perry-ui-windows/src/media_playback.rs b/crates/perry-ui-windows/src/media_playback.rs index 442fa645d9..beaee6a4bb 100644 --- a/crates/perry-ui-windows/src/media_playback.rs +++ b/crates/perry-ui-windows/src/media_playback.rs @@ -27,8 +27,8 @@ use windows::Storage::Streams::RandomAccessStreamReference; extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; fn js_string_from_bytes(data: *const u8, len: i32) -> i64; fn js_string_new_sso(data: *const u8, len: u32) -> f64; fn js_run_stdlib_pump(); @@ -624,7 +624,11 @@ fn fire_state_callback(closure_f64: f64, state: MediaState) { let s = state.as_str(); let str_f64 = js_string_new_sso(s.as_ptr(), s.len() as u32); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call1(closure_ptr as *const u8, str_f64); + let _ = js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + str_f64, + ); } } @@ -633,6 +637,11 @@ fn fire_time_callback(closure_f64: f64, current: f64, duration: f64) { js_run_stdlib_pump(); let _ = js_promise_run_microtasks(); let closure_ptr = js_nanbox_get_pointer(closure_f64); - let _ = js_closure_call2(closure_ptr as *const u8, current, duration); + let _ = js_closure_call2( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + current, + duration, + ); } } diff --git a/crates/perry-ui-windows/src/menu.rs b/crates/perry-ui-windows/src/menu.rs index 0e24edbd97..2d7d60517c 100644 --- a/crates/perry-ui-windows/src/menu.rs +++ b/crates/perry-ui-windows/src/menu.rs @@ -8,7 +8,7 @@ use windows::Win32::Foundation::*; use windows::Win32::UI::WindowsAndMessaging::*; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -476,6 +476,6 @@ pub fn dispatch_menu_item(id: u16) { None }); if let Some(ptr) = callback_ptr { - unsafe { js_closure_call0(ptr) }; + unsafe { js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED) }; } } diff --git a/crates/perry-ui-windows/src/network_stub.rs b/crates/perry-ui-windows/src/network_stub.rs index 0ca620f5a8..a29356026e 100644 --- a/crates/perry-ui-windows/src/network_stub.rs +++ b/crates/perry-ui-windows/src/network_stub.rs @@ -7,7 +7,7 @@ //! true reachability detection should target iOS / macOS / Android. extern "C" { - fn js_closure_call2(closure: *const u8, arg0: f64, arg1: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg0: f64, arg1: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: u32) -> *mut u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -28,7 +28,12 @@ pub extern "C" fn perry_system_network_get_status(callback: f64) { if cb.is_null() { return; } - js_closure_call2(cb, f64::from_bits(TAG_TRUE), nb_str("unknown")); + js_closure_call2( + cb, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(TAG_TRUE), + nb_str("unknown"), + ); } } diff --git a/crates/perry-ui-windows/src/pointer.rs b/crates/perry-ui-windows/src/pointer.rs index 8fbe6fff24..8a2e1ccaf2 100644 --- a/crates/perry-ui-windows/src/pointer.rs +++ b/crates/perry-ui-windows/src/pointer.rs @@ -36,8 +36,8 @@ use windows::Win32::UI::WindowsAndMessaging::{ }; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_pointer_event_new(x: f64, y: f64, button: u32, pointer_type: u32) -> f64; } @@ -273,7 +273,7 @@ fn fire_click(handle: i64) { unsafe { let closure_ptr = js_nanbox_get_pointer(cb_f64); if closure_ptr != 0 { - js_closure_call0(closure_ptr as *const u8); + js_closure_call0(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED); } } } @@ -286,7 +286,7 @@ fn fire_pointer_event(cb_f64: f64, x: f64, y: f64, button: u32) { return; } let pe = js_pointer_event_new(x, y, button, POINTER_TYPE_MOUSE); - js_closure_call1(closure_ptr as *const u8, pe); + js_closure_call1(closure_ptr as *const u8, perry_ffi::JsThis::UNDEFINED, pe); } } @@ -301,7 +301,11 @@ fn fire_hover(handle: i64, is_hovering: bool) { return; } let bits = if is_hovering { TAG_TRUE } else { TAG_FALSE }; - js_closure_call1(closure_ptr as *const u8, f64::from_bits(bits)); + js_closure_call1( + closure_ptr as *const u8, + perry_ffi::JsThis::UNDEFINED, + f64::from_bits(bits), + ); } } diff --git a/crates/perry-ui-windows/src/state.rs b/crates/perry-ui-windows/src/state.rs index 1816d2af07..17b81151ee 100644 --- a/crates/perry-ui-windows/src/state.rs +++ b/crates/perry-ui-windows/src/state.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::widgets; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -257,7 +257,7 @@ pub fn state_set(handle: i64, value: f64) { .unwrap_or_default() }); for callback_ptr in onchange_snapshot { - unsafe { js_closure_call1(callback_ptr, value) }; + unsafe { js_closure_call1(callback_ptr, perry_ffi::JsThis::UNDEFINED, value) }; } // Update textfield bindings (state → textfield direction) @@ -319,7 +319,8 @@ fn render_for_each(container: i64, closure: f64, count: f64) { let n = count as i64; let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; widgets::add_child(container, child_handle); } diff --git a/crates/perry-ui-windows/src/toolbar.rs b/crates/perry-ui-windows/src/toolbar.rs index d9a8e6580a..dcae976762 100644 --- a/crates/perry-ui-windows/src/toolbar.rs +++ b/crates/perry-ui-windows/src/toolbar.rs @@ -29,7 +29,7 @@ pub(crate) fn scan_windows_toolbar_gc_roots(visitor: &mut perry_ffi::GcRootVisit } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } diff --git a/crates/perry-ui-windows/src/tray.rs b/crates/perry-ui-windows/src/tray.rs index eac515e4ad..e612bfa48f 100644 --- a/crates/perry-ui-windows/src/tray.rs +++ b/crates/perry-ui-windows/src/tray.rs @@ -28,7 +28,7 @@ use windows::Win32::UI::WindowsAndMessaging::{ }; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -378,7 +378,7 @@ pub fn handle_callback_message(wparam: usize, lparam: isize) { WM_LBUTTONUP => { if !callback_ptr.is_null() { unsafe { - js_closure_call0(callback_ptr); + js_closure_call0(callback_ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-windows/src/widgets/bottom_nav.rs b/crates/perry-ui-windows/src/widgets/bottom_nav.rs index 65dd306bc6..4b1c9291ca 100644 --- a/crates/perry-ui-windows/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-windows/src/widgets/bottom_nav.rs @@ -24,7 +24,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -290,7 +290,7 @@ pub fn set_selected(handle: i64, index: i64) { return; } unsafe { - js_closure_call1(closure_ptr, index as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, index as f64); } } diff --git a/crates/perry-ui-windows/src/widgets/button.rs b/crates/perry-ui-windows/src/widgets/button.rs index 8d3e5b20c4..8546db4326 100644 --- a/crates/perry-ui-windows/src/widgets/button.rs +++ b/crates/perry-ui-windows/src/widgets/button.rs @@ -18,7 +18,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -153,7 +153,7 @@ pub fn handle_click(handle: i64) { callbacks.get(&handle).copied() }); if let Some(ptr) = ptr { - unsafe { js_closure_call0(ptr) }; + unsafe { js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED) }; } } diff --git a/crates/perry-ui-windows/src/widgets/calendar.rs b/crates/perry-ui-windows/src/widgets/calendar.rs index 33190b79d5..46acc4bd48 100644 --- a/crates/perry-ui-windows/src/widgets/calendar.rs +++ b/crates/perry-ui-windows/src/widgets/calendar.rs @@ -19,7 +19,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -209,6 +209,6 @@ pub fn handle_selection_change(handle: i64) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } diff --git a/crates/perry-ui-windows/src/widgets/combobox.rs b/crates/perry-ui-windows/src/widgets/combobox.rs index 3e2d207c34..9a8e225f87 100644 --- a/crates/perry-ui-windows/src/widgets/combobox.rs +++ b/crates/perry-ui-windows/src/widgets/combobox.rs @@ -20,7 +20,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -198,7 +198,7 @@ pub fn handle_change(handle: i64) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } diff --git a/crates/perry-ui-windows/src/widgets/command_palette.rs b/crates/perry-ui-windows/src/widgets/command_palette.rs index cfbcc9e365..e87c7807c5 100644 --- a/crates/perry-ui-windows/src/widgets/command_palette.rs +++ b/crates/perry-ui-windows/src/widgets/command_palette.rs @@ -30,7 +30,7 @@ use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus; use windows::Win32::UI::WindowsAndMessaging::*; extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -301,7 +301,7 @@ fn run_selected() { unsafe { let closure_ptr = js_nanbox_get_pointer(on_run) as *const u8; if !closure_ptr.is_null() { - js_closure_call0(closure_ptr); + js_closure_call0(closure_ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-windows/src/widgets/date_picker.rs b/crates/perry-ui-windows/src/widgets/date_picker.rs index 5f7d6b7dee..75fab080eb 100644 --- a/crates/perry-ui-windows/src/widgets/date_picker.rs +++ b/crates/perry-ui-windows/src/widgets/date_picker.rs @@ -23,7 +23,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -231,6 +231,6 @@ pub fn handle_selection_change(handle: i64) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } diff --git a/crates/perry-ui-windows/src/widgets/foreach_registry.rs b/crates/perry-ui-windows/src/widgets/foreach_registry.rs index 376d74ccb1..db0a282496 100644 --- a/crates/perry-ui-windows/src/widgets/foreach_registry.rs +++ b/crates/perry-ui-windows/src/widgets/foreach_registry.rs @@ -23,7 +23,7 @@ //! `js_nanbox_get_pointer` and call `widgets::add_child`. extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -43,7 +43,8 @@ pub extern "C" fn render_handler(container_handle: i64, render_closure: f64, cou return; } for i in 0..n { - let child_f64 = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_f64 = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child_handle = unsafe { js_nanbox_get_pointer(child_f64) }; if child_handle != 0 { super::add_child(container_handle, child_handle); diff --git a/crates/perry-ui-windows/src/widgets/image_gallery.rs b/crates/perry-ui-windows/src/widgets/image_gallery.rs index 369364c673..18eea7325c 100644 --- a/crates/perry-ui-windows/src/widgets/image_gallery.rs +++ b/crates/perry-ui-windows/src/widgets/image_gallery.rs @@ -17,7 +17,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -161,7 +161,7 @@ pub fn set_index(handle: i64, index: i64) { let closure_ptr = unsafe { js_nanbox_get_pointer(closure) } as *const u8; if !closure_ptr.is_null() { unsafe { - js_closure_call1(closure_ptr, idx as f64); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, idx as f64); } } } diff --git a/crates/perry-ui-windows/src/widgets/lazyvstack.rs b/crates/perry-ui-windows/src/widgets/lazyvstack.rs index b6b6573dfb..1c8e303a97 100644 --- a/crates/perry-ui-windows/src/widgets/lazyvstack.rs +++ b/crates/perry-ui-windows/src/widgets/lazyvstack.rs @@ -5,7 +5,7 @@ use std::cell::RefCell; use std::collections::HashMap; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -45,7 +45,8 @@ pub fn create(count: f64, render_closure: f64) -> i64 { // Render all items for i in 0..item_count { - let child_handle = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_handle = + unsafe { js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) }; let child = child_handle as i64; if child > 0 { super::add_child(vstack_handle, child); @@ -81,7 +82,9 @@ pub fn update(handle: i64, count: i64) { // Re-render all items for i in 0..count { - let child_handle = unsafe { js_closure_call1(closure_ptr, i as f64) }; + let child_handle = unsafe { + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, i as f64) + }; let child = child_handle as i64; if child > 0 { super::add_child(vstack_handle, child); diff --git a/crates/perry-ui-windows/src/widgets/mod.rs b/crates/perry-ui-windows/src/widgets/mod.rs index 11f025f390..e474c8a530 100644 --- a/crates/perry-ui-windows/src/widgets/mod.rs +++ b/crates/perry-ui-windows/src/widgets/mod.rs @@ -1054,7 +1054,7 @@ pub fn handle_scroll(_wparam: usize, _lparam: isize) {} // ============================================================================= extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } diff --git a/crates/perry-ui-windows/src/widgets/picker.rs b/crates/perry-ui-windows/src/widgets/picker.rs index 924038e420..4450890c86 100644 --- a/crates/perry-ui-windows/src/widgets/picker.rs +++ b/crates/perry-ui-windows/src/widgets/picker.rs @@ -13,7 +13,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -202,7 +202,7 @@ pub fn handle_selchange(handle: i64) { PICKER_CALLBACKS.with(|cb| { let callbacks = cb.borrow(); if let Some(&ptr) = callbacks.get(&handle) { - unsafe { js_closure_call1(ptr, index as f64) }; + unsafe { js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, index as f64) }; } }); } diff --git a/crates/perry-ui-windows/src/widgets/rich_text.rs b/crates/perry-ui-windows/src/widgets/rich_text.rs index 631bead6a4..ded5768b8e 100644 --- a/crates/perry-ui-windows/src/widgets/rich_text.rs +++ b/crates/perry-ui-windows/src/widgets/rich_text.rs @@ -23,7 +23,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -360,6 +360,6 @@ pub fn handle_change(handle: i64) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } diff --git a/crates/perry-ui-windows/src/widgets/scrollview.rs b/crates/perry-ui-windows/src/widgets/scrollview.rs index 31986b33e3..6cd99643c9 100644 --- a/crates/perry-ui-windows/src/widgets/scrollview.rs +++ b/crates/perry-ui-windows/src/widgets/scrollview.rs @@ -52,7 +52,7 @@ pub(crate) fn scan_windows_scrollview_gc_roots(visitor: &mut perry_ffi::GcRootVi } extern "C" { - fn js_closure_call0(closure: *const u8) -> f64; + fn js_closure_call0(closure: *const u8, this: perry_ffi::JsThis) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -84,7 +84,7 @@ fn check_scroll_end(handle: i64) { if closure_to_fire != 0.0 { unsafe { let ptr = js_nanbox_get_pointer(closure_to_fire) as *const u8; - js_closure_call0(ptr); + js_closure_call0(ptr, perry_ffi::JsThis::UNDEFINED); } } } diff --git a/crates/perry-ui-windows/src/widgets/securefield.rs b/crates/perry-ui-windows/src/widgets/securefield.rs index 5892a8e118..d5f2767706 100644 --- a/crates/perry-ui-windows/src/widgets/securefield.rs +++ b/crates/perry-ui-windows/src/widgets/securefield.rs @@ -15,7 +15,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -132,7 +132,7 @@ pub fn handle_change(handle: i64) { bytes.len() as u32, ); let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; - unsafe { js_closure_call1(ptr, nanboxed) }; + unsafe { js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, nanboxed) }; } }); } diff --git a/crates/perry-ui-windows/src/widgets/slider.rs b/crates/perry-ui-windows/src/widgets/slider.rs index 10c58e93f7..8633695084 100644 --- a/crates/perry-ui-windows/src/widgets/slider.rs +++ b/crates/perry-ui-windows/src/widgets/slider.rs @@ -15,7 +15,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -210,7 +210,9 @@ pub fn handle_scroll(handle: i64) { let info = info.borrow(); if let Some(si) = info.get(&handle) { let value = pos_to_value(pos, si.min, si.max); - unsafe { js_closure_call1(si.callback_ptr, value) }; + unsafe { + js_closure_call1(si.callback_ptr, perry_ffi::JsThis::UNDEFINED, value) + }; } }); } diff --git a/crates/perry-ui-windows/src/widgets/table.rs b/crates/perry-ui-windows/src/widgets/table.rs index 0b377f87fd..58dea15f75 100644 --- a/crates/perry-ui-windows/src/widgets/table.rs +++ b/crates/perry-ui-windows/src/widgets/table.rs @@ -46,8 +46,8 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -133,7 +133,14 @@ fn render_cell(handle: i64, row: i64, col: i64) -> CellContent { // handle (POINTER_TAG'd small integer) or a string-typed widget for the // simplest Text-only path. The string-typed return shape (top16 == // 0x7FFF) is the common case — extract the bytes directly. - let result = unsafe { js_closure_call2(closure_ptr, row as f64, col as f64) }; + let result = unsafe { + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + row as f64, + col as f64, + ) + }; let bits = result.to_bits(); let top16 = (bits >> 48) as u16; if top16 == 0x7FFF { @@ -838,7 +845,11 @@ pub fn handle_itemchanged(handle: i64, lparam: LPARAM) { return; } unsafe { - js_closure_call1(closure_ptr, info.i_item as f64); + js_closure_call1( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + info.i_item as f64, + ); } } @@ -883,7 +894,12 @@ pub fn handle_columnclick(handle: i64, lparam: LPARAM) { return; } unsafe { - js_closure_call2(closure_ptr, col as f64, if ascending { 1.0 } else { 0.0 }); + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + col as f64, + if ascending { 1.0 } else { 0.0 }, + ); } // Clear the cache so next paint re-renders post-sort. let old_contents = drain_cell_contents(handle); diff --git a/crates/perry-ui-windows/src/widgets/textarea.rs b/crates/perry-ui-windows/src/widgets/textarea.rs index 8ea538600f..0c317236dd 100644 --- a/crates/perry-ui-windows/src/widgets/textarea.rs +++ b/crates/perry-ui-windows/src/widgets/textarea.rs @@ -20,7 +20,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -179,7 +179,7 @@ pub fn handle_change(handle: i64) { let str_ptr = perry_runtime::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; - unsafe { js_closure_call1(ptr, nanboxed) }; + unsafe { js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, nanboxed) }; } } } diff --git a/crates/perry-ui-windows/src/widgets/textfield.rs b/crates/perry-ui-windows/src/widgets/textfield.rs index 1e47ff4361..bf673038ef 100644 --- a/crates/perry-ui-windows/src/widgets/textfield.rs +++ b/crates/perry-ui-windows/src/widgets/textfield.rs @@ -21,7 +21,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; } @@ -160,7 +160,7 @@ pub fn handle_change(handle: i64) { let str_ptr = perry_runtime::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); let nanboxed = unsafe { js_nanbox_string(str_ptr as i64) }; - unsafe { js_closure_call1(ptr, nanboxed) }; + unsafe { js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, nanboxed) }; } } } diff --git a/crates/perry-ui-windows/src/widgets/toggle.rs b/crates/perry-ui-windows/src/widgets/toggle.rs index 5791fe9fdc..151c9eff4a 100644 --- a/crates/perry-ui-windows/src/widgets/toggle.rs +++ b/crates/perry-ui-windows/src/widgets/toggle.rs @@ -15,7 +15,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -111,7 +111,7 @@ pub fn handle_click(handle: i64) { callbacks.get(&handle).copied() }); if let Some(ptr) = ptr { - unsafe { js_closure_call1(ptr, value) }; + unsafe { js_closure_call1(ptr, perry_ffi::JsThis::UNDEFINED, value) }; } } } diff --git a/crates/perry-ui-windows/src/widgets/tree_view.rs b/crates/perry-ui-windows/src/widgets/tree_view.rs index 8a23ddfb17..c89373c45b 100644 --- a/crates/perry-ui-windows/src/widgets/tree_view.rs +++ b/crates/perry-ui-windows/src/widgets/tree_view.rs @@ -19,7 +19,7 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_string_from_bytes(ptr: *const u8, len: i64) -> *const u8; fn js_nanbox_string(ptr: i64) -> f64; @@ -381,7 +381,7 @@ pub fn handle_selection_change(handle: i64) { let header = js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64); let arg = js_nanbox_string(header as i64); let closure_ptr = js_nanbox_get_pointer(on) as *const u8; - js_closure_call1(closure_ptr, arg); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, arg); } } diff --git a/crates/perry-ui-windows/src/widgets/webview.rs b/crates/perry-ui-windows/src/widgets/webview.rs index 026f09c584..baeaa99371 100644 --- a/crates/perry-ui-windows/src/widgets/webview.rs +++ b/crates/perry-ui-windows/src/widgets/webview.rs @@ -49,8 +49,8 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{alloc_control_id, register_widget, WidgetKind}; extern "C" { - fn js_closure_call1(closure: *const u8, arg: f64) -> f64; - fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64) -> f64; + fn js_closure_call1(closure: *const u8, this: perry_ffi::JsThis, arg: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, arg1: f64, arg2: f64) -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; fn js_nanbox_string(ptr: i64) -> f64; fn js_is_truthy(value: f64) -> i32; @@ -490,7 +490,9 @@ fn install_navigation_handlers(handle: i64, webview: &ICoreWebView2) { catch_panic( "webview onShouldNavigate", std::panic::AssertUnwindSafe(|| { - let r = unsafe { js_closure_call1(closure_ptr, url_nb) }; + let r = unsafe { + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb) + }; result_cell_ref.set(r); }), ); @@ -547,7 +549,7 @@ fn install_navigation_handlers(handle: i64, webview: &ICoreWebView2) { catch_panic( "webview onLoaded", std::panic::AssertUnwindSafe(|| unsafe { - js_closure_call1(closure_ptr, url_nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, url_nb); }), ); } @@ -568,7 +570,12 @@ fn install_navigation_handlers(handle: i64, webview: &ICoreWebView2) { catch_panic( "webview onError", std::panic::AssertUnwindSafe(|| unsafe { - js_closure_call2(closure_ptr, status as f64, msg_nb); + js_closure_call2( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + status as f64, + msg_nb, + ); }), ); } @@ -788,7 +795,7 @@ pub fn evaluate_js(handle: i64, js_ptr: *const u8, callback: f64) { catch_panic( "webview evaluateJs callback", std::panic::AssertUnwindSafe(|| unsafe { - js_closure_call1(closure_ptr, nb); + js_closure_call1(closure_ptr, perry_ffi::JsThis::UNDEFINED, nb); }), ); } diff --git a/crates/perry-ui/src/key_dispatch.rs b/crates/perry-ui/src/key_dispatch.rs index 1cd47cdebd..b7c7644fb5 100644 --- a/crates/perry-ui/src/key_dispatch.rs +++ b/crates/perry-ui/src/key_dispatch.rs @@ -31,8 +31,9 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; extern "C" { - fn js_closure_call2(closure: *const u8, a: f64, b: f64) -> f64; - fn js_closure_call3(closure: *const u8, a: f64, b: f64, c: f64) -> f64; + fn js_closure_call2(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64; + fn js_closure_call3(closure: *const u8, this: perry_ffi::JsThis, a: f64, b: f64, c: f64) + -> f64; fn js_nanbox_get_pointer(value: f64) -> i64; } @@ -216,9 +217,15 @@ fn dispatch(callback: f64, code: KeyCode, mods: u32, is_down: bool, is_repeat: b unsafe { if is_down { let repeat_f = if is_repeat { 1.0 } else { 0.0 }; - js_closure_call3(closure_ptr, key_f, mods_f, repeat_f); + js_closure_call3( + closure_ptr, + perry_ffi::JsThis::UNDEFINED, + key_f, + mods_f, + repeat_f, + ); } else { - js_closure_call2(closure_ptr, key_f, mods_f); + js_closure_call2(closure_ptr, perry_ffi::JsThis::UNDEFINED, key_f, mods_f); } } } diff --git a/crates/perry/tests/fixtures/this_param_routes.ts b/crates/perry/tests/fixtures/this_param_routes.ts new file mode 100644 index 0000000000..a42e0df9f3 --- /dev/null +++ b/crates/perry/tests/fixtures/this_param_routes.ts @@ -0,0 +1,99 @@ +// Every route by which a JS body that reads its dynamic `this` is entered. +// Each caller hands the body its receiver as the `this` PARAMETER, the only +// way a body learns it; a route that passes the wrong receiver changes the +// printed sums. +const N = process.argv.length > 99 ? 1 : 300; +const out: string[] = []; +let s = 0; + +function P(this: any, k: number) { this.k = k; } +(P as any).prototype.m = function (x: number) { return this.k + x; }; +Object.defineProperty((P as any).prototype, "kk", { + get: function (this: any) { return this.k * 10; }, + configurable: true, +}); +const p1: any = new (P as any)(1); +const p2: any = new (P as any)(2); + +const fexpr = function (this: any, x: number) { + return (this && typeof this.k === "number" ? this.k : -1000) + x; +}; +const owner: any = { k: 7 }; +owner.f = function (this: any, x: number) { return this.k * x; }; + +function topLevel(this: any, x: number) { + return (this && typeof this.k === "number" ? this.k : -1000) + x; +} +owner.t = topLevel; + +class C { + k = 5; + m(x: number) { return this.k + x; } +} +class D extends C { + g(x: number) { const f = super.m; return f.call(this, x); } +} +const d = new D(); + +const bound = fexpr.bind(owner); +// A plain call made while a method is running binds `this` to undefined +// (this module is strict): the method's receiver must not leak into a +// callback the runtime calls without one. +let leaks = 0; +const sorter: any = { + k: 3, + run(this: any) { + const a = [3, 1, 2]; + a.sort(function (this: any, x: number, y: number) { + if (this !== undefined) leaks++; + return x - y; + }); + const r = a.reduce(function (this: any, acc: number, v: number) { + if (this !== undefined) leaks++; + return acc + v; + }, 0); + return a.join("") + this.k + r; + }, +}; +// Async and generator bodies bind their receiver at entry too. +const og: any = { + k: 9, + async am(this: any, x: number) { await 0; return this.k + x; }, + async an(this: any, x: number) { return this.k - x; }, // no await: not state-machine lowered + *gm(this: any, x: number) { yield this.k + x; yield this.k * x; }, +}; + +for (let i = 0; i < N; i++) { + const o = i & 1 ? p1 : p2; + s += o.m(i); // ES5 prototype method, method site + s += o.kk; // prototype getter + s += owner.f(i); // own function-valued property + s += owner.t(i); // top-level function as a method + s += fexpr(i); // receiverless call of a function expression + s += fexpr.call(owner, i); // call + s += fexpr.apply(p1, [i]); // apply + s += bound(i); // bound function + s += d.g(i); // super method value (class method wrapper) + [1, 2].forEach(function (this: any, v: number) { s += this.k * v; }, p2); // callback + thisArg + s += [4].map(function (this: any, v: number) { return this.k + v; }, owner)[0]; + const q: any = new (fexpr as any)(1); // `new F()` + s += q instanceof (fexpr as any) ? 1 : 0; + const it = og.gm(i); // generator method + s += it.next().value + it.next().value; + if (i % 97 === 0) out.push(String(s)); +} +out.push(sorter.run()); +console.log(s, out.join(","), "leaks=" + leaks); +// Function EXPRESSIONS (not literal methods) installed as methods. +og.af = async function (this: any, x: number) { return this.k * x; }; +og.gf = function* (this: any) { yield this.k; }; +og.am(1).then((v: number) => + og.an(v).then((w: number) => + og.af(2).then((z: number) => console.log("async", v, w, z, og.gf().next().value)))); + +// Value wrappers of every function kind (installed as function objects). +export function exportedValue(this: any) { return typeof this; } +async function asyncValue(this: any) { return 1; } +function* genValue(this: any) { yield 1; } +const values: any[] = [exportedValue, asyncValue, genValue, topLevel]; +console.log(values.map((f) => typeof f).join(",")); diff --git a/crates/perry/tests/source_graph_export_regressions.rs b/crates/perry/tests/source_graph_export_regressions.rs index aa4fdad25f..49f6e12062 100644 --- a/crates/perry/tests/source_graph_export_regressions.rs +++ b/crates/perry/tests/source_graph_export_regressions.rs @@ -314,11 +314,11 @@ fn cross_module_arrow_callback_dispatch_is_resolved_once_and_fails_closed() { let sink_run = llvm_function_body_containing( &all_ir, "@perry_method_sink_ts__Sink__run", - "@js_closure_resolve_arrow_direct_call(", + "@js_closure_resolve_plain_direct_call(", ); assert_eq!( sink_run - .matches("@js_closure_resolve_arrow_direct_call(") + .matches("@js_closure_resolve_plain_direct_call(") .count(), 1, "the callback target must be resolved once at method entry:\n{sink_run}" diff --git a/crates/perry/tests/this_param_abi.rs b/crates/perry/tests/this_param_abi.rs new file mode 100644 index 0000000000..e3969e084a --- /dev/null +++ b/crates/perry/tests/this_param_abi.rs @@ -0,0 +1,178 @@ +//! This-as-a-parameter: every JS body — compiled closure, value wrapper, +//! native builtin — is `double body(i64 callee, i64 this, double a0, ...)` +//! (`perry_abi::JS_BODY_*`), and the `this` parameter is the ONLY way a body +//! learns its receiver. There is no thread-local receiver cell: a caller that +//! binds a receiver passes it, and a plain call passes `undefined`. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +const ROUTES: &str = include_str!("fixtures/this_param_routes.ts"); +/// node v26.5.1's output for `fixtures/this_param_routes.ts`. +const ROUTES_NODE: &str = "797550 -924,22981,272713,748250,12336 leaks=0\n\ + function,function,function,function\n\ + async 10 -1 18 9"; + +struct Run { + stdout: String, + stderr: String, + ll_dir: tempfile::TempDir, + _dir: tempfile::TempDir, +} + +/// Compile `source` with the constructed IR saved, run it. +fn compile_and_run(source: &str) -> Run { + let dir = tempfile::tempdir().expect("tempdir"); + let ll_dir = tempfile::tempdir().expect("ll tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .env("PERRY_NO_CACHE", "1") + .env("PERRY_SAVE_LL", ll_dir.path()) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + let stderr = String::from_utf8_lossy(&run.stderr).into_owned(); + assert!( + run.status.success(), + "binary failed ({:?})\nstderr:\n{stderr}", + run.status + ); + Run { + stdout: String::from_utf8_lossy(&run.stdout).trim().to_owned(), + stderr, + ll_dir, + _dir: dir, + } +} + +/// Every route into a body that reads `this` hands it the receiver node +/// binds: the sums fold `this.k` from every route, so one route passing the +/// wrong receiver (or `undefined`) changes them. Sabotage: the method-site +/// hit passes `undefined` -> the first sum differs. Sabotage: the runtime's +/// sort/reduce callback call passes the running method's receiver instead of +/// the plain-call `undefined` (what a thread-local cell did) -> `leaks=` is +/// non-zero. Sabotage: an async or generator body skips its entry bind -> +/// the sum or the `async` line differs. +#[test] +fn every_call_route_hands_the_body_its_receiver() { + let run = compile_and_run(ROUTES); + assert_eq!(run.stdout, ROUTES_NODE, "stderr:\n{}", run.stderr); +} + +/// Every symbol the module hands to a closure allocator or registers as a +/// closure body is DEFINED with the JS body ABI. Sabotage: a wrapper family +/// (e.g. `__perry_wrap_`) defined without `%js_this` -> named here. +#[test] +fn every_installed_body_is_defined_with_the_js_body_abi() { + let run = compile_and_run(ROUTES); + assert_eq!(run.stdout, ROUTES_NODE, "stderr:\n{}", run.stderr); + let ir = read_ir(run.ll_dir.path()); + let installed = installed_bodies(&ir); + assert!( + installed.len() >= 10, + "found only {} installed bodies in the IR: {installed:?}", + installed.len() + ); + let mut checked = 0; + let mut wrong = Vec::new(); + for name in &installed { + let Some(params) = defined_params(&ir, name) else { + continue; // defined in another module or the runtime + }; + checked += 1; + if !params.starts_with("i64 %this_closure, i64 %js_this") { + wrong.push(format!("@{name}({params})")); + } + } + assert!( + checked >= 10, + "only {checked} installed bodies are defined here" + ); + assert!( + wrong.is_empty(), + "bodies installed as function objects without the JS body ABI:\n{}", + wrong.join("\n") + ); + // Every wrapper family this fixture exercises is among them. + for family in ["@perry_closure_", "@__perry_wrap_"] { + assert!( + installed + .iter() + .any(|n| format!("@{n}").starts_with(family)), + "no {family}* body installed: {installed:?}" + ); + } +} + +fn read_ir(dir: &Path) -> String { + let mut ir = String::new(); + for entry in std::fs::read_dir(dir).expect("ll dir") { + let path = entry.expect("ll entry").path(); + if path.extension().is_some_and(|e| e == "ll") { + ir.push_str(&std::fs::read_to_string(&path).expect("read ll")); + ir.push('\n'); + } + } + assert!(!ir.is_empty(), "PERRY_SAVE_LL wrote no IR"); + ir +} + +/// Symbols passed as `ptr @X` to `js_closure_alloc*` / `js_register_closure_*`. +fn installed_bodies(ir: &str) -> Vec { + let mut out: Vec = Vec::new(); + for call in ["@js_closure_alloc", "@js_register_closure_"] { + for (at, _) in ir.match_indices(call) { + let rest = &ir[at..]; + let Some(open) = rest.find('(') else { continue }; + let Some(close) = rest[open..].find(')') else { + continue; + }; + for arg in rest[open + 1..open + close].split(',') { + let Some(sym) = arg.trim().strip_prefix("ptr @") else { + continue; + }; + let sym = sym.trim_matches('"').to_string(); + if !out.contains(&sym) { + out.push(sym); + } + } + } + } + out +} + +/// The parameter list of `define ... @name(...)`, if defined in `ir`. +fn defined_params(ir: &str, name: &str) -> Option { + let quoted = format!("@\"{name}\"("); + let plain = format!("@{name}("); + ir.lines() + .filter(|l| l.starts_with("define ")) + .find_map(|l| { + let at = l + .find(&plain) + .map(|i| i + plain.len()) + .or_else(|| l.find("ed).map(|i| i + quoted.len()))?; + let rest = &l[at..]; + Some(rest[..rest.find(')')?].to_string()) + }) +} diff --git a/scripts/check_js_body_call_funnel.py b/scripts/check_js_body_call_funnel.py new file mode 100755 index 0000000000..afa3a10cac --- /dev/null +++ b/scripts/check_js_body_call_funnel.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 +"""Every call of a JS function body goes through ONE funnel per side. + +A JS body is the native code a function object runs (a compiled closure body, +a value wrapper, a native builtin), and the class-method / static-method +bodies the runtime calls by raw pointer. Their calling convention is about to +change (the receiver becomes a parameter), so the calls are funneled: + +* runtime: only `crates/perry-runtime/src/closure/body_call.rs` may turn a + code pointer into a callable body type (`mem::transmute` to an + `fn(...) -> f64` whose parameters are all `f64` / `*const ClosureHeader`); +* codegen: only `crates/perry-codegen/src/expr/body_call.rs` (and the block + builder that defines them) may emit an indirect call. + +A transmute FROM a body type to an erased pointer (installing a native body) +is not a call and is not this gate's business. + +The receiver is a parameter of every JS body (`perry_abi::JS_BODY_THIS_PARAM`, +this-as-a-parameter stage 1), and a native body is installed through an +erased `*const u8`, so nothing the compiler checks connects a native's +signature to the ABI it is called with. Two definition rules close that: + +* every `extern "C" fn` DEFINITION shaped like a JS body — its first + parameter a closure header, every other parameter `f64`, returning `f64` — + declares the receiver (`JsThis`) as its second parameter, unless it is a + call ENTRY (`perry_abi::JS_CALL_ENTRIES`: it takes a closure as an ordinary + argument) or says `NOT-A-JS-BODY: ` on one of the three lines above; +* a function whose address is handed straight to a closure allocator + (`js_closure_alloc*`, `js_register_closure_*`) declares the receiver. + +Every call ENTRY (`perry_abi::JS_CALL_ENTRIES`) takes the receiver after the +function, and three rules keep native code outside the runtime on that ABI: + +* every definition AND every `extern` declaration of an entry, in any crate + (a `#[link_name]` alias included), declares `JsThis` as its second + parameter — a stale declaration is otherwise invisible to the compiler; +* outside the runtime, stdlib and perry-ffi no crate declares a closure + allocator or registrar (`js_closure_alloc*`, `js_register_closure_*`): + addons register bodies through perry-ffi, whose API is typed; +* perry-ffi's registration functions (`alloc_closure`, `register_closure_*`) + take no `*const u8`: a body is a `JsBody` pointer type, so a wrong + signature does not compile. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + +RUNTIME_ROOTS = ("crates/perry-runtime/src", "crates/perry-stdlib/src") # + crates/perry-ext-*/src +RUNTIME_FUNNEL = "crates/perry-runtime/src/closure/body_call.rs" +CODEGEN_ROOT = "crates/perry-codegen/src" +CODEGEN_ALLOWED = { + "crates/perry-codegen/src/expr/body_call.rs", # the funnel + "crates/perry-codegen/src/block.rs", # defines call_indirect (+ its unit tests) + "crates/perry-codegen/src/gc_call_effects.rs", # effect-classifier unit tests only +} + +TRANSMUTE_RE = re.compile(r"\btransmute\b") +# A callable JS/method/static body type: returns f64, and every parameter is +# f64 or a closure-header pointer. +PARAM = r"\s*(?:f64|\*const\s+(?:crate::closure::|super::|perry_runtime::closure::)?ClosureHeader)\s*" +BODY_TYPE_RE = re.compile( + r'(?:unsafe\s+)?extern\s+"C(?:-unwind)?"\s+fn\s*\((?:' + PARAM + r"(?:," + PARAM + r")*,?)?\s*\)\s*->\s*f64" +) +TURBOFISH_RE = re.compile(r"transmute\s*::\s*<(.*)>\s*\(", re.S) +LET_TYPE_RE = re.compile(r"let\s+(?:mut\s+)?\w+\s*:\s*([^=]+)=\s*(?:unsafe\s*\{\s*)?(?:std::mem::|::std::mem::|mem::)?transmute\b", re.S) +ALIAS_RE = re.compile(r"type\s+(\w+)\s*=\s*([^;]+);") + + +def split_turbofish(args: str) -> str: + """Return the target (second) type of `transmute::`.""" + depth = 0 + for i, ch in enumerate(args): + if ch in "(<[": + depth += 1 + elif ch in ")>]": + depth -= 1 + elif ch == "," and depth == 0: + return args[i + 1 :] + return "" + + +def transmute_targets(text: str): + """Yield (line, target_type_text) for every transmute call in `text`.""" + aliases = {m.group(1): m.group(2) for m in ALIAS_RE.finditer(text)} + for m in TRANSMUTE_RE.finditer(text): + start = m.start() + line = text.count("\n", 0, start) + 1 + tail = text[start : start + 600] + target = "" + tf = TURBOFISH_RE.match(tail) + if tf: + inner = tf.group(1) + # cut at the matching `>` of the turbofish + depth = 0 + for i, ch in enumerate(inner): + if ch == "<": + depth += 1 + elif ch == ">" and not (i > 0 and inner[i - 1] == "-"): + if depth == 0: + inner = inner[:i] + break + depth -= 1 + target = split_turbofish(inner) + else: + window = text[max(0, start - 400) : start + len("transmute")] + stmt = window[window.rfind(";") + 1 :] + lt = LET_TYPE_RE.search(stmt) + if lt: + target = lt.group(1) + target = aliases.get(target.strip(), target) + yield line, target + + +NOT_A_BODY = "NOT-A-JS-BODY:" + +# Rust definitions (never a pointer type: those have no name). +FN_HEAD_RE = re.compile(r'extern\s+"C(?:-unwind)?"\s+fn\s+(\$?\w+)\s*(?:<[^>]*>)?\s*\(') +RET_F64_BODY_RE = re.compile(r"\s*->\s*f64\s*(?:where[^{;]*)?\{") + + +def fn_defs(text: str): + """Yield (name, params_text, start) for every `extern "C" fn` DEFINITION + returning `f64`; the parameter list may nest parentheses (a macro's + `$($a: f64),*`).""" + for m in FN_HEAD_RE.finditer(text): + depth, i = 1, m.end() + while i < len(text) and depth: + depth += {"(": 1, ")": -1}.get(text[i], 0) + i += 1 + if depth == 0 and RET_F64_BODY_RE.match(text, i): + yield m.group(1), text[m.end() : i - 1], m.start() + + +CLOSURE_PARAM_RE = re.compile( + r"^\*(?:const|mut)\s+(?:[\w:]*::)?(?:ClosureHeader|RawClosureHeader)$|^ClosurePtr$" +) +JS_THIS_RE = re.compile(r"^(?:[\w:]*::)?JsThis$") +ALLOC_RE = re.compile( + r"\b(?:js_closure_alloc(?:_singleton|_with_captures_singleton)?|js_register_closure_\w+" + r"|alloc_closure|register_closure_arity)\s*\(\s*(\w+)\s+as\s+\*const", + re.S, +) + + +def call_entries(root: Path) -> set[str]: + """`perry_abi::JS_CALL_ENTRIES`: runtime entry points that take a closure + as an ordinary argument (and take the receiver as a parameter or bind + `undefined`).""" + text = (root / "crates/perry-abi/src/lib.rs").read_text(encoding="utf-8") + m = re.search(r"pub const JS_CALL_ENTRIES:[^=]*=\s*\[(.*?)\];", text, re.S) + if not m: + raise SystemExit("check_js_body_call_funnel: perry_abi::JS_CALL_ENTRIES not found") + return set(re.findall(r'"(\w+)"', m.group(1))) + + +def param_types(params: str) -> list[str]: + """Types of a comma-separated parameter list (macro repetitions such as + `$($a: f64),*` count as `f64`).""" + params = re.sub(r"\$\(|\)\s*,?\s*[*+]", ",", params) + out = [] + for p in (x.strip() for x in params.split(",")): + if not p: + continue + out.append(p.split(":", 1)[-1].strip() if ":" in p else p) + return out + + +def definition_violations(root: Path): + entries = call_entries(root) + bases = list(RUNTIME_ROOTS) + sorted( + p.relative_to(root).as_posix() for p in root.glob("crates/perry-ext-*/src") + ) + ["crates/perry-ffi/src"] + defs: dict[str, list[tuple[str, int, list[str]]]] = {} + texts = [] + out = [] + for base in bases: + if not (root / base).exists(): + continue + for path in sorted((root / base).rglob("*.rs")): + rel = path.relative_to(root).as_posix() + text = path.read_text(encoding="utf-8") + lines = text.split("\n") + texts.append((rel, text)) + for name, params, start in fn_defs(text): + types = param_types(params) + line = text.count("\n", 0, start) + 1 + defs.setdefault(name, []).append((rel, line, types)) + if not types or not CLOSURE_PARAM_RE.match(types[0]): + continue + if len(types) > 1 and JS_THIS_RE.match(types[1]): + continue + if not all(t == "f64" for t in types[1:]): + continue + if name in entries or exempt(lines, line): + continue + out.append( + f"{rel}:{line}: JS body `{name}` does not declare the receiver " + "(`_this: JsThis` after the closure; perry_abi::JS_BODY_THIS_PARAM)" + ) + for rel, text in texts: + for m in ALLOC_RE.finditer(text): + name = m.group(1) + found = defs.get(name) + if not found: + continue # a typed local; the compiler checked its type + if any(len(t) > 1 and JS_THIS_RE.match(t[1]) for _, _, t in found): + continue + line = text.count("\n", 0, m.start()) + 1 + out.append( + f"{rel}:{line}: `{name}` is installed as a JS body but does not declare " + "the callee and the receiver (perry_abi::JS_BODY_*)" + ) + return out + + +# The V8 callback trampoline adapter keeps V8's receiverless contract. +ENTRY_WITHOUT_RECEIVER = {"js_closure_v8_callback"} +# `fn NAME(params) -> ret;` inside an extern block (no body), with an optional +# `#[link_name = "..."]` a few lines above. +DECL_RE = re.compile(r"\bfn\s+(\w+)\s*\(") +LINK_NAME_RE = re.compile(r'#\[link_name\s*=\s*"(\w+)"\]') +REGISTRAR_RE = re.compile(r"^(?:js_closure_alloc\w*|js_register_closure_\w+)$") +REGISTRAR_CRATES_ALLOWED = ("crates/perry-runtime/", "crates/perry-stdlib/", "crates/perry-ffi/") +FFI_CLOSURE = "crates/perry-ffi/src" + + +def fn_items(text: str): + """Yield (name, params_text, is_declaration, start) for every `fn` item; + a declaration ends in `;` after its signature (an extern-block item).""" + for m in DECL_RE.finditer(text): + depth, i = 1, m.end() + while i < len(text) and depth: + depth += {"(": 1, ")": -1}.get(text[i], 0) + i += 1 + if depth: + continue + j = i + while j < len(text) and text[j] not in "{;": + j += 1 + yield m.group(1), text[m.end() : i - 1], j < len(text) and text[j] == ";", m.start() + + +def entry_abi_violations(root: Path): + entries = call_entries(root) - ENTRY_WITHOUT_RECEIVER + out = [] + for path in sorted((root / "crates").glob("*/src/**/*.rs")): + rel = path.relative_to(root).as_posix() + text = path.read_text(encoding="utf-8") + for name, params, is_decl, start in fn_items(text): + # This item's own attributes: back to the previous item's end. + head = text[max(text.rfind(";", 0, start), text.rfind("{", 0, start), text.rfind("}", 0, start)) + 1 : start] + ln = LINK_NAME_RE.search(head) if is_decl else None + symbol = ln.group(1) if ln else name + line = text.count("\n", 0, start) + 1 + if symbol in entries and (is_decl or "extern" in text[max(0, start - 40) : start]): + types = param_types(params) + if len(types) < 2 or not JS_THIS_RE.match(types[1]): + what = "declares" if is_decl else "defines" + out.append( + f"{rel}:{line}: `{name}` {what} the call entry `{symbol}` without the " + "receiver (`this: JsThis` after the function; perry_abi::JS_CALL_ENTRIES)" + ) + if ( + is_decl + and REGISTRAR_RE.match(symbol) + and not rel.startswith(REGISTRAR_CRATES_ALLOWED) + ): + out.append( + f"{rel}:{line}: declares the closure registrar `{symbol}`; register a body " + "through perry-ffi (`alloc_closure` / `register_closure_*`, typed `JsBody`)" + ) + if ( + rel.startswith(FFI_CLOSURE) + and not is_decl + and re.match(r"^(?:alloc_closure|register_closure_\w+)$", name) + and "*const u8" in params + ): + out.append( + f"{rel}:{line}: perry-ffi's `{name}` takes an untyped `*const u8` body; " + "take `F: JsBody`" + ) + return out + + +def exempt(lines, line: int) -> bool: + """A native (non-JS) callback with a body-shaped type — a Rust helper + another crate registers — is exempt when one of the three lines above the + transmute says `NOT-A-JS-BODY: `.""" + return any(NOT_A_BODY in lines[i] for i in range(max(0, line - 4), line)) + + +def runtime_violations(root: Path): + out = [] + bases = list(RUNTIME_ROOTS) + sorted( + p.relative_to(root).as_posix() for p in root.glob("crates/perry-ext-*/src") + ) + for base in bases: + for path in sorted((root / base).rglob("*.rs")): + rel = path.relative_to(root).as_posix() + if rel == RUNTIME_FUNNEL: + continue + text = path.read_text(encoding="utf-8") + lines = text.split("\n") + for line, target in transmute_targets(text): + if BODY_TYPE_RE.search(target) and not exempt(lines, line): + out.append(f"{rel}:{line}: transmute to a JS body type outside {RUNTIME_FUNNEL}") + return out + + +INDIRECT_RE = re.compile(r"\.call_indirect(?:_gc_leaf)?\s*\(") + + +def codegen_violations(root: Path): + out = [] + for path in sorted((root / CODEGEN_ROOT).rglob("*.rs")): + rel = path.relative_to(root).as_posix() + if rel in CODEGEN_ALLOWED or rel.endswith("_tests.rs"): + continue + text = path.read_text(encoding="utf-8") + for m in INDIRECT_RE.finditer(text): + line = text.count("\n", 0, m.start()) + 1 + out.append(f"{rel}:{line}: indirect call built outside crates/perry-codegen/src/expr/body_call.rs") + return out + + +def self_test() -> int: + import tempfile + + cases = { + # (file, text, expect_red) + "rt_let": ("crates/perry-runtime/src/x.rs", + 'fn f(p: *const u8) { let g: extern "C" fn(*const ClosureHeader, f64) -> f64 = unsafe { std::mem::transmute(p) }; }', True), + "rt_turbofish": ("crates/perry-runtime/src/x.rs", + 'fn f(p: usize) { let _ = unsafe { std::mem::transmute:: f64>(p) }; }', True), + "rt_alias": ("crates/perry-runtime/src/x.rs", + 'type Cb = unsafe extern "C" fn(*const ClosureHeader) -> f64;\nfn f(p: usize) { let c: Cb = std::mem::transmute(p); }', True), + "rt_erase_ok": ("crates/perry-runtime/src/x.rs", + 'fn f() { let _ = unsafe { std::mem::transmute:: f64, extern "C" fn()>(g) }; }', False), + "rt_foreign_ok": ("crates/perry-runtime/src/x.rs", + 'fn f(p: usize) { let g: extern "C" fn(f64) -> i64 = unsafe { std::mem::transmute(p) }; }', False), + "rt_exempt_ok": ("crates/perry-runtime/src/x.rs", + 'fn f(p: usize) {\n // NOT-A-JS-BODY: a registered Rust helper.\n let g: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(p) };\n}', False), + "rt_static_getter": ("crates/perry-runtime/src/x.rs", + 'fn f(p: usize) { let g: extern "C" fn() -> f64 = std::mem::transmute(p); }', True), + "rt_funnel_ok": (RUNTIME_FUNNEL, + 'fn f(p: *const u8) { let g: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(p) }; }', False), + "cg_indirect": ("crates/perry-codegen/src/lower_call/x.rs", "fn f() { blk.call_indirect(DOUBLE, &p, &a); }", True), + "def_no_this": ("crates/perry-runtime/src/x.rs", + 'extern "C" fn body(_c: *const ClosureHeader, a: f64) -> f64 { a }', True), + "def_with_this": ("crates/perry-runtime/src/x.rs", + 'extern "C" fn body(_c: *const ClosureHeader, _this: crate::closure::JsThis, a: f64) -> f64 { a }', False), + "def_macro_no_this": ("crates/perry-runtime/src/x.rs", + 'macro_rules! m { ($n:ident) => { extern "C" fn $n(_c: *const ClosureHeader, $($a: f64),*) -> f64 { 0.0 } }; }', True), + "def_entry_ok": ("crates/perry-runtime/src/x.rs", + 'pub extern "C" fn js_closure_call2(c: *const ClosureHeader, this: JsThis, a: f64, b: f64) -> f64 { a }', False), + "def_exempt_ok": ("crates/perry-runtime/src/x.rs", + '// NOT-A-JS-BODY: a helper taking a closure argument.\nextern "C" fn helper(c: *const ClosureHeader, a: f64) -> f64 { a }', False), + "def_macro_rep_no_this": ("crates/perry-runtime/src/x.rs", + 'macro_rules! m { ($n:ident) => { extern "C" fn $n(c: *const ClosureHeader $(, $a: f64)*) -> f64 { 0.0 } }; }', True), + "def_macro_rep_ok": ("crates/perry-runtime/src/x.rs", + 'macro_rules! m { ($n:ident) => { extern "C" fn $n(c: *const ClosureHeader, _this: JsThis $(, $a: f64)*) -> f64 { 0.0 } }; }', False), + "def_ffi_no_this": ("crates/perry-ext-zz/src/x.rs", + 'extern "C" fn cb(_c: *const RawClosureHeader, a: f64) -> f64 { a }', True), + "alloc_receiverless": ("crates/perry-runtime/src/x.rs", + 'extern "C" fn noop() -> f64 { 0.0 }\nfn f() { js_closure_alloc(noop as *const u8, 0); }', True), + "alloc_ok": ("crates/perry-runtime/src/x.rs", + 'extern "C" fn b(_c: *const ClosureHeader, _this: JsThis) -> f64 { 0.0 }\nfn f() { js_closure_alloc(b as *const u8, 0); }', False), + "cg_funnel_ok": ("crates/perry-codegen/src/expr/body_call.rs", "fn f() { blk.call_indirect(DOUBLE, &p, &a); }", False), + "entry_def_no_this": ("crates/perry-runtime/src/x.rs", + 'pub extern "C" fn js_closure_call2(c: *const ClosureHeader, a: f64, b: f64) -> f64 { a }', True), + "entry_decl_no_this": ("crates/perry-ui-zz/src/x.rs", + 'extern "C" {\n fn js_closure_call2(c: *const u8, a: f64, b: f64) -> f64;\n}', True), + "entry_decl_ok": ("crates/perry-ui-zz/src/x.rs", + 'extern "C" {\n fn js_closure_call2(c: *const u8, this: perry_ffi::JsThis, a: f64, b: f64) -> f64;\n}', False), + "entry_link_name_no_this": ("crates/perry-stdlib/src/x.rs", + 'extern "C" {\n #[link_name = "js_closure_call2"]\n fn provider_call2(c: *const ClosureHeader, a: f64, b: f64) -> f64;\n}', True), + "ext_registrar_decl": ("crates/perry-ext-zz/src/x.rs", + 'extern "C" {\n fn js_closure_alloc(f: *const u8, n: u32) -> *mut u8;\n}', True), + "ffi_untyped_alloc": ("crates/perry-ffi/src/closure.rs", + 'pub fn alloc_closure(func: *const u8, capture_count: u32) -> *mut ClosureHeader { todo!() }', True), + "ffi_typed_alloc_ok": ("crates/perry-ffi/src/closure.rs", + 'pub fn alloc_closure>(func: F, capture_count: u32) -> *mut ClosureHeader { todo!() }', False), + } + failed = 0 + for name, (rel, text, expect_red) in cases.items(): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for d in (*RUNTIME_ROOTS, CODEGEN_ROOT, "crates/perry-abi/src"): + (root / d).mkdir(parents=True, exist_ok=True) + (root / "crates/perry-abi/src/lib.rs").write_text( + 'pub const JS_CALL_ENTRIES: [&str; 1] = ["js_closure_call2"];', encoding="utf-8" + ) + p = root / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text, encoding="utf-8") + red = bool( + runtime_violations(root) + or codegen_violations(root) + or definition_violations(root) + or entry_abi_violations(root) + ) + if red != expect_red: + print(f"self-test {name}: expected {'red' if expect_red else 'green'}, got {'red' if red else 'green'}") + failed += 1 + if failed: + return 1 + print(f"check_js_body_call_funnel self-test: {len(cases)} cases OK") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + ap.add_argument("--self-test", action="store_true") + args = ap.parse_args() + if args.self_test: + return self_test() + violations = ( + runtime_violations(REPO) + + codegen_violations(REPO) + + definition_violations(REPO) + + entry_abi_violations(REPO) + ) + if violations: + print("\n".join(violations)) + print(f"\n{len(violations)} violation(s): JS body calls outside the funnels " + "(runtime: closure/body_call.rs macros; codegen: expr::body_call::emit_js_body_call) " + "or JS body / call-entry signatures without the receiver") + return 1 + print( + "check_js_body_call_funnel: every JS body call goes through its funnel " + "and every JS body declares the receiver" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_runtime_symbols.sh b/scripts/check_runtime_symbols.sh index caad53e384..35d9bb81c6 100755 --- a/scripts/check_runtime_symbols.sh +++ b/scripts/check_runtime_symbols.sh @@ -54,7 +54,7 @@ SENTINELS=( js_closure_get_capture_bits js_closure_set_capture_bits js_closure_set_box_capture_ptr - js_closure_resolve_arrow_direct_call + js_closure_resolve_plain_direct_call js_register_closure_trusted_direct # #9188: codegen switched module init from the copying spellings to these. # A runtime archive predating the split defines neither, and every program diff --git a/scripts/gc_root_dominance_check.py b/scripts/gc_root_dominance_check.py index 21db88dec4..9765207f48 100755 --- a/scripts/gc_root_dominance_check.py +++ b/scripts/gc_root_dominance_check.py @@ -567,8 +567,6 @@ def build_cfg(f): # (closure/dynamic_props.rs:1040) when the callee captures `this`. That one # IS a collection point, and #7154's fix re-reads the arguments below it. "js_closure_unbox_callee_checked", - # object/this_binding.rs:160 -- a thread-local cell swap - "js_implicit_this_set", "js_implicit_this_get", # `js_gc_note_slot_layout` (gc/layout.rs:814) and its `_aware` sibling # (:833). `_aware` is the same body behind an early return taken when # neither the new nor the old bits are pointer-bearing, so it does strictly @@ -1515,14 +1513,8 @@ def is_collecting(callee): # Calling a JS closure. The four names this replaces # (`js_call_closure`, `js_invoke_closure`, `js_function_call`, # `js_apply_function`) were not symbols; these are. - "js_closure_call0", "js_closure_call1", "js_closure_call2", - "js_closure_call3", "js_closure_call4", "js_closure_call5", - "js_closure_call6", "js_closure_call7", "js_closure_call8", - "js_closure_call9", "js_closure_call10", "js_closure_call11", - "js_closure_call12", "js_closure_call13", "js_closure_call14", - "js_closure_call15", "js_closure_call16", - "js_closure_call_array", "js_closure_call_apply_with_spread", - "js_native_call_value", + # ... every JS-call entry point, read from `perry_abi::JS_CALL_ENTRIES` + # (added below) so a new entry cannot be missing here. "js_object_get_property_key", "js_object_set_property_key", "js_object_set_property_key_method", "js_object_get_field_by_name", "js_object_set_field_by_name", @@ -1719,6 +1711,24 @@ def is_collecting(callee): } +def _js_call_entries(): + """`perry_abi::JS_CALL_ENTRIES` — the runtime entry points emitted code + calls to run a JS function. Read from the ABI crate, the one list both + sides compile against; an empty read is an error, never an empty set.""" + abi = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", + "crates", "perry-abi", "src", "lib.rs") + with open(abi, encoding="utf-8") as fh: + text = fh.read() + m = re.search(r"pub const JS_CALL_ENTRIES:[^=]*=\s*\[(.*?)\];", text, re.S) + names = re.findall(r'"([A-Za-z0-9_]+)"', m.group(1)) if m else [] + if not names: + raise SystemExit("gc_root_dominance_check: perry_abi::JS_CALL_ENTRIES not found") + return set(names) + + +POLL_CAPABLE_RUNTIME |= _js_call_entries() + + def compute_poll_reaching(all_funcs): """Names of compiled functions that can transitively reach a moving-minor safepoint (`js_gc_loop_safepoint`) or a runtime helper that re-enters JS.""" @@ -2789,17 +2799,14 @@ def rewritten_load_kind(text): "js_closure_get_capture_ptr", "js_gc_temp_root_get", # a MUTABLE root: rewritten on evacuation "js_box_get_bits", # box.rs mutable-capture cell read - "js_implicit_this_get", # object/this_binding.rs:160 thread-local "js_new_target_get", - # object/this_binding.rs:159 -- `js_implicit_this_SET` is a swap, so its - # RETURN value is a read of the same scanned mutable cell - # (`scan_implicit_this_roots_mut`, this_binding.rs:176) and the swap has - # already overwritten the only other copy. Listing the setter as a reader - # looks odd, which is exactly why #7214 left `prev_this` unrooted for a - # whole PR: the checker saw a call it knew could not collect, never - # classified the result as a heap value, and reported nothing at either - # end. Being non-collecting is what makes a call a root READ. - "js_implicit_this_set", + # A non-collecting SETTER of a scanned mutable cell that returns the + # previous value is a root READ too (#7214 left `prev_this` unrooted for a + # whole PR because the implicit-`this` swap was not listed here): being + # non-collecting is what makes a call a root read. `js_new_target_set` + # returns its previous value too; it must be listed here if generated + # code ever uses that result (`new_target_save` reads the cell with + # `js_new_target_get` first instead). # `js_get_string_pointer_unified` is a candidate and is deliberately left # out for now: its result IS a raw heap address in a bare register, but it # is not in NONCOLLECTING (its SSO branch allocates), so classifying it as @@ -3145,7 +3152,7 @@ def run_stale(parsed, poll_reaching, verbose, moving_only, fatal_only, # hands back an object the collector can move). HEAP_SOURCE_CALLS = frozenset({ "js_gc_temp_root_get", "js_shadow_slot_get", "js_closure_get_capture_bits", - "js_box_get_bits", "js_implicit_this_get", "js_new_target_get", + "js_box_get_bits", "js_new_target_get", "js_static_this_resolve", "js_get_exception", }) diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index ff786948d4..616868aa4d 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -262,12 +262,6 @@ "verdict": "not_a_gc_pointer", "why": "Atomic count of spawned children that have not reached close. It is only a lock-free emptiness gate for the reactor and root scanner; the ChildProcess values themselves live in CP_LIVE and are visited by cp_reactor_scan_roots_mut." }, - { - "file": "crates/perry-runtime/src/child_process/reactor.rs", - "name": "CP_NEXT_LIVE_ID", - "verdict": "not_a_gc_pointer", - "why": "Monotonic id counter for CP_LIVE keys. CP_LIVE itself is covered by cp_reactor_scan_roots_mut." - }, { "file": "crates/perry-runtime/src/child_process/reactor.rs", "name": "CP_REFED_COUNT", @@ -398,7 +392,7 @@ "file": "crates/perry-runtime/src/gc/census.rs", "name": "PASS1_MARKED", "verdict": "non_moving_snapshot", - "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete \u2192 sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs \u2014 it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase \u2014 after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged \u2014 `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` \u2014 and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` \u2192 `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only \u2014 no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound \u2014 the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses \u2014 no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects \u2014 and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module \u2014 all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed. Re-audited for #8512: gc/mod.rs only enables the existing PTY mutable-root scanner on Windows; it changes no mark/sweep phase or census boundary. The scanner visits NaN-boxed slots without running JS callbacks. Re-audited 2026-09-12 for the single regular-expression engine: `gc/mod.rs` changes `mod prefetch;` to `pub(crate) mod prefetch;` so the RegExp owner-table walks can prefetch headers, a visibility change with no new call in collector control flow; `gc/census.rs` changes only its `#[cfg(test)]` `regex_census_tests` module, dropping assertions for the previous engine's cache rows. Neither boundary (`census_pass1_if_armed` in `step_mark_propagation`, `census_take_if_armed_at_full_sweep_start` in `step_sweep`) nor the synchronous mark-complete to sweep-entry interval changes. Re-audited 2026-09-13 after the #10169 fix touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains only `pub(crate) use` re-exports (`policy::note_young_leaf_born_old`, `policy::young_generation_holds_a_nursery`, `promote_in_place::{young_generation_measured_dying, young_generation_measured_retained}`, and cfg(test) survival seeders). `gc/policy.rs` gains a `Cell` thread-local (`GC_YOUNG_LEAF_BORN_OLD`, no pointer), its setter, a pure predicate over `copying_from_space_in_use_bytes` vs the base nursery cap, and a consumed-once branch at the top of `gc_budgeted_due_trigger` that may answer `YoungScavengeCap` ahead of `OldReclaim`. That branch decides WHICH collection a safepoint starts (a minor instead of a full); it runs before any cycle begins and never inside one, so the mark-complete \u2192 sweep-entry window of a synchronous full \u2014 where PASS1_MARKED is populated and consumed within one `run_to_completion` \u2014 is unchanged, and neither hunk adds an allocation, a JS callback, or a relocation to it. Re-audited 2026-09-13 for the heap generation (#10164 cross-call search positions): `gc/mod.rs` only declares `pub(crate) mod heap_generation;`. `gc/cycle.rs` wraps the `Sweep` and `Reclaim` arms of `GcCycleState::step` in a `HeapChange` scope and opens one inside `atomic_finalize_minor_prelude`'s evacuation branch (with a nested one around old-page defrag). Opening and closing a scope only increments two thread-local integer cells (`HEAP_GENERATION`, `OPEN_HEAP_CHANGES`); a first thread-local read may allocate a key through the global allocator, which neither relocates nor runs JS. The `Sweep` scope opens immediately before `step_sweep`, i.e. before `census_take_if_armed_at_full_sweep_start` takes PASS1_MARKED out of TLS, and adds no relocation, collection or JS callback to the synchronous mark-complete to sweep-entry window; the minor-prelude scope is unreachable from a full cycle, which bypasses `MinorPrelude`. Neither boundary nor the intervening control flow changed. Re-audited 2026-09-13 for #10182 block-granular reclamation, which touched `gc/cycle.rs`. Two hunks: (a) in the `RememberedSetRebuild` subphase of AtomicFinalize \u2014 INSIDE the window \u2014 the require-marked old-to-young rebuild is now constructed with `OldToYoungRememberedRebuildState::new_skipping`, whose cursor never enters blocks the census recorded as holding no reached, pinned or pre-marked object (`BlockCensus::unmarked_blocks`); computing that list reads `arena_block_snapshots()` and allocates one `Vec` through the global allocator. It visits a subset of the same objects the rebuild already walked (every skipped object would have been rejected as unmarked), and it neither allocates a GC object, relocates anything, nor runs a JS callback. (b) In `step_sweep`, `IncrementalSweepState::with_block_skip` runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED out of TLS. Neither boundary moved and the synchronous mark-complete to sweep-entry interval gains no relocation, collection or callback. Re-audited 2026-09-11 for the startup memory profile: gc/mod.rs only retains the pre-main allocator-policy constructor in js_gc_init. The constructor applies process allocation options, without invoking GC or JS. No census boundary, collector phase, or mark-complete to sweep-entry control flow changed. Re-audited 2026-09-13 for #10179: census.rs only adds a native regex cache metadata row and its unit assertion; snapshot consumption and the full-cycle window are unchanged. Re-audited 2026-09-14 for the GC due-check fast path, which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` only changes the safepoint re-exports: `gc_runtime_safepoint` becomes cfg(test) and `gc_runtime_safepoint_poll` is added. `gc/policy.rs`: the budgeted step returns a debt-free `GcStepReport` (debt is attached by the FFI and test entry points after the step returns) and moves cycle start/step into an out-of-line `gc_budgeted_start_or_step`; `gc_check_trigger` reuses a repeatable due-trigger answer through `DueTriggerMemo`, placed after its `GC_FLAG_IN_ALLOC` and suppression early returns; the young scavenge cap reuses the old-gen pressure value the due trigger already read and checks the census-seeded flag first. All of it runs from mutator safepoints, allocation-point trigger checks and the host step API, before a cycle starts or between budgeted steps. None of it is reachable between `census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` of a synchronous full: an allocation inside that window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before the changed code. No allocation, relocation, collection or JS callback is added to the window. Re-audited 2026-09-14 for the tiny-parse nursery-cap boundary, which touched `gc/policy.rs`. It adds `tiny_parse_generational_collection_due`, a pure predicate (the existing `tiny_parse_pressure_due` OR the existing `young_scavenge_cap_due` read), and calls it instead of `tiny_parse_pressure_due` from `gc_bump_malloc_trigger_inner` and `gc_collect_pending_suppressed_parse_slow` (generational branch only) and from `gc_schedule_parse_boundary_collection_if_pressure`. All three are JSON.parse mutator-side boundaries, none reachable from `step_mark_propagation` or `step_sweep`; the predicate reads counters and allocates nothing. Neither census boundary nor the synchronous mark-complete to sweep-entry interval changed. Re-audited 2026-09-13 for #10182's full-collection throughput follow-up, which touched `gc/cycle.rs` in one hunk, INSIDE the window: the `RememberedSetRebuild` subphase of a synchronous full now first asks `verify::full_remembered_rebuild_provably_empty` and, when it holds, installs `OldToYoungRememberedRebuildState::provably_empty()` (an empty sticky set, no walk) instead of the require-marked rebuild. The predicate reads `arena_block_snapshots()` (one `Vec` through the global allocator), the census's per-block reached/pre-marked facts and the malloc registry's length; the constructor bumps a `Cell` counter and prints one line under `PERRY_GC_DIAG`. None of it allocates a GC object, relocates anything, collects, or runs a JS callback, and both census boundaries stay where they were. Re-audited 2026-09-14 for #10182's pacing-full work, which touched `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs`. `gc/cycle.rs`: `GcCycleState::new_full` no longer calls `materialize_all_promoted_page_runs`; that call ran in the constructor, before the census and far before `census_pass1_if_armed`, and removing it adds nothing to the window. `gc/mod.rs`: one `mod promoted_cohort;` declaration. `gc/policy.rs`: (a) `credit_promoted_bytes_to_old_baseline` also credits a `Cell` cohort counter (it runs after a copying minor completes); (b) `finish_full_old_reclaim_baseline` also records the verified old live bytes and resets that counter (Publish, after `step_sweep` consumed the snapshot); (c) `gc_safepoint_moving_minor` arms and disarms the promotion-census record around its nursery minor and calls `run_promoted_cohort_full_if_due`, which starts a synchronous full through the same `gc_collect_full_mark_sweep_with_trigger` entry and reads byte counters before and after it. All of it runs before a cycle starts or after it completes; none of it runs between mark completion and sweep entry, allocates a GC object, relocates anything, or calls into JS. The census the promoted-cohort full may adopt from the promotion walk is built in `BuildValidPointerSet`, before either boundary. Both boundaries are unchanged. Re-audited 2026-09-14 for the #10182 dead-stack scrub in `gc/cycle.rs`: `step_build_valid_pointer_set` now calls `scrub_dead_stack_below`, which zeroes a local array in its own frame (dead stack below the caller), right after the census finishes \u2014 in `BuildValidPointerSet`, before the root scan and far before `census_pass1_if_armed`. It writes no heap memory, allocates nothing, relocates nothing and calls no JS; both boundaries are unchanged. Re-audited 2026-09-14 for #10241 (cohort survival), which touched `gc/cycle.rs` and `gc/policy.rs`. `gc/cycle.rs`: one call, `promoted_cohort::survival::check_minor_view_at_full_sweep_start()`, in `step_sweep` immediately AFTER `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS, i.e. outside the window. It is a no-op unless a promoted-cohort full armed its survival probe; when armed it walks the old page index over the preceding minor's dirty pages (`old_arena_walk_objects_on_pages`, Rust-allocator Vecs), reads GC headers' mark flags and the slots of unmarked ones, and records one enum. It writes no heap memory, allocates no GC object, relocates nothing and calls no JS. `gc/policy.rs`: `run_promoted_cohort_full_if_due` arms the probe before `gc_collect_full_mark_sweep_with_trigger` and takes it after the full returns (feeding `note_full_measured_promotion_survival` and one diagnostic line); both run before a cycle starts or after it completes. Both boundaries are unchanged. Re-audited 2026-09-14 for #10241's in-place-only cohort: `gc/policy.rs` drops the `promoted_cohort::note_promoted` call from `credit_promoted_bytes_to_old_baseline` (the copying minor now calls `promoted_cohort::note_minor_promotion` itself, after the credit). Both run at the end of a copying minor, outside any full cycle; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-14 for the parse-boundary side-allocation band (medium-parse pacing), which touched `gc/policy.rs`. Three hunks: (a) a `Cell` thread-local (`GC_LAST_COLLECTION_EXTERNAL_SIDE_BYTES`, a byte COUNT, no pointer) plus three pure predicates over it and `external_side_live_bytes()`; (b) that predicate added as a third disjunct of `tiny_parse_generational_collection_due`, which is read only from the three JSON.parse mutator-side boundaries (`gc_bump_malloc_trigger_inner`, `gc_collect_pending_suppressed_parse_slow`, `gc_schedule_parse_boundary_collection_if_pressure`), none of them reachable from `step_mark_propagation` or `step_sweep`; and (c) one extra `Cell` store in `note_collection_finished_arena_occupancy` plus two extra reads in the `PERRY_GC_DIAG` tiny-parse line. `note_collection_finished_arena_occupancy` runs from `publish_reclaim_outcome` in the Publish subphase, i.e. AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local, exactly as #9831's store on the same line does. Nothing added allocates a GC object, relocates anything, or runs a JS callback, and neither census boundary moved. Re-audited 2026-09-14 for the drained-bytes counterweight to that band, which touched `gc/policy.rs` again. Four hunks: a second `Cell` thread-local (`GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL`, a byte COUNT); one increment of it inside `gc_note_external_side_free`; a pure read (`external_side_old_reclaim_pressure_bytes`) substituted for `external_side_live_bytes()` at the four old-reclaim pressure sites; and one `Cell` store at the top of `finish_full_old_reclaim_baseline`. None of it can run between the census boundaries. `gc_note_external_side_free` is also reached by mutator-side tape materialization, regex scratch teardown, native-addon adjustments and buffer replacement. Its added operation is only a saturating increment of a scalar Cell, with no GC allocation, relocation, collection or JS callback, so this wider caller set does not invalidate the census window. `finish_full_old_reclaim_baseline` runs from `publish_reclaim_outcome` in the Publish subphase, the same place #9831's store already sits. The pressure reads happen at trigger decisions, before a cycle starts. No allocation, relocation, collection or JS callback is added to the mark-complete -> sweep-entry window, and neither boundary moved. Re-audited 2026-09-15 for turnloop P0, which touched `gc/mod.rs` with one added call: `crate::event_pump::shutdown_wait_driver()` inside `js_gc_release_current_thread_collection_side_allocations`, the process-exit funnel. That function runs once no more JavaScript can run on the thread, never from inside a collection cycle; the added call drops the thread's turnloop wait loop (closing its kqueue/epoll descriptor) and may print a diagnostic line. It allocates no GC object, relocates nothing, starts no collection and runs no JS callback. The census boundaries and the mark-complete -> sweep-entry window are untouched.. Re-audited 2026-09-16 for the copying minor's per-parent weak-holder fact: `gc/mod.rs` gains exactly one line, `mod copying_parent_facts;`, a module declaration. The module it declares holds `weak_holder_fact` (a read of the parent's `obj_type`/`class_id` via `weakref::is_weak_holder_header`) and the copying minor's `visit_slot_with_parent`, moved verbatim out of `gc/copying.rs` for the 2000-line lint. Both run only inside a COPYING MINOR, which skips both census boundaries (`census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` are synchronous-full only). Nothing was added to any full-cycle phase, and the declaration itself executes no code. Neither boundary moved and the synchronous mark-complete to sweep-entry window gains no allocation, relocation, collection or JS callback. Re-audited 2026-09-18 for the #10532 follow-up argument-list rooting fix, which touched `gc/mod.rs`. The only change there is `mod collection_points;` plus a `pub(crate) use collection_points::collection_point;` re-export (and, under `#[cfg(test)]`, `arm_collection_point`). `collection_point` is an inline no-op outside `cfg(test)`; under test it only runs a copying minor when called from ordinary MUTATOR code (`proxy.rs`'s `Reflect.apply` rebind path and `registry.rs`'s rest-array bundler), never from inside `step_mark_propagation` or `step_sweep`. Neither `census_pass1_if_armed` nor `census_take_if_armed_at_full_sweep_start` is reachable from it, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-18 (same PR, round 2) for the added `arm_collection_point_after` re-export in `gc/mod.rs`: another pure re-export line, same as the `collection_point`/`arm_collection_point` one already covered above. `arm_collection_point_after` only changes test-only arming state in `collection_points.rs` (which named site fires and on which hit); it still runs no mark/sweep control flow. Re-audited 2026-09-19 for #10735 (require.main threading): gc/mod.rs gains exactly one line, `reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);`, registering the new CJS_MAIN_MODULE thread-local's mutable-root scanner beside the existing `scan_module_path_roots_mut` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it runs during root scanning, before mark propagation completes, and does not execute between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-20 for #10834 (inherited-property read cache). `gc/mod.rs` gains exactly one line: `reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);` in `gc_init()`. A scanner registration adds a root SOURCE for the mutable-root walks. The walk runs inside `RootScanCycleState::step_current_subphase`, i.e. entirely within the RootScan phase: `step_root_scan` only sets `self.phase = GcCyclePhase::MarkPropagation` once that loop reports done (`gc/cycle.rs:958-961`), and `census_pass1_if_armed()` fires at the END of `step_mark_propagation` (`gc/cycle.rs:982`). The scanner therefore runs strictly BEFORE the window opens and can never execute between the boundaries. Its body is a bounded walk of a fixed 512-entry thread-local array calling `visit_tagged_usize_slot` / `visit_usize_slot`; it allocates nothing, relocates nothing and runs no JS callback. Same shape as #9769, #9976/#9977, #10054, #10055 and #10735, all previously cleared. The PR also adds an `INHERITED_READ_CACHE` entry to `DEAD_KEY_PRUNES` in `gc/dead_owner.rs` (not a pinned source). That registry is consumed by `IncrementalSweepState::with_dead_collection_finalize` at `gc/cycle.rs:1548`, which is AFTER `census_take_if_armed_at_full_sweep_start` at `gc/cycle.rs:1505` has already `take()`n the snapshot out of the thread-local -- the same argument that cleared #9845's `collect_dead_registered_regexps_post_trace`. The prune reads addresses and zeroes entries; no GC allocation, relocation or callback. Both additions sit outside the window, on opposite sides of it. Neither boundary moved and the synchronous mark-complete to sweep-entry interval is unchanged. Re-audited 2026-09-22 for #10399 (per-thread module init), which touched `gc/mod.rs`. Two hunks, both init-time: a new free function `raise_default_thread_stack_floor()` and one call to it at the top of `js_gc_init`, before `enter_current_thread_image`'s successor statements. The function reads `RUST_MIN_STACK` from the environment and, only when it is unset, sets it to 32 MiB so a thread spawned against a multi-megabyte static TLS block still has usable stack (glibc carves static TLS out of the thread's stack mapping). It touches no heap object, allocates no GC object, relocates nothing and runs no JS callback. `js_gc_init` is the first runtime call of a compiled `main`, so it runs once before any cycle exists, and it is not reachable from `step_mark_propagation` or `step_sweep`. Same shape as the 2026-09-11 startup-memory-profile re-audit, which cleared the pre-main allocator-policy constructor in the same function. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-23 (size/runtime-decouple, #11135) after the binary-size branch touched `gc/census.rs`, `gc/mod.rs` and `gc/policy.rs`. census.rs: `census_pass1_if_armed` / `census_take_if_armed_at_full_sweep_start` keep their bodies verbatim, moved into `_impl` functions compiled only with the new `gc-instruments` feature (without it both are empty and `census_path()` is `None`, so nothing is ever armed); the take still empties PASS1_MARKED before `take_census`. gc/mod.rs: `gc_init` gains a startup env check that aborts when an instrument knob is set without the feature, before any cycle exists. gc/policy.rs: env-knob OnceLock caches now initialize through `crate::once_init::get_or_init` (same closures, same values). No mark/sweep control flow between the two census boundaries changed; the window is unchanged. Re-audited for Fetch handle reclamation: cycle.rs only redirects the incomplete-cycle Drop cancellation hook to also cancel the Fetch trace. The full-trace finish hook removes native records and cached slots without allocating GC objects or invoking JS; it cannot relocate the census addresses before sweep entry. Re-audited 2026-09-22 for #10928 (one proportional old-reclaim rule), which touched `gc/policy.rs`. Six hunks. (a) Two new thread-locals, `GC_OLD_RECLAIM_PRE_IN_USE_BYTES` (`Cell`) and `GC_OLD_RECLAIM_BACKOFF_SHIFT` (`Cell`): both are byte/shift COUNTS, neither holds a pointer. (b) `gc_old_reclaim_growth_band_bytes` gains a `Cell` read and a left shift -- pure arithmetic over byte counts. (c) `old_reclaim_pressure_due` loses the #7937 absolute first-crossing arm, splits its pure form out as `old_reclaim_pressure_due_inner`, and calls `note_old_reclaim_cycle_started()` when the answer is true. That predicate is read at TRIGGER decisions only -- the allocation-point `gc_check_trigger` and `gc_budgeted_due_trigger` at safepoints -- i.e. before a cycle starts, never between the boundaries; an allocation inside the window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before this code, the same argument the 2026-09-14 due-check fast-path re-audit made for the same function. Even if it did run there it would be sound: `note_old_reclaim_cycle_started` stores one scalar `Cell` from `pacing_arena_in_use_bytes()` (a read of `arena_live_allocated_bytes`), which allocates no GC object, relocates nothing and runs no JS callback -- the window's contract. (d) `update_old_reclaim_backoff` is called only from `finish_full_old_reclaim_baseline`, which runs from `publish_reclaim_outcome` in the Publish subphase, AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local -- exactly where #9831's store and the medium-parse pacing store already sit. (e) `gc_old_reclaim_debt_bytes` drops the absolute arm it mirrored; it remains pure arithmetic read at debt/trigger decisions. (f) `#[cfg(test)]` seams, absent from production builds. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback between them; the change alters only WHEN a collection is scheduled, never what runs inside one. Neither boundary moved and the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-24 for #10960 (growth-aware old-reclaim backoff), which touched `gc/policy.rs` again. One new thread-local, `GC_OLD_RECLAIM_LAST_POST_IN_USE_BYTES` (`Cell`), a byte COUNT that holds no pointer. It is written only by `update_old_reclaim_backoff`, which runs from `finish_full_old_reclaim_baseline` in the Publish subphase, after `step_sweep` has already taken the snapshot out of the thread-local; the change there is pure integer arithmetic deciding whether to widen the band. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback, and neither window boundary moved. Re-audited 2026-09-24 after the class-capture environment added one `reg_scanner!` registration (`scan_class_env_roots_mut`, visiting each guarded class environment's owner class object) to `gc/mod.rs`: a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-25 after the inherited-access lane touched `gc/mod.rs`: the change is one `reg_scanner!` registration for `object::chain_store::scan_chain_store_roots_mut`, a root scanner that visits store-site chain verdicts (one interned key pointer each) during root scanning. It runs at the start of a cycle, never between mark completion and sweep entry, relocates nothing and runs no JS callback. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10698 (allocation-point trigger watermark), which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains `pub(crate) mod trigger_watermark;` and a `pub(crate) use` re-export -- no control flow. In `gc/policy.rs`: (a) `gc_check_trigger` becomes an out-of-line wrapper over an inlined fast path that reads the watermark cell, the malloc registry's length through an unguarded shared borrow and the inline allocator's offset, then either returns or runs the previous body unchanged (`gc_check_trigger_evaluate`). An allocation inside the window reaches it with `GC_FLAG_IN_ALLOC` set: the fast path returns without acting, which is the outcome of the `GC_FLAG_IN_ALLOC` early return it would otherwise reach, and the slow path still takes that early return. (b) The due-trigger evaluation also returns a watermark -- integer arithmetic over values it already read -- published only on the no-action return past every entry guard. (c) The ladder's thresholds and flags are retyped `Cell` -> `TriggerInput`, whose writes add one store retiring the watermark; heap-generation advances and `Arena::set_current` retire it too. (d) `#[cfg(test)]` seams. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback; `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10498 (class-accessor cache), which adds one `reg_scanner!` registration (`scan_class_accessor_cache_roots_mut`, marking and rewriting the cache's key strings) to `gc/mod.rs`: again a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-26 for #10572: `gc/mod.rs` gains one `#[cfg(not(feature = \"hot-diag\"))]` call to `hot_diag::refuse_knobs_without_hot_diag()` in `gc_init`, a startup-only knob check that runs before any cycle and alters no mark/sweep control flow; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 (charter step 5, P0): the field-representation census adds `mod census_field_repr;` to `gc/mod.rs` and, in `census.rs`, one Rust-owned accumulator fed from `visit_object` inside `take_census`, which runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED; it reads slots and the per-object layout maps (try_borrow), allocates nothing on the JS heap and alters no mark/sweep control flow. The window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 (class constructors as function objects, #11414): `gc/mod.rs` gains one `reg_scanner!` registration (`object::class_value::scan_class_value_roots_mut`, the per-agent class function-object table, visited and rewritten) \u2014 a root-scanner registration that alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 after #11659 added `verify::verify_array_hole_tails_at_collection()` to `gc/mod.rs` and `gc/policy.rs`. Every call sits at a collection or budgeted-cycle ENTRY, beside `roots::ensure_stack_maps_built()`, i.e. before mark begins and so before `census_pass1_if_armed` opens the window. The call is a read-only walk of array headers (debug builds, or release with PERRY_GC_VERIFY_ARRAY_HOLES): it neither allocates GC memory, relocates, runs JS, nor reads or writes PASS1_MARKED, and it panics rather than continuing on a violation. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Pin re-audited 2026-09-29 after PR #11646 added one call inside that window, in `cycle.rs` beside `census_take_if_armed_at_full_sweep_start`: `object::shapes::store_kind::audit_heap_at_full_sweep_start()`. It compiles to nothing unless the `shape-fact-audit` feature is on; when on it walks the arena read-only (`gc::for_each_live_object_at_sweep_start`, in `gc/verify.rs`) and reads each marked object header and its shape record. It allocates no GC object, moves nothing, runs no JS callback and holds no address past the walk, so PASS1_MARKED stays valid across it; a disagreement panics rather than continuing. Re-audited 2026-09-29 after the pinned-roots fix touched `gc/cycle.rs` and `gc/mod.rs`: the block-persistence live-block predicate now reads GC_FLAG_MARKED alone (pinned objects are marked as roots, so the set of live blocks is unchanged), and `gc/mod.rs` adds one `reg_scanner!` registration. Neither relocates, runs JS, or moves the mark-complete to sweep-entry window. Re-audited again 2026-09-29 (same fix, copying-minor follow-up): `BlockPersistCycleState`'s force-mark in `gc/cycle.rs` no longer skips an unmarked pinned header, so it marks and pushes it like any other object of a live block. That is marking inside the budgeted cycle's persistence step: nothing relocates, no JS runs, and the budgeted path still skips both census boundaries. The window is unchanged.", + "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete \u2192 sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs \u2014 it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase \u2014 after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged \u2014 `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` \u2014 and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` \u2192 `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only \u2014 no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound \u2014 the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses \u2014 no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects \u2014 and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module \u2014 all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed. Re-audited for #8512: gc/mod.rs only enables the existing PTY mutable-root scanner on Windows; it changes no mark/sweep phase or census boundary. The scanner visits NaN-boxed slots without running JS callbacks. Re-audited 2026-09-12 for the single regular-expression engine: `gc/mod.rs` changes `mod prefetch;` to `pub(crate) mod prefetch;` so the RegExp owner-table walks can prefetch headers, a visibility change with no new call in collector control flow; `gc/census.rs` changes only its `#[cfg(test)]` `regex_census_tests` module, dropping assertions for the previous engine's cache rows. Neither boundary (`census_pass1_if_armed` in `step_mark_propagation`, `census_take_if_armed_at_full_sweep_start` in `step_sweep`) nor the synchronous mark-complete to sweep-entry interval changes. Re-audited 2026-09-13 after the #10169 fix touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains only `pub(crate) use` re-exports (`policy::note_young_leaf_born_old`, `policy::young_generation_holds_a_nursery`, `promote_in_place::{young_generation_measured_dying, young_generation_measured_retained}`, and cfg(test) survival seeders). `gc/policy.rs` gains a `Cell` thread-local (`GC_YOUNG_LEAF_BORN_OLD`, no pointer), its setter, a pure predicate over `copying_from_space_in_use_bytes` vs the base nursery cap, and a consumed-once branch at the top of `gc_budgeted_due_trigger` that may answer `YoungScavengeCap` ahead of `OldReclaim`. That branch decides WHICH collection a safepoint starts (a minor instead of a full); it runs before any cycle begins and never inside one, so the mark-complete \u2192 sweep-entry window of a synchronous full \u2014 where PASS1_MARKED is populated and consumed within one `run_to_completion` \u2014 is unchanged, and neither hunk adds an allocation, a JS callback, or a relocation to it. Re-audited 2026-09-13 for the heap generation (#10164 cross-call search positions): `gc/mod.rs` only declares `pub(crate) mod heap_generation;`. `gc/cycle.rs` wraps the `Sweep` and `Reclaim` arms of `GcCycleState::step` in a `HeapChange` scope and opens one inside `atomic_finalize_minor_prelude`'s evacuation branch (with a nested one around old-page defrag). Opening and closing a scope only increments two thread-local integer cells (`HEAP_GENERATION`, `OPEN_HEAP_CHANGES`); a first thread-local read may allocate a key through the global allocator, which neither relocates nor runs JS. The `Sweep` scope opens immediately before `step_sweep`, i.e. before `census_take_if_armed_at_full_sweep_start` takes PASS1_MARKED out of TLS, and adds no relocation, collection or JS callback to the synchronous mark-complete to sweep-entry window; the minor-prelude scope is unreachable from a full cycle, which bypasses `MinorPrelude`. Neither boundary nor the intervening control flow changed. Re-audited 2026-09-13 for #10182 block-granular reclamation, which touched `gc/cycle.rs`. Two hunks: (a) in the `RememberedSetRebuild` subphase of AtomicFinalize \u2014 INSIDE the window \u2014 the require-marked old-to-young rebuild is now constructed with `OldToYoungRememberedRebuildState::new_skipping`, whose cursor never enters blocks the census recorded as holding no reached, pinned or pre-marked object (`BlockCensus::unmarked_blocks`); computing that list reads `arena_block_snapshots()` and allocates one `Vec` through the global allocator. It visits a subset of the same objects the rebuild already walked (every skipped object would have been rejected as unmarked), and it neither allocates a GC object, relocates anything, nor runs a JS callback. (b) In `step_sweep`, `IncrementalSweepState::with_block_skip` runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED out of TLS. Neither boundary moved and the synchronous mark-complete to sweep-entry interval gains no relocation, collection or callback. Re-audited 2026-09-11 for the startup memory profile: gc/mod.rs only retains the pre-main allocator-policy constructor in js_gc_init. The constructor applies process allocation options, without invoking GC or JS. No census boundary, collector phase, or mark-complete to sweep-entry control flow changed. Re-audited 2026-09-13 for #10179: census.rs only adds a native regex cache metadata row and its unit assertion; snapshot consumption and the full-cycle window are unchanged. Re-audited 2026-09-14 for the GC due-check fast path, which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` only changes the safepoint re-exports: `gc_runtime_safepoint` becomes cfg(test) and `gc_runtime_safepoint_poll` is added. `gc/policy.rs`: the budgeted step returns a debt-free `GcStepReport` (debt is attached by the FFI and test entry points after the step returns) and moves cycle start/step into an out-of-line `gc_budgeted_start_or_step`; `gc_check_trigger` reuses a repeatable due-trigger answer through `DueTriggerMemo`, placed after its `GC_FLAG_IN_ALLOC` and suppression early returns; the young scavenge cap reuses the old-gen pressure value the due trigger already read and checks the census-seeded flag first. All of it runs from mutator safepoints, allocation-point trigger checks and the host step API, before a cycle starts or between budgeted steps. None of it is reachable between `census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` of a synchronous full: an allocation inside that window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before the changed code. No allocation, relocation, collection or JS callback is added to the window. Re-audited 2026-09-14 for the tiny-parse nursery-cap boundary, which touched `gc/policy.rs`. It adds `tiny_parse_generational_collection_due`, a pure predicate (the existing `tiny_parse_pressure_due` OR the existing `young_scavenge_cap_due` read), and calls it instead of `tiny_parse_pressure_due` from `gc_bump_malloc_trigger_inner` and `gc_collect_pending_suppressed_parse_slow` (generational branch only) and from `gc_schedule_parse_boundary_collection_if_pressure`. All three are JSON.parse mutator-side boundaries, none reachable from `step_mark_propagation` or `step_sweep`; the predicate reads counters and allocates nothing. Neither census boundary nor the synchronous mark-complete to sweep-entry interval changed. Re-audited 2026-09-13 for #10182's full-collection throughput follow-up, which touched `gc/cycle.rs` in one hunk, INSIDE the window: the `RememberedSetRebuild` subphase of a synchronous full now first asks `verify::full_remembered_rebuild_provably_empty` and, when it holds, installs `OldToYoungRememberedRebuildState::provably_empty()` (an empty sticky set, no walk) instead of the require-marked rebuild. The predicate reads `arena_block_snapshots()` (one `Vec` through the global allocator), the census's per-block reached/pre-marked facts and the malloc registry's length; the constructor bumps a `Cell` counter and prints one line under `PERRY_GC_DIAG`. None of it allocates a GC object, relocates anything, collects, or runs a JS callback, and both census boundaries stay where they were. Re-audited 2026-09-14 for #10182's pacing-full work, which touched `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs`. `gc/cycle.rs`: `GcCycleState::new_full` no longer calls `materialize_all_promoted_page_runs`; that call ran in the constructor, before the census and far before `census_pass1_if_armed`, and removing it adds nothing to the window. `gc/mod.rs`: one `mod promoted_cohort;` declaration. `gc/policy.rs`: (a) `credit_promoted_bytes_to_old_baseline` also credits a `Cell` cohort counter (it runs after a copying minor completes); (b) `finish_full_old_reclaim_baseline` also records the verified old live bytes and resets that counter (Publish, after `step_sweep` consumed the snapshot); (c) `gc_safepoint_moving_minor` arms and disarms the promotion-census record around its nursery minor and calls `run_promoted_cohort_full_if_due`, which starts a synchronous full through the same `gc_collect_full_mark_sweep_with_trigger` entry and reads byte counters before and after it. All of it runs before a cycle starts or after it completes; none of it runs between mark completion and sweep entry, allocates a GC object, relocates anything, or calls into JS. The census the promoted-cohort full may adopt from the promotion walk is built in `BuildValidPointerSet`, before either boundary. Both boundaries are unchanged. Re-audited 2026-09-14 for the #10182 dead-stack scrub in `gc/cycle.rs`: `step_build_valid_pointer_set` now calls `scrub_dead_stack_below`, which zeroes a local array in its own frame (dead stack below the caller), right after the census finishes \u2014 in `BuildValidPointerSet`, before the root scan and far before `census_pass1_if_armed`. It writes no heap memory, allocates nothing, relocates nothing and calls no JS; both boundaries are unchanged. Re-audited 2026-09-14 for #10241 (cohort survival), which touched `gc/cycle.rs` and `gc/policy.rs`. `gc/cycle.rs`: one call, `promoted_cohort::survival::check_minor_view_at_full_sweep_start()`, in `step_sweep` immediately AFTER `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS, i.e. outside the window. It is a no-op unless a promoted-cohort full armed its survival probe; when armed it walks the old page index over the preceding minor's dirty pages (`old_arena_walk_objects_on_pages`, Rust-allocator Vecs), reads GC headers' mark flags and the slots of unmarked ones, and records one enum. It writes no heap memory, allocates no GC object, relocates nothing and calls no JS. `gc/policy.rs`: `run_promoted_cohort_full_if_due` arms the probe before `gc_collect_full_mark_sweep_with_trigger` and takes it after the full returns (feeding `note_full_measured_promotion_survival` and one diagnostic line); both run before a cycle starts or after it completes. Both boundaries are unchanged. Re-audited 2026-09-14 for #10241's in-place-only cohort: `gc/policy.rs` drops the `promoted_cohort::note_promoted` call from `credit_promoted_bytes_to_old_baseline` (the copying minor now calls `promoted_cohort::note_minor_promotion` itself, after the credit). Both run at the end of a copying minor, outside any full cycle; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-14 for the parse-boundary side-allocation band (medium-parse pacing), which touched `gc/policy.rs`. Three hunks: (a) a `Cell` thread-local (`GC_LAST_COLLECTION_EXTERNAL_SIDE_BYTES`, a byte COUNT, no pointer) plus three pure predicates over it and `external_side_live_bytes()`; (b) that predicate added as a third disjunct of `tiny_parse_generational_collection_due`, which is read only from the three JSON.parse mutator-side boundaries (`gc_bump_malloc_trigger_inner`, `gc_collect_pending_suppressed_parse_slow`, `gc_schedule_parse_boundary_collection_if_pressure`), none of them reachable from `step_mark_propagation` or `step_sweep`; and (c) one extra `Cell` store in `note_collection_finished_arena_occupancy` plus two extra reads in the `PERRY_GC_DIAG` tiny-parse line. `note_collection_finished_arena_occupancy` runs from `publish_reclaim_outcome` in the Publish subphase, i.e. AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local, exactly as #9831's store on the same line does. Nothing added allocates a GC object, relocates anything, or runs a JS callback, and neither census boundary moved. Re-audited 2026-09-14 for the drained-bytes counterweight to that band, which touched `gc/policy.rs` again. Four hunks: a second `Cell` thread-local (`GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL`, a byte COUNT); one increment of it inside `gc_note_external_side_free`; a pure read (`external_side_old_reclaim_pressure_bytes`) substituted for `external_side_live_bytes()` at the four old-reclaim pressure sites; and one `Cell` store at the top of `finish_full_old_reclaim_baseline`. None of it can run between the census boundaries. `gc_note_external_side_free` is also reached by mutator-side tape materialization, regex scratch teardown, native-addon adjustments and buffer replacement. Its added operation is only a saturating increment of a scalar Cell, with no GC allocation, relocation, collection or JS callback, so this wider caller set does not invalidate the census window. `finish_full_old_reclaim_baseline` runs from `publish_reclaim_outcome` in the Publish subphase, the same place #9831's store already sits. The pressure reads happen at trigger decisions, before a cycle starts. No allocation, relocation, collection or JS callback is added to the mark-complete -> sweep-entry window, and neither boundary moved. Re-audited 2026-09-15 for turnloop P0, which touched `gc/mod.rs` with one added call: `crate::event_pump::shutdown_wait_driver()` inside `js_gc_release_current_thread_collection_side_allocations`, the process-exit funnel. That function runs once no more JavaScript can run on the thread, never from inside a collection cycle; the added call drops the thread's turnloop wait loop (closing its kqueue/epoll descriptor) and may print a diagnostic line. It allocates no GC object, relocates nothing, starts no collection and runs no JS callback. The census boundaries and the mark-complete -> sweep-entry window are untouched.. Re-audited 2026-09-16 for the copying minor's per-parent weak-holder fact: `gc/mod.rs` gains exactly one line, `mod copying_parent_facts;`, a module declaration. The module it declares holds `weak_holder_fact` (a read of the parent's `obj_type`/`class_id` via `weakref::is_weak_holder_header`) and the copying minor's `visit_slot_with_parent`, moved verbatim out of `gc/copying.rs` for the 2000-line lint. Both run only inside a COPYING MINOR, which skips both census boundaries (`census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` are synchronous-full only). Nothing was added to any full-cycle phase, and the declaration itself executes no code. Neither boundary moved and the synchronous mark-complete to sweep-entry window gains no allocation, relocation, collection or JS callback. Re-audited 2026-09-18 for the #10532 follow-up argument-list rooting fix, which touched `gc/mod.rs`. The only change there is `mod collection_points;` plus a `pub(crate) use collection_points::collection_point;` re-export (and, under `#[cfg(test)]`, `arm_collection_point`). `collection_point` is an inline no-op outside `cfg(test)`; under test it only runs a copying minor when called from ordinary MUTATOR code (`proxy.rs`'s `Reflect.apply` rebind path and `registry.rs`'s rest-array bundler), never from inside `step_mark_propagation` or `step_sweep`. Neither `census_pass1_if_armed` nor `census_take_if_armed_at_full_sweep_start` is reachable from it, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-18 (same PR, round 2) for the added `arm_collection_point_after` re-export in `gc/mod.rs`: another pure re-export line, same as the `collection_point`/`arm_collection_point` one already covered above. `arm_collection_point_after` only changes test-only arming state in `collection_points.rs` (which named site fires and on which hit); it still runs no mark/sweep control flow. Re-audited 2026-09-19 for #10735 (require.main threading): gc/mod.rs gains exactly one line, `reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);`, registering the new CJS_MAIN_MODULE thread-local's mutable-root scanner beside the existing `scan_module_path_roots_mut` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it runs during root scanning, before mark propagation completes, and does not execute between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-20 for #10834 (inherited-property read cache). `gc/mod.rs` gains exactly one line: `reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);` in `gc_init()`. A scanner registration adds a root SOURCE for the mutable-root walks. The walk runs inside `RootScanCycleState::step_current_subphase`, i.e. entirely within the RootScan phase: `step_root_scan` only sets `self.phase = GcCyclePhase::MarkPropagation` once that loop reports done (`gc/cycle.rs:958-961`), and `census_pass1_if_armed()` fires at the END of `step_mark_propagation` (`gc/cycle.rs:982`). The scanner therefore runs strictly BEFORE the window opens and can never execute between the boundaries. Its body is a bounded walk of a fixed 512-entry thread-local array calling `visit_tagged_usize_slot` / `visit_usize_slot`; it allocates nothing, relocates nothing and runs no JS callback. Same shape as #9769, #9976/#9977, #10054, #10055 and #10735, all previously cleared. The PR also adds an `INHERITED_READ_CACHE` entry to `DEAD_KEY_PRUNES` in `gc/dead_owner.rs` (not a pinned source). That registry is consumed by `IncrementalSweepState::with_dead_collection_finalize` at `gc/cycle.rs:1548`, which is AFTER `census_take_if_armed_at_full_sweep_start` at `gc/cycle.rs:1505` has already `take()`n the snapshot out of the thread-local -- the same argument that cleared #9845's `collect_dead_registered_regexps_post_trace`. The prune reads addresses and zeroes entries; no GC allocation, relocation or callback. Both additions sit outside the window, on opposite sides of it. Neither boundary moved and the synchronous mark-complete to sweep-entry interval is unchanged. Re-audited 2026-09-22 for #10399 (per-thread module init), which touched `gc/mod.rs`. Two hunks, both init-time: a new free function `raise_default_thread_stack_floor()` and one call to it at the top of `js_gc_init`, before `enter_current_thread_image`'s successor statements. The function reads `RUST_MIN_STACK` from the environment and, only when it is unset, sets it to 32 MiB so a thread spawned against a multi-megabyte static TLS block still has usable stack (glibc carves static TLS out of the thread's stack mapping). It touches no heap object, allocates no GC object, relocates nothing and runs no JS callback. `js_gc_init` is the first runtime call of a compiled `main`, so it runs once before any cycle exists, and it is not reachable from `step_mark_propagation` or `step_sweep`. Same shape as the 2026-09-11 startup-memory-profile re-audit, which cleared the pre-main allocator-policy constructor in the same function. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-23 (size/runtime-decouple, #11135) after the binary-size branch touched `gc/census.rs`, `gc/mod.rs` and `gc/policy.rs`. census.rs: `census_pass1_if_armed` / `census_take_if_armed_at_full_sweep_start` keep their bodies verbatim, moved into `_impl` functions compiled only with the new `gc-instruments` feature (without it both are empty and `census_path()` is `None`, so nothing is ever armed); the take still empties PASS1_MARKED before `take_census`. gc/mod.rs: `gc_init` gains a startup env check that aborts when an instrument knob is set without the feature, before any cycle exists. gc/policy.rs: env-knob OnceLock caches now initialize through `crate::once_init::get_or_init` (same closures, same values). No mark/sweep control flow between the two census boundaries changed; the window is unchanged. Re-audited for Fetch handle reclamation: cycle.rs only redirects the incomplete-cycle Drop cancellation hook to also cancel the Fetch trace. The full-trace finish hook removes native records and cached slots without allocating GC objects or invoking JS; it cannot relocate the census addresses before sweep entry. Re-audited 2026-09-22 for #10928 (one proportional old-reclaim rule), which touched `gc/policy.rs`. Six hunks. (a) Two new thread-locals, `GC_OLD_RECLAIM_PRE_IN_USE_BYTES` (`Cell`) and `GC_OLD_RECLAIM_BACKOFF_SHIFT` (`Cell`): both are byte/shift COUNTS, neither holds a pointer. (b) `gc_old_reclaim_growth_band_bytes` gains a `Cell` read and a left shift -- pure arithmetic over byte counts. (c) `old_reclaim_pressure_due` loses the #7937 absolute first-crossing arm, splits its pure form out as `old_reclaim_pressure_due_inner`, and calls `note_old_reclaim_cycle_started()` when the answer is true. That predicate is read at TRIGGER decisions only -- the allocation-point `gc_check_trigger` and `gc_budgeted_due_trigger` at safepoints -- i.e. before a cycle starts, never between the boundaries; an allocation inside the window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before this code, the same argument the 2026-09-14 due-check fast-path re-audit made for the same function. Even if it did run there it would be sound: `note_old_reclaim_cycle_started` stores one scalar `Cell` from `pacing_arena_in_use_bytes()` (a read of `arena_live_allocated_bytes`), which allocates no GC object, relocates nothing and runs no JS callback -- the window's contract. (d) `update_old_reclaim_backoff` is called only from `finish_full_old_reclaim_baseline`, which runs from `publish_reclaim_outcome` in the Publish subphase, AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local -- exactly where #9831's store and the medium-parse pacing store already sit. (e) `gc_old_reclaim_debt_bytes` drops the absolute arm it mirrored; it remains pure arithmetic read at debt/trigger decisions. (f) `#[cfg(test)]` seams, absent from production builds. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback between them; the change alters only WHEN a collection is scheduled, never what runs inside one. Neither boundary moved and the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-24 for #10960 (growth-aware old-reclaim backoff), which touched `gc/policy.rs` again. One new thread-local, `GC_OLD_RECLAIM_LAST_POST_IN_USE_BYTES` (`Cell`), a byte COUNT that holds no pointer. It is written only by `update_old_reclaim_backoff`, which runs from `finish_full_old_reclaim_baseline` in the Publish subphase, after `step_sweep` has already taken the snapshot out of the thread-local; the change there is pure integer arithmetic deciding whether to widen the band. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback, and neither window boundary moved. Re-audited 2026-09-24 after the class-capture environment added one `reg_scanner!` registration (`scan_class_env_roots_mut`, visiting each guarded class environment's owner class object) to `gc/mod.rs`: a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-25 after the inherited-access lane touched `gc/mod.rs`: the change is one `reg_scanner!` registration for `object::chain_store::scan_chain_store_roots_mut`, a root scanner that visits store-site chain verdicts (one interned key pointer each) during root scanning. It runs at the start of a cycle, never between mark completion and sweep entry, relocates nothing and runs no JS callback. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10698 (allocation-point trigger watermark), which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains `pub(crate) mod trigger_watermark;` and a `pub(crate) use` re-export -- no control flow. In `gc/policy.rs`: (a) `gc_check_trigger` becomes an out-of-line wrapper over an inlined fast path that reads the watermark cell, the malloc registry's length through an unguarded shared borrow and the inline allocator's offset, then either returns or runs the previous body unchanged (`gc_check_trigger_evaluate`). An allocation inside the window reaches it with `GC_FLAG_IN_ALLOC` set: the fast path returns without acting, which is the outcome of the `GC_FLAG_IN_ALLOC` early return it would otherwise reach, and the slow path still takes that early return. (b) The due-trigger evaluation also returns a watermark -- integer arithmetic over values it already read -- published only on the no-action return past every entry guard. (c) The ladder's thresholds and flags are retyped `Cell` -> `TriggerInput`, whose writes add one store retiring the watermark; heap-generation advances and `Arena::set_current` retire it too. (d) `#[cfg(test)]` seams. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback; `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10498 (class-accessor cache), which adds one `reg_scanner!` registration (`scan_class_accessor_cache_roots_mut`, marking and rewriting the cache's key strings) to `gc/mod.rs`: again a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-26 for #10572: `gc/mod.rs` gains one `#[cfg(not(feature = \"hot-diag\"))]` call to `hot_diag::refuse_knobs_without_hot_diag()` in `gc_init`, a startup-only knob check that runs before any cycle and alters no mark/sweep control flow; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 (charter step 5, P0): the field-representation census adds `mod census_field_repr;` to `gc/mod.rs` and, in `census.rs`, one Rust-owned accumulator fed from `visit_object` inside `take_census`, which runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED; it reads slots and the per-object layout maps (try_borrow), allocates nothing on the JS heap and alters no mark/sweep control flow. The window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 (class constructors as function objects, #11414): `gc/mod.rs` gains one `reg_scanner!` registration (`object::class_value::scan_class_value_roots_mut`, the per-agent class function-object table, visited and rewritten) \u2014 a root-scanner registration that alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 after #11659 added `verify::verify_array_hole_tails_at_collection()` to `gc/mod.rs` and `gc/policy.rs`. Every call sits at a collection or budgeted-cycle ENTRY, beside `roots::ensure_stack_maps_built()`, i.e. before mark begins and so before `census_pass1_if_armed` opens the window. The call is a read-only walk of array headers (debug builds, or release with PERRY_GC_VERIFY_ARRAY_HOLES): it neither allocates GC memory, relocates, runs JS, nor reads or writes PASS1_MARKED, and it panics rather than continuing on a violation. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Pin re-audited 2026-09-29 after PR #11646 added one call inside that window, in `cycle.rs` beside `census_take_if_armed_at_full_sweep_start`: `object::shapes::store_kind::audit_heap_at_full_sweep_start()`. It compiles to nothing unless the `shape-fact-audit` feature is on; when on it walks the arena read-only (`gc::for_each_live_object_at_sweep_start`, in `gc/verify.rs`) and reads each marked object header and its shape record. It allocates no GC object, moves nothing, runs no JS callback and holds no address past the walk, so PASS1_MARKED stays valid across it; a disagreement panics rather than continuing. Re-audited 2026-09-29 after the pinned-roots fix touched `gc/cycle.rs` and `gc/mod.rs`: the block-persistence live-block predicate now reads GC_FLAG_MARKED alone (pinned objects are marked as roots, so the set of live blocks is unchanged), and `gc/mod.rs` adds one `reg_scanner!` registration. Neither relocates, runs JS, or moves the mark-complete to sweep-entry window. Re-audited again 2026-09-29 (same fix, copying-minor follow-up): `BlockPersistCycleState`'s force-mark in `gc/cycle.rs` no longer skips an unmarked pinned header, so it marks and pushes it like any other object of a live block. That is marking inside the budgeted cycle's persistence step: nothing relocates, no JS runs, and the budgeted path still skips both census boundaries. The window is unchanged. Re-audited 2026-09-29 for this-as-a-parameter stage 3: `gc/mod.rs` registers `scan_dispatch_binding_roots_mut` in place of `scan_implicit_this_roots_mut` (the implicit-`this` cell is deleted; the same scanner body keeps `new.target`, the static-`this` override and the static private-owner stack) and rewords its comment; a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window.", "window": { "start": { "file": "crates/perry-runtime/src/gc/census.rs", @@ -415,7 +409,7 @@ "sources": { "crates/perry-runtime/src/gc/census.rs": "b2b60a124ea0f1131108940a8e675dae92f24192ecb482f87b6183e7641a2677", "crates/perry-runtime/src/gc/cycle.rs": "9b992d27700382c84758f6f4a5574115a9862234bed2e18e0c5a3cca25c25236", - "crates/perry-runtime/src/gc/mod.rs": "99fc1ae6ee60476713e4d9d2d3641a65de200d26e5345986c4263f5904af02b6", + "crates/perry-runtime/src/gc/mod.rs": "ee122498f4071588eca2ea07199a5c60dd85184a485a430a688f379f41cd81d5", "crates/perry-runtime/src/gc/policy.rs": "84a869e0aa09e932a4d2b6601186260129ba851b4482f29579047713c60fc95e", "crates/perry-runtime/src/gc/progress.rs": "a5ad3971bbe4047229ca57325234780daa85921dbc778e1c08dff4ad07ccfb96" } diff --git a/scripts/local_binding_type_allowlist.json b/scripts/local_binding_type_allowlist.json index 1ec895ec04..e1f2adbf22 100644 --- a/scripts/local_binding_type_allowlist.json +++ b/scripts/local_binding_type_allowlist.json @@ -23,7 +23,7 @@ "access": "local_type_hint", "count": 1, "classification": "runtime-validated", - "reason": "The declared function type only narrows which loop-called bindings are ATTEMPTED for entry resolution; it licenses no call. The resolution itself goes through js_closure_resolve_arrow_direct_call(handle, arity), which validates the value is an arrow closure of that arity at runtime, so a wrong or stale declared type yields a failed resolution and the ordinary dynamic call, never a wrong callee. Async and generator function types are excluded at this read." + "reason": "The declared function type only narrows which loop-called bindings are ATTEMPTED for entry resolution; it licenses no call. The resolution itself goes through js_closure_resolve_plain_direct_call(handle, arity), which validates the value is a directly callable closure of that arity at runtime, so a wrong or stale declared type yields a failed resolution and the ordinary dynamic call, never a wrong callee. Async and generator function types are excluded at this read." }, { "path": "crates/perry-codegen/src/codegen/mod.rs", diff --git a/scripts/publish_perry_ffi.sh b/scripts/publish_perry_ffi.sh index 32f7ccdfd1..bb5cf1ff7c 100755 --- a/scripts/publish_perry_ffi.sh +++ b/scripts/publish_perry_ffi.sh @@ -4,23 +4,26 @@ # `cargo login` once if missing). # # **Prerequisites — publish dependency packages before perry-ffi.** The -# required perry-native-registration dependency and the optional perry-runtime -# dependency (runtime-link) must both resolve on crates.io. Cargo checks the -# optional edge even when external wrappers leave that feature off. -# For dependency versions not already available, the order is: +# required perry-native-registration and perry-abi dependencies and the +# optional perry-runtime dependency (runtime-link) must all resolve on +# crates.io. Cargo checks the optional edge even when external wrappers leave +# that feature off. For dependency versions not already available, the order +# is: # # 1. cargo publish --dry-run -p perry-native-registration # cargo publish -p perry-native-registration +# cargo publish -p perry-abi (the JS body ABI perry-ffi re-exports) # 2. cargo publish -p perry-runtime # (Its own workspace dependencies need prior publication as appropriate; # dependency publication remains a manual maintainer prerequisite.) # 3. ./scripts/publish_perry_ffi.sh (perry-ffi dry run) # ./scripts/publish_perry_ffi.sh --really-publish (perry-ffi publication) # -# Run from the workspace root. Ships whatever the -# `[workspace.package].version` currently in `Cargo.toml` says, so -# make sure the changeset + Cargo.toml were updated for this release -# first (the standard workflow already covers that). +# Run from the workspace root. perry-ffi carries its OWN version +# (`crates/perry-ffi/Cargo.toml`, 0.6 since the receiver became a body +# parameter); its dependencies ship at the `[workspace.package].version` +# currently in `Cargo.toml`, so make sure the changeset + Cargo.toml were +# updated for this release first (the standard workflow already covers that). set -euo pipefail WORKSPACE_ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -28,7 +31,7 @@ cd "$WORKSPACE_ROOT" VERSION="$(grep -E '^version = "0\.5\.' Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" echo "Workspace version: ${VERSION}" -echo "Prerequisites: publish perry-native-registration, then perry-runtime, before perry-ffi." +echo "Prerequisites: publish perry-native-registration and perry-abi, then perry-runtime, before perry-ffi." echo "The dependency versions required by Cargo.toml must already resolve on crates.io." # Verify the package builds and would publish cleanly. `--allow-dirty` diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index aa559be145..5fd1f68588 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -69,7 +69,8 @@ 31 crates/perry-runtime/src/array/iterator.rs 12 crates/perry-runtime/src/array/push_pop.rs 12 crates/perry-runtime/src/array/sort.rs -12 crates/perry-runtime/src/async_hooks.rs +11 crates/perry-runtime/src/async_hooks.rs +1 crates/perry-runtime/src/async_hooks/context_snapshots.rs # moved-from: crates/perry-runtime/src/async_hooks.rs 5 crates/perry-runtime/src/atomics.rs 7 crates/perry-runtime/src/builtins/console.rs 12 crates/perry-runtime/src/builtins/globals.rs diff --git a/scripts/thread_exit_address_globals.json b/scripts/thread_exit_address_globals.json index ad737206f1..7baa7ac1f8 100644 --- a/scripts/thread_exit_address_globals.json +++ b/scripts/thread_exit_address_globals.json @@ -2684,6 +2684,14 @@ "verdict": "no_heap_address", "why": "Per-bucket NmAttachFn fn pointers stored at native_module_registry.rs:759; code addresses." }, + { + "file": "crates/perry-runtime/src/object/native_module_registry.rs", + "names": [ + "NM_STREAM_SUBCLASS_INIT" + ], + "verdict": "no_heap_address", + "why": "The node_stream_subclass_init NmStreamSubclassInitFn fn pointer, its only store at native_module_registry.rs:518 (js_nm_install_stream); a code address." + }, { "file": "crates/perry-runtime/src/object/native_module_registry.rs", "names": [ diff --git a/scripts/unrooted_local_shape_baseline.json b/scripts/unrooted_local_shape_baseline.json index e6bcb04335..33e5dd77ad 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -2,9 +2,8 @@ "per_file": { "crates/perry-ext-better-sqlite3/src/lib.rs": 13, "crates/perry-ext-cheerio/src/lib.rs": 7, - "crates/perry-ext-events/src/lib.rs": 12, + "crates/perry-ext-events/src/lib.rs": 5, "crates/perry-ext-events/src/module_iterators.rs": 2, - "crates/perry-ext-events/src/module_on.rs": 3, "crates/perry-ext-events/src/tests.rs": 2, "crates/perry-ext-http/src/agent.rs": 3, "crates/perry-ext-http/src/client_request_surface.rs": 2, @@ -40,7 +39,7 @@ "crates/perry-stdlib/src/sqlite/dispatch.rs": 6, "crates/perry-stdlib/src/sqlite/node_stmt_session.rs": 4, "crates/perry-stdlib/src/sqlite/node_tag_store.rs": 3, - "crates/perry-stdlib/src/streams.rs": 25, + "crates/perry-stdlib/src/streams.rs": 24, "crates/perry-stdlib/src/streams/byob.rs": 12, "crates/perry-stdlib/src/streams/pipe.rs": 13, "crates/perry-stdlib/src/streams/strategy.rs": 9, @@ -57,9 +56,9 @@ "crates/perry-stdlib/src/webcrypto/util.rs": 1, "crates/perry-stdlib/src/worker_threads.rs": 12, "crates/perry-stdlib/src/worker_threads/direct_message.rs": 2, - "crates/perry-stdlib/src/worker_threads/worker_surface.rs": 7, + "crates/perry-stdlib/src/worker_threads/worker_surface.rs": 6, "crates/perry-stdlib/src/zlib.rs": 3 }, "schema_version": 3, - "total": 380 + "total": 368 } diff --git a/test-files/test_compat_buffers_typed.ts b/test-files/test_compat_buffers_typed.ts index e2661f1d0b..11dffd012a 100644 --- a/test-files/test_compat_buffers_typed.ts +++ b/test-files/test_compat_buffers_typed.ts @@ -340,7 +340,6 @@ crates/perry-runtime/src/closure.rs: - js_closure_get_func - js_closure_set_capture_f64 - js_closure_set_capture_ptr - - js_closure_unbind_this - js_create_callback - js_native_call_value - js_new_instance diff --git a/test-files/test_gap_this_param_receiver_routes.ts b/test-files/test_gap_this_param_receiver_routes.ts new file mode 100644 index 0000000000..2abde40b12 --- /dev/null +++ b/test-files/test_gap_this_param_receiver_routes.ts @@ -0,0 +1,30 @@ +import * as util from "node:util"; +const out: string[] = []; +const obj: any = { + k: 7, + cb(this: any, x: number, done: (e: any, v?: any) => void) { done(null, this && this.k + x); }, + async am(this: any, x: number) { return this.k * x; }, + old(this: any) { return this && this.k; }, +}; +obj.p = util.promisify(obj.cb); +obj.c = util.callbackify(obj.am); +obj.d = util.deprecate(obj.old, "old is deprecated", "DEP_TEST"); +function argsAccessor(this: any, a: number) { + const args: any = arguments; + Object.defineProperty(args, "g", { get(this: any) { return this === args; }, configurable: true }); + return args.g; +} +out.push("args-accessor-this=" + argsAccessor(1)); +const holder: any = Object.create((Intl as any).NumberFormat.prototype); +const r = (Intl as any).NumberFormat.call(holder, "en-US"); +out.push("intl-legacy-chain=" + (r === holder)); +out.push("deprecate=" + obj.d()); +const sorted = [3, 1, 2].sort(function (this: any, a: number, b: number) { return (this === undefined ? 0 : 100) + a - b; }); +out.push("sort=" + sorted.join("")); +obj.p(3).then((v: any) => { + out.push("promisify=" + v); + obj.c(5, (e: any, v: any) => { + out.push("callbackify=" + v); + console.log(out.join(" ")); + }); +});