You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/reference/python-integration.md
+99-28Lines changed: 99 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,17 +6,88 @@ file_format: mystnb
6
6
7
7
Alongside [the support for builtin `egglog` functionality](./egglog-translation.md), `egglog` also provides functionality to more easily integrate with the Python ecosystem.
8
8
9
-
## Retrieving Primitive Values
9
+
## Retrieving Values
10
10
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:
12
14
13
15
```{code-cell} python
14
16
from __future__ import annotations
15
17
16
18
from egglog import *
17
19
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:
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
+
classMyExpr(Expr):
76
+
def__init__(self, value: StringLike): ...
77
+
78
+
__match_args__= ("value",)
79
+
80
+
@method(preserve=True)
81
+
@property
82
+
defvalue(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}")
20
91
```
21
92
22
93
## Python Object Sort
@@ -53,10 +124,10 @@ Creating hashable objects is safer, since while the rule might create new Python
53
124
54
125
### Retrieving Python Objects
55
126
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:
57
128
58
129
```{code-cell} python
59
-
assert egraph.eval(lst) == [1, 2, 3]
130
+
assert lst.value == [1, 2, 3]
60
131
```
61
132
62
133
### Builtin methods
@@ -66,29 +137,29 @@ Currently, we only support a few methods on `PyObject`s, but we plan to add more
66
137
Conversion to/from a string:
67
138
68
139
```{code-cell} python
69
-
egraph.eval(PyObject('hi').to_string())
140
+
EGraph().extract(PyObject('hi').to_string())
70
141
```
71
142
72
143
```{code-cell} python
73
-
egraph.eval(PyObject.from_string("1"))
144
+
EGraph().extract(PyObject.from_string("1"))
74
145
```
75
146
76
147
Conversion from an int:
77
148
78
149
```{code-cell} python
79
-
egraph.eval(PyObject.from_int(1))
150
+
EGraph().extract(PyObject.from_int(1))
80
151
```
81
152
82
153
We also support evaluating arbitrary Python code, given some locals and globals. This technically allows us to implement any Python method:
83
154
84
155
```{code-cell} python
85
-
egraph.eval(py_eval("1 + 2"))
156
+
EGraph().extract(py_eval("1 + 2"))
86
157
```
87
158
88
159
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.
89
160
90
161
```{code-cell} python
91
-
egraph.eval(py_exec("x = 1 + 2"))
162
+
EGraph().extract(py_exec("x = 1 + 2"))
92
163
```
93
164
94
165
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.
@@ -303,13 +374,18 @@ Note that the following list of methods are only supported as "preserved" since
303
374
-`__iter_`
304
375
-`__index__`
305
376
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.
307
381
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
309
383
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.
311
385
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.
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
-
355
426
### Mutating arguments
356
427
357
428
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.
0 commit comments