Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,11 @@ final case class CommandSchedule[NodeT](batchZero: (ArraySeq[EClassSymbol.Virtua
* Executes the command schedule against an immutable e-graph.
*
* @param egraph
* Destination e-graph. Implementations may either return it unchanged or produce a new immutable
* e-graph snapshot.
* @param reification
* Mapping from virtual symbols to concrete calls available before this command runs. This is used
* to ground virtual references present in [[uses]].
* Destination e-graph. A new immutable e-graph snapshot is produced if any change occurs.
* @param parallelize
* Parallelization strategy to label and distribute any internal work.
* Parallelization strategy to label and distribute any internal work.
* @return
* `Some(newGraph)` if any change occurred, or `None` for a no-op.
* `Some(newGraph)` if any change occurred, or `None` for a no-op.
*/
def applyImmutable[
Repr <: immutable.EGraphLike[NodeT, Repr] with immutable.EGraph[NodeT]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import foresight.eqsat.{EClassCall, MixedTree}

/**
* An e-graph compactly represents many equivalent expressions at once by grouping structurally
* * compatible nodes into e-classes. This enables fast equality saturation and rewrite exploration
* * without duplicating common substructure.
* compatible nodes into e-classes. This enables fast equality saturation and rewrite exploration
* without duplicating common substructure.
*
* `EGraph` is a convenience alias of [[EGraphLike]] where the self type equals the trait itself:
* `EGraph[NodeT]` extends `EGraphLike[NodeT, EGraph[NodeT]]`. Use this when you don’t need a custom
Expand Down
21 changes: 12 additions & 9 deletions foresight/src/main/scala/foresight/eqsat/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ package foresight
* This representation supports efficient, parallel, and reversible rewrite-driven
* optimization, with explicit tracking of symbolic bindings (*slots*) and rich metadata.
*
* E-graphs in Foresight are purely functional: every operation returns a new value
* rather than mutating in place. This immutability underpins reproducibility,
* concurrent analysis, and caching across snapshots.
* E-graphs in Foresight come in two flavors: an **immutable** variant
* ([[eqsat.immutable.EGraph]]) where every operation returns a new e-graph value
* without modifying the original (underpinning reproducibility, concurrent analysis, and
* caching across snapshots), and a **mutable** variant ([[eqsat.mutable.EGraph]]) that
* supports in-place additions and unions for workloads where allocation overhead matters.
* Both variants implement the same read-only query interface ([[eqsat.readonly.EGraph]]).
*
* ## Core Concepts
*
Expand Down Expand Up @@ -87,13 +90,13 @@ package foresight
* // Create an empty e-graph for a custom node type
* val g0: EGraph[MyNode] = EGraph.empty
*
* // Build a command queue
* val builder = new CommandQueueBuilder[MyNode]
* val root = builder.add(myENode)
* val queue = builder.queue.optimized
* // Build a command schedule using a builder
* val builder = CommandScheduleBuilder.newConcurrentBuilder[MyNode]
* builder.add(EClassSymbol.virtual(), myENodeSymbol, batch = 0)
* val schedule = builder.result()
*
* // Apply commands to produce a new graph
* val (g1, reif) = queue.applyImmutable(g0, Map.empty, ParallelMap.sequential)
* // Apply the schedule to produce a new immutable graph
* val g1 = schedule.applyImmutable(g0, ParallelMap.sequential).getOrElse(g0)
*
* // Run a saturation strategy
* val rules: Seq[Rule[MyNode, MyMatch, EGraph[MyNode]]] = Seq(r1, r2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ object Applier {
* Pre-flatMap the input match type: expand one match into **many** then apply and batch them.
*
* Useful when a single outer match implies multiple inner edits (e.g., expand an equivalence to
* a set of updates). The resulting [[CommandQueue]] preserves the order of the produced
* a set of updates). The resulting [[foresight.eqsat.commands.CommandSchedule]] preserves the order of the produced
* inner commands (though the engine may later optimize/deduplicate).
*
* @param applier Downstream applier that consumes `MatchT2`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import foresight.eqsat.readonly.EGraph
* 2. **Structural-only matches** — the match stores only stable, structural data (e.g., constants,
* shapes). In this case, `port` can simply return `this`. Even so, implementing `PortableMatch`
* is valuable because the caching and recording infrastructure requires all matches to be portable:
* - [[foresight.eqsat.saturation.EGraphWithRecordedApplications]] records applied matches per rule
* - [[foresight.eqsat.immutable.EGraphWithRecordedApplications]] records applied matches per rule
* and re-ports them after unions.
* - [[foresight.eqsat.saturation.SearchAndApply$.immutableWithCaching]] filters out already-applied matches; portability
* keeps equality and hashing consistent as the graph evolves.
Expand Down Expand Up @@ -54,7 +54,7 @@ import foresight.eqsat.readonly.EGraph
* def port(egraph: EGraph[NodeT]): ConstShapeMatch[NodeT] = this
* }
* }}}
* @see [[foresight.eqsat.saturation.EGraphWithRecordedApplications]]
* @see [[foresight.eqsat.immutable.EGraphWithRecordedApplications]]
* for how applied matches are recorded and re-ported after unions; and
* [[foresight.eqsat.saturation.SearchAndApply$.immutableWithCaching]]
* for how cached applications are filtered using `PortableMatch`.
Expand Down
16 changes: 7 additions & 9 deletions foresight/src/main/scala/foresight/eqsat/rewriting/Rewrite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import foresight.eqsat.readonly.EGraph
* - [[apply]]/[[applyImmutable]]/[[tryApply]]: Search and apply all matches immediately, returning
* either the updated e-graph or a flag indicating whether any change occurred.
*
* The staged command produced by [[delayed]] can be enqueued into a [[CommandQueue]] along with
* other rules, allowing callers to batch multiple rewrites into a single saturation step.
* The staged command produced by [[delayed]] can be batched with commands from other rules by
* sharing a [[foresight.eqsat.commands.CommandScheduleBuilder]], allowing callers to
* combine multiple rewrites into a single saturation step.
*
* @tparam NodeT Node type for expressions represented by the e-graph.
* @tparam MatchT Type of matches produced by this rule's search phase.
Expand Down Expand Up @@ -64,7 +65,7 @@ trait Rewrite[NodeT, MatchT, -EGraphT <: EGraph[NodeT]] {
* @param matches Matches to apply.
* @param egraph Target e-graph from which matches were derived.
* @param parallelize Parallel strategy used when building per-match commands.
* @return An optimized [[CommandQueue]] encapsulated as a [[CommandSchedule]].
* @return A single, optimized [[foresight.eqsat.commands.CommandSchedule]] that applies all current matches of this rule.
* @throws Rule.ApplicationException
* if constructing the per-match commands fails.
*/
Expand Down Expand Up @@ -152,14 +153,11 @@ trait Rewrite[NodeT, MatchT, -EGraphT <: EGraph[NodeT]] {
}

/**
* Search and apply all matches immediately.
* Search and apply all matches immediately, mutating the e-graph in place.
*
* If the rule makes any changes, `true` is returned without mutating the e-graph.
* If no changes occur, `false` is returned.
*
* @param egraph Target e-graph.
* @param egraph Target e-graph. Mutated in place if any changes occur.
* @param parallelize Parallel strategy used for both search and apply.
* @return The updated e-graph if changes occurred; otherwise the original `egraph`.
* @return `true` if any change occurred; `false` otherwise.
*/
def apply[
MutEGraphT <: EGraphT with mutable.EGraph[NodeT]
Expand Down
18 changes: 10 additions & 8 deletions foresight/src/main/scala/foresight/eqsat/rewriting/Rule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import foresight.eqsat.readonly.EGraph
*
* After search and apply, a rule performs a **composition step**:
* - Command aggregation – all per-match commands are combined into a single, optimized
* [[CommandQueue]] so they can be executed as one logical unit.
* [[foresight.eqsat.commands.CommandSchedule]] so they can be executed as one logical unit.
* - This aggregation deduplicates operations, removes no-ops, and preserves an execution order
* that is valid but not necessarily identical to the match order.
* - The result can be executed immediately (via [[apply]] / [[tryApply]]) or returned for later
Expand Down Expand Up @@ -57,15 +57,17 @@ import foresight.eqsat.readonly.EGraph
* }}}
* @example Staging a rule and applying later (batching with other rules)
* {{{
* val r1Cmd = r1.delayed(egraph) // stage first rule
* val r2Cmd = r2.delayed(egraph) // stage second rule
* import foresight.eqsat.commands.CommandScheduleBuilder
* import foresight.eqsat.parallel.ParallelMap
*
* // Merge both into one optimized command
* val batched = CommandQueue(Seq(r1Cmd, r2Cmd)).optimized
* // Batch two rules into a single command schedule using a shared builder
* val builder = CommandScheduleBuilder.newConcurrentBuilder[MyNode]
* r1.delayed(egraph, ParallelMap.default, builder) // stage first rule
* r2.delayed(egraph, ParallelMap.default, builder) // stage second rule
* val batched = builder.result()
*
* // Somewhere later in the saturation loop:
* val (maybeNewEGraph, _) = batched(egraph, Map.empty, ParallelMap.default)
* val next = maybeNewEGraph.getOrElse(egraph) // fallback to original if no changes
* val next = batched.applyImmutable(egraph, ParallelMap.default).getOrElse(egraph)
* }}}
*/
final case class Rule[NodeT, MatchT, EGraphT <: EGraph[NodeT]](override val name: String,
Expand All @@ -88,7 +90,7 @@ final case class Rule[NodeT, MatchT, EGraphT <: EGraph[NodeT]](override val name
* Build a staged command that, when executed, applies this rule's matches to `egraph`.
*
* This does not mutate the e-graph now; instead it returns a [[Command]] that you can:
* - enqueue into a [[CommandQueue]] with other rules; and
* - enqueue alongside other rules via a [[foresight.eqsat.commands.CommandScheduleBuilder]]; and
* - execute later as part of a larger saturation step.
*
* @param egraph The e-graph to search for matches. The staged command is intended to be run
Expand Down
21 changes: 13 additions & 8 deletions foresight/src/main/scala/foresight/eqsat/rewriting/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ package foresight.eqsat
* Matches are also **portable**. By implementing [[PortableMatch]], a match can
* remain meaningful across multiple e-graph snapshots, enabling caching and
* reapplication through
* [[foresight.eqsat.saturation.EGraphWithRecordedApplications]] and
* [[foresight.eqsat.immutable.EGraphWithRecordedApplications]] and
* [[foresight.eqsat.saturation.SearchAndApply$.immutableWithCaching]].
*
* ## Typical workflow
Expand Down Expand Up @@ -74,7 +74,7 @@ package foresight.eqsat
* a new snapshot, preserving structural meaning. Purely structural matches can
* treat `port` as a no-op, while ID-bearing matches perform translations such as
* canonicalization. Recording and replaying matches is supported by
* [[foresight.eqsat.saturation.EGraphWithRecordedApplications]], and caching is
* [[foresight.eqsat.immutable.EGraphWithRecordedApplications]], and caching is
* provided by [[foresight.eqsat.saturation.SearchAndApply$.immutableWithCaching]].
*
* ## Design contracts
Expand All @@ -91,7 +91,7 @@ package foresight.eqsat
* [[ReversibleSearcher]]; for application:
* [[Applier]] and [[ReversibleApplier]]; for composition: [[Rule]]; for matches
* across snapshots: [[PortableMatch]]; and for caching and recording:
* [[foresight.eqsat.saturation.EGraphWithRecordedApplications]] and
* [[foresight.eqsat.immutable.EGraphWithRecordedApplications]] and
* [[foresight.eqsat.saturation.SearchAndApply$.immutableWithCaching]]. Strategies for
* high-level orchestration include
* [[foresight.eqsat.saturation.Strategy]],
Expand All @@ -103,7 +103,7 @@ package foresight.eqsat
* {{{
* import foresight.eqsat.rewriting._
* import foresight.eqsat.parallel.ParallelMap
* import foresight.eqsat.commands.CommandQueue
* import foresight.eqsat.commands.CommandScheduleBuilder
*
* // 1) Search: find matches in the e-graph
* val s: Searcher[MyNode, MyMatch, MyEGraph] = ...
Expand All @@ -114,9 +114,13 @@ package foresight.eqsat
* // 3) Rule: run now or stage/batch
* val r = Rule("my-rule", s, a)
* val updated = r(egraph, ParallelMap.default) // immediate
* val staged = r.delayed(egraph) // staged
* val batched = CommandQueue(Seq(staged, r2.delayed(egraph))).optimized
* val (next, _) = batched(egraph, Map.empty, ParallelMap.default) // execute batch
*
* // Batch two rules via a shared builder
* val builder = CommandScheduleBuilder.newConcurrentBuilder[MyNode]
* r.delayed(egraph, ParallelMap.default, builder)
* r2.delayed(egraph, ParallelMap.default, builder)
* val batched = builder.result()
* val next = batched.applyImmutable(egraph, ParallelMap.default).getOrElse(egraph)
* }}}
* @example Run with a saturation strategy
* {{{
Expand All @@ -141,9 +145,10 @@ package foresight.eqsat
* @example Portability and caching
* {{{
* import foresight.eqsat.saturation._
* import foresight.eqsat.immutable.EGraphWithRecordedApplications
*
* type M = MyMatch with PortableMatch[MyNode, M]
* val sa = SearchAndApply.withCaching[MyNode, MyEGraph, M]
* val sa = SearchAndApply.immutableWithCaching[MyNode, MyEGraph, M]
*
* val found: Map[String, Seq[M]] =
* sa.search(Seq(r1, r2), EGraphWithRecordedApplications(egraph), ParallelMap.default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import foresight.util.random.{Random, Sample}
* {{{
* val strategy = StochasticRuleApplication(
* rules = myRules,
* searchAndApply = SearchAndApply.withoutCaching,
* searchAndApply = SearchAndApply.immutable,
* priorities = MyCustomPriorities,
* random = Random(42)
* )
Expand Down Expand Up @@ -151,7 +151,7 @@ object StochasticRuleApplication {
priorities: MatchPriorities[NodeT, Rewrite[NodeT, MatchT, EGraphT], EGraphT, MatchT],
random: Random
): StochasticRuleApplication[NodeT, Rewrite[NodeT, MatchT, EGraphT], EGraphT, MatchT] = {
apply(rules, SearchAndApply.immutable[NodeT, EGraphT, MatchT], priorities)
new StochasticRuleApplication(rules, SearchAndApply.immutable[NodeT, EGraphT, MatchT], priorities, random)
}

/**
Expand Down
Loading