Skip to content

Add apyds-egg support package with E-Graph implementation - #146

Merged
hzhangxyz merged 8 commits into
mainfrom
copilot/add-support-package-apyds-egg-again
Dec 22, 2025
Merged

Add apyds-egg support package with E-Graph implementation#146
hzhangxyz merged 8 commits into
mainfrom
copilot/add-support-package-apyds-egg-again

Conversation

Copilot AI commented Dec 22, 2025

Copy link
Copy Markdown
Contributor

Implementation Plan for apyds-egg Package

  • Create /egg directory structure similar to /bnf
  • Create egg/pyproject.toml with proper configuration
  • Create egg/setup.py (minimal, no ANTLR generation needed) Removed - not needed for modern build backends
  • Create egg/README.md with package documentation Removed - will be added in a separate PR
  • Create egg/apyds_egg/__init__.py with core E-Graph implementation
    • Implement UnionFind class
    • Implement ENode class (using string op and tuple of EClassId children)
    • Implement EGraph class with add, merge, rebuild, repair methods
    • Adapt add method to accept apyds.Term as input
    • Use from __future__ import annotations for forward references
    • Remove string type hints (e.g., "ENode" -> ENode)
  • Create egg/tests/test_egraph.py with comprehensive tests
  • Create egg/apyds_egg/py.typed marker file
  • Create egg/apyds_egg/.gitignore if needed Removed - handled by repo root .gitignore
  • Verify all tests pass (24 tests, 100% coverage)
  • Address review feedback
    • Remove egg/setup.py (not needed for modern build backends)
    • Remove egg/apyds_egg/.gitignore (repo root handles it)
    • Remove egg/README.md (will be added separately)
    • Add from __future__ import annotations to avoid string type hints
    • Remove tests for error handling (code simplified to assume correct input)
Original prompt

This section details on the original issue you should resolve

<issue_title>添加一个support package叫做apyds-egg</issue_title>
<issue_description>放在/egg下吧,组织形式参考/bnf下的结构。
不过这个只需要实现python的就可以了,不用js。

item和variable直接那字符串当op,list的op可以用绝对不会出现在item和variable中的“()"。

具体的egg可以参考下面来自gpt的demo:

# egraph_demo_typed_py313.py
# Minimal, typed E-Graph demo with deferred rebuilding (egg-style)
# Python 3.13+

from __future__ import annotations

from dataclasses import dataclass
from typing import NewType, Callable
from collections import defaultdict


# ---------- Strongly-typed IDs ----------

EClassId = NewType("EClassId", int)


# ---------- Union-Find ----------

class UnionFind:
    parent: dict[EClassId, EClassId]

    def __init__(self) -> None:
        self.parent = {}

    def find(self, x: EClassId) -> EClassId:
        if x not in self.parent:
            self.parent[x] = x
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, a: EClassId, b: EClassId) -> EClassId:
        ra, rb = self.find(a), self.find(b)
        if ra != rb:
            self.parent[rb] = ra
        return ra


# ---------- ENode ----------

@dataclass(frozen=True)
class ENode:
    op: str
    children: tuple[EClassId, ...]

    def canonicalize(
        self,
        find: Callable[[EClassId], EClassId],
    ) -> ENode:
        return ENode(
            self.op,
            tuple(find(c) for c in self.children),
        )


# ---------- EGraph ----------

class EGraph:
    uf: UnionFind
    next_id: int

    classes: dict[EClassId, set[ENode]]
    parents: dict[EClassId, set[tuple[ENode, EClassId]]]
    hashcons: dict[ENode, EClassId]

    worklist: set[EClassId]

    def __init__(self) -> None:
        self.uf = UnionFind()
        self.next_id = 0

        self.classes = {}
        self.parents = defaultdict(set)
        self.hashcons = {}

        self.worklist = set()

    # ----- basic ops -----

    def _fresh_id(self) -> EClassId:
        eid = EClassId(self.next_id)
        self.next_id += 1
        return eid

    def find(self, eclass: EClassId) -> EClassId:
        return self.uf.find(eclass)

    def add(self, enode: ENode) -> EClassId:
        enode = enode.canonicalize(self.find)

        if enode in self.hashcons:
            return self.find(self.hashcons[enode])

        eid = self._fresh_id()

        self.uf.parent[eid] = eid
        self.classes[eid] = {enode}
        self.hashcons[enode] = eid

        for c in enode.children:
            self.parents[c].add((enode, eid))

        return eid

    def merge(self, a: EClassId, b: EClassId) -> EClassId:
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return ra

        r = self.uf.union(ra, rb)

        # merge e-nodes
        self.classes[r] |= self.classes[rb]
        del self.classes[rb]

        # merge parent info
        self.parents[r] |= self.parents[rb]
        del self.parents[rb]

        # defer congruence restoration
        self.worklist.add(r)

        return r

    # ----- rebuilding (egg-style) -----

    def rebuild(self) -> None:
        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:
        new_parents: dict[ENode, EClassId] = {}

        for (pnode, peclass) in list(self.parents[eclass]):
            # remove stale hashcons entry
            self.hashcons.pop(pnode, None)

            canon = pnode.canonicalize(self.find)
            peclass = self.find(peclass)

            if canon in new_parents:
                # congruent parents => upward merge
                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()}

    # ----- debug helpers -----

    def dump(self) -> None:
        print("E-Graph:")
        for eid, nodes in self.classes.items():
            print(f"  class {eid}:")
            for n in nodes:
                print(f"    {n}")
        print()


# ---------- Demo ----------

if __name__ == "__main__":
    eg = EGraph()

    # constants
    x = eg.add(ENode("x", ()))
    a = eg.add(ENode("a", ()))
    b = eg.add(ENode("b", ()))

    ax = eg.add(ENode("+", (a, x)))
    bx = eg.add(ENode("+", (b, x)))

    print("Before merge:")
    print("  (+ a x) eclass =", eg.find(ax))
    print("  (+ b x) eclass =", eg.find(bx))
    eg.dump()

    # assert a ≡ b
    eg.merge(a, b)
    eg.rebuild()

    print("After merge + rebuild:")
    print("  (+ a x) eclass =", eg.find(ax))
    print("  (+ b x) eclass =", eg.find(bx))
    eg.dump()

需要注意,这个demo无论是注释/风格/类型,都不适合我们这个仓库,需要做较大的改动

add的输入应该是一个apyds的Term,apyds的用法从/tests/下的python测试文件中可以了解。</issue_description>

C...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI self-assigned this Dec 22, 2025
Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com>
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Comment thread egg/tests/test_egraph.py Fixed
Copilot AI changed the title [WIP] Add support package named apyds-egg Add apyds-egg support package with E-Graph implementation Dec 22, 2025
Copilot AI requested a review from hzhangxyz December 22, 2025 00:46
Comment thread egg/apyds_egg/.gitignore Outdated
@@ -0,0 +1,25 @@
__pycache__/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

这个文件是不需要的, 会被repo root的ignore给忽略掉.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in b04b828. The repo root .gitignore handles the necessary patterns.

Comment thread egg/setup.py Outdated
@@ -0,0 +1,4 @@
import setuptools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

这个文件在现代的build backend中是不需要的.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in b04b828. The package builds correctly with just pyproject.toml using the setuptools build backend.

Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com>

@hzhangxyz hzhangxyz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  1. README.md 先删掉, 不要在此pr中实现
  2. init.py中我做了一些修改, 删除了一些意外的判断, 这个不需要判断, 类型标记写好, 然后假设用户只会这样输入, 我尽量修改了, 如果有漏的, 你也改一下
  3. 用from future import annotation 那个, 不要写 "ENode" 这样的字符串类型
  4. init.py 中我做了一些修改, 所以tests有些过不了了, 你修修删删.

