unstable-subst: substitution over a reachable sub-e-graph - #60
Draft
oflatt-claude wants to merge 4 commits into
Draft
unstable-subst: substitution over a reachable sub-e-graph#60oflatt-claude wants to merge 4 commits into
oflatt-claude wants to merge 4 commits into
Conversation
`(unstable-subst root map)` takes an e-class of any eq-sort and a Map from an eq-sort to itself, walks the constructor rows reachable from the root, and copies the part of that sub-e-graph the substitution touches with each key e-class replaced by its mapped value, returning the copied root. E-classes the substitution does not affect are shared rather than copied; container children are rebuilt around their substituted contents. Also available as egglog_experimental::subst. Copies are named by lookup_or_insert like any other action-built term, so no e-class id is invented: a cyclic e-class is copied when one of its e-nodes has all its children outside the cycle, and a cycle with no such e-node is reported instead of half-copied. Reads live tables, so it is a Context::Full primitive - top-level actions and :naive rule heads. Built entirely on egglog's public API: Read::enodes_for_eclass to walk a constructor's rows by output e-class, Read::table_schema / table_subtype to classify each column, and Core::rebuild_container for container children. The type constraint enumerates the declared Map and eq-sorts through TypeInfo::get_arcsorts_by, identifying a Map sort by the Rust type its values intern under since the ContainerSort impl behind an ArcSort is not nameable from out of tree. Cargo.toml points at a local egglog checkout while that API is unmerged. See SUBST_DESIGN.md for the semantics and the sharp edges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Points the egglog dependencies at egraphs-good/egglog#986, which adds Read::enodes_for_eclass, Read::table_schema / table_subtype, and Core::rebuild_container. Moves back to an egraphs-good rev once that merges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
egglog now splits its schema accessor by subtype, so resolving the walk's constructors is one call that rejects function tables (and so globals, which lower to function tables) instead of a subtype check plus a schema lookup. is_constructor now asks table_subtype instead of starting a constructor scan and reading the answer off the error, which is what that accessor was for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
egglog now keeps one FuncType per function, shared between TypeInfo and Function, so the schema accessors hand back an Arc<FuncType> and Function::schema() is Function::func_type(). The walk holds the Arc directly instead of a borrowed slice of input sorts. Co-Authored-By: Claude Opus 5 <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.
Blocked on egraphs-good/egglog#986 — draft until that merges, since the
Cargo.tomlhere points at its branch. Review that one first; this is theconsumer that motivated it.
rootis an e-class of any eq-sort;mapis aMapwhose key and value sortsare the same eq-sort. It walks the constructor rows reachable from
root,copies the part of that sub-e-graph the substitution actually touches with each
key e-class replaced by its mapped value, and returns the copied root.
E-classes the substitution does not affect are shared with the original rather
than copied, so substituting an empty map returns
rootitself and writesnothing. Container children are walked into and rebuilt around their
substituted contents. Also available from Rust as
egglog_experimental::subst.SUBST_DESIGN.mdhas the full semantics; the three things worth knowing beforerelying on it:
The region's equations are substituted along with its terms. Copying an
e-class copies every one of its e-nodes, so
t1 = t2in the original becomesσ(t1) = σ(t2)in the copy — and an e-node with no substituted children copiesto itself, merging the copy back into the original class. That is correct for
equations a rewrite rule derived, which hold for every value of the substituted
class: with
(rewrite (Mul a (Num 0)) (Num 0)), substitutingx := 5into(Mul x (Num 0))merges back into the class of0, and5 * 0really is0.It is wrong for a ground
unionpinning a substituted class down. So onlysubstitute classes that behave like universally quantified variables. Both
directions are pinned by tests.
No e-class id is ever invented. Every copied e-node goes in through
lookup_or_insert, the same way(Add a b)in an action does. A cyclice-class is therefore copied only when one of its e-nodes has all its children
outside the cycle to name the copy first —
x = {Var "x", Add x (Num 0)}qualifies and works. A cycle with no such e-node is reported rather than
half-copied.
It reads live tables, so it is a
Context::Fullprimitive: top-levelactions and
:naiverule heads. A term the enclosing action has only juststaged is not yet in the tables and comes back unsubstituted; terms from
earlier commands and earlier rule iterations are fine, which is what the
:naivebeta-reduction shape needs.Implementation
Three passes: collect the reachable e-nodes and container contents; mark the
e-classes the substitution changes by worklist from the map keys; then copy in
postorder, sweeping until no further progress, which is what lets a grounded
cycle close. E-class walks use an explicit stack, so term depth is bounded by
the heap (there is a 20k-deep test).
Built entirely on egglog's public API:
Read::enodes_for_eclassto walk aconstructor's rows by output e-class,
Read::constructor_schemato classifyeach column, and
Core::rebuild_containerfor container children. Also swapstable_rows::is_constructoroff its subtype probe and ontoRead::table_subtype. The type constraint enumerates thedeclared
Mapand eq-sorts throughTypeInfo::get_arcsorts_by, identifying aMapsort by the Rust type its values intern under, since theContainerSortimpl behind an
ArcSortis not nameable from out of tree.Notes
tests/subst.rs. Full suite passes;cargo clippy --testsandcargo fmt --checkclean."primitive panicked", with the reason in the log: registering a custom panic
message needs
egglog_bridge::EGraph::new_panic, and egglog exposes noaccessor for its backend. A
Write::panic_with(message)upstream would fixit.
primitive is registered without a validator, so
program_supports_proofsalready excludes programs using it from proof-checking runs.
🤖 Generated with Claude Code