egglog/
extract.rs

1//! Cost models used by extraction.
2//!
3//! [`Cost`] is the minimum requirement for ranking extracted terms.
4//! [`TreeCostModel`] computes a node annotation and then folds it with selected
5//! child costs. The annotation can carry context that should not be part of the
6//! final cost, so tree extraction does not require costs to support addition.
7//!
8//! [`MonoidCost`] adds the associative and commutative combination operation
9//! needed to charge shared dependencies once. [`DagCostModel`] computes the
10//! marginal costs consumed by DAG extractors. [`TreeCostModelFromDag`] adapts
11//! such a model to tree extraction by combining each node's marginal cost with
12//! its selected child costs. [`DEFAULT_COST_MODEL`] is the default additive
13//! tree model.
14
15use crate::termdag::{TermDag, TermId};
16use crate::util::{HashMap, HashSet};
17use crate::*;
18use std::collections::VecDeque;
19
20/// A value that can be used to rank extraction candidates.
21pub trait Cost: Clone + Ord {}
22
23impl<T: Clone + Ord> Cost for T {}
24
25/// An extraction cost with an identity and a combination operation.
26///
27/// Implementations must make `identity` a two-sided identity and `combine`
28/// associative, commutative, deterministic, non-panicking, and monotone with
29/// respect to [`Cost`]'s ordering. Rust cannot enforce these laws.
30pub trait MonoidCost: Cost {
31    /// The identity cost, usually zero.
32    fn identity() -> Self;
33
34    /// Combines two costs without overflowing or panicking.
35    fn combine(self, other: &Self) -> Self;
36}
37
38macro_rules! monoid_unsigned_cost {
39    ($($cost:ty),* $(,)?) => {$(
40        impl MonoidCost for $cost {
41            fn identity() -> Self {
42                0
43            }
44
45            fn combine(self, other: &Self) -> Self {
46                self.saturating_add(*other)
47            }
48        }
49    )*};
50}
51
52monoid_unsigned_cost!(u8, u16, u32, u64, u128, usize);
53
54macro_rules! monoid_exact_cost {
55    ($($cost:ty),* $(,)?) => {$(
56        impl MonoidCost for $cost {
57            fn identity() -> Self {
58                use num::Zero;
59                Self::zero()
60            }
61
62            fn combine(self, other: &Self) -> Self {
63                self + other
64            }
65        }
66    )*};
67}
68
69monoid_exact_cost!(num::BigInt, num::BigRational);
70
71/// Computes marginal costs for extraction that charges shared dependencies once.
72///
73/// The returned costs exclude selected child and container-element costs.
74/// Implementations must return equal costs for equivalent arguments during one
75/// extraction.
76pub trait DagCostModel<C: MonoidCost> {
77    /// Computes the cost of a non-container primitive value.
78    fn base_value_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C;
79    /// Computes an enode's cost, excluding its children.
80    fn enode_cost(&self, egraph: &EGraph, func: &Function, enode: &Enode<'_>) -> C;
81
82    /// Computes a container's cost, excluding its elements.
83    fn container_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C {
84        let _ = (egraph, sort, value);
85        C::identity()
86    }
87}
88
89/// Computes tree-extraction costs from node annotations and selected child costs.
90///
91/// Models used by [`TreeExtractor`] must be deterministic and satisfy optimal
92/// substructure: replacing a child with a lower-cost extraction must not make
93/// its parent more expensive. They must also avoid making cyclic terms improve
94/// indefinitely. Otherwise extraction may be non-optimal, fail to converge, or
95/// fail during reconstruction. The annotation types let the first stage retain
96/// context, such as the constructor kind, without encoding it in `C`.
97pub trait TreeCostModel<C: Cost> {
98    /// Context retained between [`TreeCostModel::enode_cost`] and its fold.
99    type EnodeCost;
100    /// Context retained between [`TreeCostModel::container_cost`] and its fold.
101    type ContainerCost;
102
103    /// Computes the cost of a non-container primitive value.
104    fn base_value_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C;
105
106    /// Computes the annotation folded with an enode's selected child costs.
107    fn enode_cost(&self, egraph: &EGraph, func: &Function, enode: &Enode<'_>) -> Self::EnodeCost;
108
109    /// Computes the annotation folded with a container's selected element costs.
110    fn container_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> Self::ContainerCost;
111
112    /// Produces an enode's total cost from its annotation and child costs.
113    fn fold_enode_cost(&self, enode_cost: Self::EnodeCost, child_costs: &[C]) -> C;
114
115    /// Produces a container's total cost from its annotation and element costs.
116    fn fold_container_cost(&self, container_cost: Self::ContainerCost, element_costs: &[C]) -> C;
117}
118
119trait TreeExtractorCostModel<C: Cost> {
120    fn base_value_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C;
121    fn total_enode_cost(
122        &self,
123        egraph: &EGraph,
124        func: &Function,
125        enode: &Enode<'_>,
126        child_costs: &[C],
127    ) -> C;
128    fn total_container_cost(
129        &self,
130        egraph: &EGraph,
131        sort: &ArcSort,
132        value: Value,
133        element_costs: &[C],
134    ) -> C;
135}
136
137impl<C: Cost, M: TreeCostModel<C>> TreeExtractorCostModel<C> for M {
138    fn base_value_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C {
139        TreeCostModel::base_value_cost(self, egraph, sort, value)
140    }
141
142    fn total_enode_cost(
143        &self,
144        egraph: &EGraph,
145        func: &Function,
146        enode: &Enode<'_>,
147        child_costs: &[C],
148    ) -> C {
149        self.fold_enode_cost(self.enode_cost(egraph, func, enode), child_costs)
150    }
151
152    fn total_container_cost(
153        &self,
154        egraph: &EGraph,
155        sort: &ArcSort,
156        value: Value,
157        element_costs: &[C],
158    ) -> C {
159        self.fold_container_cost(self.container_cost(egraph, sort, value), element_costs)
160    }
161}
162
163/// Adapts a [`DagCostModel`] to tree extraction by combining marginal costs.
164#[derive(Clone, Debug)]
165pub struct TreeCostModelFromDag<M>(pub M);
166
167impl<C: MonoidCost, M: DagCostModel<C>> TreeCostModel<C> for TreeCostModelFromDag<M> {
168    type EnodeCost = C;
169    type ContainerCost = C;
170
171    fn base_value_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C {
172        self.0.base_value_cost(egraph, sort, value)
173    }
174
175    fn enode_cost(&self, egraph: &EGraph, func: &Function, enode: &Enode<'_>) -> C {
176        self.0.enode_cost(egraph, func, enode)
177    }
178
179    fn container_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C {
180        self.0.container_cost(egraph, sort, value)
181    }
182
183    fn fold_enode_cost(&self, enode_cost: C, child_costs: &[C]) -> C {
184        child_costs
185            .iter()
186            .fold(enode_cost, |cost, child| cost.combine(child))
187    }
188
189    fn fold_container_cost(&self, container_cost: C, element_costs: &[C]) -> C {
190        element_costs
191            .iter()
192            .fold(container_cost, |cost, element| cost.combine(element))
193    }
194}
195
196/// The default extraction cost type.
197pub type DefaultCost = u64;
198
199/// The marginal-cost model underlying default tree and DAG extraction.
200///
201/// With [`DefaultCost`], constructor `:cost` declarations override `node_cost`
202/// and selected child costs are combined with each constructor's cost.
203#[derive(Clone, Debug)]
204pub struct AdditiveCostModel {
205    /// The fallback cost for primitive values and constructors without `:cost`.
206    pub node_cost: DefaultCost,
207}
208
209impl Default for AdditiveCostModel {
210    fn default() -> Self {
211        Self { node_cost: 1 }
212    }
213}
214
215/// The default additive model used by tree extraction.
216pub const DEFAULT_COST_MODEL: TreeCostModelFromDag<AdditiveCostModel> =
217    TreeCostModelFromDag(AdditiveCostModel { node_cost: 1 });
218
219impl DagCostModel<DefaultCost> for AdditiveCostModel {
220    fn base_value_cost(&self, _egraph: &EGraph, _sort: &ArcSort, _value: Value) -> DefaultCost {
221        self.node_cost
222    }
223
224    fn enode_cost(&self, egraph: &EGraph, func: &Function, _enode: &Enode<'_>) -> DefaultCost {
225        func.extraction_head_cost(egraph).unwrap_or(self.node_cost)
226    }
227}
228
229/// One extracted root or root variant.
230///
231/// The cost is the objective value assigned by the selected extractor. Tree
232/// extraction reports tree cost; DAG extractors may instead report a per-root
233/// or per-variant DAG cost. `term` indexes the enclosing result's shared
234/// [`TermDag`].
235#[derive(Clone, Debug)]
236pub struct ExtractedTerm<C> {
237    /// The selected extractor's cost for this result.
238    pub cost: C,
239    /// The extracted root in the enclosing result's [`TermDag`].
240    pub term: TermId,
241}
242
243/// Best-extraction results for a batch of requested roots.
244///
245/// All returned term ids index the shared [`ExtractedTerms::termdag`].
246#[derive(Clone, Debug)]
247pub struct ExtractedTerms<C> {
248    /// Shared term storage for every extracted root.
249    pub termdag: TermDag,
250    /// One extraction result per requested root, in request order.
251    ///
252    /// `None` means that root is unextractable with the selected cost model and
253    /// available constructors.
254    pub terms: Vec<Option<ExtractedTerm<C>>>,
255}
256
257/// Root-variant extraction results for a batch of requested roots.
258///
259/// All returned term ids index the shared [`ExtractedTermVariants::termdag`].
260#[derive(Clone, Debug)]
261pub struct ExtractedTermVariants<C> {
262    /// Shared term storage for every extracted variant.
263    pub termdag: TermDag,
264    /// Outer vector follows requested-root order; each inner vector contains that root's variants.
265    pub variants: Vec<Vec<ExtractedTerm<C>>>,
266}
267
268/// Bellman-Ford-like tree extraction with reusable cost preparation.
269///
270/// The prepared state borrows the e-graph because reconstruction still needs
271/// its sort storage and constructor metadata. This prevents prepared costs from
272/// being used after the e-graph is mutated.
273pub struct TreeExtractor<'g, C: Cost> {
274    egraph: &'g EGraph,
275    funcs: Vec<String>,
276    cost_model: Box<dyn TreeExtractorCostModel<C> + 'g>,
277    costs: HashMap<String, HashMap<Value, C>>,
278    topo_rnk_cnt: usize,
279    topo_rnk: HashMap<String, HashMap<Value, usize>>,
280    parent_edge: HashMap<String, HashMap<Value, (String, Vec<Value>)>>,
281}
282
283impl<'g, C: Cost> TreeExtractor<'g, C> {
284    /// Prepares extraction costs for constructors reachable from `rootsorts`.
285    ///
286    /// Pass `None` to prepare every extractable root sort. Later calls to
287    /// [`TreeExtractor::extract_best_with_sort`] and
288    /// [`TreeExtractor::extract_variants_with_sort`] reuse the prepared best
289    /// costs and producer choices.
290    ///
291    /// Primitive and container roots are costed when extracted. Variant
292    /// extraction also uses the model to rescore candidate root enodes, so the
293    /// model must return stable results for the extractor's lifetime.
294    pub fn compute_costs_from_rootsorts(
295        rootsorts: Option<Vec<ArcSort>>,
296        egraph: &'g EGraph,
297        cost_model: impl TreeCostModel<C> + 'g,
298    ) -> Self {
299        // We filter out tables unreachable from the root sorts
300        let extract_all_sorts = rootsorts.is_none();
301
302        let mut rootsorts = rootsorts.unwrap_or_default();
303
304        // Built a reverse index from output sort to function head symbols
305        // Only include constructors (not regular functions), and respect the user-facing
306        // hidden and unextractable flags.
307        let mut rev_index: HashMap<String, Vec<String>> = Default::default();
308        for func in egraph.functions.iter() {
309            let unextractable = func.1.decl.unextractable;
310            let hidden = func.1.decl.internal_hidden;
311
312            // Only extract constructors and view tables, which reconstruct as their
313            // term_constructor. Proof extraction uses its own root-directed extractor
314            // and does not need alternate behavior here.
315            if !unextractable
316                && !hidden
317                && (func.1.decl.subtype == FunctionSubtype::Constructor
318                    || func.1.decl.term_constructor.is_some())
319            {
320                let func_name = func.0.clone();
321                // For view tables (with term_constructor in proof mode), the e-class is the last input column
322                let output_sort_name = func.1.extraction_output_sort().name();
323                if let Some(v) = rev_index.get_mut(output_sort_name) {
324                    v.push(func_name);
325                } else {
326                    rev_index.insert(output_sort_name.to_owned(), vec![func_name]);
327                    if extract_all_sorts {
328                        rootsorts.push(func.1.extraction_output_sort().clone());
329                    }
330                }
331            }
332        }
333
334        // Do a BFS to find reachable tables
335        let mut q: VecDeque<ArcSort> = VecDeque::new();
336        let mut seen: HashSet<String> = Default::default();
337        for rootsort in rootsorts.iter() {
338            if seen.insert(rootsort.name().to_owned()) {
339                q.push_back(rootsort.clone());
340            }
341        }
342
343        let mut funcs_set: HashSet<String> = Default::default();
344        let mut funcs: Vec<String> = Vec::new();
345        while !q.is_empty() {
346            let sort = q.pop_front().unwrap();
347            if sort.is_container_sort() {
348                let inner_sorts = sort.inner_sorts();
349                for s in inner_sorts {
350                    if !seen.contains(s.name()) {
351                        q.push_back(s.clone());
352                        seen.insert(s.name().to_owned());
353                    }
354                }
355            } else if sort.is_eq_sort()
356                && let Some(head_symbols) = rev_index.get(sort.name())
357            {
358                for h in head_symbols {
359                    if !funcs_set.contains(h) {
360                        let func = egraph.functions.get(h).unwrap();
361                        // For view tables, children are all but the last input (which is the e-class)
362                        let num_children = func.extraction_num_children();
363                        for ch in func.func_type.input.iter().take(num_children) {
364                            let ch_name = ch.name();
365                            if !seen.contains(ch_name) {
366                                q.push_back(ch.clone());
367                                seen.insert(ch_name.to_owned());
368                            }
369                        }
370                        funcs_set.insert(h.clone());
371                        funcs.push(h.clone());
372                    }
373                }
374            }
375        }
376
377        // Initialize the tables to have the reachable entries
378        let mut costs: HashMap<String, HashMap<Value, C>> = Default::default();
379        let mut topo_rnk: HashMap<String, HashMap<Value, usize>> = Default::default();
380        let mut parent_edge: HashMap<String, HashMap<Value, (String, Vec<Value>)>> =
381            Default::default();
382
383        for func_name in funcs.iter() {
384            let func = egraph.functions.get(func_name).unwrap();
385            let output_sort_name = func.extraction_output_sort().name();
386            if !costs.contains_key(output_sort_name) {
387                costs.insert(output_sort_name.to_owned(), Default::default());
388                topo_rnk.insert(output_sort_name.to_owned(), Default::default());
389                parent_edge.insert(output_sort_name.to_owned(), Default::default());
390            }
391        }
392
393        let mut extractor = TreeExtractor {
394            egraph,
395            funcs,
396            cost_model: Box::new(cost_model),
397            costs,
398            topo_rnk_cnt: 0,
399            topo_rnk,
400            parent_edge,
401        };
402
403        extractor.bellman_ford(egraph);
404
405        extractor
406    }
407
408    /// Compute the cost of a single enode
409    /// Recurse if container
410    /// Returns None if contains an undefined eqsort term (potentially after unfolding)
411    fn compute_cost_node(&self, egraph: &EGraph, value: Value, sort: &ArcSort) -> Option<C> {
412        if sort.is_container_sort() {
413            let elements = sort.inner_values(egraph.backend.container_values(), value);
414            let mut ch_costs: Vec<C> = Vec::new();
415            for ch in elements.iter() {
416                ch_costs.push(self.compute_cost_node(egraph, ch.1, &ch.0)?);
417            }
418            Some(
419                self.cost_model
420                    .total_container_cost(egraph, sort, value, &ch_costs),
421            )
422        } else if sort.is_eq_sort() {
423            self.costs.get(sort.name())?.get(&value).cloned()
424        } else {
425            // Primitive
426            Some(self.cost_model.base_value_cost(egraph, sort, value))
427        }
428    }
429
430    /// A row in a constructor table is a hyperedge from the set of input terms to the constructed output term.
431    fn compute_cost_hyperedge(
432        &self,
433        egraph: &EGraph,
434        row: &egglog_bridge::ScanEntry,
435        func: &Function,
436    ) -> Option<C> {
437        let mut ch_costs: Vec<C> = Vec::new();
438        let sorts = &func.func_type.input;
439        let num_children = func.extraction_num_children();
440        for (value, sort) in row.vals.iter().take(num_children).zip(sorts.iter()) {
441            ch_costs.push(self.compute_cost_node(egraph, *value, sort)?);
442        }
443        let head_name = func.extraction_term_name();
444        let output_idx = func.extraction_output_index();
445        let enode = Enode {
446            name: head_name,
447            children: &row.vals[..output_idx],
448            eclass: row.vals[output_idx],
449            subsumed: row.subsumed,
450        };
451        let cost_func = func
452            .decl
453            .term_constructor
454            .as_ref()
455            .and_then(|name| egraph.functions.get(name))
456            .unwrap_or(func);
457        Some(
458            self.cost_model
459                .total_enode_cost(egraph, cost_func, &enode, &ch_costs),
460        )
461    }
462
463    fn compute_topo_rnk_node(&self, egraph: &EGraph, value: Value, sort: &ArcSort) -> usize {
464        if sort.is_container_sort() {
465            sort.inner_values(egraph.backend.container_values(), value)
466                .iter()
467                .fold(0, |ret, (sort, value)| {
468                    usize::max(ret, self.compute_topo_rnk_node(egraph, *value, sort))
469                })
470        } else if sort.is_eq_sort() {
471            if let Some(t) = self.topo_rnk.get(sort.name()) {
472                *t.get(&value).unwrap_or(&usize::MAX)
473            } else {
474                usize::MAX
475            }
476        } else {
477            0
478        }
479    }
480
481    fn compute_topo_rnk_hyperedge(
482        &self,
483        egraph: &EGraph,
484        row: &egglog_bridge::ScanEntry,
485        func: &Function,
486    ) -> usize {
487        let sorts = &func.func_type.input;
488        let num_children = func.extraction_num_children();
489        row.vals
490            .iter()
491            .take(num_children)
492            .zip(sorts.iter())
493            .fold(0, |ret, (value, sort)| {
494                usize::max(ret, self.compute_topo_rnk_node(egraph, *value, sort))
495            })
496    }
497
498    /// We use Bellman-Ford to compute the costs of the relevant eq sorts' terms
499    /// [Bellman-Ford](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) is a shortest path algorithm.
500    /// The version implemented here computes the shortest path from any node in a set of sources to all the reachable nodes.
501    /// Computing the minimum cost for terms is treated as a shortest path problem on a hypergraph here.
502    /// In this hypergraph, the nodes corresponde to eclasses, the distances are the costs to extract a term of those eclasses,
503    /// and each enode is a hyperedge that goes from the set of children eclasses to the enode's eclass.
504    /// The sources are the eclasses with known costs from the cost model.
505    /// Additionally, to avoid cycles in the extraction even when the cost model can assign an equal cost to a term and its subterm.
506    /// It computes a topological rank for each eclass
507    /// and only allows each eclass to have children of classes of strictly smaller ranks in the extraction.
508    fn bellman_ford(&mut self, egraph: &EGraph) {
509        let mut ensure_fixpoint = false;
510
511        let funcs = self.funcs.clone();
512
513        while !ensure_fixpoint {
514            ensure_fixpoint = true;
515
516            for func_name in funcs.iter() {
517                let func = egraph.functions.get(func_name).unwrap();
518                let target_sort = func.extraction_output_sort();
519
520                let output_idx = func.extraction_output_index();
521                let relax_hyperedge = |row: egglog_bridge::ScanEntry| {
522                    if !row.subsumed {
523                        let target = &row.vals[output_idx];
524                        let mut updated = false;
525                        if let Some(new_cost) = self.compute_cost_hyperedge(egraph, &row, func) {
526                            match self
527                                .costs
528                                .get_mut(target_sort.name())
529                                .unwrap()
530                                .entry(*target)
531                            {
532                                HEntry::Vacant(e) => {
533                                    updated = true;
534                                    e.insert(new_cost);
535                                }
536                                HEntry::Occupied(mut e) => {
537                                    if new_cost < *(e.get()) {
538                                        updated = true;
539                                        e.insert(new_cost);
540                                    }
541                                }
542                            }
543                        }
544                        // record the chronological order of the updates
545                        // which serves as a topological order that avoids cycles
546                        // even when a term has a cost equal to its subterms
547                        if updated {
548                            ensure_fixpoint = false;
549                            self.topo_rnk_cnt += 1;
550                            self.topo_rnk
551                                .get_mut(target_sort.name())
552                                .unwrap()
553                                .insert(*target, self.topo_rnk_cnt);
554                        }
555                    }
556                };
557
558                egraph.backend.for_each(func.backend_id, relax_hyperedge);
559            }
560        }
561
562        // Save the edges for reconstruction
563        for func_name in funcs.iter() {
564            let func = egraph.functions.get(func_name).unwrap();
565            let target_sort = func.extraction_output_sort();
566            let output_idx = func.extraction_output_index();
567
568            let save_best_parent_edge = |row: egglog_bridge::ScanEntry| {
569                if !row.subsumed {
570                    let target = &row.vals[output_idx];
571                    if let Some(best_cost) = self.costs.get(target_sort.name()).unwrap().get(target)
572                        && Some(best_cost.clone())
573                            == self.compute_cost_hyperedge(egraph, &row, func)
574                    {
575                        // one of the possible best parent edges
576                        let target_topo_rnk = *self
577                            .topo_rnk
578                            .get(target_sort.name())
579                            .unwrap()
580                            .get(target)
581                            .unwrap();
582                        if target_topo_rnk > self.compute_topo_rnk_hyperedge(egraph, &row, func) {
583                            // one of the parent edges that avoids cycles
584                            if let HEntry::Vacant(e) = self
585                                .parent_edge
586                                .get_mut(target_sort.name())
587                                .unwrap()
588                                .entry(*target)
589                            {
590                                e.insert((func.decl.name.clone(), row.vals.to_vec()));
591                            }
592                        }
593                    }
594                }
595            };
596
597            egraph
598                .backend
599                .for_each(func.backend_id, save_best_parent_edge);
600        }
601    }
602
603    /// This recursively reconstruct the termdag that gives the minimum cost for eclass value.
604    fn reconstruct_termdag_node_helper(
605        &self,
606        egraph: &EGraph,
607        termdag: &mut TermDag,
608        value: Value,
609        sort: &ArcSort,
610        cache: &mut HashMap<(Value, String), TermId>,
611    ) -> TermId {
612        let key = (value, sort.name().to_owned());
613        if let Some(term) = cache.get(&key) {
614            return *term;
615        }
616
617        let term = if sort.is_container_sort() {
618            let elements = sort.inner_values(egraph.backend.container_values(), value);
619            let mut ch_terms: Vec<TermId> = Vec::new();
620            for ch in elements.iter() {
621                ch_terms.push(
622                    self.reconstruct_termdag_node_helper(egraph, termdag, ch.1, &ch.0, cache),
623                );
624            }
625            sort.reconstruct_termdag_container(
626                egraph.backend.container_values(),
627                value,
628                termdag,
629                ch_terms,
630            )
631        } else if sort.is_eq_sort() {
632            let (func_name, hyperedge) = self
633                .parent_edge
634                .get(sort.name())
635                .unwrap()
636                .get(&value)
637                .unwrap();
638            let func = egraph.functions.get(func_name).unwrap();
639            let ch_sorts = &func.func_type.input;
640
641            let num_children = func.extraction_num_children();
642            let output_name = func.extraction_term_name();
643
644            let mut ch_terms: Vec<TermId> = Vec::new();
645            for (value, sort) in hyperedge.iter().take(num_children).zip(ch_sorts.iter()) {
646                ch_terms.push(
647                    self.reconstruct_termdag_node_helper(egraph, termdag, *value, sort, cache),
648                );
649            }
650            termdag.app(output_name.to_string(), ch_terms)
651        } else {
652            // Base value case
653            sort.reconstruct_termdag_base(egraph.backend.base_values(), value, termdag)
654        };
655
656        cache.insert(key, term);
657        term
658    }
659
660    /// Extracts the best term for `value` from a prepared sort.
661    ///
662    /// `sort` may be a requested root sort, a sort reachable from those roots,
663    /// a primitive sort, or a container of prepared sorts.
664    pub fn extract_best_with_sort(
665        &self,
666        termdag: &mut TermDag,
667        value: Value,
668        sort: ArcSort,
669    ) -> Option<ExtractedTerm<C>> {
670        self.extract_best_with_sort_cached(termdag, &mut Default::default(), value, sort)
671    }
672
673    fn extract_best_with_sort_cached(
674        &self,
675        termdag: &mut TermDag,
676        cache: &mut HashMap<(Value, String), TermId>,
677        value: Value,
678        sort: ArcSort,
679    ) -> Option<ExtractedTerm<C>> {
680        let egraph = self.egraph;
681        // Canonicalize the value using the union-find if available (for term-encoding mode)
682        let canonical_value = self.find_canonical(egraph, value, &sort);
683
684        let best_cost = self.compute_cost_node(egraph, canonical_value, &sort)?;
685        let term =
686            self.reconstruct_termdag_node_helper(egraph, termdag, canonical_value, &sort, cache);
687
688        Some(ExtractedTerm {
689            cost: best_cost,
690            term,
691        })
692    }
693
694    /// Find the canonical representative of a value using the union-find table.
695    /// If no UF is registered for this sort, returns the original value.
696    /// The UF table stores (value, canonical) pairs - one hop lookup.
697    fn find_canonical(&self, egraph: &EGraph, value: Value, sort: &ArcSort) -> Value {
698        // Check if there's a UF registered for this sort
699        let Some(uf_name) = egraph.proof_state.uf_parent.get(sort.name()) else {
700            return value;
701        };
702
703        // Get the UF function
704        let Some(uf_func) = egraph.functions.get(uf_name) else {
705            return value;
706        };
707
708        // Single lookup in UF table - it's guaranteed to be one hop to canonical
709        let mut canonical = value;
710        egraph
711            .backend
712            .for_each(uf_func.backend_id, |row: egglog_bridge::ScanEntry| {
713                // UF table has (child, parent) as inputs
714                if row.vals[0] == value {
715                    canonical = row.vals[1];
716                }
717            });
718
719        canonical
720    }
721
722    /// Extracts up to `nvariants` root variants for `value` from a prepared sort.
723    ///
724    /// Variants are selected by first picking the lowest-cost root e-nodes and
725    /// then extracting each e-node's children with their best prepared terms.
726    pub fn extract_variants_with_sort(
727        &self,
728        termdag: &mut TermDag,
729        value: Value,
730        nvariants: usize,
731        sort: ArcSort,
732    ) -> Vec<ExtractedTerm<C>> {
733        if nvariants == 0 {
734            return vec![];
735        }
736
737        let egraph = self.egraph;
738        if sort.is_eq_sort() {
739            // Canonicalize the value using the union-find if available
740            let canonical_value = self.find_canonical(egraph, value, &sort);
741
742            let mut root_variants: Vec<(C, String, Vec<Value>)> = Vec::new();
743
744            for func_name in self.funcs.iter().filter(|func_name| {
745                // Need an eq on sorts - use extraction_output_sort for view table support
746                sort.name()
747                    == egraph
748                        .functions
749                        .get(*func_name)
750                        .unwrap()
751                        .extraction_output_sort()
752                        .name()
753            }) {
754                let func = egraph.functions.get(func_name).unwrap();
755                let output_idx = func.extraction_output_index();
756
757                let find_root_variants = |row: egglog_bridge::ScanEntry| {
758                    if !row.subsumed {
759                        let target = &row.vals[output_idx];
760                        // A variant whose cost is `None` has a child e-class with no
761                        // finite extraction (e.g. a purely cyclic child); such a variant
762                        // can never appear in a minimal extraction, so we skip it. The
763                        // target e-class still extracts via its other, costed variants.
764                        if *target == canonical_value
765                            && let Some(cost) = self.compute_cost_hyperedge(egraph, &row, func)
766                        {
767                            root_variants.push((cost, func_name.clone(), row.vals.to_vec()));
768                        }
769                    }
770                };
771
772                egraph.backend.for_each(func.backend_id, find_root_variants);
773            }
774
775            let mut res: Vec<ExtractedTerm<C>> = Vec::new();
776            let mut cache: HashMap<(Value, String), TermId> = Default::default();
777            root_variants.sort();
778            root_variants.truncate(nvariants);
779            for (cost, func_name, hyperedge) in root_variants {
780                let mut ch_terms: Vec<TermId> = Vec::new();
781                let func = egraph.functions.get(&func_name).unwrap();
782                let ch_sorts = &func.func_type.input;
783                let num_children = func.extraction_num_children();
784                // For view tables, children are all but the last input (which is the e-class)
785                for (value, sort) in hyperedge.iter().zip(ch_sorts.iter()).take(num_children) {
786                    ch_terms.push(self.reconstruct_termdag_node_helper(
787                        egraph, termdag, *value, sort, &mut cache,
788                    ));
789                }
790                // Use extraction_term_name for view tables (maps to the original constructor)
791                res.push(ExtractedTerm {
792                    cost,
793                    term: termdag.app(func.extraction_term_name().to_string(), ch_terms),
794                });
795            }
796
797            res
798        } else {
799            log::warn!(
800                "extracting multiple variants for containers or primitives is not implemented, returning a single variant."
801            );
802            if let Some(res) = self.extract_best_with_sort(termdag, value, sort) {
803                vec![res]
804            } else {
805                vec![]
806            }
807        }
808    }
809}
810
811impl Function {
812    /// Returns the configured extraction head cost for this table.
813    ///
814    /// View tables inherit the cost of their referenced hidden term constructor.
815    pub(crate) fn extraction_head_cost(&self, egraph: &EGraph) -> Option<DefaultCost> {
816        if let Some(term_constructor) = &self.decl.term_constructor {
817            egraph
818                .functions
819                .get(term_constructor)
820                .and_then(|func| func.decl.cost)
821        } else {
822            self.decl.cost
823        }
824    }
825
826    /// For view tables (with term_constructor), the effective output sort is the last input column.
827    /// For regular tables, it's the output sort.
828    /// This is used by extraction to determine which sort a table produces values for.
829    pub(crate) fn extraction_output_sort(&self) -> &ArcSort {
830        if self.decl.term_constructor.is_some() {
831            self.func_type.input.last().unwrap()
832        } else {
833            &self.func_type.output
834        }
835    }
836
837    /// Returns the number of children for extraction purposes.
838    /// For view tables, this excludes the last column (the e-class).
839    pub(crate) fn extraction_num_children(&self) -> usize {
840        if self.decl.term_constructor.is_some() {
841            self.func_type.input.len() - 1
842        } else {
843            self.func_type.input.len()
844        }
845    }
846
847    /// Returns the name to use when building terms during extraction.
848    /// For view tables, this is the term_constructor name.
849    pub(crate) fn extraction_term_name(&self) -> &str {
850        self.decl
851            .term_constructor
852            .as_ref()
853            .unwrap_or(&self.decl.name)
854    }
855
856    /// Returns the index of the output value in a row for extraction purposes.
857    /// For view tables, the e-class is the last input column (second-to-last in the row).
858    /// For regular tables, it's the last column (the actual output).
859    pub(crate) fn extraction_output_index(&self) -> usize {
860        if self.decl.term_constructor.is_some() {
861            // For view tables: input is [children..., eclass], output is view_sort
862            // Row is [children..., eclass, view_sort]
863            // We want eclass which is at index input.len() - 1
864            self.func_type.input.len() - 1
865        } else {
866            // For regular tables: row is [inputs..., output]
867            self.func_type.input.len()
868        }
869    }
870}
871
872impl EGraph {
873    /// Extracts the best tree term for each requested `(sort, value)` root
874    /// using [`DEFAULT_COST_MODEL`].
875    pub fn extract_best(
876        &self,
877        roots: Vec<(ArcSort, Value)>,
878    ) -> Result<ExtractedTerms<DefaultCost>, Error> {
879        self.extract_best_with_cost_model(roots, DEFAULT_COST_MODEL)
880    }
881
882    /// Extracts the best tree term for each requested `(sort, value)` root.
883    ///
884    /// This is the normal user extraction path: it respects `:unextractable`
885    /// and hidden internal functions. The cost model must satisfy the
886    /// optimal-substructure and convergence requirements on [`TreeExtractor`].
887    pub fn extract_best_with_cost_model<C: Cost, M: TreeCostModel<C>>(
888        &self,
889        roots: Vec<(ArcSort, Value)>,
890        cost_model: M,
891    ) -> Result<ExtractedTerms<C>, Error> {
892        let rootsorts = roots.iter().map(|(sort, _)| sort.clone()).collect();
893        let extractor =
894            TreeExtractor::compute_costs_from_rootsorts(Some(rootsorts), self, cost_model);
895        let mut termdag = TermDag::default();
896        let mut cache = Default::default();
897        let extracted_roots = roots
898            .into_iter()
899            .map(|(sort, value)| {
900                extractor.extract_best_with_sort_cached(&mut termdag, &mut cache, value, sort)
901            })
902            .collect();
903
904        Ok(ExtractedTerms {
905            termdag,
906            terms: extracted_roots,
907        })
908    }
909
910    /// Extracts up to `nvariants` tree root variants for each requested root
911    /// using [`DEFAULT_COST_MODEL`].
912    pub fn extract_variants(
913        &self,
914        roots: Vec<(ArcSort, Value)>,
915        nvariants: usize,
916    ) -> Result<ExtractedTermVariants<DefaultCost>, Error> {
917        self.extract_variants_with_cost_model(roots, nvariants, DEFAULT_COST_MODEL)
918    }
919
920    /// Extracts up to `nvariants` tree root variants for each requested root.
921    ///
922    /// The cost model must satisfy the optimal-substructure and convergence
923    /// requirements on [`TreeExtractor`].
924    pub fn extract_variants_with_cost_model<C: Cost, M: TreeCostModel<C>>(
925        &self,
926        roots: Vec<(ArcSort, Value)>,
927        nvariants: usize,
928        cost_model: M,
929    ) -> Result<ExtractedTermVariants<C>, Error> {
930        if nvariants == 0 {
931            return Ok(ExtractedTermVariants {
932                termdag: TermDag::default(),
933                variants: roots.iter().map(|_| Vec::new()).collect(),
934            });
935        }
936
937        let rootsorts = roots.iter().map(|(sort, _)| sort.clone()).collect();
938        let extractor =
939            TreeExtractor::compute_costs_from_rootsorts(Some(rootsorts), self, cost_model);
940        let mut termdag = TermDag::default();
941        let variants = roots
942            .into_iter()
943            .map(|(sort, value)| {
944                extractor.extract_variants_with_sort(&mut termdag, value, nvariants, sort)
945            })
946            .collect();
947
948        Ok(ExtractedTermVariants { termdag, variants })
949    }
950
951    /// Extracts the best term for one value using the default additive cost model.
952    pub fn extract_value(
953        &self,
954        sort: &ArcSort,
955        value: Value,
956    ) -> Result<(TermDag, TermId, DefaultCost), Error> {
957        let sort_name = sort.name().to_owned();
958        let mut extracted = self.extract_best(vec![(sort.clone(), value)])?;
959        let root = extracted
960            .terms
961            .pop()
962            .expect("one requested root produces one result")
963            .ok_or_else(|| {
964                Error::ExtractError(format!(
965                    "Unable to find any valid extraction for sort {sort_name}"
966                ))
967            })?;
968        Ok((extracted.termdag, root.term, root.cost))
969    }
970
971    /// For constructors and relations, the output column can be ignored
972    pub fn function_to_dag(
973        &self,
974        sym: &str,
975        n: usize,
976        include_output: bool,
977    ) -> Result<(Vec<TermId>, Option<Vec<TermId>>, TermDag), Error> {
978        let func = self
979            .functions
980            .get(sym)
981            .ok_or(TypeError::UnboundFunction(sym.to_owned(), span!()))?;
982        let mut rootsorts = func.func_type.input.clone();
983        if include_output {
984            rootsorts.push(func.func_type.output.clone());
985        }
986        let extractor =
987            TreeExtractor::compute_costs_from_rootsorts(Some(rootsorts), self, DEFAULT_COST_MODEL);
988
989        let mut termdag = TermDag::default();
990        let mut inputs: Vec<TermId> = Vec::new();
991        let mut output: Option<Vec<TermId>> = if include_output {
992            Some(Vec::new())
993        } else {
994            None
995        };
996
997        let extract_row = |row: egglog_bridge::ScanEntry| {
998            if inputs.len() < n {
999                // include subsumed rows
1000                let mut children: Vec<TermId> = Vec::new();
1001                for (value, sort) in row.vals.iter().zip(&func.func_type.input) {
1002                    let term = extractor
1003                        .extract_best_with_sort(&mut termdag, *value, sort.clone())
1004                        .map(|extracted| extracted.term)
1005                        .unwrap_or_else(|| termdag.var("Unextractable".into()));
1006                    children.push(term);
1007                }
1008                inputs.push(termdag.app(sym.to_owned(), children));
1009                if include_output {
1010                    let value = row.vals[func.func_type.input.len()];
1011                    let term = extractor
1012                        .extract_best_with_sort(&mut termdag, value, func.func_type.output.clone())
1013                        .map(|extracted| extracted.term)
1014                        .unwrap_or_else(|| termdag.var("Unextractable".into()));
1015                    output.as_mut().unwrap().push(term);
1016                }
1017                true
1018            } else {
1019                false
1020            }
1021        };
1022
1023        self.backend.for_each_while(func.backend_id, extract_row);
1024
1025        Ok((inputs, output, termdag))
1026    }
1027}