…ests

Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com>

Copilot AI commented Dec 22, 2025

Copy link
Copy Markdown
Contributor Author
  1. README.md 先删掉, 不要在此pr中实现
  2. init.py中我做了一些修改, 删除了一些意外的判断, 这个不需要判断, 类型标记写好, 然后假设用户只会这样输入,...

All addressed in 9acd306:

  1. README.md removed
  2. Tests updated - removed error-checking tests that no longer apply
  3. Added from __future__ import annotations and changed "ENode" to ENode
  4. All 24 tests pass with 100% coverage

Comment thread egg/tests/test_egraph.py
def test_egraph_congruence():
eg = EGraph()

x = eg.add(apyds.Term("x"))
Comment thread egg/tests/test_egraph.py

a = eg.add(apyds.Term("a"))
b = eg.add(apyds.Term("b"))
c = eg.add(apyds.Term("c"))
Comment thread egg/tests/test_egraph.py
def test_egraph_complex_example():
eg = EGraph()

x = eg.add(apyds.Term("x"))
Comment thread egg/tests/test_egraph.py
def test_egraph_associativity_example():
eg = EGraph()

x = eg.add(apyds.Term("x"))
Comment thread egg/tests/test_egraph.py
eg = EGraph()

x = eg.add(apyds.Term("x"))
y = eg.add(apyds.Term("y"))
Comment thread egg/tests/test_egraph.py

x = eg.add(apyds.Term("x"))
y = eg.add(apyds.Term("y"))
z = eg.add(apyds.Term("z"))
Comment thread egg/tests/test_egraph.py
def test_egraph_hashcons():
eg = EGraph()

a = eg.add(apyds.Term("a"))
Comment thread egg/tests/test_egraph.py
eg = EGraph()

a = eg.add(apyds.Term("a"))
b = eg.add(apyds.Term("b"))
@hzhangxyz
hzhangxyz marked this pull request as ready for review December 22, 2025 03:15
Copilot AI review requested due to automatic review settings December 22, 2025 03:15
@hzhangxyz
hzhangxyz merged commit c200215 into main Dec 22, 2025
59 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 adds the apyds-egg support package, implementing an E-Graph (Equality Graph) data structure for representing and manipulating equivalence classes of terms. The package provides a Python implementation following a structure similar to the existing /bnf package.

Key changes:

  • Implementation of core E-Graph components: UnionFind, ENode, and EGraph classes
  • Integration with apyds.Term for seamless term handling
  • Comprehensive test suite with 24 tests covering basic operations, congruence, and edge cases

Reviewed changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
egg/pyproject.toml Package configuration with build system, dependencies, and tool settings
egg/uv.lock Dependency lock file with apyds 0.0.10 and dev dependencies
egg/apyds_egg/init.py Core E-Graph implementation with union-find, node canonicalization, and congruence maintenance
egg/apyds_egg/py.typed Type checking marker file for PEP 561 compliance
egg/tests/test_egraph.py Comprehensive test suite covering all E-Graph operations and edge cases

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread egg/apyds_egg/__init__.py
return eid

def merge(self, a: EClassId, b: EClassId) -> EClassId:
"""Merge two E-classes and schedule rebuilding."""

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

The merge method lacks complete documentation. It should document its parameters (what a and b represent), return value (what the returned EClassId represents), and the important side effect that rebuild() must be called after merge operations to maintain congruence.

Suggested change
"""Merge two E-classes and schedule rebuilding."""
"""Merge two E-classes and schedule rebuilding.
Parameters
----------
a : EClassId
The ID of the first E-class to merge.
b : EClassId
The ID of the second E-class to merge.
Returns
-------
EClassId
The canonical E-class ID representing the union of ``a`` and ``b``.
Notes
-----
This method updates internal parent/class/parent-links structures and
adds the resulting E-class to the worklist for congruence repair.
Call :meth:`rebuild` after performing merges to restore global
congruence in the E-Graph.
"""

Copilot uses AI. Check for mistakes.
Comment thread egg/apyds_egg/__init__.py
return eid

def find(self, eclass: EClassId) -> EClassId:
"""Find the canonical representative of an E-class."""

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

The find method lacks documentation about its parameter. It should document what eclass represents (an E-class ID to look up) and clarify that it returns the canonical representative after any merge operations.

Suggested change
"""Find the canonical representative of an E-class."""
"""Find the canonical representative of an E-class.
Args:
eclass: The E-class ID to look up.
Returns:
The canonical E-class ID representing ``eclass`` after applying
any union/merge operations.
"""

Copilot uses AI. Check for mistakes.
Comment thread egg/apyds_egg/__init__.py
Comment on lines +86 to +87
for i in range(len(inner)):
child_term = inner[i]

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

The loop iterating over List elements using range(len(inner)) could be simplified to iterate directly over the List. Instead of using a range-based loop, consider iterating directly: for child_term in inner. This would be more idiomatic Python and slightly more efficient.

Suggested change
for i in range(len(inner)):
child_term = inner[i]
for child_term in inner:

Copilot uses AI. Check for mistakes.
Comment thread egg/pyproject.toml
requires-python = ">=3.11, <3.15"
authors = [{ name = "Hao Zhang", email = "hzhangxyz@outlook.com" }]
description = "E-Graph implementation for apyds"
readme = "README.md"

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

The pyproject.toml references a README.md file that doesn't exist in the egg directory. This will cause package build failures. According to the PR description, the README.md was intentionally removed and will be added in a separate PR, so this reference should also be removed from pyproject.toml.

Suggested change
readme = "README.md"

Copilot uses AI. Check for mistakes.
Comment thread egg/tests/test_egraph.py
def test_egraph_congruence():
eg = EGraph()

x = eg.add(apyds.Term("x"))

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

Variable x is not used.

Suggested change
x = eg.add(apyds.Term("x"))

Copilot uses AI. Check for mistakes.
Comment thread egg/tests/test_egraph.py
def test_egraph_associativity_example():
eg = EGraph()

x = eg.add(apyds.Term("x"))

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

Variable x is not used.

Copilot uses AI. Check for mistakes.
Comment thread egg/tests/test_egraph.py
x = eg.add(apyds.Term("x"))
y = eg.add(apyds.Term("y"))
z = eg.add(apyds.Term("z"))

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

Variable y is not used.

Suggested change
assert isinstance(x, int)
assert isinstance(y, int)
assert isinstance(z, int)

Copilot uses AI. Check for mistakes.
Comment thread egg/tests/test_egraph.py
x = eg.add(apyds.Term("x"))
y = eg.add(apyds.Term("y"))
z = eg.add(apyds.Term("z"))

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

Variable z is not used.

Suggested change
assert isinstance(x, int)
assert isinstance(y, int)
assert isinstance(z, int)

Copilot uses AI. Check for mistakes.
Comment thread egg/tests/test_egraph.py
Comment on lines +281 to +282
a = eg.add(apyds.Term("a"))
b = eg.add(apyds.Term("b"))

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

Variable a is not used.

Suggested change
a = eg.add(apyds.Term("a"))
b = eg.add(apyds.Term("b"))
eg.add(apyds.Term("a"))
eg.add(apyds.Term("b"))

Copilot uses AI. Check for mistakes.
Comment thread egg/tests/test_egraph.py
eg = EGraph()

a = eg.add(apyds.Term("a"))
b = eg.add(apyds.Term("b"))

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

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

Variable b is not used.

Suggested change
b = eg.add(apyds.Term("b"))
eg.add(apyds.Term("b"))

Copilot uses AI. Check for mistakes.
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.

添加一个support package叫做apyds-egg

3 participants