Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions changelog.d/11524-builtin-callee-no-toobject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
### Runtime

- fix(runtime): a method call on a primitive whose callee is a BUILT-IN no
longer boxes the receiver (#11509, re-land of #9800's `32f150b22`). ECMA-262
§10.3.1: a built-in function's `[[Call]]` does not run
`OrdinaryCallBindThis`. It receives `thisArg` unchanged and does its own
coercion, which every `String`/`Number`/`Boolean`/`BigInt`/`Object`
prototype thunk already does (they accept the raw primitive before looking
for a wrapper payload). `call_primitive_closure_value` boxed for them
anyway. For a string receiver, that `ToObject` wrapper materialises an own
index property per UTF-16 code unit. Only a sloppy USER callee gets the
wrapper now, which is the distinction the spec draws.

Unlike the original commit, built-in-ness is not read from the per-instance
`builtin_closure_length` side table. It is a new `BUILTIN` kind bit on the
closure BODY record (`closure/registry.rs`, next to `STRICT` and #10521's
`NON_CONSTRUCTOR`), set once per thunk by `install_proto_method`,
`install_proto_method_rest_with_length` and
`primitive_proto_method_closure_value`. The call site checks
`STRICT | BUILTIN` in the same single body-record lookup it already made for
strictness. The bit is a positive mark, so any body not registered as a
built-in keeps the old wrapper behaviour. No new table, and no
per-closure entries for the collector to prune.
5 changes: 4 additions & 1 deletion crates/perry-runtime/src/closure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub(crate) fn closure_side_table_census() -> Vec<crate::gc::census::SideTableRow
rows.extend(dynamic_props::dynamic_props_census());
rows
}
pub(crate) use registry::{
body_receives_primitive_this, closure_body_is_non_constructor, register_closure_body_builtin,
register_closure_body_non_constructor,
};
pub use registry::{
build_rest_array, build_rest_array_rooted, closure_arity, closure_is_arrow,
closure_is_bound_method, closure_length, dispatch_rest_bundled, dispatch_with_arity,
Expand All @@ -53,7 +57,6 @@ pub use registry::{
real_capture_count, resolve_strategy, DispatchStrategy, BOUND_FUNCTION_FUNC_PTR,
BOUND_METHOD_FUNC_PTR, CAPTURES_THIS_FLAG, CLOSURE_MAGIC, NO_THIS_REBIND_FLAG,
};
pub(crate) use registry::{closure_body_is_non_constructor, register_closure_body_non_constructor};

pub(crate) use dispatch::{
bound_function_lazy_name, bound_method_source_func_ptr, coerce_call_this, rebind_explicit_this,
Expand Down
32 changes: 31 additions & 1 deletion crates/perry-runtime/src/closure/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ pub(crate) struct ClosureBodyRecord {
/// site, which is faster — the registry is consulted only when needed.
rest_arity: u16,
/// `HAS_*` presence bits plus the boolean attributes (`ARROW`, `STRICT`,
/// `ASYNC`, `GENERATOR`, `ASYNC_GENERATOR`, `NON_CONSTRUCTOR`) and the
/// `ASYNC`, `GENERATOR`, `ASYNC_GENERATOR`, `NON_CONSTRUCTOR`, `BUILTIN`)
/// and the
/// 2-bit rest kind.
flags: u16,
/// 1-based index into `TRUSTED_TARGETS`; 0 = this body has no
Expand Down Expand Up @@ -126,6 +127,13 @@ mod body_flags {
/// such closure had to populate at allocation and the collector had to
/// prune when it died.
pub(super) const NON_CONSTRUCTOR: u16 = 1 << 10;
/// #11509: the body is a runtime-native BUILT-IN function. ECMA-262
/// §10.3.1: a built-in's `[[Call]]` does not run OrdinaryCallBindThis —
/// it receives `thisArg` unchanged and does its own coercion — so a
/// primitive receiver reaches it unboxed, exactly as for a `STRICT` body.
/// Recorded once per body by the built-in prototype-method installers,
/// never per closure instance.
pub(super) const BUILTIN: u16 = 1 << 11;
}

impl ClosureBodyRecord {
Expand Down Expand Up @@ -909,6 +917,28 @@ pub fn is_registered_strict_function(func_ptr: *const u8) -> bool {
body_record(func_ptr).is_some_and(|record| record.has(body_flags::STRICT))
}

/// #11509: mark every closure whose body is `func_ptr` as a built-in function,
/// so a method call on a primitive hands it the raw receiver instead of a
/// `ToObject` wrapper. Only runtime-native thunks may be registered here.
pub(crate) fn register_closure_body_builtin(func_ptr: *const u8) {
if func_ptr.is_null() {
return;
}
update_body_record(func_ptr, |record| record.flags |= body_flags::BUILTIN);
}

/// OrdinaryCallBindThis, decided by function KIND in one registry lookup:
/// a strict body or a built-in body observes the primitive `thisArg`
/// unchanged; only a sloppy user body is owed the `ToObject` wrapper.
#[inline(always)]
pub(crate) fn body_receives_primitive_this(func_ptr: *const u8) -> bool {
if func_ptr.is_null() {
return false;
}
body_record(func_ptr)
.is_some_and(|record| record.has(body_flags::STRICT) || record.has(body_flags::BUILTIN))
}

pub fn closure_is_arrow(closure: *const ClosureHeader) -> bool {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/object/global_this/install_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ pub(crate) fn install_proto_method(
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
crate::closure::js_register_closure_arity(func_ptr, arity);
crate::closure::register_closure_body_builtin(func_ptr);
super::super::native_module::set_bound_native_closure_name(closure, method_name);
// #3143: record this method's spec `.length` per closure instance — all
// noop-backed methods share one func_ptr, so the func-ptr arity registry
Expand Down Expand Up @@ -835,6 +836,7 @@ pub(crate) fn install_proto_method_rest_with_length(
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
crate::closure::js_register_closure_rest(func_ptr, call_fixed_arity);
crate::closure::register_closure_body_builtin(func_ptr);
super::super::native_module::set_bound_native_closure_name(closure, method_name);
super::super::native_module::set_builtin_closure_length(closure as usize, spec_length);
super::super::native_module::set_builtin_closure_non_constructable(closure as usize);
Expand Down
14 changes: 8 additions & 6 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,15 @@ unsafe fn call_primitive_closure_value(
}
// OrdinaryCallBindThis: a strict callee observes the raw primitive
// receiver (`Number.prototype.f = function(){"use strict"; return
// typeof this}` must see `"number"` for `(5).f()`); only a sloppy
// callee gets the ToObject wrapper — boxed ONCE up front so writes
// through `this` land on the wrapper the body later observes.
// typeof this}` must see `"number"` for `(5).f()`), and so does a
// BUILT-IN (§10.3.1: its [[Call]] takes `thisArg` unchanged and coerces
// itself — every primitive prototype thunk accepts the raw primitive
// before it looks for a wrapper payload). Only a sloppy USER callee gets
// the ToObject wrapper — boxed ONCE up front so writes through `this`
// land on the wrapper the body later observes. For a string receiver
// that wrapper costs an own index property per UTF-16 code unit (#11509).
let func_ptr = crate::closure::get_valid_func_ptr(ptr as *const crate::closure::ClosureHeader);
let strict_callee =
!func_ptr.is_null() && crate::closure::is_registered_strict_function(func_ptr);
let this_receiver = if strict_callee {
let this_receiver = if crate::closure::body_receives_primitive_this(func_ptr) {
receiver_h.get_nanbox_f64()
} else {
crate::object::js_object_coerce(receiver_h.get_nanbox_f64())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,64 @@ fn the_wrapper_counter_moves_when_a_receiver_is_boxed() {
"if this cannot move, the codePointAt assertion above is vacuous"
);
}

/// #11509: ECMA-262 §10.3.1 hands a BUILT-IN's `[[Call]]` the `thisArg`
/// unchanged, so `call_primitive_closure_value` must not `ToObject` a primitive
/// receiver for one — only a sloppy USER callee is owed that wrapper. The
/// built-in-ness is a property of the closure BODY (a registry bit set by the
/// prototype-method installer), not a per-instance side-table entry.
///
/// Pins both directions. A method installed through `install_proto_method`
/// receives the raw string (no new wrapper, and it still answers correctly);
/// an unregistered closure body — what a sloppy user function looks like to
/// this predicate — still gets boxed. Without the negative half, a predicate
/// that answered "built-in" for everything would pass.
#[test]
fn builtin_callee_gets_the_primitive_receiver_and_a_user_callee_the_wrapper() {
unsafe {
let s = crate::string::js_string_from_bytes(b"a".as_ptr(), 1);
let recv = f64::from_bits(JSValue::string_ptr(s).bits());

let proto = crate::object::js_object_alloc(0, 4);
let method = crate::object::global_this::install_proto_method(
proto,
"codePointAt",
crate::object::string_proto_thunks::string_proto_code_point_at_thunk as *const u8,
1,
);
let before = crate::builtins::test_boxed_primitive_payload_count();
let cp = super::call_primitive_closure_value(
recv,
JSValue::from_bits(method.to_bits()),
[0.0f64].as_ptr(),
1,
);
assert_eq!(
cp,
Some(97.0),
"the built-in still sees \"a\" through the raw receiver"
);
assert_eq!(
crate::builtins::test_boxed_primitive_payload_count(),
before,
"a built-in callee must receive the primitive, not a ToObject wrapper"
);

extern "C" fn sloppy_user_body(_c: *const crate::closure::ClosureHeader) -> f64 {
0.0
}
let user = crate::closure::js_closure_alloc(sloppy_user_body as *const u8, 0);
let user_value = crate::value::js_nanbox_pointer(user as i64);
let before = crate::builtins::test_boxed_primitive_payload_count();
let _ = super::call_primitive_closure_value(
recv,
JSValue::from_bits(user_value.to_bits()),
std::ptr::null(),
0,
);
assert!(
crate::builtins::test_boxed_primitive_payload_count() > before,
"a sloppy user callee is still owed the ToObject wrapper"
);
}
}
1 change: 1 addition & 0 deletions crates/perry-runtime/src/object/primitive_proto_thunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ fn primitive_proto_method_closure_value(method_name: &str, func_ptr: *const u8,
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
crate::closure::js_register_closure_arity(func_ptr, arity);
crate::closure::register_closure_body_builtin(func_ptr);
super::native_module::set_bound_native_closure_name(closure, method_name);
super::native_module::set_builtin_closure_length(closure as usize, arity);
super::native_module::set_builtin_closure_non_constructable(closure as usize);
Expand Down
Loading