Skip to content

feat(error): source location on dynamic call-dispatch TypeErrors (#5247)#5250

Merged
proggeramlug merged 3 commits into
mainfrom
feat/runtime-error-source-location
Jun 16, 2026
Merged

feat(error): source location on dynamic call-dispatch TypeErrors (#5247)#5250
proggeramlug merged 3 commits into
mainfrom
feat/runtime-error-source-location

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

#5247 — source location on dynamic call-dispatch TypeErrors (first increment)

Perry runtime errors carried no source location: make_stack hardcoded
at <anonymous>, so a native-compiled npm package's runtime TypeError
("X is not a function") couldn't be localized.

This is the bounded first increment: the dynamic call-dispatch throw path
("X is not a function") now carries a real file:line in the thrown
TypeError's .stack, gated behind --debug-symbols. Default (release) builds
are byte-identical with no per-call overhead.

Pipeline (byte_offset → line table → stack)

  1. HIRExpr::Call gains byte_offset: u32 set from call.span.lo.0 at
    AST→HIR lowering (follows AOT: defer non-resolvable dynamic import() to a runtime error (shared notice + strict), like #5206 #5230's DynamicImport.byte_offset precedent). 0
    for synthesized/transform/intrinsic calls. Excluded from the stable hash
    (diagnostic-only) so source-offset shifts don't bust the object cache.
  2. CodegenStringPool carries an optional per-module debug-location
    context (file path + source text) installed only under --debug-symbols, and
    resolves byte_offset → 1-based line via call_location_for. The Expr::Call
    dispatcher records the pending offset; the dynamic method-dispatch emission
    sites (js_typed_feedback_native_call_method + universal
    js_native_call_method) emit js_set_call_location(file, line) immediately
    before the throwing dispatch — after the call's args are lowered, so a
    nested-call argument's location no longer shadows the outer call's.
  3. Runtime — a thread-local CURRENT_CALL_LOCATION (set by the new
    js_set_call_location FFI) is read by make_stack at <file>:<line>;
    falls back to at <anonymous> when unset.

Which throws carry location

  • (number/string/...).method is not a function and bare value is not a function via the dynamic method dispatch (js_native_call_method and the
    typed-feedback wrapper) — the highest-value path (npm-package method calls on
    any/primitive receivers).

Flag

Reuses --debug-symbols (CompileArgs.debug_symbols
CompileOptions.debug_locations + module_source). Default OFF. Added to the
object-cache key (dbgloc) so toggling invalidates cached .o.

Tests

  • Unit (StringPool::call_location_for): line boundaries, first/last line,
    zero-offset sentinel, no-context (default build), UTF-8 safety.
  • Integration (issue_5247_runtime_error_source_location): const f: any = 5; f.test(); → WITH --debug-symbols the .stack contains at main.ts:4
    and no <anonymous>; WITHOUT the flag it stays at <anonymous> with no
    file:line.
  • Cache key: toggling debug_locations changes the object-cache key.

Test evidence (local, macOS): byte→line unit tests 6/6; codegen lib 103/103;
perry-hir lib 148/148; perry-transform lib 34/34; perry-runtime error:: 4/4;
integration 2/2; cache-key toggle 1/1 — all green.

f.test() result

WITHOUT flag: STACK:TypeError: (number).test is not a function
                  at <anonymous>
WITH --debug-symbols: STACK:TypeError: (number).test is not a function
                  at ftest2.ts:3      # the f.test() line

ajv localization (payoff check)

new Ajv().compile({...}) under --debug-symbols:

TypeError: (number).test is not a function
    at node_modules/ajv/dist/compile/codegen/code.js:3

The (number).test sentinel bug now localizes to the exact compiled-package
file:line (reported only — not fixed here).

Deferred (follow-up)

  • Multi-frame stacks (single frame today).
  • Other throw sites: new X not-a-constructor, the string-typed unknown-method
    catch-all (lower_string_method.rs), proxy/reflect, collection-iter.
  • Clearing a stale location for an offset-0 synthesized call that subsequently
    throws (currently the previous located frame persists for such a throw).

Version / CHANGELOG note (maintainer)

No [workspace.package] version, CLAUDE.md Current Version, CHANGELOG.md, or
Cargo.lock edits — per contributor guidance, please fold the version bump +
changelog entry in at merge.

Summary by CodeRabbit

Release Notes

  • New Features
    • Runtime TypeErrors from dynamic method dispatch now include accurate source location information (file:line) when compiled with the --debug-symbols flag, providing better error diagnostics instead of generic location markers.

First bounded increment of #5247: give the dynamic call-dispatch throw
path ("X is not a function") a real source `file:line` in the thrown
TypeError's `.stack`, gated behind `--debug-symbols` so the default
(release) build is byte-identical and incurs no per-call overhead.

Pipeline (byte_offset -> line table -> stack):
  1. HIR: `Expr::Call` gains a `byte_offset: u32` (`call.span.lo.0`),
     set at AST->HIR lowering (mirrors #5230's DynamicImport.byte_offset).
     0 sentinel for synthesized/transform/intrinsic calls. Excluded from
     the stable hash (diagnostic-only) so source-offset shifts don't bust
     the object cache.
  2. Codegen: `StringPool` carries an optional per-module debug-location
     context (file path + source) installed only under --debug-symbols, and
     resolves a byte offset -> 1-based line via `call_location_for`. The
     `Expr::Call` dispatcher records the pending offset; the dynamic
     method-dispatch emission sites (typed-feedback + universal
     `js_native_call_method`) emit `js_set_call_location(file,line)`
     immediately before the throwing dispatch (after args lower, so a
     nested-call arg's location no longer shadows the outer call's).
  3. Runtime: a thread-local CURRENT_CALL_LOCATION (set by the new
     `js_set_call_location` FFI) is read by `make_stack` to render
     `at <file>:<line>`; falls back to `at <anonymous>` when unset.

Flag: reuses --debug-symbols (CompileArgs.debug_symbols ->
CompileOptions.debug_locations + module_source). Added to the object-cache
key so toggling it invalidates cached .o.

Tests: byte->line unit tests (boundaries, first/last line, zero-offset,
no-context, UTF-8 safety); integration issue_5247 (`f.test()` -> with the
flag `.stack` contains `at main.ts:4`; without it stays `<anonymous>`);
cache-key toggle test.

Deferred (noted for follow-up): multi-frame stacks; the other throw sites
(`new X` not-a-constructor, string-typed unknown-method catch-all,
proxy/reflect, collection-iter); clearing a stale location for an
offset-0 synthesized call that throws.

NOTE (maintainer): no version/CHANGELOG/Cargo.lock bump per contributor
guidance — please fold in at merge.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 51108829-bd44-4902-b9ba-1d7309351110

📥 Commits

Reviewing files that changed from the base of the PR and between b6803a8 and 550f1e1.

📒 Files selected for processing (7)
  • crates/perry-hir/src/lower/const_fold_fn.rs
  • crates/perry-hir/src/lower/expr_call/module_class_static.rs
  • crates/perry-hir/src/lower/expr_call/native_module.rs
  • crates/perry-hir/src/lower/expr_member.rs
  • crates/perry-hir/src/lower/expr_object.rs
  • crates/perry-hir/src/lower/lower_expr.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs

📝 Walkthrough

Walkthrough

Adds byte_offset: u32 to HIR Expr::Call, threads per-module source text through CompileOptions and StringPool, implements the js_set_call_location runtime FFI that stores a thread-local (file, line), hooks it into two dynamic-dispatch codegen paths, and adds an end-to-end regression test asserting at main.ts:4 appears in thrown TypeError stacks under --debug-symbols.

Changes

Source location for dynamic-dispatch TypeErrors

Layer / File(s) Summary
Expr::Call byte_offset field and downstream adaptations
crates/perry-hir/src/ir/expr.rs, crates/perry-hir/src/lower/expr_call/mod.rs, crates/perry-hir/src/monomorph/substitute_expr.rs, crates/perry-hir/src/monomorph/driver.rs, crates/perry-hir/src/monomorph/update_call_sites.rs, crates/perry-hir/src/stable_hash/expr.rs
Expr::Call gains a byte_offset: u32 field documented as the SWC span offset; lower_call_inner captures the real span; monomorphization destructuring/reconstruction and stable-hash are updated to carry or exclude the field.
StringPool debug context and byte-to-line mapping
crates/perry-codegen/src/strings.rs
StringPool gains debug_location_ctx and pending_call_offset fields, a newline-counting call_location_for method, and a unit-test suite covering sentinel 0, boundary, out-of-range, and UTF-8 cases.
CompileOptions fields, compiler wiring, and object cache key
crates/perry-codegen/src/codegen/opts.rs, crates/perry-codegen/src/codegen/mod.rs, crates/perry/src/commands/compile.rs, crates/perry/src/commands/compile/object_cache.rs
CompileOptions adds debug_locations and module_source; the compiler reads the module file when --debug-symbols is set; compile_module installs the StringPool context; the object cache key includes dbgloc and a test verifies key invalidation.
Runtime js_set_call_location FFI and error-stack rendering
crates/perry-runtime/src/error.rs
Introduces CURRENT_CALL_LOCATION thread-local, exports js_set_call_location (sets or clears location), adds a #[used] keepalive, and updates make_stack to render at <file>:<line> or fall back to at <anonymous>.
Codegen: emit_call_location_at, dispatch hooks, and FFI declaration
crates/perry-codegen/src/runtime_decls/strings_part2.rs, crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/expr/calls.rs, crates/perry-codegen/src/lower_call/property_get.rs, crates/perry-codegen/src/lower_call/console_promise.rs, crates/perry-codegen/src/expr/proxy_reflect.rs
Declares js_set_call_location as an FFI; exposes calls as pub(crate); implements emit_call_location_at; the Expr::Call lowering arm stores the pending offset; both throw-capable dynamic-dispatch paths capture the offset before argument lowering and emit it before the runtime call.
Mechanical byte_offset: 0 propagation across all HIR lowering sites
crates/perry-hir/src/..., crates/perry-transform/src/...
Every HIR/transform construction site that synthesizes Expr::Call or Expr::ExternFuncRef (destructuring, JSX, decorators, iterators, generators, deforest, state_desugar, and many more) is updated to supply byte_offset: 0 as the synthesized sentinel.
Regression test and test scaffold updates
crates/perry/tests/issue_5247_runtime_error_source_location.rs, crates/perry-codegen/tests/*, crates/perry-hir/src/monomorph/tests.rs, crates/perry-transform/src/deforest/tests.rs
Adds the end-to-end regression test asserting at main.ts:4 with --debug-symbols and at <anonymous> without; updates all existing test helpers and fixtures to include the new struct fields.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as perry CLI
  participant Compile as compile.rs
  participant StringPool
  participant lower_expr as lower_expr (Expr::Call)
  participant dispatch as property_get / console_promise
  participant emit as emit_call_location_at
  participant Runtime as js_set_call_location
  participant make_stack

  CLI->>Compile: --debug-symbols flag
  Compile->>StringPool: set_debug_location_ctx(file, source)
  lower_expr->>StringPool: set_pending_call_offset(byte_offset)
  dispatch->>StringPool: pending_call_offset() → call_byte_offset
  dispatch->>emit: emit_call_location_at(ctx, call_byte_offset)
  emit->>StringPool: call_location_for(offset) → (file, line)
  emit->>Runtime: js_set_call_location(file, line)
  Note over Runtime: TypeError thrown by dynamic dispatch
  make_stack->>Runtime: current_stack_frame()
  Runtime-->>make_stack: "at main.ts:4"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐇 A call went astray, no name could be found,
The stack whispered softly, <anonymous> the sound.
But now with an offset, a line and a file,
The error says "main.ts:4" with a smile.
No more anonymous hops through the dark —
The bunny leaves breadcrumbs, each byte leaves a mark! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: adding source location information to dynamic call-dispatch TypeErrors, which is the primary change across the entire changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the problem statement, solution approach (three-stage pipeline), implementation details, flag configuration, testing strategy, and deferred work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/runtime-error-source-location

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/expr/calls.rs`:
- Around line 63-69: The early return in the else block (when call_location_for
returns None) skips the runtime clear path, allowing stale location state from
prior dispatches to persist. Before returning when
call_location_for(byte_offset) fails to resolve a location, invoke the runtime
clear operation (the same cleanup that executes in the normal path) to reset the
call location state to its default anonymous value, ensuring offsetless or
synthesized calls do not inherit stale file:line information from previous
operations.

In `@crates/perry-runtime/src/error.rs`:
- Around line 109-116: The thread-local storage location is being set via the
function containing the diff but is never cleared after being consumed, causing
stale file:line data to leak to subsequent unrelated errors. After the call
location is read and used for stack rendering (which occurs at the location
referenced as "Also applies to: 126-131"), clear the thread-local storage by
setting it back to None to prevent stale frame data from persisting across
multiple error contexts.

In `@crates/perry/src/commands/compile/object_cache.rs`:
- Around line 168-173: The cache key generation for debug locations (the dbgloc
field) only considers whether the debug_locations flag is enabled, but doesn't
include information about the actual source text content. This allows stale
cached objects to be reused when the source code changes but the HIR remains
identical, causing incorrect file:line information to be reported. Include a
fingerprint of the module_source in the cache key when opts.debug_locations is
true, so that source-text edits properly invalidate the cached object and
prevent serving stale results with incorrect location data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c840a7c9-da63-4c8f-b377-5d80541ab6cc

📥 Commits

Reviewing files that changed from the base of the PR and between a5d7b3e and 9618d4f.

📒 Files selected for processing (70)
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/codegen/opts.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/pointer_locals.rs
  • crates/perry-codegen/src/expr/calls.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/proxy_reflect.rs
  • crates/perry-codegen/src/lower_call/console_promise.rs
  • crates/perry-codegen/src/lower_call/property_get.rs
  • crates/perry-codegen/src/runtime_decls/strings_part2.rs
  • crates/perry-codegen/src/strings.rs
  • crates/perry-codegen/tests/class_keys_gc_root.rs
  • crates/perry-codegen/tests/constructor_recursion.rs
  • crates/perry-codegen/tests/large_object_barriers.rs
  • crates/perry-codegen/tests/macos_bundle_chdir_gate.rs
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
  • crates/perry-codegen/tests/static_symbol_hygiene.rs
  • crates/perry-codegen/tests/typed_feedback.rs
  • crates/perry-codegen/tests/typed_shape_descriptor.rs
  • crates/perry-codegen/tests/typed_shape_descriptors.rs
  • crates/perry-hir/src/destructuring/assignment_stmt.rs
  • crates/perry-hir/src/dynamic_import/tests.rs
  • crates/perry-hir/src/ir/expr.rs
  • crates/perry-hir/src/jsx.rs
  • crates/perry-hir/src/lower/array_fold.rs
  • crates/perry-hir/src/lower/const_fold_fn.rs
  • crates/perry-hir/src/lower/decorators.rs
  • crates/perry-hir/src/lower/eval_super_scan.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-hir/src/lower/expr_call/crypto.rs
  • crates/perry-hir/src/lower/expr_call/globals.rs
  • crates/perry-hir/src/lower/expr_call/intrinsics.rs
  • crates/perry-hir/src/lower/expr_call/mod.rs
  • crates/perry-hir/src/lower/expr_call/module_class_static.rs
  • crates/perry-hir/src/lower/expr_call/module_static.rs
  • crates/perry-hir/src/lower/expr_call/native_module.rs
  • crates/perry-hir/src/lower/expr_call/post_args_dispatch.rs
  • crates/perry-hir/src/lower/expr_call/url_date_instance.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/expr_member.rs
  • crates/perry-hir/src/lower/expr_misc.rs
  • crates/perry-hir/src/lower/expr_new.rs
  • crates/perry-hir/src/lower/expr_object.rs
  • crates/perry-hir/src/lower/lower_expr.rs
  • crates/perry-hir/src/lower/stmt_loops.rs
  • crates/perry-hir/src/lower/widget_decl.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-hir/src/lower_decl/body_stmt/detect.rs
  • crates/perry-hir/src/lower_decl/body_stmt/for_await.rs
  • crates/perry-hir/src/lower_decl/class_computed.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/lower_decl/helpers.rs
  • crates/perry-hir/src/monomorph/driver.rs
  • crates/perry-hir/src/monomorph/substitute_expr.rs
  • crates/perry-hir/src/monomorph/tests.rs
  • crates/perry-hir/src/monomorph/update_call_sites.rs
  • crates/perry-hir/src/stable_hash/expr.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-transform/src/deforest/call_sites.rs
  • crates/perry-transform/src/deforest/tests.rs
  • crates/perry-transform/src/generator/helpers.rs
  • crates/perry-transform/src/generator/linearize.rs
  • crates/perry-transform/src/generator/lower.rs
  • crates/perry-transform/src/state_desugar.rs
  • crates/perry/src/commands/compile.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/tests/issue_5247_runtime_error_source_location.rs

Comment on lines +63 to +69
let Some((file, line)) = ctx
.strings
.call_location_for(byte_offset)
.map(|(f, l)| (f.to_string(), l))
else {
return;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale call-location state when no location is resolvable.

On Line 67, returning early skips the runtime clear path. If a prior dispatch set a location, later offsetless/synthesized calls can inherit that stale <file>:<line> instead of falling back to <anonymous>.

Suggested fix
 pub(crate) fn emit_call_location_at(ctx: &mut FnCtx<'_>, byte_offset: u32) {
     let Some((file, line)) = ctx
         .strings
         .call_location_for(byte_offset)
         .map(|(f, l)| (f.to_string(), l))
     else {
+        // Clear any previously latched call-site location.
+        ctx.block().call_void(
+            "js_set_call_location",
+            &[(PTR, "null"), (I64, "0"), (I32, "0")],
+        );
         return;
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/calls.rs` around lines 63 - 69, The early
return in the else block (when call_location_for returns None) skips the runtime
clear path, allowing stale location state from prior dispatches to persist.
Before returning when call_location_for(byte_offset) fails to resolve a
location, invoke the runtime clear operation (the same cleanup that executes in
the normal path) to reset the call location state to its default anonymous
value, ensuring offsetless or synthesized calls do not inherit stale file:line
information from previous operations.

Comment on lines +109 to +116
if line == 0 || file_ptr.is_null() || file_len == 0 {
CURRENT_CALL_LOCATION.with(|c| *c.borrow_mut() = None);
return;
}
let bytes = std::slice::from_raw_parts(file_ptr, file_len);
let file = String::from_utf8_lossy(bytes).into_owned();
CURRENT_CALL_LOCATION.with(|c| *c.borrow_mut() = Some((file, line)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume call location after first stack render to avoid stale frame leakage

At Lines 126-131, the TLS location is read but never cleared. After one instrumented call site sets it, later unrelated errors can inherit stale file:line data.

Suggested fix
 fn current_stack_frame() -> String {
-    CURRENT_CALL_LOCATION.with(|c| match &*c.borrow() {
-        Some((file, line)) => format!("    at {}:{}", file, line),
-        None => "    at <anonymous>".to_string(),
-    })
+    CURRENT_CALL_LOCATION.with(|c| {
+        let mut slot = c.borrow_mut();
+        match slot.take() {
+            Some((file, line)) => format!("    at {}:{}", file, line),
+            None => "    at <anonymous>".to_string(),
+        }
+    })
 }

Also applies to: 126-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/error.rs` around lines 109 - 116, The thread-local
storage location is being set via the function containing the diff but is never
cleared after being consumed, causing stale file:line data to leak to subsequent
unrelated errors. After the call location is read and used for stack rendering
(which occurs at the location referenced as "Also applies to: 126-131"), clear
the thread-local storage by setting it back to None to prevent stale frame data
from persisting across multiple error contexts.

Comment on lines +168 to 173
// #5247: `--debug-symbols` flips per-call `js_set_call_location` emission,
// which changes the emitted IR (and `.o` bytes). Without this in the key,
// toggling the flag would serve the previously-cached object and the
// source locations would silently not appear.
h.field("dbgloc", if opts.debug_locations { "1" } else { "0" });
h.field(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include source-text fingerprint in cache key when debug locations are enabled

At Line 172, dbgloc invalidates only on flag toggles, but debug-location output also depends on module_source line layout. Source-only edits that keep HIR identical can currently reuse stale cached objects and report incorrect file:line.

Suggested fix
     h.field("dbgloc", if opts.debug_locations { "1" } else { "0" });
+    if opts.debug_locations {
+        // Debug-location emission depends on source layout (byte->line mapping).
+        let src_hash = opts
+            .module_source
+            .as_ref()
+            .map(|s| format!("{:016x}", djb2_hash(s.as_bytes())))
+            .unwrap_or_else(|| "none".to_string());
+        h.field("dbgloc_src", &src_hash);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/object_cache.rs` around lines 168 - 173,
The cache key generation for debug locations (the dbgloc field) only considers
whether the debug_locations flag is enabled, but doesn't include information
about the actual source text content. This allows stale cached objects to be
reused when the source code changes but the HIR remains identical, causing
incorrect file:line information to be reported. Include a fingerprint of the
module_source in the cache key when opts.debug_locations is true, so that
source-text edits properly invalidate the cached object and prevent serving
stale results with incorrect location data.

Formats four files flagged by the cargo fmt --check lint gate:
- perry-codegen/src/strings.rs (this PR's change)
- perry-stdlib/src/readline.rs, perry/.../collect_modules.rs,
  perry/.../collect_modules/wasm_asset.rs (inherited from main —
  merged during the CI window where the Tests workflow was in
  startup_failure and lint never ran).
@proggeramlug proggeramlug merged commit 7dd805d into main Jun 16, 2026
12 of 15 checks passed
@proggeramlug proggeramlug deleted the feat/runtime-error-source-location branch June 16, 2026 11:13
proggeramlug added a commit that referenced this pull request Jun 16, 2026
…set after #5250) (#5266)

* test(hir): add byte_offset to Expr::Call inits in cheerio_call_rewrite (fix main test compile after #5250)

#5250 added Expr::Call.byte_offset but missed this integration test → cargo test -p perry-hir
failed to compile on main. Add byte_offset: 0 to the 3 Expr::Call constructions.

* test(hir): rustfmt — indent byte_offset in loop-body Expr::Call (fix lint)

---------

Co-authored-by: Ralph Küpper <ralph2@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant