Skip to content

Support verifying a library main contract via a generated harness - #132

Open
shellygr wants to merge 13 commits into
masterfrom
shelly/library-harness
Open

Support verifying a library main contract via a generated harness#132
shellygr wants to merge 13 commits into
masterfrom
shelly/library-harness

Conversation

@shellygr

@shellygr shellygr commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Why

The Prover cannot verify a library as the verification target, and fails silently — there is no error anywhere in certora-cli or the JVM. CalculateMethodParamFilters.kt:182 filters libraries out of parametric rules, so the run reaches the prover, instantiates zero methods, and comes back vacuous.

The Prover itself prescribes the fix: "Calling Library functions from spec is not supported. Use a harness function that calls the library one if needed." This automates it.

How

New certora_autosetup/harnesser/ package. Everything completes before Autosetup.run starts:

detect contractKind == library
  → emit a stub harness in certora/harnesses/
  → probe-build it WITH the library in `files`
  → read the library's allMethods
  → refill the harness, drop the placeholder
  → swap the main contract
AutoSetup then runs once, normally.

Three findings shaped this, each verified against real builds:

  • The library must be listed in the conf's files. An imported-but-unused library is not compiled as its own contract — the build reports its structs and none of its functions.
  • The API comes from allMethods, not internalFunctions. The latter is autofinder instrumentation, and certora-cli skips autofinders for library-hosted functions (certoraBuild.py:2751), so it is systematically empty for libraries. Reading it yields zero wrappers for a library whose surface is all-internal.
  • The fill is not re-entrant into autosetup. setup_prover.py:327 re-seeds from the base build-system dict, so a second run_compilation_analysis would discard every workaround from the first; and a swap inside setup_prover cannot propagate (it returns no handle, ContractHandle is frozen).

Detection uses solc --standard-json with stopAfter: "parsing" — real AST, no build, no import resolution, milliseconds. It keys on contractKind, never on a message, since there is no message.

Codegen

Storage receivers become harness-owned state; CVL keywords are escaped before ABI-collision mangling (atat_, then suffixed); collision suffixes come from the dropped receiver keyed positionally (overloads share a name); storage readers reach through nested structs and mappings; in-place memory mutators get a synthesized return.

Skipped and reported, not silently dropped: private, storage-pointer returns, internal-only types, opaque bytes32 ptr handles. Zero surviving wrappers is a hard error — a method-less target proves nothing.

Coverage

Matches hardhat-exposed's independent counts where they overlap:

Library Wrapped
OZ Math 14/14
OZ EnumerableSet 18/24 6 private
OZ Checkpoints 20/33 incl. the function-type push
OZ StorageSlot refused all 8 return storage pointers
Solady LibBitmap 10/10
Solady LibSort 57/68 in-place mutators
Solady EnumerableSetLib 45/51 both utils/ and g/

Validation

Full local autosetup on OZ EnumerableSet: emits a conf whose verify and parametric_contracts are CertoraLibraryHarness_EnumerableSet, typechecker passes, local prover run succeeds, certoraRun --compilation_steps_only exits 0. A/B against OpenZeppelin's hand-written EnumerableSetHarness.sol — the generated names now read add/remove/contains/at_Bytes32Set/Bytes32Set_inner_indexes against the human's add/remove/contains/at_/_indexOf; the remaining differences are the two extra set types the human harness does not cover.

Tests live in the Autosetup repo (29 unit tests over real trimmed build fixtures); needs a submodule pin bump there once this merges.

Known limitation

Solady's EnumerableSetLib sets are struct { uint256 _spacer; } with assembly-derived slots, so the owned state variable may not model where the data actually lives. Treat its 45/51 as unproven until a prover run confirms it.

🤖 Generated with Claude Code

shellygr and others added 3 commits August 7, 2026 20:56
The Prover skips libraries when instantiating parametric rules, so verifying a
library as the main contract silently proves nothing. There is no error to key
on; the run just comes back vacuous.

