diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 0d9c621145..5c47560502 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -985,7 +985,7 @@ extern "C" fn global_this_queue_microtask_thunk( /// Tag detection uses the same coarse NaN-box / GC-type discrimination /// the rest of the runtime relies on: arrays → `"[object Array]"`, /// strings → `"[object String]"`, null/undefined → matching tags, -/// numbers/bools → primitive tags, generic objects/closures → +/// numbers/bools/functions → primitive/builtin tags, generic objects → /// `"[object Object]"`. /// /// Unblocks ramda's `_isArguments.js` IIFE which evaluates @@ -1780,6 +1780,11 @@ fn set_intrinsic_to_string_tag(obj: *mut ObjectHeader, tag: &str) { f64::from_bits(crate::js_nanbox_string(tag_str as i64).to_bits()), ); } + crate::symbol::set_symbol_property_attrs( + obj as usize, + sym as usize, + super::PropertyAttrs::new(false, false, true), + ); } /// Build a `TypeError` value for a `%Generator.prototype%` method invoked on a @@ -2545,9 +2550,18 @@ pub(crate) fn populate_global_this_builtins(singleton: *mut ObjectHeader) { // gated on the AST shape and never read these fields. Math uses the // richer install that also exposes per-method name/length descriptors. match name { - "Math" => install_math_namespace(ns_obj), - "JSON" => install_json_namespace_members(ns_obj), - "Reflect" => install_reflect_namespace_members(ns_obj), + "Math" => { + install_math_namespace(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "Math"); + } + "JSON" => { + install_json_namespace_members(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "JSON"); + } + "Reflect" => { + install_reflect_namespace_members(ns_obj); + set_intrinsic_to_string_tag(ns_obj, "Reflect"); + } "Atomics" => install_atomics_namespace_members(ns_obj), "Intl" => crate::intl::install_intl_namespace(ns_obj), _ => {} diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index a6a3720dbc..00e1a89616 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1912,6 +1912,21 @@ pub unsafe extern "C" fn js_object_to_string(value: f64) -> f64 { let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); } + if (raw_addr >= 0x10000 && crate::closure::is_closure_ptr(raw_addr)) + || crate::object::is_class_object_ptr(raw_addr as *const u8) + { + let bytes = b"[object Function]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + if jsv.is_int32() { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if crate::object::is_class_id_registered(class_id) { + let bytes = b"[object Function]"; + let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); + } + } if jsv.is_int32() || jsv.is_number() { let bytes = b"[object Number]"; let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); diff --git a/test-parity/node-suite/globals/object-tostring-brands.ts b/test-parity/node-suite/globals/object-tostring-brands.ts new file mode 100644 index 0000000000..6251bd4550 --- /dev/null +++ b/test-parity/node-suite/globals/object-tostring-brands.ts @@ -0,0 +1,35 @@ +function ordinary() {} +const arrow = () => {}; +class Klass {} + +for (const [label, value] of [ + ["ordinary", ordinary], + ["arrow", arrow], + ["class", Klass], + ["Array", Array], + ["Math.max", Math.max], + ["Math", Math], + ["JSON", JSON], + ["Reflect", Reflect], +] as const) { + console.log(label + ":", Object.prototype.toString.call(value)); +} + +for (const [label, value] of [ + ["Math", Math], + ["JSON", JSON], + ["Reflect", Reflect], +] as const) { + const desc = Object.getOwnPropertyDescriptor(value, Symbol.toStringTag); + const rendered = desc + ? JSON.stringify([desc.value, desc.writable, desc.enumerable, desc.configurable]) + : "missing"; + console.log(label + " tag desc:", rendered); +} + +ordinary[Symbol.toStringTag] = "CallableOverride"; +console.log("function override:", Object.prototype.toString.call(ordinary)); + +const namespaceOverride = Object.create(Math); +Object.defineProperty(namespaceOverride, Symbol.toStringTag, { value: "NamespaceOverride" }); +console.log("namespace override:", Object.prototype.toString.call(namespaceOverride));