Skip to content

Commit 5c2d07e

Browse files
Merge origin/main into pre-commit-ci-update-config
2 parents 8b63db0 + 5b25a5e commit 5c2d07e

28 files changed

+1740
-788
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Update Changelog
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited]
6+
7+
jobs:
8+
update-changelog:
9+
# Only run if this is not a PR from a fork to avoid permission issues
10+
# and not a commit made by GitHub Action to avoid infinite loops
11+
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'github-actions[bot]'
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
# Checkout the PR head ref
22+
ref: ${{ github.event.pull_request.head.ref }}
23+
token: ${{ secrets.GITHUB_TOKEN }}
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.12'
29+
30+
- name: Update changelog
31+
run: |
32+
python modify_changelog.py update_changelog \
33+
"${{ github.event.pull_request.number }}" \
34+
"${{ github.event.pull_request.title }}"
35+
36+
- name: Check for changes
37+
id: changes
38+
run: |
39+
if git diff --quiet docs/changelog.md; then
40+
echo "changed=false" >> $GITHUB_OUTPUT
41+
else
42+
echo "changed=true" >> $GITHUB_OUTPUT
43+
fi
44+
45+
- name: Commit and push changes
46+
if: steps.changes.outputs.changed == 'true'
47+
run: |
48+
git config --local user.email "[email protected]"
49+
git config --local user.name "GitHub Action"
50+
git add docs/changelog.md
51+
git commit -m "Add changelog entry for PR #${{ github.event.pull_request.number }}"
52+
git push

.github/workflows/version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- run: |
4040
git config user.name github-actions[bot]
4141
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
42-
VERSION=$(python increment_version.py $TYPE)
42+
VERSION=$(python modify_changelog.py bump_version $TYPE)
4343
git checkout -b "version-$VERSION"
4444
git commit -am "Version $VERSION"
4545
git push -u origin HEAD

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ _This project uses semantic versioning_
44

55
## UNRELEASED
66

7+
- Add ability to parse egglog expressions into Python values [#319](https://github.com/egraphs-good/egglog-python/pull/319)
8+
- Deprecates `.eval()` method on primitives in favor of `.value` which can be used with pattern matching.
9+
- Support methods like on expressions [#315](https://github.com/egraphs-good/egglog-python/pull/315)
10+
- Automatically Create Changelog Entry for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313)
711
- Upgrade egglog which includes new backend.
812
- Fixes implementation of the Python Object sort to work with objects with dupliating hashes but the same value.
913
Also changes the representation to be an index into a list instead of the ID, making egglog programs more deterministic.
1014
- Prefix constant declerations and unbound variables to not shadow let variables
1115
- BREAKING: Remove `simplify` since it was removed upstream. You can manually replace it with an insert, run, then extract.
16+
- Change how anonymous functions are converted to remove metaprogramming and lift only the unbound variables as args
17+
- Add support for getting the "value" of a function type with `.eval()`, i.e. `assert UnstableFn(f).eval() == f`.
1218

1319
## 10.0.2 (2025-06-22)
1420

docs/reference/contributing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ or bug fixes.
8484

8585
When you are ready to submit your changes, please open a pull request. The CI will run the tests and check the code style.
8686

87+
#### Changelog Automation
88+
89+
When you open a pull request, a GitHub Action automatically adds an entry to the UNRELEASED section of the changelog using your PR title and number. This ensures the changelog stays up-to-date without manual intervention.
90+
8791
## Documentation
8892

8993
We use the [Diátaxis framework](https://diataxis.fr/) to organize our documentation. The "explanation" section has

docs/reference/egglog-translation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ and also will make sure the variables won't be used outside of the scope of the
340340
# egg: (rewrite (Mul a b) (Mul b a))
341341
# egg: (rewrite (Add a b) (Add b a))
342342
343-
@egraph.register
343+
@EGraph().register
344344
def _math(a: Math, b: Math):
345345
yield rewrite(a * b).to(b * a)
346346
yield rewrite(a + b).to(b + a)

docs/reference/python-integration.md

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,88 @@ file_format: mystnb
66

77
Alongside [the support for builtin `egglog` functionality](./egglog-translation.md), `egglog` also provides functionality to more easily integrate with the Python ecosystem.
88

9-
## Retrieving Primitive Values
9+
## Retrieving Values
1010

11-
If you have a egglog primitive, you can turn it into a Python object by using `egraph.eval(...)` method:
11+
If you have an egglog value, you might want to convert it from an expression to a native Python object. This is done through a number of helper functions:
12+
13+
For a primitive value (like `i64`, `f64`, `Bool`, `String`, or `PyObject`), use `get_literal_value(expr)` or the `.value` property:
1214

1315
```{code-cell} python
1416
from __future__ import annotations
1517
1618
from egglog import *
1719
18-
egraph = EGraph()
19-
assert egraph.eval(i64(1) + 20) == 21
20+
assert get_literal_value(i64(42)) == 42
21+
assert get_literal_value(i64(42) + i64(1)) == None # This is because i64(42) + i64(1) is a call expression, not a literal
22+
assert i64(42).value == 42
23+
assert get_literal_value(f64(3.14)) == 3.14
24+
assert Bool(True).value is True
25+
assert String("hello").value == "hello"
26+
assert PyObject([1,2,3]).value == [1,2,3]
27+
```
28+
29+
To check if an expression is a let value and get its name, use `get_let_name(expr)`:
30+
31+
```{code-cell} python
32+
x = EGraph().let("my_var", i64(1))
33+
assert get_let_name(x) == "my_var"
34+
```
35+
36+
To check if an expression is a variable and get its name, use `get_var_name(expr)`:
37+
38+
```{code-cell} python
39+
from egglog import var, get_var_name
40+
v = var("x", i64)
41+
assert get_var_name(v) == "x"
42+
```
43+
44+
For a callable (method, function, classmethod, or constructor), use `get_callable_fn(expr)` to get the underlying Python function:
45+
46+
```{code-cell} python
47+
expr = i64(1) + i64(2)
48+
fn = get_callable_fn(expr)
49+
assert fn == i64.__add__
50+
```
51+
52+
To get the arguments to a callable, use `get_callable_args(expr)`. If you want to match against a specific callable, use `get_callable_args(expr, fn)`, where `fn` is the Python function you want to match against. This will return `None` if the callable does not match the function, and if it does match, the args will be properly typed:
53+
54+
```{code-cell} python
55+
assert get_callable_args(expr) == (i64(1), i64(2))
56+
57+
assert get_callable_args(expr, i64.__add__) == (i64(1), i64(2))
58+
assert get_callable_args(expr, i64.__sub__) == None
59+
```
60+
61+
### Pattern Matching
62+
63+
You can use Python's structural pattern matching (`match`/`case`) to destructure egglog primitives:
64+
65+
```{code-cell} python
66+
x = i64(5)
67+
match i64(5):
68+
case i64(i):
69+
print(f"Integer literal: {i}")
70+
```
71+
72+
You can add custom support for pattern matching against your classes by adding `__match_args__` to your class:
73+
74+
```python
75+
class MyExpr(Expr):
76+
def __init__(self, value: StringLike): ...
77+
78+
__match_args__ = ("value",)
79+
80+
@method(preserve=True)
81+
@property
82+
def value(self) -> str:
83+
match get_callable_args(self, MyExpr):
84+
case (String(value),):
85+
return value
86+
raise ExprValueError(self, "MyExpr")
87+
88+
match MyExpr("hello"):
89+
case MyExpr(value):
90+
print(f"Matched MyExpr with value: {value}")
2091
```
2192

2293
## Python Object Sort
@@ -53,10 +124,10 @@ Creating hashable objects is safer, since while the rule might create new Python
53124

54125
### Retrieving Python Objects
55126

56-
Like other primitives, we can retrieve the Python object from the e-graph by using the `egraph.eval(...)` method:
127+
Like other primitives, we can retrieve the Python object from the e-graph by using the `.value` property:
57128

58129
```{code-cell} python
59-
assert egraph.eval(lst) == [1, 2, 3]
130+
assert lst.value == [1, 2, 3]
60131
```
61132

62133
### Builtin methods
@@ -66,29 +137,29 @@ Currently, we only support a few methods on `PyObject`s, but we plan to add more
66137
Conversion to/from a string:
67138

68139
```{code-cell} python
69-
egraph.eval(PyObject('hi').to_string())
140+
EGraph().extract(PyObject('hi').to_string())
70141
```
71142

72143
```{code-cell} python
73-
egraph.eval(PyObject.from_string("1"))
144+
EGraph().extract(PyObject.from_string("1"))
74145
```
75146

76147
Conversion from an int:
77148

78149
```{code-cell} python
79-
egraph.eval(PyObject.from_int(1))
150+
EGraph().extract(PyObject.from_int(1))
80151
```
81152

82153
We also support evaluating arbitrary Python code, given some locals and globals. This technically allows us to implement any Python method:
83154

84155
```{code-cell} python
85-
egraph.eval(py_eval("1 + 2"))
156+
EGraph().extract(py_eval("1 + 2"))
86157
```
87158

88159
Executing Python code is also supported. In this case, the return value will be the updated globals dict, which will be copied first before using.
89160

90161
```{code-cell} python
91-
egraph.eval(py_exec("x = 1 + 2"))
162+
EGraph().extract(py_exec("x = 1 + 2"))
92163
```
93164

94165
Alongside this, we support a function `dict_update` method, which can allow you to combine some local egglog expressions alongside, say, the locals and globals of the Python code you are evaluating.
@@ -100,7 +171,7 @@ def my_add(a, b):
100171
101172
amended_globals = PyObject(globals()).dict_update("one", 1)
102173
evalled = py_eval("my_add(one, 2)", locals(), amended_globals)
103-
assert egraph.eval(evalled) == 3
174+
assert EGraph().extract(evalled).value == 3
104175
```
105176

106177
### Simpler Eval
@@ -116,7 +187,7 @@ def my_add(a, b):
116187
return a + b
117188
118189
evalled = py_eval_fn(lambda a: my_add(a, 2))(1)
119-
assert egraph.eval(evalled) == 3
190+
assert EGraph().extract(evalled).value == 3
120191
```
121192

122193
## Functions
@@ -263,10 +334,12 @@ class Boolean(Expr):
263334
# Run until the e-graph saturates
264335
egraph.run(10)
265336
# Extract the Python object from the e-graph
266-
return egraph.eval(self.to_py())
267-
268-
def to_py(self) -> PyObject:
269-
...
337+
value = EGraph().extract(self)
338+
if value == TRUE:
339+
return True
340+
elif value == FALSE:
341+
return False
342+
raise ExprValueError(self, "Boolean expression must be TRUE or FALSE")
270343
271344
def __or__(self, other: Boolean) -> Boolean:
272345
...
@@ -278,8 +351,6 @@ FALSE = egraph.constant("FALSE", Boolean)
278351
@egraph.register
279352
def _bool(x: Boolean):
280353
return [
281-
set_(TRUE.to_py()).to(PyObject(True)),
282-
set_(FALSE.to_py()).to(PyObject(False)),
283354
rewrite(TRUE | x).to(TRUE),
284355
rewrite(FALSE | x).to(x),
285356
]
@@ -303,6 +374,11 @@ Note that the following list of methods are only supported as "preserved" since
303374
- `__iter_`
304375
- `__index__`
305376

377+
If you want to register additional methods as always preserved and defined on the `Expr` class itself, if needed
378+
instead of the normal mechanism which relies on `__getattr__`, you can call `egglog.define_expr_method(name: str)`,
379+
with the name of a method. This is only needed for third party code that inspects the type object itself to see if a
380+
method is defined instead of just attempting to call it.
381+
306382
### Reflected methods
307383

308384
Note that reflected methods (i.e. `__radd__`) are handled as a special case. If defined, they won't create their own egglog functions.

increment_version.py

Lines changed: 0 additions & 79 deletions
This file was deleted.

0 commit comments

Comments
 (0)