certora_autosetup/harnesser generates a plain contract exposing one public
wrapper per library function, which the caller verifies instead:

- detect: solc --standard-json with stopAfter "parsing" reads contractKind
  without resolving imports or building, so detection costs milliseconds.
- run: emit a placeholder harness, probe-build it alongside the library, read
  the library's API, then refill the file. The library is listed in the build's
  files explicitly, because an imported-but-unused library is not compiled as
  its own contract and the build then reports none of its functions. The
  placeholder carries one external function since a method-less contract is
  dropped by contract discovery and by the signature database.
- read_build: the API comes from allMethods (external + internal + private),
  not internalFunctions, which holds autofinder instrumentation and is
  systematically empty for libraries. A library is located by (name, file):
  Solady ships 17 library names twice with differently scoped structs.
- plan: classify each function, own a state variable per storage receiver,
  escape CVL keywords before mangling ABI collisions, derive storage readers,
  and synthesize a return for in-place memory mutators.

render_wrapper_contract grows a parentless mode (a library cannot be a base
contract) and a slot for file-scoped pragmas.

Measured against the corpora, matching hardhat-exposed's independent counts
where they overlap: OZ Math 14/14, EnumerableSet 18/24, Checkpoints 20/33,
StorageSlot refused (all 8 return storage pointers); Solady LibBitmap 10/10,
LibSort 57/68, EnumerableSetLib 45/51.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both entry points swap the main contract before anything derives state from
its name. The name reaches the sanity spec, the base conf, the verify target
and the result keys, so swapping later would leave those naming a contract the
Prover instantiates no methods against.

- autosetup CLI swaps right after the main handle is parsed.
- composer swaps ahead of SourceFields, so component analysis, CVL authoring
  and the conf's verify target all agree on one name — AutoSetup keys its
  returned summary and config by that name too.

The library stays in the scene beside the harness: the wrappers call into it,
and the build only reports a library's own functions when it is named as a
file in its own right.

The LLM harness agent owns certora/harnesses and rewrites every entry it is
given, so it now skips files carrying the generated-harness sentinel — that
file is the verification target, not a wrapper to be improved on.

Verified end to end: autosetup on OZ EnumerableSet emits a conf whose verify
is CertoraLibraryHarness_EnumerableSet and whose files list both the harness
and the library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every storage receiver belongs to the library being harnessed, so repeating its
name in each identifier only made them long: at_EnumerableSet_Bytes32Set and
certoraStore_EnumerableSet_Bytes32Set_inner_indexes where the hand-written
harness beside it writes at_ and _indexOf.

Names now read at_Bytes32Set, length_Bytes32Set, _certoraStore_Bytes32Set and
Bytes32Set_inner_indexes. A type from outside the library keeps its qualifier,
which is what still tells it apart. Readers are named for the type and member
path rather than the state variable, so they read as accessors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread certora_autosetup/harnesser/cli.py Outdated
from certora_autosetup.harnesser.run import ensure_library_harness


def _split_target(target: str) -> tuple[str, str]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

to Claude: I'm surprised we don't have such a utility function yet

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

+1 on this question

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

btw, autosetup uses the ContractHandle class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: we do. split_contract_spec in utils/contract_utils.py, and _split_target was a near duplicate of it. The CLI calls it now, and --extra-file goes through parse_contract_files, so a typo fails at parse time instead of as a probe build error minutes later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: it does exist, split_contract_spec in utils/contract_utils.py. The CLI uses it now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: done. ContractHandle carries the library and the harness through cli, run, plan, model and swap, so the (file, name) string pairs are gone.

Two places kept their current shape on purpose: the manifest still writes four flat JSON keys, because swap.library_behind_harness reads it back in a later process, and the probe build still spells the explicit :Name, which is what tells apart a file declaring several libraries.

@shellygr
shellygr requested a review from jar-ben August 30, 2026 16:13
@shellygr

Copy link
Copy Markdown
Contributor Author

@jar-ben before I push forward on this - does this conflict with your work on symbolic modeling? if yes maybe we should discuss merging in together

