Skip to content

Commit b32378e

Browse files
Merge pull request #352 from egraphs-good/tutorial
Add egglog tutorials, change display to not inline by default, and fix bug looking up binary methods
2 parents c8d24a3 + 6b97b85 commit b32378e

File tree

14 files changed

+1113
-15
lines changed

14 files changed

+1113
-15
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ _This project uses semantic versioning_
44

55
## UNRELEASED
66

7+
- Add egglog tutorials, change display to not inline by default, and fix bug looking up binary methods [#352](https://github.com/egraphs-good/egglog-python/pull/352)
78
- Add `back_off` scheduler [#350](https://github.com/egraphs-good/egglog-python/pull/350)
89
## 11.2.0 (2025-09-03)
910

docs/conf.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
post_auto_image = 1
1414

1515

16-
html_sidebars = {
17-
"**": [
18-
"sidebar-nav-bs",
19-
"ablog/postcard.html",
20-
"ablog/recentposts.html",
21-
"ablog/tagcloud.html",
22-
"ablog/categories.html",
23-
]
24-
}
16+
# html_sidebars = {
17+
# "**": [
18+
# "sidebar-nav-bs",
19+
# "ablog/postcard.html",
20+
# "ablog/recentposts.html",
21+
# "ablog/tagcloud.html",
22+
# "ablog/categories.html",
23+
# ]
24+
# }
2525
##
2626
# Myst
2727
##
@@ -146,8 +146,8 @@
146146
# myst_nb default settings
147147

148148
# Custom formats for reading notebook; suffix -> reader
149-
# nb_custom_formats = {}
150-
149+
# https://github.com/mwouts/jupytext/blob/main/docs/formats-scripts.md#the-light-format
150+
nb_custom_formats = {".py": ["jupytext.reads", {"fmt": "py:light"}]}
151151
# Notebook level metadata key for config overrides
152152
# nb_metadata_key = 'mystnb'
153153

docs/tutorials.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44
auto_examples/index
55
tutorials/getting-started
66
tutorials/sklearn
7+
tutorials/tut_1_basics
8+
tutorials/tut_2_datalog
9+
tutorials/tut_3_analysis
10+
tutorials/tut_4_scheduling
11+
tutorials/tut_5_extraction
712
```

docs/tutorials/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Tutorials
3+
"""

docs/tutorials/tut_1_basics.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# # 01 - Basics of Equality Saturation
2+
#
3+
# _[This tutorial is translated from egglog.](https://egraphs-good.github.io/egglog-tutorial/01-basics.html)_
4+
#
5+
# In this tutorial, we will build an optimizer for a subset of linear algebra using egglog.
6+
# We will start by optimizing simple integer arithmetic expressions.
7+
# Our initial DSL supports constants, variables, addition, and multiplication.
8+
9+
# +
10+
# mypy: disable-error-code="empty-body"
11+
from __future__ import annotations
12+
from typing import TypeAlias
13+
from collections.abc import Iterable
14+
from egglog import *
15+
16+
17+
class Num(Expr):
18+
def __init__(self, value: i64Like) -> None: ...
19+
20+
@classmethod
21+
def var(cls, name: StringLike) -> Num: ...
22+
23+
def __add__(self, other: NumLike) -> Num: ...
24+
def __mul__(self, other: NumLike) -> Num: ...
25+
26+
# Support inverse operations for convenience
27+
# they will be translated to non-reversed ones
28+
def __radd__(self, other: NumLike) -> Num: ...
29+
def __rmul__(self, other: NumLike) -> Num: ...
30+
31+
32+
NumLike: TypeAlias = Num | StringLike | i64Like
33+
# -
34+
35+
36+
# The signature here takes `NumLike` not `Num` so that you can write `Num(1) + 2` instead of
37+
# `Num(1) + Num(2)`. This is helpful for ease of use and also for compatibility when you are trying to
38+
# create expressions that act like Python objects which perform upcasting.
39+
#
40+
# To support this, you must define conversions between primitive types and your expression types.
41+
# When a value is passed into a function, it will find the type it should be converted to and
42+
# transitively apply the conversions you have defined:
43+
44+
converter(i64, Num, Num)
45+
converter(String, Num, Num.var)
46+
47+
# Now, let's define some simple expressions.
48+
49+
egraph = EGraph()
50+
x = Num.var("x")
51+
expr1 = egraph.let("expr1", 2 * (x * 3))
52+
expr2 = egraph.let("expr2", 6 * x)
53+
54+
# You should see an e-graph with two expressions.
55+
56+
egraph
57+
58+
# We can `.extract` the values of the expressions as well to see their fully expanded forms.
59+
60+
egraph.extract(String("Hello, world!"))
61+
62+
egraph.extract(i64(42))
63+
64+
egraph.extract(expr1)
65+
66+
egraph.extract(expr2)
67+
68+
# We can use the `check` commands to check properties of our e-graph.
69+
70+
x, y = vars_("x y", Num)
71+
egraph.check(expr1 == x * y)
72+
73+
# This checks if `expr1` is equivalent to some expression `x * y`, where `x` and `y` are
74+
# variables that can be mapped to any `Num` expression in the e-graph.
75+
#
76+
# Checks can fail. For example the following check fails because `expr1` is not equivalent to
77+
# `x + y` for any `x` and `y` in the e-graph.
78+
79+
egraph.check_fail(expr1 == x + y)
80+
81+
# Let us define some rewrite rules over our small DSL.
82+
83+
84+
@egraph.register
85+
def _add_comm(x: Num, y: Num):
86+
yield rewrite(x + y).to(y + x)
87+
88+
89+
# This could also been written like:
90+
#
91+
# ```python
92+
# x, y = vars_("x y", Num)
93+
# egraph.register(rewrite(x + y).to(y + x))
94+
# ```
95+
#
96+
# In this tutorial we will use the function form to define rewrites and rules, because then then we only
97+
# have to write the variable names once as arguments and they are not leaked to the outer scope.
98+
99+
100+
# This rule asserts that addition is commutative. More concretely, this rules says, if the e-graph
101+
# contains expressions of the form `x + y`, then the e-graph should also contain the
102+
# expression `y + x`, and they should be equivalent.
103+
#
104+
# Similarly, we can define the associativity rule for addition.
105+
106+
107+
@egraph.register
108+
def _add_assoc(x: Num, y: Num, z: Num) -> Iterable[RewriteOrRule]:
109+
yield rewrite(x + (y + z)).to((x + y) + z)
110+
111+
112+
# This rule says, if the e-graph contains expressions of the form `x + (y + z)`, then the e-graph should also contain
113+
# the expression `(x + y) + z`, and they should be equivalent.
114+
115+
# There are two subtleties to rules:
116+
#
117+
# 1. Defining a rule is different from running it. The following check would fail at this point
118+
# because the commutativity rule has not been run (we've inserted `x + 3` but not yet derived `3 + x`).
119+
120+
egraph.check_fail((x + 3) == (3 + x))
121+
122+
# 2. Rules are not instantiated for every possible term; they are only instantiated for terms that are
123+
# in the e-graph. For instance, even if we ran the commutativity rule above, the following check would
124+
# still fail because the e-graph does not contain either of the terms `Num(-2) + Num(2)` or `Num(2) + Num(-2)`.
125+
126+
egraph.check_fail(Num(-2) + 2 == Num(2) + -2)
127+
128+
# Let's also define commutativity and associativity for multiplication.
129+
130+
131+
@egraph.register
132+
def _mul(x: Num, y: Num, z: Num) -> Iterable[RewriteOrRule]:
133+
yield rewrite(x * y).to(y * x)
134+
yield rewrite(x * (y * z)).to((x * y) * z)
135+
136+
137+
# `egglog` also defines a set of built-in functions over primitive types, such as `+` and `*`,
138+
# and supports operator overloading, so the same operator can be used with different types.
139+
140+
egraph.extract(i64(1) + 2)
141+
142+
egraph.extract(String("1") + "2")
143+
144+
egraph.extract(f64(1.0) + 2.0)
145+
146+
# With primitives, we can define rewrite rules that talk about the semantics of operators.
147+
# The following rules show constant folding over addition and multiplication.
148+
149+
150+
@egraph.register
151+
def _const_fold(a: i64, b: i64) -> Iterable[RewriteOrRule]:
152+
yield rewrite(Num(a) + Num(b)).to(Num(a + b))
153+
yield rewrite(Num(a) * Num(b)).to(Num(a * b))
154+
155+
156+
# While we have defined several rules, the e-graph has not changed since we inserted the two
157+
# expressions. To run rules we have defined so far, we can use `run`.
158+
159+
egraph.run(10)
160+
161+
# This tells `egglog` to run our rules for 10 iterations. More precisely, egglog runs the
162+
# following pseudo code:
163+
#
164+
# ```
165+
# for i in range(10):
166+
# for r in rules:
167+
# ms = r.find_matches(egraph)
168+
# for m in ms:
169+
# egraph = egraph.apply_rule(r, m)
170+
# egraph = rebuild(egraph)
171+
# ```
172+
#
173+
# In other words, `egglog` computes all the matches for one iteration before making any
174+
# updates to the e-graph. This is in contrast to an evaluation model where rules are immediately
175+
# applied and the matches are obtained on demand over a changing e-graph.
176+
#
177+
# We can now look at the e-graph and see that that `2 * (x + 3)` and `6 + (2 * x)` are now in the same E-class.
178+
179+
egraph
180+
181+
# We can also check this fact explicitly
182+
183+
egraph.check(expr1 == expr2)

0 commit comments

Comments
 (0)