From de7c1a80fe9a9f24ec10ea8ec10d6e78c264a3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 16 Jun 2026 18:58:41 +0200 Subject: [PATCH 1/2] feat(error): source location on construct + ReferenceError throws (#5253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #5247/#5250. Extends the `--debug-symbols` runtime source-location pipeline (which #5250 gave the dynamic call-dispatch "X is not a function" throw) to two more throw classes: 1. `new X()` "X is not a constructor" — adds `byte_offset` to `Expr::New`, `Expr::NewDynamic`, and `Expr::NewDynamicSpread`, set from the AST `new_expr.span.lo.0` at lowering. Codegen calls `emit_call_location_at` before the `js_new_function_construct{,_apply}` / `js_throw_not_a_constructor` dispatch. `const X: any = undefined; new X()` lowers to `NewDynamic` (callee `LocalGet`), so the offset lands on that variant — localizing ajv's `undefined is not a constructor`. 2. `ReferenceError: X is not defined` — the bare unresolved-identifier read already lowers to a `Call` into `js_global_get_or_throw_unresolved`; that Call now carries `ident.span.lo.0` as its `byte_offset`, and `lower_call` emits `js_set_call_location` before the helper call. Localizes winston's `module is not defined`. The runtime needs no change: both throw paths already route through `alloc_error` → `make_stack`, which reads `CURRENT_CALL_LOCATION`. Default behavior is unchanged (no debug-location ctx installed → no-op; offset 0 → ``). `byte_offset` is excluded from the stable hash (like `Call.byte_offset`) so whitespace edits don't bust the object cache. All other New construction sites (transforms, intrinsics, synthesized news) pass `byte_offset: 0`. Extracted `module_constructor_name` / `global_member_constructor_name` from `expr_new.rs` into a new `expr_new_builtins.rs` to keep the file under the 2000-line cap after the field additions. Tests: new `issue_5253_construct_reference_source_location.rs` (4 cases: not-a-constructor + ReferenceError, each WITH/WITHOUT `--debug-symbols`). #5247/#5250 tests stay green. No version/CHANGELOG/Cargo.lock edits (maintainer folds in at merge). --- crates/perry-codegen-js/src/emit/exprs.rs | 2 +- .../src/emit/expr/classes.rs | 2 +- .../src/emit/string_collection.rs | 2 +- .../src/collectors/escape_check.rs | 2 +- .../src/collectors/escape_news.rs | 2 +- crates/perry-codegen/src/collectors/refs.rs | 2 +- .../src/collectors/this_as_value.rs | 2 +- crates/perry-codegen/src/expr/new_dynamic.rs | 44 +++- crates/perry-codegen/src/lower_call/mod.rs | 17 ++ crates/perry-hir/src/analysis.rs | 4 +- crates/perry-hir/src/ir/expr.rs | 16 ++ .../src/js_transform/cross_module_natives.rs | 2 +- crates/perry-hir/src/js_transform/imports.rs | 2 +- .../perry-hir/src/lower/closure_analysis.rs | 6 +- .../perry-hir/src/lower/expr_call/globals.rs | 1 + .../src/lower/expr_call/native_module.rs | 1 + crates/perry-hir/src/lower/expr_new.rs | 86 +++----- .../perry-hir/src/lower/expr_new_builtins.rs | 68 +++++++ crates/perry-hir/src/lower/expr_object.rs | 1 + crates/perry-hir/src/lower/lower_expr.rs | 4 +- crates/perry-hir/src/lower/mod.rs | 1 + crates/perry-hir/src/lower_decl/block.rs | 1 + crates/perry-hir/src/monomorph/driver.rs | 1 + .../src/monomorph/substitute_expr.rs | 2 + .../src/monomorph/update_call_sites.rs | 1 + crates/perry-hir/src/stable_hash/expr.rs | 8 +- crates/perry-hir/src/walker/expr_mut.rs | 4 +- crates/perry-hir/src/walker/expr_ref.rs | 4 +- ...253_construct_reference_source_location.rs | 188 ++++++++++++++++++ 29 files changed, 394 insertions(+), 82 deletions(-) create mode 100644 crates/perry-hir/src/lower/expr_new_builtins.rs create mode 100644 crates/perry/tests/issue_5253_construct_reference_source_location.rs diff --git a/crates/perry-codegen-js/src/emit/exprs.rs b/crates/perry-codegen-js/src/emit/exprs.rs index 92ecfc710d..2e2b52d791 100644 --- a/crates/perry-codegen-js/src/emit/exprs.rs +++ b/crates/perry-codegen-js/src/emit/exprs.rs @@ -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(")("); diff --git a/crates/perry-codegen-wasm/src/emit/expr/classes.rs b/crates/perry-codegen-wasm/src/emit/expr/classes.rs index 90c8d472e0..09bc41b41a 100644 --- a/crates/perry-codegen-wasm/src/emit/expr/classes.rs +++ b/crates/perry-codegen-wasm/src/emit/expr/classes.rs @@ -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); diff --git a/crates/perry-codegen-wasm/src/emit/string_collection.rs b/crates/perry-codegen-wasm/src/emit/string_collection.rs index c4f8c56e4a..d81bd31559 100644 --- a/crates/perry-codegen-wasm/src/emit/string_collection.rs +++ b/crates/perry-codegen-wasm/src/emit/string_collection.rs @@ -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); diff --git a/crates/perry-codegen/src/collectors/escape_check.rs b/crates/perry-codegen/src/collectors/escape_check.rs index de42a979f4..f5c559655c 100644 --- a/crates/perry-codegen/src/collectors/escape_check.rs +++ b/crates/perry-codegen/src/collectors/escape_check.rs @@ -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); diff --git a/crates/perry-codegen/src/collectors/escape_news.rs b/crates/perry-codegen/src/collectors/escape_news.rs index 85c6da3942..8d2c3fdf46 100644 --- a/crates/perry-codegen/src/collectors/escape_news.rs +++ b/crates/perry-codegen/src/collectors/escape_news.rs @@ -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); diff --git a/crates/perry-codegen/src/collectors/refs.rs b/crates/perry-codegen/src/collectors/refs.rs index ca6b7f6a1e..023e4ed412 100644 --- a/crates/perry-codegen/src/collectors/refs.rs +++ b/crates/perry-codegen/src/collectors/refs.rs @@ -473,7 +473,7 @@ pub fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet) { // 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); diff --git a/crates/perry-codegen/src/collectors/this_as_value.rs b/crates/perry-codegen/src/collectors/this_as_value.rs index 1c083e738d..306e6d1228 100644 --- a/crates/perry-codegen/src/collectors/this_as_value.rs +++ b/crates/perry-codegen/src/collectors/this_as_value.rs @@ -359,7 +359,7 @@ pub fn expr_uses_this_as_value(e: &perry_hir::Expr, fields: &HashSet) -> .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)) } diff --git a/crates/perry-codegen/src/expr/new_dynamic.rs b/crates/perry-codegen/src/expr/new_dynamic.rs index d84245d244..ea0789d1af 100644 --- a/crates/perry-codegen/src/expr/new_dynamic.rs +++ b/crates/perry-codegen/src/expr/new_dynamic.rs @@ -67,8 +67,18 @@ fn new_callee_is_primitive_literal(callee: &Expr) -> bool { pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { 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 (...spread)` — spread-bearing construction. Fold every // argument (regular pushed, spread sources expanded via @@ -76,8 +86,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // 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 { @@ -104,6 +119,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } } 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", @@ -155,7 +172,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // 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 (…)` 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` @@ -168,6 +194,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { 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", &[])); } @@ -499,10 +526,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { 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); } @@ -570,6 +599,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .map(|a| lower_expr(ctx, a)) .collect::>>()?; 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", @@ -593,6 +626,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .map(|a| lower_expr(ctx, a)) .collect::>>()?; let (args_ptr, args_len) = lower_js_args_array(ctx, &lowered_args); + // #5253: locate the not-a-constructor throw for `new ` / + // `new ` 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", diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index d1253004c9..1bff31bf98 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -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 { + // #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. diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index 7d5164f6cd..4e53c495b0 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -1406,7 +1406,7 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec { + Expr::NewDynamic { callee, args, .. } => { collect_assigned_locals_expr(callee, assigned); for arg in args { collect_assigned_locals_expr(arg, assigned); @@ -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); diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index cff4e9af91..6416c267e9 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -304,6 +304,14 @@ pub enum Expr { args: Vec, /// Explicit type arguments (e.g., new Box(42)) type_args: Vec, + /// #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 ``. Mirrors `Call.byte_offset` + /// (#5247) and is excluded from stable-hashing. + byte_offset: u32, }, /// Dynamic new expression (new with non-identifier callee) @@ -314,6 +322,11 @@ pub enum Expr { callee: Box, /// Arguments to pass to the constructor args: Vec, + /// #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 (...args)`. @@ -325,6 +338,9 @@ pub enum Expr { NewDynamicSpread { callee: Box, args: Vec, + /// #5253: source byte offset of the `new` expression — see + /// `New::byte_offset`. + byte_offset: u32, }, /// Runtime `new.target` value for ordinary functions. diff --git a/crates/perry-hir/src/js_transform/cross_module_natives.rs b/crates/perry-hir/src/js_transform/cross_module_natives.rs index 7c6ebe21e7..fd36bb4ae7 100644 --- a/crates/perry-hir/src/js_transform/cross_module_natives.rs +++ b/crates/perry-hir/src/js_transform/cross_module_natives.rs @@ -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); diff --git a/crates/perry-hir/src/js_transform/imports.rs b/crates/perry-hir/src/js_transform/imports.rs index e44495926b..dc40ff5dec 100644 --- a/crates/perry-hir/src/js_transform/imports.rs +++ b/crates/perry-hir/src/js_transform/imports.rs @@ -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); diff --git a/crates/perry-hir/src/lower/closure_analysis.rs b/crates/perry-hir/src/lower/closure_analysis.rs index 8b71076492..e436d00d62 100644 --- a/crates/perry-hir/src/lower/closure_analysis.rs +++ b/crates/perry-hir/src/lower/closure_analysis.rs @@ -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); @@ -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); @@ -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); diff --git a/crates/perry-hir/src/lower/expr_call/globals.rs b/crates/perry-hir/src/lower/expr_call/globals.rs index 47798c1f37..9b941ba9e3 100644 --- a/crates/perry-hir/src/lower/expr_call/globals.rs +++ b/crates/perry-hir/src/lower/expr_call/globals.rs @@ -137,6 +137,7 @@ pub(super) fn try_global_builtins( class_name: "Array".to_string(), args, type_args: Vec::new(), + byte_offset: 0, })); } "isNaN" => { diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index aa972309c0..b3562b6a03 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -1172,6 +1172,7 @@ pub(super) fn try_native_module_methods( class_name: cls_name, args: new_args, type_args: vec![], + byte_offset: 0, })); } } diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 14e66e2756..40fd3e981b 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -17,6 +17,7 @@ use crate::ir::Expr; use crate::lower_decl::lower_class_from_ast; use crate::lower_types::extract_ts_type_with_ctx; +use super::expr_new_builtins::{global_member_constructor_name, module_constructor_name}; use super::{lower_expr, LoweringContext}; /// Lower `new TextDecoder(label?, { fatal?, ignoreBOM? })` into @@ -214,61 +215,6 @@ fn is_url_encoding_constructor_name(name: &str) -> bool { ) } -fn module_constructor_name(module_name: &str, method_name: Option<&str>) -> Option<&'static str> { - match (module_name, method_name) { - ("events", Some("EventEmitterAsyncResource")) => Some("EventEmitterAsyncResource"), - ("url", Some("URL")) => Some("URL"), - ("url", Some("URLSearchParams")) => Some("URLSearchParams"), - ("url", Some("URLPattern")) => Some("URLPattern"), - ("util", Some("TextEncoder")) => Some("TextEncoder"), - ("util", Some("TextDecoder")) => Some("TextDecoder"), - ("stream/web", Some("TextEncoderStream")) - | ("node:stream/web", Some("TextEncoderStream")) => Some("TextEncoderStream"), - ("stream/web", Some("TextDecoderStream")) - | ("node:stream/web", Some("TextDecoderStream")) => Some("TextDecoderStream"), - ("stream/web", Some("CompressionStream")) - | ("node:stream/web", Some("CompressionStream")) => Some("CompressionStream"), - ("stream/web", Some("DecompressionStream")) - | ("node:stream/web", Some("DecompressionStream")) => Some("DecompressionStream"), - _ => None, - } -} - -fn global_member_constructor_name( - ctx: &LoweringContext, - obj_name: &str, - prop_name: &str, -) -> Option<&'static str> { - if obj_name == "globalThis" && ctx.lookup_local("globalThis").is_none() { - return match prop_name { - "URL" => Some("URL"), - "URLSearchParams" => Some("URLSearchParams"), - "URLPattern" => Some("URLPattern"), - "TextEncoder" => Some("TextEncoder"), - "TextDecoder" => Some("TextDecoder"), - "MessageChannel" => Some("MessageChannel"), - "BroadcastChannel" => Some("BroadcastChannel"), - "TextEncoderStream" => Some("TextEncoderStream"), - "TextDecoderStream" => Some("TextDecoderStream"), - "CompressionStream" => Some("CompressionStream"), - "DecompressionStream" => Some("DecompressionStream"), - _ => None, - }; - } - - if let Some(module_name) = ctx.lookup_builtin_module_alias(obj_name) { - if let Some(name) = module_constructor_name(module_name, Some(prop_name)) { - return Some(name); - } - } - if let Some((module_name, None)) = ctx.lookup_native_module(obj_name) { - if let Some(name) = module_constructor_name(module_name, Some(prop_name)) { - return Some(name); - } - } - None -} - fn is_worker_messaging_constructor_name(name: &str) -> bool { matches!(name, "MessageChannel" | "BroadcastChannel") } @@ -332,6 +278,12 @@ fn is_global_object_expr(ctx: &LoweringContext, expr: &Expr) -> bool { pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> Result { let callee_expr = peel_new_callee(new_expr.callee.as_ref()); + // #5253: source byte offset of this `new` expression, captured once and + // threaded into every `New`/`NewDynamic`/`NewDynamicSpread` we build below. + // Under `--debug-symbols`, codegen resolves it to a `file:line` for the + // runtime "X is not a constructor" TypeError. Mirrors `Call.byte_offset` + // (#5247). `_byte_offset` because not every arm below builds a New variant. + let new_byte_offset = new_expr.span.lo.0; // `new (...args)` — spread arguments. Every per-constructor branch // below collapses spreads into a plain array argument (they map over @@ -352,6 +304,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R return Ok(Expr::NewDynamicSpread { callee: Box::new(callee), args, + byte_offset: new_byte_offset, }); } } @@ -407,6 +360,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: "EventEmitter".to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } } @@ -445,6 +399,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: class_name.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } if let Some(expr) = @@ -462,6 +417,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: prop_ident.sym.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } @@ -553,6 +509,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R property: prop_ident.sym.to_string(), }), args, + byte_offset: new_byte_offset, }); } let is_url_module = @@ -789,6 +746,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: prop_ident.sym.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) { @@ -814,6 +772,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: class_name.to_string(), args, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } } @@ -993,6 +952,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: class_name.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } @@ -1031,6 +991,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R property: method_name, }), args, + byte_offset: new_byte_offset, }); } @@ -1058,6 +1019,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R property: export, }), args, + byte_offset: new_byte_offset, }); } @@ -1095,6 +1057,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R property: "GCProfiler".to_string(), }), args, + byte_offset: new_byte_offset, }); } @@ -1803,6 +1766,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: resolved, args, type_args, + byte_offset: new_byte_offset, }); } } @@ -1828,6 +1792,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R return Ok(Expr::NewDynamic { callee: Box::new(Expr::LocalGet(local_id)), args, + byte_offset: new_byte_offset, }); } // ES5 function constructors: `function Foo(){ this.x = … }` @@ -1847,6 +1812,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R return Ok(Expr::NewDynamic { callee: Box::new(Expr::FuncRef(func_id)), args, + byte_offset: new_byte_offset, }); } // #4698: `new ()` where the binding is a @@ -1894,6 +1860,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name, args, type_args, + byte_offset: new_byte_offset, }) } // Non-identifier callee (e.g., new (condition ? A : B)() or new someVar()) @@ -1935,6 +1902,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: synthetic_name, args, type_args, + byte_offset: new_byte_offset, }); } @@ -1974,6 +1942,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: property.clone(), args, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } if matches!(object.as_ref(), Expr::NativeModuleRef(module) @@ -1985,10 +1954,15 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R class_name: property.clone(), args, type_args: Vec::new(), + byte_offset: new_byte_offset, }); } } - Ok(Expr::NewDynamic { callee, args }) + Ok(Expr::NewDynamic { + callee, + args, + byte_offset: new_byte_offset, + }) } } } diff --git a/crates/perry-hir/src/lower/expr_new_builtins.rs b/crates/perry-hir/src/lower/expr_new_builtins.rs new file mode 100644 index 0000000000..66c54488eb --- /dev/null +++ b/crates/perry-hir/src/lower/expr_new_builtins.rs @@ -0,0 +1,68 @@ +//! Built-in / native-module constructor-name resolution helpers for `new` +//! lowering. Extracted from `expr_new.rs` to keep that file under the +//! 2000-line cap (#5253). Pure mechanical move — no behavior change; the two +//! functions are leaf lookups consulted by `expr_new::lower_new`. + +use super::LoweringContext; + +/// Map a `(module, export)` pair to the canonical built-in constructor name +/// `lower_new` uses (URL/TextEncoder/stream-web wrappers, EventEmitter*). +pub(super) fn module_constructor_name( + module_name: &str, + method_name: Option<&str>, +) -> Option<&'static str> { + match (module_name, method_name) { + ("events", Some("EventEmitterAsyncResource")) => Some("EventEmitterAsyncResource"), + ("url", Some("URL")) => Some("URL"), + ("url", Some("URLSearchParams")) => Some("URLSearchParams"), + ("url", Some("URLPattern")) => Some("URLPattern"), + ("util", Some("TextEncoder")) => Some("TextEncoder"), + ("util", Some("TextDecoder")) => Some("TextDecoder"), + ("stream/web", Some("TextEncoderStream")) + | ("node:stream/web", Some("TextEncoderStream")) => Some("TextEncoderStream"), + ("stream/web", Some("TextDecoderStream")) + | ("node:stream/web", Some("TextDecoderStream")) => Some("TextDecoderStream"), + ("stream/web", Some("CompressionStream")) + | ("node:stream/web", Some("CompressionStream")) => Some("CompressionStream"), + ("stream/web", Some("DecompressionStream")) + | ("node:stream/web", Some("DecompressionStream")) => Some("DecompressionStream"), + _ => None, + } +} + +/// Resolve `new .()` against the global object or a built-in / +/// native module alias to a canonical built-in constructor name. +pub(super) fn global_member_constructor_name( + ctx: &LoweringContext, + obj_name: &str, + prop_name: &str, +) -> Option<&'static str> { + if obj_name == "globalThis" && ctx.lookup_local("globalThis").is_none() { + return match prop_name { + "URL" => Some("URL"), + "URLSearchParams" => Some("URLSearchParams"), + "URLPattern" => Some("URLPattern"), + "TextEncoder" => Some("TextEncoder"), + "TextDecoder" => Some("TextDecoder"), + "MessageChannel" => Some("MessageChannel"), + "BroadcastChannel" => Some("BroadcastChannel"), + "TextEncoderStream" => Some("TextEncoderStream"), + "TextDecoderStream" => Some("TextDecoderStream"), + "CompressionStream" => Some("CompressionStream"), + "DecompressionStream" => Some("DecompressionStream"), + _ => None, + }; + } + + if let Some(module_name) = ctx.lookup_builtin_module_alias(obj_name) { + if let Some(name) = module_constructor_name(module_name, Some(prop_name)) { + return Some(name); + } + } + if let Some((module_name, None)) = ctx.lookup_native_module(obj_name) { + if let Some(name) = module_constructor_name(module_name, Some(prop_name)) { + return Some(name); + } + } + None +} diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index d7129e9dc8..a41f81318e 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -698,6 +698,7 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R class_name, args, type_args: Vec::new(), + byte_offset: 0, }); } } diff --git a/crates/perry-hir/src/lower/lower_expr.rs b/crates/perry-hir/src/lower/lower_expr.rs index 5ae2de1ffe..89da302994 100644 --- a/crates/perry-hir/src/lower/lower_expr.rs +++ b/crates/perry-hir/src/lower/lower_expr.rs @@ -641,7 +641,9 @@ fn lower_expr_impl(ctx: &mut LoweringContext, expr: &ast::Expr) -> Result }), args: vec![Expr::String(name.clone())], type_args: Vec::new(), - byte_offset: 0, + // #5253: localize the `X is not defined` ReferenceError to + // this identifier's source position (winston `module`). + byte_offset: ident.span.lo.0, }); } if !known_global { diff --git a/crates/perry-hir/src/lower/mod.rs b/crates/perry-hir/src/lower/mod.rs index ed92009467..4f6d28f6e1 100644 --- a/crates/perry-hir/src/lower/mod.rs +++ b/crates/perry-hir/src/lower/mod.rs @@ -43,6 +43,7 @@ mod expr_member; pub(crate) use expr_member::{wrap_private_guard, PRIV_OP_READ, PRIV_OP_WRITE}; mod expr_misc; mod expr_new; +mod expr_new_builtins; mod expr_object; mod unimpl_hints; pub(crate) use context::*; diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index f958ee77ac..03785839e1 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -1188,6 +1188,7 @@ pub fn lower_stmts_using_aware( ), ], type_args: Vec::new(), + byte_offset: 0, }), ))], else_branch: Some(vec![ diff --git a/crates/perry-hir/src/monomorph/driver.rs b/crates/perry-hir/src/monomorph/driver.rs index 36e8fda847..e27f9edd3d 100644 --- a/crates/perry-hir/src/monomorph/driver.rs +++ b/crates/perry-hir/src/monomorph/driver.rs @@ -252,6 +252,7 @@ fn collect_instantiations_in_expr( class_name, args, type_args, + .. } => { for arg in args { collect_instantiations_in_expr(arg, ctx, module, idx); diff --git a/crates/perry-hir/src/monomorph/substitute_expr.rs b/crates/perry-hir/src/monomorph/substitute_expr.rs index fea3abf25f..a5d6940a38 100644 --- a/crates/perry-hir/src/monomorph/substitute_expr.rs +++ b/crates/perry-hir/src/monomorph/substitute_expr.rs @@ -259,6 +259,7 @@ pub(crate) fn substitute_expr(expr: &Expr, substitutions: &HashMap class_name, args, type_args, + byte_offset, } => Expr::New { class_name: class_name.clone(), args: args @@ -269,6 +270,7 @@ pub(crate) fn substitute_expr(expr: &Expr, substitutions: &HashMap .iter() .map(|t| substitute_type(t, substitutions)) .collect(), + byte_offset: *byte_offset, }, // Class/Enum references diff --git a/crates/perry-hir/src/monomorph/update_call_sites.rs b/crates/perry-hir/src/monomorph/update_call_sites.rs index 3f0bb7f9f1..d5a48a7106 100644 --- a/crates/perry-hir/src/monomorph/update_call_sites.rs +++ b/crates/perry-hir/src/monomorph/update_call_sites.rs @@ -176,6 +176,7 @@ fn update_call_sites_in_expr( class_name, args, type_args, + .. } => { for arg in args.iter_mut() { update_call_sites_in_expr(arg, ctx, lookup); diff --git a/crates/perry-hir/src/stable_hash/expr.rs b/crates/perry-hir/src/stable_hash/expr.rs index 2cd353a424..076e214488 100644 --- a/crates/perry-hir/src/stable_hash/expr.rs +++ b/crates/perry-hir/src/stable_hash/expr.rs @@ -82,9 +82,11 @@ impl SH for Expr { Expr::PrivateGuard { class_name, field_name, kind, op, object } => { tag(h, 12402); class_name.hash(h); field_name.hash(h); kind.hash(h); op.hash(h); object.as_ref().hash(h); } Expr::Await(e) => { tag(h, 40); e.as_ref().hash(h); } Expr::Yield { value, delegate } => { tag(h, 41); value.hash(h); delegate.hash(h); } - Expr::New { class_name, args, type_args, } => { tag(h, 42); class_name.hash(h); args.hash(h); type_args.hash(h); } - Expr::NewDynamic { callee, args } => { tag(h, 43); callee.as_ref().hash(h); args.hash(h); } - Expr::NewDynamicSpread { callee, args } => { tag(h, 12507); callee.as_ref().hash(h); args.hash(h); } + // #5253: `byte_offset` is diagnostic-only — excluded from the hash + // for the same reason as `Call.byte_offset` (see #5247 above). + Expr::New { class_name, args, type_args, .. } => { tag(h, 42); class_name.hash(h); args.hash(h); type_args.hash(h); } + Expr::NewDynamic { callee, args, .. } => { tag(h, 43); callee.as_ref().hash(h); args.hash(h); } + Expr::NewDynamicSpread { callee, args, .. } => { tag(h, 12507); callee.as_ref().hash(h); args.hash(h); } Expr::NewTarget => { tag(h, 12301); } Expr::ClassRef(s) => { tag(h, 44); s.hash(h); } Expr::EnumMember { enum_name, member_name, } => { tag(h, 45); enum_name.hash(h); member_name.hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index 890f3cea6f..9cfceb9af2 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -897,13 +897,13 @@ where f(v); } } - Expr::NewDynamic { callee, args } => { + Expr::NewDynamic { callee, args, .. } => { f(callee); for a in args { f(a); } } - Expr::NewDynamicSpread { callee, args } => { + Expr::NewDynamicSpread { callee, args, .. } => { f(callee); for a in args { match a { diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index bc6ad98efc..9816f1e341 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -894,13 +894,13 @@ where f(v); } } - Expr::NewDynamic { callee, args } => { + Expr::NewDynamic { callee, args, .. } => { f(callee); for a in args { f(a); } } - Expr::NewDynamicSpread { callee, args } => { + Expr::NewDynamicSpread { callee, args, .. } => { f(callee); for a in args { match a { diff --git a/crates/perry/tests/issue_5253_construct_reference_source_location.rs b/crates/perry/tests/issue_5253_construct_reference_source_location.rs new file mode 100644 index 0000000000..5a3db6f23e --- /dev/null +++ b/crates/perry/tests/issue_5253_construct_reference_source_location.rs @@ -0,0 +1,188 @@ +//! Regression test for #5253 (follow-up to #5247/#5250): the runtime +//! source-location diagnostics that #5250 gave the dynamic call-dispatch +//! ("X is not a function") throw are extended to two more throw classes, both +//! gated on `--debug-symbols`: +//! +//! 1. `new X()` "X is not a constructor" — `const X: any = undefined; +//! new X();` lowers to `Expr::NewDynamic` (callee `LocalGet`) and the +//! runtime construct path throws a TypeError. This is the shape that +//! localizes ajv's `undefined is not a constructor`. +//! 2. `ReferenceError: X is not defined` — a bare unresolved identifier read +//! (`notDefinedAnywhere`) lowers to a call into +//! `js_global_get_or_throw_unresolved`, which throws a ReferenceError. +//! This is the shape that localizes winston's `module is not defined`. +//! +//! Behavior: +//! • WITH `--debug-symbols`: the thrown error's `.stack` contains +//! `at :` pointing at the offending line. +//! • WITHOUT the flag (default build): unchanged — `at `. + +use std::path::PathBuf; +use std::process::Command; +use std::sync::Once; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("canonicalize workspace root") +} + +fn target_debug_dir() -> PathBuf { + std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| workspace_root().join("target")) + .join("debug") +} + +/// Build `libperry_runtime.a` once so the compiled binaries can link. +fn ensure_runtime_archive() { + static BUILD_RUNTIME: Once = Once::new(); + BUILD_RUNTIME.call_once(|| { + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let build = Command::new(cargo) + .current_dir(workspace_root()) + .arg("build") + .arg("-p") + .arg("perry-runtime") + .output() + .expect("run cargo build -p perry-runtime"); + assert!( + build.status.success(), + "cargo build -p perry-runtime failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&build.stdout), + String::from_utf8_lossy(&build.stderr) + ); + }); +} + +fn runtime_dir() -> PathBuf { + ensure_runtime_archive(); + target_debug_dir() +} + +fn compile(root: &std::path::Path, extra_args: &[&str]) -> std::process::Output { + let entry = root.join("main.ts"); + let output = root.join("main_bin"); + let mut cmd = Command::new(perry_bin()); + cmd.current_dir(root) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache"); + for a in extra_args { + cmd.arg(a); + } + cmd.env("PERRY_NO_AUTO_OPTIMIZE", "1"); + cmd.env("PERRY_RUNTIME_DIR", runtime_dir()); + cmd.output().expect("run perry compile") +} + +fn run_fixture(fixture: &str, extra_args: &[&str]) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + std::fs::write(root.join("main.ts"), fixture).expect("write entry"); + + let out = compile(root, extra_args); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "compile must succeed (args {extra_args:?}); stderr:\n{stderr}" + ); + + let bin = root.join("main_bin"); + let run = Command::new(&bin).output().expect("run compiled binary"); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +// ---- 1. `new X()` not-a-constructor ---- + +/// `new X()` is on line 4 (1 = blank from the raw-string leading newline, +/// 2 = `const`, 3 = `try {`, 4 = `new X();`). +const NOT_A_CONSTRUCTOR_FIXTURE: &str = r#" +const X: any = undefined; +try { + new X(); +} catch (e: any) { + console.log("MSG:" + e.message); + console.log("STACK:" + e.stack); +} +"#; + +#[test] +fn debug_symbols_attaches_file_line_to_not_a_constructor_throw() { + let stdout = run_fixture(NOT_A_CONSTRUCTOR_FIXTURE, &["--debug-symbols"]); + assert!( + stdout.contains("MSG:") && stdout.contains("is not a constructor"), + "expected a 'is not a constructor' TypeError; got:\n{stdout}" + ); + assert!( + stdout.contains("at main.ts:4"), + "expected 'at main.ts:4' frame with --debug-symbols; got:\n{stdout}" + ); + assert!( + !stdout.contains(""), + "the location must replace the frame; got:\n{stdout}" + ); +} + +#[test] +fn default_build_keeps_anonymous_frame_for_not_a_constructor() { + let stdout = run_fixture(NOT_A_CONSTRUCTOR_FIXTURE, &[]); + assert!( + stdout.contains("is not a constructor"), + "expected a 'is not a constructor' TypeError; got:\n{stdout}" + ); + assert!( + !stdout.contains("at main.ts:"), + "default build must NOT emit a source location; got:\n{stdout}" + ); +} + +// ---- 2. ReferenceError `X is not defined` ---- + +/// The bare read of `notDefinedAnywhere` is on line 3 (1 = blank, 2 = `try {`, +/// 3 = the throwing read). +const REFERENCE_ERROR_FIXTURE: &str = r#" +try { + console.log(notDefinedAnywhere); +} catch (e: any) { + console.log("MSG:" + e.message); + console.log("STACK:" + e.stack); +} +"#; + +#[test] +fn debug_symbols_attaches_file_line_to_reference_error_throw() { + let stdout = run_fixture(REFERENCE_ERROR_FIXTURE, &["--debug-symbols"]); + assert!( + stdout.contains("MSG:") && stdout.contains("is not defined"), + "expected a 'is not defined' ReferenceError; got:\n{stdout}" + ); + assert!( + stdout.contains("at main.ts:3"), + "expected 'at main.ts:3' frame with --debug-symbols; got:\n{stdout}" + ); + assert!( + !stdout.contains(""), + "the location must replace the frame; got:\n{stdout}" + ); +} + +#[test] +fn default_build_keeps_anonymous_frame_for_reference_error() { + let stdout = run_fixture(REFERENCE_ERROR_FIXTURE, &[]); + assert!( + stdout.contains("is not defined"), + "expected a 'is not defined' ReferenceError; got:\n{stdout}" + ); + assert!( + !stdout.contains("at main.ts:"), + "default build must NOT emit a source location; got:\n{stdout}" + ); +} From e4356d1d51f7f8261dc4b2eeb53cc2a11678802c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 17 Jun 2026 05:35:22 +0200 Subject: [PATCH 2/2] test: add byte_offset to Expr::New inits surfaced by rebase Tests added to main after this branch was cut (typed_shape_descriptor, constructor_recursion, generator/id_scan, inline) construct Expr::New literals; this branch makes byte_offset a required field, so they need it too. Pure mechanical (byte_offset: 0). No version/CHANGELOG/Cargo.lock edits. --- crates/perry-codegen/tests/constructor_recursion.rs | 2 ++ crates/perry-codegen/tests/typed_shape_descriptor.rs | 1 + crates/perry-transform/src/generator/id_scan.rs | 1 + crates/perry-transform/src/inline/mod.rs | 1 + 4 files changed, 5 insertions(+) diff --git a/crates/perry-codegen/tests/constructor_recursion.rs b/crates/perry-codegen/tests/constructor_recursion.rs index 56dc066296..fb41ffe500 100644 --- a/crates/perry-codegen/tests/constructor_recursion.rs +++ b/crates/perry-codegen/tests/constructor_recursion.rs @@ -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, }], @@ -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(), diff --git a/crates/perry-codegen/tests/typed_shape_descriptor.rs b/crates/perry-codegen/tests/typed_shape_descriptor.rs index 87d74b49d1..a58b9d4d31 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptor.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptor.rs @@ -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, diff --git a/crates/perry-transform/src/generator/id_scan.rs b/crates/perry-transform/src/generator/id_scan.rs index 75cc79e155..526a46b055 100644 --- a/crates/perry-transform/src/generator/id_scan.rs +++ b/crates/perry-transform/src/generator/id_scan.rs @@ -445,6 +445,7 @@ mod tests { class_name: "__AnonShape_test".to_string(), args: vec![closure], type_args: Vec::new(), + byte_offset: 0, }], }; diff --git a/crates/perry-transform/src/inline/mod.rs b/crates/perry-transform/src/inline/mod.rs index d71877ff9b..f8956f5f73 100644 --- a/crates/perry-transform/src/inline/mod.rs +++ b/crates/perry-transform/src/inline/mod.rs @@ -720,6 +720,7 @@ mod tests { class_name: name.to_string(), args: Vec::new(), type_args: Vec::new(), + byte_offset: 0, }) }