Skip to content

fix(edr_solidity): solx stack-trace attribution for declaration-level and line-0 DWARF - #1577

Merged
nebasuke merged 29 commits into
mainfrom
fix/solx-dwarf-attribution
Jul 27, 2026
Merged

fix(edr_solidity): solx stack-trace attribution for declaration-level and line-0 DWARF#1577
nebasuke merged 29 commits into
mainfrom
fix/solx-dwarf-attribution

Conversation

@nebasuke

@nebasuke nebasuke commented Jul 25, 2026

Copy link
Copy Markdown
Member

Adding test coverage for stack trace tests, along with a test fixture regen within edr_tool_cli. I will follow-up after hardhat-solx is published as this gives multiple opportunities to simplify/merge the infrastructure and add it to CI.

This PR notable bumps the used version of solx from 0.1.4 to 0.1.6.

Fixes for solx -> stack trace mappings (as part of 0.1.4 -> 0.1.6):

Further test coverage in #1552. Note that due to the nature of the fixtures I pre-added all scenarios to StackTraceScenarios.sol including the tests I saved for #1552.

Added test cases for -O3 in specific, as DWARF mapping are slightly worse there. This likely due to underlying solx optimisations, but I thought it would be good to have as a red-green test, as the original tests (by Marian) were using -O3.

Claude summary

Fixes the solx (DWARF) stack-trace attribution gaps found while broadening provider-path coverage, and adapts the DWARF consumption to solx 0.1.6's line-0 emission — including a frame-loss regression the parity sweep caught on 0.1.6 that would otherwise have shipped. The fixtures move to solx 0.1.6 and become regenerable in-repo, with a mode-3 variant keeping every compat path red-provable.

Split out of #1552, which now stacks the broad provider-path test coverage on top of this.

Root cause

solx attributes compiler-generated helper code — calldata decoding, shared revert builders — to the declaration of the enclosing function or contract in its DWARF line table, where solc leaves the same code unmapped in source maps. Since 0.1.6 solx emits line 0 for that code instead (solx#582), which the decoder's Pass-3 fallback turns back into the enclosing function's declaration — so the declaration-attributed paths below are the permanent consumer architecture, not a pre-0.1.6 shim. At hardhat-solx's default -O1, solx 0.1.6 statement-attributes most of this code directly; other optimizer modes and older releases still produce the declaration-attributed and unmapped shapes.

Three location-anchored inference paths broke on that difference:

  1. fails_right_after_call / is_call_failed_error compare every post-CALL step location to the call site by strict equality. Under solc, unmapped helper instructions are skipped; under solx, the same instructions carry declaration-level locations (whose range merely contains the statement), so the heuristics bailed and the failure misclassified as a generic revert at the call site.
  2. Shared revert helpers flattened out of a modifier are attributed to the modified function's declaration line, so a modifier's failing require was reported at the function signature. The statement's real line is present in the line table — on the message-building instructions executed just before the revert — so this is recoverable EDR-side.
  3. A modifier's bare revert() compiles to a shared helper that is entirely unmapped, and its return data is empty, so every revert heuristic missed and inference degraded to OtherExecutionError at the contract declaration.

Both fixes for (1) and (2) live in the solx strategy; SolcTraceStrategy is behavior-identical (strict equality kept, new parameters ignored):

  • New TraceStrategy::step_still_at_statement(step, statement): solx treats a step location that contains the statement (declaration-level padding) as "still at the statement"; solc keeps ==. Used by is_last_location.
  • TraceStrategy::revert_source_reference receives a lazy step_pcs thunk (same pattern as PanicHelperContext). When the reverting instruction sits on the function's declaration line, SolxTraceStrategy walks the executed steps back to the last statement-level location.

For (3), the revert branch falls back to a strategy hook when the reverting instruction has no resolvable containing function: failing_function_from_calldata resolves the called function from the calldata selector under solx (solc returns None, behavior-identical), gated to REVERT opcodes and to calldata that actually decodes for the resolved function, so dispatch-level reverts still classify as InvalidParamsError. revert_source_reference takes the location as an Option — unmapped gets the same walk-back recovery as declaration padding, with the function start as last resort.

Review hardening of the walk-back

A review round produced three constraints (no existing golden changed): only statements of the failing function or of a modifier executed in the frame qualify — a statement of any other function ends the walk (previously the first resolvable location won, even the dispatcher's or an earlier internal call's); declaration matching requires source-file identity instead of bare line numbers; and the contract-declaration line counts as declaration-level padding alongside the function's. The step_still_at_statement containment proxy is deliberately kept as-is: it can only over-match when every instruction of a later statement is declaration-attributed, and the parity sweep guards that residual. A later pass tried to also unify the constructor/fallback/receive display names between the two source-reference builders; the hardhat-tests corpus rejected it — function-start frames pin the raw (empty) AST name — so the asymmetry stays, documented at the site.

solx 0.1.6 adaptation: the line-0 frame loss

solx 0.1.6 also emits DW_AT_call_line 0 for inlined subroutines whose call site is compiler-generated — most importantly user functions inlined into the __entry dispatch. That cost solx traces the Contract.function callstack frame solc renders between the caller frame and the revert (sweep scenarios ModifierRevertTest/NestedModifierRevertTest collapsed from 3 frames to 2). Two-part fix:

  1. range_call_site_to_location falls back to the inlined function's own decl_file/decl_line when call_file/call_line are absent or zero — the same declaration line solc renders for that frame. (solx's DWARF carries no decl_column — LLVM subprograms track only file and line — so the resolved offset skips the line's indentation to land inside the function's AST span.)
  2. intermediate_frames previously deduped against the raw failing function — under Pass-3 synthesis that names the flattened-into function while the bottom frame actually renders under the modifier (via the walk-back), so the just-recovered frame was dropped as a "duplicate". It now dedups against the bottom frame's already-resolved source reference, computed before the intermediate frames at each call site.

Effect on the traces

  • ReturndataSizeError surfaces at the call site for both a returndata-size mismatch and a typed call to a codeless account, replacing a bare revert frame at the call site. This is parity with the solc route: solc ≥ 0.8.10 emits no EXTCODESIZE probe for returndata-expecting calls either, so both routes classify the EOA case as a returndata failure. (A true NoncontractAccountCalledError would need a void-returning call scenario — future coverage.)
  • A multi-statement modifier revert reports the failing require's line instead of the declaration line — removing the provider-level twin of the sweep's pinned NestedModifierRevertTest divergence.
  • A modifier's bare revert() reports the revert() statement inside the modifier, matching solc. At optimizer mode 3 — where the entry path carries no statement line — the walk-back lands one line earlier, on the guard condition, instead of collapsing to OtherExecutionError at the contract declaration; the remaining one-line gap is the solx-side residual.
  • The called function's frame survives a cross-contract modifier revert.

The solx_stack_trace.rs diff is deliberately additive — the existing tests and helpers are untouched (the one deletion is an import line), so what shows up there is what changed semantically. Deduplicating those helpers happens in the stacked coverage PR, where the breadth of tests motivates it.

Includes a changeset (patch), following #1569's precedent for user-visible stack-trace fixes.

Test provenance

All eight new provider goldens (plus the red-green decoder unit test for the line-0 call site) fail on main. The -O1 fixtures can no longer reproduce the declaration-attribution symptoms — solx 0.1.6 statement-attributes the default pipeline — so those tests pin the parity shape, while the mode-3 fixture variant pins the compat inference paths. Each mechanism was verified load-bearing by disabling it individually:

Mechanism Red without it (and on main)
calldata-selector recovery mode3_bare_modifier_revert_recovers_the_failing_function (OtherExecutionError at the contract declaration)
declaration-attribution detection mode3_nested… / mode3_cross_contract… (revert at the declaration line instead of the failing require)
revert walk-back all three mode-3 goldens
step_still_at_statement containment both returndata goldens (misclassified as a generic revert)
dwarf line-0 → declaration fallback cross_contract… (dropped callee frame) + the decoder unit test

Trace-shape assertions go through assert_trace_shape, which prints one line per entry: a gained frame may be an improvement (update the pin), a lost frame is a regression.

Fixtures: solx 0.1.6 + gen-solx-fixtures

cargo run -p edr_tool_cli -- gen-solx-fixtures <solx-binary> regenerates the self-contained fixture outputs (counter, stack_trace_scenarios, stack_trace_scenarios_mode3): it splices the fixtures/sources/ contents into the committed input JSON (whose content fields are deliberately empty), runs solx --standard-json, and writes solx's verbatim output. It lives next to gen-execution-api — the repo's existing home for generators of committed artifacts. That surfaced two latent bugs fixed here: update::project_root() still assumed the crate's pre-#1139 crates/tools/ depth, so it resolved to crates/ instead of the repo root; and update() failed with a pathless ENOENT on a not-yet-committed output file instead of creating it.

The inputs pin settings.optimizer.mode explicitly (trace shapes differ per mode) at hardhat-solx's -O1, the pipeline these fixtures stand in for — not solx's own default. The mode-3 variant of stack_trace_scenarios exists because -O1 DWARF is statement-attributed since 0.1.6: without it, the walk-back, declaration-attribution, and selector-recovery paths would be live code with no reachable input in the repo — they guard non-default modes and pre-0.1.6 artifacts. Re-running the subcommand against the committed tree yields zero diff, so the fixtures round-trip deterministically.

Everything is regenerated with solx 0.1.6 (released 2026-07-22): the debug info changed for every contract, deployed bytecode is identical modulo the metadata hash, and every pre-existing golden holds. Two PC-anchored decoder tests moved: solx#583 resolves inline-assembly source refs to Yul AST nodes, so the assembly revert and invalid() now report solc's statement lines instead of the function-decl fall-back.

The scenarios fixture is outside gen-solx-fixtures' reach: its input also names forge-std sources whose contents are scrubbed from the committed JSON, so it regenerates from the sweep project instead — pnpm regen-fixtures, the flow #1572 added — which is why it moves in its own step here. That also makes it the one fixture built by the real hardhat-solx pipeline rather than a bare solx --standard-json.

New StackTraceScenarios.sol fixture: plain contracts, no forge-std, so gen-solx-fixtures can rebuild it from the repo alone. It is committed whole — GuardedBareRevert, ValidatedCounter(Caller) and ExpectsWord pin the fixes here, the dispatch-level contracts are consumed by the stacked coverage PR. Splitting the file would move the line numbers every golden pins.

Sweep pins

BareModifierRevertTest reaches parity with the bare-revert fix; InlineAssemblyRevertTest and InvalidOpcodeTest with solx#583. Only InternalRecurseTest (optimizer unrolls 3-deep self-recursion) stays pinned. The pin set now assumes hardhat-solx maps 0.8.34 to solx 0.1.6.

Verification

  • solx_stack_trace.rs: 11/11 (3 pre-existing goldens unchanged across the regeneration, 5 -O1 + 3 mode-3 new; all 8 new ones proven red on main, plus the per-mechanism disable checks above).
  • edr_solidity unit tests: 80/80; edr_provider harness: 54 lib + 191 integration tests green at the tip (fork tests skip without ALCHEMY_URL).
  • JS parity sweep run locally against solx 0.1.4, 0.1.5 and 0.1.6 (hardhat-solx at -O1, local plugin build): 0.1.6 + these fixes → strict parity green; 0.1.4 and 0.1.5 + these fixes → still green, so the changes are inert for pre-line-0 artifacts.
  • solc behavior is guarded code-structurally (solc strategy unchanged) and by the hardhat-tests stack-trace corpus in CI.

nebasuke added 6 commits July 25, 2026 09:52
…ation

#1139 moved the crate from crates/tools/ to crates/tool/cli/ without
updating project_root(), which has resolved to crates/ instead of the
repo root since — gen-execution-api joins its paths onto the wrong
directory. One ancestor further up fixes it (and the new caller in
gen-solx-fixtures).
…xtures

