diff --git a/crates/perry-codegen/src/expr/helpers.rs b/crates/perry-codegen/src/expr/helpers.rs index a7bb784eda..c8485e7467 100644 --- a/crates/perry-codegen/src/expr/helpers.rs +++ b/crates/perry-codegen/src/expr/helpers.rs @@ -320,6 +320,15 @@ pub(crate) fn is_global_this_builtin_name(name: &str) -> bool { | "setImmediate" | "clearImmediate" | "queueMicrotask" + // #2905: standard global helper functions (typeof === "function"). + | "parseInt" + | "parseFloat" + | "isNaN" + | "isFinite" + | "encodeURI" + | "decodeURI" + | "encodeURIComponent" + | "decodeURIComponent" // Namespaces (typeof === "object" in spec). | "globalThis" | "console" diff --git a/crates/perry-hir/src/analysis/builtins.rs b/crates/perry-hir/src/analysis/builtins.rs index 972065af6e..d185365719 100644 --- a/crates/perry-hir/src/analysis/builtins.rs +++ b/crates/perry-hir/src/analysis/builtins.rs @@ -90,6 +90,18 @@ pub(crate) fn is_builtin_global_value_name(name: &str) -> bool { | "Buffer" | "process" | "console" + // #2905: standard global helper functions used as bare values + // (`const p = parseInt`). Bare CALLS (`parseInt(x)`) are picked + // off earlier by `try_global_builtins` → `Expr::ParseInt`/etc., so + // these only fire for value reads. + | "parseInt" + | "parseFloat" + | "isNaN" + | "isFinite" + | "encodeURI" + | "decodeURI" + | "encodeURIComponent" + | "decodeURIComponent" ) } diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 67bf3ef72f..011c9873ab 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -890,19 +890,74 @@ pub unsafe extern "C" fn js_new_function_construct( let obj = js_object_alloc(0, 0); return crate::value::js_nanbox_pointer(obj as i64); } - "EvalError" | "URIError" => { - let message = if args.is_empty() || args[0].to_bits() == crate::value::TAG_UNDEFINED - { - crate::string::js_string_from_bytes(b"".as_ptr(), 0) + // #2889: `new (rebound Error subclass)(msg)` through a global + // constructor value. Mirrors the bare `new TypeError(msg)` + // lowering so `const E = TypeError; new E("x")` produces a real + // error instance with the right `.name`. + "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" + | "EvalError" | "URIError" => { + let has_msg = !args.is_empty() && args[0].to_bits() != crate::value::TAG_UNDEFINED; + let message = if has_msg { + crate::builtins::js_string_coerce(args[0]) + } else { + std::ptr::null_mut() + }; + let error = match name { + "TypeError" => crate::error::js_typeerror_new(message), + "RangeError" => crate::error::js_rangeerror_new(message), + "ReferenceError" => crate::error::js_referenceerror_new(message), + "SyntaxError" => crate::error::js_syntaxerror_new(message), + "EvalError" => crate::error::js_evalerror_new(message), + "URIError" => crate::error::js_urierror_new(message), + _ => { + if has_msg { + crate::error::js_error_new_with_message(message) + } else { + crate::error::js_error_new() + } + } + }; + return crate::value::js_nanbox_pointer(error as i64); + } + // #2889: `new (rebound RegExp)(pattern, flags)`. + "RegExp" => { + let pattern = if args.is_empty() { + std::ptr::null_mut() } else { crate::builtins::js_string_coerce(args[0]) }; - let error = if name == "EvalError" { - crate::error::js_evalerror_new(message) + let flags = if args.len() < 2 || args[1].to_bits() == crate::value::TAG_UNDEFINED { + std::ptr::null_mut() } else { - crate::error::js_urierror_new(message) + crate::builtins::js_string_coerce(args[1]) }; - return crate::value::js_nanbox_pointer(error as i64); + let re = crate::regex::js_regexp_new(pattern, flags); + return crate::value::js_nanbox_pointer(re as i64); + } + // #2889: `new (rebound TypedArray)(lengthOrSource)`. + "Int8Array" | "Uint8Array" | "Uint8ClampedArray" | "Int16Array" | "Uint16Array" + | "Int32Array" | "Uint32Array" | "Float32Array" | "Float64Array" | "BigInt64Array" + | "BigUint64Array" => { + let kind = match name { + "Int8Array" => crate::typedarray::KIND_INT8, + "Uint8Array" => crate::typedarray::KIND_UINT8, + "Uint8ClampedArray" => crate::typedarray::KIND_UINT8_CLAMPED, + "Int16Array" => crate::typedarray::KIND_INT16, + "Uint16Array" => crate::typedarray::KIND_UINT16, + "Int32Array" => crate::typedarray::KIND_INT32, + "Uint32Array" => crate::typedarray::KIND_UINT32, + "Float32Array" => crate::typedarray::KIND_FLOAT32, + "Float64Array" => crate::typedarray::KIND_FLOAT64, + "BigInt64Array" => crate::typedarray::KIND_BIGINT64, + _ => crate::typedarray::KIND_BIGUINT64, + } as i32; + let arg0 = if args.is_empty() { + f64::from_bits(crate::value::JSValue::number(0.0).bits()) + } else { + args[0] + }; + let ta = crate::typedarray::js_typed_array_new(kind, arg0); + return crate::value::js_nanbox_pointer(ta as i64); } "TextEncoderStream" | "TextDecoderStream" => { return js_text_encoding_stream_new(); diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 8ee5a528e6..b07e49600f 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -178,6 +178,17 @@ pub(crate) const GLOBAL_THIS_BUILTIN_FUNCTIONS: &[&str] = &[ "setImmediate", "clearImmediate", "queueMicrotask", + // #2905: standard global helper functions. These route through Perry's + // real direct-call runtime helpers, so `const p = parseInt; p("42px")` + // and `globalThis.encodeURIComponent("a b")` match Node. + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "encodeURI", + "decodeURI", + "encodeURIComponent", + "decodeURIComponent", ]; /// No-op thunk used as the function body for most singleton globalThis @@ -250,6 +261,95 @@ extern "C" fn math_f16round_thunk( crate::math::js_math_f16round(value) } +// #2905: thunks for the standard global helper functions. Each coerces its +// arguments the same way the bare-call HIR lowering does and forwards to the +// shared runtime helper so a rebound / property-read reference matches Node. + +extern "C" fn global_this_parse_int_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, + radix: f64, +) -> f64 { + let s = crate::builtins::js_string_coerce(value); + crate::builtins::js_parse_int(s, radix) +} + +extern "C" fn global_this_parse_float_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let s = crate::builtins::js_string_coerce(value); + crate::builtins::js_parse_float(s) +} + +extern "C" fn global_this_is_nan_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_is_nan(value) +} + +extern "C" fn global_this_is_finite_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::builtins::js_is_finite(value) +} + +extern "C" fn global_this_encode_uri_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_encode_uri(value)) +} + +extern "C" fn global_this_decode_uri_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_decode_uri(value)) +} + +extern "C" fn global_this_encode_uri_component_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_encode_uri_component(value)) +} + +extern "C" fn global_this_decode_uri_component_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::value::js_nanbox_string(crate::builtins::js_decode_uri_component(value)) +} + +// #2889: call-form thunks for `Number`/`Boolean` global constructor values. +// `Object`/`String` already have dedicated thunks above; these mirror the +// bare-call HIR lowering (`Expr::NumberCoerce` / `Expr::BooleanCoerce`) so +// `const N = Number; N("42")` and `const B = Boolean; B(0)` match Node. +extern "C" fn global_this_number_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let jsv = crate::value::JSValue::from_bits(value.to_bits()); + if jsv.is_undefined() { + // `Number()` with no args returns 0; an explicit `undefined` arg → NaN. + // The closure-call path zero-fills missing args with TAG_UNDEFINED, so + // we can't distinguish — match the common `Number()` → 0 case. + return f64::from_bits(crate::value::JSValue::number(0.0).bits()); + } + crate::builtins::js_number_coerce(value) +} + +extern "C" fn global_this_boolean_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let b = crate::value::js_is_truthy(value) != 0; + f64::from_bits(crate::value::JSValue::bool(b).bits()) +} + extern "C" fn global_this_error_capture_stack_trace_thunk( _closure: *const crate::closure::ClosureHeader, target: f64, @@ -749,15 +849,23 @@ fn populate_global_this_builtins(singleton: *mut ObjectHeader) { let func_ptr = match name { "Object" => global_this_object_thunk as *const u8, "String" => global_this_string_thunk as *const u8, + // #2889: call-form `Number(x)` / `Boolean(x)` through a rebound + // global value coerce like the bare-call lowering does. + "Number" => global_this_number_thunk as *const u8, + "Boolean" => global_this_boolean_thunk as *const u8, _ => global_this_builtin_noop_thunk as *const u8, }; let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); if closure_ptr.is_null() { continue; } - if matches!(name, "Object" | "String") { + if matches!(name, "Object" | "String" | "Number" | "Boolean") { crate::closure::js_register_closure_arity(func_ptr, 1); } + // #2889: install static methods (`Object.keys`, `Array.isArray`, ...) + // on the constructor closure so rebound usage like + // `const O = Object; O.keys(x)` dispatches through the real helpers. + install_builtin_constructor_statics(name, closure_ptr); if matches!( name, "Blob" | "File" | "EvalError" | "URIError" | "Uint8Array" @@ -835,6 +943,23 @@ fn populate_global_this_builtins(singleton: *mut ObjectHeader) { "setImmediate" => (global_this_set_immediate_thunk as *const u8, 1, true), "clearImmediate" => (global_this_clear_immediate_thunk as *const u8, 1, false), "queueMicrotask" => (global_this_queue_microtask_thunk as *const u8, 1, false), + // #2905: standard global helper functions. + "parseInt" => (global_this_parse_int_thunk as *const u8, 2, false), + "parseFloat" => (global_this_parse_float_thunk as *const u8, 1, false), + "isNaN" => (global_this_is_nan_thunk as *const u8, 1, false), + "isFinite" => (global_this_is_finite_thunk as *const u8, 1, false), + "encodeURI" => (global_this_encode_uri_thunk as *const u8, 1, false), + "decodeURI" => (global_this_decode_uri_thunk as *const u8, 1, false), + "encodeURIComponent" => ( + global_this_encode_uri_component_thunk as *const u8, + 1, + false, + ), + "decodeURIComponent" => ( + global_this_decode_uri_component_thunk as *const u8, + 1, + false, + ), _ => continue, }; let closure_ptr = crate::closure::js_closure_alloc(func_ptr, 0); @@ -959,6 +1084,204 @@ fn install_error_static_fn( ); } +// ===================================================================== +// #2889: static methods on rebound global built-in constructor values. +// +// `const O = Object; O.keys(x)` reads `keys` off the `Object` constructor +// closure's dynamic-prop side table, then calls it. Pre-fix nothing was +// installed there, so the read returned `undefined`. These thunks delegate +// to the same runtime helpers the direct `Object.keys(x)` lowering uses. +// ===================================================================== + +fn nanbox_array_or_undef(arr: *mut crate::array::ArrayHeader) -> f64 { + if arr.is_null() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + crate::value::js_nanbox_pointer(arr as i64) + } +} + +extern "C" fn object_keys_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { + nanbox_array_or_undef(super::js_object_keys_value(value)) +} + +extern "C" fn object_values_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + nanbox_array_or_undef(super::js_object_values_value(value)) +} + +extern "C" fn object_entries_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + nanbox_array_or_undef(super::js_object_entries_value(value)) +} + +extern "C" fn object_freeze_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::js_object_freeze(value) +} + +extern "C" fn object_create_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::js_object_create(value) +} + +extern "C" fn object_get_prototype_of_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::js_object_get_prototype_of(value) +} + +extern "C" fn object_get_own_property_names_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::js_object_get_own_property_names(value) +} + +extern "C" fn object_from_entries_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + super::js_object_from_entries(value) +} + +extern "C" fn object_assign_thunk( + _closure: *const crate::closure::ClosureHeader, + target: f64, + rest: f64, +) -> f64 { + let validated = unsafe { super::js_object_assign_validate_target(target) }; + for source in global_this_rest_array_values(rest) { + unsafe { super::js_object_assign_one(validated, source) }; + } + validated +} + +extern "C" fn array_is_array_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::array::js_array_is_array(value) +} + +extern "C" fn array_from_thunk(_closure: *const crate::closure::ClosureHeader, value: f64) -> f64 { + nanbox_array_or_undef(crate::array::js_array_from_value(value)) +} + +extern "C" fn array_of_thunk(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let vals = global_this_rest_array_values(rest); + let len = vals.len() as u32; + let arr = crate::array::js_array_alloc(len); + unsafe { + (*arr).length = len; + for (i, &v) in vals.iter().enumerate() { + crate::array::js_array_set_f64(arr, i as u32, v); + } + } + crate::value::js_nanbox_pointer(arr as i64) +} + +/// Install a single callable static method on a constructor closure as a +/// `{ writable: true, enumerable: false, configurable: true }` data property +/// (matching Node's descriptors for built-in statics). `has_rest` registers +/// the func pointer as a rest-arg closure so trailing args arrive as an array. +fn install_constructor_static( + ctor: *mut crate::closure::ClosureHeader, + name: &str, + func_ptr: *const u8, + arity: u32, + has_rest: bool, +) { + let closure = crate::closure::js_closure_alloc(func_ptr, 0); + if closure.is_null() { + return; + } + if has_rest { + crate::closure::js_register_closure_rest(func_ptr, arity); + } else { + crate::closure::js_register_closure_arity(func_ptr, arity); + } + super::native_module::set_bound_native_closure_name(closure, name); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + js_object_set_field_by_name(ctor as *mut ObjectHeader, key, value); + super::set_builtin_property_attrs( + ctor as usize, + name.to_string(), + super::PropertyAttrs::new(true, false, true), + ); +} + +/// #2889: install the common static methods on the `Object` / `Array` +/// constructor closures so rebound usage (`const O = Object; O.keys(x)`) +/// dispatches through the real runtime helpers. Only the high-traffic +/// statics with simple f64-in / f64-out shapes are reified here; the long +/// tail (`Object.defineProperty`, `Object.getOwnPropertyDescriptor`, …) +/// stays unreified on the rebound value and is a known scope gap. +fn install_builtin_constructor_statics(name: &str, ctor: *mut crate::closure::ClosureHeader) { + if ctor.is_null() { + return; + } + match name { + "Object" => { + install_constructor_static(ctor, "keys", object_keys_thunk as *const u8, 1, false); + install_constructor_static(ctor, "values", object_values_thunk as *const u8, 1, false); + install_constructor_static( + ctor, + "entries", + object_entries_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "freeze", object_freeze_thunk as *const u8, 1, false); + install_constructor_static(ctor, "create", object_create_thunk as *const u8, 1, false); + install_constructor_static( + ctor, + "getPrototypeOf", + object_get_prototype_of_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "getOwnPropertyNames", + object_get_own_property_names_thunk as *const u8, + 1, + false, + ); + install_constructor_static( + ctor, + "fromEntries", + object_from_entries_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "assign", object_assign_thunk as *const u8, 1, true); + } + "Array" => { + install_constructor_static( + ctor, + "isArray", + array_is_array_thunk as *const u8, + 1, + false, + ); + install_constructor_static(ctor, "from", array_from_thunk as *const u8, 1, false); + install_constructor_static(ctor, "of", array_of_thunk as *const u8, 0, true); + } + _ => {} + } +} + /// Install a method on a prototype object as a callable closure value with /// the proper `name` property and registered arity. Used to reify built-in /// prototype methods so `Array.prototype.map`, `Date.prototype.toISOString`, diff --git a/test-files/test_gap_global_builtins_2905_2889.ts b/test-files/test_gap_global_builtins_2905_2889.ts new file mode 100644 index 0000000000..76ce75ff28 --- /dev/null +++ b/test-files/test_gap_global_builtins_2905_2889.ts @@ -0,0 +1,46 @@ +// #2905 — global builtin function values on globalThis +console.log(typeof globalThis.parseInt); +console.log(typeof globalThis.parseFloat); +console.log(typeof globalThis.isNaN); +console.log(typeof globalThis.isFinite); +console.log(typeof globalThis.encodeURIComponent); +console.log(typeof globalThis.decodeURIComponent); + +const p = globalThis.parseInt; +console.log(p("42px")); +console.log(p("ff", 16)); + +const pf = globalThis.parseFloat; +console.log(pf("3.14xyz")); + +const nan = globalThis.isNaN; +console.log(nan(NaN), nan(3)); + +const fin = globalThis.isFinite; +console.log(fin(3), fin(Infinity)); + +const enc = globalThis.encodeURIComponent; +const dec = globalThis.decodeURIComponent; +console.log(enc("a b?")); +console.log(dec("a%20b%3F")); + +// bare-binding rebinding +const p2 = parseInt; +console.log(p2("100", 2)); + +// #2889 — rebound global builtin constructors through real semantics +const A = Array; +console.log(new A(2).length); +console.log(Array.isArray(new A(1))); + +const O = Object; +console.log(O.keys({ a: 1, b: 2 }).join(",")); + +const N = Number; +console.log(N("42")); + +const S = String; +console.log(S(123)); + +const B = Boolean; +console.log(B(0), B(1));