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
2 changes: 1 addition & 1 deletion crates/perry-codegen-js/src/emit/exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ impl JsEmitter {
}
self.output.push(')');
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
self.output.push_str("new (");
self.emit_expr(callee);
self.output.push_str(")(");
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen-wasm/src/emit/expr/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ impl<'a> FuncEmitCtx<'a> {
}
// If no compiled constructor, just leave the instance handle on stack
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
// Dynamic new — approximate with regular call via mem_call
self.emit_frame_begin(func, (args.len() + 1) as u32);
self.emit_store_arg(func, 0, callee);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen-wasm/src/emit/string_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,7 @@ impl WasmModuleEmitter {
self.collect_strings_in_expr(a);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
self.collect_strings_in_expr(callee);
for a in args {
self.collect_strings_in_expr(a);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/collectors/escape_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ pub fn check_escapes_in_expr(
check_escapes_in_expr(fi, candidates, classes, escaped);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
check_escapes_in_expr(callee, candidates, classes, escaped);
for a in args {
check_escapes_in_expr(a, candidates, classes, escaped);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/collectors/escape_news.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ fn collect_used_new_fields_in_expr(
collect_used_new_fields_in_expr(arg, non_escaping_news, used);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
collect_used_new_fields_in_expr(callee, non_escaping_news, used);
for arg in args {
collect_used_new_fields_in_expr(arg, non_escaping_news, used);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/collectors/refs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ pub fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet<u32>) {
// soft fallback (returns 0.0) and `js_new_function_construct`
// saw a NaN-zero callee, allocating a class_id=0 empty object
// with no prototype-method dispatch.
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
walk(callee, out);
for a in args {
walk(a, out);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/collectors/this_as_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ pub fn expr_uses_this_as_value(e: &perry_hir::Expr, fields: &HashSet<String>) ->
.iter()
.any(|(_, e)| expr_uses_this_as_value(e, fields)),
Expr::New { args, .. } => args.iter().any(|a| expr_uses_this_as_value(a, fields)),
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
expr_uses_this_as_value(callee, fields)
|| args.iter().any(|a| expr_uses_this_as_value(a, fields))
}
Expand Down
44 changes: 40 additions & 4 deletions crates/perry-codegen/src/expr/new_dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,32 @@ fn new_callee_is_primitive_literal(callee: &Expr) -> bool {
pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
match expr {
Expr::New {
class_name, args, ..
} => lower_new(ctx, class_name, args),
class_name,
args,
byte_offset,
..
} => {
// #5253: under `--debug-symbols`, attach this `new`'s source
// `file:line` so a "X is not a constructor" throw from `lower_new`'s
// runtime-construct fallback (or a built-in non-constructor) renders
// a location. No-op for resolved user classes (no throw fires).
crate::expr::calls::emit_call_location_at(ctx, *byte_offset);
lower_new(ctx, class_name, args)
}

// `new <callee>(...spread)` — spread-bearing construction. Fold every
// argument (regular pushed, spread sources expanded via
// `js_array_like_to_array` + concat) into a single JS array in
// evaluation order, then dispatch through `js_new_function_construct_apply`
// which materialises a flat buffer and reuses the full callee-shape
// dispatch of the non-spread `js_new_function_construct`.
Expr::NewDynamicSpread { callee, args } => {
Expr::NewDynamicSpread {
callee,
args,
byte_offset,
} => {
use perry_hir::CallArg;
let new_byte_offset = *byte_offset;
let func_double = lower_expr(ctx, callee)?;
let mut acc_handle = ctx.block().call(I64, "js_array_alloc", &[(I32, "0")]);
for a in args {
Expand All @@ -104,6 +119,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}
}
let args_box = nanbox_pointer_inline(ctx.block(), &acc_handle);
// #5253: locate the not-a-constructor throw the apply path can raise.
crate::expr::calls::emit_call_location_at(ctx, new_byte_offset);
let result = ctx.block().call(
DOUBLE,
"js_new_function_construct_apply",
Expand Down Expand Up @@ -155,7 +172,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// inspects the callee's NaN tag and dispatches to the right
// class constructor. That's a separate followup tracked in
// the v0.5.8 changelog.
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic {
callee,
args,
byte_offset,
} => {
// #5253: source location of this `new` for the not-a-constructor
// throws below. `const X: any = undefined; new X()` lowers here
// (callee `LocalGet`), so this is what localizes ajv's
// `undefined is not a constructor`.
let new_byte_offset = *byte_offset;
// `new <primitive-literal>(…)` is always a `TypeError` — a primitive
// is never a constructor (`new 1`, `new 1.5`, `new true`, `new null`,
// `new undefined`, `new "s"`). Number literals lower to a plain `f64`
Expand All @@ -168,6 +194,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
for a in args {
let _ = lower_expr(ctx, a)?;
}
crate::expr::calls::emit_call_location_at(ctx, new_byte_offset);
return Ok(ctx.block().call(DOUBLE, "js_throw_not_a_constructor", &[]));
}

Expand Down Expand Up @@ -499,10 +526,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let then_synth = Expr::NewDynamic {
callee: then_expr.clone(),
args: args.clone(),
byte_offset: new_byte_offset,
};
let else_synth = Expr::NewDynamic {
callee: else_expr.clone(),
args: args.clone(),
byte_offset: new_byte_offset,
};
return lower_conditional(ctx, condition, &then_synth, &else_synth);
}
Expand Down Expand Up @@ -570,6 +599,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
.map(|a| lower_expr(ctx, a))
.collect::<Result<Vec<_>>>()?;
let (args_ptr, args_len) = lower_js_args_array(ctx, &lowered_args);
// #5253: locate a not-a-constructor throw from the runtime
// construct path (a `LocalGet` callee holding `undefined`, a
// non-callable value, etc.).
crate::expr::calls::emit_call_location_at(ctx, new_byte_offset);
let result = ctx.block().call(
DOUBLE,
"js_new_function_construct",
Expand All @@ -593,6 +626,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
.map(|a| lower_expr(ctx, a))
.collect::<Result<Vec<_>>>()?;
let (args_ptr, args_len) = lower_js_args_array(ctx, &lowered_args);
// #5253: locate the not-a-constructor throw for `new <primitive>` /
// `new <non-constructor-value>` rejected inside the runtime helper.
crate::expr::calls::emit_call_location_at(ctx, new_byte_offset);
let result = ctx.block().call(
DOUBLE,
"js_new_function_construct",
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-codegen/src/lower_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,23 @@ pub(crate) use native_table::iter_native_module_table;
/// 2. `console.log(expr)` where `expr` lowers to a double — emits a
/// `js_console_log_number` call and returns `0.0` as the statement value.
pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> Result<String> {
// #5253: localize the `ReferenceError: X is not defined` thrown by the
// unresolved-identifier runtime helper. A bare unresolved identifier
// (`module`, winston's stray globals, …) lowers to
// `Call { callee: ExternFuncRef("js_global_get_or_throw_unresolved"),
// args: [name], byte_offset }` (perry-hir lower_expr). That helper throws a
// ReferenceError through `js_referenceerror_new` → `make_stack`, which reads
// `CURRENT_CALL_LOCATION`. Emit a `js_set_call_location` from the call's
// recorded byte offset (set on `pending_call_offset` by the `Expr::Call`
// dispatcher) before lowering the call, so the throw carries `at file:line`.
// No-op in the default build; offset 0 (synthesized refs) resolves to none.
if let Expr::ExternFuncRef { name, .. } = callee {
if name == "js_global_get_or_throw_unresolved" {
let off = ctx.strings.pending_call_offset();
crate::expr::calls::emit_call_location_at(ctx, off);
}
}

// #3656: `p.call(thisArg, …)` / `p.apply(thisArg, argsArray)` on a Proxy
// routes through the proxy's `[[Call]]` (apply trap) rather than reading
// `.call`/`.apply` off the forwarded target.
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/tests/constructor_recursion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ fn module_with_recursive_constructor_return() -> Module {
class_name: "RecursiveCtor".to_string(),
args: vec![Expr::Bool(false), Expr::LocalGet(11)],
type_args: Vec::new(),
byte_offset: 0,
}))],
else_branch: None,
}],
Expand Down Expand Up @@ -125,6 +126,7 @@ fn module_with_recursive_constructor_return() -> Module {
class_name: "RecursiveCtor".to_string(),
args: vec![Expr::Bool(true), Expr::Undefined],
type_args: Vec::new(),
byte_offset: 0,
})],
exported_native_instances: Vec::new(),
exported_func_return_native_instances: Vec::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/tests/typed_shape_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ fn module_with_new(class: Class) -> Module {
class_name,
args: Vec::new(),
type_args: Vec::new(),
byte_offset: 0,
}))],
is_async: false,
is_generator: false,
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-hir/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,7 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec<Local
}
}
// Dynamic new expression
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
collect_assigned_locals_expr(callee, assigned);
for arg in args {
collect_assigned_locals_expr(arg, assigned);
Expand Down Expand Up @@ -2053,7 +2053,7 @@ fn replace_this_in_expr(expr: &mut Expr, this_id: LocalId) {
replace_this_in_expr(a, this_id);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
replace_this_in_expr(callee, this_id);
for a in args {
replace_this_in_expr(a, this_id);
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-hir/src/ir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ pub enum Expr {
args: Vec<Expr>,
/// Explicit type arguments (e.g., new Box<number>(42))
type_args: Vec<Type>,
/// #5253: byte offset (`new_expr.span.lo.0`) of this `new` expression
/// in its module's source, captured at AST→HIR lowering. Used by
/// codegen (under `--debug-symbols`) to attach a `file:line` to the
/// runtime "X is not a constructor" TypeError. `0` when unknown
/// (synthesized `new` from transforms/intrinsics) — resolves to no
/// location, falling back to `<anonymous>`. Mirrors `Call.byte_offset`
/// (#5247) and is excluded from stable-hashing.
byte_offset: u32,
},

/// Dynamic new expression (new with non-identifier callee)
Expand All @@ -314,6 +322,11 @@ pub enum Expr {
callee: Box<Expr>,
/// Arguments to pass to the constructor
args: Vec<Expr>,
/// #5253: source byte offset of the `new` expression — see
/// `New::byte_offset`. The `const X: any = undefined; new X()`
/// not-a-constructor case lowers here (callee is `LocalGet`), so this
/// is the field that localizes ajv's `undefined is not a constructor`.
byte_offset: u32,
},

/// Dynamic `new` with spread arguments — `new <callee>(...args)`.
Expand All @@ -325,6 +338,9 @@ pub enum Expr {
NewDynamicSpread {
callee: Box<Expr>,
args: Vec<CallArg>,
/// #5253: source byte offset of the `new` expression — see
/// `New::byte_offset`.
byte_offset: u32,
},

/// Runtime `new.target` value for ordinary functions.
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/js_transform/cross_module_natives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ pub fn fix_native_instance_expr(
fix_native_instance_expr(arg, native_instances, local_id_instances);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
fix_native_instance_expr(callee, native_instances, local_id_instances);
for arg in args {
fix_native_instance_expr(arg, native_instances, local_id_instances);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/js_transform/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ pub fn transform_expr(
}

// Dynamic new expressions - may be for JS classes (e.g., new ObjectId(str))
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
// Transform the callee first
transform_expr(callee, js_imports, extern_func_to_js, local_name_to_js, tracker);

Expand Down
6 changes: 3 additions & 3 deletions crates/perry-hir/src/lower/closure_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ fn widen_mutable_captures_expr(
widen_mutable_captures_expr(arg, scope_mutable);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
widen_mutable_captures_expr(callee, scope_mutable);
for arg in args {
widen_mutable_captures_expr(arg, scope_mutable);
Expand Down Expand Up @@ -554,7 +554,7 @@ fn collect_closure_assigned_expr(expr: &Expr, out: &mut std::collections::HashSe
collect_closure_assigned_expr(arg, out);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
collect_closure_assigned_expr(callee, out);
for arg in args {
collect_closure_assigned_expr(arg, out);
Expand Down Expand Up @@ -1281,7 +1281,7 @@ fn collect_closure_assigned_in_body_expr(
collect_closure_assigned_in_body_expr(arg, out);
}
}
Expr::NewDynamic { callee, args } => {
Expr::NewDynamic { callee, args, .. } => {
collect_closure_assigned_in_body_expr(callee, out);
for arg in args {
collect_closure_assigned_in_body_expr(arg, out);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/lower/expr_call/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub(super) fn try_global_builtins(
class_name: "Array".to_string(),
args,
type_args: Vec::new(),
byte_offset: 0,
}));
}
"isNaN" => {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-hir/src/lower/expr_call/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,7 @@ pub(super) fn try_native_module_methods(
class_name: cls_name,
args: new_args,
type_args: vec![],
byte_offset: 0,
}));
}
}
Expand Down
Loading
Loading