Skip to content

Commit 5b25a5e

Browse files
Merge pull request #319 from egraphs-good/add-deconstruct
Add ability to parse egglog expressions into Python values
2 parents cbf9160 + 8876276 commit 5b25a5e

16 files changed

+758
-263
lines changed

docs/changelog.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ _This project uses semantic versioning_
44

55
## UNRELEASED
66

7-
- Support methods like on expressions [#315](https://github.com/egraphs-good/egglog-python/pull/315)
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)
810
- Automatically Create Changelog Entry for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313)
911
- Upgrade egglog which includes new backend.
1012
- Fixes implementation of the Python Object sort to work with objects with dupliating hashes but the same value.

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: 90 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
]

python/egglog/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .bindings import EggSmolError # noqa: F401
77
from .builtins import * # noqa: UP029
88
from .conversion import *
9+
from .deconstruct import *
910
from .egraph import *
1011
from .runtime import define_expr_method as define_expr_method # noqa: PLC0414
1112

0 commit comments

Comments
 (0)