Skip to content

Address review feedback on container proof support#5

Merged
oflatt merged 1 commit into
oflatt:oflatt-container-proofsfrom
oflatt-claude:container-proofs-review-cleanup
Jun 26, 2026
Merged

Address review feedback on container proof support#5
oflatt merged 1 commit into
oflatt:oflatt-container-proofsfrom
oflatt-claude:container-proofs-review-cleanup

Conversation

@oflatt-claude

Copy link
Copy Markdown

Follow-up cleanups for egraphs-good#927 ("Proof support for containers"), addressing review comments from @saulshanabrook, Copilot, and @oflatt. Targets the oflatt-container-proofs branch so it stacks on top of the PR.

Correctness / defensive (Copilot)

  • pair-first / pair-second validators (src/sort/pair.rs): guard arity with let [pair] = args else { return None } before indexing, matching pair_validator and the other validators. Can't trigger today (arity is enforced by typechecking), but no longer relies on that.
  • odd-arity map-of (src/sort/map.rs, src/termdag.rs): map_of_validator now rejects odd-length argument lists, and normalization leaves a malformed map term unchanged rather than silently dropping the unpaired key/value (collect_map_entries returns None on a malformed spine instead of truncating via chunks(2)).

Cleanup (saulshanabrook / oflatt)

  • ast_cmp docs (src/termdag.rs): explain why the Lit/Var arms are spelled out instead of l.cmp(r)Term intentionally has no derived Ord, and a derived Ord on App would order children by insertion order, which is exactly what ast_cmp exists to avoid. Only the App arm needs the DAG recursion.
  • normalization (src/termdag.rs): replaced the hard-coded head-name match in normalize_container_term with a ContainerNormalization strategy enum + a single builtin_container_normalization(head) source of truth, and extracted a reusable sort_terms_by_ast helper that custom container sorts can call (e.g. after their own dedup). Behavior is unchanged for the built-ins.
    • Note: the proof checker is deliberately sort-agnostic (it works on a TermDag + the program, no Sorts), 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.
  • absolute path (src/typechecking.rs): use crate::proofs::proof_encoding::register_container_rebuild_from_spec; instead of the inline crate::… path.
  • :internal-container-rebuild spec (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 positional next(&mut i) / count prefixes. Pairs are sorted for a deterministic spec.

Rebuilder simplification (oflatt)

  • core-relations: split Rebuilder into a value-level ValueRebuilder supertrait (rebuild_val + defaulted rebuild_slice) and the table-level Rebuilder: ValueRebuilder (hint_col / rebuild_buf / rebuild_subset). ContainerValue::rebuild_contents now takes &dyn ValueRebuilder, so ClosureRebuilder (the single-container rebuilder) drops both of its unreachable!() table-level methods — the "value-level only" guarantee is now type-level. Canonicalizer (union-find) implements both; the call sites in apply_rebuild_* upcast &dyn Rebuilder → &dyn ValueRebuilder automatically.

Testing

  • cargo test --release --workspace (+ doctests): pass.
  • cargo clippy --tests -- -D warnings: clean.
  • cargo fmt --check: clean.
  • No snapshot changes (the normalization refactor is behavior-preserving and the spec string isn't snapshotted).

🤖 Generated with Claude Code

@oflatt-claude
oflatt-claude force-pushed the container-proofs-review-cleanup branch 8 times, most recently from ae43915 to 88efea4 Compare June 26, 2026 00:16
@oflatt
oflatt marked this pull request as draft June 26, 2026 18:10
@oflatt-claude
oflatt-claude force-pushed the container-proofs-review-cleanup branch 2 times, most recently from cfa4922 to 24f0241 Compare June 26, 2026 22:20
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
oflatt-claude force-pushed the container-proofs-review-cleanup branch from 24f0241 to c4c97c1 Compare June 26, 2026 22:36
@oflatt
oflatt marked this pull request as ready for review June 26, 2026 22:37
Copilot AI review requested due to automatic review settings June 26, 2026 22:37
@oflatt
oflatt merged commit 07650be into oflatt:oflatt-container-proofs Jun 26, 2026
5 of 32 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR 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 ContainerRebuildSpec s-expression, and restores needed UF/proof constructor metadata via proof_state.
  • Reworks container-term canonicalization to be validator-driven (plus small shared helpers like TermDag::sort_terms_by_ast), including rejecting malformed map-of terms rather than truncating.
  • Splits Rebuilder into ValueRebuilder + Rebuilder to 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants