Skip to content

Upgrade to current Egglog, expose extraction controls, and retain Param-Eq demos - #414

Draft
saulshanabrook wants to merge 4 commits into
mainfrom
sr
Draft

Upgrade to current Egglog, expose extraction controls, and retain Param-Eq demos#414
saulshanabrook wants to merge 4 commits into
mainfrom
sr

Conversation

@saulshanabrook

@saulshanabrook saulshanabrook commented Apr 6, 2026

Copy link
Copy Markdown
Member

Summary

Upgrade the Rust extension and high-level Python APIs to current Egglog 3 and the matching experimental APIs. This brings the branch onto the built-in greedy-DAG and dynamic-cost infrastructure, exposes the new runtime and extraction controls in Python, and leaves the paused Param-Eq research as a maintained experimental module with bounded CI stress demos.

User-facing changes

  • Breaking: remove Map.rebuild(), Set.rebuild(), and Vec.rebuild(); Egglog 3 rebuilds container values in the backend.
  • Preserve Egglog 3 declarations, values, metadata, reports, and source syntax through the low-level bindings, including arbitrary-size BigInt, generic extraction, constructor/relation lookup, correctly scaled run durations, and parse-and-run source filenames.
  • Add generic Pair and Maybe, undefined-result catch, map folding, container lengths, numeric conversions, guarded f64 math, and f64.is_finite(). Maybe, catch, and map folding intentionally remain unsupported in proof mode.
  • Expand experimental Rational with public RationalLike conversions, reflected arithmetic, powers, min/max, comparisons, and partial-operation behavior consistent with surrounding numeric APIs.
  • Lower bodyful Python functions, methods, and constants as eager primitives by default while keeping explicit-ruleset bodies rewrite-backed. Preserve Python argument order for reverse_args callables and isolate higher-order probing from live rulesets.
  • Add persistent backoff schedules and RunReport.can_stop; expose safe naive and opt-in unsafe-seminaive rule evaluation.
  • Configure worker threads and global rule decomposition per EGraph, and allow rule(..., no_decomp=True) per rule. The process-global RAYON_NUM_THREADS path is removed.

Extraction and dynamic costs

  • Add extractor="tree" | "greedy-dag" to extraction APIs.
  • Add ordered heterogeneous multi-root extract_multiple, including empty per-root results, and destructive keep_best for table-backed callables. Multi-root and keep-best paths use the upstream dynamic cost model.
  • Consume experimental multi-extract as one structured aggregate with a shared TermDag and ordered per-root term-ID groups. Low-level bindings expose the known output through UserDefinedCommandOutput.as_multi_extract() -> MultiExtractOutput | None; the high-level sequence API converts it automatically and clones the shared DAG only once.
  • Use canonical set_cost tables consistently. Compatible raw tables and callable aliases share the backend table and survive freeze/replay; incompatible schemas fail clearly, and literal negative costs are rejected before reaching the backend.
  • Rename the general callback protocol to TreeCostModel, retaining CostModel as an alias, and add frozen additive DagCostModel values usable by tree or greedy-DAG extraction.
  • Keep custom Python models single-root. Read-only callback table lookups are safe for pre-registered tables keyed by callback-supplied values; derived same-graph keys are rejected while extraction holds the graph read-only.
  • Breaking: remove GreedyDagCost, GreedyDagCostModel, and greedy_dag_cost_model; use DagCostModel(..., extractor="greedy-dag").

Param-Eq handoff

egglog.exp.param_eq retains the binary and container representations plus a CLI. Three project-authored end-to-end cases run in normal pytest/CI, parse the extracted result, and compare it with the source at finite sample points. Constant-folding and domain regressions are covered directly.

experiments/param_eq remains the optional private-corpus runner with resource guards, provenance checks, and aggregate-only output. Private expressions and external archives are not redistributed. Both retained variants use ordinary persistent upstream backoff; the binary repeated-monomial case remains an explicitly reported iteration-limit stress case.

Dependencies

  • egraphs-good/egglog#1008 supplies rule-name round-tripping and safe user-output downcasting at ee58d8a537cec77e07595b1f0b577eee617f34b6, based on current Egglog main at integration.
  • egraphs-good/egglog-experimental#67 supplies the minimal reusable Param-Eq primitives plus structured shared-DAG multi-extraction and keep-best fixes at 2b4627a5806f8476bc34814ecf817e3b77c16f87.

Cargo patches the canonical Egglog source so the direct bindings and experimental dependency use one Rust type identity.

Validation

  • uv sync --reinstall-package egglog --all-extras --locked
  • focused downcast and multi-root extraction tests, including both extractors, empty roots, heterogeneous roots, and exact n=2 grouping
  • uv run pytest --benchmark-disable -q — 920 passed, 1 skipped, 3 expected xfails
  • make mypy
  • make stubtest
  • uv run ruff check .
  • uv run ruff format --check .
  • uv lock --check
  • cargo fmt --check
  • cargo check --locked
  • cargo test --locked --lib — 2 passed
  • cargo clippy --locked --all-targets
  • clean make docs — all 12 gallery examples executed successfully

Comment thread python/tests/test_high_level.py Fixed
Comment thread python/tests/test_high_level.py Fixed
Comment thread python/tests/test_high_level.py Fixed
Comment thread python/egglog/exp/param_eq/pipeline.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/exp/param_eq/pipeline.py Fixed
Comment thread python/egglog/exp/param_eq/pipeline.py Fixed
Comment thread python/egglog/exp/param_eq/pipeline.py Fixed
@codspeed-hq

codspeed-hq Bot commented Apr 6, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 43.18%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 12 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_jit[lda] 7.4 s 5 s +47.59%
WallTime test_jit[lda] 7.9 s 5.7 s +38.9%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing sr (ddae949) with main (6b2016e)

Open in CodSpeed

Comment thread python/egglog/exp/param_eq/pipeline.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
Comment thread python/egglog/builtins.py Fixed
@saulshanabrook saulshanabrook changed the title Sr Upgrade the Python bindings to Egglog 3 and retain Param-Eq stress demos Aug 31, 2026
def test_init(self):
class B(Expr):
def __init__(self, value: i64Like) -> None:
return B.wrap(value) # type: ignore[return-value] # noqa: PLE0101 - symbolic constructor body
CallableDecl: TypeAlias = RelationDecl | ConstantDecl | FunctionCallableDecl


def is_callable_decl_constructor(decls: Declarations, decl: CallableDecl) -> bool:
Comment on lines +550 to +554
def action_to_egg( # noqa: C901, PLR0911, PLR0912
self,
action: ActionDecl,
expr_to_let: bool = False,
) -> bindings._Action | None:
return f"cost_table_{self.callable_ref_to_egg(ref)[0]}"

def fact_to_egg(self, fact: FactDecl) -> bindings._Fact:
def fact_to_egg(self, fact: FactDecl, *, expr_to_let: bool = False) -> bindings._Fact:
def _allocate_callable_egg_name(self, ref: CallableRef) -> str:
return self._allocate_name(self._generate_callable_egg_name_candidates(ref), self._backend_symbol_is_occupied)

def _generate_callable_egg_name_candidates(self, ref: CallableRef) -> tuple[str, ...]:
Comment thread python/egglog/pretty.py Fixed
Comment thread python/tests/test_high_level.py Fixed
Comment thread python/egglog/exp/param_eq/domain.py Fixed
Comment thread python/egglog/pretty.py Fixed
Comment thread python/egglog/pretty.py Fixed
Migrate the bindings to Egglog 3, consolidate the general API and correctness fixes with tests and changelog coverage, and preserve Param-Eq as a reusable module and CLI, bounded CI stress cases, and an optional aggregate-only external harness.

Pin the Egglog v3 compatibility fix and experimental primitives to immutable revisions, patching the core workspace source so direct and transitive dependencies share one Rust type identity.
Comment thread python/egglog/builtins.py Outdated
Comment on lines +582 to +607
@method(preserve=True)
def pick_key(self) -> T:
runtime_self = to_runtime_expr(self)
key_type, _value_type = runtime_self.__egg_typed_expr__.tp.args
maybe_type = RuntimeClass(
Thunk.value(Declarations.create(runtime_self, cast("HasDeclarations", Maybe))),
TypeRefWithVars(Ident.builtin("Maybe"), (key_type.to_var(),)),
_egg_has_params=True,
)
initial = cast("Maybe[T]", maybe_type.none())
return map_fold_kv(
lambda picked, key, _value: picked.match(lambda _: picked, cast("Maybe[T]", maybe_type.some(key))),
initial,
self,
).unwrap()

@method(preserve=True)
def keys(self) -> Set[T]:
runtime_self = to_runtime_expr(self)
key_type, _value_type = runtime_self.__egg_typed_expr__.tp.args
set_type = RuntimeClass(
Thunk.value(Declarations.create(runtime_self, cast("HasDeclarations", Set))),
TypeRefWithVars(Ident.builtin("Set"), (key_type.to_var(),)),
_egg_has_params=True,
)
return map_fold_kv(lambda keys, key, _value: keys.insert(key), cast("Set[T]", set_type.empty()), self)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be normal expressions, not sure why they are like this... if they aren't builtins then we shouldnt include them here, instead they should just be inlined where we need them... as normal expression, we shouldnt use theprivate runtime expression APIs in public places like this

Comment thread python/egglog/builtins.py Outdated
Comment on lines +632 to +678
def map_filter_kv(f: Callable[[T, V], Unit], xs: Map[T, V]) -> Map[T, V]:
runtime_xs = to_runtime_expr(xs)
map_type = RuntimeClass(
Thunk.value(Declarations.create(runtime_xs, cast("HasDeclarations", Map))),
runtime_xs.__egg_typed_expr__.tp.to_var(),
_egg_has_params=True,
)
return map_fold_kv(
lambda result, key, value: catch(lambda: f(key, value)).match(lambda _: result.insert(key, value), result),
cast("Map[T, V]", map_type.empty()),
xs,
)


def map_map_values(f: Callable[[T, V], V2], xs: Map[T, V]) -> Map[T, V2]:
runtime_xs = to_runtime_expr(xs)
key_type, value_type = runtime_xs.__egg_typed_expr__.tp.args
probe_decls = runtime_xs.__egg_decls__.copy()
dummy_key = RuntimeExpr.__from_values__(probe_decls, TypedExprDecl(key_type, DummyDecl()))
dummy_value = RuntimeExpr.__from_values__(probe_decls, TypedExprDecl(value_type, DummyDecl()))
with set_current_ruleset(None):
transformed = cast("Callable[[RuntimeExpr, RuntimeExpr], object]", f)(dummy_key, dummy_value)
if not isinstance(transformed, RuntimeExpr):
raise TypeError(f"Map value transform must return an egglog expression, got {type(transformed)}")
output_type = transformed.__egg_typed_expr__.tp
map_type = RuntimeClass(
Thunk.value(Declarations.create(runtime_xs, transformed, cast("HasDeclarations", Map))),
TypeRefWithVars(Ident.builtin("Map"), (key_type.to_var(), output_type.to_var())),
_egg_has_params=True,
)
return map_fold_kv(
lambda result, key, value: result.insert(key, f(key, value)),
cast("Map[T, V2]", map_type.empty()),
xs,
)


def map_merge_with(f: Callable[[V, V], V], left: Map[T, V], right: Map[T, V]) -> Map[T, V]:
return map_fold_kv(
lambda result, key, value: catch(lambda: result[key]).match(
lambda old: result.insert(key, f(old, value)), result.insert(key, value)
),
left,
right,
)


Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these also shouldnt be here if they are not primitives... they should just be normal expressions where they are used

cost_callables: set[CallableRef] = field(default_factory=set)
# Cache of top-level expressions lowered with any available synthetic let
# references. Rules and rewrites must never read from this cache.
expr_to_let_egg_cache: dict[ExprDecl, bindings._Expr] = field(default_factory=dict)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this different from expr_to_egg_cache? Are both required?

Comment thread python/egglog/egraph_state.py Outdated
Comment on lines +666 to +667
# Use constructor declaration instead of constant b/c constants cannot be extracted
# https://github.com/egraphs-good/egglog/issues/334

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constants dont exist anymore

Comment thread python/egglog/pretty.py Outdated
Comment on lines +430 to +446
from .builtins import ExprValueError # noqa: PLC0415 - avoid a module import cycle
from .runtime import RuntimeExpr # noqa: PLC0415 - avoid a module import cycle

if tp.ident == Ident.builtin("Map"):
runtime_expr = RuntimeExpr.__from_values__(self.decls, decl)
try:
as_dict = cast("Map[BaseExpr, BaseExpr]", runtime_expr).value
except ExprValueError:
return self(expr, unwrap_lit=unwrap_lit, ruleset_ident=ruleset_ident, parens=parens), "expr"
if unwrap_lit:
items = ", ".join(
f"{self(cast('RuntimeExpr', k).__egg_typed_expr__, unwrap_lit=True)}: {self(cast('RuntimeExpr', v).__egg_typed_expr__, unwrap_lit=True)}"
for k, v in as_dict.items()
)
return f"{{{items}}}", "Map"
map_str = f"{tp}.empty()"
for key, value in as_dict.items():

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not use the runtime to rebuild things here... why is this neccessary? Doesnt have to be canoncial, just print expressions as they are without getting value

Comment thread python/tests/test_high_level.py Outdated
Comment on lines +181 to +182
assert '(let $__expr_0 (LetConflictNum_var "explicit"))' in egglog_string
assert '(let $__expr_1 (LetConflictNum_var "synthetic"))' in egglog_string

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In these tests dont test for the exact string, this seems birttle... also all tests in here when they can should avoid using low level _ APIs and instead just use high level APIs that actually test behavior and not the low level behavior. remove any tests that aren't needed if we just are testing high level behavior

Comment thread python/tests/test_high_level.py Outdated
Comment on lines +569 to +573
def test_anonymous_combined_rulesets_use_deterministic_generated_names() -> None:
first = ruleset(name="combined_name_probe_first")
second = ruleset(name="combined_name_probe_second")
combined = unstable_combine_rulesets(first, second)
egraph = EGraph(save_egglog_string=True)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like this one too, make sure the tests are verifying a user seeable output, not low level implementaiton details

Comment thread python/tests/param_eq/test_domain.py Fixed
Comment thread python/egglog/exp/param_eq/domain.py Fixed
@saulshanabrook saulshanabrook changed the title Upgrade the Python bindings to Egglog 3 and retain Param-Eq stress demos Upgrade to current Egglog, expose extraction controls, and retain Param-Eq demos Sep 1, 2026
def __eq__(self, other: object) -> bool:
return False

def __lt__(self, other: CompareErrorCost) -> bool:
msg = "comparison failed"
raise RuntimeError(msg)

def __le__(self, other: CompareErrorCost) -> bool:
msg = "comparison failed"
raise RuntimeError(msg)

def __gt__(self, other: CompareErrorCost) -> bool:
msg = "comparison failed"
raise RuntimeError(msg)

def __ge__(self, other: CompareErrorCost) -> bool:
Comment thread python/egglog/egraph.py
ExtractionMode: TypeAlias = Literal["tree", "greedy-dag"]


def _extractor_options(extractor: str) -> tuple[bindings._Expr, ...]:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant