Skip to content

Commit b8383a6

Browse files
Merge pull request #416 from kaeun97/kaeun97/pretty-report
feat: add pretty run report
2 parents 8812ec9 + 568d6bf commit b8383a6

10 files changed

Lines changed: 829 additions & 386 deletions

File tree

Cargo.lock

Lines changed: 439 additions & 354 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ opentelemetry_sdk = "0.28"
2323
# egglog-core-relations = { path = "../egg-smol/core-relations" }
2424
# egglog-ast = { path = "../egg-smol/egglog-ast" }
2525
# egglog-reports = { path = "../egg-smol/egglog-reports" }
26-
egglog = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug", default-features = false }
27-
egglog-ast = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
28-
egglog-core-relations = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
29-
egglog-reports = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
30-
egglog-bridge = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
31-
32-
26+
egglog = { git = "https://github.com/egraphs-good/egglog.git", rev = "2e5657b", default-features = false }
27+
egglog-ast = { git = "https://github.com/egraphs-good/egglog.git", rev = "2e5657b" }
28+
egglog-core-relations = { git = "https://github.com/egraphs-good/egglog.git", rev = "2e5657b" }
29+
egglog-reports = { git = "https://github.com/egraphs-good/egglog.git", rev = "2e5657b" }
30+
egglog-bridge = { git = "https://github.com/egraphs-good/egglog.git", rev = "2e5657b" }
3331
egglog-experimental = { git = "https://github.com/egraphs-good/egglog-experimental", branch = "main", default-features = false }
3432
egraph-serialize = { version = "0.3", features = ["serde", "graphviz"] }
3533
serde_json = "1"
@@ -52,11 +50,11 @@ base64 = "0.22.1"
5250
# egglog-reports = { path = "../egg-smol/egglog-reports" }
5351
# egglog-bridge = { path = "../egg-smol/egglog-bridge" }
5452

55-
egglog = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
56-
egglog-ast = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
57-
egglog-core-relations = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
58-
egglog-bridge = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
59-
egglog-reports = { git = "https://github.com/saulshanabrook/egg-smol.git", branch = "fix-container-fn-bug" }
53+
egglog = { git = "https://github.com/saulshanabrook/egg-smol.git", rev = "2e5657b" }
54+
egglog-ast = { git = "https://github.com/saulshanabrook/egg-smol.git", rev = "2e5657b" }
55+
egglog-core-relations = { git = "https://github.com/saulshanabrook/egg-smol.git", rev = "2e5657b" }
56+
egglog-bridge = { git = "https://github.com/saulshanabrook/egg-smol.git", rev = "2e5657b" }
57+
egglog-reports = { git = "https://github.com/saulshanabrook/egg-smol.git", rev = "2e5657b" }
6058

6159
# enable debug symbols for easier profiling
6260
[profile.release]

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ _This project uses semantic versioning_
44

55
## UNRELEASED
66

7+
- Add Python-friendly `RunReport` wrapper that returns `CommandDecl` objects as rule keys instead of raw egglog s-expression strings, with pretty-printed Python syntax in `str()` output [#416](https://github.com/egraphs-good/egglog-python/pull/416)
8+
79
## 13.1.0 (2026-03-25)
810

911
- Improve high-level Python ergonomics and docs [#397](https://github.com/egraphs-good/egglog-python/pull/397)

python/egglog/bindings.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,11 @@ class Rewrite:
403403
lhs: _Expr
404404
rhs: _Expr
405405
conditions: list[_Fact]
406+
name: str
406407

407-
def __new__(cls, span: _Span, lhs: _Expr, rhs: _Expr, conditions: list[_Fact] = ...) -> Rewrite: ...
408+
def __new__(
409+
cls, span: _Span, lhs: _Expr, rhs: _Expr, conditions: list[_Fact] = ..., name: str = ...
410+
) -> Rewrite: ...
408411

409412
@final
410413
class RunConfig:

python/egglog/egraph.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .egraph_state import *
4343
from .ipython_magic import IN_IPYTHON
4444
from .pretty import pretty_decl
45+
from .run_report import RunReport
4546
from .runtime import *
4647
from .thunk import *
4748

@@ -70,6 +71,7 @@
7071
"GreedyDagCost",
7172
"RewriteOrRule",
7273
"Ruleset",
74+
"RunReport",
7375
"Schedule",
7476
"_BirewriteBuilder",
7577
"_EqBuilder",
@@ -953,36 +955,34 @@ def output(self) -> None:
953955
raise NotImplementedError(msg)
954956

955957
@overload
956-
def run(self, limit: int, /, *until: Fact, ruleset: Ruleset | None = None) -> bindings.RunReport: ...
958+
def run(self, limit: int, /, *until: Fact, ruleset: Ruleset | None = None) -> RunReport: ...
957959

958960
@overload
959-
def run(self, schedule: Schedule, /) -> bindings.RunReport: ...
961+
def run(self, schedule: Schedule, /) -> RunReport: ...
960962

961963
@_TRACER.start_as_current_span("run")
962-
def run(
963-
self, limit_or_schedule: int | Schedule, /, *until: Fact, ruleset: Ruleset | None = None
964-
) -> bindings.RunReport:
964+
def run(self, limit_or_schedule: int | Schedule, /, *until: Fact, ruleset: Ruleset | None = None) -> RunReport:
965965
"""
966966
Run the egraph until the given limit or until the given facts are true.
967967
"""
968968
if isinstance(limit_or_schedule, int):
969969
limit_or_schedule = run(ruleset, *until) * limit_or_schedule
970970
return self._run_schedule(limit_or_schedule)
971971

972-
def _run_schedule(self, schedule: Schedule) -> bindings.RunReport:
972+
def _run_schedule(self, schedule: Schedule) -> RunReport:
973973
self._add_decls(schedule)
974974
cmd = self._state.run_schedule_to_egg(schedule.schedule)
975975
(command_output,) = self._run_program(cmd)
976976
assert isinstance(command_output, bindings.RunScheduleOutput)
977-
return command_output.report
977+
return RunReport._from_bindings(command_output.report, self._state)
978978

979-
def stats(self) -> bindings.RunReport:
979+
def stats(self) -> RunReport:
980980
"""
981981
Returns the overall run report for the egraph.
982982
"""
983983
(output,) = self._run_program(bindings.PrintOverallStatistics(span(1), None))
984984
assert isinstance(output, bindings.OverallStatistics)
985-
return output.report
985+
return RunReport._from_bindings(output.report, self._state)
986986

987987
def check_bool(self, *facts: FactLike) -> bool:
988988
"""

python/egglog/egraph_state.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ class EGraphState:
8686
# Counter for deterministic synthetic names assigned to unnamed functions.
8787
unnamed_function_counter: int = 0
8888

89+
# Counter for numeric rule names
90+
rule_name_counter: int = 0
91+
# Mapping from numeric name (str) to command decl
92+
rule_name_to_command_decl: dict[str, RuleDecl | BiRewriteDecl | RewriteDecl] = field(default_factory=dict)
93+
8994
def copy(self) -> EGraphState:
9095
"""
9196
Returns a copy of the state. The egraph reference is kept the same. Used for pushing/popping.
@@ -102,6 +107,8 @@ def copy(self) -> EGraphState:
102107
cost_callables=self.cost_callables.copy(),
103108
expr_to_let_counter=self.expr_to_let_counter,
104109
unnamed_function_counter=self.unnamed_function_counter,
110+
rule_name_counter=self.rule_name_counter,
111+
rule_name_to_command_decl=self.rule_name_to_command_decl.copy(),
105112
)
106113

107114
def _run_program(self, *commands: bindings._Command) -> list[bindings._CommandOutput]:
@@ -283,24 +290,35 @@ def command_to_egg(self, cmd: CommandDecl, ruleset: Ident) -> bindings._Command
283290
return bindings.ActionCommand(action_egg)
284291
case RewriteDecl(tp, lhs, rhs, conditions) | BiRewriteDecl(tp, lhs, rhs, conditions):
285292
self.type_ref_to_egg(tp)
293+
name = str(self.rule_name_counter)
294+
self.rule_name_counter += 1
286295
rewrite = bindings.Rewrite(
287296
span(),
288297
self._expr_to_egg(lhs),
289298
self._expr_to_egg(rhs),
290299
[self.fact_to_egg(c) for c in conditions],
300+
name,
291301
)
292-
return (
293-
bindings.RewriteCommand(str(ruleset), rewrite, cmd.subsume)
294-
if isinstance(cmd, RewriteDecl)
295-
else bindings.BiRewriteCommand(str(ruleset), rewrite)
296-
)
302+
egg_cmd: bindings._Command
303+
if isinstance(cmd, RewriteDecl):
304+
self.rule_name_to_command_decl[name] = cmd
305+
egg_cmd = bindings.RewriteCommand(str(ruleset), rewrite, cmd.subsume)
306+
else:
307+
self.rule_name_to_command_decl[f"{name}=>"] = cmd
308+
self.rule_name_to_command_decl[f"{name}<="] = cmd
309+
egg_cmd = bindings.BiRewriteCommand(str(ruleset), rewrite)
310+
return egg_cmd
297311
case RuleDecl(head, body, name):
312+
if not name:
313+
name = str(self.rule_name_counter)
314+
self.rule_name_counter += 1
315+
self.rule_name_to_command_decl[name] = cmd
298316
return bindings.RuleCommand(
299317
bindings.Rule(
300318
span(),
301319
[self.action_to_egg(a) for a in head],
302320
[self.fact_to_egg(f) for f in body],
303-
name or "",
321+
name,
304322
str(ruleset),
305323
)
306324
)

python/egglog/run_report.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
from datetime import timedelta
5+
6+
from . import bindings
7+
from .declarations import BiRewriteDecl, Declarations, RewriteDecl, RuleDecl
8+
from .egraph_state import EGraphState
9+
from .pretty import pretty_decl
10+
11+
RewriteOrRuleDecl = RuleDecl | BiRewriteDecl | RewriteDecl
12+
13+
14+
@dataclass
15+
class RuleReport:
16+
plan: bindings.Plan | None
17+
search_and_apply_time: timedelta
18+
num_matches: int
19+
20+
@classmethod
21+
def _from_bindings(cls, report: bindings.RuleReport) -> RuleReport:
22+
return cls(
23+
plan=report.plan,
24+
search_and_apply_time=report.search_and_apply_time,
25+
num_matches=report.num_matches,
26+
)
27+
28+
29+
@dataclass
30+
class RuleSetReport:
31+
_decls: Declarations = field(repr=False)
32+
changed: bool = False
33+
rule_reports: dict[RewriteOrRuleDecl, list[RuleReport]] = field(default_factory=dict)
34+
search_and_apply_time: timedelta = field(default_factory=timedelta)
35+
merge_time: timedelta = field(default_factory=timedelta)
36+
37+
@classmethod
38+
def _from_bindings(
39+
cls, report: bindings.RuleSetReport, rule_map: dict[str, RewriteOrRuleDecl], decls: Declarations
40+
) -> RuleSetReport:
41+
rule_reports: dict[RewriteOrRuleDecl, list[RuleReport]] = {}
42+
for k, v in report.rule_reports.items():
43+
translated = rule_map[k]
44+
reports = [RuleReport._from_bindings(rr) for rr in v]
45+
if translated in rule_reports:
46+
rule_reports[translated].extend(reports)
47+
else:
48+
rule_reports[translated] = reports
49+
return cls(
50+
_decls=decls,
51+
changed=report.changed,
52+
rule_reports=rule_reports,
53+
search_and_apply_time=report.search_and_apply_time,
54+
merge_time=report.merge_time,
55+
)
56+
57+
def __repr__(self) -> str:
58+
rule_reports_str = {pretty_decl(self._decls, k): v for k, v in self.rule_reports.items()}
59+
return (
60+
f"RuleSetReport(changed={self.changed}, "
61+
f"rule_reports={rule_reports_str}, "
62+
f"search_and_apply_time={self.search_and_apply_time}, "
63+
f"merge_time={self.merge_time})"
64+
)
65+
66+
67+
@dataclass
68+
class IterationReport:
69+
rule_set_report: RuleSetReport
70+
rebuild_time: timedelta
71+
72+
@classmethod
73+
def _from_bindings(
74+
cls, report: bindings.IterationReport, rule_map: dict[str, RewriteOrRuleDecl], decls: Declarations
75+
) -> IterationReport:
76+
return cls(
77+
rule_set_report=RuleSetReport._from_bindings(report.rule_set_report, rule_map, decls),
78+
rebuild_time=report.rebuild_time,
79+
)
80+
81+
82+
@dataclass
83+
class RunReport:
84+
"""Python-friendly wrapper around bindings.RunReport."""
85+
86+
_decls: Declarations = field(repr=False)
87+
iterations: list[IterationReport] = field(default_factory=list)
88+
updated: bool = False
89+
search_and_apply_time_per_rule: dict[RewriteOrRuleDecl, timedelta] = field(default_factory=dict)
90+
num_matches_per_rule: dict[RewriteOrRuleDecl, int] = field(default_factory=dict)
91+
search_and_apply_time_per_ruleset: dict[str, timedelta] = field(default_factory=dict)
92+
merge_time_per_ruleset: dict[str, timedelta] = field(default_factory=dict)
93+
rebuild_time_per_ruleset: dict[str, timedelta] = field(default_factory=dict)
94+
95+
def __repr__(self) -> str:
96+
time_per_rule = {pretty_decl(self._decls, k): v for k, v in self.search_and_apply_time_per_rule.items()}
97+
matches_per_rule = {pretty_decl(self._decls, k): v for k, v in self.num_matches_per_rule.items()}
98+
return (
99+
f"RunReport(iterations={self.iterations}, "
100+
f"updated={self.updated}, "
101+
f"search_and_apply_time_per_rule={time_per_rule}, "
102+
f"num_matches_per_rule={matches_per_rule}, "
103+
f"search_and_apply_time_per_ruleset={self.search_and_apply_time_per_ruleset}, "
104+
f"merge_time_per_ruleset={self.merge_time_per_ruleset}, "
105+
f"rebuild_time_per_ruleset={self.rebuild_time_per_ruleset})"
106+
)
107+
108+
@classmethod
109+
def _from_bindings(cls, report: bindings.RunReport, state: EGraphState) -> RunReport:
110+
rule_map = state.rule_name_to_command_decl
111+
decls = state.__egg_decls__
112+
113+
search_and_apply_time_per_rule: dict[RewriteOrRuleDecl, timedelta] = {}
114+
for k, v in report.search_and_apply_time_per_rule.items():
115+
translated = rule_map[k]
116+
if translated in search_and_apply_time_per_rule:
117+
search_and_apply_time_per_rule[translated] += v
118+
else:
119+
search_and_apply_time_per_rule[translated] = v
120+
121+
num_matches_per_rule: dict[RewriteOrRuleDecl, int] = {}
122+
for k, v in report.num_matches_per_rule.items():
123+
translated = rule_map[k]
124+
if translated in num_matches_per_rule:
125+
num_matches_per_rule[translated] += v
126+
else:
127+
num_matches_per_rule[translated] = v
128+
129+
return cls(
130+
_decls=decls,
131+
iterations=[IterationReport._from_bindings(it, rule_map, decls) for it in report.iterations],
132+
updated=report.updated,
133+
search_and_apply_time_per_rule=search_and_apply_time_per_rule,
134+
num_matches_per_rule=num_matches_per_rule,
135+
search_and_apply_time_per_ruleset=report.search_and_apply_time_per_ruleset,
136+
merge_time_per_ruleset=report.merge_time_per_ruleset,
137+
rebuild_time_per_ruleset=report.rebuild_time_per_ruleset,
138+
)

python/tests/test_bindings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def get_egglog_folder() -> pathlib.Path:
4242
"typeinfer",
4343
"repro-typechecking-schedule",
4444
"stresstest_large_expr",
45+
"eggcc-2mm",
4546
}
4647

4748

0 commit comments

Comments
 (0)