Skip to content

Typed pydantic models of the Solidity compact AST (--dump_asts) + autosetup migration#76

Open
shellygr wants to merge 12 commits into
masterfrom
shelly/solidity-ast-pydantic
Open

Typed pydantic models of the Solidity compact AST (--dump_asts) + autosetup migration#76
shellygr wants to merge 12 commits into
masterfrom
shelly/solidity-ast-pydantic

Conversation

@shellygr

Copy link
Copy Markdown
Contributor

What

A new subpackage certora_autosetup/solidity_ast/: a full, hand-authored pydantic v2 model of the solc compact AST as it appears in the .asts.json dump produced by certoraRun --dump_asts, plus migration of the two autosetup consumers of that dump onto it.

  • 74 node models (57 Solidity + 17 Yul), transcribed field-by-field from the OpenZeppelin solidity-ast JSON Schema (vendored at solidity_ast/schema/schema.json, v0.4.62, MIT — see schema/NOTICE). The schema unions solc versions >= 0.6: version-added fields are optional, genuinely-null fields are nullable.
  • Discriminated unions (Expression, Statement, TypeName, SourceUnitNode, ContractBodyNode, Node, YulStatement, YulExpression) on nodeType, each with an UnknownNode fallback member so an unrecognized node type from a newer solc degrades per-node instead of failing the file.
  • Loader (AstDump.load / load_cached) for the dump's 3-level structure {original_file: {source_file: {node_id: node}}}: validates each source's SourceUnit tree exactly once (the flat id map duplicates nodes nested in ancestors), derives the id index by traversal, keeps the raw map alongside. Vyper sources pass through raw behind an explicit raw_kind="vyper"; a source the models reject entirely degrades to raw_kind="parse_failed" with the raw map retained.
  • Traversal utilities: iterative walk/find_all, typed build_parent_map, iter_nodes_of_type (typed-first + raw flat-map sweep for nodes the typed walk can't reach), and build_parent_graph_json — a byte-compatible relocation of the legacy parent-graph heuristic.

Consumer migration (parity-pinned)

  • setup_prover.py: declared-contracts and inheritance/abstract extraction now go through typed ContractDefinition access with a raw-scan fallback; generate_ast_graph delegates to the relocated builder — output file stays byte-identical (golden test).
  • auto_munges.py: .code access detection iterates typed MemberAccess candidates with the raw sweep, so no access can be hidden by an unknown ancestor node or an unparsable source. Patches now read/target the source file the offsets refer to (previously a .code access in an in-scope imported file produced a patch against the compilation unit's main file at foreign offsets — silent source corruption) and are deduplicated across compilation units.

Guardrails

  • tests/solidity_ast/test_schema_conformance.py: machine-checks every model against the vendored schema — coverage in both directions, per-property presence/requiredness/nullability/enum/one-level kind (155 checks). Two deliberate, allowlisted deviations: InlineAssembly.evmVersion/flags are open strings (a closed EVM-fork enum would demote every assembly-containing file on the first post-schema solc release).
  • Committed real fixtures generated via certoraRun --dump_asts for solc 0.6.12 / 0.7.6 / 0.8.30 (path-sanitized; regeneration script included, dev-only) + a hand-written Vyper-mixed fixture. Strict parsing asserts zero unknown nodes and zero unmodeled fields — the schema-drift alarm.
  • Parity tests re-implement the legacy raw-dict algorithms inline and assert identical results, plus an end-to-end detect_code_accesses run producing correct patches.
  • 421 tests pass, pyright clean (CI scope).

Known pre-existing issue (not fixed here)

solc src offsets are byte offsets, but patch extraction/application slices Python strings by character index — any non-ASCII character before a .code access shifts the spans (detect and apply err consistently, so the mismatch check does not catch it). Fixing it properly touches apply_code_access_patches/_inject_load_code_into_contract and deserves its own change.

Manual validation

uv run python -m certora_autosetup.solidity_ast <path/to/.certora_internal/all_asts.json>

Point it at any existing project dump: prints per-source parse status, node counts, unknown/unmodeled fields (should be none), and contracts with resolved inheritance.

🤖 Generated with Claude Code

shellygr and others added 12 commits July 15, 2026 18:18
Foundation for a full pydantic model of the solc compact AST as dumped by
certoraRun --dump_asts. schema/schema.json is the OpenZeppelin solidity-ast
0.4.62 JSON Schema (MIT, see schema/NOTICE); models will be hand-authored and
machine-verified against it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mance tests, fixtures

74 hand-authored pydantic v2 models (Solidity + Yul compact AST), discriminated
unions with an UnknownNode fallback per union, single-point forward-ref rebuild,
a loader for the 3-level --dump_asts structure (single-parse-per-source, Vyper
raw passthrough, per-source degradation), traversal utilities incl. the frozen
byte-compatible legacy parent-graph builder, a 151-case schema-conformance suite
against the vendored schema, and real .asts.json fixtures generated via
certoraRun for solc 0.6.12 / 0.7.6 / 0.8.30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…olden test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setup_prover: declared-contracts and inheritance extraction go through a shared
_iter_contract_declarations (typed find_all with a raw-scan fallback for sources
the models cannot parse); generate_ast_graph delegates to the relocated
byte-compatible parent-graph builder. auto_munges: .code detection iterates
typed MemberAccess nodes (per-node raw salvage on unparsable sources) and
decodes src offsets via parse_src. Parity with the legacy raw algorithms is
pinned by tests/solidity_ast/test_consumer_migration.py on the real fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enums, cached loads

Review findings addressed: iter_nodes_of_type gives typed-first iteration with a
raw flat-map sweep for nodes the typed walk cannot reach (e.g. under an unknown
future nodeType), used by both migrated consumers so no ContractDefinition or
.code MemberAccess can be hidden; InlineAssembly.evmVersion/flags are open
strings (a closed fork enum would demote whole files on the next solc release —
conformance test carries the allowlisted deviation); .code patches now read and
target the source file the offsets refer to (not the compilation unit's main
file) and are deduplicated across units; the dump is loaded once per setup run
via AstDump.load_cached; union tag sets got a drift-guard test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roundtrip_diffs() compares a re-serialized typed tree against the exact source
JSON (model_dump with exclude_unset; the internalFunctionIDs de-stamping is the
one reversed normalization). Wired into the __main__ validator (with a --json
machine mode for corpus sweeps) and pinned on the committed fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Corpus-driven: real certoraRun fixtures for solc 0.4.26 and 0.5.17 exposed the
full legacy dialect, now handled typed (no raw-dict fallback): lenient defaults
for fields that did not exist yet (mutability, virtual, tryCall, kind, abstract),
string-form documentation and ElementaryTypeNameExpression.typeName, the pre-0.6
InlineAssembly shape (operations text, keyed externalReferences, no Yul AST),
file-level EventDefinition (solc >= 0.8.22, missing from the vendored schema),
and modeled 0.4-era FunctionDefinition flags. Every deviation from the schema is
machine-tracked in the conformance test (LENIENT_REQUIRED / DELIBERATELY_OPEN /
FIELD_ALLOWLIST). All five fixtures hold the same strict invariants: fully typed
parse, zero unknown nodes, zero unmodeled fields, exact round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the producing solc version is known, a gated field absent at or above its
introduction gate fails the source instead of silently reading as None (crash
over wrong results); fixtures now always parse with their version so the gates
stay exercised. virtual is None below 0.6 (everything was implicitly
overridable — False would mislead). effective_mutability/effective_kind derive
the pre-gate values faithfully from constant/isConstructor without touching the
serialized form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Empirical solc ladder over every release 0.4.24-0.8.36 pinned the exact gate
boundaries: FunctionCall.tryCall from 0.6.0 and InlineAssembly.evmVersion from
0.6.2 (both previously gated later, i.e. under-enforced). solc 0.7.2 alone
omits isLValue on enum-member MemberAccess nodes — a bug window, present before
and after, so it is lenient (None) rather than gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…solidity_ast

stream_ast_files's four call sites collapse into the typed layer: new
AstDump.stream_units() (per-compilation-unit typed streaming with unit_filter
and solc_version gates) and stream_raw_units() (raw pairs for the byte-compat
parent-graph pass, which now accepts streamed units). All three consumers
stream instead of whole-file loading; load_cached is gone (the memory win
outweighs the shared parse), stream_ast_files removed with its purpose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shellygr
shellygr requested review from jar-ben and jtoman July 17, 2026 17:33
@shellygr

Copy link
Copy Markdown
Contributor Author

@jtoman @jar-ben I do not expect you to read 200K loc. John you were excited about this so we can discuss design choices over DMs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant