diff --git a/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala b/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala index 1658f62e..1311cea9 100644 --- a/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala +++ b/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala @@ -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] diff --git a/foresight/src/main/scala/foresight/eqsat/immutable/EGraph.scala b/foresight/src/main/scala/foresight/eqsat/immutable/EGraph.scala index 013a9600..5f54348d 100644 --- a/foresight/src/main/scala/foresight/eqsat/immutable/EGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/immutable/EGraph.scala @@ -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 diff --git a/foresight/src/main/scala/foresight/eqsat/package.scala b/foresight/src/main/scala/foresight/eqsat/package.scala index 2f617eff..dd7dc7c2 100644 --- a/foresight/src/main/scala/foresight/eqsat/package.scala +++ b/foresight/src/main/scala/foresight/eqsat/package.scala @@ -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 * @@ -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) diff --git a/foresight/src/main/scala/foresight/eqsat/rewriting/Applier.scala b/foresight/src/main/scala/foresight/eqsat/rewriting/Applier.scala index d02c78cb..c7d75c9f 100644 --- a/foresight/src/main/scala/foresight/eqsat/rewriting/Applier.scala +++ b/foresight/src/main/scala/foresight/eqsat/rewriting/Applier.scala @@ -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`. diff --git a/foresight/src/main/scala/foresight/eqsat/rewriting/PortableMatch.scala b/foresight/src/main/scala/foresight/eqsat/rewriting/PortableMatch.scala index bdb79d47..dcc0d467 100644 --- a/foresight/src/main/scala/foresight/eqsat/rewriting/PortableMatch.scala +++ b/foresight/src/main/scala/foresight/eqsat/rewriting/PortableMatch.scala @@ -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. @@ -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`. diff --git a/foresight/src/main/scala/foresight/eqsat/rewriting/Rewrite.scala b/foresight/src/main/scala/foresight/eqsat/rewriting/Rewrite.scala index 4f9f4e14..4473e877 100644 --- a/foresight/src/main/scala/foresight/eqsat/rewriting/Rewrite.scala +++ b/foresight/src/main/scala/foresight/eqsat/rewriting/Rewrite.scala @@ -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. @@ -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. */ @@ -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] diff --git a/foresight/src/main/scala/foresight/eqsat/rewriting/Rule.scala b/foresight/src/main/scala/foresight/eqsat/rewriting/Rule.scala index be2f0072..3e9e7d9e 100644 --- a/foresight/src/main/scala/foresight/eqsat/rewriting/Rule.scala +++ b/foresight/src/main/scala/foresight/eqsat/rewriting/Rule.scala @@ -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 @@ -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, @@ -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 diff --git a/foresight/src/main/scala/foresight/eqsat/rewriting/package.scala b/foresight/src/main/scala/foresight/eqsat/rewriting/package.scala index aa083d01..d2d49d89 100644 --- a/foresight/src/main/scala/foresight/eqsat/rewriting/package.scala +++ b/foresight/src/main/scala/foresight/eqsat/rewriting/package.scala @@ -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 @@ -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 @@ -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]], @@ -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] = ... @@ -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 * {{{ @@ -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) diff --git a/foresight/src/main/scala/foresight/eqsat/saturation/StochasticRuleApplication.scala b/foresight/src/main/scala/foresight/eqsat/saturation/StochasticRuleApplication.scala index 61dbb6d2..24f2774e 100644 --- a/foresight/src/main/scala/foresight/eqsat/saturation/StochasticRuleApplication.scala +++ b/foresight/src/main/scala/foresight/eqsat/saturation/StochasticRuleApplication.scala @@ -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) * ) @@ -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) } /**