`cargo run -p edr_tool_cli -- gen-solx-fixtures <solx-binary>` regenerates
the self-contained solx compiler-output fixtures in
`crates/edr_solidity/fixtures`: it splices the `fixtures/sources/`
contents into the committed input JSON (whose `content` fields are
deliberately empty), runs `solx --standard-json`, and writes solx's
verbatim output. Lives next to `gen-execution-api` — the repo's existing
home for generators of committed artifacts.

stdin is fed from a scoped thread so a large input can't deadlock against
the child filling its stdout pipe. Standard-json compilers read stdin to
EOF before emitting output, so this cannot fire today, but fixtures grow.

The `scenarios` fixture is deliberately not covered: its input also
depends on forge-std sources whose contents are scrubbed from the
committed JSON, so it stays hardhat-generated.
A self-contained solx fixture (plain contracts, no forge-std) for the
provider-path stack-trace tests, regenerable in-repo via
`gen-solx-fixtures` — unlike the `scenarios` fixture, which needs the
sweep project. The input pins `settings.optimizer.mode` to "1": trace
shapes differ per optimizer mode, and "1" is hardhat-solx's default, the
pipeline these fixtures stand in for (solx's own default is "3"). Output
compiled with solx 0.1.6.

The scenario set is complete as a corpus — `GuardedBareRevert`,
`ValidatedCounter`(`Caller`) and `ExpectsWord` pin the attribution fixes
in this PR, the dispatch-level contracts (payability, missing
fallback/receive, calldata decoding, external library linking) are
consumed by the stacked coverage PR. Keeping it whole means the file's
line numbers — which every golden pins — don't move between the two.
…tack traces

solx attributes compiler-generated helper code (calldata decoding, shared
revert builders) to the *declaration* of the enclosing function or
contract, where solc leaves such code unmapped. Two inference paths broke
on that difference:

1. fails_right_after_call / is_call_failed_error compared every post-CALL
   step location to the call site by strict equality, so declaration-level
   padding (whose range merely contains the statement) made the heuristics
   bail and reverts degraded to OtherExecutionError at the contract
   declaration line. New TraceStrategy::locations_equivalent treats a
   containing (declaration-level) location as "still at the statement"
   for solx; solc keeps strict equality.

2. A shared revert helper flattened out of a modifier is attributed to the
   modified function's declaration line. SolxTraceStrategy::
   revert_source_reference now walks the executed steps back from a
   declaration-line revert to the statement that led there (the require's
   message-building code keeps its own line), matching solc.

Provider-path effects, pinned in solx_stack_trace.rs by a later commit in
this PR:
- ReturndataSizeError now surfaces at the call site for both a
  returndata-size mismatch and a typed call to a codeless account
  (parity with solc >= 0.8.10, which emits no EXTCODESIZE probe for
  returndata-expecting calls), replacing OtherExecutionError at the
  contract declaration.
- A multi-statement modifier revert reports the failing require's line
  instead of the function declaration line, removing the provider-level
  twin of the sweep's NestedModifierRevertTest divergence.

The solc strategy is behavior-identical.
Review findings on the walk in SolxTraceStrategy::revert_source_reference:

1. The walk was unbounded — the first resolvable location off the
   declaration line won, even if it belonged to the dispatcher or an
   internal function executed earlier in the message. It now only accepts
   statements of the failing function itself or of a modifier (flattened
   into its frame, possibly from another file — pure function containment
   would regress the NestedModifierRevertTest golden); a statement of any
   other function ends the walk and keeps the declaration reference.
2. Both the trigger and the skip compared bare line numbers, which don't
   identify a location across files. Declaration matching now requires
   file identity (via SourceLocation::contains, which checks the source
   file pointer).
3. The trigger only recognized the function declaration line, though solx
   also attributes helpers to the enclosing contract declaration; both
   lines now count as declaration-level padding.

The locations_equivalent containment proxy reviewed alongside is kept
as-is: it can only over-match when every instruction of a later statement
is declaration-attributed, and the parity sweep guards that residual.

No golden changes. The `GuardedBareRevert` scenario of the fixture — a
modifier's bare revert(), with no message-building code to keep its own
line — surfaces the adjacent third gap this constraint set doesn't
address: the whole helper is attributed to the contract declaration,
whose location has no containing function, so inference degrades before
revert_source_reference is consulted. Fixed in the next commit.
A modifier's bare revert() compiles to a shared helper that solx leaves
entirely unmapped (no DWARF line at all), and its return data is empty,
so every revert heuristic missed and inference fell through to
OtherExecutionError at the contract start.

check_last_instruction's revert branch now falls back to a strategy hook
when the reverting instruction has no resolvable containing function:
TraceStrategy::declaration_attributed_failing_function resolves the
called function from the calldata selector under solx (solc returns None
— behavior-identical, its reverts are mapped). The fallback only engages
for REVERT opcodes and only when the calldata decodes for the resolved
function, so dispatch-level reverts on undecodable calldata still reach
the InvalidParamsError classification below.

revert_source_reference now takes Option<&SourceLocation>: an unmapped
instruction gets the same walk-back recovery as declaration-attributed
padding, with the failing function's start as last resort (unmapped and
contract-declaration locations can't become source references
themselves).

