The e-graph moves from a harness into the kernel, behind an explicit opt-in - #1101
The e-graph moves from a harness into the kernel, behind an explicit opt-in#1101Rafael-SOWNet wants to merge 3 commits into
Conversation
…opt-in #746 tier 2's own row names two things as remaining "in dependency order": no production caller for the reversible-rule mechanism, and folding on insertion. The second is done (this workspace's `work/egraph` harness, proven against a 16-expression corpus: 7 of 16 saturating under the full rule set became 15 of 16, neutral-element churn from 13-34% to 0%). This gives the first a real, if narrowly-scoped, answer. EGraph is that harness's design moved here unchanged: e-classes over a union-find, e-nodes hash-consed by operator and child class, congruence restored by Rebuild, neutral elements folded on insertion rather than discovered later by a rule. `MatchPattern.Construct` -- until now `private`, used to build a rule's right-hand side from bindings -- is exposed as `ConstructNode` so extraction rebuilds a node's type from the same registry the rest of the pattern-matching layer already trusts, rather than a second, independently-drifting list of the same operators. `Transformation.EqualitySaturation(WorkBudget, CostModel)` is the caller: builds an e-graph from the input, fires every rule whose `RewriteRuleGrowth` is `Collects` or `Rearranges` -- withholding `Expands` and `Unknown` for the same reason, unproven is not the same as safe -- against `AngouriMath.Core.Budgets.BudgetLedger`, the same budget type Gröbner elimination already answers to, and extracts the cheapest candidate under the given `CostModel`. Nothing runs this by default, the same standing as `RationalCanonicalization` and `Canonicalization`: `Simplify` applies a rule set once and moves on, so an expanding rule and a collecting one never meet, and equality saturation deletes exactly that ordering. What this is not, stated in its own doc comment rather than left implicit: the harness this is built from enumerates a class's terms and rewrites each, which finds what e-matching would but by materialising terms a real e-matcher never builds. That instrument moved here unchanged. A production e-matcher over `MatchPattern` is not this, and tier 2 still names it as the production caller's other missing half. Nor does a 16-expression textbook corpus settle whether this generalises to what `Simplify` is actually asked to handle -- the budget is the honest acknowledgment of that, not a solved problem's formality. One test flake chased down rather than papered over: `a / b / c` rewritten to `a / (b * c)` occasionally failed a numeric equality check under `dotnet test` and never once under 2,300+ raw concurrent calls to the same code in an isolated process. The two chains are the same value reached by differently-ordered complex divisions, and comparing them by the exact equality `ExpressionNumerical.AreEqual` uses is comparing two floating-point rounding paths, not the value each settles on. Fixed by verifying against `Entity.EqualsImprecisely` -- the tolerance this library already has for exactly that comparison -- not by loosening what the transformation itself promises. TDD throughout: EGraph's union-find, hash-consing, congruence rebuild and neutral folding (all five operator cases, including the two -- `0 - x`, `1 / x` -- that must not fold) each have a test that failed for the right reason before the line that makes it pass. `PublicApi.txt` regenerated for the one new public member.
Tried the obvious move: give RewriteRule a Reversed the same way AsAddressable() already gives it a Growth. Measured against the live registry rather than assumed to work -- grep says AsAddressable() is called exactly once, for RationalizeDenominator, and every other set's addressable Rules still comes from RuleRegistryGenerator reading a switch's arms, kept around after the exchange purely for that. Wiring Reversed into AsAddressable() alone therefore changes nothing for 29 of 30 sets, and the one it does reach has two code-built, non-reversible rules -- so the change measured zero reversible rules registry-wide and was reverted rather than shipped speculative. Recorded here rather than in a commit message that stops being read: what a real fix costs (extending the generator to compute reversibility from syntax, or re-deriving Rules from AsAddressable() for every converted set and reconciling two independent renderings that have never been compared), and why RationalizeDenominator specifically is the wrong set to prototype against. Not fixed here -- estimating which option is worth its cost is a separate question from measuring that the cheap option does not exist.
|
Added a follow-up commit: while working on this, tried extending |
…erve EGraph.Extract rebuilt every node through a bare constructor, which restores neither Entity.Codomain nor the reference identity that keeps EulerIntrinsic out of a binder over the name e -- both confirmed wrong-answer bugs, both fixed with a regression test at the EGraph level and at the public Transformation.EqualitySaturation level. The other thirteen findings from the same review are recorded in EqualitySaturationReviewFindings.md rather than fixed here: several point at the same underlying gap (the e-graph's node model has nowhere to carry metadata beyond raw tree shape), which is a design question worth its own pass rather than a patch alongside these two. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
A code review of this PR and #1102 found fifteen issues before either had a human reviewer. Two were confirmed wrong-answer bugs and are fixed in the latest commit, each with a regression test at the
The other thirteen findings are recorded in Full suite green: 8828 passed, 14 skipped (pre-existing), 0 failed. |
11 TDD tasks covering EBindings, NodeCount/Growth on MatchPattern and MatchedRule, the EGraph helpers e-matching needs, CanEMatch/EMatch/ ETryBuild on all four pattern kinds, MatchedRule.TryEMatchApply, and rewiring EqualitySaturationTransformation to source from Matching.MatchedRules. Also rebases this branch onto tier2-inverse-pair-table (#1101), which EGraph.cs and EqualitySaturationTransformation only exist on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
#746 tier 2's row names two things remaining "in dependency order": no production caller for the reversible-rule mechanism, and folding on insertion. This closes the second (already proven in the
work/egraphmeasurement harness — 7/16 → 15/16 corpus expressions saturating under the full rule set, neutral-element churn 13–34% → 0%) and gives the first a real, narrowly-scoped answer.EGraph— the harness's e-graph design (union-find, hash-consed e-nodes, congruence rebuild, neutral-element folding on insertion) moved into the kernel unchanged, as an internal type.MatchPattern.Construct→ConstructNode— widened fromprivatetointernal(asConstructNode) so extraction rebuilds a node's type from the same registry the pattern-matching layer already trusts, instead of a second, independently-drifting operator list.Transformation.EqualitySaturation(WorkBudget, CostModel)— the production caller. Builds an e-graph from the input, fires only rules whoseRewriteRuleGrowthisCollectsorRearranges(withholdingExpandsandUnknown— unproven isn't the same as safe), bounded byAngouriMath.Core.Budgets.BudgetLedger(the same budget type Gröbner elimination already answers to), and extracts the cheapest candidate under the givenCostModel.Nothing runs this by default — same standing as
RationalCanonicalization/Canonicalization.Simplifyapplies a rule set once and moves on; equality saturation deletes exactly that ordering, which is why it needs a budget rather than a pass count.What this is not, stated in the doc comment rather than left implicit: the harness this is built from enumerates a class's terms and rewrites each — it finds what e-matching would, but by materialising terms a real e-matcher never builds. That instrument moved here unchanged; a production e-matcher over
MatchPatternis not this, and tier 2 still names it as the production caller's other missing half. Nor does a 16-expression textbook corpus settle whether this generalises to whatSimplifyis actually asked to handle — the budget parameter is the honest acknowledgment of that, not a solved problem's formality.A flake chased down, not papered over
a / b / crewritten toa / (b * c)occasionally failed a numeric equality check underdotnet test, and never once across 2,300+ raw concurrent calls to the same code in an isolated process. Both chains are the same value reached by differently-ordered complex divisions; comparing them with the exact equalityExpressionNumerical.AreEqualuses is comparing two floating-point rounding paths, not the value each settles on. Fixed by verifying againstEntity.EqualsImprecisely— the tolerance this library already has for exactly that comparison — rather than loosening what the transformation itself promises.Test plan
EGraph's union-find, hash-consing, congruence rebuild, and neutral folding (all 5 operator cases, including the 2 —0 - x,1 / x— that must not fold) each have a test that failed for the right reason before the fix.Transformation.EqualitySaturationtested for its stated relation/soundness, that it declines to change an already-cheapest expression, that it never throws under a starved budget, and — across the existingTransformationTest.Corpus— that it never changes the value it claims to preserve (numerically, at several real check points).PublicApi.txtregenerated for the one new public member (Transformation.EqualitySaturation) — diff is exactly that one line.🤖 Generated with Claude Code