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
20 changes: 20 additions & 0 deletions crates/perry-hir/src/lower/array_fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,26 @@ pub(crate) fn is_known_string_prototype_method(name: &str) -> bool {
)
}

/// Names of the universal `Object.prototype.<name>` methods inherited by
/// every object and boxed primitive (numbers, strings, booleans). Used by
/// the `typeof <Ctor>.prototype.<m>` AST fold (#2058) so feature-detection
/// idioms like `typeof Object.prototype.isPrototypeOf === "function"` and
/// `typeof Number.prototype.hasOwnProperty === "function"` agree with Node.
/// These are real functions on `Object.prototype`, so they resolve to
/// callable values on any inheriting receiver.
pub(crate) fn is_known_object_prototype_method(name: &str) -> bool {
matches!(
name,
"hasOwnProperty"
| "isPrototypeOf"
| "propertyIsEnumerable"
| "toLocaleString"
| "toString"
| "valueOf"
| "constructor"
)
}

/// Names of `Array.prototype.<name>` instance methods that Perry's runtime
/// implements (or short-circuits) — used by the `typeof Array.prototype.<m>`
/// / `typeof [].<m>` AST fold (#1777) so feature detection and the indirect
Expand Down
46 changes: 32 additions & 14 deletions crates/perry-hir/src/lower/lower_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,23 +775,41 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result<
if proto_prop.sym.as_ref() == "prototype"
&& ctx.lookup_local(ctor).is_none()
{
// #2058: every built-in prototype inherits the
// universal `Object.prototype` methods
// (`isPrototypeOf`, `hasOwnProperty`,
// `toString`, …), so `typeof
// Object.prototype.isPrototypeOf` /
// `typeof Number.prototype.hasOwnProperty` are
// "function" in Node. Plus each ctor's own
// prototype methods (and `Function.prototype`'s
// `call`/`apply`/`bind`).
let is_obj_proto = is_known_object_prototype_method(prop_name);
let is_fn = match ctor {
"Array" => is_known_array_prototype_method(prop_name),
"String" => is_known_string_prototype_method(prop_name),
"Object" => is_obj_proto,
"Function" => {
is_obj_proto
|| matches!(prop_name, "call" | "apply" | "bind")
}
"Array" => {
is_obj_proto
|| is_known_array_prototype_method(prop_name)
}
"String" => {
is_obj_proto
|| is_known_string_prototype_method(prop_name)
}
// Number/Boolean prototypes: the handful of
// real methods are all functions in Node.
"Number" => matches!(
prop_name,
"toFixed"
| "toPrecision"
| "toExponential"
| "toString"
| "valueOf"
| "toLocaleString"
),
"Boolean" => {
matches!(prop_name, "toString" | "valueOf")
// ctor-specific methods plus the inherited
// Object.prototype methods are all functions.
"Number" => {
is_obj_proto
|| matches!(
prop_name,
"toFixed" | "toPrecision" | "toExponential"
)
}
"Boolean" => is_obj_proto,
_ => false,
};
if is_fn {
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-hir/src/lower/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ pub(crate) use typed_parse::{extract_typed_parse_source_order, resolve_typed_par

mod array_fold;
pub(crate) use array_fold::{
is_known_array_prototype_method, is_known_array_static_method, is_known_object_static_method,
is_known_array_prototype_method, is_known_array_static_method,
is_known_object_prototype_method, is_known_object_static_method,
is_known_string_prototype_method, try_fold_array_method_call,
};

Expand Down
55 changes: 55 additions & 0 deletions crates/perry-runtime/src/object/field_get_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,41 @@ pub extern "C" fn js_object_get_field_by_name(
}
}
}
// #2058: a raw, unboxed finite f64 NUMBER receiver (e.g. `(5).toString`,
// or `n.isPrototypeOf` where `n: number`) reaches here with its float
// bits intact — numbers are NOT NaN-boxed in Perry, so `5.0` arrives as
// 0x4014_0000_0000_0000. That is neither a NaN-box tag (top16 >= 0x7FF8)
// nor a masked heap pointer (those have top16 == 0), so the generic
// pointer logic below would dereference the float bits as an
// `ObjectHeader` → SIGSEGV. Detect the primitive number first: return a
// bound-method closure for the inherited Number/Object prototype methods
// (so `typeof n.toString === "function"` holds and the value is
// callable), and `undefined` for any other key (matching property reads
// on primitives). Date timestamps and Web-Stream handles are raw f64 too,
// but both are special-cased above, so they never reach this branch.
{
let bits = obj as u64;
let f = f64::from_bits(bits);
// Registered Date timestamps are also raw finite f64 — leave them to
// the existing Date handling (the `_f64` wrapper above already routed
// `date.constructor`), so this branch never changes Date behavior.
if !key.is_null()
&& f.is_finite()
&& (bits >> 48) != 0
&& !crate::date::is_registered_date_bits(bits)
{
unsafe {
let name_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
let name_len = (*key).byte_len as usize;
let name_bytes = std::slice::from_raw_parts(name_ptr, name_len);
if is_primitive_proto_method(name_bytes) {
let result = super::js_class_method_bind(f, name_ptr, name_len);
return JSValue::from_bits(result.to_bits());
}
}
return JSValue::undefined();
}
}
// Strip NaN-boxing tags if present (defensive: handle POINTER_TAG, UNDEFINED, NULL, etc.)
let obj = {
let bits = obj as u64;
Expand Down Expand Up @@ -2150,6 +2185,26 @@ pub extern "C" fn js_object_get_field_by_name_f64(
f64::from_bits(value.bits())
}

/// #2058: the universal `Object.prototype` methods inherited by every value,
/// including primitive numbers. Read as a property *value* (e.g.
/// `const f = n.toString`, `typeof n.isPrototypeOf`), these resolve to real
/// callable functions in Node — Perry binds them lazily via
/// `js_class_method_bind` so the value is both `typeof "function"` and
/// dispatchable through `js_native_call_method` (every name here has a
/// corresponding dispatch arm). `constructor` is excluded: it is a property
/// holding the `Number` function, not a bound method.
fn is_primitive_proto_method(key: &[u8]) -> bool {
matches!(
key,
b"toString"
| b"valueOf"
| b"hasOwnProperty"
| b"isPrototypeOf"
| b"propertyIsEnumerable"
| b"toLocaleString"
)
}

fn is_timer_handle_method_key(key: &[u8]) -> bool {
matches!(
key,
Expand Down
33 changes: 33 additions & 0 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,39 @@ pub unsafe extern "C" fn js_native_call_method(
return f64::from_bits(JSValue::bool(true).bits());
}

// `prim.isPrototypeOf(v)` — true iff the receiver appears in `v`'s
// prototype chain. #2058: a primitive receiver (number/string/boolean,
// reached via the `js_class_method_bind` value-read path) is never on
// another object's prototype chain, so the result is always `false`.
// Scoped to non-pointer receivers so object/class-prototype receivers
// keep their existing dispatch. Returning a clean boolean keeps
// `typeof n.isPrototypeOf === "function"` honest: the bound value is
// actually callable rather than throwing.
"isPrototypeOf" if !jsval.is_pointer() => {
return f64::from_bits(JSValue::bool(false).bits());
}

// `prim.valueOf()` — a primitive's `valueOf` returns the primitive
// itself (number/boolean/string/bigint). #2058: makes the bound
// value-read `const f = n.valueOf` callable. Pointer receivers keep
// their existing object/handle-specific handling above.
"valueOf" if !jsval.is_pointer() => {
return object;
}

// `value.toLocaleString()` — for primitives Node returns the same
// string as `toString()` (no locale data). Delegate so the bound
// value-read (#2058) is callable.
"toLocaleString" if !jsval.is_pointer() => {
return js_native_call_method(
object,
b"toString".as_ptr() as *const i8,
"toString".len(),
args_ptr,
args_len,
);
}

// Function.prototype.call(thisArg, ...args) — invoke the receiver
// closure with `thisArg` bound as `this` and the remaining args
// passed positionally. Ramda's curry helpers (`_curry1`, `_curry2`,
Expand Down
32 changes: 32 additions & 0 deletions test-files/test_issue_2058_proto_method_values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Issue #2058: built-in prototype methods read AS PROPERTY VALUES (not called)
// — via the prototype object, or off a primitive receiver — resolved to
// `undefined` (and a primitive receiver SIGSEGV'd). They are real functions in
// Node, so `typeof` must be "function" and the bound value must be callable.

// --- Via the prototype object (Object/Number/String/Array/Function). ---
console.log(typeof Object.prototype.isPrototypeOf);
console.log(typeof Object.prototype.hasOwnProperty);
console.log(typeof Object.prototype.toString);
console.log(typeof Object.prototype.valueOf);
console.log(typeof Object.prototype.propertyIsEnumerable);
console.log(typeof Number.prototype.isPrototypeOf);
console.log(typeof Number.prototype.hasOwnProperty);
console.log(typeof Number.prototype.toFixed);
console.log(typeof String.prototype.isPrototypeOf);
console.log(typeof Array.prototype.hasOwnProperty);

// --- The sibling Function.prototype.{call,apply,bind} gap (the title). ---
console.log(typeof Function.prototype.call);
console.log(typeof Function.prototype.apply);
console.log(typeof Function.prototype.bind);
console.log(typeof Function.prototype.toString);

// --- On a primitive receiver (this previously crashed). ---
var n = 5;
console.log(typeof n.isPrototypeOf, typeof n.hasOwnProperty, typeof n.toString);
console.log(typeof n.valueOf, typeof n.toLocaleString, typeof n.propertyIsEnumerable);

// --- The bound values are actually callable (direct invocation). ---
console.log(n.toString());
console.log(n.valueOf());
console.log(n.isPrototypeOf({}));