Effect on the GuardedBareRevert scenario: RevertError at the revert()
statement inside the modifier — parity with solc — instead of
OtherExecutionError at the contract declaration. (At optimizer mode 3 the
walk-back stops one line earlier, at the guard condition; the fixtures
compile at hardhat-solx's -O1, where the statement line survives.)
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 13:32 — with GitHub Actions Inactive
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 58a940d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@nomicfoundation/edr Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 49.29972% with 181 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.72%. Comparing base (c66cd68) to head (58a940d).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
crates/tool/cli/src/solx_fixtures.rs 0.00% 98 Missing ⚠️
crates/edr_solidity/src/error_inferrer.rs 56.47% 29 Missing and 8 partials ⚠️
crates/edr_solidity/src/trace_strategy.rs 67.61% 22 Missing and 12 partials ⚠️
crates/tool/cli/src/update.rs 0.00% 10 Missing ⚠️
crates/edr_solidity/src/debug_info/dwarf.rs 98.27% 1 Missing ⚠️
crates/tool/cli/src/main.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1577      +/-   ##
==========================================
- Coverage   79.87%   75.72%   -4.16%     
==========================================
  Files         452      453       +1     
  Lines       78781    79038     +257     
  Branches    78781    79038     +257     
==========================================
- Hits        62924    59848    -3076     
- Misses      13688    17128    +3440     
+ Partials     2169     2062     -107     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 13:55 — with GitHub Actions Error
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 13:55 — with GitHub Actions Error
@nebasuke
nebasuke force-pushed the fix/solx-dwarf-attribution branch from a4d67d2 to b1de264 Compare July 25, 2026 13:57
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 13:57 — with GitHub Actions Inactive
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 14:00 — with GitHub Actions Failure
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 14:00 — with GitHub Actions Error
@nebasuke
nebasuke force-pushed the fix/solx-dwarf-attribution branch from b1de264 to a0d6af5 Compare July 25, 2026 14:26
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 14:26 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 14:29 — with GitHub Actions Inactive
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 14:29 — with GitHub Actions Failure
nebasuke added 2 commits July 25, 2026 14:39
…declaration

Since 0.1.6 solx emits DW_AT_call_line 0 for inlined subroutines whose
call site is compiler-generated code — most importantly user functions
inlined into the `__entry` dispatch. The decoder treated line 0 as
unresolvable and dropped the range from inline_call_sites, which cost
solx traces the callstack frame for the called function (solc renders
that frame at the function's declaration line).

Fall back to the abstract origin's decl_file/decl_line — the inlined
function's own declaration — when call_file/call_line are absent or
zero. DWARF gives us no decl_column, so the resolved offset sits at
the line start; skip the indentation so the location lands on the
declaration's first token, inside the function's AST span, where
get_containing_function can name it.

Proven red on the regenerated 0.1.6 fixture: the `unlucky` require of
ValidatedCounter carried no inline call site for bumpIfValid at all.
…tom frame

intermediate_frames seeded its duplicate-frame check with the raw
failing function — the containing function of the reverting
instruction's location. Under solx 0.1.6 line-0 emission that location
is the decoder's Pass-3 synthesized function declaration, so for a
revert inside a flattened modifier the seed named the modified function
while the bottom frame actually renders under the modifier (via the
revert walk-back) — and the called function's frame, just recovered
from its line-0 dispatch call site, was dropped as a "duplicate".
Result: solx traces lost the middle `Contract.function` frame that solc
renders (sweep scenarios ModifierRevertTest / NestedModifierRevertTest
went from 3 frames to 2 with solx 0.1.6).

Seed the dedup with the bottom frame's already-resolved source
reference instead, computing that entry before the intermediate frames
at each call site. Pinned by a cross-contract golden on the
StackTraceScenarios fixture: ValidatedCounterCaller -> ValidatedCounter
keeps the bumpIfValid declaration frame between the caller frame and the
modifier revert.

Sweep validation (hardhat-solx, -O1): strict parity green on 0.1.6;
still green on 0.1.4/0.1.5, so the change is inert for pre-line-0
artifacts.
@nebasuke
nebasuke force-pushed the fix/solx-dwarf-attribution branch from a0d6af5 to fb576c7 Compare July 25, 2026 15:21
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 15:21 — with GitHub Actions Inactive
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 15:23 — with GitHub Actions Failure
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 15:23 — with GitHub Actions Inactive
@nebasuke
nebasuke force-pushed the fix/solx-dwarf-attribution branch from fb576c7 to 689ff85 Compare July 25, 2026 17:10
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 17:10 — with GitHub Actions Inactive
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 17:14 — with GitHub Actions Failure
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 17:14 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 21:14 — with GitHub Actions Inactive
@nebasuke
nebasuke had a problem deploying to github-action-benchmark July 25, 2026 21:16 — with GitHub Actions Failure
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 21:16 — with GitHub Actions Inactive
@nebasuke
nebasuke requested a review from a team July 25, 2026 21:21
@nebasuke
nebasuke marked this pull request as ready for review July 25, 2026 21:21
…eferences

The display-name unification (98308f2) mapped constructor/fallback/
receive to their keywords in function_start_source_reference too, but the
hardhat-tests corpus pins the empty AST name for function-start frames —
modifier-private-method renders receive()'s callstack entry with
function "", and CI caught <receive> instead. The asymmetry between the
two source-reference builders is deliberate; documented at the site so
the next unification attempt reads why not. Verified against the corpus
locally: the failing scenario passes on both compile profiles.
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 22:06 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 22:08 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 22:08 — with GitHub Actions Inactive
@nebasuke
nebasuke requested a review from Copilot July 25, 2026 22:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates EDR’s solx (DWARF) stack-trace attribution to handle declaration-attributed and line-0 debug locations introduced/relied on by solx 0.1.6, and adds new fixtures + integration tests to pin the corrected behavior (including optimizer-mode differences).

Changes:

  • Update stack-trace inference strategy for solx to better attribute reverts/frames (declaration padding, unmapped reverts, line-0 DWARF call sites).
  • Add/refresh solx fixtures (including new StackTraceScenarios + mode-3 variant) and add a CLI subcommand to regenerate supported fixtures in-repo.
  • Expand provider-path integration tests and adjust parity-sweep pinning/docs based on improved solx parity.

Reviewed changes

Copilot reviewed 16 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
js/integration-tests/solx-parity-sweep/test/sweep.ts Removes previously pinned divergences now expected to be at parity.
js/integration-tests/solx-parity-sweep/README.md Updates documentation around divergence pinning/toolchain dependence.
crates/tool/cli/src/update.rs Improves overwrite behavior when output file doesn’t exist; fixes repo-root resolution depth.
crates/tool/cli/src/solx_fixtures.rs Adds fixture regeneration implementation for solx outputs.
crates/tool/cli/src/main.rs Exposes gen-solx-fixtures CLI command.
crates/edr_solidity/src/trace_strategy.rs Extends per-compiler trace strategy hooks for solx-specific attribution recovery.
crates/edr_solidity/src/error_inferrer.rs Integrates new strategy hooks; adds calldata decoding validation and step-PC thunking.
crates/edr_solidity/src/debug_info/dwarf.rs Handles line-0 call sites by falling back to declaration location; adds decoder test.
crates/edr_solidity/fixtures/sources/StackTraceScenarios.sol Adds new Solidity scenario contracts for provider-path stack-trace variants.
crates/edr_solidity/fixtures/solx_compiler_input*.json Pins optimizer modes and adds new fixture input JSONs.
crates/edr_solidity/fixtures/solx_compiler_output*.json Updates/regenerates solx outputs for 0.1.6 + adds new outputs.
crates/edr_solidity/fixtures/README.md Documents regeneration paths for new fixture sets.
crates/edr_provider/tests/integration/solx_stack_trace.rs Adds provider-path integration coverage for new scenarios and mode-3 variants.
.changeset/solx-declaration-attributed-inference.md Adds a patch changeset describing user-visible stack-trace fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/tool/cli/src/update.rs
Comment thread crates/tool/cli/src/solx_fixtures.rs
…g write

The create branch wrote the caller's raw contents, so a Windows regen
could commit CRLF into a brand-new fixture while every later update
normalizes. Hoist the normalization above the read so both paths write
the same bytes. (Raised by Copilot.)
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 22:59 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 23:01 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 25, 2026 23:01 — with GitHub Actions Inactive
@popescuoctavian
popescuoctavian self-requested a review July 27, 2026 09:29

@popescuoctavian popescuoctavian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM overall! Left a few minor suggestions which are not blocking. Feel free to merge without requesting another review. Thanks!

Comment thread crates/tool/cli/src/update.rs Outdated
fs::write(path, &contents)?;
return Ok(());
}
Err(error) => bail!("reading {}: {error}", path.display()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: use typed error instead of formatting it to a message (i.e. add message to context - Err(error) => Err(error).context(format!("reading {}", path.display()))?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied directly

{
let bottom_source_reference = bottom_entry
.source_reference()
.expect("CustomError always carries a source reference");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: the ErrorInferrer's convention is to use InferrerError:InvariantViolation for errors that that are are not locally-provable. I believe the same can be applied here for consistency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah thanks for the heads up, applied!

/// Lazily yields the trace's EVM step PCs in execution order. A thunk
/// because trace steps are generic over the halt-reason type, which can't
/// cross the object-safe trait boundary.
pub type StepPcs<'a> = &'a dyn Fn() -> Vec<u32>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: the type alias hides the reference. Including & in the alias means that use sites no longer read as borrows. I'd avoid this by removing the reference from the alias and adding it to the use sites to make the borrow visible (&StepPcs).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Cool, thanks for the suggestion. Applied!

nebasuke added 4 commits July 27, 2026 15:16
…ource references

The two bottom-frame source references introduced here were asserted with
expect; the invariant holds but is only provable in
solidity_stack_trace.rs, so it follows the inferrer's convention for
non-locally-provable invariants instead.
The alias hid the borrow, so the use sites read as by-value. The lifetime
parameter stays on the alias as the trait object's bound — without it the
object defaults to 'static, which the step-borrowing closures don't
satisfy.
bail! flattened it into a string, dropping the error kind and diverging
from the with_context idiom used by solx_fixtures.rs and the solidity
tool.
…io failures

update()'s writes and the fixture-input read propagated io::Errors with a
bare `?`, and io::Error's Display carries no path — a read-only fixture or
a missing input reported "Permission denied (os error 13)" with nothing
identifying the file. The update() call also gains the fixture name, since
generate()'s loop adds no context of its own.
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 27, 2026 15:28 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 27, 2026 15:39 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark July 27, 2026 15:39 — with GitHub Actions Inactive
@nebasuke
nebasuke added this pull request to the merge queue Jul 27, 2026
Merged via the queue into main with commit 440e770 Jul 27, 2026
60 of 63 checks passed
@nebasuke
nebasuke deleted the fix/solx-dwarf-attribution branch July 27, 2026 18:26
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.

3 participants