egglog_bridge/
lib.rs

1//! An implementation of egglog-style queries on top of core-relations.
2//!
3//! This module translates a well-typed egglog-esque query into the abstractions
4//! from the `core-relations` crate. The main higher-level functionality that it
5//! implements are seminaive evaluation, default values, and merge functions.
6//!
7//! This crate is essentially involved in desugaring: it elaborates the encoding
8//! of core egglog functionality, but it does not implement algorithms for
9//! joins, union-finds, etc.
10
11use std::{
12    fmt::Debug,
13    hash::Hash,
14    iter, mem,
15    ops::{Index, IndexMut},
16    sync::{Arc, Mutex},
17};
18
19use crate::core_relations::{
20    BaseValue, BaseValueId, BaseValues, ColumnId, Constraint, ContainerValue, ContainerValues,
21    CounterId, Database, DisplacedTable, ExecutionState, ExternalFunction, ExternalFunctionId,
22    MergeVal, Offset, PlanStrategy, SortedWritesTable, TableId, TaggedRowBuffer, Value,
23    WrappedTable,
24};
25use crate::numeric_id::{DenseIdMap, DenseIdMapWithReuse, NumericId, define_id};
26use egglog_concurrency::ThreadPool;
27use egglog_core_relations as core_relations;
28use egglog_numeric_id as numeric_id;
29use egglog_reports::{IterationReport, ReportLevel, RuleSetReport};
30use hashbrown::HashMap;
31use indexmap::IndexSet;
32use log::info;
33use once_cell::sync::Lazy;
34use smallvec::SmallVec;
35use web_time::{Duration, Instant};
36
37pub mod macros;
38pub(crate) mod rule;
39#[cfg(test)]
40mod tests;
41
42pub use rule::{Function, QueryEntry, RuleBuilder};
43use thiserror::Error;
44
45/// A live registry of action handles for use by typed primitives.
46///
47/// Maps table name to [`TableAction`] (plus the shared [`UnionAction`]
48/// and the default-panic external-function id) and is owned by the
49/// bridge `EGraph`. The state wrappers (`PureState`/`ReadState`/
50/// `WriteState`/`FullState`) live in the `egglog` crate; they read
51/// from this registry at invoke time to back name-indexed action
52/// methods. Held by the bridge `EGraph` inside an `Arc<RwLock<_>>`.
53#[derive(Clone)]
54pub struct ActionRegistry {
55    table_actions: hashbrown::HashMap<String, TableAction>,
56    union_action: UnionAction,
57    default_panic_id: ExternalFunctionId,
58}
59
60impl ActionRegistry {
61    pub(crate) fn new(union_action: UnionAction, default_panic_id: ExternalFunctionId) -> Self {
62        Self {
63            table_actions: hashbrown::HashMap::new(),
64            union_action,
65            default_panic_id,
66        }
67    }
68
69    pub(crate) fn register_table(&mut self, name: String, action: TableAction) {
70        self.table_actions.insert(name, action);
71    }
72
73    /// Look up the [`TableAction`] for a table by name, or `None` if
74    /// no table with that name has been registered.
75    pub fn lookup_table(&self, name: &str) -> Option<&TableAction> {
76        self.table_actions.get(name)
77    }
78
79    /// Snapshot the registered table names and their current row counts.
80    pub fn table_sizes(&self, state: &ExecutionState) -> Vec<(&str, usize)> {
81        self.table_actions
82            .iter()
83            .map(|(name, action)| (name.as_str(), action.row_count(state)))
84            .collect()
85    }
86
87    /// The shared [`UnionAction`] for this EGraph's union-find.
88    pub fn union_action(&self) -> &UnionAction {
89        &self.union_action
90    }
91
92    /// The default panic external function id, used by the egglog
93    /// crate's `ActionView::panic`.
94    pub fn default_panic_id(&self) -> ExternalFunctionId {
95        self.default_panic_id
96    }
97}
98
99#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
100pub enum ColumnTy {
101    Id,
102    Base(BaseValueId),
103}
104
105define_id!(pub RuleId, u32, "An egglog-style rule");
106define_id!(pub FunctionId, u32, "An id representing an egglog function");
107define_id!(pub(crate) Timestamp, u32, "An abstract timestamp used to track execution of egglog rules");
108impl Timestamp {
109    fn to_value(self) -> Value {
110        Value::new(self.rep())
111    }
112}
113
114/// The state associated with an egglog program.
115#[derive(Clone)]
116pub struct EGraph {
117    db: Database,
118    uf_table: TableId,
119    id_counter: CounterId,
120    timestamp_counter: CounterId,
121    rules: DenseIdMapWithReuse<RuleId, RuleInfo>,
122    funcs: DenseIdMap<FunctionId, FunctionInfo>,
123    panic_message: SideChannel<String>,
124    /// This is a cache of all the different panic messages that we may use while executing rules
125    /// against the EGraph. Oftentimes, these messages are generated dynamically: keeping this map
126    /// around allows us to cache external function ids with repeat panic messages and they can
127    /// also serve as a debugging tool in the case that the number of panic messages grows without
128    /// bound.
129    panic_funcs: HashMap<String, ExternalFunctionId>,
130    report_level: ReportLevel,
131    /// Live registry of name-indexed action handles. Shared (via
132    /// `Arc<RwLock<_>>`) with state wrappers and primitive callbacks
133    /// in the egglog crate so name-indexed action methods on
134    /// [`WriteState`] / [`FullState`] can resolve table actions at
135    /// invoke time. Mutated in place from [`add_table`](EGraph::add_table).
136    action_registry: Arc<std::sync::RwLock<ActionRegistry>>,
137    threads: usize,
138    thread_pool: Option<Arc<ThreadPool>>,
139}
140
141pub type Result<T> = std::result::Result<T, anyhow::Error>;
142
143fn normalize_thread_count(threads: usize) -> usize {
144    #[cfg(target_family = "wasm")]
145    {
146        if threads > 1 {
147            panic!("cannot use more than 1 thread on wasm");
148        }
149        1
150    }
151    #[cfg(not(target_family = "wasm"))]
152    {
153        if threads == 0 {
154            std::thread::available_parallelism()
155                .map(usize::from)
156                .unwrap_or(1)
157        } else {
158            threads
159        }
160    }
161}
162
163fn install_thread_pool<R>(thread_pool: Option<Arc<ThreadPool>>, f: impl FnOnce() -> R) -> R {
164    match thread_pool {
165        Some(thread_pool) => thread_pool.install(f),
166        None => egglog_concurrency::without_current_pool(f),
167    }
168}
169
170impl Default for EGraph {
171    fn default() -> Self {
172        Self::new(1)
173    }
174}
175
176/// Properties of a function added to an [`EGraph`].
177pub struct FunctionConfig {
178    /// The function's schema. The last column in the schema is the return type.
179    pub schema: Vec<ColumnTy>,
180    /// The behavior of the function when lookups are made on keys not currently present.
181    pub default: DefaultVal,
182    /// How to resolve FD conflicts for the function.
183    pub merge: MergeFn,
184    /// The function's name
185    pub name: String,
186    /// Whether or not subsumption is enabled for this function.
187    pub can_subsume: bool,
188}
189
190impl EGraph {
191    /// Create an e-graph configured to use `threads` worker threads.
192    ///
193    /// Passing `1` keeps execution serial and does not allocate a thread pool.
194    /// Passing `0` uses the host's available parallelism.
195    pub fn new(threads: usize) -> Self {
196        let threads = normalize_thread_count(threads);
197        let thread_pool = (threads > 1).then(|| Arc::new(ThreadPool::new(threads)));
198        let (mut db, uf_table, id_counter, ts_counter) =
199            install_thread_pool(thread_pool.clone(), || {
200                let mut db = Database::new();
201                let uf_table = db.add_table_named(
202                    DisplacedTable::default(),
203                    "$uf".into(),
204                    iter::empty(),
205                    iter::empty(),
206                );
207                let id_counter = db.add_counter();
208                let ts_counter = db.add_counter();
209                // Start the timestamp counter at 1.
210                db.inc_counter(ts_counter);
211                (db, uf_table, id_counter, ts_counter)
212            });
213
214        // Register a default panic external function so the typed
215        // state wrappers' `panic()` method has an id to call. This
216        // also seeds `panic_funcs` so a later `new_panic` with the
217        // same message reuses the id.
218        let panic_message: SideChannel<String> = Default::default();
219        let mut panic_funcs: HashMap<String, ExternalFunctionId> = Default::default();
220        let default_panic_msg = "primitive panicked".to_string();
221        let default_panic_id = db.add_external_function(Box::new(Panic(
222            default_panic_msg.clone(),
223            panic_message.clone(),
224        )));
225        panic_funcs.insert(default_panic_msg, default_panic_id);
226
227        let union_action = UnionAction {
228            table: uf_table,
229            timestamp: ts_counter,
230        };
231        let action_registry = Arc::new(std::sync::RwLock::new(ActionRegistry::new(
232            union_action,
233            default_panic_id,
234        )));
235
236        Self {
237            db,
238            uf_table,
239            id_counter,
240            timestamp_counter: ts_counter,
241            rules: Default::default(),
242            funcs: Default::default(),
243            panic_message,
244            panic_funcs,
245            report_level: Default::default(),
246            action_registry,
247            threads,
248            thread_pool,
249        }
250    }
251
252    /// Return a copy of this e-graph configured with a different thread count.
253    pub fn with_num_threads(mut self, threads: usize) -> Self {
254        self.set_num_threads(threads);
255        self
256    }
257
258    /// Set the number of worker threads used by this e-graph.
259    ///
260    /// Passing `1` disables the pool. Passing `0` uses available parallelism.
261    pub fn set_num_threads(&mut self, threads: usize) {
262        let threads = normalize_thread_count(threads);
263        self.threads = threads;
264        self.thread_pool = (threads > 1).then(|| Arc::new(ThreadPool::new(threads)));
265    }
266
267    /// Return the configured thread count.
268    pub fn num_threads(&self) -> usize {
269        self.threads
270    }
271
272    fn thread_pool(&self) -> Option<Arc<ThreadPool>> {
273        self.thread_pool.clone()
274    }
275
276    fn next_ts(&self) -> Timestamp {
277        Timestamp::from_usize(self.db.read_counter(self.timestamp_counter))
278    }
279
280    fn inc_ts(&mut self) {
281        self.db.inc_counter(self.timestamp_counter);
282    }
283
284    /// Get a mutable reference to the underlying table of base values for this
285    /// `EGraph`.
286    pub fn base_values_mut(&mut self) -> &mut BaseValues {
287        self.db.base_values_mut()
288    }
289
290    /// Get a mutable reference to the underlying table of containers for this
291    /// `EGraph`.
292    pub fn container_values_mut(&mut self) -> &mut ContainerValues {
293        self.db.container_values_mut()
294    }
295
296    /// Get a reference to the underlying table of containers for this `EGraph`.
297    pub fn container_values(&self) -> &ContainerValues {
298        self.db.container_values()
299    }
300
301    /// Intern the given container value into the EGraph.
302    pub fn get_container_value<C: ContainerValue>(&mut self, val: C) -> Value {
303        self.register_container_ty::<C>();
304        self.db
305            .with_execution_state(|state| state.clone().container_values().register_val(val, state))
306    }
307
308    /// Register the given [`ContainerValue`] type with this EGraph.
309    ///
310    /// The given container will use the EGraph's union-find to manage rebuilding and the merging
311    /// of containers with a common id.
312    pub fn register_container_ty<C: ContainerValue>(&mut self) {
313        let uf_table = self.uf_table;
314        let ts_counter = self.timestamp_counter;
315        self.db.container_values_mut().register_type::<C>(
316            self.id_counter,
317            move |state, old, new| {
318                if old != new {
319                    let next_ts = Value::from_usize(state.read_counter(ts_counter));
320                    state.stage_insert(uf_table, &[old, new, next_ts]);
321                    std::cmp::min(old, new)
322                } else {
323                    old
324                }
325            },
326        );
327    }
328
329    /// Get a reference to the underlying table of base values for this `EGraph`.
330    pub fn base_values(&self) -> &BaseValues {
331        self.db.base_values()
332    }
333
334    /// Create a [`QueryEntry`] for a base value.
335    pub fn base_value_constant<T>(&self, x: T) -> QueryEntry
336    where
337        T: BaseValue,
338    {
339        QueryEntry::Const {
340            val: self.base_values().get(x),
341            ty: ColumnTy::Base(self.base_values().get_ty::<T>()),
342        }
343    }
344
345    /// Register a low-level external function. The callback receives a
346    /// raw `&mut ExecutionState`.
347    ///
348    /// # Seminaive-safety trust boundary
349    ///
350    /// Like [`EGraph::with_execution_state`], this is a raw escape —
351    /// the registered function has unrestricted access and is not
352    /// tracked by the per-context validity system. Prefer building
353    /// primitives via the higher-level `egglog::Primitive` /
354    /// `egglog::EGraph::add_primitive` API, which enforces #772's
355    /// seminaive-safety contract.
356    pub fn register_external_func(
357        &mut self,
358        func: Box<dyn ExternalFunction + 'static>,
359    ) -> ExternalFunctionId {
360        self.db.add_external_function(func)
361    }
362
363    pub fn free_external_func(&mut self, func: ExternalFunctionId) {
364        self.db.free_external_function(func)
365    }
366
367    /// Generate a fresh id.
368    pub fn fresh_id(&mut self) -> Value {
369        Value::from_usize(self.db.inc_counter(self.id_counter))
370    }
371
372    /// Look up the canonical value for `val` in the union-find.
373    ///
374    /// If the value has never been inserted into the union-find, `val` is returned.
375    fn get_canon_in_uf(&self, val: Value) -> Value {
376        let table = self.db.get_table(self.uf_table);
377        let row = table.get_row(&[val]);
378        row.map(|row| row.vals[1]).unwrap_or(val)
379    }
380
381    /// Get the canonical representation for `val` based on type.
382    ///
383    /// For [`ColumnTy::Id`], it looks up the union find; otherwise,
384    /// it returns the value itself.
385    pub fn get_canon_repr(&self, val: Value, ty: ColumnTy) -> Value {
386        match ty {
387            ColumnTy::Id => self.get_canon_in_uf(val),
388            ColumnTy::Base(_) => val,
389        }
390    }
391
392    /// Load the given values into the database.
393    ///
394    /// # Panics
395    /// This method panics if the values do not match the arity of the function.
396    ///
397    /// NB: this is not an efficient interface for bulk loading. We should add
398    /// one that allows us to pass through a series of RowBuffers before
399    /// incrementing the timestamp.
400    pub fn add_values(&mut self, values: impl IntoIterator<Item = (FunctionId, Vec<Value>)>) {
401        let mut extended_row = Vec::<Value>::new();
402        let mut bufs = DenseIdMap::default();
403        for (func, row) in values.into_iter() {
404            let table_info = &self.funcs[func];
405            let schema_math = SchemaMath {
406                subsume: table_info.can_subsume,
407                func_cols: table_info.schema.len(),
408            };
409            let table_id = table_info.table;
410            extended_row.extend_from_slice(&row);
411            schema_math.write_table_row(
412                &mut extended_row,
413                RowVals {
414                    timestamp: self.next_ts().to_value(),
415                    subsume: schema_math.subsume.then_some(NOT_SUBSUMED),
416                    ret_val: None, // already filled in.
417                },
418            );
419            let buf = bufs.get_or_insert(table_id, || self.db.new_buffer(table_id));
420            buf.stage_insert(&extended_row);
421            extended_row.clear();
422        }
423        // Flush the buffers.
424        mem::drop(bufs);
425        self.flush_updates();
426    }
427
428    /// A term-oriented means of adding data to the database: hand back a "term
429    /// id" for the given function and keys for the function.
430    ///
431    /// # Panics
432    /// This method panics if the values do not match the arity of the function.
433    pub fn add_term(&mut self, func: FunctionId, inputs: &[Value]) -> Value {
434        let info = &self.funcs[func];
435        let schema_math = SchemaMath {
436            subsume: info.can_subsume,
437            func_cols: info.schema.len(),
438        };
439        let mut extended_row = Vec::new();
440        extended_row.extend_from_slice(inputs);
441        let res = self.fresh_id();
442        schema_math.write_table_row(
443            &mut extended_row,
444            RowVals {
445                timestamp: self.next_ts().to_value(),
446                ret_val: Some(res),
447                subsume: schema_math.subsume.then_some(NOT_SUBSUMED),
448            },
449        );
450        extended_row[schema_math.ret_val_col()] = res;
451        let table_id = self.funcs[func].table;
452        self.db.new_buffer(table_id).stage_insert(&extended_row);
453        self.flush_updates();
454        self.get_canon_in_uf(res)
455    }
456
457    /// Lookup the id associated with a function `func` and the given arguments
458    /// (`key`).
459    pub fn lookup_id(&self, func: FunctionId, key: &[Value]) -> Option<Value> {
460        let info = &self.funcs[func];
461        let schema_math = SchemaMath {
462            subsume: info.can_subsume,
463            func_cols: info.schema.len(),
464        };
465        let table_id = info.table;
466        let table = self.db.get_table(table_id);
467        let row = table.get_row(key)?;
468        Some(row.vals[schema_math.ret_val_col()])
469    }
470
471    pub fn approx_table_size(&self, table: FunctionId) -> usize {
472        self.db.estimate_size(self.funcs[table].table, None)
473    }
474
475    pub fn table_size(&self, table: FunctionId) -> usize {
476        self.db.get_table(self.funcs[table].table).len()
477    }
478
479    /// Remove every row from the given function's backing table.
480    ///
481    /// This is the bulk counterpart to staging a `remove` for every key in the
482    /// table: the underlying `Database::clear_table` drops the row buffer in
483    /// O(1)-in-row-count time and bumps the table's major generation, which
484    /// lazily invalidates any cached subsets or indexes a later reader might
485    /// consult. Any rows staged for this table by an in-flight
486    /// `MutationBuffer` are dropped along with the table contents.
487    ///
488    /// Callers that have staged inserts/removes for *other* tables that they
489    /// want flushed first should call [`EGraph::flush_updates`] before
490    /// clearing.
491    pub fn clear_table(&mut self, func: FunctionId) {
492        let table_id = self.funcs[func].table;
493        self.db.clear_table(table_id);
494    }
495
496    /// Read the contents of the given function.
497    ///
498    /// The callback `f` is called with each row and its subsumption status.
499    pub fn for_each(&self, table: FunctionId, mut f: impl FnMut(ScanEntry<'_>)) {
500        self.for_each_while(table, |row| {
501            f(row);
502            true
503        });
504    }
505
506    /// Iterate over the rows of a function table, calling `f` on each row. If `f` returns `false`
507    /// the function returns early and stops reading rows from the table.
508    pub fn for_each_while(&self, table: FunctionId, mut f: impl FnMut(ScanEntry<'_>) -> bool) {
509        let info = &self.funcs[table];
510        let table = self.funcs[table].table;
511        let schema_math = SchemaMath {
512            subsume: info.can_subsume,
513            func_cols: info.schema.len(),
514        };
515        let imp = self.db.get_table(table);
516        let all = imp.all();
517        let mut cur = Offset::new(0);
518        let mut buf = TaggedRowBuffer::new(imp.spec().arity());
519        // This somewhat awkward iteration strategy is forced on us by the `scan_bounded` API. We
520        // should look into ways to avoid this cludge where the loop body effectively must be
521        // repeated at the end. The obvious and idiomatic ways to do this all require
522        // `dyn`-compatibility on `Table` or dynamic dispatch per row.
523        macro_rules! drain_buf {
524            ($buf:expr) => {
525                for (_, row) in $buf.non_stale() {
526                    let subsumed =
527                        schema_math.subsume && row[schema_math.subsume_col()] == SUBSUMED;
528                    if !f(ScanEntry {
529                        vals: &row[0..schema_math.func_cols],
530                        subsumed,
531                    }) {
532                        return;
533                    }
534                }
535                $buf.clear();
536            };
537        }
538        while let Some(next) = imp.scan_bounded(all.as_ref(), cur, 32, &mut buf) {
539            drain_buf!(buf);
540            cur = next;
541        }
542        drain_buf!(buf);
543    }
544
545    /// A basic method for dumping the state of the database to `log::info!`.
546    ///
547    /// For large tables, this is unlikely to give particularly useful output.
548    pub fn dump_debug_info(&self) {
549        info!("=== View Tables ===");
550        for (id, info) in self.funcs.iter() {
551            let table = self.db.get_table(info.table);
552            self.scan_table(table, |row| {
553                info!(
554                    "View Table {name} / {id:?} / {table:?}: {row:?}",
555                    name = info.name,
556                    table = info.table
557                )
558            });
559        }
560    }
561
562    /// A helper for scanning the entries in a table.
563    fn scan_table(&self, table: &WrappedTable, mut f: impl FnMut(&[Value])) {
564        const BATCH_SIZE: usize = 128;
565        let all = table.all();
566        let mut cur = Offset::new(0);
567        let mut out = TaggedRowBuffer::new(table.spec().arity());
568        while let Some(next) = table.scan_bounded(all.as_ref(), cur, BATCH_SIZE, &mut out) {
569            out.non_stale().for_each(|(_, row)| f(row));
570            out.clear();
571            cur = next;
572        }
573        out.non_stale().for_each(|(_, row)| f(row));
574    }
575
576    /// Register a function in this EGraph.
577    pub fn add_table(&mut self, config: FunctionConfig) -> FunctionId {
578        let FunctionConfig {
579            schema,
580            default,
581            merge,
582            name,
583            can_subsume,
584        } = config;
585        assert!(
586            !schema.is_empty(),
587            "must have at least one column in schema"
588        );
589        let to_rebuild: Vec<ColumnId> = schema
590            .iter()
591            .enumerate()
592            .filter(|(_, ty)| matches!(ty, ColumnTy::Id))
593            .map(|(i, _)| ColumnId::from_usize(i))
594            .collect();
595        let schema_math = SchemaMath {
596            subsume: can_subsume,
597            func_cols: schema.len(),
598        };
599        let n_args = schema_math.num_keys();
600        let n_cols = schema_math.table_columns();
601        let next_func_id = self.funcs.next_id();
602        let mut read_deps = IndexSet::<TableId>::new();
603        let mut write_deps = IndexSet::<TableId>::new();
604        merge.fill_deps(self, &mut read_deps, &mut write_deps);
605        let merge_fn = merge.to_callback(schema_math, &name, self);
606        let table = install_thread_pool(self.thread_pool(), || {
607            SortedWritesTable::new(
608                n_args,
609                n_cols,
610                Some(ColumnId::from_usize(schema.len())),
611                to_rebuild,
612                merge_fn,
613            )
614        });
615        let name: Arc<str> = name.into();
616        let table_id = self.db.add_table_named(
617            table,
618            name.clone(),
619            read_deps.iter().copied(),
620            write_deps.iter().copied(),
621        );
622
623        let res = self.funcs.push(FunctionInfo {
624            table: table_id,
625            schema: schema.clone(),
626            incremental_rebuild_rules: Default::default(),
627            nonincremental_rebuild_rule: RuleId::new(!0),
628            default_val: default,
629            can_subsume,
630            name,
631        });
632        debug_assert_eq!(res, next_func_id);
633        let incremental_rebuild_rules = self.incremental_rebuild_rules(res, &schema);
634        let nonincremental_rebuild_rule = self.nonincremental_rebuild(res, &schema);
635        let info = &mut self.funcs[res];
636        info.incremental_rebuild_rules = incremental_rebuild_rules;
637        info.nonincremental_rebuild_rule = nonincremental_rebuild_rule;
638        let action = TableAction::new(self, res);
639        let table_name = self.funcs[res].name.to_string();
640        self.action_registry
641            .write()
642            .unwrap()
643            .register_table(table_name, action);
644        res
645    }
646
647    /// A handle to the live [`ActionRegistry`] for this EGraph.
648    /// The handle is shared (`Arc<RwLock<_>>`); cloning the outer
649    /// `Arc` does not duplicate the underlying registry. Used by the
650    /// egglog crate's primitive machinery to thread the registry into
651    /// state wrappers at invoke time.
652    pub fn action_registry(&self) -> &Arc<std::sync::RwLock<ActionRegistry>> {
653        &self.action_registry
654    }
655
656    /// Run the given rules, returning whether the database changed.
657    ///
658    /// If the given rules are malformed, this method can return an error.
659    pub fn run_rules(&mut self, rules: &[RuleId]) -> Result<IterationReport> {
660        let thread_pool = self.thread_pool();
661        install_thread_pool(thread_pool, || self.run_rules_inner(rules))
662    }
663
664    fn run_rules_inner(&mut self, rules: &[RuleId]) -> Result<IterationReport> {
665        let ts = self.next_ts();
666
667        let uf_size_before = self.db.get_table(self.uf_table).len();
668        let rule_set_report =
669            run_rules_impl(&mut self.db, &mut self.rules, rules, ts, self.report_level)?;
670        if let Some(message) = self.panic_message.lock().unwrap().take() {
671            return Err(PanicError(message).into());
672        }
673
674        let mut iteration_report = IterationReport {
675            rule_set_report,
676            rebuild_time: Duration::ZERO,
677        };
678        let uf_size_after = self.db.get_table(self.uf_table).len();
679        if uf_size_before == uf_size_after {
680            // No new unions: skip the full rebuild but still advance the
681            // timestamp so that seminaive evaluation sees a fresh epoch.
682            // Rebuilding is only necessary when new unions have been made because ids may need to be updated.
683            // Adding terms doesn't necessarily touch the union-find, only doing a union between existing ids does.
684            self.inc_ts();
685            return Ok(iteration_report);
686        }
687
688        let rebuild_timer = Instant::now();
689        self.rebuild()?;
690        iteration_report.rebuild_time = rebuild_timer.elapsed();
691
692        if let Some(message) = self.panic_message.lock().unwrap().take() {
693            return Err(PanicError(message).into());
694        }
695
696        Ok(iteration_report)
697    }
698
699    fn rebuild(&mut self) -> Result<()> {
700        let do_parallel = egglog_concurrency::current_num_threads() > 1;
701        if self.db.get_table(self.uf_table).rebuilder(&[]).is_some() {
702            // The UF implementation supports "native"  rebuilding.
703            let mut tables = Vec::with_capacity(self.funcs.next_id().index());
704            for (_, func) in self.funcs.iter() {
705                tables.push(func.table);
706            }
707            loop {
708                // Order matters here: we need to rebuild containers first and then rebuild the
709                // tables. Why?
710                //
711                // Say we have a sort that can map to and from a vector containing only itself:
712                // (sort X)
713                // (function to-vec (X) (Vec X) :no-merge)
714                // (constructor from-vec (Vec X) X)
715                // (constructor Num (i64) X)
716                // (constructor Add (X X) X)
717                //
718                // Along with rules:
719                // (rule ((= x (Num i))) ((set (to-vec x) (vec-of x))))
720                // (rule ((= x (Add i j))) ((set (to-vec x) (vec-of x))))
721                // (rule ((= x (from-vec v))) ((set (to-vec x) v))
722                // (rewrite (Add (Num i) (Num j)) (Num (+ i j)))
723                //
724                // These rules, while redundant, should be safe. However, if we rebuild tables
725                // before containers some schedules can cause us to violate the `:no-merge`
726                // directive, which asserts that all values written for a key are equal.
727                //
728                // Suppose we start off with x1=(Num 1), x2=(Num 3), and x3=(Add (Num 1) (Num 2)) as
729                // expressions, with `to-vec` and `from-vec` entries for all three expressions.
730                // We'll call (to-vec xi) vi for all i.
731                //
732                // Now suppose we run the `rewrite` above: now, x3 = x2. But v3 will only equal v2
733                // _after_ we rebuild the `Vec` container. That means that if we rebuild `to-vec`
734                // we will collapse the the rows for x3 and x2, but then fail to merge v3 and v2
735                // because they are not (yet) equal.
736                //
737                // Rebuilding containers first will find that v3 and v2 are equal, and the rest of
738                // the rules can proceed.
739                let container_rebuild = self.db.rebuild_containers(self.uf_table);
740                let next_ts = self.next_ts().to_value();
741                let table_rebuild = self.db.apply_rebuild(self.uf_table, &tables, next_ts);
742                // Container rebuild can make a parent row newly matchable without
743                // changing the row's stored id. Re-timestamp those parents so
744                // seminaive sees the newly enabled match on the next pass.
745                let dirty_ids: Vec<Value> = container_rebuild.dirty_ids().iter().copied().collect();
746                let refreshed_rows = self
747                    .db
748                    .refresh_rows_for_values(&tables, &dirty_ids, next_ts);
749                self.inc_ts();
750                if !table_rebuild && !refreshed_rows && !container_rebuild.changed() {
751                    break;
752                }
753            }
754            return Ok(());
755        }
756        if do_parallel {
757            return self.rebuild_parallel();
758        }
759        let start = Instant::now();
760
761        // The database changed. Rebuild. New entries should land after the given rules.
762        let mut changed = true;
763        while changed {
764            changed = false;
765            // We need to iterate rebuilding to a fixed point. Future scans
766            // should look only at the latest updates.
767            self.inc_ts();
768            let ts = self.next_ts();
769            for (_, info) in self.funcs.iter_mut() {
770                let last_rebuilt_at = self.rules[info.nonincremental_rebuild_rule].last_run_at;
771                let table_size = self.db.estimate_size(info.table, None);
772                let uf_size = self.db.estimate_size(
773                    self.uf_table,
774                    Some(Constraint::GeConst {
775                        col: ColumnId::new(2),
776                        val: last_rebuilt_at.to_value(),
777                    }),
778                );
779                if incremental_rebuild(uf_size, table_size, false) {
780                    marker_incremental_rebuild(|| -> Result<()> {
781                        // Run each of the incremental rules serially.
782                        //
783                        // This is to avoid recanonicalizing the same row multiple
784                        // times.
785                        for rule in &info.incremental_rebuild_rules {
786                            changed |= run_rules_impl(
787                                &mut self.db,
788                                &mut self.rules,
789                                &[*rule],
790                                ts,
791                                ReportLevel::TimeOnly,
792                            )?
793                            .changed;
794                        }
795                        // Reset the rule we did not run. These two should be equivalent.
796                        self.rules[info.nonincremental_rebuild_rule].last_run_at = ts;
797                        Ok(())
798                    })?;
799                } else {
800                    marker_nonincremental_rebuild(|| -> Result<()> {
801                        changed |= run_rules_impl(
802                            &mut self.db,
803                            &mut self.rules,
804                            &[info.nonincremental_rebuild_rule],
805                            ts,
806                            ReportLevel::TimeOnly,
807                        )?
808                        .changed;
809                        for rule in &info.incremental_rebuild_rules {
810                            self.rules[*rule].last_run_at = ts;
811                        }
812                        Ok(())
813                    })?;
814                }
815            }
816        }
817        log::info!("rebuild took {:?}", start.elapsed());
818        Ok(())
819    }
820
821    /// A variant of `rebuild` that attempts to combine rebuild rules into
822    /// larger rulesets to increase parallelism. This kind of preprocessing can
823    /// slow processing down in a single-threaded setting, so it is only used
824    /// when the number of active threads is greater than 1.
825    fn rebuild_parallel(&mut self) -> Result<()> {
826        let start = Instant::now();
827        #[derive(Default)]
828        struct RebuildState {
829            nonincremental: Vec<FunctionId>,
830            incremental: DenseIdMap<usize, SmallVec<[FunctionId; 2]>>,
831        }
832
833        impl RebuildState {
834            fn clear(&mut self) {
835                self.nonincremental.clear();
836                self.incremental.iter_mut().for_each(|(_, v)| v.clear());
837            }
838        }
839
840        let mut changed = true;
841        let mut state = RebuildState::default();
842        let mut scratch = Vec::new();
843        while changed {
844            changed = false;
845            state.clear();
846            self.inc_ts();
847            // First, figure out which functions will be rebuilt nonincrementally,
848            // vs. incrementally. Group them together.
849            for (func, info) in self.funcs.iter_mut() {
850                let last_rebuilt_at = self.rules[info.nonincremental_rebuild_rule].last_run_at;
851                let table_size = self.db.estimate_size(info.table, None);
852                let uf_size = self.db.estimate_size(
853                    self.uf_table,
854                    Some(Constraint::GeConst {
855                        col: ColumnId::new(2),
856                        val: last_rebuilt_at.to_value(),
857                    }),
858                );
859                if incremental_rebuild(uf_size, table_size, true) {
860                    for (i, _) in info.incremental_rebuild_rules.iter().enumerate() {
861                        state.incremental.get_or_default(i).push(func);
862                    }
863                } else {
864                    state.nonincremental.push(func);
865                }
866            }
867            let ts = self.next_ts();
868            for func in state.nonincremental.iter().copied() {
869                scratch.push(self.funcs[func].nonincremental_rebuild_rule);
870                for rule in &self.funcs[func].incremental_rebuild_rules {
871                    self.rules[*rule].last_run_at = ts;
872                }
873            }
874            changed |= run_rules_impl(
875                &mut self.db,
876                &mut self.rules,
877                &scratch,
878                ts,
879                ReportLevel::TimeOnly,
880            )?
881            .changed;
882            scratch.clear();
883            let ts = self.next_ts();
884            for (i, funcs) in state.incremental.iter() {
885                for func in funcs.iter().copied() {
886                    let info = &mut self.funcs[func];
887                    scratch.push(info.incremental_rebuild_rules[i]);
888                    self.rules[info.nonincremental_rebuild_rule].last_run_at = ts;
889                }
890                changed |= run_rules_impl(
891                    &mut self.db,
892                    &mut self.rules,
893                    &scratch,
894                    ts,
895                    ReportLevel::TimeOnly,
896                )?
897                .changed;
898                scratch.clear();
899            }
900        }
901        log::info!("rebuild took {:?}", start.elapsed());
902        Ok(())
903    }
904
905    fn incremental_rebuild_rules(&mut self, table: FunctionId, schema: &[ColumnTy]) -> Vec<RuleId> {
906        schema
907            .iter()
908            .enumerate()
909            .filter_map(|(i, ty)| match ty {
910                ColumnTy::Id => {
911                    Some(self.incremental_rebuild_rule(table, schema, ColumnId::from_usize(i)))
912                }
913                ColumnTy::Base(_) => None,
914            })
915            .collect()
916    }
917
918    fn incremental_rebuild_rule(
919        &mut self,
920        table: FunctionId,
921        schema: &[ColumnTy],
922        col: ColumnId,
923    ) -> RuleId {
924        let subsume = self.funcs[table].can_subsume;
925        let table_id = self.funcs[table].table;
926        let uf_table = self.uf_table;
927        // Two atoms, one binding a whole tuple, one binding a displaced column
928        let mut rb = self.new_rule(&format!("incremental rebuild {table:?}, {col:?}"), true);
929        rb.set_plan_strategy(PlanStrategy::MinCover);
930        let mut vars = Vec::<QueryEntry>::with_capacity(schema.len());
931        for ty in schema {
932            vars.push(rb.new_var(*ty).into());
933        }
934        let canon_val: QueryEntry = rb.new_var(ColumnTy::Id).into();
935        let subsume_var = subsume.then(|| rb.new_var(ColumnTy::Id));
936        rb.add_atom_with_timestamp_and_func(
937            table_id,
938            Some(table),
939            subsume_var.clone().map(QueryEntry::from),
940            &vars,
941        );
942        rb.add_atom_with_timestamp_and_func(
943            uf_table,
944            None,
945            None,
946            &[vars[col.index()].clone(), canon_val.clone()],
947        );
948        rb.set_focus(1); // Set the uf atom as the sole focus.
949
950        // Now canonicalize the entire row.
951        let mut canon = Vec::<QueryEntry>::with_capacity(schema.len());
952        for (i, (var, ty)) in vars.iter().zip(schema.iter()).enumerate() {
953            canon.push(if i == col.index() {
954                canon_val.clone()
955            } else if let ColumnTy::Id = ty {
956                rb.lookup_uf(var.clone()).unwrap().into()
957            } else {
958                var.clone()
959            })
960        }
961
962        // Remove the old row and insert the new one.
963        rb.rebuild_row(table, &vars, &canon, subsume_var);
964        rb.build()
965    }
966
967    fn nonincremental_rebuild(&mut self, table: FunctionId, schema: &[ColumnTy]) -> RuleId {
968        let can_subsume = self.funcs[table].can_subsume;
969        let table_id = self.funcs[table].table;
970        let mut rb = self.new_rule(&format!("nonincremental rebuild {table:?}"), false);
971        rb.set_plan_strategy(PlanStrategy::MinCover);
972        let mut vars = Vec::<QueryEntry>::with_capacity(schema.len());
973        for ty in schema {
974            vars.push(rb.new_var(*ty).into());
975        }
976        let subsume_var = can_subsume.then(|| rb.new_var(ColumnTy::Id));
977        rb.add_atom_with_timestamp_and_func(
978            table_id,
979            Some(table),
980            subsume_var.clone().map(QueryEntry::from),
981            &vars,
982        );
983        let mut lhs = SmallVec::<[QueryEntry; 4]>::new();
984        let mut rhs = SmallVec::<[QueryEntry; 4]>::new();
985        let mut canon = Vec::<QueryEntry>::with_capacity(schema.len());
986        for (var, ty) in vars.iter().zip(schema.iter()) {
987            canon.push(if let ColumnTy::Id = ty {
988                lhs.push(var.clone());
989                let canon_var = QueryEntry::from(rb.lookup_uf(var.clone()).unwrap());
990                rhs.push(canon_var.clone());
991                canon_var
992            } else {
993                var.clone()
994            })
995        }
996        rb.check_for_update(&lhs, &rhs).unwrap();
997        rb.rebuild_row(table, &vars, &canon, subsume_var);
998        rb.build()
999    }
1000
1001    /// Gives the user a handle to the underlying ExecutionState. Useful for staging updates
1002    /// to the database.
1003    ///
1004    /// The staged updates are not immediately reflected in the EGraph, so you may want to
1005    /// manually flush the updates using [`EGraph::flush_updates`].
1006    ///
1007    /// # Seminaive-safety trust boundary
1008    ///
1009    /// This method hands out a raw `&mut ExecutionState`, which bypasses
1010    /// the egglog crate's `Read` / `Write` capability wrappers
1011    /// (`PureState`, `WriteState`, `ReadState`, `FullState`) that
1012    /// enforce #772's seminaive-safety model. Treat it as top-level
1013    /// / global-action context: appropriate for one-shot database
1014    /// manipulation from outside any rule, not for use inside
1015    /// primitive implementations.
1016    pub fn with_execution_state<R>(&self, f: impl FnOnce(&mut ExecutionState<'_>) -> R) -> R {
1017        let thread_pool = self.thread_pool();
1018        install_thread_pool(thread_pool, || self.db.with_execution_state(f))
1019    }
1020
1021    /// Like [`EGraph::with_execution_state`], but also reports whether `f`
1022    /// staged any mutation. A read-only closure leaves the flag `false`, so
1023    /// callers can skip a [`EGraph::flush_updates`] that would otherwise be a
1024    /// no-op merge plus a spurious timestamp bump.
1025    pub fn with_execution_state_tracked<R>(
1026        &self,
1027        f: impl FnOnce(&mut ExecutionState<'_>) -> R,
1028    ) -> (R, bool) {
1029        self.db.with_execution_state_tracked(f)
1030    }
1031
1032    /// Flush the pending update buffers to the EGraph.
1033    /// Returns `true` if the database is updated.
1034    pub fn flush_updates(&mut self) -> bool {
1035        let thread_pool = self.thread_pool();
1036        install_thread_pool(thread_pool, || self.flush_updates_inner())
1037    }
1038
1039    fn flush_updates_inner(&mut self) -> bool {
1040        let uf_size_before = self.db.get_table(self.uf_table).len();
1041        let updated = self.db.merge_all();
1042        self.inc_ts();
1043        let uf_size_after = self.db.get_table(self.uf_table).len();
1044        if uf_size_before != uf_size_after {
1045            // Rebuilding is only necessary when new unions have been made because ids may need to be updated.
1046            // Adding terms doesn't necessarily touch the union-find, only doing a union between existing ids does.
1047            self.rebuild().unwrap();
1048        }
1049        updated
1050    }
1051
1052    pub fn set_report_level(&mut self, level: ReportLevel) {
1053        self.report_level = level;
1054    }
1055}
1056
1057#[derive(Clone)]
1058struct RuleInfo {
1059    last_run_at: Timestamp,
1060    query: rule::Query,
1061    cached_plan: Option<CachedPlanInfo>,
1062    desc: Arc<str>,
1063}
1064
1065#[derive(Clone)]
1066struct CachedPlanInfo {
1067    plan: Arc<core_relations::CachedPlan>,
1068    /// A mapping from index into a [`rule::Query`]'s atoms to the atoms in the underlying cached
1069    /// plan.
1070    atom_mapping: Vec<core_relations::AtomId>,
1071}
1072
1073#[derive(Clone)]
1074struct FunctionInfo {
1075    table: TableId,
1076    schema: Vec<ColumnTy>,
1077    incremental_rebuild_rules: Vec<RuleId>,
1078    nonincremental_rebuild_rule: RuleId,
1079    default_val: DefaultVal,
1080    can_subsume: bool,
1081    name: Arc<str>,
1082}
1083
1084impl FunctionInfo {
1085    fn ret_ty(&self) -> ColumnTy {
1086        self.schema.last().copied().unwrap()
1087    }
1088}
1089
1090/// How defaults are computed for the given function.
1091#[derive(Copy, Clone)]
1092pub enum DefaultVal {
1093    /// Generate a fresh UF id.
1094    FreshId,
1095    /// Cause an egglog-level panic if a lookup fails.
1096    Fail,
1097    /// Insert a constant of some kind.
1098    Const(Value),
1099}
1100
1101/// How to resolve FD conflicts for a table.
1102pub enum MergeFn {
1103    /// Panic if the old and new values don't match.
1104    AssertEq,
1105    /// Use congruence to resolve FD conflicts.
1106    UnionId,
1107    /// The output of a merge is determined by applying the given ExternalFunction to the result
1108    /// of the argument merge functions.
1109    Primitive(ExternalFunctionId, Vec<MergeFn>),
1110    /// The output of a merge is determined by looking up the value for the given function and the
1111    /// given arguments in the egraph.
1112    Function(FunctionId, Vec<MergeFn>),
1113    /// Always return the old value for the given function.
1114    Old,
1115    /// Always return the new value for the given function.
1116    New,
1117    /// Always overwrite the new value for the given function with a constant. This is more useful
1118    /// as a "base case" in a more complicated merge function (e.g. one that clamps a value between
1119    /// 1 and 100) than it is as a standalone merge function.
1120    Const(Value),
1121}
1122
1123impl MergeFn {
1124    fn fill_deps(
1125        &self,
1126        egraph: &EGraph,
1127        read_deps: &mut IndexSet<TableId>,
1128        write_deps: &mut IndexSet<TableId>,
1129    ) {
1130        use MergeFn::*;
1131        match self {
1132            Primitive(_, args) => {
1133                args.iter()
1134                    .for_each(|arg| arg.fill_deps(egraph, read_deps, write_deps));
1135                write_deps.insert(egraph.uf_table);
1136            }
1137            Function(func, args) => {
1138                read_deps.insert(egraph.funcs[*func].table);
1139                write_deps.insert(egraph.funcs[*func].table);
1140                args.iter()
1141                    .for_each(|arg| arg.fill_deps(egraph, read_deps, write_deps));
1142            }
1143            UnionId => {
1144                write_deps.insert(egraph.uf_table);
1145            }
1146            AssertEq | Old | New | Const(..) => {}
1147        }
1148    }
1149
1150    fn to_callback(
1151        &self,
1152        schema_math: SchemaMath,
1153        function_name: &str,
1154        egraph: &mut EGraph,
1155    ) -> Box<core_relations::MergeFn> {
1156        let resolved = self.resolve(function_name, egraph);
1157
1158        Box::new(move |state, cur, new, out| {
1159            let timestamp = new[schema_math.ts_col()];
1160
1161            let mut changed = false;
1162
1163            let ret_val = {
1164                let cur = cur[schema_math.ret_val_col()];
1165                let new = new[schema_math.ret_val_col()];
1166                let out = resolved.run(state, cur, new, timestamp);
1167                changed |= cur != out;
1168                out
1169            };
1170
1171            let subsume = schema_math.subsume.then(|| {
1172                let cur = cur[schema_math.subsume_col()];
1173                let new = new[schema_math.subsume_col()];
1174                let out = combine_subsumed(cur, new);
1175                changed |= cur != out;
1176                out
1177            });
1178            if changed {
1179                out.extend_from_slice(new);
1180                schema_math.write_table_row(
1181                    out,
1182                    RowVals {
1183                        timestamp,
1184                        subsume,
1185                        ret_val: Some(ret_val),
1186                    },
1187                );
1188            }
1189
1190            changed
1191        })
1192    }
1193
1194    fn resolve(&self, function_name: &str, egraph: &mut EGraph) -> ResolvedMergeFn {
1195        match self {
1196            MergeFn::Const(v) => ResolvedMergeFn::Const(*v),
1197            MergeFn::Old => ResolvedMergeFn::Old,
1198            MergeFn::New => ResolvedMergeFn::New,
1199            MergeFn::AssertEq => ResolvedMergeFn::AssertEq {
1200                panic: egraph.new_panic(format!(
1201                    "Illegal merge attempted for function {function_name}"
1202                )),
1203            },
1204            MergeFn::UnionId => ResolvedMergeFn::UnionId {
1205                uf_table: egraph.uf_table,
1206            },
1207            // NB: The primitive and function-based merge functions heap allocate a single callback
1208            // for each layer of nesting. This introduces a bit of overhead, particularly for cases
1209            // that look like `(f old new)` or `(f new old)`. We could special-case common cases in
1210            // this function if that overhead shows up.
1211            MergeFn::Primitive(prim, args) => ResolvedMergeFn::Primitive {
1212                prim: *prim,
1213                args: args
1214                    .iter()
1215                    .map(|arg| arg.resolve(function_name, egraph))
1216                    .collect::<Vec<_>>(),
1217                panic: egraph.new_panic(format!(
1218                    "Merge function for {function_name} primitive call failed"
1219                )),
1220            },
1221            MergeFn::Function(func, args) => {
1222                let func_info = &egraph.funcs[*func];
1223                assert_eq!(
1224                    func_info.schema.len(),
1225                    args.len() + 1,
1226                    "Merge function for {function_name} must match function arity for {}",
1227                    func_info.name
1228                );
1229                ResolvedMergeFn::Function {
1230                    func: TableAction::new(egraph, *func),
1231                    panic: egraph.new_panic(format!(
1232                        "Lookup on {} failed in the merge function for {function_name}",
1233                        func_info.name
1234                    )),
1235                    args: args
1236                        .iter()
1237                        .map(|arg| arg.resolve(function_name, egraph))
1238                        .collect::<Vec<_>>(),
1239                }
1240            }
1241        }
1242    }
1243}
1244
1245/// This enum is taking the place of a
1246/// `Box<dyn Fn(&mut ExecutionState, Value, Value, Value) -> Value + Send + Sync>`
1247/// to avoid extra boxes. It stores the data needed to run a `MergeFn` without
1248/// holding onto any references, so it can be `move`d inside the `core_relations::MergeFn`.
1249enum ResolvedMergeFn {
1250    Const(Value),
1251    Old,
1252    New,
1253    AssertEq {
1254        panic: ExternalFunctionId,
1255    },
1256    UnionId {
1257        uf_table: TableId,
1258    },
1259    Primitive {
1260        prim: ExternalFunctionId,
1261        args: Vec<ResolvedMergeFn>,
1262        panic: ExternalFunctionId,
1263    },
1264    Function {
1265        func: TableAction,
1266        args: Vec<ResolvedMergeFn>,
1267        panic: ExternalFunctionId,
1268    },
1269}
1270
1271impl ResolvedMergeFn {
1272    fn run(&self, state: &mut ExecutionState, cur: Value, new: Value, ts: Value) -> Value {
1273        match self {
1274            ResolvedMergeFn::Const(v) => *v,
1275            ResolvedMergeFn::Old => cur,
1276            ResolvedMergeFn::New => new,
1277            ResolvedMergeFn::AssertEq { panic } => {
1278                if cur != new {
1279                    let res = state.call_external_func(*panic, &[]);
1280                    assert_eq!(res, None);
1281                }
1282                cur
1283            }
1284            ResolvedMergeFn::UnionId { uf_table } => {
1285                if cur != new {
1286                    state.stage_insert(*uf_table, &[cur, new, ts]);
1287                    // We pick the minimum when unioning. This matches the original egglog
1288                    // behavior. THIS MUST MATCH THE UNION-FIND IMPLEMENTATION!
1289                    std::cmp::min(cur, new)
1290                } else {
1291                    cur
1292                }
1293            }
1294            // NB: The primitive and function-based merge functions heap allocate a single callback
1295            // for each layer of nesting. This introduces a bit of overhead, particularly for cases
1296            // that look like `(f old new)` or `(f new old)`. We could special-case common cases in
1297            // this function if that overhead shows up.
1298            ResolvedMergeFn::Primitive { prim, args, panic } => {
1299                let args = args
1300                    .iter()
1301                    .map(|arg| arg.run(state, cur, new, ts))
1302                    .collect::<Vec<_>>();
1303
1304                match state.call_external_func(*prim, &args) {
1305                    Some(result) => result,
1306                    None => {
1307                        let res = state.call_external_func(*panic, &[]);
1308                        assert_eq!(res, None);
1309                        cur
1310                    }
1311                }
1312            }
1313            ResolvedMergeFn::Function { func, args, panic } => {
1314                // see github.com/egraphs-good/egglog/pull/287
1315                if cur == new {
1316                    return cur;
1317                }
1318
1319                let args = args
1320                    .iter()
1321                    .map(|arg| arg.run(state, cur, new, ts))
1322                    .collect::<Vec<_>>();
1323
1324                // Merge functions dispatch to another function that may be
1325                // a constructor (mint fresh id on miss) or a custom function
1326                // (return `None` → panic). `lookup_or_insert` preserves
1327                // both behaviors; the pure-read `lookup` would skip
1328                // constructor minting.
1329                func.lookup_or_insert(state, &args).unwrap_or_else(|| {
1330                    let res = state.call_external_func(*panic, &[]);
1331                    assert_eq!(res, None);
1332                    cur
1333                })
1334            }
1335        }
1336    }
1337}
1338
1339/// Coarse classification of a table — `Constructor` mints a fresh
1340/// eclass id when a row is missed; `Function` does not. Mirrors the
1341/// `FunctionSubtype` split on the egglog side without dragging that
1342/// type into the bridge crate.
1343#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1344pub enum TableKind {
1345    Function,
1346    Constructor,
1347}
1348
1349/// This is an intern-able struct that holds all the data needed
1350/// to do table operations with an [`ExecutionState`], assuming
1351/// that the [`FunctionId`] for the table is known ahead of time.
1352#[derive(Debug, PartialEq, Eq, Hash, Clone)]
1353pub struct TableAction {
1354    table: TableId,
1355    table_math: SchemaMath,
1356    default: Option<MergeVal>,
1357    timestamp: CounterId,
1358    kind: TableKind,
1359}
1360
1361impl TableAction {
1362    /// Create a new `TableAction` to be used later.
1363    /// This requires access to the `egglog_bridge::EGraph`.
1364    pub fn new(egraph: &EGraph, func: FunctionId) -> TableAction {
1365        let func_info = &egraph.funcs[func];
1366        let kind = match &func_info.default_val {
1367            DefaultVal::FreshId => TableKind::Constructor,
1368            DefaultVal::Fail | DefaultVal::Const(_) => TableKind::Function,
1369        };
1370        TableAction {
1371            table: func_info.table,
1372            table_math: SchemaMath {
1373                func_cols: func_info.schema.len(),
1374                subsume: func_info.can_subsume,
1375            },
1376            default: match &func_info.default_val {
1377                DefaultVal::FreshId => Some(MergeVal::Counter(egraph.id_counter)),
1378                DefaultVal::Fail => None,
1379                DefaultVal::Const(val) => Some(MergeVal::Constant(*val)),
1380            },
1381            timestamp: egraph.timestamp_counter,
1382            kind,
1383        }
1384    }
1385
1386    /// Whether this table is a `Function` (no auto-insert) or a
1387    /// `Constructor` (mints a fresh eclass id on miss).
1388    pub fn kind(&self) -> TableKind {
1389        self.kind
1390    }
1391
1392    /// Number of input columns (schema minus the trailing output
1393    /// column).
1394    pub fn input_arity(&self) -> usize {
1395        self.table_math.func_cols - 1
1396    }
1397
1398    /// Look up a row and return its return-value column, or `None` if the
1399    /// key is not present. **This is a pure read**: it never inserts a row,
1400    /// regardless of the table's configured [`DefaultVal`].
1401    ///
1402    /// For the lookup-or-insert behavior that mints fresh eclass IDs for
1403    /// constructors, use [`TableAction::lookup_or_insert`].
1404    pub fn lookup(&self, state: &ExecutionState, key: &[Value]) -> Option<Value> {
1405        state
1406            .get_table(self.table)
1407            .get_row(key)
1408            .map(|row| row.vals[self.table_math.ret_val_col()])
1409    }
1410
1411    /// Return the current number of rows in this table.
1412    pub fn row_count(&self, state: &ExecutionState) -> usize {
1413        state.get_table(self.table).len()
1414    }
1415
1416    /// Iterate this table's rows, calling `f` on each function row.
1417    /// Mirrors [`EGraph::for_each`] but reaches the table through an
1418    /// [`ExecutionState`] — so it's callable from primitive bodies via
1419    /// the typed `Read`-style API.
1420    pub fn for_each(&self, state: &ExecutionState, mut f: impl FnMut(ScanEntry<'_>)) {
1421        self.for_each_while(state, |row| {
1422            f(row);
1423            true
1424        });
1425    }
1426
1427    /// Like [`TableAction::for_each`], but stops as soon as `f`
1428    /// returns `false`.
1429    pub fn for_each_while(&self, state: &ExecutionState, mut f: impl FnMut(ScanEntry<'_>) -> bool) {
1430        let schema_math = self.table_math;
1431        let imp = state.get_table(self.table);
1432        let all = imp.all();
1433        let mut cur = Offset::new(0);
1434        let mut buf = TaggedRowBuffer::new(imp.spec().arity());
1435        macro_rules! drain_buf {
1436            ($buf:expr) => {
1437                for (_, row) in $buf.non_stale() {
1438                    let subsumed =
1439                        schema_math.subsume && row[schema_math.subsume_col()] == SUBSUMED;
1440                    if !f(ScanEntry {
1441                        vals: &row[0..schema_math.func_cols],
1442                        subsumed,
1443                    }) {
1444                        return;
1445                    }
1446                }
1447                $buf.clear();
1448            };
1449        }
1450        while let Some(next) = imp.scan_bounded(all.as_ref(), cur, 32, &mut buf) {
1451            drain_buf!(buf);
1452            cur = next;
1453        }
1454        drain_buf!(buf);
1455    }
1456
1457    /// Look up a row, inserting the configured default value if absent.
1458    /// For constructor tables this mints a fresh eclass ID; for custom
1459    /// functions (no default) this behaves identically to
1460    /// [`TableAction::lookup`].
1461    ///
1462    /// This is a write operation — only safe in action contexts. See
1463    /// issue #772.
1464    pub fn lookup_or_insert(&self, state: &mut ExecutionState, key: &[Value]) -> Option<Value> {
1465        match self.default {
1466            Some(default) => {
1467                let timestamp =
1468                    MergeVal::Constant(Value::from_usize(state.read_counter(self.timestamp)));
1469                let mut merge_vals = SmallVec::<[MergeVal; 3]>::new();
1470                SchemaMath {
1471                    func_cols: 1,
1472                    ..self.table_math
1473                }
1474                .write_table_row(
1475                    &mut merge_vals,
1476                    RowVals {
1477                        timestamp,
1478                        subsume: self
1479                            .table_math
1480                            .subsume
1481                            .then_some(MergeVal::Constant(NOT_SUBSUMED)),
1482                        ret_val: Some(default),
1483                    },
1484                );
1485                Some(
1486                    state.predict_val(self.table, key, merge_vals.iter().copied())
1487                        [self.table_math.ret_val_col()],
1488                )
1489            }
1490            None => self.lookup(state, key),
1491        }
1492    }
1493
1494    /// Insert a row into this table.
1495    pub fn insert(&self, state: &mut ExecutionState, row: impl Iterator<Item = Value>) {
1496        let ts = Value::from_usize(state.read_counter(self.timestamp));
1497        let mut scratch = row.collect::<SmallVec<[_; 8]>>();
1498        self.table_math.write_table_row(
1499            &mut scratch,
1500            RowVals {
1501                timestamp: ts,
1502                subsume: self.table_math.subsume.then_some(NOT_SUBSUMED),
1503                ret_val: None,
1504            },
1505        );
1506        state.stage_insert(self.table, &scratch);
1507    }
1508
1509    /// Delete a row from this table.
1510    pub fn remove(&self, state: &mut ExecutionState, key: &[Value]) {
1511        state.stage_remove(self.table, key);
1512    }
1513
1514    /// Subsume a row in this table.
1515    pub fn subsume(&self, state: &mut ExecutionState, key: impl Iterator<Item = Value>) {
1516        let ts = Value::from_usize(state.read_counter(self.timestamp));
1517        let mut scratch = key.collect::<SmallVec<[_; 8]>>();
1518
1519        let ret_val = self.lookup(state, &scratch).expect("subsume lookup failed");
1520
1521        self.table_math.write_table_row(
1522            &mut scratch,
1523            RowVals {
1524                timestamp: ts,
1525                subsume: Some(SUBSUMED),
1526                ret_val: Some(ret_val),
1527            },
1528        );
1529        state.stage_insert(self.table, &scratch);
1530    }
1531}
1532
1533/// A variant of `TableAction` for the union-find.
1534#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1535pub struct UnionAction {
1536    table: TableId,
1537    timestamp: CounterId,
1538}
1539
1540impl UnionAction {
1541    /// Create a new `UnionAction` to be used later.
1542    /// This requires access to the `egglog_bridge::EGraph`.
1543    pub fn new(egraph: &EGraph) -> UnionAction {
1544        UnionAction {
1545            table: egraph.uf_table,
1546            timestamp: egraph.timestamp_counter,
1547        }
1548    }
1549
1550    /// Union two values.
1551    pub fn union(&self, state: &mut ExecutionState, x: Value, y: Value) {
1552        let ts = Value::from_usize(state.read_counter(self.timestamp));
1553        state.stage_insert(self.table, &[x, y, ts]);
1554    }
1555}
1556
1557fn run_rules_impl(
1558    db: &mut Database,
1559    rule_info: &mut DenseIdMapWithReuse<RuleId, RuleInfo>,
1560    rules: &[RuleId],
1561    next_ts: Timestamp,
1562    report_level: ReportLevel,
1563) -> Result<RuleSetReport> {
1564    for rule in rules {
1565        let info = &mut rule_info[*rule];
1566        if info.cached_plan.is_none() {
1567            info.cached_plan = Some(info.query.build_cached_plan(db, &info.desc)?);
1568        }
1569    }
1570    let mut rsb = db.new_rule_set();
1571    for rule in rules {
1572        let info = &mut rule_info[*rule];
1573        let cached_plan = info.cached_plan.as_ref().unwrap();
1574        info.query
1575            .add_rules_from_cached(&mut rsb, info.last_run_at, cached_plan);
1576        info.last_run_at = next_ts;
1577    }
1578    let ruleset = rsb.build();
1579    Ok(db.run_rule_set(&ruleset, report_level))
1580}
1581
1582// These markers are just used to make it easy to distinguish time spent in
1583// incremental vs. nonincremental rebuilds in time-based profiles.
1584
1585#[inline(never)]
1586fn marker_incremental_rebuild<R>(f: impl FnOnce() -> R) -> R {
1587    f()
1588}
1589
1590#[inline(never)]
1591fn marker_nonincremental_rebuild<R>(f: impl FnOnce() -> R) -> R {
1592    f()
1593}
1594
1595/// A useful type definition for external functions that need to pass data
1596/// to outside code, such as `Panic`.
1597pub type SideChannel<T> = Arc<Mutex<Option<T>>>;
1598
1599/// An external function used to grab a value out of the database matching a
1600/// particular query.
1601//
1602// TODO: once we have parallelism wired in, we'll want to replace this with a
1603// more efficient solution (e.g. one based on crossbeam or arcswap).
1604/// This is a variant on [`Panic`] that avoids eager construction of the panic message.
1605///
1606/// The main thing this is used for is to avoid constructing the panic message ahead of time during
1607/// a call to [`RuleBuilder::call_external_func`]; these panic messages are often quite rare and
1608/// may never need to be constructed at all. Furthermore, a closure to produce the panic message in
1609/// most cases need only close over a few cheap-to-clone values.
1610///
1611/// The downside of this, and why we do not use it everywhere, is that there's no natural "key"
1612/// that we can use to cache duplicate panic messages. We would need a more complex API to support
1613/// both and fully replace our use of `Panic`.
1614struct LazyPanic<F>(Arc<Lazy<String, F>>, SideChannel<String>);
1615
1616impl<F: FnOnce() -> String + Send> ExternalFunction for LazyPanic<F> {
1617    fn invoke(&self, state: &mut core_relations::ExecutionState, args: &[Value]) -> Option<Value> {
1618        assert!(args.is_empty());
1619        state.trigger_early_stop();
1620        let mut guard = self.1.lock().unwrap();
1621        if guard.is_none() {
1622            *guard = Some(Lazy::force(&self.0).clone());
1623        }
1624        None
1625    }
1626}
1627
1628impl<F> Clone for LazyPanic<F> {
1629    fn clone(&self) -> Self {
1630        LazyPanic(self.0.clone(), self.1.clone())
1631    }
1632}
1633
1634/// An external function used to store a message when a panic occurs.
1635//
1636// TODO: once we have parallelism wired in, we'll want to replace this with a
1637// more efficient solution (e.g. one based on crossbeam or arcswap).
1638#[derive(Clone)]
1639struct Panic(String, SideChannel<String>);
1640
1641impl EGraph {
1642    /// Create a new `ExternalFunction` that panics with the given message.
1643    pub fn new_panic(&mut self, message: String) -> ExternalFunctionId {
1644        *self
1645            .panic_funcs
1646            .entry(message.to_string())
1647            .or_insert_with(|| {
1648                let panic = Panic(message, self.panic_message.clone());
1649                self.db.add_external_function(Box::new(panic))
1650            })
1651    }
1652
1653    pub fn new_panic_lazy(
1654        &mut self,
1655        message: impl FnOnce() -> String + Send + 'static,
1656    ) -> ExternalFunctionId {
1657        let lazy = Lazy::new(message);
1658        let panic = LazyPanic(Arc::new(lazy), self.panic_message.clone());
1659        self.db.add_external_function(Box::new(panic))
1660    }
1661}
1662
1663impl ExternalFunction for Panic {
1664    fn invoke(&self, state: &mut core_relations::ExecutionState, args: &[Value]) -> Option<Value> {
1665        // TODO (egglog feature): change this to support interpolating panic messages
1666        assert!(args.is_empty());
1667
1668        state.trigger_early_stop();
1669        let mut guard = self.1.lock().unwrap();
1670        if guard.is_none() {
1671            *guard = Some(self.0.clone());
1672        }
1673        None
1674    }
1675}
1676
1677/// Heuristic for deciding whether to do an incremental or nonincremental
1678/// rebuild for a given table.
1679fn incremental_rebuild(uf_size: usize, table_size: usize, parallel: bool) -> bool {
1680    if parallel {
1681        uf_size <= (table_size / 16)
1682    } else {
1683        uf_size <= (table_size / 8)
1684    }
1685}
1686
1687pub(crate) const SUBSUMED: Value = Value::new_const(1);
1688pub(crate) const NOT_SUBSUMED: Value = Value::new_const(0);
1689fn combine_subsumed(v1: Value, v2: Value) -> Value {
1690    std::cmp::max(v1, v2)
1691}
1692
1693/// A struct helping with some calculations of where some information is stored at the
1694/// core-relations Table level for a given function.
1695///
1696/// Functions can have multiple "output columns" in the underlying core-relations layer depending
1697/// on whether different features are enabled. Roughly, tables are laid out as:
1698///
1699/// > `[key0, ..., keyn, return value, timestamp, subsume?]`
1700///
1701/// Where there are `n+1` key columns and columns marked with a question mark are optional,
1702/// depending on the egraph and table-level configuration.
1703#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1704struct SchemaMath {
1705    /// Whether or not the table is enabled for subsumption.
1706    subsume: bool,
1707    /// The number of columns in the function (including the return value).
1708    func_cols: usize,
1709}
1710
1711/// A struct containing possible non-key portions of a table row. To be used with
1712/// [`SchemaMath::write_table_row`].
1713///
1714/// This is the write side (building a row); [`ScanEntry`] is the
1715/// read side (a row yielded by a table scan).
1716struct RowVals<T> {
1717    /// The timestamp for the row.
1718    timestamp: T,
1719    /// The subsumption tag for the row. Only relevant if the table has subsumption enabled.
1720    subsume: Option<T>,
1721    /// The return value of the row. Return values are mandatory but callers may have already
1722    /// filled it in.
1723    ret_val: Option<T>,
1724}
1725
1726/// A raw row yielded by a table scan; `vals` includes the trailing
1727/// output/eclass column.
1728///
1729/// Public so the `egglog` crate can consume it, but **do not re-export
1730/// it from `egglog`'s public API** — the user-facing row types are
1731/// `egglog::FunctionEntry` and `egglog::Enode`.
1732#[derive(Clone, Debug)]
1733pub struct ScanEntry<'a> {
1734    pub vals: &'a [Value],
1735    pub subsumed: bool,
1736}
1737
1738impl SchemaMath {
1739    fn write_table_row<T: Clone>(
1740        &self,
1741        row: &mut impl HasResizeWith<T>,
1742        RowVals {
1743            timestamp,
1744            subsume,
1745            ret_val,
1746        }: RowVals<T>,
1747    ) {
1748        row.resize_with(self.table_columns(), || timestamp.clone());
1749        row[self.ts_col()] = timestamp;
1750        if let Some(ret_val) = ret_val {
1751            row[self.ret_val_col()] = ret_val;
1752        }
1753        if let Some(subsume) = subsume {
1754            row[self.subsume_col()] = subsume;
1755        } else {
1756            assert!(
1757                !self.subsume,
1758                "subsume flag must be provided if subsumption is enabled"
1759            );
1760        }
1761    }
1762
1763    fn num_keys(&self) -> usize {
1764        self.func_cols - 1
1765    }
1766
1767    fn table_columns(&self) -> usize {
1768        self.func_cols + 1 /* timestamp */ + if self.subsume { 1 } else { 0 }
1769    }
1770
1771    fn ret_val_col(&self) -> usize {
1772        self.func_cols - 1
1773    }
1774
1775    fn ts_col(&self) -> usize {
1776        self.func_cols
1777    }
1778
1779    #[track_caller]
1780    fn subsume_col(&self) -> usize {
1781        assert!(self.subsume);
1782        self.func_cols + 1
1783    }
1784}
1785
1786#[derive(Error, Debug)]
1787#[error("Panic: {0}")]
1788struct PanicError(String);
1789
1790/// Basic ad-hoc polymorphism around `resize_with` in order to get [`SchemaMath::write_table_row`]
1791/// to work with both `Vec` and `SmallVec`.
1792trait HasResizeWith<T>:
1793    AsMut<[T]> + AsRef<[T]> + Index<usize, Output = T> + IndexMut<usize, Output = T>
1794{
1795    fn resize_with<F>(&mut self, new_size: usize, f: F)
1796    where
1797        F: FnMut() -> T;
1798}
1799
1800impl<T> HasResizeWith<T> for Vec<T> {
1801    fn resize_with<F>(&mut self, new_size: usize, f: F)
1802    where
1803        F: FnMut() -> T,
1804    {
1805        self.resize_with(new_size, f);
1806    }
1807}
1808
1809impl<T, A: smallvec::Array<Item = T>> HasResizeWith<T> for SmallVec<A> {
1810    fn resize_with<F>(&mut self, new_size: usize, f: F)
1811    where
1812        F: FnMut() -> T,
1813    {
1814        self.resize_with(new_size, f);
1815    }
1816}