Skip to content

test(edr_solidity): consolidate all solx fixtures onto gen-solx-fixtures; retire the sweep fixture - #1574

Draft
nebasuke wants to merge 41 commits into
feat/solx-stack-trace-provider-coveragefrom
test/scenarios-fixture-port
Draft

test(edr_solidity): consolidate all solx fixtures onto gen-solx-fixtures; retire the sweep fixture#1574
nebasuke wants to merge 41 commits into
feat/solx-stack-trace-provider-coveragefrom
test/scenarios-fixture-port

Conversation

@nebasuke

Copy link
Copy Markdown
Member

Claude summary

Stacked on #1552 (review/merge that first — the first ~20 commits here are its). This PR finishes what #1552 and #1572 set up: every Rust test that pinned solx DWARF/trace behavior against the committed sweep fixture now pins it against the regenerable stack_trace_scenarios fixture instead, and the sweep fixture — with its entire regeneration apparatus — is deleted. One regen flow remains for all committed solx fixtures: cargo run -p edr_tool_cli -- gen-solx-fixtures <solx>.

Why

The repo carried two fixture regeneration flows. The gen-solx-fixtures tool (from #1552) regenerates its fixtures from committed inputs with any solx binary — no JS toolchain involved. The sweep scenarios fixture (faithful capture since #1572) needed the hardhat-solx pipeline, a pnpm regen-fixtures script, and a frozen-prefix regime: because the live Scenarios.t.sol was spliced into a fixture compiled from an older version of itself, the source was append-only, enforced by a keccak prefix-hash guard, documented in four places, with a planned CI sync test to keep source and fixture from drifting.

All of that apparatus existed to serve the fixture's Rust consumers. Moving the consumers dissolves it: the sweep itself compiles its corpus live on every run and never read the committed fixture.

What moved

  • Corpus (StackTraceScenarios.sol): ~20 new plain contracts mirroring the sweep scenarios' shapes — the require/assert/panic family (0x01/0x11/0x12/0x21/0x31/0x32), custom error, constructor reverts (direct + via internal helper), the recursion trio (external deep, internal, cross-contract mutual), internal helper chain, internal library, modifiers, fallback/receive reverts, inline-assembly revert, invalid(). Forge-std is not needed: every pinned DWARF shape is a plain contract construct — the forge harness framing (invariant/fuzz/expectRevert) never reached the DWARF and stays sweep-only.
  • edr_provider breadth tests (18): now deploy the plain equivalents from the regenerable fixture. Assertions unchanged in kind — entry variant + source line, full assert_trace_shape pins where they existed.
  • edr_solidity dwarf pins (20): all line- and PC-anchored tests re-anchored against the new fixture via the documented probe. Every predicted line and trace-shape pin held on the first run; only three PC anchors needed re-deriving (0x26 divide REVERT, 0x36 assembly REVERT, 0x87 modifier require — the last one landed on the same PC as the old corpus).

Coverage preserved deliberately

  • Cross-file inheritance: new StackTraceScenariosBase.sol with a base contract, inherited by a scenario contract in the main file. The build model resolves inherited functions through base contracts' ASTs (linearizedBaseContracts) — the exact shape that broke CI during test(solx-parity-sweep): add bare-modifier-revert and linked-library scenarios #1572's fixture cut — and a new provider test asserts an inherited function's revert resolves into the base's own source file. The gen-solx-fixtures sources table is multi-entry now.
  • Dense multi-contract compilation unit: the cross-contract line-leak guard (an INVALID opcode once picked up a different contract's line row) keeps its neighbors — the corpus is one file with ~30 contracts.

Deleted, and why that's safe

Deleted Why
solx_compiler_{input,output}_scenarios.json (7.5 MB) zero consumers left
scripts/regen-fixtures.js + pnpm regen-fixtures nothing left to regenerate
scenarios_source_is_append_only (keccak guard) the corpus is no longer spliced into a frozen fixture
scenarios_fixture_satisfies_contract_metadata_extraction it policed the regen script's closure filter; gen-solx-fixtures writes solx's verbatim output — no filter exists
pretest copy step in maybe-build.js Scenarios.t.sol now lives in the sweep project directly
pin_invariant_failure DWARF-identical to pin_direct_require once the forge invariant framing (which never reached the DWARF) is gone
nested_modifier_revert_points_at_failing_require its own doc declared it superseded by nested_modifier_revert_walks_back_from_line_zero_helper on current artifacts

This also retires the planned compare-only CI sync test from the sweep-in-CI follow-up list: there is no committed sweep artifact left to drift. The remaining Rust↔sweep coupling is intentional duplication — the same shapes exist in two independently compiled corpora, one pinned in Rust against a committed fixture, one checked live against solc for parity.

Sweep side

Scenarios.t.sol moves into the sweep as an ordinary committed source (contracts/ is no longer gitignored). Adding a sweep scenario is now: edit the file, run pnpm test — no fixture regen, no frozen prefix, no cross-crate re-pin. The sweep's own behavior is untouched: same corpus, same live compile, same divergence pins (InternalRecurseTest line references unchanged).

Notes

  • Validation: edr_solidity 77/77; edr_provider --test main solx_stack_trace 34/34; sweep 4/4 against solx 0.1.6; a fresh gen-solx-fixtures run after all changes is a zero-diff no-op (idempotent); provider suite at its known baseline (the 12 fork tests need ALCHEMY_URL).
  • Like test(edr_provider): broaden solx provider-path stack-trace coverage #1552, the fixture is generated at solx 0.1.6 with hardhat-solx's -O1, so this rides the pending SOLIDITY_TO_SOLX_VERSION_MAP 0.8.34 → 0.1.6 bump.
  • Test-only: no changeset needed.

nebasuke added 30 commits July 22, 2026 21:28
Scenarios.t.sol is spliced at test time into the committed scenarios
fixture, which was compiled once (solx 0.1.4) and is not regenerable
in-repo — its AST and DWARF reference byte offsets into the source as
it existed at generation time. The file is therefore append-only, but
nothing enforced that: an edit inside the frozen prefix (a rename, a
formatter run) shifts offsets and desyncs every line assertion on the
fixture, in the worst case to plausible-but-wrong locations.

Pin the frozen prefix (length + keccak) with a test whose failure
message states the rule. Appending new sweep scenarios keeps it green.
…scenarios

Two sweep scenarios for surfaces the corpus missed, appended to
Scenarios.t.sol (the frozen-fixture prefix is untouched, guarded by
scenarios_source_is_append_only):

- BareModifierRevertTest: a modifier's bare `revert()` compiles to a
  shared helper that is unmapped in the DWARF and returns no data, so
  every revert heuristic misses. Pinned in scenariosDivergingFromSolc
  at today's shape: the target contract's frames vanish and the trace
  collapses to the test's own call site with the raw EvmError, where
  solc reports the `revert()` statement with a 3-frame stack. A fix is
  queued in #1552 (selector-based recovery + revert walk-back); when it
  lands this golden breaks in the improvement direction and the pin
  shrinks to the one-line guard-vs-revert residual (solx line-table
  merge, WONTFIX solx-side).
- ExternalLibraryRevertTest: `public` library function, so the call is
  a DELEGATECALL into a separately deployed, link-referenced library
  and the frame must resolve inside the library's own debug info —
  unlike RevertingLib, whose `internal` function inlines into the
  caller. At parity with solc today; joins the strict check as a
  regression net.

Verified against solx 0.1.4 (hardhat-solx's pin): 4/4 sweep tests green.
The regeneration tool lives in #1552, which is not on main yet; state
the fixture's provenance (forge-std sources scrubbed from the committed
input) directly instead.
…ucibility

The "not regenerable" folklore around the scenarios fixture was
overstated: verified by round-trip that forge-std's v1.14.0 GitHub tag
(the npm registry package is stale) + this file's frozen prefix + the
solx 0.1.4 release binary reproduce all 90 committed bytecode objects
byte-identically, metadata hashes included. The debugInfo payloads
differ only because solx embeds the build directory in the DWARF
(solx#594); until that is fixed, regeneration is machine-dependent and
the fixture stays frozen with the source append-only.

Ship the verification as scripts/reproduce-scenarios-fixture.sh and
document the scenario-adding workflow (append-only rule + guard test,
dynamic key discovery, divergence-table pinning etiquette).
…he standard flow

The scenarios fixture is not a special artifact needing an out-of-band
reconstruction ritual — it is this project's own build output (the
build-info pair from a solx-profile hardhat compile, forge-std contents
blanked before committing). What that compile produces depends on the
toolchain of the day: hardhat-solx's settings defaults and the solx
release selected by SOLIDITY_TO_SOLX_VERSION_MAP. The committed
snapshot predated hardhat#8419's optimizer-default fix, so the standard
flow no longer reproduced it.

Resolve that by regenerating the fixture through the standard flow with
the fixed plugin (solx 0.1.4 via the current version map, explicit -O1,
now including this PR's scenario contracts) instead of enshrining the
stale snapshot, and re-pin what the new codegen moved: the four
PC-anchored dwarf tests (the INVALID-opcode one now finds its
instruction by opcode instead of PC) and the source-prefix guard's
constants. All other pins hold, including the three provider goldens.
The guard's contract is restated for the regen era: append freely for
sweep-only scenarios; edits inside the generation-time prefix mean
regenerate + re-pin + update the constants.

Drop scripts/reproduce-scenarios-fixture.sh accordingly; the README's
provenance section now documents the standard flow, the version-map
dependency (0.8.34 -> 0.1.4 today, 0.1.6 bump planned), and the
solx#594 caveat (embedded build dir) for regenerators.
One place answering, per committed compiler artifact: what it is, what
generates it, and how to regenerate it — plus the conventions that were
previously only discoverable by archaeology (source splicing, the CBOR
solcx version stamp, the solx#594 debugInfo path caveat). Opens with the
rule the scenarios-fixture investigation earned: regenerate through the
documented flow and diff; never reconstruct out-of-band.
…rgences table

Deferred until #1571's table rework landed to avoid conflicting.
…ios source

The tests deserialize only `sources` and `contracts` and read only
`project/contracts/Scenarios.t.sol`'s section; the rest of the verbatim
compiler output was ~40 MB of forge-std artifacts (safeconsole.sol's AST
alone is 7.6 MB). Keep only the section the tests read: 42.6 MB -> 2.9 MB
on disk, fixture parse time 6.5 s -> 0.2 s for the 78-test suite.

The regeneration command in the sweep README gains the corresponding jq
filter; the committed file remains byte-for-byte that command's output.
With a hand-maintained GENERATION_PREFIX_LEN, a regeneration that forgot
to update the constants stayed green while the guard silently
under-covered the newly frozen suffix. The generation-time length is
already recorded in the fixture itself — the end of the SourceUnit's
`src` span — so read it from there: a regeneration now moves the hashed
prefix automatically and forces the remaining keccak constant to be
re-pinned (the failing assertion prints the new value).
`pnpm regen-fixtures` replaces the copy-pasted jq commands: it wipes
`artifacts/build-info` (so exactly one fresh pair exists), compiles the
solx profile, and writes the extracted pair to
`crates/edr_solidity/fixtures/`. Pure Node — no jq dependency;
`JSON.stringify(_, null, 2)` reproduces the committed bytes exactly
(verified: running the script leaves a clean tree).

Deliberately NOT part of `pnpm test`: a test run that rewrites the
committed fixtures would absorb toolchain drift instead of detecting
it, and test runs should not dirty the tree. A compare-only sync test
can gate this in CI once hardhat-solx is published and the sweep runs
there.
Same rules, plainer voice: say who uses the fixtures, lead with "don't
hand-edit, regenerate", and phrase the toolchain-drift advice as a
troubleshooting note instead of a directive.
…tures

Fresh-eyes review of the script found three quiet failure modes and one
hedge:

- `contracts/` is gitignored AND hardhat's sources dir, so a stray
  leftover .sol was silently compiled into the committed input (its
  blanked content easy to miss in review). Rebuild the dir from scratch
  instead of only overwriting Scenarios.t.sol.
- A missing scenarios section in the compile output was silently
  dropped by JSON.stringify and committed as `{}`, deferring the
  failure to the Rust suite. Throw instead.
- Junk files (.DS_Store) in build-info tripped the exactly-one-pair
  check spuriously; count only .json files.
- The closing message hedged ("if the fixtures changed..."); ask git
  and say "nothing to re-pin" or "re-pin now". This also makes the
  script a manual drift check: run it on a clean tree — a dirty tree
  means the committed fixtures were stale.
…ommand

Command and re-pin instruction first for the dev who just needs the
flow; the toolchain/filter/splice details move to a "when a
regeneration surprises you" list below. Same facts, reordered.
The one step of a codegen-changing regeneration that was still tribal
knowledge (and the one that burned the fixture-provenance session):
probe with decode_deployed_for + opcode filter under --nocapture, and
appends never move sibling PCs, so this only applies on toolchain
changes.
…ance closure

The leaf-only cut in 76588e7 broke edr_provider's solx_stack_trace
tests on CI: EDR's build model resolves forge-std-inherited functions
(StdInvariant's targetSelectors() etc.) through the BASE contracts'
ASTs via linearizedBaseContracts, and with those stripped the selector
fix-up in correct_selectors hard-errors. The fixture's real invariant
is that `sources` and `contracts` cover the same inheritance closure,
not just the leaf source.

regen-fixtures now computes the closure from the scenarios AST's
linearizedBaseContracts, keeping Scenarios.t.sol + 7 forge-std bases:
7.6 MB (vs 42.6 MB verbatim, 2.9 MB broken leaf-only), and the
console/safeconsole/Vm ASTs it drops are read by nothing. The
scenarios file's own section is byte-identical to the previous
fixture, so every PC pin and the guard hash hold.

Verified: edr_solidity 78/78; edr_provider solx_stack_trace 3/3 (the
two CI failures pass). README: closure invariant documented, both
consumers added to the re-pin instructions, and a note to verify the
CBOR solcx stamp after regenerating — a locally linked hardhat-solx
build can silently bring a different version map.
…onsumer

The closure filter in the sweep's regen script encoded a Rust-domain
invariant (what the build model reads) on the wrong side of the
language boundary. Invert the ownership: a new test runs
extract_solx_contract_metadata — the function that failed on CI — over
the committed scenarios fixture, so any over-filtered regen fails
`cargo test -p edr_solidity` directly, in the fixture's own crate,
with the real error. The JS filter is thereby demoted to a pure size
optimization; nothing re-describes the build model's reads anywhere.

Red-proven against the leaf-only fixture from the pushed tip: the test
reproduces CI's exact "Failed to fix up the selector for ...
DirectRequireTest" error. Green on the closure fixture (79 tests).
Extends solx_stack_trace.rs from 3 to 21 tests over the existing scenarios
fixture: panic sub-codes (0x01/0x12/0x21/0x31/0x32), CREATE-path reverts
(plain + internal-helper constructor), modifier bodies, cross-contract and
mutually-recursive CALL chains, external recursion frame counts, internal
helpers/recursion, internal libraries, fallback/receive bodies, and
per-statement line discrimination between requires.

Assertions stay at entry-variant + source line; frame-shape parity with
solc remains the JS sweep's job. One golden divergence pinned: solx
attributes a multi-statement modifier revert to the function declaration
line (420) instead of the failing require (415), the provider-level twin
of the sweep's NestedModifierRevertTest divergence.

Also adds brief_trace so assertion failures print one line per entry
instead of dumping the embedded source_content per SourceReference.
…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).
…ifacts

Adds a LongTail fixture (plain contracts, no forge-std) targeting the
dispatch-level and call-shape StackTraceEntry variants the solc corpus
covers but no solx test reached: FunctionNotPayable,
UnrecognizedFunctionWithoutFallback, MissingFallbackOrReceive, both
FallbackNotPayable variants, InvalidParams (truncated calldata), plus an
externally linked library exercising linkReferences substitution and
DELEGATECALL decoding into the library's own debugInfo, and
DirectLibraryCall.

Two golden inference gaps documented: where the solc route infers
ReturndataSizeError and NoncontractAccountCalledError at the call site,
the DWARF route currently degrades both to OtherExecutionError attributed
to the contract declaration line. The pins flip loudly if inference
improves.

Also adds a gen-solx-fixtures subcommand to edr_tool_cli that regenerates
the self-contained fixture outputs (counter, long_tail) against a given
solx binary: it splices the fixtures/sources/ contents into the committed
input JSON (whose content fields are deliberately empty), runs
solx --standard-json, and rewrites the output JSON with solx's verbatim
(minified) output. The committed long_tail output is produced by the
subcommand with solx 0.1.5. The scenarios fixture is not regenerable from
the repo (forge-std contents are scrubbed) and stays hardhat-generated;
decode is comp_dir-independent, verified by running the counter test
against a locally regenerated blob.
…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 (goldens flipped in solx_stack_trace.rs):
- 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
  (415) instead of the function declaration line (420), removing the
  provider-level twin of the sweep's NestedModifierRevertTest divergence.

The solc strategy is behavior-identical; edr_solidity unit tests (77) and
the edr_provider integration harness (198) pass.
cargo run -p edr_tool_cli -- gen-solx-fixtures <solx-0.1.5-binary>

The deployed bytecode changes, but every trace golden holds: edr_solidity
unit tests (77) and the solx provider suite (31) pass unchanged. The
scenarios fixture remains as generated by hardhat-solx (not regenerable
from the repo).
Review feedback: 'long tail' described the coverage gap the fixture was
created to fill, not its contents. StackTraceScenarios names what it is —
the self-contained, regenerable counterpart to Scenarios.t.sol for
provider-path stack-trace tests. Fixture regenerated via gen-solx-fixtures
(the source name is embedded in the compiler output and DWARF).
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.

New GuardedBareRevert scenario (a modifier's bare revert() — no message-
building code keeps its own line): empirically the DWARF route attributes
the whole helper to the contract declaration, whose location has no
containing function, so inference degrades to OtherExecutionError at the
contract-declaration line before revert_source_reference is consulted
(solc reports RevertError at the revert() line). Pinned as a golden gap;
fixing it needs failing-function resolution for containing-function-less
locations, out of scope here. No existing golden changed.
…rary

Review finding: the helper substituted every placeholder with the single
provided address, silently mis-linking bytecode that references more than
one distinct library. Same-library placeholders legitimately repeat, so
require all placeholders to share one hash and bail otherwise.
Review finding: run_solx wrote the whole input before reading stdout,
which deadlocks if the input exceeds the OS pipe buffer while the child
is already filling its stdout pipe. Standard-json compilers read stdin to
EOF before emitting output, so this cannot fire today — but fixtures grow
and the fix is a scoped thread.
A modifier's bare revert() compiles to a shared helper that solx leaves
entirely unmapped (no DWARF line at all — probing corrected the earlier
contract-declaration reading), 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).

Golden flip: the GuardedBareRevert scenario now reports RevertError at
the guard condition (line 67, inside the modifier) instead of
OtherExecutionError at the contract declaration. solc reports the
revert() statement itself (line 68); that last line is absent from the
DWARF, so closing the residual delta is solx line-table fidelity, not
inferrable EDR-side.
… solx 0.1.6

Deployed and creation bytecode are identical modulo the trailing metadata
hash; only the DWARF debugInfo payloads change (line 0 for
compiler-generated code, solx#582; Yul AST refs for inline assembly,
solx#583). All 32 provider goldens and 77 edr_solidity tests pass
unchanged: line-0 rows decode to `line: None`, and the decoder's Pass-3
function-AST fallback feeds the same declaration-attributed walk-back as
before.

The `scenarios` fixture stays on its original solx 0.1.4 (per its CBOR
`solcx` stamp) — not regenerable in-repo, see `solx_fixtures.rs`.
…rtifacts

The declaration-attributed walk-back trigger was expected to become
legacy-only once solx emitted line 0 for compiler-generated code
(solx#582). It does not: line-0 rows decode to `line: None`, Pass 2
skips them, and the decoder's Pass-3 fallback synthesizes the failing
function's AST location — declaration-level — so the same trigger is the
permanent consumer path for current artifacts. Prove it with a
`ValidatedCounter` scenario in the regenerable fixture: the flattened
`validates` modifier's revert helper walks back to the `require` that
fired, matching solc, on 0.1.6 output.

The pre-0.1.6 raw declaration-line attribution stays covered by
`nested_modifier_revert_points_at_failing_require` (scenarios fixture,
frozen on its original solx 0.1.4). Strategy comments updated to name
both sources of declaration-level padding.
…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.
nebasuke added 11 commits July 23, 2026 10:31
…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. Covered by a cross-contract golden on the
regenerated 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.
The regenerable fixture inputs relied on solx's default optimization
level. Trace shapes differ per mode (the 0.1.6 inlined-frame loss
reproduced differently at -O1 vs the default), so a change to solx's
default would silently change what the fixtures test on the next
regeneration. Pin settings.optimizer.mode to "3" — byte-identical to
today's default output, verified by a zero-diff regen.
The goldens assert entry variant + source line; frame-shape parity was
delegated entirely to the JS sweep, which is not in CI and runs at a
different optimizer mode and solx version — the 0.1.6 inlined-frame
loss passed all 32 goldens. Add assert_trace_shape (brief_trace
equality) to four representative shapes: cross-contract call,
cross-contract modifier revert (the recovered middle frame), external
recursion, and a linked-library DELEGATECALL. A gained frame reads as
an improvement to re-pin; a lost frame is a regression.
With the bare-revert attribution fix in this PR and solx 0.1.6's
statement-line mapping for assembly opcodes (solx#583),
BareModifierRevertTest, InlineAssemblyRevertTest and InvalidOpcodeTest
all match solc exactly; only InternalRecurseTest remains pinned. The
pin set now assumes hardhat-solx maps 0.8.34 to solx 0.1.6.
…-solx-fixtures

The fixtures README predates this PR's edr_tool_cli generator: the
Counter row still described the manual solx --standard-json flow and
the stack_trace_scenarios pair had no row at all.
…0.1.6

pnpm regen-fixtures against hardhat-solx's 0.1.6 map; the input pair is
byte-identical, only the compiled output moved. Re-pins in the same
commit, per the sweep README: solx 0.1.6 emits .debug_line rows for
assembly opcodes (solx#583), so the inline-assembly REVERT and the
invalid() INVALID now resolve to their statement lines (135, 183)
instead of the function-decl fall-back. PC anchors verified unchanged
via the documented decode_deployed_for probe.
…-solx's -O1

The gen-solx-fixtures inputs pinned optimizer mode "3" (solx's own
default); the pipeline they stand in for compiles at hardhat-solx's
-O1 default. Flip the pin to "1" and regenerate at solx 0.1.6. One
golden improves: the bare-modifier-revert bottom frame lands on the
revert() statement (line 68) instead of stopping at the guard
condition (67) — that residual was a mode-3-only shape (shape 4 of
the solx line-attribution issue). Coverage of other optimizer modes
is deliberately deferred.
…shapes

Plain-contract equivalents of every Scenarios.t.sol shape whose Rust
tests will move off the frozen sweep fixture: the require/assert/panic
family, custom error, constructor paths, recursion trio, helper chains,
internal library, modifiers, fallback/receive, assembly revert and
invalid(). Adds a second source file with a base contract so the
multi-source inheritance resolution (linearizedBaseContracts across
files) stays exercised by a regenerable fixture. Existing line pins are
unshifted; all 80 + 34 tests hold on the regenerated output.
…ture

Every breadth test now runs against the regenerable stack-trace
fixture's plain-contract equivalents; the sweep fixture has no provider
consumers left. Dropped nested_modifier_revert_points_at_failing_require
— its doc already declared it superseded by
nested_modifier_revert_walks_back_from_line_zero_helper on current
artifacts. Added inherited_function_revert_points_into_the_base_source_file
to keep cross-file inheritance resolution (linearizedBaseContracts)
covered end-to-end now that the forge-std closure is gone.
All PC- and line-anchored dwarf tests now decode the regenerable
stack-trace fixture; anchors re-derived via the documented probe. The
two apparatus tests go with the dependency: scenarios_source_is_append_only
(no frozen fixture left to splice into) and
scenarios_fixture_satisfies_contract_metadata_extraction (gen-solx-fixtures
writes verbatim output — there is no filter left to police).
pin_invariant_failure is dropped as a duplicate of pin_direct_require:
its forge invariant framing was the only difference, and that never
reached the DWARF.
…apparatus

With every Rust consumer on the gen-solx-fixtures pair, the committed
sweep fixture had no readers left: the sweep compiles its corpus live
on every run and never read it. Deleted: the 7.5MB fixture pair,
scripts/regen-fixtures.js, and the pretest copy step. Scenarios.t.sol
moves into the sweep project as an ordinary committed source — the
frozen-prefix/append-only regime existed only because the file was
spliced into a fixture compiled from an older version of itself.
Docs collapse to one regen flow (gen-solx-fixtures); the PC-anchor
probe how-to moves to the fixtures index next to the tests it serves.
@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4a205a8

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

@nebasuke nebasuke added the no changeset needed This PR doesn't require a changeset label Jul 23, 2026
@nebasuke
nebasuke force-pushed the feat/solx-stack-trace-provider-coverage branch 11 times, most recently from 601e5e3 to 099d9c8 Compare July 28, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changeset needed This PR doesn't require a changeset

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant