Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ _This project uses semantic versioning_

## UNRELEASED

- Add ability to parse egglog expressions into Python values [#319](https://github.com/egraphs-good/egglog-python/pull/319)
- Support methods like on expressions [#315](https://github.com/egraphs-good/egglog-python/pull/315)
- Automatically Create Changelog Entry for PRs [#313](https://github.com/egraphs-good/egglog-python/pull/313)
- Upgrade egglog which includes new backend.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/egglog-translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ and also will make sure the variables won't be used outside of the scope of the
# egg: (rewrite (Mul a b) (Mul b a))
# egg: (rewrite (Add a b) (Add b a))

@egraph.register
@EGraph().register
def _math(a: Math, b: Math):
yield rewrite(a * b).to(b * a)
yield rewrite(a + b).to(b + a)
Expand Down
113 changes: 94 additions & 19 deletions docs/reference/python-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,88 @@ file_format: mystnb

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

## Retrieving Primitive Values
## Retrieving Values

If you have a egglog primitive, you can turn it into a Python object by using `egraph.eval(...)` method:
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:

For a primitive value (like `i64`, `f64`, `Bool`, `String`, or `PyObject`), use `get_literal_value(expr)` or the `.value` property:

```{code-cell} python
from __future__ import annotations

from egglog import *

egraph = EGraph()
assert egraph.eval(i64(1) + 20) == 21
assert get_literal_value(i64(42)) == 42
assert get_literal_value(i64(42) + i64(1)) == None # This is because i64(42) + i64(1) is a call expression, not a literal
assert i64(42).value == 42
assert get_literal_value(f64(3.14)) == 3.14
assert Bool(True).value is True
assert String("hello").value == "hello"
assert PyObject([1,2,3]).value == [1,2,3]
```

To check if an expression is a let value and get its name, use `get_let_name(expr)`:

```{code-cell} python
x = EGraph().let("my_var", i64(1))
assert get_let_name(x) == "my_var"
```

To check if an expression is a variable and get its name, use `get_var_name(expr)`:

```{code-cell} python
from egglog import var, get_var_name
v = var("x", i64)
assert get_var_name(v) == "x"
```

For a callable (method, function, classmethod, or constructor), use `get_callable_fn(expr)` to get the underlying Python function:

```{code-cell} python
expr = i64(1) + i64(2)
fn = get_callable_fn(expr)
assert fn == i64.__add__
```

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:

```{code-cell} python
assert get_callable_args(expr) == (i64(1), i64(2))

assert get_callable_args(expr, i64.__add__) == (i64(1), i64(2))
assert get_callable_args(expr, i64.__sub__) == None
```

### Pattern Matching

You can use Python's structural pattern matching (`match`/`case`) to destructure egglog primitives:

```{code-cell} python
x = i64(5)
match i64(5):
case i64(i):
print(f"Integer literal: {i}")
```

You can add custom support for pattern matching against your classes by adding `__match_args__` to your class:

```python
class MyExpr(Expr):
def __init__(self, value: StringLike): ...

__match_args__ = ("value",)

@method(preserve=True)
@property
def value(self) -> str:
match get_callable_args(self, MyExpr):
case (String(value),):
return value
raise ExprValueError(self, "MyExpr")

match MyExpr("hello"):
case MyExpr(value):
print(f"Matched MyExpr with value: {value}")
```

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

### Retrieving Python Objects

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

```{code-cell} python
assert egraph.eval(lst) == [1, 2, 3]
assert lst.value == [1, 2, 3]
```

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

```{code-cell} python
egraph.eval(PyObject('hi').to_string())
EGraph().extract(PyObject('hi').to_string())
```

```{code-cell} python
egraph.eval(PyObject.from_string("1"))
EGraph().extract(PyObject.from_string("1"))
```

Conversion from an int:

```{code-cell} python
egraph.eval(PyObject.from_int(1))
EGraph().extract(PyObject.from_int(1))
```

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

```{code-cell} python
egraph.eval(py_eval("1 + 2"))
EGraph().extract(py_eval("1 + 2"))
```

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.

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

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.
Expand All @@ -100,7 +171,7 @@ def my_add(a, b):

amended_globals = PyObject(globals()).dict_update("one", 1)
evalled = py_eval("my_add(one, 2)", locals(), amended_globals)
assert egraph.eval(evalled) == 3
assert EGraph().extract(evalled).value == 3
```

### Simpler Eval
Expand All @@ -116,7 +187,7 @@ def my_add(a, b):
return a + b

evalled = py_eval_fn(lambda a: my_add(a, 2))(1)
assert egraph.eval(evalled) == 3
assert EGraph().extract(evalled).value == 3
```

## Functions
Expand Down Expand Up @@ -263,10 +334,12 @@ class Boolean(Expr):
# Run until the e-graph saturates
egraph.run(10)
# Extract the Python object from the e-graph
return egraph.eval(self.to_py())

def to_py(self) -> PyObject:
...
value = EGraph().extract(self)
if value == TRUE:
return True
elif value == FALSE:
return False
raise ExprValueError(self, "Boolean expression must be TRUE or FALSE")

def __or__(self, other: Boolean) -> Boolean:
...
Expand All @@ -278,8 +351,6 @@ FALSE = egraph.constant("FALSE", Boolean)
@egraph.register
def _bool(x: Boolean):
return [
set_(TRUE.to_py()).to(PyObject(True)),
set_(FALSE.to_py()).to(PyObject(False)),
rewrite(TRUE | x).to(TRUE),
rewrite(FALSE | x).to(x),
]
Expand Down Expand Up @@ -568,3 +639,7 @@ r = ruleset(
)
egraph.saturate(r)
```

```

```
1 change: 1 addition & 0 deletions python/egglog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .bindings import EggSmolError # noqa: F401
from .builtins import * # noqa: UP029
from .conversion import *
from .deconstruct import *
from .egraph import *
from .runtime import define_expr_method as define_expr_method # noqa: PLC0414

Expand Down
Loading