Getting Started With Egglog Elixir
Copy MarkdownPrerequisites
This notebook uses egglog_elixir, which includes a Rustler NIF. Rustler
compiles a small native Rust library the first time Livebook evaluates
Mix.install/1.
The Livebook runtime must have:
- Rust installed
cargoavailable onPATH- Graphviz installed, with
dotavailable onPATH, if you want SVG e-graph pictures
Check this in the same shell or container that starts Livebook:
cargo --version
rustc --version
dot -V
If Cargo is installed through rustup, make sure Livebook sees it:
export PATH="$HOME/.cargo/bin:$PATH"
livebook server
When using the published package, replace the dependency with {:egglog_elixir, "~> 0.1"}.
Mix.install([
{:egglog_elixir, path: Path.expand("..", __DIR__)},
{:kino, "~> 0.19.0"}
])What This Notebook Teaches
This is a first-pass tour of the wrapper. It keeps the egglog language visible and uses Elixir only to load theories, submit problems, print outputs, and render snapshots.
The examples are a small "best of" from the first three upstream egglog tutorial chapters:
- equality saturation over symbolic arithmetic
- congruence closure through constructors
- Datalog-style reachability
- analysis facts used as guards for rewrites
- the difference between
Egglog.ProgramandEgglog.EGraph
The important boundary is:
- egglog does parsing, e-matching, congruence closure, Datalog evaluation, rebuilding, scheduling, and extraction
- Elixir submits source/input and receives outputs
- snapshots come from native egglog serialization, not from an Elixir graph reconstruction
Tiny Helpers
These helpers are display-only. They do not run egglog.
defmodule GettingStarted do
def print_output(result, type) do
result.outputs
|> Enum.filter(&(&1.type == type))
|> Enum.map(& &1.text)
|> Enum.reject(&is_nil/1)
|> Enum.map(&String.trim_trailing/1)
|> Enum.reject(&(&1 == ""))
|> Enum.each(&IO.puts/1)
end
def print_outputs(result) do
result.outputs
|> Enum.map(& &1.text)
|> Enum.reject(&is_nil/1)
|> Enum.map(&String.trim_trailing/1)
|> Enum.reject(&(&1 == ""))
|> Enum.each(&IO.puts/1)
end
def show_run(result) do
%{
status: result.status,
stats: result.stats
}
end
def render_snapshot(%{format: :svg, svg: svg}) when is_binary(svg) do
kino_html = Module.concat(Kino, HTML)
if Code.ensure_loaded?(kino_html) do
apply(kino_html, :new, [svg])
else
svg
end
end
def render_snapshot(%{text: text}), do: text
def snapshot_summary(snapshot) do
summary = Egglog.Snapshot.summary(snapshot)
%{
e_nodes: summary.nodes,
e_classes: summary.classes,
by_operator: summary.operators,
omitted: summary.omitted
}
end
end1. A Loaded Program
Use Egglog.Program when you have a reusable theory and want each query to be
isolated from the others.
The theory below declares a tiny arithmetic language and a few equations. In universal-algebra terms, the datatype gives us a finite signature; the rewrite rules generate equalities in the quotient term algebra.
math_theory = """
(datatype Math
(Num i64)
(Var String)
(Add Math Math)
(Mul Math Math))
(rewrite (Add (Num a) (Num b)) (Num (+ a b)))
(rewrite (Mul (Num a) (Num b)) (Num (* a b)))
(rewrite (Add (Mul a x) (Mul b x)) (Mul (Add a b) x))
(rewrite (Add x (Num 0)) x)
(rewrite (Mul x (Num 1)) x)
"""
math = Egglog.Program.load!(math_theory)Now submit one query-local term and ask for extraction.
input = %{
terms: %{
"goal" => "(Add (Mul (Num 3) (Var \"x\")) (Mul (Num 4) (Var \"x\")))"
},
budget: %{rounds: 5},
requests: [%{type: :extract, expr: "goal"}]
}
result = Egglog.Program.run!(math, input)
GettingStarted.print_output(result, :extract)
GettingStarted.show_run(result)The extracted term is the representative chosen by egglog after saturation. Elixir did not simplify the expression. Native egglog matched the rewrite schemas, added equalities, rebuilt congruence closure, and extracted a cheap representative.
2. Looking Inside
Most egglog commands are quiet when they succeed. A snapshot lets us inspect what native egglog built.
snapshot = Egglog.snapshot!(math, input)
GettingStarted.snapshot_summary(
Egglog.snapshot!(math, input, render: :json)
)GettingStarted.render_snapshot(snapshot)The picture is not a rewrite trace. Equality saturation keeps several equivalent syntactic forms in one e-class and postpones the choice of a concrete representative until extraction.
3. Checks And Congruence Closure
Checks are assertions about the current e-graph. With Egglog.Program, we can
add query-local unions without mutating the loaded base.
congruence_theory = """
(datatype Term
(A)
(B)
(Wrap Term)
(Pair Term Term))
"""
cc = Egglog.Program.load!(congruence_theory)First, without an equality between A and B, the two context terms are
distinct.
before_union =
Egglog.Program.run!(cc, %{
terms: %{
"left" => "(Pair (Wrap (A)) (A))",
"right" => "(Pair (Wrap (B)) (B))"
},
requests: [
%{type: :extract, expr: "left"},
%{type: :extract, expr: "right"}
]
})
GettingStarted.print_outputs(before_union)Now assert A = B for this query. Congruence closure propagates that equality
through Wrap and Pair.
after_union_input = %{
terms: %{
"left" => "(Pair (Wrap (A)) (A))",
"right" => "(Pair (Wrap (B)) (B))"
},
unions: [{"(A)", "(B)"}],
budget: %{rounds: 1},
requests: [
"(check (= left right))",
%{type: :extract, expr: "left"}
]
}
after_union = Egglog.Program.run!(cc, after_union_input)
GettingStarted.print_output(after_union, :extract)
GettingStarted.show_run(after_union)Egglog.snapshot!(cc, after_union_input)
|> GettingStarted.render_snapshot()The equality was asserted only at the leaves, but native rebuild maintains the congruence invariant: equal children imply equal parent applications.
4. Datalog Facts
Egglog is also a Datalog-style engine. Relations are represented as unit-valued functions, and rules derive new rows.
This example computes transitive reachability.
reachability_theory = """
(relation edge (i64 i64))
(relation path (i64 i64))
(rule ((edge x y))
((path x y)))
(rule ((path x y) (edge y z))
((path x z)))
"""
reach = Egglog.Program.load!(reachability_theory)Submit a small graph as query-local facts.
reach_input = %{
facts: [
"(edge 1 2)",
"(edge 2 3)",
"(edge 3 4)"
],
budget: %{rounds: 5},
requests: [
"(check (path 1 4))",
%{type: :print_function, name: "path", limit: 20}
]
}
reach_result = Egglog.Program.run!(reach, reach_input)
GettingStarted.print_output(reach_result, :print_function)
GettingStarted.show_run(reach_result)The interesting point is that the same native runtime is maintaining both relations and equality. We will use that combination in the next example.
5. Analysis Facts Guard Rewrites
Many equations are valid only under hypotheses. Instead of writing an Elixir callback, keep the hypothesis in egglog as a relation.
Here (Div x x) = 1 is allowed only when x is known to be non-zero.
guarded_theory = """
(datatype Math
(Num i64)
(Div Math Math))
(relation non-zero (Math))
(rewrite (Div x x) (Num 1)
:when ((non-zero x)))
"""
guarded = Egglog.Program.load!(guarded_theory)Without the fact, extraction returns the original division.
without_guard_input = %{
terms: %{"x" => "(Div (Num 2) (Num 2))"},
budget: %{rounds: 2},
requests: [%{type: :extract, expr: "x"}]
}
without_guard = Egglog.Program.run!(guarded, without_guard_input)
GettingStarted.print_output(without_guard, :extract)With the fact, the guarded rewrite fires.
with_guard_input = %{
terms: %{"x" => "(Div (Num 2) (Num 2))"},
facts: ["(non-zero (Num 2))"],
budget: %{rounds: 2},
requests: [%{type: :extract, expr: "x"}]
}
with_guard = Egglog.Program.run!(guarded, with_guard_input)
GettingStarted.print_output(with_guard, :extract)Egglog.snapshot!(guarded, with_guard_input)
|> GettingStarted.render_snapshot()The relation non-zero is an analysis fact. Once it is present, egglog can
justify the equality between the division term and (Num 1).
6. Stateful Sessions
Egglog.Program is query-local: every run clones the loaded base and temporary
facts do not accumulate. For interactive work, use Egglog.EGraph instead.
{:ok, egraph} =
Egglog.EGraph.new("""
(datatype Math
(Num i64)
(Add Math Math))
""")Each successful run/3 mutates this one native session.
{:ok, _} = Egglog.EGraph.run(egraph, "(let x (Add (Num 1) (Num 0)))")
{:ok, _} = Egglog.EGraph.run(egraph, "(rewrite (Add a (Num 0)) a)")
{:ok, egraph_result} =
Egglog.EGraph.run(egraph, """
(run 2)
(check (= x (Num 1)))
(extract x)
""")
GettingStarted.print_output(egraph_result, :extract)Because the state persists, later calls can inspect what earlier calls inserted.
{:ok, true} = Egglog.EGraph.check(egraph, "(= x (Num 1))")
{:ok, "(Num 1)"} = Egglog.EGraph.extract(egraph, "x")Egglog.EGraph.snapshot!(egraph)
|> GettingStarted.render_snapshot()Use the two models deliberately:
Egglog.Programfor reproducible, isolated queries against a loaded theory.Egglog.EGraphfor REPL/Livebook sessions where state should accumulate.
What To Try Next
Natural next steps:
- Open
livebooks/egglog-tutorial/01-basics.livemdfor a deeper equality saturation walkthrough. - Open
livebooks/egglog-tutorial/02-datalog.livemdfor more relational examples. - Open
livebooks/egglog-tutorial/03-analysis.livemdfor richer analysis facts and guarded rewrites. - Change the arithmetic rules above and watch how the snapshot changes.