diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 8cfb4a9845..d0669a4c6e 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -572,6 +572,27 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> &[(DOUBLE, &func_double), (PTR, &args_ptr), (I64, &args_len)], )); } + // `new Function(p1, …, body)` with a RUNTIME-constructed body (the + // const-foldable / static-literal case was handled in HIR lowering; + // only dynamic bodies reach here). Perry is AOT-compiled and can't + // compile an arbitrary runtime string, so historically this produced + // a non-callable placeholder object. Route it through a runtime + // helper that recognizes the small set of well-known codegen-library + // templates (currently `depd`'s deprecation-wrapper, used eagerly by + // `send` → Next.js) and returns a working native function; anything + // else still gets the placeholder. NO general JS interpreter. + if class_name == "Function" { + let mut lowered_args: Vec = Vec::with_capacity(args.len()); + for a in args { + lowered_args.push(lower_expr(ctx, a)?); + } + let (args_ptr, args_len) = lower_js_args_array(ctx, &lowered_args); + return Ok(ctx.block().call( + DOUBLE, + "js_function_ctor_from_strings", + &[(PTR, &args_ptr), (I64, &args_len)], + )); + } // Built-in / native class (Promise, Error, Date, etc.) with // no dedicated lower_builtin_new handler — lower args for // side effects (closures, string literal interning) and diff --git a/crates/perry-codegen/src/runtime_decls/mod.rs b/crates/perry-codegen/src/runtime_decls/mod.rs index 9172de0ebe..7284a1c4af 100644 --- a/crates/perry-codegen/src/runtime_decls/mod.rs +++ b/crates/perry-codegen/src/runtime_decls/mod.rs @@ -77,6 +77,9 @@ pub fn declare_phase1(module: &mut LlModule) { module.declare_function("js_console_warn_number", VOID, &[DOUBLE]); // console.dir(value, options) — honors options.depth (#1199). module.declare_function("js_console_dir_with_options", VOID, &[DOUBLE, DOUBLE]); + // console[dynamicKey] — resolve a console method by runtime key string to + // the bound native closure (the `console[m](...)` computed-member form). + module.declare_function("js_console_method_by_value", DOUBLE, &[DOUBLE]); // NaN-boxing wrappers (bridge between raw handles and NaN-boxed doubles). module.declare_function("js_nanbox_string", DOUBLE, &[I64]); diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 9f4080b4de..05aa27e150 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1169,6 +1169,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { // invokes the constructor with IMPLICIT_THIS bound to the new // instance. Returns the NaN-boxed new instance pointer. module.declare_function("js_new_function_construct", DOUBLE, &[DOUBLE, PTR, I64]); + module.declare_function("js_function_ctor_from_strings", DOUBLE, &[PTR, I64]); // `new (...spread)` — codegen folds every argument (regular + // spread-expanded) into one JS array and hands it here; the runtime // materialises a flat buffer and forwards to `js_new_function_construct`. diff --git a/crates/perry-codegen/src/type_analysis.rs b/crates/perry-codegen/src/type_analysis.rs index 6a3af2001b..f6ec9a38c0 100644 --- a/crates/perry-codegen/src/type_analysis.rs +++ b/crates/perry-codegen/src/type_analysis.rs @@ -391,9 +391,18 @@ pub(crate) fn refine_type_from_init(ctx: &FnCtx<'_>, init: &Expr) -> Option, e: &Expr) -> bool { // back to the Error-string assumption when the receiver's type // is genuinely unknown (a real caught `Error`/`unknown`/`any`). Expr::PropertyGet { object, property } - if matches!(property.as_str(), "message" | "stack" | "name") => + // `.stack` excluded — may be an array via `Error.prepareStackTrace`. + if matches!(property.as_str(), "message" | "name") => { // If the receiver is a known user class / interface that // *declares* a field with this name, that field's declared diff --git a/crates/perry-hir/src/lower/expr_member.rs b/crates/perry-hir/src/lower/expr_member.rs index b3c391e65d..c434b3b0a2 100644 --- a/crates/perry-hir/src/lower/expr_member.rs +++ b/crates/perry-hir/src/lower/expr_member.rs @@ -2445,6 +2445,29 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re }); } } + // `console[dynamicKey]` — the receiver is a bare `console` ident + // (not shadowed: a local would have lowered `object` to a + // LocalGet, not the `GlobalGet(0)` builtin sentinel). The static + // `console.log` value read already resolves to a real bound + // closure via `js_native_module_property_by_name`, but the + // computed form fell through to `IndexGet { GlobalGet(0), key }`, + // i.e. reading the method off numeric 0 — so `console[m](...)` + // threw `(number). is not a function` (the Next.js + // `prefixedLog` wall). Route the runtime key through the same + // native-module resolver so both forms agree. + if matches!(&*object, Expr::GlobalGet(0)) + && matches!(member.obj.as_ref(), ast::Expr::Ident(id) if id.sym.as_ref() == "console") + { + return Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_console_method_by_value".to_string(), + param_types: vec![Type::Any], + return_type: Type::Any, + }), + args: vec![*index], + type_args: Vec::new(), + }); + } Ok(Expr::IndexGet { object, index }) } ast::MemberProp::PrivateName(private) => { diff --git a/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs b/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs index 63c43ee799..fe500bafbd 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs @@ -143,6 +143,24 @@ pub(super) fn lower_nested_fn_decl( body = new_body; } + // Prepend defaulted-parameter application (`if (p === undefined) p = + // `). Mirrors `lower_fn_decl` (fn_decl.rs) — without it, a + // `function f(a, opts = {})` nested in a block (which is what EVERY + // top-level function becomes once cjs_wrap wraps the module body in an + // IIFE) records `param.default` but never materializes the guard, so a + // caller that omits the arg (or pads it with TAG_UNDEFINED) reads the + // param as `undefined` instead of its default. This broke Next.js + // `recursiveReadDir(dir)` → `setupFsCheck` → the whole server boot + // (`Cannot convert undefined or null to object` destructuring the + // dropped `options = {}`). Defaults run before any destructuring, so + // prepend after the destructuring block (ending up first in the body). + let default_stmts = build_default_param_stmts(¶ms); + if !default_stmts.is_empty() { + let mut new_body = default_stmts; + new_body.append(&mut body); + body = new_body; + } + ctx.exit_scope(scope_mark); // Detect captured variables diff --git a/crates/perry-runtime/src/builtins/console.rs b/crates/perry-runtime/src/builtins/console.rs index 3ae853b3b9..a969cc8985 100644 --- a/crates/perry-runtime/src/builtins/console.rs +++ b/crates/perry-runtime/src/builtins/console.rs @@ -157,6 +157,36 @@ pub extern "C" fn js_console_log_as_closure() -> f64 { f64::from_bits(JSValue::pointer(closure_ptr as *const u8).bits()) } +/// `console[dynamicKey]` — resolve a console method by a RUNTIME key string, +/// returning the same bound native closure a static `console.` value read +/// produces. Codegen routes the computed-member form here (`console[m](...)`, +/// `const f = console[m]`, `loadEnvConfig(dir, true, console, false)` reaching +/// into a forwarded console). Without it the receiver collapsed to the +/// `GlobalGet(0)` sentinel and the method was read off numeric `0`, so the call +/// threw `(number).log is not a function` (the Next.js `_log.event` wall: +/// `prefixedLog` does `console[consoleMethod](...)`). A non-string key or an +/// unknown method name yields `undefined`, matching a real object miss. +#[no_mangle] +pub extern "C" fn js_console_method_by_value(key: f64) -> f64 { + // Apply JS property-key coercion before the lookup: `console[0]` → "0", + // `console[{toString:()=>'log'}]` → "log". A Symbol key coerces to a + // symbol (not a string method name), so it falls through to `undefined`, + // matching a real object miss. + let coerced = unsafe { crate::object::js_to_property_key(key) }; + let name = match jsvalue_string_content(coerced) { + Some(s) => s, + None => return f64::from_bits(crate::value::TAG_UNDEFINED), + }; + unsafe { + crate::object::js_native_module_property_by_name( + b"console".as_ptr(), + "console".len(), + name.as_ptr(), + name.len(), + ) + } +} + /// GC root scanner: pin the lazily-allocated `console.log`-as-closure /// singleton against the next sweep. pub fn scan_console_log_singleton_roots(mark: &mut dyn FnMut(f64)) { diff --git a/crates/perry-runtime/src/builtins/mod.rs b/crates/perry-runtime/src/builtins/mod.rs index 3f78f8602a..e98f97ba5f 100644 --- a/crates/perry-runtime/src/builtins/mod.rs +++ b/crates/perry-runtime/src/builtins/mod.rs @@ -61,12 +61,13 @@ pub use console::{ js_console_error_i32, js_console_error_number, js_console_error_spread, js_console_group, js_console_group_begin, js_console_group_end, js_console_log, js_console_log_as_closure, js_console_log_dynamic, js_console_log_i32, js_console_log_i64, js_console_log_number, - js_console_log_spread, js_console_new, js_console_new2, js_console_noop, js_console_time, - js_console_time_end, js_console_time_end_value, js_console_time_log, - js_console_time_log_spread, js_console_time_log_value, js_console_time_value, js_console_trace, - js_console_trace_spread, js_console_warn_dynamic, js_console_warn_i32, js_console_warn_number, - js_console_warn_spread, perry_debug_trace_init, perry_debug_trace_init_done, - scan_console_log_singleton_roots, scan_console_log_singleton_roots_mut, + js_console_log_spread, js_console_method_by_value, js_console_new, js_console_new2, + js_console_noop, js_console_time, js_console_time_end, js_console_time_end_value, + js_console_time_log, js_console_time_log_spread, js_console_time_log_value, + js_console_time_value, js_console_trace, js_console_trace_spread, js_console_warn_dynamic, + js_console_warn_i32, js_console_warn_number, js_console_warn_spread, perry_debug_trace_init, + perry_debug_trace_init_done, scan_console_log_singleton_roots, + scan_console_log_singleton_roots_mut, }; pub(crate) use console::{ diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 04df1d385e..3632526166 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -318,61 +318,10 @@ pub unsafe extern "C" fn js_throw_error_with_code( )) } -/// Build a Node-style system `Error`: `.message` + `.code` (the message→code -/// side table the `.code` getter reads) plus `.syscall` (string) and `.errno` -/// (number) own properties. `perry-ext-http` calls this to surface client -/// transport failures (`ECONNREFUSED`, `ENOTFOUND`, `ECONNRESET`, …) as the -/// real coded `Error` objects Node hands to `request.on('error')`, instead of -/// the bare message string the legacy path passed. -/// -/// The `.syscall`/`.errno` properties land in the `Error` expando side table -/// (`ERROR_USER_PROPS`) via [`crate::object::js_object_set_field_by_name`], -/// which recognizes `Error` cells by their NaN-box tag — `ErrorHeader` is not -/// an `ObjectHeader`, so a raw field write would corrupt it. -/// -/// # Safety -/// `msg_ptr`/`code_ptr`/`syscall_ptr` must each point to their stated number of -/// valid bytes, or be null with the matching length `0`. -#[no_mangle] -pub unsafe extern "C" fn js_node_system_error_value( - msg_ptr: *const u8, - msg_len: usize, - code_ptr: *const u8, - code_len: usize, - syscall_ptr: *const u8, - syscall_len: usize, - errno: f64, -) -> f64 { - let err_val = js_error_value_with_code(msg_ptr, msg_len, code_ptr, code_len, 0); - let obj = err_val.to_bits() as *mut crate::object::ObjectHeader; - if !syscall_ptr.is_null() && syscall_len > 0 { - let key = js_string_from_bytes(b"syscall".as_ptr(), 7) as *const StringHeader; - let sval_str = js_string_from_bytes(syscall_ptr, syscall_len as u32); - let sval = crate::value::js_nanbox_string(sval_str as i64); - crate::object::js_object_set_field_by_name(obj, key, sval); - } - { - let key = js_string_from_bytes(b"errno".as_ptr(), 5) as *const StringHeader; - crate::object::js_object_set_field_by_name(obj, key, errno); - } - err_val -} - // These FFI entries are referenced only from extension archives (linked after // the runtime's bitcode is optimized), so the auto-optimize LTO pass would // otherwise dead-strip them (see project_auto_optimize_keepalive_3320). The // `#[used]` anchors pin them. -#[used] -static KEEP_JS_NODE_SYSTEM_ERROR_VALUE: unsafe extern "C" fn( - *const u8, - usize, - *const u8, - usize, - *const u8, - usize, - f64, -) -> f64 = js_node_system_error_value; - #[used] static KEEP_JS_ERROR_VALUE_WITH_CODE: unsafe extern "C" fn( *const u8, @@ -979,6 +928,171 @@ fn throw_capture_stack_trace_target_type_error() -> ! { crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } +// ───────────────────────── V8 CallSite shim ───────────────────────── +// Packages like `depd` and `source-map-support` set `Error.prepareStackTrace` +// and then call CallSite METHODS (`getFileName`, `getLineNumber`, …) on the +// "structured stack trace" array V8 passes as the 2nd argument. Perry doesn't +// capture real JS frames yet, so these return spec-compatible placeholders +// (`getFileName()` may legitimately be `null`; line/column 0). The point is the +// API SHAPE — without callable CallSite methods, `obj.stack.slice(1)[i] +// .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 { + f64::from_bits(crate::value::TAG_UNDEFINED) +} +extern "C" fn callsite_null(_c: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(crate::value::TAG_NULL) +} +extern "C" fn callsite_zero(_c: *const crate::closure::ClosureHeader) -> f64 { + 0.0 +} +extern "C" fn callsite_false(_c: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(crate::value::TAG_FALSE) +} +extern "C" fn callsite_true(_c: *const crate::closure::ClosureHeader) -> f64 { + f64::from_bits(crate::value::TAG_TRUE) +} +extern "C" fn callsite_to_string(_c: *const crate::closure::ClosureHeader) -> f64 { + let s = js_string_from_bytes(b"".as_ptr(), 11); + crate::value::js_nanbox_string(s as i64) +} + +fn attach_call_site_method(obj: *mut crate::object::ObjectHeader, name: &str, fp: *const u8) { + crate::closure::js_register_closure_arity(fp, 0); + // Singleton (func_ptr-keyed, permanent) closure — never collected/moved, so + // the CallSite's method field stays valid across GC cycles. + let closure = crate::closure::js_closure_alloc_singleton(fp); + if closure.is_null() { + return; + } + let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = crate::value::js_nanbox_pointer(closure as i64); + crate::object::js_object_set_field_by_name(obj, key, value); +} + +fn make_call_site() -> f64 { + let obj = crate::object::js_object_alloc(0, 20); + let undef = callsite_undefined as *const u8; + let null = callsite_null as *const u8; + let zero = callsite_zero as *const u8; + let f = callsite_false as *const u8; + let t = callsite_true as *const u8; + attach_call_site_method(obj, "getFileName", undef); + attach_call_site_method(obj, "getScriptNameOrSourceURL", undef); + attach_call_site_method(obj, "getLineNumber", zero); + attach_call_site_method(obj, "getColumnNumber", zero); + attach_call_site_method(obj, "getEnclosingLineNumber", zero); + attach_call_site_method(obj, "getEnclosingColumnNumber", zero); + attach_call_site_method(obj, "getFunctionName", null); + attach_call_site_method(obj, "getMethodName", null); + attach_call_site_method(obj, "getTypeName", null); + attach_call_site_method(obj, "getFunction", undef); + attach_call_site_method(obj, "getThis", undef); + attach_call_site_method(obj, "getEvalOrigin", undef); + attach_call_site_method(obj, "getPosition", zero); + attach_call_site_method(obj, "isNative", f); + attach_call_site_method(obj, "isEval", f); + attach_call_site_method(obj, "isToplevel", t); + attach_call_site_method(obj, "isConstructor", f); + attach_call_site_method(obj, "isAsync", f); + attach_call_site_method(obj, "isPromiseAll", f); + attach_call_site_method(obj, "toString", callsite_to_string as *const u8); + crate::value::js_nanbox_pointer(obj as i64) +} + +/// Build a structured-stack array of `n` (clamped) CallSite objects. +fn build_structured_stack(n: usize) -> f64 { + let n = n.clamp(1, 64); + // All CallSites carry identical placeholder data, so share ONE object across + // every slot. Returned to the caller (GC-rooted like any native-call result). + let cs = make_call_site(); + let cs_jsv = crate::value::JSValue::from_bits(cs.to_bits()); + let mut arr = crate::array::js_array_alloc(n as u32); + for _ in 0..n { + arr = crate::array::js_array_push(arr, cs_jsv); + } + f64::from_bits(crate::value::JSValue::array_ptr(arr).bits()) +} + +/// If the global `Error.prepareStackTrace` has been overridden with a user +/// function (i.e. it is no longer Perry's default thunk), return its NaN-boxed +/// closure value; otherwise `None`. +fn error_prepare_stack_trace_override() -> Option { + let ctor = crate::object::ERROR_CONSTRUCTOR_PTR.with(|c| c.get()); + if ctor == 0 { + return None; + } + let key = js_string_from_bytes(b"prepareStackTrace".as_ptr(), 17); + let val = + crate::object::js_object_get_field_by_name(ctor as *const crate::object::ObjectHeader, key); + if !val.is_pointer() { + return None; + } + let val_f64 = f64::from_bits(val.bits()); + let ptr = crate::value::js_nanbox_get_pointer(val_f64) as usize; + if !crate::closure::is_closure_ptr(ptr) { + return None; + } + let fp = unsafe { (*(ptr as *const crate::closure::ClosureHeader)).func_ptr } as usize; + if fp == crate::object::default_prepare_stack_trace_func_ptr() { + return None; + } + Some(val_f64) +} + +/// Compute a `.stack` value honoring the current `Error.prepareStackTrace`. +/// With a user override, returns `prepareStackTrace(receiver, [CallSite, …])`; +/// otherwise the coarse string. Returning it (vs storing it) keeps the result +/// GC-rooted by the reading caller — a Rust-side store of the nursery CallSite +/// array gets reclaimed the moment this returns (V8 itself evaluates `.stack` +/// lazily for exactly this reason). +unsafe fn compute_stack_value(receiver: f64) -> f64 { + 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); + } + let s = make_stack("Error", ""); + crate::value::js_nanbox_string(s as i64) +} + +/// 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(); + unsafe { compute_stack_value(receiver) } +} + +/// Install the lazy `stack` getter accessor on `target`. +unsafe fn install_lazy_stack_accessor(target_ptr: *mut crate::object::ObjectHeader) { + let fp = error_stack_lazy_getter as *const u8; + crate::closure::js_register_closure_arity(fp, 0); + let closure = crate::closure::js_closure_alloc(fp, 0); + if closure.is_null() { + return; + } + let getter_bits = crate::value::js_nanbox_pointer(closure as i64).to_bits(); + let key = js_string_from_bytes(b"stack".as_ptr(), 5); + crate::object::ensure_key_in_keys_array(target_ptr, key); + crate::object::install_builtin_getter(target_ptr, "stack", getter_bits); + crate::object::set_accessor_descriptor( + target_ptr as usize, + "stack".to_string(), + crate::object::AccessorDescriptor { + get: getter_bits, + set: 0, + }, + ); + crate::object::set_property_attrs( + target_ptr as usize, + "stack".to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); +} + /// `Error.captureStackTrace(target[, constructorOpt])`. /// /// Perry's stack strings are intentionally coarse today; this helper installs @@ -998,15 +1112,13 @@ pub extern "C" fn js_error_capture_stack_trace(target: f64, _constructor_opt: f6 throw_capture_stack_trace_target_type_error(); } - let stack = make_stack("Error", ""); - let key = js_string_from_bytes(b"stack".as_ptr(), 5); - let value = crate::value::js_nanbox_string(stack as i64); - crate::object::js_object_set_field_by_name(target_ptr, key, value); - crate::object::set_property_attrs( - target_ptr as usize, - "stack".to_string(), - crate::object::PropertyAttrs::new(true, false, true), - ); + // Install a lazy `stack` getter (V8 semantics): on read it consults the + // current `Error.prepareStackTrace` and returns `prepareStackTrace(this, + // [CallSite, …])` — the contract `depd` / `source-map-support` rely on — + // or the coarse string when there is no override. A getter RETURNS the + // value to its caller (GC-rooted), unlike a stored nursery array which a + // minor GC would reclaim the instant `captureStackTrace` returns. + install_lazy_stack_accessor(target_ptr); } f64::from_bits(crate::value::TAG_UNDEFINED) diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 017f58a73b..ff40fe3f3c 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -1246,6 +1246,81 @@ extern "C" fn global_this_error_is_error_thunk( crate::error::js_error_is_error(value) } +/// `new Function(...)` with a RUNTIME-constructed body. Static/const bodies are +/// AOT-compiled in HIR; only dynamic ones reach here. Perry has no JS +/// interpreter, but it CAN recognize the fixed templates a few popular codegen +/// libraries emit and return a real native function. Currently: `depd`'s +/// deprecation wrapper (used eagerly by `send` → Next.js). depd's wrapper just +/// logs a deprecation then forwards to the wrapped fn, so the "wrapper" can +/// simply BE that fn — `new Function(...)(fn,log,deprecate,msg,site)` returns +/// `fn`. Unrecognized templates fall back to a non-callable placeholder object +/// (prior behavior); there is no general eval. +#[no_mangle] +pub extern "C" fn js_function_ctor_from_strings(args_ptr: *const f64, args_len: usize) -> f64 { + let arg_str = |i: usize| -> String { + if i >= args_len || args_ptr.is_null() { + return String::new(); + } + let v = unsafe { *args_ptr.add(i) }; + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + match crate::string::str_bytes_from_jsvalue(v, &mut scratch) { + Some((p, n)) if !p.is_null() => { + let bytes = unsafe { std::slice::from_raw_parts(p, n as usize) }; + std::str::from_utf8(bytes).unwrap_or("").to_string() + } + _ => String::new(), + } + }; + // depd `wrapfunction`: `new Function("fn","log","deprecate","message", + // "site", '…return function (…) { log.call(deprecate, message, site)\n + // return fn.apply(this, arguments)\n}')`. The outer, called with + // (fn,log,deprecate,message,site), returns that wrapper. Match the FULL + // shape — exactly six args, the five parameter names verbatim, AND the + // body substrings — so an unrelated dynamic Function body that happens to + // contain the substrings isn't misclassified as depd's wrapper. + if args_len == 6 + && arg_str(0) == "fn" + && arg_str(1) == "log" + && arg_str(2) == "deprecate" + && arg_str(3) == "message" + && arg_str(4) == "site" + { + let body = arg_str(5); + if body.contains("return function (") + && body.contains("log.call(deprecate, message, site)") + && body.contains("return fn.apply(this, arguments)") + { + let fp = depd_wrapfunction_outer_thunk as *const u8; + crate::closure::js_register_closure_arity(fp, 5); + let closure = crate::closure::js_closure_alloc_singleton(fp); + if !closure.is_null() { + return crate::value::js_nanbox_pointer(closure as i64); + } + } + } + let obj = crate::object::js_object_alloc(0, 0); + crate::value::js_nanbox_pointer(obj as i64) +} + +/// depd `wrapfunction` outer `(fn, log, deprecate, message, site) => wrapper`. +/// The wrapper forwards to `fn` (deprecation logging dropped — a non-essential +/// warning), so return `fn` itself: calling the "deprecated" function calls the +/// real one with identical `this`/arguments. +extern "C" fn depd_wrapfunction_outer_thunk( + _closure: *const crate::closure::ClosureHeader, + fn_v: f64, + _log: f64, + _deprecate: f64, + _message: f64, + _site: f64, +) -> f64 { + fn_v +} + +#[used] +static KEEP_JS_FUNCTION_CTOR_FROM_STRINGS: extern "C" fn(*const f64, usize) -> f64 = + js_function_ctor_from_strings; + /// #2904: `Error.prepareStackTrace` default — Node leaves a hook here that /// formats the stack from structured frames. Perry's stack strings are /// coarse; the installed default returns the existing `error.stack` string @@ -4945,10 +5020,29 @@ fn alias_number_static_to_global_function(singleton: *mut ObjectHeader, name: &s ); } +thread_local! { + /// Raw address of THIS thread's `Error` constructor closure, captured at + /// install. Read by `error::error_prepare_stack_trace_override` so + /// `captureStackTrace` / `error.stack` can honor a user-set + /// `Error.prepareStackTrace`. Thread-local, not a process-global: each + /// `perry/thread` agent has its own arena + realm, and an `Error` + /// constructor / `prepareStackTrace` from another thread's arena can be a + /// foreign or freed pointer — the same reason `globalThis` is per-thread. + pub(crate) static ERROR_CONSTRUCTOR_PTR: std::cell::Cell = + const { std::cell::Cell::new(0) }; +} + +/// The default `Error.prepareStackTrace` thunk's address — used to tell a +/// user override apart from Perry's built-in default. +pub(crate) fn default_prepare_stack_trace_func_ptr() -> usize { + global_this_error_prepare_stack_trace_thunk as *const u8 as usize +} + fn install_error_static_methods(ctor: *mut crate::closure::ClosureHeader) { if ctor.is_null() { return; } + ERROR_CONSTRUCTOR_PTR.with(|c| c.set(ctor as usize)); let func_ptr = global_this_error_capture_stack_trace_thunk as *const u8; let closure = crate::closure::js_closure_alloc(func_ptr, 0); if closure.is_null() { diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index cc1548fd7f..6035b01c19 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -40,6 +40,7 @@ mod field_get_set; mod field_set_by_name; mod global_fetch; mod global_this; +pub(crate) use global_this::{default_prepare_stack_trace_func_ptr, ERROR_CONSTRUCTOR_PTR}; mod global_this_tables; mod groupby; pub(crate) mod has_own_helpers; @@ -57,6 +58,7 @@ mod native_module_stream; mod native_this_alias; mod object_literal_ops; mod object_ops; +pub(crate) use object_ops::{ensure_key_in_keys_array, install_builtin_getter}; mod object_ops_frozen; mod polymorphic_index; mod primitive_proto_thunks; diff --git a/crates/perry-runtime/src/util_inherits.rs b/crates/perry-runtime/src/util_inherits.rs index 7e49c1b904..4b6188a351 100644 --- a/crates/perry-runtime/src/util_inherits.rs +++ b/crates/perry-runtime/src/util_inherits.rs @@ -73,19 +73,50 @@ fn ensure_function_prototype(value: f64) -> f64 { if current.to_bits() != crate::value::TAG_UNDEFINED { return current; } - if closure_ptr(value) == 0 { - return current; + let cptr = closure_ptr(value); + if cptr != 0 { + let class_id = crate::object::synthetic_class_id_for_function(value); + if class_id != 0 { + let proto = crate::object::ensure_function_prototype_object(value, class_id); + if !proto.is_null() { + return crate::value::js_nanbox_pointer(proto as i64); + } + } } - let class_id = crate::object::synthetic_class_id_for_function(value); - if class_id == 0 { + // A constructor with no `.prototype` used as a `util.inherits` base — most + // importantly Perry's native `require('stream')` Stream, which is a callable + // native-module object (NOT a CLOSURE_MAGIC closure) with `.Readable` etc. + // but no prototype. Node exposes a real `.prototype` on these so + // `util.inherits(MyStream, Stream)` can chain `MyStream.prototype.__proto__ + // = Stream.prototype`. Synthesize a plain object once and cache it on the + // value (closure dynamic-prop or object field) so later reads + the + // `Object.setPrototypeOf` below see it. + let proto = crate::object::js_object_alloc(0, 0); + if proto.is_null() { return current; } - let proto = crate::object::ensure_function_prototype_object(value, class_id); - if proto.is_null() { - current - } else { - crate::value::js_nanbox_pointer(proto as i64) + let proto_val = crate::value::js_nanbox_pointer(proto as i64); + if cptr != 0 { + crate::closure::closure_set_dynamic_prop(cptr, "prototype", proto_val); + return proto_val; + } + // Restrict the object-backed synthesis to CALLABLE native-module exports + // (the `require('stream')` Stream case — a native-module object that acts as + // a legacy constructor). A plain non-callable object must NOT gain a + // synthesized `.prototype` here: that would let it slip past + // `js_util_inherits`'s "superCtor.prototype must be of type object" + // validation instead of failing as Node does. (Closures already returned + // above via the `cptr != 0` path.) + let obj = object_ptr(value); + if !obj.is_null() + && crate::object::js_object_get_class_id(obj as *const crate::object::ObjectHeader) + == crate::object::NATIVE_MODULE_CLASS_ID + { + let key = named_key(b"prototype"); + crate::object::js_object_set_field_by_name(obj, key, proto_val); + return proto_val; } + current } fn set_super_property(ctor: f64, super_ctor: f64) {