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
21 changes: 21 additions & 0 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/runtime_decls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <callee>(...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`.
Expand Down
18 changes: 14 additions & 4 deletions crates/perry-codegen/src/type_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,18 @@ pub(crate) fn refine_type_from_init(ctx: &FnCtx<'_>, init: &Expr) -> Option<HirT
// dispatch in js_object_get_field_by_name_f64. Refining to
// String lets `const m = e.message; m.length` hit the
// string fast path instead of returning undefined.
if matches!(property.as_str(), "message" | "stack" | "name") {
let _ = object;
return Some(HirType::String);
// NOTE: `.stack` is deliberately excluded — `Error.prepareStackTrace`
// can make `.stack` an ARRAY of CallSites (depd / source-map-support),
// and a plain object may carry any `.stack` value. Typing it String
// unconditionally corrupted those array values on store (the array
// pointer got reinterpreted as a string). `.stack` stays `Any`.
if matches!(property.as_str(), "message" | "name") {
// A user class's DECLARED field type wins over the Error String assumption.
let declared = receiver_class_name(ctx, object).and_then(|c| {
let class = ctx.classes.get(&c)?;
class.fields.iter().find(|f| f.name == *property).map(|f| f.ty.clone())
});
return Some(declared.unwrap_or(HirType::String));
}
// obj.field where obj is a known class instance → field's
// declared type. Reuses the same walk static_type_of uses.
Expand Down Expand Up @@ -1332,7 +1341,8 @@ pub(crate) fn is_string_expr(ctx: &FnCtx<'_>, 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
Expand Down
23 changes: 23 additions & 0 deletions crates/perry-hir/src/lower/expr_member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).<m> 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) => {
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ pub(super) fn lower_nested_fn_decl(
body = new_body;
}

// Prepend defaulted-parameter application (`if (p === undefined) p =
// <default>`). 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(&params);
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
Expand Down
30 changes: 30 additions & 0 deletions crates/perry-runtime/src/builtins/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<key>` 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),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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)) {
Expand Down
13 changes: 7 additions & 6 deletions crates/perry-runtime/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
Loading
Loading