egglog/
extract.rs

1use crate::ast::FunctionSubtype;
2use crate::termdag::{TermDag, TermId};
3use crate::util::{HashMap, HashSet};
4use crate::*;
5use std::collections::VecDeque;
6
7/// An interface for custom cost model.
8///
9/// To use it with the default extractor, the cost type must also satisfy `Ord + Eq + Clone + Debug`.
10/// Additionally, the cost model should guarantee that a term has a no-smaller cost
11/// than its subterms to avoid cycles in the extracted terms for common case usages.
12/// For more niche usages, a term can have a cost less than its subterms.
13/// As long as there is no negative cost cycle,
14/// the default extractor is guaranteed to terminate in computing the costs.
15/// However, the user needs to be careful to guarantee acyclicity in the extracted terms.
16pub trait CostModel<C: Cost> {
17    /// The total cost of a term given the cost of the root e-node and its immediate children's total costs.
18    fn fold(&self, head: &str, children_cost: &[C], head_cost: C) -> C;
19
20    /// The cost of an enode (without the cost of children)
21    fn enode_cost(&self, egraph: &EGraph, func: &Function, enode: &Enode<'_>) -> C;
22
23    /// The cost of a container value given the costs of its elements.
24    ///
25    /// The default cost for containers is just the sum of all the elements inside
26    fn container_cost(
27        &self,
28        egraph: &EGraph,
29        sort: &ArcSort,
30        value: Value,
31        element_costs: &[C],
32    ) -> C {
33        let _egraph = egraph;
34        let _sort = sort;
35        let _value = value;
36        element_costs
37            .iter()
38            .fold(C::identity(), |s, c| s.combine(c))
39    }
40
41    /// Compute the cost of a (non-container) primitive value.
42    ///
43    /// The default cost for base values is the constant one
44    fn base_value_cost(&self, egraph: &EGraph, sort: &ArcSort, value: Value) -> C {
45        let _egraph = egraph;
46        let _sort = sort;
47        let _value = value;
48        C::unit()
49    }
50}
51
52/// Requirements for a type to be usable as a cost by a [`CostModel`].
53pub trait Cost {
54    /// An identity element, usually zero.
55    fn identity() -> Self;
56
57    /// The default cost for a node with no children, usually one.
58    fn unit() -> Self;
59
60    /// A binary operation to combine costs, usually addition.
61    /// This operation must NOT overflow or panic when given large values!
62    fn combine(self, other: &Self) -> Self;
63}
64
65macro_rules! cost_impl_int {
66    ($($cost:ty),*) => {$(
67        impl Cost for $cost {
68            fn identity() -> Self { 0 }
69            fn unit()     -> Self { 1 }
70            fn combine(self, other: &Self) -> Self {
71                self.saturating_add(*other)
72            }
73        }
74    )*};
75}
76cost_impl_int!(u8, u16, u32, u64, u128, usize);
77cost_impl_int!(i8, i16, i32, i64, i128, isize);
78
79macro_rules! cost_impl_num {
80    ($($cost:ty),*) => {$(
81        impl Cost for $cost {
82            fn identity() -> Self {
83                use num::Zero;
84                Self::zero()
85            }
86            fn unit() -> Self {
87                use num::One;
88                Self::one()
89            }
90            fn combine(self, other: &Self) -> Self {
91                self + other
92            }
93        }
94    )*};
95}
96cost_impl_num!(num::BigInt, num::BigRational);
97use ordered_float::OrderedFloat;
98cost_impl_num!(f32, f64, OrderedFloat<f32>, OrderedFloat<f64>);
99
100pub type DefaultCost = u64;
101
102/// A cost model that computes the cost by summing the cost of each node.
103#[derive(Default, Clone)]
104pub struct TreeAdditiveCostModel {}
105
106impl CostModel<DefaultCost> for TreeAdditiveCostModel {
107    fn fold(
108        &self,
109        _head: &str,
110        children_cost: &[DefaultCost],
111        head_cost: DefaultCost,
112    ) -> DefaultCost {
113        children_cost.iter().fold(head_cost, |s, c| s.combine(c))
114    }
115
116    fn enode_cost(&self, egraph: &EGraph, func: &Function, _enode: &Enode<'_>) -> DefaultCost {
117        func.extraction_head_cost(egraph)
118    }
119}
120
121/// The default, Bellman-Ford like extractor. This extractor is optimal for [`CostModel`].
122///
123/// Note that this assumes optimal substructure in the cost model, that is, a lower-cost
124/// subterm should always lead to a non-worse superterm, to guarantee the extracted term
125/// being optimal under the given cost model.
126/// If this is not followed, the extractor may panic on reconstruction
127pub struct Extractor<C: Cost + Ord + Eq + Clone + Debug> {
128    rootsorts: Vec<ArcSort>,
129    funcs: Vec<String>,
130    cost_model: Box<dyn CostModel<C>>,
131    costs: HashMap<String, HashMap<Value, C>>,
132    topo_rnk_cnt: usize,
133    topo_rnk: HashMap<String, HashMap<Value, usize>>,
134    parent_edge: HashMap<String, HashMap<Value, (String, Vec<Value>)>>,
135}
136
137impl<C: Cost + Ord + Eq + Clone + Debug> Extractor<C> {
138    /// Bulk of the computation happens at initialization time.
139    /// The later extractions only reuses saved results.
140    /// This means a new extractor must be created if the egraph changes.
141    /// Holding a reference to the egraph would enforce this but prevents the extractor being reused.
142    ///
143    /// For convenience, if the rootsorts is `None`, it defaults to extract all extractable rootsorts.
144    pub fn compute_costs_from_rootsorts(
145        rootsorts: Option<Vec<ArcSort>>,
146        egraph: &EGraph,
147        cost_model: impl CostModel<C> + 'static,
148    ) -> Self {
149        // We filter out tables unreachable from the root sorts
150        let extract_all_sorts = rootsorts.is_none();
151
152        let mut rootsorts = rootsorts.unwrap_or_default();
153
154        // Built a reverse index from output sort to function head symbols
155        // Only include constructors (not regular functions), and respect the user-facing
156        // hidden and unextractable flags.
157        let mut rev_index: HashMap<String, Vec<String>> = Default::default();
158        for func in egraph.functions.iter() {
159            let unextractable = func.1.decl.unextractable;
160            let hidden = func.1.decl.internal_hidden;
161
162            // Only extract constructors and view tables, which reconstruct as their
163            // term_constructor. Proof extraction uses its own root-directed extractor
164            // and does not need alternate behavior here.
165            if !unextractable
166                && !hidden
167                && (func.1.decl.subtype == FunctionSubtype::Constructor
168                    || func.1.decl.term_constructor.is_some())
169            {
170                let func_name = func.0.clone();
171                // For view tables (with term_constructor in proof mode), the e-class is the last input column
172                let output_sort_name = func.1.extraction_output_sort().name();
173                if let Some(v) = rev_index.get_mut(output_sort_name) {
174                    v.push(func_name);
175                } else {
176                    rev_index.insert(output_sort_name.to_owned(), vec![func_name]);
177                    if extract_all_sorts {
178                        rootsorts.push(func.1.extraction_output_sort().clone());
179                    }
180                }
181            }
182        }
183
184        // Do a BFS to find reachable tables
185        let mut q: VecDeque<ArcSort> = VecDeque::new();
186        let mut seen: HashSet<String> = Default::default();
187        for rootsort in rootsorts.iter() {
188            q.push_back(rootsort.clone());
189            seen.insert(rootsort.name().to_owned());
190        }
191
192        let mut funcs_set: HashSet<String> = Default::default();
193        let mut funcs: Vec<String> = Vec::new();
194        while !q.is_empty() {
195            let sort = q.pop_front().unwrap();
196            if sort.is_container_sort() {
197                let inner_sorts = sort.inner_sorts();
198                for s in inner_sorts {
199                    if !seen.contains(s.name()) {
200                        q.push_back(s.clone());
201                        seen.insert(s.name().to_owned());
202                    }
203                }
204            } else if sort.is_eq_sort()
205                && let Some(head_symbols) = rev_index.get(sort.name())
206            {
207                for h in head_symbols {
208                    if !funcs_set.contains(h) {
209                        let func = egraph.functions.get(h).unwrap();
210                        // For view tables, children are all but the last input (which is the e-class)
211                        let num_children = func.extraction_num_children();
212                        for ch in func.schema.input.iter().take(num_children) {
213                            let ch_name = ch.name();
214                            if !seen.contains(ch_name) {
215                                q.push_back(ch.clone());
216                                seen.insert(ch_name.to_owned());
217                            }
218                        }
219                        funcs_set.insert(h.clone());
220                        funcs.push(h.clone());
221                    }
222                }
223            }
224        }
225
226        // Initialize the tables to have the reachable entries
227        let mut costs: HashMap<String, HashMap<Value, C>> = Default::default();
228        let mut topo_rnk: HashMap<String, HashMap<Value, usize>> = Default::default();
229        let mut parent_edge: HashMap<String, HashMap<Value, (String, Vec<Value>)>> =
230            Default::default();
231
232        for func_name in funcs.iter() {
233            let func = egraph.functions.get(func_name).unwrap();
234            let output_sort_name = func.extraction_output_sort().name();
235            if !costs.contains_key(output_sort_name) {
236                costs.insert(output_sort_name.to_owned(), Default::default());
237                topo_rnk.insert(output_sort_name.to_owned(), Default::default());
238                parent_edge.insert(output_sort_name.to_owned(), Default::default());
239            }
240        }
241
242        let mut extractor = Extractor {
243            rootsorts,
244            funcs,
245            cost_model: Box::new(cost_model),
246            costs,
247            topo_rnk_cnt: 0,
248            topo_rnk,
249            parent_edge,
250        };
251
252        extractor.bellman_ford(egraph);
253
254        extractor
255    }
256
257    /// Compute the cost of a single enode
258    /// Recurse if container
259    /// Returns None if contains an undefined eqsort term (potentially after unfolding)
260    fn compute_cost_node(&self, egraph: &EGraph, value: Value, sort: &ArcSort) -> Option<C> {
261        if sort.is_container_sort() {
262            let elements = sort.inner_values(egraph.backend.container_values(), value);
263            let mut ch_costs: Vec<C> = Vec::new();
264            for ch in elements.iter() {
265                ch_costs.push(self.compute_cost_node(egraph, ch.1, &ch.0)?);
266            }
267            Some(
268                self.cost_model
269                    .container_cost(egraph, sort, value, &ch_costs),
270            )
271        } else if sort.is_eq_sort() {
272            self.costs.get(sort.name())?.get(&value).cloned()
273        } else {
274            // Primitive
275            Some(self.cost_model.base_value_cost(egraph, sort, value))
276        }
277    }
278
279    /// A row in a constructor table is a hyperedge from the set of input terms to the constructed output term.
280    fn compute_cost_hyperedge(
281        &self,
282        egraph: &EGraph,
283        row: &egglog_bridge::ScanEntry,
284        func: &Function,
285    ) -> Option<C> {
286        let mut ch_costs: Vec<C> = Vec::new();
287        let sorts = &func.schema.input;
288        let num_children = func.extraction_num_children();
289        for (value, sort) in row.vals.iter().take(num_children).zip(sorts.iter()) {
290            ch_costs.push(self.compute_cost_node(egraph, *value, sort)?);
291        }
292        let head_name = func.extraction_term_name();
293        let output_idx = func.extraction_output_index();
294        let enode = Enode {
295            children: &row.vals[..output_idx],
296            eclass: row.vals[output_idx],
297            subsumed: row.subsumed,
298        };
299        Some(self.cost_model.fold(
300            head_name,
301            &ch_costs,
302            self.cost_model.enode_cost(egraph, func, &enode),
303        ))
304    }
305
306    fn compute_topo_rnk_node(&self, egraph: &EGraph, value: Value, sort: &ArcSort) -> usize {
307        if sort.is_container_sort() {
308            sort.inner_values(egraph.backend.container_values(), value)
309                .iter()
310                .fold(0, |ret, (sort, value)| {
311                    usize::max(ret, self.compute_topo_rnk_node(egraph, *value, sort))
312                })
313        } else if sort.is_eq_sort() {
314            if let Some(t) = self.topo_rnk.get(sort.name()) {
315                *t.get(&value).unwrap_or(&usize::MAX)
316            } else {
317                usize::MAX
318            }
319        } else {
320            0
321        }
322    }
323
324    fn compute_topo_rnk_hyperedge(
325        &self,
326        egraph: &EGraph,
327        row: &egglog_bridge::ScanEntry,
328        func: &Function,
329    ) -> usize {
330        let sorts = &func.schema.input;
331        let num_children = func.extraction_num_children();
332        row.vals
333            .iter()
334            .take(num_children)
335            .zip(sorts.iter())
336            .fold(0, |ret, (value, sort)| {
337                usize::max(ret, self.compute_topo_rnk_node(egraph, *value, sort))
338            })
339    }
340
341    /// We use Bellman-Ford to compute the costs of the relevant eq sorts' terms
342    /// [Bellman-Ford](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) is a shortest path algorithm.
343    /// The version implemented here computes the shortest path from any node in a set of sources to all the reachable nodes.
344    /// Computing the minimum cost for terms is treated as a shortest path problem on a hypergraph here.
345    /// In this hypergraph, the nodes corresponde to eclasses, the distances are the costs to extract a term of those eclasses,
346    /// and each enode is a hyperedge that goes from the set of children eclasses to the enode's eclass.
347    /// The sources are the eclasses with known costs from the cost model.
348    /// Additionally, to avoid cycles in the extraction even when the cost model can assign an equal cost to a term and its subterm.
349    /// It computes a topological rank for each eclass
350    /// and only allows each eclass to have children of classes of strictly smaller ranks in the extraction.
351    fn bellman_ford(&mut self, egraph: &EGraph) {
352        let mut ensure_fixpoint = false;
353
354        let funcs = self.funcs.clone();
355
356        while !ensure_fixpoint {
357            ensure_fixpoint = true;
358
359            for func_name in funcs.iter() {
360                let func = egraph.functions.get(func_name).unwrap();
361                let target_sort = func.extraction_output_sort();
362
363                let output_idx = func.extraction_output_index();
364                let relax_hyperedge = |row: egglog_bridge::ScanEntry| {
365                    if !row.subsumed {
366                        let target = &row.vals[output_idx];
367                        let mut updated = false;
368                        if let Some(new_cost) = self.compute_cost_hyperedge(egraph, &row, func) {
369                            match self
370                                .costs
371                                .get_mut(target_sort.name())
372                                .unwrap()
373                                .entry(*target)
374                            {
375                                HEntry::Vacant(e) => {
376                                    updated = true;
377                                    e.insert(new_cost);
378                                }
379                                HEntry::Occupied(mut e) => {
380                                    if new_cost < *(e.get()) {
381                                        updated = true;
382                                        e.insert(new_cost);
383                                    }
384                                }
385                            }
386                        }
387                        // record the chronological order of the updates
388                        // which serves as a topological order that avoids cycles
389                        // even when a term has a cost equal to its subterms
390                        if updated {
391                            ensure_fixpoint = false;
392                            self.topo_rnk_cnt += 1;
393                            self.topo_rnk
394                                .get_mut(target_sort.name())
395                                .unwrap()
396                                .insert(*target, self.topo_rnk_cnt);
397                        }
398                    }
399                };
400
401                egraph.backend.for_each(func.backend_id, relax_hyperedge);
402            }
403        }
404
405        // Save the edges for reconstruction
406        for func_name in funcs.iter() {
407            let func = egraph.functions.get(func_name).unwrap();
408            let target_sort = func.extraction_output_sort();
409            let output_idx = func.extraction_output_index();
410
411            let save_best_parent_edge = |row: egglog_bridge::ScanEntry| {
412                if !row.subsumed {
413                    let target = &row.vals[output_idx];
414                    if let Some(best_cost) = self.costs.get(target_sort.name()).unwrap().get(target)
415                        && Some(best_cost.clone())
416                            == self.compute_cost_hyperedge(egraph, &row, func)
417                    {
418                        // one of the possible best parent edges
419                        let target_topo_rnk = *self
420                            .topo_rnk
421                            .get(target_sort.name())
422                            .unwrap()
423                            .get(target)
424                            .unwrap();
425                        if target_topo_rnk > self.compute_topo_rnk_hyperedge(egraph, &row, func) {
426                            // one of the parent edges that avoids cycles
427                            if let HEntry::Vacant(e) = self
428                                .parent_edge
429                                .get_mut(target_sort.name())
430                                .unwrap()
431                                .entry(*target)
432                            {
433                                e.insert((func.decl.name.clone(), row.vals.to_vec()));
434                            }
435                        }
436                    }
437                }
438            };
439
440            egraph
441                .backend
442                .for_each(func.backend_id, save_best_parent_edge);
443        }
444    }
445
446    /// This recursively reconstruct the termdag that gives the minimum cost for eclass value.
447    fn reconstruct_termdag_node(
448        &self,
449        egraph: &EGraph,
450        termdag: &mut TermDag,
451        value: Value,
452        sort: &ArcSort,
453    ) -> TermId {
454        self.reconstruct_termdag_node_helper(egraph, termdag, value, sort, &mut Default::default())
455    }
456
457    fn reconstruct_termdag_node_helper(
458        &self,
459        egraph: &EGraph,
460        termdag: &mut TermDag,
461        value: Value,
462        sort: &ArcSort,
463        cache: &mut HashMap<(Value, String), TermId>,
464    ) -> TermId {
465        let key = (value, sort.name().to_owned());
466        if let Some(term) = cache.get(&key) {
467            return *term;
468        }
469
470        let term = if sort.is_container_sort() {
471            let elements = sort.inner_values(egraph.backend.container_values(), value);
472            let mut ch_terms: Vec<TermId> = Vec::new();
473            for ch in elements.iter() {
474                ch_terms.push(
475                    self.reconstruct_termdag_node_helper(egraph, termdag, ch.1, &ch.0, cache),
476                );
477            }
478            sort.reconstruct_termdag_container(
479                egraph.backend.container_values(),
480                value,
481                termdag,
482                ch_terms,
483            )
484        } else if sort.is_eq_sort() {
485            let (func_name, hyperedge) = self
486                .parent_edge
487                .get(sort.name())
488                .unwrap()
489                .get(&value)
490                .unwrap();
491            let func = egraph.functions.get(func_name).unwrap();
492            let ch_sorts = &func.schema.input;
493
494            let num_children = func.extraction_num_children();
495            let output_name = func.extraction_term_name();
496
497            let mut ch_terms: Vec<TermId> = Vec::new();
498            for (value, sort) in hyperedge.iter().take(num_children).zip(ch_sorts.iter()) {
499                ch_terms.push(
500                    self.reconstruct_termdag_node_helper(egraph, termdag, *value, sort, cache),
501                );
502            }
503            termdag.app(output_name.to_string(), ch_terms)
504        } else {
505            // Base value case
506            sort.reconstruct_termdag_base(egraph.backend.base_values(), value, termdag)
507        };
508
509        cache.insert(key, term);
510        term
511    }
512
513    /// Extract the best term of a value from a given sort.
514    ///
515    /// This function expects the sort to be already computed,
516    /// which can be one of the rootsorts, or reachable from rootsorts, or primitives, or containers of computed sorts.
517    pub fn extract_best_with_sort(
518        &self,
519        egraph: &EGraph,
520        termdag: &mut TermDag,
521        value: Value,
522        sort: ArcSort,
523    ) -> Option<(C, TermId)> {
524        // Canonicalize the value using the union-find if available (for term-encoding mode)
525        let canonical_value = self.find_canonical(egraph, value, &sort);
526
527        match self.compute_cost_node(egraph, canonical_value, &sort) {
528            Some(best_cost) => {
529                log::debug!("Best cost for the extract root: {best_cost:?}");
530
531                let term = self.reconstruct_termdag_node(egraph, termdag, canonical_value, &sort);
532
533                Some((best_cost, term))
534            }
535            None => {
536                log::error!("Unextractable root {value:?} with sort {sort:?}",);
537                None
538            }
539        }
540    }
541
542    /// A convenience method for extraction.
543    ///
544    /// This expects the value to be of the unique sort the extractor has been initialized with
545    pub fn extract_best(
546        &self,
547        egraph: &EGraph,
548        termdag: &mut TermDag,
549        value: Value,
550    ) -> Option<(C, TermId)> {
551        assert!(
552            self.rootsorts.len() == 1,
553            "extract_best requires a single rootsort"
554        );
555        self.extract_best_with_sort(
556            egraph,
557            termdag,
558            value,
559            self.rootsorts.first().unwrap().clone(),
560        )
561    }
562
563    /// Find the canonical representative of a value using the union-find table.
564    /// If no UF is registered for this sort, returns the original value.
565    /// The UF table stores (value, canonical) pairs - one hop lookup.
566    fn find_canonical(&self, egraph: &EGraph, value: Value, sort: &ArcSort) -> Value {
567        // Check if there's a UF registered for this sort
568        let Some(uf_name) = egraph.proof_state.uf_parent.get(sort.name()) else {
569            return value;
570        };
571
572        // Get the UF function
573        let Some(uf_func) = egraph.functions.get(uf_name) else {
574            return value;
575        };
576
577        // Single lookup in UF table - it's guaranteed to be one hop to canonical
578        let mut canonical = value;
579        egraph
580            .backend
581            .for_each(uf_func.backend_id, |row: egglog_bridge::ScanEntry| {
582                // UF table has (child, parent) as inputs
583                if row.vals[0] == value {
584                    canonical = row.vals[1];
585                }
586            });
587
588        canonical
589    }
590
591    /// Extract variants of an e-class.
592    ///
593    /// The variants are selected by first picking `nvairants` e-nodes with the lowest cost from the e-class
594    /// and then extracting a term from each e-node.
595    pub fn extract_variants_with_sort(
596        &self,
597        egraph: &EGraph,
598        termdag: &mut TermDag,
599        value: Value,
600        nvariants: usize,
601        sort: ArcSort,
602    ) -> Vec<(C, TermId)> {
603        debug_assert!(self.rootsorts.iter().any(|s| { s.name() == sort.name() }));
604
605        if sort.is_eq_sort() {
606            // Canonicalize the value using the union-find if available
607            let canonical_value = self.find_canonical(egraph, value, &sort);
608
609            let mut root_variants: Vec<(C, String, Vec<Value>)> = Vec::new();
610
611            let mut root_funcs: Vec<String> = Vec::new();
612
613            for func_name in self.funcs.iter() {
614                // Need an eq on sorts - use extraction_output_sort for view table support
615                if sort.name()
616                    == egraph
617                        .functions
618                        .get(func_name)
619                        .unwrap()
620                        .extraction_output_sort()
621                        .name()
622                {
623                    root_funcs.push(func_name.clone());
624                }
625            }
626
627            for func_name in root_funcs.iter() {
628                let func = egraph.functions.get(func_name).unwrap();
629                let output_idx = func.extraction_output_index();
630
631                let find_root_variants = |row: egglog_bridge::ScanEntry| {
632                    if !row.subsumed {
633                        let target = &row.vals[output_idx];
634                        // A variant whose cost is `None` has a child e-class with no
635                        // finite extraction (e.g. a purely cyclic child); such a variant
636                        // can never appear in a minimal extraction, so we skip it. The
637                        // target e-class still extracts via its other, costed variants.
638                        if *target == canonical_value
639                            && let Some(cost) = self.compute_cost_hyperedge(egraph, &row, func)
640                        {
641                            root_variants.push((cost, func_name.clone(), row.vals.to_vec()));
642                        }
643                    }
644                };
645
646                egraph.backend.for_each(func.backend_id, find_root_variants);
647            }
648
649            let mut res: Vec<(C, TermId)> = Vec::new();
650            let mut cache: HashMap<(Value, String), TermId> = Default::default();
651            root_variants.sort();
652            root_variants.truncate(nvariants);
653            for (cost, func_name, hyperedge) in root_variants {
654                let mut ch_terms: Vec<TermId> = Vec::new();
655                let func = egraph.functions.get(&func_name).unwrap();
656                let ch_sorts = &func.schema.input;
657                let num_children = func.extraction_num_children();
658                // For view tables, children are all but the last input (which is the e-class)
659                for (value, sort) in hyperedge.iter().zip(ch_sorts.iter()).take(num_children) {
660                    ch_terms.push(self.reconstruct_termdag_node_helper(
661                        egraph, termdag, *value, sort, &mut cache,
662                    ));
663                }
664                // Use extraction_term_name for view tables (maps to the original constructor)
665                res.push((
666                    cost,
667                    termdag.app(func.extraction_term_name().to_string(), ch_terms),
668                ));
669            }
670
671            res
672        } else {
673            log::warn!(
674                "extracting multiple variants for containers or primitives is not implemented, returning a single variant."
675            );
676            if let Some(res) = self.extract_best_with_sort(egraph, termdag, value, sort) {
677                vec![res]
678            } else {
679                vec![]
680            }
681        }
682    }
683
684    /// A convenience method for extracting variants of a value.
685    ///
686    /// This expects the value to be of the unique sort the extractor has been initialized with.
687    pub fn extract_variants(
688        &self,
689        egraph: &EGraph,
690        termdag: &mut TermDag,
691        value: Value,
692        nvariants: usize,
693    ) -> Vec<(C, TermId)> {
694        assert!(
695            self.rootsorts.len() == 1,
696            "extract_variants requires a single rootsort"
697        );
698        self.extract_variants_with_sort(
699            egraph,
700            termdag,
701            value,
702            nvariants,
703            self.rootsorts.first().unwrap().clone(),
704        )
705    }
706}
707
708impl Function {
709    /// Returns the extraction head cost for this table.
710    /// View tables inherit the cost of their referenced hidden term constructor.
711    pub(crate) fn extraction_head_cost(&self, egraph: &EGraph) -> DefaultCost {
712        if let Some(term_constructor) = &self.decl.term_constructor {
713            egraph
714                .functions
715                .get(term_constructor)
716                .and_then(|func| func.decl.cost)
717                .unwrap_or(DefaultCost::unit())
718        } else {
719            self.decl.cost.unwrap_or(DefaultCost::unit())
720        }
721    }
722
723    /// For view tables (with term_constructor), the effective output sort is the last input column.
724    /// For regular tables, it's the output sort.
725    /// This is used by extraction to determine which sort a table produces values for.
726    pub(crate) fn extraction_output_sort(&self) -> &ArcSort {
727        if self.decl.term_constructor.is_some() {
728            self.schema.input.last().unwrap()
729        } else {
730            &self.schema.output
731        }
732    }
733
734    /// Returns the number of children for extraction purposes.
735    /// For view tables, this excludes the last column (the e-class).
736    pub(crate) fn extraction_num_children(&self) -> usize {
737        if self.decl.term_constructor.is_some() {
738            self.schema.input.len() - 1
739        } else {
740            self.schema.input.len()
741        }
742    }
743
744    /// Returns the name to use when building terms during extraction.
745    /// For view tables, this is the term_constructor name.
746    pub(crate) fn extraction_term_name(&self) -> &str {
747        self.decl
748            .term_constructor
749            .as_ref()
750            .unwrap_or(&self.decl.name)
751    }
752
753    /// Returns the index of the output value in a row for extraction purposes.
754    /// For view tables, the e-class is the last input column (second-to-last in the row).
755    /// For regular tables, it's the last column (the actual output).
756    pub(crate) fn extraction_output_index(&self) -> usize {
757        if self.decl.term_constructor.is_some() {
758            // For view tables: input is [children..., eclass], output is view_sort
759            // Row is [children..., eclass, view_sort]
760            // We want eclass which is at index input.len() - 1
761            self.schema.input.len() - 1
762        } else {
763            // For regular tables: row is [inputs..., output]
764            self.schema.input.len()
765        }
766    }
767}
768
769impl EGraph {
770    /// Extract a value to a [`TermDag`] and [`TermId`] in the [`TermDag`] using the default cost model.
771    /// See also [`EGraph::extract_value_with_cost_model`] for more control.
772    pub fn extract_value(
773        &self,
774        sort: &ArcSort,
775        value: Value,
776    ) -> Result<(TermDag, TermId, DefaultCost), Error> {
777        self.extract_value_with_cost_model(sort, value, TreeAdditiveCostModel::default())
778    }
779
780    /// Extract a value to a [`TermDag`] and [`TermId`] in the [`TermDag`].
781    /// Note that the `TermDag` may contain a superset of the nodes referenced by the returned `TermId`.
782    /// See also [`EGraph::extract_value_to_string`] for convenience.
783    pub fn extract_value_with_cost_model<CM: CostModel<DefaultCost> + 'static>(
784        &self,
785        sort: &ArcSort,
786        value: Value,
787        cost_model: CM,
788    ) -> Result<(TermDag, TermId, DefaultCost), Error> {
789        let extractor =
790            Extractor::compute_costs_from_rootsorts(Some(vec![sort.clone()]), self, cost_model);
791        let mut termdag = TermDag::default();
792        let (cost, term) = extractor
793            .extract_best(self, &mut termdag, value)
794            .ok_or_else(|| {
795                Error::ExtractError(
796                    "Unable to find any valid extraction (likely due to subsume or delete)"
797                        .to_string(),
798                )
799            })?;
800        Ok((termdag, term, cost))
801    }
802
803    /// Extract a value to a string for printing.
804    /// See also [`EGraph::extract_value`] for more control.
805    pub fn extract_value_to_string(
806        &self,
807        sort: &ArcSort,
808        value: Value,
809    ) -> Result<(String, DefaultCost), Error> {
810        let (termdag, term, cost) = self.extract_value(sort, value)?;
811        Ok((termdag.to_string(term), cost))
812    }
813
814    /// For constructors and relations, the output column can be ignored
815    pub fn function_to_dag(
816        &self,
817        sym: &str,
818        n: usize,
819        include_output: bool,
820    ) -> Result<(Vec<TermId>, Option<Vec<TermId>>, TermDag), Error> {
821        let func = self
822            .functions
823            .get(sym)
824            .ok_or(TypeError::UnboundFunction(sym.to_owned(), span!()))?;
825        let mut rootsorts = func.schema.input.clone();
826        if include_output {
827            rootsorts.push(func.schema.output.clone());
828        }
829        let extractor = Extractor::compute_costs_from_rootsorts(
830            Some(rootsorts),
831            self,
832            TreeAdditiveCostModel::default(),
833        );
834
835        let mut termdag = TermDag::default();
836        let mut inputs: Vec<TermId> = Vec::new();
837        let mut output: Option<Vec<TermId>> = if include_output {
838            Some(Vec::new())
839        } else {
840            None
841        };
842
843        let extract_row = |row: egglog_bridge::ScanEntry| {
844            if inputs.len() < n {
845                // include subsumed rows
846                let mut children: Vec<TermId> = Vec::new();
847                for (value, sort) in row.vals.iter().zip(&func.schema.input) {
848                    let (_, term_id) = extractor
849                        .extract_best_with_sort(self, &mut termdag, *value, sort.clone())
850                        .unwrap_or_else(|| (0, termdag.var("Unextractable".into())));
851                    children.push(term_id);
852                }
853                inputs.push(termdag.app(sym.to_owned(), children));
854                if include_output {
855                    let value = row.vals[func.schema.input.len()];
856                    let sort = &func.schema.output;
857                    let (_, term) = extractor
858                        .extract_best_with_sort(self, &mut termdag, value, sort.clone())
859                        .unwrap_or_else(|| (0, termdag.var("Unextractable".into())));
860                    output.as_mut().unwrap().push(term);
861                }
862                true
863            } else {
864                false
865            }
866        };
867
868        self.backend.for_each_while(func.backend_id, extract_row);
869
870        Ok((inputs, output, termdag))
871    }
872}