Address review feedback on container proof support#5
Merged
oflatt merged 1 commit intoJun 26, 2026
Merged
Conversation
oflatt-claude
force-pushed
the
container-proofs-review-cleanup
branch
8 times, most recently
from
June 26, 2026 00:16
ae43915 to
88efea4
Compare
oflatt
marked this pull request as draft
June 26, 2026 18:10
oflatt-claude
force-pushed
the
container-proofs-review-cleanup
branch
2 times, most recently
from
June 26, 2026 22:20
cfa4922 to
24f0241
Compare
Follow-up cleanups for "Proof support for containers", addressing review
comments from saulshanabrook, Copilot, and oflatt:
- pair-first/pair-second validators: guard argument arity with `let [pair] =
args else { return None }` before indexing, matching the other validators
(Copilot).
- map-of: reject odd-arity argument lists in the validator, and leave
malformed map terms unchanged during normalization instead of silently
dropping the unpaired key/value (Copilot).
- ast_cmp: document why the Lit/Var arms are spelled out rather than
delegating to `l.cmp(r)` -- Term intentionally has no derived Ord, and a
derived Ord on App would order children by insertion order, which is the
thing ast_cmp exists to avoid (saulshanabrook).
- typechecking: `use` register_container_rebuild_from_spec instead of calling
it via an absolute crate path (oflatt).
- normalization is now per-container, with no "built-in container" special
casing: TermDag carries no container knowledge (only the generic `ast_cmp`
and a reusable `sort_terms_by_ast` helper). Each container sort normalizes
its own term form in its constructor validator (set: sort+dedup; multiset:
sort; map: collapse to flat `map-of`), exactly as a user-defined container
would. The proof checker recomputes a container's canonical form by applying
the validator registered for the term's head (collected from the e-graph's
primitives), so built-in and user containers are handled identically
(oflatt/saulshanabrook).
- :internal-container-rebuild is a typed AST field (ast::ContainerRebuildSpec)
rather than an opaque string: the egglog parser parses it once into the struct
(ast/parse.rs) and Display serializes it, so register_container_rebuild_from_spec
just reads the struct — no hand-rolled tokenizing or re-parsing in the proof
pass; build constructs the struct directly (oflatt).
- trim the spec to the minimum (oflatt): it is now just
`(container-rebuild-spec <prim> [<proof-prim>])`. Everything else is recovered
at register time from proof_state, repopulated on re-parse from per-sort/global
annotations rather than listed per-container:
* per-element UF index names <- element sorts' :internal-uf (now
`:internal-uf <constructor> [<index>]`);
* per-container `<CSort>Proof` tables <- container sorts' :internal-proof-func
(now emitted for containers too);
* the global Congr/Trans/Sym/ContainerNormalize/Proof names <- a single
:internal-proof-names record on the `Proof` sort.
value_prim renamed internal_rebuild_prim.
- proof extraction: parse_proof recognizes each extracted proof term's head by
exact match against the recorded constructor names (EncodingNames) instead of
fragile `head.contains("Congr")`-style substring guessing (oflatt).
- move the container-rebuild primitives + their runtime + the spec decoder out
of proof_encoding.rs (2027 -> 1674 lines) into a new
proofs/proof_container_rebuild.rs; proof_encoding.rs keeps only the
encoder-side spec build. Pure relocation, no behavior change.
- core-relations: split Rebuilder into a value-level ValueRebuilder supertrait
plus the table-level Rebuilder. ContainerValue::rebuild_contents now takes
&dyn ValueRebuilder, so ClosureRebuilder (the single-container rebuilder)
drops its two unreachable!() table-level methods entirely (oflatt).
- docs: concision pass over the proof/term-encoding docs -- tightened
proof_encoding.md (kept structure + examples) and shortened verbose doc
comments throughout, and fixed two pre-existing broken intra-doc links
(`Fact`, `Propostion`) in proof_format.rs (oflatt).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oflatt-claude
force-pushed
the
container-proofs-review-cleanup
branch
from
June 26, 2026 22:36
24f0241 to
c4c97c1
Compare
oflatt
marked this pull request as ready for review
June 26, 2026 22:37
There was a problem hiding this comment.
Pull request overview
This PR is a follow-up cleanup to the container proof support work (stacked on egraphs-good#927), focusing on making container rebuild/proof metadata round-trip reliably through parsing/serialization, tightening validator defensiveness, and simplifying rebuild trait boundaries (value-level vs table-level).
Changes:
- Refactors container rebuild registration/spec handling: moves registration into a dedicated module, stores rebuild metadata as a structured
ContainerRebuildSpecs-expression, and restores needed UF/proof constructor metadata viaproof_state. - Reworks container-term canonicalization to be validator-driven (plus small shared helpers like
TermDag::sort_terms_by_ast), including rejecting malformedmap-ofterms rather than truncating. - Splits
RebuilderintoValueRebuilder+Rebuilderto make “value-only rebuilders” type-safe and remove unreachable table-level methods from single-container rebuild paths.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/typechecking.rs | Restores UF/proof metadata into proof_state during sort typechecking; adds TypeInfo::primitive_validators; uses new container rebuild registration path. |
| src/termdag.rs | Removes hard-coded container normalization; adds sort_terms_by_ast helper and clarifies ast_cmp docs. |
| src/sort/vec.rs | Switches container rebuild hook to &dyn ValueRebuilder. |
| src/sort/set.rs | Implements explicit canonical set term normalization using sort_terms_by_ast + dedup; updates validators/canonical form. |
| src/sort/pair.rs | Switches to ValueRebuilder; makes pair-first/pair-second validators arity-safe. |
| src/sort/multiset.rs | Switches to ValueRebuilder; adds explicit multiset term normalization helper. |
| src/sort/mod.rs | Re-exports ValueRebuilder instead of Rebuilder from core_relations. |
| src/sort/map.rs | Switches to ValueRebuilder; refactors and hardens map term normalization (rejects malformed spines/odd arity). |
| src/sort/fn.rs | Switches container rebuild hook to &dyn ValueRebuilder. |
| src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap | Updates snapshot for new :internal-uf annotation shape including UF index table. |
| src/proofs/proof_simplification.rs | Uses new ProofStore::normalize_container instead of TermDag::normalize_container_term. |
| src/proofs/proof_format.rs | Stores encoding names in RawProofStore; switches proof constructor recognition to exact match; threads container normalizers and adds normalize_container. |
| src/proofs/proof_extraction.rs | Passes primitive_validators() into proof store creation for container normalization in checking/simplification. |
| src/proofs/proof_encoding.rs | Removes in-file container rebuild registration/primitive implementations; emits richer UF/proof-func metadata for re-parse. |
| src/proofs/proof_encoding.md | Updates/condenses documentation and aligns container normalization explanation with validator-driven approach. |
| src/proofs/proof_encoding_helpers.rs | Records global proof constructor names on the Proof sort via :internal-proof-names. |
| src/proofs/proof_container_rebuild.rs | New module containing container rebuild registration and the rebuild primitives (value + proof). |
| src/proofs/proof_checker.rs | Recomputes container normalization via ProofStore::normalize_container (validator-driven) when checking ContainerNormalize. |
| src/proofs/mod.rs | Exposes new proof_container_rebuild module. |
| src/prelude.rs | Threads new proof_constructors field when constructing Sort commands. |
| src/lib.rs | Restores UF ctor/index and global proof constructor names into proof_state when running sort commands. |
| src/ast/parse.rs | Parses :internal-container-rebuild as a structured s-expression; parses :internal-uf as ctor + optional index; adds :internal-proof-names. |
| src/ast/mod.rs | Introduces ContainerRebuildSpec + ProofConstructorNames; extends sort command representation and printing accordingly. |
| src/ast/desugar.rs | Threads proof_constructors through desugaring pipeline. |
| egglog-bridge/src/tests.rs | Updates tests for ValueRebuilder rename/split. |
| core-relations/src/uf/mod.rs | Makes Canonicalizer implement ValueRebuilder; updates Rebuilder impl to rely on the supertrait for value rebuild. |
| core-relations/src/table_spec.rs | Introduces ValueRebuilder; makes Rebuilder: ValueRebuilder and provides default rebuild_slice. |
| core-relations/src/lib.rs | Re-exports ValueRebuilder. |
| core-relations/src/containers/tests.rs | Updates fake rebuilders/tests to the ValueRebuilder + Rebuilder split. |
| core-relations/src/containers/mod.rs | Updates ContainerValue::rebuild_contents to accept &dyn ValueRebuilder; simplifies closure-based rebuilder to value-level only. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+136
to
+138
| /// Look up an eq-sort element's union-find leader, if a `UF_<E>f` row exists. | ||
| /// In proof mode the index stores `(pair leader proof)`, so take `pair-first`. | ||
| /// Returns `Ok(None)` when there is no UF row (element is its own leader). |
| /// has taken place. | ||
| pub trait ContainerValue: Hash + Eq + Clone + Send + Sync + 'static { | ||
| /// Rebuild an additional container in place according the the given [`Rebuilder`]. | ||
| /// Rebuild an additional container in place according the the given [`ValueRebuilder`]. |
oflatt
added a commit
that referenced
this pull request
Jun 29, 2026
* Pare back out-of-scope doc churn from the review cleanup The container-proof review cleanup (PR #5) bundled a doc-concision pass that overreached: it rewrote prose in docs that only needed targeted updates, and reworded a doc comment unrelated to the PR. Restore those to their originals, keeping only the content the code actually forced. - proof_encoding.md: revert the Term Encoding / Globals / Proof Tracking prose rewrite. The net change vs the original doc is now +9/-10 lines -- the two paragraphs the code forced (per-container normalization replacing the shared normalize_container_term, and the trimmed :internal-container-rebuild annotation) -- instead of a 100/190 rewrite. - proof_format.rs: revert Justification::Rule's doc comment (unrelated to container proofs) to the original verbatim. - containers/mod.rs: shorten the verbose rebuild_val_with doc comment. Doc comments / markdown only; no code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Trim the container CHANGELOG entry to user-visible changes Drop the redundant proof phrasing (proofs shipped in 2.0.0) and the internal ContainerNormalize / rebuild_val_with details; keep the two user-visible extraction changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Collect container proof normalizers from the sort trait Containers now declare their canonicalization validator via a new `container_term_normalizer` trait method instead of the proof extractor scanning every registered primitive for a validator. The method is optional (`None`, the default), so a future container does not have to support proofs right away. - Add `Sort::container_term_normalizer` (default None) and the same on the `ContainerSort` trait, delegated by `ContainerSortImpl`. - Implement it for Set/Map/Vec/MultiSet/Pair, returning the canonical constructor head and the same validator already registered for that constructor (reusing each sort's normalize helper). - Replace `TypeInfo::primitive_validators` (which scanned all primitives) with `TypeInfo::container_term_normalizers`, collecting only container sorts' declared normalizers. The proof checker still reads primitive validators directly via `prim.validator()`; this only changes how the container-normalize map handed to the proof format layer is built. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Oliver Flatt <oflatt@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up cleanups for egraphs-good#927 ("Proof support for containers"), addressing review comments from @saulshanabrook, Copilot, and @oflatt. Targets the
oflatt-container-proofsbranch so it stacks on top of the PR.Correctness / defensive (Copilot)
src/sort/pair.rs): guard arity withlet [pair] = args else { return None }before indexing, matchingpair_validatorand the other validators. Can't trigger today (arity is enforced by typechecking), but no longer relies on that.map-of(src/sort/map.rs,src/termdag.rs):map_of_validatornow rejects odd-length argument lists, and normalization leaves a malformed map term unchanged rather than silently dropping the unpaired key/value (collect_map_entriesreturnsNoneon a malformed spine instead of truncating viachunks(2)).Cleanup (saulshanabrook / oflatt)
ast_cmpdocs (src/termdag.rs): explain why theLit/Vararms are spelled out instead ofl.cmp(r)—Termintentionally has no derivedOrd, and a derivedOrdonAppwould order children by insertion order, which is exactly whatast_cmpexists to avoid. Only theApparm needs the DAG recursion.src/termdag.rs): replaced the hard-coded head-name match innormalize_container_termwith aContainerNormalizationstrategy enum + a singlebuiltin_container_normalization(head)source of truth, and extracted a reusablesort_terms_by_asthelper that custom container sorts can call (e.g. after their own dedup). Behavior is unchanged for the built-ins.TermDag+ the program, noSorts), so normalization stays keyed by head rather than dispatched through a per-sort trait method. Supporting a user-defined canonicalizing container in proofs would mean threading a head→strategy map into the checker; documented as future work.src/typechecking.rs):use crate::proofs::proof_encoding::register_container_rebuild_from_spec;instead of the inlinecrate::…path.:internal-container-rebuildspec (src/proofs/proof_encoding.rs): build and parse the spec as an egglog s-expression through the existing parser (SexpParser/Sexp::expect_*), like the rest of the proof pass, instead of pushing and re-splitting space-separated tokens with positionalnext(&mut i)/ count prefixes. Pairs are sorted for a deterministic spec.Rebuilder simplification (oflatt)
core-relations: splitRebuilderinto a value-levelValueRebuildersupertrait (rebuild_val+ defaultedrebuild_slice) and the table-levelRebuilder: ValueRebuilder(hint_col/rebuild_buf/rebuild_subset).ContainerValue::rebuild_contentsnow takes&dyn ValueRebuilder, soClosureRebuilder(the single-container rebuilder) drops both of itsunreachable!()table-level methods — the "value-level only" guarantee is now type-level.Canonicalizer(union-find) implements both; the call sites inapply_rebuild_*upcast&dyn Rebuilder → &dyn ValueRebuilderautomatically.Testing
cargo test --release --workspace(+ doctests): pass.cargo clippy --tests -- -D warnings: clean.cargo fmt --check: clean.🤖 Generated with Claude Code