shellygr and others added 2 commits September 5, 2026 14:33
A reader's key and leaf types come from struct members, and a member declaration
carries no location to copy, so the location has to come from the type itself.
Without it a bytes or string leaf renders as `returns (bytes)` and the harness
does not compile. Seen on OpenZeppelin's EnumerableSet, whose BytesSet and
StringSet reach exactly that path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Left to itself certora-cli concatenates package.json with remappings.txt and
refuses the build on any key present in both. That is the normal state of a
project whose remappings were generated with node_modules installed, so the
probe build failed on OpenZeppelin while every other build in the run succeeded.

AutoSetup already merges the four remapping sources with a priority order. The
probe build is the one build that runs before AutoSetup, so it has to do the
same merge itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shellygr
shellygr marked this pull request as ready for review September 5, 2026 14:36
shellygr and others added 4 commits September 5, 2026 17:56
The Prover's scene is the files the conf names. solc inlines the library into
the harness, so the build succeeds while the library itself is absent: curated
summaries that name it cannot typecheck, and the build reports none of its
functions.

The library now joins the run's additional contracts. AutoProver's pipeline
swaps in its own process, so on that path AutoSetup sees a contract that is not
a library and has nothing to detect; the manifest written beside the harness is
what still names the library, and `library_behind_harness` reads it back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both sides add to the CLI right after the main handle is parsed. The scope check
runs first, on the contract the caller named, since that is the one
auto-detection can drop; the swap then appends the harness and skips whatever is
already in the scene.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A summary replaces the code it summarizes, and the harness exists so that library
can be verified. Summarizing it means every rule is asserted against the summary
while the library itself goes unverified.

The curated half fails loudly today: OpenZeppelin's BitMaps summary reroutes to a
companion contract and its spec does not typecheck. The LLM half would fail
silently — its non-linear-ops recipe targets exactly the internal pure functions
an arithmetic library is made of, so a harnessed Math-shaped library would come
back green having proved nothing.

Curated keys naming the library are subtracted after the match loop rather than
skipped inside the matcher: the matcher also returns the tuples that shield those
same methods from the LLM step, and skipping earlier would drop the summary and
hand the methods to the LLM instead. Dependencies still summarize; only the
library under verification is exempt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
certoraRun rejects the whole conf on a repeated entry in `files`, and the two
sources merged there legitimately overlap: the harnessed library belongs to the
scene, which covers the ordinary path, and to the additional contracts, which
cover the path where `files` is rewritten from the main contract plus additional
contracts.

Deduped on the (file, contract) pair rather than the string, since to_config_str
drops the contract name when it matches the file stem and so gives one contract
two spellings. Two libraries sharing a source file stay two entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread certora_autosetup/harnesser/cli.py Outdated
Comment on lines +3 to +5
AutoProver invokes this as a subprocess and reads the JSON record from the file named by
``--output``, so the Solidity generation stays on the autosetup side while the decision
to swap the main contract stays with the caller. The result goes to a file rather than

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Don't talk about AutoProver here. That's just one of the possible consumers of this tool. Just describe what the tool does and how to use it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: done. The docstring now says what the tool does and shows the invocation, with no consumer named.

Comment thread certora_autosetup/harnesser/cli.py Outdated
from certora_autosetup.harnesser.run import ensure_library_harness


def _split_target(target: str) -> tuple[str, str]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

+1 on this question

Comment thread certora_autosetup/harnesser/cli.py Outdated
from certora_autosetup.harnesser.run import ensure_library_harness


def _split_target(target: str) -> tuple[str, str]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

btw, autosetup uses the ContractHandle class


#: Identifier-shaped terminals of the CVL grammar. Operators and punctuation are
#: omitted: they cannot collide with a Solidity function name.
CVL_RESERVED_WORDS: FrozenSet[str] = frozenset(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Double check we don't have this defined somewhere in AutoProver yet. I am sure I saw a list like this (perhaps just a sublist, but if yes, it could be good to centralise similar frozen lists / enums to a single location)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: you did see it. CVL_RESERVED_WORDS was already in setup/setup_summaries.py with 60 words, and this branch added a second one of 82 under the same name. Both are gone; there is one list now, in utils/cvl_keywords.py.

The 22 extra words were also wrong. 20 of them are the grammar's usable_keywords, which CVL accepts wherever an identifier is expected. I checked with certoraRun --compilation_steps_only: a contract declaring exists, sum, old, forall and invariant typechecks, the same shape with havoc or rule is a syntax error. No generated name changes as a result, since at and sort are the only escapes real libraries trigger.

return None


def _parse_only_ast(solc: str, source_file: Path, content: str) -> Optional[Dict]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about we reuse (or create if not existing) an AST manipulation utils in Autoprover? This seems like a pretty common function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: moved to solidity_ast/contracts.py. It is now the one place that turns a ContractDefinition into name, kind, abstract and bases, for both the post-build .asts.json stream and this pre-build probe, and setup_prover's private _ContractDeclView / _iter_contract_declarations moved there with it. Kinds come back as ContractKind instead of raw strings.

One thing I could not do is read the probe's AST through the typed models. A stopAfter: "parsing" AST carries no analysis-phase fields (no scope, no linearizedBaseContracts, no fullyImplemented), so SourceUnit.model_validate fails on it with 16 errors on a two-declaration file. That entry point stays on raw nodes, and the module says why.

BUILD_JSON_RELPATH = Path(".certora_internal/latest/.certora_build.json")


def _iter_contracts(build_data: Dict[str, Any]) -> Iterator[Dict[str, Any]]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am almost sure I have seen this somewhere. again seems like a shared util

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: you have. That loop was written five times. It is now utils/build_json.py (build_json_path, iter_contracts, contract_source_file), used by setup_prover in four places, by signature_manager, by compile_check and here. The path .certora_internal/latest/.certora_build.json was hardcoded in three of them and is now in one, with compile_check's newest-run-dir fallback folded in.


def library_behind_harness(
project_root: Path, main_contract_handle: ContractHandle
) -> Optional[ContractHandle]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So, I wanted to suggest to use ContractHandle already above (perhaps in cli.py) when you had a separated library name and path, but then thought that "well, it's not a contract so perhaps ContractName is not the right naming". But here, you use it. so use it everywhere perhaps.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

btw I know you have to use it here because it's the autosetup's interface. but the argument still mostly stands

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: done, it is ContractHandle through the harnesser now, starting at the CLI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: agreed, and the argument held. See the reply above.

Comment thread certora_autosetup/harnesser/swap.py Outdated
return list(additional_contracts)
# Compare on the (file, name) pair rather than the string: ``to_config_str`` drops the
# name when it matches the file stem, so one contract has two spellings.
already = {split_contract_spec(spec) for spec in additional_contracts}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ContractHandle would solve this? maybe

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: yes. The dedup builds handles and compares those, rather than comparing (path, name) tuples.

Comment thread certora_autosetup/setup/setup_prover.py Outdated
[ch.source_file for ch in surviving_contracts], main_contract_name
[ch.source_file for ch in surviving_contracts],
main_contract_name,
harnessed_library=harnessed_library.contract_name if harnessed_library else None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

wrongly named parameter. Instead of harnessed_library, we should call it excluded_library or something like that. Or perhaps not even library, just excluded. This seems to be a generic filter; the fact that we apply it on the harness library is just a matter of the consumer of run_setup_summaries. Similarly, drop the "harness" specific naming and comments in certora_autosetup/setup/setup_summaries.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: renamed to excluded_library, with _excluded_library_keys and _excluded_library_methods, and the harness-specific comments in setup_summaries.py now describe the filter instead of the caller. I kept library in the name because the filter selects curated summary keys by library name.

setup_prover keeps its harness wording at the call site, since that file calls library_behind_harness and legitimately knows what it is excluding and why.

ctor_forward: Optional[Tuple[str, List[str]]],
body_blocks: Optional[List[str]] = None,
header_comment_lines: Optional[List[str]] = None,
extra_pragma_lines: Optional[List[str]] = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

how much is this change related to this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: half of it belongs and half did not. parent_name: Optional[str] is needed here: a library cannot be a base contract, so the harness holds it at arm's length and passes None, which without this change renders contract X is None {. extra_pragma_lines was speculative, nothing ever populated it, and it is gone from the branch.

shellygr and others added 2 commits September 6, 2026 22:43
Two constants named CVL_RESERVED_WORDS existed with different contents, so an import
could pick either. The harnesser's copy also escaped the 20 usable_keywords terminals,
which CVL accepts wherever an identifier is expected: a contract declaring exists, sum,
old, forall and invariant typechecks, while havoc or rule does not.

No generated name changes from this. `at` and `sort` are the only escapes the libraries
we harness actually trigger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four things it duplicated now live where the rest of the repo can reach them:

- solidity_ast/contracts.py answers "what contracts does this source declare, and of
  what kind", for both the post-build .asts.json stream (setup_prover's private
  _ContractDeclView moves here) and the pre-build solc probe. Kinds come back as
  ContractKind rather than raw strings. The probe keeps raw nodes: a stopAfter parsing
  AST has no scope, linearizedBaseContracts or fullyImplemented, so the typed models
  reject it.
- utils/build_json.py holds the walk over .certora_build.json, which was written five
  times, and the path to the file, which was hardcoded in three. setup_prover,
  signature_manager, compile_check and read_build all go through it.
- ContractHandle replaces the (file, name) string pairs through the harnesser. The
  manifest keeps its flat keys, since swap reads it back in a later process, and the
  probe build keeps the explicit :Name that tells apart a file declaring several
  libraries.
- The summary filter is excluded_library, not harnessed_library. Excluding a library
  from summarization has nothing to do with harnesses; that is just who calls it.

Also drops extra_pragma_lines from render_wrapper_contract. It was plumbed end to end
and never populated.

Generated output is unchanged: regenerating the SafeCast harness gives a byte-identical
file and the same manifest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shellygr

shellygr commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Claude answers: the review is addressed, in two commits, with a reply on each thread.

c036c786 puts the CVL keyword list in one place. There were two constants named CVL_RESERVED_WORDS, and the harnesser's copy also escaped the 20 usable_keywords terminals, which CVL accepts where an identifier is expected.

71389a8c moves out what the harnesser had written for itself: solidity_ast/contracts.py for reading contract declarations and their kind, utils/build_json.py for the walk over .certora_build.json that was written five times, ContractHandle in place of the (file, name) string pairs, and excluded_library instead of harnessed_library. It also drops extra_pragma_lines, which was plumbed end to end and never populated.

Two things stayed where they are, with the reasoning on the threads: model.py and plan.py hold the harnesser's own plan and decision logic, and nothing else in the repo consumes them; and the pre-build probe reads raw AST nodes rather than the typed models, because a stopAfter: "parsing" AST has none of the analysis-phase fields the models require.

Generated output is unchanged. Regenerating a 65-wrapper harness gives a byte-identical file and the same manifest, and no wrapper name in any fixture is one of the keywords that stopped being escaped.

@shellygr
shellygr requested a review from jar-ben September 6, 2026 20:29
@shellygr

shellygr commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Claude answers: also re-ran the feature end to end on the refactored code, on a library main contract. AutoSetup swaps in the generated harness, puts the library in the conf's scene once, drops the curated summary that would have replaced it (Not summarizing BitMaps, it is the library under verification), and the cloud prover job comes back successful.

@shellygr

shellygr commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Claude answers: checked the other libraries on the refactored code too.

  • EnumerableSet as the main contract: AutoSetup exits 0, 60 of 72 functions wrapped (same as before), the library and its two dependencies are in the conf's scene, and the cloud prover job completes.
  • SafeCast and ABDKMath64x64: harness regenerated byte-identical, 65/65 and 25/27, same manifests and same plan hashes.

ABDK cannot go through AutoSetup locally, since that repo has no build system for the detector to find. That is not new here.

@jar-ben jar-ben left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

minor comments

Comment thread certora_autosetup/utils/build_json.py Outdated
Comment on lines +4 to +6
contract reached through several units appears once per unit. Five places used to walk
that structure with their own nesting checks and their own idea of where the file
lives, which is one place to get it wrong per caller.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Five places used to walk
that structure with their own nesting checks and their own idea of where the file
lives, which is one place to get it wrong per caller.

drop that part

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: dropped. Only the first paragraph is left, describing the file's shape.

single file parsed by ``solc --standard-json`` with ``stopAfter: "parsing"``, whose
nodes carry no analysis-phase fields at all: no ``scope``, no ``linearizedBaseContracts``,
no ``fullyImplemented``, so ``SourceUnit.model_validate`` rejects them (16 errors on a
two-declaration file) and the typed traversal is not available there. Both ends produce

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(16 errors on a two-declaration file)
seems like particular test specific output, i.e. drop

Also, this whole paragraph seems like it should not be here. Don't discuss users, just say what contracts.py introduces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: rewritten. It now names what the module offers and stops there:

Contract declarations and their kind, from either shape of solc AST.

ContractDeclView is the uniform view. iter_contract_declarations reads it from the typed models over an AstDump stream. parse_only_declarations reads it from the raw nodes of a stopAfter: "parsing" AST, which the typed models reject: solc emits no analysis-phase fields (scope, linearizedBaseContracts, fullyImplemented) at that stage.

I kept that last clause because it is the only reason there are two entry points instead of one. The error count is gone.

cand_parts = strip_sources_anchor(candidate)
want_parts = strip_sources_anchor(wanted)
depth = min(len(cand_parts), len(want_parts))
return cand_parts[-depth:] == want_parts[-depth:]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What if there is a nested repeated folder structure, e.g. nested .certora_sources? That also applies to strip_sources_anchor. Perhaps if we have get_sources_anchor(path) that either gives us the anchor if presented or None, then we can in same_source_file say that candidate and wanted are the same if either they are string equal or if one of them has anchor and removing the anchor prefix yields the other one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude answers: good question, and chasing it produced a better answer than the helper. The anchor strip cannot affect same_source_file at all: it removes leading components, and the comparison is on trailing ones. I ran your nested case through it, .certora_sources/vendor/.certora_sources/contracts/A.sol, along with the absolute and instrumented-copy shapes, and the result is the same with and without the strip in every one.

So the fix is subtraction rather than get_sources_anchor: same_source_file keeps only the suffix comparison, and strip_sources_anchor is gone from paths.py. There are now unit tests for those shapes in the Autosetup PR.

I also put compile_check's _strip_anchor back where it was. Merging it into paths.py was my mistake: _is_touched is directional and length-sensitive, so it only resembles same_source_file. That file keeps the one change that is real reuse, build_json_path.

On the equality-modulo-anchor shape you suggested: it would reject the case this function exists for, a build reporting one file as /abs/proj/contracts/A.sol and the other as contracts/A.sol. The suffix rule is loose in the other direction, A.sol matches contracts/A.sol, which the caller bounds by also requiring the contract name to match. Tightening it properly means normalizing both sides to project-relative where the paths are produced. Worth doing, but bigger than this PR.

shellygr and others added 2 commits September 10, 2026 21:06
# Conflicts:
#	certora_autosetup/setup/setup_prover.py
The two docstrings say what the module is, not how it came to be. `same_source_file` keeps
only the suffix comparison: it compares trailing components, and an anchor strip removes
leading ones, so the strip could never change an answer. That makes the nested
`.certora_sources` question moot there.

`compile_check` keeps its own `_strip_anchor` again. Its `_is_touched` is directional and
length-sensitive, so it only resembles `same_source_file`; folding the two together was
wrong. It still shares `build_json_path`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants