Skip to content

Commit 08392aa

Browse files
Merge branch 'main' into copilot/fix-316
2 parents c771348 + f5a0db9 commit 08392aa

31 files changed

+3118
-1934
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
- run: |
6969
export UV_PROJECT_ENVIRONMENT="${pythonLocation}"
7070
uv sync --extra test --locked
71-
- uses: CodSpeedHQ/action@v3.7.0
71+
- uses: CodSpeedHQ/action@v3.8.0
7272
with:
7373
token: ${{ secrets.CODSPEED_TOKEN }}
7474
# allow updating snapshots due to indeterministic benchmarks

.github/workflows/version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ jobs:
153153
if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch')
154154
needs: [macos, windows, linux, linux-cross, bump]
155155
steps:
156-
- uses: actions/download-artifact@v4
156+
- uses: actions/download-artifact@v5
157157
with:
158158
pattern: wheels-*
159159
merge-multiple: true

.pre-commit-config.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
exclude: ^python/tests/__snapshots__/
22
repos:
33
- repo: https://github.com/astral-sh/ruff-pre-commit
4-
rev: v0.11.2
4+
rev: v0.12.7
55
hooks:
6-
- id: ruff
7-
args: [--fix, --exit-non-zero-on-fix]
6+
- id: ruff-check
7+
args: [--fix]
88
- id: ruff-format
99
- repo: https://github.com/astral-sh/uv-pre-commit
10-
# uv version.
11-
rev: 0.6.11
10+
rev: 0.8.5
1211
hooks:
1312
- id: uv-lock

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "egglog_python"
3-
version = "10.0.2"
3+
version = "11.0.0"
44
edition = "2024"
55

66

docs/changelog.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ _This project uses semantic versioning_
44

55
## UNRELEASED
66

7-
- [WIP] Fix automatic generic of changelog entries with markdown [#317](https://github.com/egraphs-good/egglog-python/pull/317)
8-
- Automatically Create Changelog Entry for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313)
7+
- Change conversion between binary operators to consider converting both types [#320](https://github.com/egraphs-good/egglog-python/pull/320)
8+
- Add ability to parse egglog expressions into Python values [#319](https://github.com/egraphs-good/egglog-python/pull/319)
9+
- Deprecates `.eval()` method on primitives in favor of `.value` which can be used with pattern matching.
10+
- Support methods like on expressions [#315](https://github.com/egraphs-good/egglog-python/pull/315)
11+
- Automatically Create Changelog Entry for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313) [#317](https://github.com/egraphs-good/egglog-python/pull/317)
912
- Upgrade egglog which includes new backend.
1013
- Fixes implementation of the Python Object sort to work with objects with dupliating hashes but the same value.
1114
Also changes the representation to be an index into a list instead of the ID, making egglog programs more deterministic.

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: 99 additions & 28 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,13 +374,18 @@ Note that the following list of methods are only supported as "preserved" since
303374
- `__iter_`
304375
- `__index__`
305376

306-
### Reflected methods
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.
307381

308-
Note that reflected methods (i.e. `__radd__`) are handled as a special case. If defined, they won't create their own egglog functions.
382+
### Binary Method Conversions
309383

310-
Instead, whenever a reflected method is called, we will try to find the corresponding non-reflected method and call that instead.
384+
For [rich comparison methods](https://docs.python.org/3/reference/datamodel.html#object.__lt__) (like `__lt__`, `__le__`, `__eq__`, etc.) and [binary numeric methods](https://docs.python.org/3/reference/datamodel.html#object.__add__) (like `__add__`, `__sub__`, etc.), some more advanced conversion logic is needed to ensure they are converted properly. We add the `__r<name>__` methods for all expressions so that we can handle either position they are placed in.
311385

312-
Also, if a normal method fails because the arguments cannot be converted to the right types, the reflected version of the second arg will be tried.
386+
If we have two values `lhs` and `rhs`, we will try to find the minimum cost conversion for both of them, and then call the method on the converted values.
387+
If both are expression instances, we will convert at most one of them. However, if one is an expression and the other
388+
is a different Python value (like an `int`), we will consider all possible conversions of both arguments to find the minimum.
313389

314390
```{code-cell} python
315391
class Int(Expr):
@@ -347,11 +423,6 @@ converter(Int, Float, Float.from_int)
347423
assert str(-1.0 + Int.var("x")) == "Float(-1.0) + Float.from_int(Int.var(\"x\"))"
348424
```
349425

350-
For methods which allow returning `NotImplemented`, i.e. the comparison + binary math methods, we will also try upcasting both
351-
types to the type which is lowest cost to convert both to.
352-
353-
For example, if you have `Float` and `Int` wrapper types and you have write the expr `-1.0 + Int.var("x")` you might want the result to be `Float(-1.0) + Float.from_int(Int.var("x"))`:
354-
355426
### Mutating arguments
356427

357428
In order to support Python functions and methods which mutate their arguments, you can pass in the `mutate_first_arg` keyword argument to the `@function` decorator and the `mutates_self` argument to the `@method` decorator. This will cause the first argument to be mutated in place, instead of being copied.

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,11 @@ ignore = [
199199
# allow blind exception to add context
200200
"BLE001",
201201
# Don't move type checking around so that can be accessed at runtime
202-
"TCH001",
203-
"TCH002",
204-
"TCH003",
202+
"TC001",
203+
"TC002",
204+
"TC003",
205+
# allow eq without hash
206+
"PLW1641",
205207
]
206208
select = ["ALL"]
207209

python/egglog/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from . import config, ipython_magic # noqa: F401
66
from .bindings import EggSmolError # noqa: F401
77
from .builtins import * # noqa: UP029
8-
from .conversion import ConvertError, convert, converter, get_type_args # noqa: F401
8+
from .conversion import *
9+
from .deconstruct import *
910
from .egraph import *
11+
from .runtime import define_expr_method as define_expr_method
1012

1113
del ipython_magic

0 commit comments

Comments
 (0)