diff --git a/egg/apyds_egg/__init__.py b/egg/apyds_egg/__init__.py index 056713c..4c6c651 100644 --- a/egg/apyds_egg/__init__.py +++ b/egg/apyds_egg/__init__.py @@ -86,6 +86,7 @@ def __init__(self) -> None: self.unionfind: UnionFind[EClassId] = UnionFind() self.classes: dict[EClassId, set[ENode]] = {} self.parents: dict[EClassId, set[tuple[ENode, EClassId]]] = defaultdict(set) + self.worklist: set[EClassId] = set() def _fresh_id(self) -> EClassId: """Generate a fresh E-class ID.""" @@ -149,7 +150,7 @@ def _add_enode(self, enode: ENode) -> EClassId: return eid def merge(self, a: EClassId, b: EClassId) -> EClassId: - """Merge two E-classes and immediately restore congruence. + """Merge two E-classes and defer congruence restoration. Args: a: The first E-class ID to merge. @@ -170,34 +171,45 @@ def merge(self, a: EClassId, b: EClassId) -> EClassId: self.parents[r] |= self.parents[rb] del self.parents[rb] - self._repair(r) + self.worklist.add(r) return r - def _repair(self, eclass: EClassId) -> None: - """Immediately restore congruence by re-canonicalizing parents and merging congruent ones. + def rebuild(self) -> None: + """Restore congruence by processing the worklist. - This method implements the traditional E-Graph repair algorithm: + This method implements the egg-style deferred rebuilding: + - Process all E-classes in the worklist + - Re-canonicalize parents and merge congruent ones + - Continue until worklist is empty + """ + while self.worklist: + todo: set[EClassId] = {self.find(e) for e in self.worklist} + self.worklist.clear() + + for eclass in todo: + self.repair(eclass) + + def repair(self, eclass: EClassId) -> None: + """Restore congruence for a single E-class. + + This method implements the egg-style repair algorithm: - Re-canonicalize all parent nodes - - Merge congruent parents recursively - - Continue until no more changes occur + - Merge congruent parents (which may add more work to worklist) + - Update hashcons and parent tracking """ - changed = True - while changed: - changed = False - new_parents: dict[ENode, EClassId] = {} + new_parents: dict[ENode, EClassId] = {} - for pnode, peclass in list(self.parents[eclass]): - self.hashcons.pop(pnode, None) + for pnode, peclass in list(self.parents[eclass]): + self.hashcons.pop(pnode, None) - canon = pnode.canonicalize(self.find) - peclass = self.find(peclass) + canon = pnode.canonicalize(self.find) + peclass = self.find(peclass) - if canon in new_parents: - self.merge(peclass, new_parents[canon]) - changed = True - else: - new_parents[canon] = peclass - self.hashcons[canon] = peclass + if canon in new_parents: + self.merge(peclass, new_parents[canon]) + else: + new_parents[canon] = peclass + self.hashcons[canon] = peclass - self.parents[eclass] = {(p, c) for p, c in new_parents.items()} + self.parents[eclass] = {(p, c) for p, c in new_parents.items()} diff --git a/egg/tests/test_egraph.py b/egg/tests/test_egraph.py index 964856f..868acee 100644 --- a/egg/tests/test_egraph.py +++ b/egg/tests/test_egraph.py @@ -139,6 +139,7 @@ def test_egraph_congruence(): assert eg.find(ax) != eg.find(bx) eg.merge(a, b) + eg.rebuild() assert eg.find(ax) == eg.find(bx) @@ -157,6 +158,7 @@ def test_egraph_congruence_nested(): bcc = eg.add(apyds.Term("(g (f b c) c)")) eg.merge(a, b) + eg.rebuild() assert eg.find(ac) == eg.find(bc) assert eg.find(acc) == eg.find(bcc) @@ -195,6 +197,7 @@ def test_egraph_complex_example(): assert eg.find(ax) != eg.find(bx) eg.merge(a, b) + eg.rebuild() assert eg.find(ax) == eg.find(bx)