diff --git a/CHANGELOG.md b/CHANGELOG.md index bc20a947cb..e337c0c553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +## v0.5.1178 — perf(codegen): outline per-new-site inline allocator (smaller IR + faster) + +`new ClassName(...)` previously emitted the full object-allocation prologue +inline at every call site. That bloated the IR (and the resulting binary) and +slowed codegen/compile. The allocation now calls the outlined runtime helper +`js_object_alloc_class_inline_keys`, so each new-site shrinks to a single call. +Complementary to #5304 (which outlined the constructor *call*); this outlines +the *allocation*. The two touch different regions of `lower_call/new.rs`. + +Two supporting changes: + +- **Runtime (#4717):** folded the field-slot zero-fill into + `js_object_alloc_class_inline_keys`. The allocation moved out of per-site + codegen, where callers used to zero-fill `max(field_count, 8)` slots by hand; + doing it inside the helper keeps every caller — including the outlined `new C()` + path — correct by construction. Without it, a field read-before-write or a GC + scan of the still-constructing instance could observe stale recycled arena + bytes. +- Split the `FieldInitMode` enum + `apply_field_initializers_recursive` walker + out of `lower_call/new.rs` into a sibling `field_init.rs` (pure move) to keep + the file under the 2,000-LOC CI size gate. + +## v0.5.1177 — fix(codegen): injective function-symbol names (distinct names that sanitize alike) + +Two distinct module-level functions could mangle to the same LLVM symbol, so +clang rejected the module with "invalid redefinition of function". Two root +causes, both fixed: + +- `scoped_fn_name` used the lossy `sanitize` (every non-`[A-Za-z0-9_]` byte → + `_`), so minified names like `$Z5` and `_Z5` both became + `perry_fn____Z5`. Switched to the injective `sanitize_member` (the same + mangler `scoped_static_method_name` already uses); byte-identical for the + common `[A-Za-z0-9_]` case. `func_names` is keyed by func id and every call + site resolves through it, so changing the mangling stays consistent. +- Minified code reuses short names (`function A`) across scopes, and perry + lambda-lifts nested functions to module level, so two module functions can + legitimately share a name. `compile_module` now disambiguates collisions + with a `__dupN` suffix keyed by the mangled symbol. Exported functions are + referenced cross-module by their canonical `scoped_fn_name`, so they reserve + their name first and never get suffixed. + +Sibling fix to the per-class class-keys-global disambiguation (5ac457967). + ## v0.5.1176 — fix(runtime): loose equality (`==`) now treats SSO short strings as strings `js_jsvalue_loose_equals` (the helper behind `assert.equal`/`assert.deepEqual` diff --git a/CLAUDE.md b/CLAUDE.md index 3707a4a109..0b11dc724a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1176 +**Current Version:** 0.5.1178 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 2f4ab09a67..598dc28de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5283,7 +5283,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "base64", @@ -5340,14 +5340,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "cc", "libc", @@ -5355,7 +5355,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "log", @@ -5370,7 +5370,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-hir", @@ -5379,7 +5379,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-hir", @@ -5387,7 +5387,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-dispatch", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-hir", @@ -5406,7 +5406,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "base64", @@ -5419,7 +5419,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-hir", @@ -5427,7 +5427,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "async-trait", @@ -5456,14 +5456,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "serde", "serde_json", @@ -5471,7 +5471,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1176" +version = "0.5.1178" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5482,7 +5482,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "clap", @@ -5497,14 +5497,14 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-argon2" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "argon2", "perry-ffi", @@ -5512,7 +5512,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "reqwest", @@ -5521,7 +5521,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "bcrypt", "perry-ffi", @@ -5529,7 +5529,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "rusqlite", @@ -5537,7 +5537,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "scraper", @@ -5545,7 +5545,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "perry-runtime", @@ -5553,7 +5553,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "chrono", "cron 0.16.0", @@ -5563,7 +5563,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "chrono", "perry-ffi", @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "rust_decimal", @@ -5579,7 +5579,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "serde_json", @@ -5587,7 +5587,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5595,7 +5595,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "perry-runtime", @@ -5603,14 +5603,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "bytes", "http-body-util", @@ -5627,7 +5627,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "lazy_static", "perry-ffi", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5652,7 +5652,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "bytes", "h2", @@ -5675,7 +5675,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "lazy_static", "perry-ffi", @@ -5685,7 +5685,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "jsonwebtoken", @@ -5696,7 +5696,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "lru", "perry-ffi", @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "chrono", "perry-ffi", @@ -5712,7 +5712,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "bson", "futures-util", @@ -5724,7 +5724,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "chrono", "perry-ffi", @@ -5734,7 +5734,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "nanoid", "perry-ffi", @@ -5743,7 +5743,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "perry-runtime", @@ -5755,7 +5755,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "lettre", "perry-ffi", @@ -5765,7 +5765,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "printpdf", @@ -5773,7 +5773,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "sqlx", @@ -5782,7 +5782,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "governor", "perry-ffi", @@ -5790,7 +5790,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "image", @@ -5799,14 +5799,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "lazy_static", "perry-ffi", @@ -5815,7 +5815,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "uuid", @@ -5823,7 +5823,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ffi", "regex", @@ -5833,7 +5833,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "futures-util", "lazy_static", @@ -5845,7 +5845,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "brotli", "flate2", @@ -5854,7 +5854,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "dashmap", "once_cell", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-api-manifest", @@ -5881,7 +5881,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-diagnostics", @@ -5893,7 +5893,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "base64", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -6017,7 +6017,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "perry-hir", @@ -6027,7 +6027,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -6035,14 +6035,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "itoa", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "rand 0.8.6", "serde", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "cairo-rs", @@ -6092,7 +6092,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "block2", @@ -6108,7 +6108,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "block2", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1176" +version = "0.5.1178" [[package]] name = "perry-ui-test" @@ -6131,11 +6131,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.1176" +version = "0.5.1178" [[package]] name = "perry-ui-tvos" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "block2", @@ -6151,7 +6151,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "block2", @@ -6167,7 +6167,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "block2", "libc", @@ -6180,7 +6180,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "libc", @@ -6197,14 +6197,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "base64", "ed25519-dalek", @@ -6218,7 +6218,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1176" +version = "0.5.1178" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 72abc62526..af266dc0df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -215,7 +215,7 @@ strip = false codegen-units = 16 [workspace.package] -version = "0.5.1176" +version = "0.5.1178" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 427b0339df..f42994c635 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -138,6 +138,10 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> if opts.debug_locations { if let Some(src) = opts.module_source.clone() { strings.set_debug_location_ctx(Some((hir.name.clone(), src))); + // #5247 (CJS-wrap coordinate skew): `src` is the WRAPPED source for + // a CommonJS module; subtract the wrapper-prefix line count when + // resolving offsets so the rendered line is in original coordinates. + strings.set_debug_source_line_offset(opts.debug_source_line_offset); } } diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index 569643d804..a2e332fb0a 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -380,6 +380,14 @@ pub struct CompileOptions { /// `debug_locations` is on (avoids cloning source for every module in the /// common build). `None` falls back to the `` frame. pub module_source: Option, + /// #5247 (CJS-wrap coordinate skew): for a CommonJS module rewritten by + /// `cjs_wrap`, `module_source` is the WRAPPED text and `byte_offset`s are in + /// wrapped coordinates. This is the number of newlines the injected wrapper + /// prefix added before the original body; codegen subtracts it from the + /// wrapped line so the rendered location is in original-source coordinates. + /// `0` for non-wrapped modules and the entire default build (offsets inside + /// the preamble — wrapped line `<=` this — resolve to no location). + pub debug_source_line_offset: u32, } /// Issue #100: one entry in a module's namespace-population list. diff --git a/crates/perry-codegen/src/lower_call/native/mod.rs b/crates/perry-codegen/src/lower_call/native/mod.rs index 8fe74efe49..73820c58ca 100644 --- a/crates/perry-codegen/src/lower_call/native/mod.rs +++ b/crates/perry-codegen/src/lower_call/native/mod.rs @@ -112,6 +112,20 @@ pub(crate) fn lower_native_method_call( || Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))), |arg| lower_expr(ctx, arg), )?; + // #5247 (coverage gap): under `--debug-symbols`, the + // destructuring lowering passes the object-pattern's source byte + // offset as a second literal arg. Emit a `js_set_call_location` + // immediately before the coercibility check so the + // "Cannot convert undefined or null to object" throw renders + // `at :` for THIS destructure rather than the stale + // last-tracked call (which can be in an unrelated module). No-op + // in the default build (offset arg absent / locations disabled). + if ctx.strings.debug_locations_enabled() { + if let Some(Expr::Number(off)) = args.get(1) { + let byte_offset = *off as u32; + crate::expr::calls::emit_call_location_at(ctx, byte_offset); + } + } return Ok(ctx.block().call( DOUBLE, "js_require_object_coercible", diff --git a/crates/perry-codegen/src/strings.rs b/crates/perry-codegen/src/strings.rs index df90231e9c..1b94df9047 100644 --- a/crates/perry-codegen/src/strings.rs +++ b/crates/perry-codegen/src/strings.rs @@ -83,6 +83,11 @@ pub struct StringPool { /// carries `(module_file_path, module_source)` so the call-lowering site /// can resolve a `Call.byte_offset` to a `file:line`. debug_location_ctx: Option<(String, String)>, + /// #5247 (CJS-wrap coordinate skew): newlines the wrapper prefix added + /// before the original body in `debug_location_ctx`'s (wrapped) source. + /// `call_location_for` subtracts this from the wrapped line number to + /// recover the original-source line. `0` for non-wrapped modules. + debug_source_line_offset: u32, /// #5247: the byte offset of the `Expr::Call` currently being lowered, /// recorded by the call dispatcher and consumed at the dynamic /// method-dispatch emission site (after the call's arguments — which may @@ -122,6 +127,7 @@ impl StringPool { interned: HashMap::new(), entries: Vec::new(), debug_location_ctx: None, + debug_source_line_offset: 0, pending_call_offset: std::cell::Cell::new(0), } } @@ -137,6 +143,13 @@ impl StringPool { self.debug_location_ctx = ctx; } + /// #5247 (CJS-wrap coordinate skew): set the wrapper-prefix line count that + /// `call_location_for` subtracts from the wrapped line number. `0` (the + /// default) leaves resolution unchanged. + pub fn set_debug_source_line_offset(&mut self, offset: u32) { + self.debug_source_line_offset = offset; + } + /// #5247: true iff source-location tracking is active for this module /// (i.e. `--debug-symbols` installed a debug-location context). Lets the /// dispatch emission site skip all location work in the default build. @@ -170,11 +183,22 @@ impl StringPool { return None; } // 1-based line = 1 + count of newlines before the offset. - let line = 1 + src.as_bytes()[..offset] + let wrapped_line = 1 + src.as_bytes()[..offset] .iter() .filter(|&&b| b == b'\n') - .count(); - Some((file.as_str(), line as u32)) + .count() as u32; + // #5247 (CJS-wrap coordinate skew): `src` is the WRAPPED source for a + // CommonJS module, so deduct the wrapper-prefix line count to recover + // the original-source line. A wrapped line at or inside the preamble + // (`wrapped_line <= offset`) has no original counterpart → no location. + let line = if self.debug_source_line_offset == 0 { + wrapped_line + } else if wrapped_line > self.debug_source_line_offset { + wrapped_line - self.debug_source_line_offset + } else { + return None; + }; + Some((file.as_str(), line)) } /// Intern a string literal. Returns the interned index, stable for the @@ -413,4 +437,52 @@ mod tests { // A position inside line 1 → line 1. assert_eq!(p.call_location_for(2), Some(("foo.ts", 1))); } + + // ───────────── #5247 CJS-wrap coordinate skew correction ───────────── + // For a CJS-wrapped module `src` is the WRAPPED text and offsets are in + // wrapped coords. `set_debug_source_line_offset(N)` makes `call_location_for` + // report `wrapped_line - N` so the location is in original-source coords. + + #[test] + fn cjs_wrap_offset_subtracts_prefix_lines() { + // Wrapped: 3 preamble lines, then the original body's two lines. + // line1: import _req_0 ... + // line2: const _cjs = (function() { + // line3: /* preamble */ + // line4: foo(); <- original body line 1 + // line5: bar(); <- original body line 2 + let src = "import _req_0;\nconst _cjs = (function() {\n // preamble\n foo();\n bar();\n"; + let mut p = pool_with_src(src); + p.set_debug_source_line_offset(3); + // 'f' of foo() is on wrapped line 4 → original line 1. + let off_foo = (src.find("foo();").unwrap() + 1) as u32; + assert_eq!(p.call_location_for(off_foo), Some(("foo.ts", 1))); + // 'b' of bar() is on wrapped line 5 → original line 2. + let off_bar = (src.find("bar();").unwrap() + 1) as u32; + assert_eq!(p.call_location_for(off_bar), Some(("foo.ts", 2))); + } + + #[test] + fn cjs_wrap_offset_inside_preamble_is_none() { + // An offset whose wrapped line is at/inside the prefix has no original + // counterpart → no location (rather than a bogus negative/zero line). + let src = "import _req_0;\nconst _cjs = (function() {\n // preamble\n foo();\n"; + let mut p = pool_with_src(src); + p.set_debug_source_line_offset(3); + // Offset on wrapped line 1 (the injected import) → None. + assert_eq!(p.call_location_for(3), None); + // Offset on wrapped line 3 (preamble, == prefix count) → None. + let off_preamble = (src.find("// preamble").unwrap() + 1) as u32; + assert_eq!(p.call_location_for(off_preamble), None); + } + + #[test] + fn zero_offset_leaves_lines_unchanged() { + // Non-wrapped module (offset 0): line resolution is the raw wrapped line. + let src = "foo();\nbar();\n"; + let mut p = pool_with_src(src); + p.set_debug_source_line_offset(0); + let off_bar = (src.find("bar();").unwrap() + 1) as u32; + assert_eq!(p.call_location_for(off_bar), Some(("foo.ts", 2))); + } } diff --git a/crates/perry-codegen/tests/argless_builtin_extra_args.rs b/crates/perry-codegen/tests/argless_builtin_extra_args.rs index 5b5c5cf45d..44d8b51e43 100644 --- a/crates/perry-codegen/tests/argless_builtin_extra_args.rs +++ b/crates/perry-codegen/tests/argless_builtin_extra_args.rs @@ -54,6 +54,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/class_keys_gc_root.rs b/crates/perry-codegen/tests/class_keys_gc_root.rs index e62611395c..89cc445e73 100644 --- a/crates/perry-codegen/tests/class_keys_gc_root.rs +++ b/crates/perry-codegen/tests/class_keys_gc_root.rs @@ -73,6 +73,7 @@ fn entry_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/constructor_recursion.rs b/crates/perry-codegen/tests/constructor_recursion.rs index fb41ffe500..1c6077c27c 100644 --- a/crates/perry-codegen/tests/constructor_recursion.rs +++ b/crates/perry-codegen/tests/constructor_recursion.rs @@ -49,6 +49,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/destructure_call_location.rs b/crates/perry-codegen/tests/destructure_call_location.rs new file mode 100644 index 0000000000..3bd10d0523 --- /dev/null +++ b/crates/perry-codegen/tests/destructure_call_location.rs @@ -0,0 +1,133 @@ +//! Regression test for #5247 (coverage gap): object destructuring of a +//! `null`/`undefined` source throws "Cannot convert undefined or null to +//! object" via the `requireObjectCoercible` runtime call. Before this fix the +//! throw rendered the *stale* last-tracked call location (often in an unrelated +//! module) because the destructuring path emitted no `js_set_call_location`. +//! +//! The HIR destructuring lowering now carries the object-pattern's source byte +//! offset as a second literal argument to the `requireObjectCoercible` call; +//! under `--debug-symbols` codegen emits a `js_set_call_location(file, line)` +//! immediately before `js_require_object_coercible` so the thrown TypeError's +//! `.stack` points at the destructure. +//! +//! This asserts the codegen contract directly on the emitted LLVM IR: with a +//! debug-location context installed, the location call precedes the coercibility +//! check; with no `--debug-symbols` context, no location call is emitted (the +//! default build stays byte-identical / overhead-free). + +use perry_codegen::{compile_module, AppMetadata, CompileOptions}; +use perry_hir::{Expr, Module, ModuleInitKind, Stmt}; + +fn base_opts() -> CompileOptions { + CompileOptions { + target: None, + is_entry_module: true, + non_entry_module_prefixes: Vec::new(), + import_function_prefixes: std::collections::HashMap::new(), + import_function_origin_names: std::collections::HashMap::new(), + import_function_v8_specifiers: std::collections::HashMap::new(), + import_function_node_submodule: std::collections::HashMap::new(), + namespace_node_submodules: std::collections::HashMap::new(), + namespace_v8_specifiers: std::collections::HashMap::new(), + namespace_member_prefixes: std::collections::HashMap::new(), + emit_ir_only: true, + verify_native_regions: false, + disable_buffer_fast_path: false, + namespace_imports: Vec::new(), + namespace_reexport_named_imports: std::collections::HashSet::new(), + imported_classes: Vec::new(), + imported_enums: Vec::new(), + imported_async_funcs: std::collections::HashSet::new(), + type_aliases: std::collections::HashMap::new(), + imported_func_param_counts: std::collections::HashMap::new(), + imported_func_has_rest: std::collections::HashSet::new(), + imported_func_synthetic_arguments: std::collections::HashSet::new(), + imported_func_return_types: std::collections::HashMap::new(), + imported_vars: std::collections::HashSet::new(), + output_type: "executable".to_string(), + needs_stdlib: false, + needs_ui: false, + needs_geisterhand: false, + geisterhand_port: 7676, + enabled_features: Vec::new(), + native_module_init_names: Vec::new(), + js_module_specifiers: Vec::new(), + bundled_extensions: Vec::new(), + native_library_functions: Vec::new(), + i18n_table: None, + fast_math: false, + fp_contract_mode: perry_codegen::FpContractMode::Off, + app_metadata: AppMetadata::default(), + namespace_entries: Vec::new(), + dynamic_import_path_to_prefix: std::collections::HashMap::new(), + deferred_module_prefixes: std::collections::HashSet::new(), + module_init_deps: Vec::new(), + is_dynamic_import_target: false, + debug_locations: false, + module_source: None, + debug_source_line_offset: 0, + } +} + +/// A module whose init evaluates `RequireObjectCoercible(undefined)` exactly as +/// the destructuring lowering emits it: a `__perry_runtime` NativeMethodCall +/// with the source `undefined` plus the object-pattern byte offset as a second +/// `Number` arg. Offset 8 sits on line 2 of the source below. +fn module_with_destructure_coercible() -> Module { + let mut m = Module::new("destr.ts"); + m.init = vec![Stmt::Expr(Expr::NativeMethodCall { + module: "__perry_runtime".to_string(), + class_name: None, + object: None, + method: "requireObjectCoercible".to_string(), + // arg 0 = source value; arg 1 = object-pattern byte offset (1-based). + // BytePos 8 → source index 7 ('a' on line 2) → line 2. + args: vec![Expr::Undefined, Expr::Number(8.0)], + })]; + m.init_kind = ModuleInitKind::Eager; + m +} + +#[test] +fn destructure_emits_call_location_before_coercible_check() { + let mut opts = base_opts(); + opts.debug_locations = true; + // "x();\nconst {a} = o;\n" — BytePos 8 lands on line 2. + opts.module_source = Some("x();\nconst {a} = o;\n".to_string()); + + let ir = String::from_utf8(compile_module(&module_with_destructure_coercible(), opts).unwrap()) + .expect("LLVM IR should be UTF-8"); + + // Match the CALLS, not the always-present `declare`s in the runtime + // preamble (which appear earlier in the IR than any call). + let set_loc = ir + .find("call void @js_set_call_location") + .expect("expected a js_set_call_location call under --debug-symbols:\n"); + let coercible = ir + .find("call double @js_require_object_coercible") + .expect("expected a js_require_object_coercible call in IR"); + assert!( + set_loc < coercible, + "js_set_call_location must precede js_require_object_coercible:\n{ir}" + ); +} + +#[test] +fn no_call_location_without_debug_symbols() { + // Default build: debug_locations off → no per-destructure location call, + // and the coercibility check is still emitted (behavior unchanged). + let ir = String::from_utf8( + compile_module(&module_with_destructure_coercible(), base_opts()).unwrap(), + ) + .expect("LLVM IR should be UTF-8"); + assert!( + ir.contains("call double @js_require_object_coercible"), + "coercibility check must still be emitted by default:\n{ir}" + ); + // `js_set_call_location` is always `declare`d in the runtime preamble; the + // contract is that no CALL to it is emitted in the default build. + assert!( + !ir.contains("call void @js_set_call_location"), + "no js_set_call_location CALL should be emitted without --debug-symbols:\n{ir}" + ); +} diff --git a/crates/perry-codegen/tests/large_object_barriers.rs b/crates/perry-codegen/tests/large_object_barriers.rs index 448ef45c7b..3b7f958948 100644 --- a/crates/perry-codegen/tests/large_object_barriers.rs +++ b/crates/perry-codegen/tests/large_object_barriers.rs @@ -49,6 +49,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs index 5ff8acb1fd..9b25cf45a6 100644 --- a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs +++ b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs @@ -55,6 +55,7 @@ fn entry_opts(target: Option<&str>) -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/native_proof_buffer_views.rs b/crates/perry-codegen/tests/native_proof_buffer_views.rs index 910495f6d1..e9d1ffe572 100644 --- a/crates/perry-codegen/tests/native_proof_buffer_views.rs +++ b/crates/perry-codegen/tests/native_proof_buffer_views.rs @@ -54,6 +54,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index ea8a1a1553..52155e6470 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -54,6 +54,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/shadow_slot_hygiene.rs b/crates/perry-codegen/tests/shadow_slot_hygiene.rs index 56904988ec..39f4b09d8d 100644 --- a/crates/perry-codegen/tests/shadow_slot_hygiene.rs +++ b/crates/perry-codegen/tests/shadow_slot_hygiene.rs @@ -49,6 +49,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/static_symbol_hygiene.rs b/crates/perry-codegen/tests/static_symbol_hygiene.rs index 6eef851d69..6d1444fc03 100644 --- a/crates/perry-codegen/tests/static_symbol_hygiene.rs +++ b/crates/perry-codegen/tests/static_symbol_hygiene.rs @@ -49,6 +49,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index 063a7b7e6a..8543cadf22 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -80,6 +80,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/typed_shape_descriptor.rs b/crates/perry-codegen/tests/typed_shape_descriptor.rs index a58b9d4d31..e3873205be 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptor.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptor.rs @@ -49,6 +49,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index aac9a8d048..1b86972f18 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -79,6 +79,7 @@ fn empty_opts() -> CompileOptions { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry-hir/src/destructuring/pattern_binding.rs b/crates/perry-hir/src/destructuring/pattern_binding.rs index 4e8698937e..1c47563043 100644 --- a/crates/perry-hir/src/destructuring/pattern_binding.rs +++ b/crates/perry-hir/src/destructuring/pattern_binding.rs @@ -418,6 +418,15 @@ pub(crate) fn lower_pattern_binding_into( // RequireObjectCoercible: destructuring a `null`/`undefined` source // throws a TypeError even for an empty pattern `{}`, before any // property is read. + // + // #5247 (coverage gap): carry the object-pattern's source byte + // offset (`obj_pat.span.lo.0`) as a second argument so codegen can, + // under `--debug-symbols`, attach a `file:line` to the + // "Cannot convert undefined or null to object" throw — otherwise the + // last-set call location (often in an unrelated module) is rendered. + // The offset is a plain `f64` literal that the default-build codegen + // arm ignores (it reads `args.first()` only), so emitted output is + // unchanged when the flag is off. result.push(Stmt::Let { id: tmp_id, name: tmp_name, @@ -425,7 +434,7 @@ pub(crate) fn lower_pattern_binding_into( mutable: false, init: Some(runtime_iterator_call( "requireObjectCoercible", - vec![source], + vec![source, Expr::Number(f64::from(obj_pat.span.lo.0))], )), }); diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index a7a12b49c2..1c1f44918d 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -448,6 +448,9 @@ pub fn run_with_parse_cache( let mut ctx = CompilationContext::new(project_root.clone()); ctx.cache_root = object_cache_project_root(&args.input, &project_root); + // #5247: propagate `--debug-symbols` so `collect_modules` records the + // CJS-wrap source mapping needed to render original-source line numbers. + ctx.debug_symbols = args.debug_symbols; let build_cache_probe = BuildCacheProbe::new(&args, &project_root, &ctx.cache_root); let mut build_cache_stats = build_cache_probe.probe(); @@ -4037,11 +4040,35 @@ pub fn run_with_parse_cache( // the module's original source so codegen can map a Call's byte // offset to a 1-based line. debug_locations: args.debug_symbols, + // #5247: source consulted to turn a node's `byte_offset` into a + // line. For a CommonJS module the offsets are in WRAPPED-source + // coordinates (perry parsed the injected-IIFE text), so we hand + // codegen the WRAPPED source — counting newlines up to a wrapped + // offset against the original would be off by the preamble byte + // length. `debug_source_line_offset` (below) then converts the + // wrapped line back to the original line. Non-wrapped modules + // read the original from disk. module_source: if args.debug_symbols { - std::fs::read_to_string(path).ok() + match ctx.cjs_wrap_debug_sources.get(path) { + Some(w) => Some(w.wrapped_source.clone()), + None => std::fs::read_to_string(path).ok(), + } } else { None }, + // #5247 (CJS-wrap coordinate skew): the number of newlines the + // injected wrapper prefix added before the original module body. + // Codegen subtracts this from the wrapped line number so the + // rendered location is in original-source coordinates. `0` for + // non-wrapped modules (and the entire default build). + debug_source_line_offset: if args.debug_symbols { + ctx.cjs_wrap_debug_sources + .get(path) + .map(|w| w.prefix_line_count) + .unwrap_or(0) + } else { + 0 + }, }; // V2.2 + #686 object cache lookup. The key hashes every // codegen-affecting field of `opts` together with this diff --git a/crates/perry/src/commands/compile/cjs_wrap/mod.rs b/crates/perry/src/commands/compile/cjs_wrap/mod.rs index 1d0b45b9dc..0514f63000 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/mod.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/mod.rs @@ -60,7 +60,7 @@ pub(self) use hoist_classes::{ // Public API consumed by `compile.rs` / `collect_modules.rs`. pub(super) use detect::is_commonjs; -pub(super) use wrap::wrap_commonjs_for_target; +pub(super) use wrap::{wrap_commonjs_for_target, wrap_commonjs_with_body_offset}; #[cfg(test)] mod tests { @@ -74,10 +74,38 @@ mod tests { extract_require_aliases_with_ranges, extract_require_specifiers, }; use super::hoist_classes::{source_has_top_level_return, top_level_class_names}; - use super::wrap::{wrap_commonjs, wrap_commonjs_for_target}; + use super::wrap::{wrap_commonjs, wrap_commonjs_for_target, wrap_commonjs_with_body_offset}; use std::fs; use std::path::PathBuf; + // #5247: the wrapped output must report where the ORIGINAL body begins, and + // because blanking/hoisting preserve newlines, the prefix line count lets a + // wrapped body line map back to its original-source line. This is the unit + // that backs the `--debug-symbols` CJS-wrap coordinate correction. + #[test] + fn cjs_wrap_body_offset_maps_back_to_original_line() { + // Original body: `function f(){...}` on line 1, `module.exports = f` + // on line 3. A throw inside f (wrapped line L) must map to original + // line `L - prefix_line_count`. + let original = "function f() {\n return new Nope();\n}\nmodule.exports = f;\n"; + let path = PathBuf::from("/tmp/x/index.js"); + let (wrapped, body_off) = wrap_commonjs_with_body_offset(original, &path, None); + let body_off = body_off.expect("body should be locatable in wrapped output"); + // Prefix line count = newlines before the body in the wrapped output. + let prefix_lines = wrapped.as_bytes()[..body_off] + .iter() + .filter(|&&b| b == b'\n') + .count(); + // The `return new Nope();` line is original line 2. Find its wrapped + // line and confirm subtracting the prefix recovers line 2. + let needle_off = wrapped.find("return new Nope();").unwrap(); + let wrapped_line = 1 + wrapped.as_bytes()[..needle_off] + .iter() + .filter(|&&b| b == b'\n') + .count(); + assert_eq!(wrapped_line - prefix_lines, 2); + } + #[test] fn detects_module_exports_assignment() { assert!(is_commonjs("module.exports = function() {};")); diff --git a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs index 8a12f383d4..0af4d27af2 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs @@ -18,6 +18,21 @@ pub(in crate::commands::compile) fn wrap_commonjs_for_target( source_path: &Path, target: Option<&str>, ) -> String { + wrap_commonjs_with_body_offset(source, source_path, target).0 +} + +/// Like [`wrap_commonjs_for_target`], but also returns the byte offset within +/// the returned wrapped source at which the ORIGINAL module body begins (i.e. +/// the length of the injected wrapper prefix: imports + aliases + hoisted +/// classes + the IIFE/preamble scaffolding). `--debug-symbols` uses this to map +/// a wrapped-coordinate `byte_offset` back to original-source coordinates. +/// `None` when the body could not be located in the wrapped output (a +/// special-case early rewrite changed it); callers then skip the mapping. +pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( + source: &str, + source_path: &Path, + target: Option<&str>, +) -> (String, Option) { let mut source_cow = Cow::Borrowed(source); if is_depd_index_path(source_path) { @@ -726,7 +741,16 @@ const _cjs = (function() {{ wrapped ); } - wrapped + // #5247: locate the original body within the wrapped output so callers can + // translate a wrapped-coordinate byte offset back to original coordinates. + // `body_for_iife` is interpolated verbatim into `wrapped`, so the first + // occurrence is its start. Empty body → no mapping. + let body_offset = if body_for_iife.is_empty() { + None + } else { + wrapped.find(body_for_iife.as_str()) + }; + (wrapped, body_offset) } fn target_node_platform(target: Option<&str>) -> Option<&'static str> { diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index 246f194be7..a166b3518f 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -400,12 +400,53 @@ fn collect_module_one( // left untouched. let was_cjs_wrapped = (is_in_compiled_pkg || !is_in_node_modules) && super::cjs_wrap::is_commonjs(&raw_source); + // #5247: when `--debug-symbols` is on, capture where the original module + // body lands inside the wrapped output so source-location resolution can + // map a wrapped-coordinate byte offset back to an original-source line. + // `None` unless we both wrapped this module AND debug symbols are on, so + // the default build does no extra work. + let mut cjs_wrap_body_prefix_lines: Option = None; let source = if was_cjs_wrapped { - super::cjs_wrap::wrap_commonjs_for_target(&raw_source, &canonical, target) + if ctx.debug_symbols { + let (wrapped, body_off) = + super::cjs_wrap::wrap_commonjs_with_body_offset(&raw_source, &canonical, target); + // Newlines before the original body in the wrapped output = the + // wrapper prefix line count. Recorded only when the body was + // located; otherwise we skip the skew correction (graceful + // degrade to the uncorrected line rather than a wrong one). + cjs_wrap_body_prefix_lines = body_off.map(|off| { + wrapped.as_bytes()[..off] + .iter() + .filter(|&&b| b == b'\n') + .count() as u32 + }); + wrapped + } else { + super::cjs_wrap::wrap_commonjs_for_target(&raw_source, &canonical, target) + } } else { raw_source }; + // #5247: the create-require transform may prepend `import * as` lines, + // shifting BOTH the prefix and the body down by the same number of lines. + // Capture the wrapped line count, run the transform, then add the line + // delta to the prefix so the wrapped-line → original-line subtraction is + // computed against the FINAL parsed source. + let lines_before_transform = source.bytes().filter(|&b| b == b'\n').count(); let source = transform_create_require_literal_requires(&source, &ctx.compile_packages); + if was_cjs_wrapped && ctx.debug_symbols { + if let Some(prefix_lines) = cjs_wrap_body_prefix_lines { + let lines_after_transform = source.bytes().filter(|&b| b == b'\n').count(); + let added_lines = lines_after_transform.saturating_sub(lines_before_transform) as u32; + ctx.cjs_wrap_debug_sources.insert( + canonical.clone(), + super::types::CjsWrapDebugSource { + wrapped_source: source.clone(), + prefix_line_count: prefix_lines + added_lines, + }, + ); + } + } // Note (#686): we no longer hash source bytes here. The object cache key // is now keyed on a post-transform HIR fingerprint computed inside the diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index b10ba65c63..3578064ea2 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -955,6 +955,7 @@ mod object_cache_tests { is_dynamic_import_target: false, debug_locations: false, module_source: None, + debug_source_line_offset: 0, } } diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index a60b8e9ca5..526fa0ea13 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -839,6 +839,36 @@ pub struct CompilationContext { /// `NODE_ENV → "production"` default applied to `node_modules` code unless /// overridden. Keyed by the full `process.env.` string. pub define: HashMap, + /// #5247 (CJS-wrap coordinate skew): for each CommonJS module rewritten by + /// `cjs_wrap::wrap_commonjs_for_target`, the ORIGINAL (pre-wrap) source + /// text plus the number of newline characters the injected wrapper prefix + /// prepended before the original module body. Under `--debug-symbols`, + /// codegen resolves a node's `byte_offset` (which is in WRAPPED + /// coordinates) to a line by deducting this prefix line count and looking + /// up the original source — so a throw renders `at :` + /// rather than a line shifted by the preamble. Empty unless + /// `--debug-symbols` is set (the map is only populated then), keeping the + /// default build allocation-free. + pub cjs_wrap_debug_sources: HashMap, + /// #5247: mirror of the CLI `--debug-symbols` flag, set after construction. + /// Gates the CJS-wrap source mapping capture in `collect_modules` so the + /// default build never records `cjs_wrap_debug_sources`. + pub debug_symbols: bool, +} + +/// #5247: source mapping for a CJS-wrapped module, used only by the +/// `--debug-symbols` source-location path. See `cjs_wrap_debug_sources`. +#[derive(Debug, Clone)] +pub struct CjsWrapDebugSource { + /// The WRAPPED module source text (the injected-IIFE text perry parsed). + /// Byte offsets on the HIR are in these coordinates, so codegen counts + /// newlines against this to get the wrapped line number. + pub wrapped_source: String, + /// Newlines in the injected wrapper prefix that precede the original body + /// in the wrapped text. A wrapped 1-based line `L` maps to original line + /// `L - prefix_line_count` (clamped; offsets inside the preamble itself — + /// `L <= prefix_line_count` — map to no location). + pub prefix_line_count: u32, } /// #2309: a package's declared `sideEffects` (package.json). `Unknown` (the @@ -954,6 +984,8 @@ impl CompilationContext { deferred_refusals: Vec::new(), side_effects_cache: HashMap::new(), define: HashMap::new(), + cjs_wrap_debug_sources: HashMap::new(), + debug_symbols: false, } } }