Skip to content

Add ability to parse egglog expressions into Python values #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ _This project uses semantic versioning_

## UNRELEASED

- Support methods like on expressions [#315](https://github.com/egraphs-good/egglog-python/pull/315)
- Add ability to parse egglog expressions into Python values [#319](https://github.com/egraphs-good/egglog-python/pull/319)
- Deprecates `.eval()` method on primitives in favor of `.value` which can be used with pattern matching.
- 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.
- Fixes implementation of the Python Object sort to work with objects with dupliating hashes but the same value.
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
109 changes: 90 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
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
Loading