cranelift_codegen/
egraph.rs

1//! Support for egraphs represented in the DataFlowGraph.
2
3use crate::alias_analysis::{AliasAnalysis, LastStores};
4use crate::ctxhash::{CtxEq, CtxHash, CtxHashMap};
5use crate::cursor::{Cursor, CursorPosition, FuncCursor};
6use crate::dominator_tree::DominatorTree;
7use crate::egraph::domtree::DomTreeWithChildren;
8use crate::egraph::elaborate::Elaborator;
9use crate::fx::FxHashSet;
10use crate::inst_predicates::{is_mergeable_for_egraph, is_pure_for_egraph};
11use crate::ir::{
12    Block, DataFlowGraph, Function, Inst, InstructionData, Type, Value, ValueDef, ValueListPool,
13};
14use crate::loop_analysis::LoopAnalysis;
15use crate::opts::generated_code::ContextIter;
16use crate::opts::IsleContext;
17use crate::scoped_hash_map::{Entry as ScopedEntry, ScopedHashMap};
18use crate::trace;
19use crate::unionfind::UnionFind;
20use cranelift_entity::packed_option::ReservedValue;
21use cranelift_entity::SecondaryMap;
22use std::hash::Hasher;
23
24mod cost;
25mod domtree;
26mod elaborate;
27
28/// Pass over a Function that does the whole aegraph thing.
29///
30/// - Removes non-skeleton nodes from the Layout.
31/// - Performs a GVN-and-rule-application pass over all Values
32///   reachable from the skeleton, potentially creating new Union
33///   nodes (i.e., an aegraph) so that some values have multiple
34///   representations.
35/// - Does "extraction" on the aegraph: selects the best value out of
36///   the tree-of-Union nodes for each used value.
37/// - Does "scoped elaboration" on the aegraph: chooses one or more
38///   locations for pure nodes to become instructions again in the
39///   layout, as forced by the skeleton.
40///
41/// At the beginning and end of this pass, the CLIF should be in a
42/// state that passes the verifier and, additionally, has no Union
43/// nodes. During the pass, Union nodes may exist, and instructions in
44/// the layout may refer to results of instructions that are not
45/// placed in the layout.
46pub struct EgraphPass<'a> {
47    /// The function we're operating on.
48    func: &'a mut Function,
49    /// Dominator tree, used for elaboration pass.
50    domtree: &'a DominatorTree,
51    /// Alias analysis, used during optimization.
52    alias_analysis: &'a mut AliasAnalysis<'a>,
53    /// "Domtree with children": like `domtree`, but with an explicit
54    /// list of children, rather than just parent pointers.
55    domtree_children: DomTreeWithChildren,
56    /// Loop analysis results, used for built-in LICM during
57    /// elaboration.
58    loop_analysis: &'a LoopAnalysis,
59    /// Which canonical Values do we want to rematerialize in each
60    /// block where they're used?
61    ///
62    /// (A canonical Value is the *oldest* Value in an eclass,
63    /// i.e. tree of union value-nodes).
64    remat_values: FxHashSet<Value>,
65    /// Stats collected while we run this pass.
66    pub(crate) stats: Stats,
67    /// Union-find that maps all members of a Union tree (eclass) back
68    /// to the *oldest* (lowest-numbered) `Value`.
69    eclasses: UnionFind<Value>,
70}
71
72/// Context passed through node insertion and optimization.
73pub(crate) struct OptimizeCtx<'opt, 'analysis>
74where
75    'analysis: 'opt,
76{
77    // Borrowed from EgraphPass:
78    pub(crate) func: &'opt mut Function,
79    pub(crate) value_to_opt_value: &'opt mut SecondaryMap<Value, Value>,
80    pub(crate) gvn_map: &'opt mut CtxHashMap<(Type, InstructionData), Value>,
81    pub(crate) effectful_gvn_map: &'opt mut ScopedHashMap<(Type, InstructionData), Value>,
82    pub(crate) eclasses: &'opt mut UnionFind<Value>,
83    pub(crate) remat_values: &'opt mut FxHashSet<Value>,
84    pub(crate) stats: &'opt mut Stats,
85    pub(crate) alias_analysis: &'opt mut AliasAnalysis<'analysis>,
86    pub(crate) alias_analysis_state: &'opt mut LastStores,
87    // Held locally during optimization of one node (recursively):
88    pub(crate) rewrite_depth: usize,
89    pub(crate) subsume_values: FxHashSet<Value>,
90}
91
92/// For passing to `insert_pure_enode`. Sometimes the enode already
93/// exists as an Inst (from the original CLIF), and sometimes we're in
94/// the middle of creating it and want to avoid inserting it if
95/// possible until we know we need it.
96pub(crate) enum NewOrExistingInst {
97    New(InstructionData, Type),
98    Existing(Inst),
99}
100
101impl NewOrExistingInst {
102    fn get_inst_key<'a>(&'a self, dfg: &'a DataFlowGraph) -> (Type, InstructionData) {
103        match self {
104            NewOrExistingInst::New(data, ty) => (*ty, *data),
105            NewOrExistingInst::Existing(inst) => {
106                let ty = dfg.ctrl_typevar(*inst);
107                (ty, dfg.insts[*inst].clone())
108            }
109        }
110    }
111}
112
113impl<'opt, 'analysis> OptimizeCtx<'opt, 'analysis>
114where
115    'analysis: 'opt,
116{
117    /// Optimization of a single instruction.
118    ///
119    /// This does a few things:
120    /// - Looks up the instruction in the GVN deduplication map. If we
121    ///   already have the same instruction somewhere else, with the
122    ///   same args, then we can alias the original instruction's
123    ///   results and omit this instruction entirely.
124    ///   - Note that we do this canonicalization based on the
125    ///     instruction with its arguments as *canonical* eclass IDs,
126    ///     that is, the oldest (smallest index) `Value` reachable in
127    ///     the tree-of-unions (whole eclass). This ensures that we
128    ///     properly canonicalize newer nodes that use newer "versions"
129    ///     of a value that are still equal to the older versions.
130    /// - If the instruction is "new" (not deduplicated), then apply
131    ///   optimization rules:
132    ///   - All of the mid-end rules written in ISLE.
133    ///   - Store-to-load forwarding.
134    /// - Update the value-to-opt-value map, and update the eclass
135    ///   union-find, if we rewrote the value to different form(s).
136    pub(crate) fn insert_pure_enode(&mut self, inst: NewOrExistingInst) -> Value {
137        // Create the external context for looking up and updating the
138        // GVN map. This is necessary so that instructions themselves
139        // do not have to carry all the references or data for a full
140        // `Eq` or `Hash` impl.
141        let gvn_context = GVNContext {
142            union_find: self.eclasses,
143            value_lists: &self.func.dfg.value_lists,
144        };
145
146        self.stats.pure_inst += 1;
147        if let NewOrExistingInst::New(..) = inst {
148            self.stats.new_inst += 1;
149        }
150
151        // Does this instruction already exist? If so, add entries to
152        // the value-map to rewrite uses of its results to the results
153        // of the original (existing) instruction. If not, optimize
154        // the new instruction.
155        if let Some(&orig_result) = self
156            .gvn_map
157            .get(&inst.get_inst_key(&self.func.dfg), &gvn_context)
158        {
159            self.stats.pure_inst_deduped += 1;
160            if let NewOrExistingInst::Existing(inst) = inst {
161                debug_assert_eq!(self.func.dfg.inst_results(inst).len(), 1);
162                let result = self.func.dfg.first_result(inst);
163                self.value_to_opt_value[result] = orig_result;
164                self.eclasses.union(result, orig_result);
165                self.stats.union += 1;
166                result
167            } else {
168                orig_result
169            }
170        } else {
171            // Now actually insert the InstructionData and attach
172            // result value (exactly one).
173            let (inst, result, ty) = match inst {
174                NewOrExistingInst::New(data, typevar) => {
175                    let inst = self.func.dfg.make_inst(data);
176                    // TODO: reuse return value?
177                    self.func.dfg.make_inst_results(inst, typevar);
178                    let result = self.func.dfg.first_result(inst);
179                    // Add to eclass unionfind.
180                    self.eclasses.add(result);
181                    // New inst. We need to do the analysis of its result.
182                    (inst, result, typevar)
183                }
184                NewOrExistingInst::Existing(inst) => {
185                    let result = self.func.dfg.first_result(inst);
186                    let ty = self.func.dfg.ctrl_typevar(inst);
187                    (inst, result, ty)
188                }
189            };
190
191            let opt_value = self.optimize_pure_enode(inst);
192            let gvn_context = GVNContext {
193                union_find: self.eclasses,
194                value_lists: &self.func.dfg.value_lists,
195            };
196            self.gvn_map.insert(
197                (ty, self.func.dfg.insts[inst].clone()),
198                opt_value,
199                &gvn_context,
200            );
201            self.value_to_opt_value[result] = opt_value;
202            opt_value
203        }
204    }
205
206    /// Optimizes an enode by applying any matching mid-end rewrite
207    /// rules (or store-to-load forwarding, which is a special case),
208    /// unioning together all possible optimized (or rewritten) forms
209    /// of this expression into an eclass and returning the `Value`
210    /// that represents that eclass.
211    fn optimize_pure_enode(&mut self, inst: Inst) -> Value {
212        // A pure node always has exactly one result.
213        let orig_value = self.func.dfg.first_result(inst);
214
215        let mut isle_ctx = IsleContext { ctx: self };
216
217        // Limit rewrite depth. When we apply optimization rules, they
218        // may create new nodes (values) and those are, recursively,
219        // optimized eagerly as soon as they are created. So we may
220        // have more than one ISLE invocation on the stack. (This is
221        // necessary so that as the toplevel builds the
222        // right-hand-side expression bottom-up, it uses the "latest"
223        // optimized values for all the constituent parts.) To avoid
224        // infinite or problematic recursion, we bound the rewrite
225        // depth to a small constant here.
226        const REWRITE_LIMIT: usize = 5;
227        if isle_ctx.ctx.rewrite_depth > REWRITE_LIMIT {
228            isle_ctx.ctx.stats.rewrite_depth_limit += 1;
229            return orig_value;
230        }
231        isle_ctx.ctx.rewrite_depth += 1;
232
233        // Invoke the ISLE toplevel constructor, getting all new
234        // values produced as equivalents to this value.
235        trace!("Calling into ISLE with original value {}", orig_value);
236        isle_ctx.ctx.stats.rewrite_rule_invoked += 1;
237        let mut optimized_values =
238            crate::opts::generated_code::constructor_simplify(&mut isle_ctx, orig_value);
239
240        // Create a union of all new values with the original (or
241        // maybe just one new value marked as "subsuming" the
242        // original, if present.)
243        let mut union_value = orig_value;
244        while let Some(optimized_value) = optimized_values.next(&mut isle_ctx) {
245            trace!(
246                "Returned from ISLE for {}, got {:?}",
247                orig_value,
248                optimized_value
249            );
250            if optimized_value == orig_value {
251                trace!(" -> same as orig value; skipping");
252                continue;
253            }
254            if isle_ctx.ctx.subsume_values.contains(&optimized_value) {
255                // Merge in the unionfind so canonicalization
256                // still works, but take *only* the subsuming
257                // value, and break now.
258                isle_ctx.ctx.eclasses.union(optimized_value, union_value);
259                union_value = optimized_value;
260                break;
261            }
262
263            let old_union_value = union_value;
264            union_value = isle_ctx
265                .ctx
266                .func
267                .dfg
268                .union(old_union_value, optimized_value);
269            isle_ctx.ctx.stats.union += 1;
270            trace!(" -> union: now {}", union_value);
271            isle_ctx.ctx.eclasses.add(union_value);
272            isle_ctx
273                .ctx
274                .eclasses
275                .union(old_union_value, optimized_value);
276            isle_ctx.ctx.eclasses.union(old_union_value, union_value);
277        }
278
279        isle_ctx.ctx.rewrite_depth -= 1;
280
281        union_value
282    }
283
284    /// Optimize a "skeleton" instruction, possibly removing
285    /// it. Returns `true` if the instruction should be removed from
286    /// the layout.
287    fn optimize_skeleton_inst(&mut self, inst: Inst) -> bool {
288        self.stats.skeleton_inst += 1;
289
290        // First, can we try to deduplicate? We need to keep some copy
291        // of the instruction around because it's side-effecting, but
292        // we may be able to reuse an earlier instance of it.
293        if is_mergeable_for_egraph(self.func, inst) {
294            let result = self.func.dfg.inst_results(inst)[0];
295            trace!(" -> mergeable side-effecting op {}", inst);
296
297            // Does this instruction already exist? If so, add entries to
298            // the value-map to rewrite uses of its results to the results
299            // of the original (existing) instruction. If not, optimize
300            // the new instruction.
301            //
302            // Note that we use the "effectful GVN map", which is
303            // scoped: because effectful ops are not removed from the
304            // skeleton (`Layout`), we need to be mindful of whether
305            // our current position is dominated by an instance of the
306            // instruction. (See #5796 for details.)
307            let ty = self.func.dfg.ctrl_typevar(inst);
308            match self
309                .effectful_gvn_map
310                .entry((ty, self.func.dfg.insts[inst].clone()))
311            {
312                ScopedEntry::Occupied(o) => {
313                    let orig_result = *o.get();
314                    // Hit in GVN map -- reuse value.
315                    self.value_to_opt_value[result] = orig_result;
316                    self.eclasses.union(orig_result, result);
317                    trace!(" -> merges result {} to {}", result, orig_result);
318                    true
319                }
320                ScopedEntry::Vacant(v) => {
321                    // Otherwise, insert it into the value-map.
322                    self.value_to_opt_value[result] = result;
323                    v.insert(result);
324                    trace!(" -> inserts as new (no GVN)");
325                    false
326                }
327            }
328        }
329        // Otherwise, if a load or store, process it with the alias
330        // analysis to see if we can optimize it (rewrite in terms of
331        // an earlier load or stored value).
332        else if let Some(new_result) =
333            self.alias_analysis
334                .process_inst(self.func, self.alias_analysis_state, inst)
335        {
336            self.stats.alias_analysis_removed += 1;
337            let result = self.func.dfg.first_result(inst);
338            trace!(
339                " -> inst {} has result {} replaced with {}",
340                inst,
341                result,
342                new_result
343            );
344            self.value_to_opt_value[result] = new_result;
345            true
346        }
347        // Otherwise, generic side-effecting op -- always keep it, and
348        // set its results to identity-map to original values.
349        else {
350            // Set all results to identity-map to themselves
351            // in the value-to-opt-value map.
352            for &result in self.func.dfg.inst_results(inst) {
353                self.value_to_opt_value[result] = result;
354                self.eclasses.add(result);
355            }
356            false
357        }
358    }
359}
360
361impl<'a> EgraphPass<'a> {
362    /// Create a new EgraphPass.
363    pub fn new(
364        func: &'a mut Function,
365        domtree: &'a DominatorTree,
366        loop_analysis: &'a LoopAnalysis,
367        alias_analysis: &'a mut AliasAnalysis<'a>,
368    ) -> Self {
369        let num_values = func.dfg.num_values();
370        let domtree_children = DomTreeWithChildren::new(func, domtree);
371        Self {
372            func,
373            domtree,
374            domtree_children,
375            loop_analysis,
376            alias_analysis,
377            stats: Stats::default(),
378            eclasses: UnionFind::with_capacity(num_values),
379            remat_values: FxHashSet::default(),
380        }
381    }
382
383    /// Run the process.
384    pub fn run(&mut self) {
385        self.remove_pure_and_optimize();
386
387        trace!("egraph built:\n{}\n", self.func.display());
388        if cfg!(feature = "trace-log") {
389            for (value, def) in self.func.dfg.values_and_defs() {
390                trace!(" -> {} = {:?}", value, def);
391                match def {
392                    ValueDef::Result(i, 0) => {
393                        trace!("  -> {} = {:?}", i, self.func.dfg.insts[i]);
394                    }
395                    _ => {}
396                }
397            }
398        }
399        trace!("stats: {:?}", self.stats);
400        self.elaborate();
401    }
402
403    /// Remove pure nodes from the `Layout` of the function, ensuring
404    /// that only the "side-effect skeleton" remains, and also
405    /// optimize the pure nodes. This is the first step of
406    /// egraph-based processing and turns the pure CFG-based CLIF into
407    /// a CFG skeleton with a sea of (optimized) nodes tying it
408    /// together.
409    ///
410    /// As we walk through the code, we eagerly apply optimization
411    /// rules; at any given point we have a "latest version" of an
412    /// eclass of possible representations for a `Value` in the
413    /// original program, which is itself a `Value` at the root of a
414    /// union-tree. We keep a map from the original values to these
415    /// optimized values. When we encounter any instruction (pure or
416    /// side-effecting skeleton) we rewrite its arguments to capture
417    /// the "latest" optimized forms of these values. (We need to do
418    /// this as part of this pass, and not later using a finished map,
419    /// because the eclass can continue to be updated and we need to
420    /// only refer to its subset that exists at this stage, to
421    /// maintain acyclicity.)
422    fn remove_pure_and_optimize(&mut self) {
423        let mut cursor = FuncCursor::new(self.func);
424        let mut value_to_opt_value: SecondaryMap<Value, Value> =
425            SecondaryMap::with_default(Value::reserved_value());
426        // Map from instruction to value for hash-consing of pure ops
427        // into the egraph. This can be a standard (non-scoped)
428        // hashmap because pure ops have no location: they are
429        // "outside of" control flow.
430        //
431        // Note also that we keep the controlling typevar (the `Type`
432        // in the tuple below) because it may disambiguate
433        // instructions that are identical except for type.
434        let mut gvn_map: CtxHashMap<(Type, InstructionData), Value> =
435            CtxHashMap::with_capacity(cursor.func.dfg.num_values());
436        // Map from instruction to value for GVN'ing of effectful but
437        // idempotent ops, which remain in the side-effecting
438        // skeleton. This needs to be scoped because we cannot
439        // deduplicate one instruction to another that is in a
440        // non-dominating block.
441        //
442        // Note that we can use a ScopedHashMap here without the
443        // "context" (as needed by CtxHashMap) because in practice the
444        // ops we want to GVN have all their args inline. Equality on
445        // the InstructionData itself is conservative: two insts whose
446        // struct contents compare shallowly equal are definitely
447        // identical, but identical insts in a deep-equality sense may
448        // not compare shallowly equal, due to list indirection. This
449        // is fine for GVN, because it is still sound to skip any
450        // given GVN opportunity (and keep the original instructions).
451        //
452        // As above, we keep the controlling typevar here as part of
453        // the key: effectful instructions may (as for pure
454        // instructions) be differentiated only on the type.
455        let mut effectful_gvn_map: ScopedHashMap<(Type, InstructionData), Value> =
456            ScopedHashMap::new();
457
458        // In domtree preorder, visit blocks. (TODO: factor out an
459        // iterator from this and elaborator.)
460        let root = self.domtree_children.root();
461        enum StackEntry {
462            Visit(Block),
463            Pop,
464        }
465        let mut block_stack = vec![StackEntry::Visit(root)];
466        while let Some(entry) = block_stack.pop() {
467            match entry {
468                StackEntry::Visit(block) => {
469                    // We popped this block; push children
470                    // immediately, then process this block.
471                    block_stack.push(StackEntry::Pop);
472                    block_stack
473                        .extend(self.domtree_children.children(block).map(StackEntry::Visit));
474                    effectful_gvn_map.increment_depth();
475
476                    trace!("Processing block {}", block);
477                    cursor.set_position(CursorPosition::Before(block));
478
479                    let mut alias_analysis_state = self.alias_analysis.block_starting_state(block);
480
481                    for &param in cursor.func.dfg.block_params(block) {
482                        trace!("creating initial singleton eclass for blockparam {}", param);
483                        self.eclasses.add(param);
484                        value_to_opt_value[param] = param;
485                    }
486                    while let Some(inst) = cursor.next_inst() {
487                        trace!("Processing inst {}", inst);
488
489                        // While we're passing over all insts, create initial
490                        // singleton eclasses for all result and blockparam
491                        // values.  Also do initial analysis of all inst
492                        // results.
493                        for &result in cursor.func.dfg.inst_results(inst) {
494                            trace!("creating initial singleton eclass for {}", result);
495                            self.eclasses.add(result);
496                        }
497
498                        // Rewrite args of *all* instructions using the
499                        // value-to-opt-value map.
500                        cursor.func.dfg.resolve_aliases_in_arguments(inst);
501                        cursor.func.dfg.map_inst_values(inst, |_, arg| {
502                            let new_value = value_to_opt_value[arg];
503                            trace!("rewriting arg {} of inst {} to {}", arg, inst, new_value);
504                            debug_assert_ne!(new_value, Value::reserved_value());
505                            new_value
506                        });
507
508                        // Build a context for optimization, with borrows of
509                        // state. We can't invoke a method on `self` because
510                        // we've borrowed `self.func` mutably (as
511                        // `cursor.func`) so we pull apart the pieces instead
512                        // here.
513                        let mut ctx = OptimizeCtx {
514                            func: cursor.func,
515                            value_to_opt_value: &mut value_to_opt_value,
516                            gvn_map: &mut gvn_map,
517                            effectful_gvn_map: &mut effectful_gvn_map,
518                            eclasses: &mut self.eclasses,
519                            rewrite_depth: 0,
520                            subsume_values: FxHashSet::default(),
521                            remat_values: &mut self.remat_values,
522                            stats: &mut self.stats,
523                            alias_analysis: self.alias_analysis,
524                            alias_analysis_state: &mut alias_analysis_state,
525                        };
526
527                        if is_pure_for_egraph(ctx.func, inst) {
528                            // Insert into GVN map and optimize any new nodes
529                            // inserted (recursively performing this work for
530                            // any nodes the optimization rules produce).
531                            let inst = NewOrExistingInst::Existing(inst);
532                            ctx.insert_pure_enode(inst);
533                            // We've now rewritten all uses, or will when we
534                            // see them, and the instruction exists as a pure
535                            // enode in the eclass, so we can remove it.
536                            cursor.remove_inst_and_step_back();
537                        } else {
538                            if ctx.optimize_skeleton_inst(inst) {
539                                cursor.remove_inst_and_step_back();
540                            }
541                        }
542                    }
543                }
544                StackEntry::Pop => {
545                    effectful_gvn_map.decrement_depth();
546                }
547            }
548        }
549    }
550
551    /// Scoped elaboration: compute a final ordering of op computation
552    /// for each block and update the given Func body. After this
553    /// runs, the function body is back into the state where every
554    /// Inst with an used result is placed in the layout (possibly
555    /// duplicated, if our code-motion logic decides this is the best
556    /// option).
557    ///
558    /// This works in concert with the domtree. We do a preorder
559    /// traversal of the domtree, tracking a scoped map from Id to
560    /// (new) Value. The map's scopes correspond to levels in the
561    /// domtree.
562    ///
563    /// At each block, we iterate forward over the side-effecting
564    /// eclasses, and recursively generate their arg eclasses, then
565    /// emit the ops themselves.
566    ///
567    /// To use an eclass in a given block, we first look it up in the
568    /// scoped map, and get the Value if already present. If not, we
569    /// need to generate it. We emit the extracted enode for this
570    /// eclass after recursively generating its args. Eclasses are
571    /// thus computed "as late as possible", but then memoized into
572    /// the Id-to-Value map and available to all dominated blocks and
573    /// for the rest of this block. (This subsumes GVN.)
574    fn elaborate(&mut self) {
575        let mut elaborator = Elaborator::new(
576            self.func,
577            self.domtree,
578            &self.domtree_children,
579            self.loop_analysis,
580            &mut self.remat_values,
581            &mut self.eclasses,
582            &mut self.stats,
583        );
584        elaborator.elaborate();
585
586        self.check_post_egraph();
587    }
588
589    #[cfg(debug_assertions)]
590    fn check_post_egraph(&self) {
591        // Verify that no union nodes are reachable from inst args,
592        // and that all inst args' defining instructions are in the
593        // layout.
594        for block in self.func.layout.blocks() {
595            for inst in self.func.layout.block_insts(block) {
596                self.func
597                    .dfg
598                    .inst_values(inst)
599                    .for_each(|arg| match self.func.dfg.value_def(arg) {
600                        ValueDef::Result(i, _) => {
601                            debug_assert!(self.func.layout.inst_block(i).is_some());
602                        }
603                        ValueDef::Union(..) => {
604                            panic!("egraph union node {} still reachable at {}!", arg, inst);
605                        }
606                        _ => {}
607                    })
608            }
609        }
610    }
611
612    #[cfg(not(debug_assertions))]
613    fn check_post_egraph(&self) {}
614}
615
616/// Implementation of external-context equality and hashing on
617/// InstructionData. This allows us to deduplicate instructions given
618/// some context that lets us see its value lists and the mapping from
619/// any value to "canonical value" (in an eclass).
620struct GVNContext<'a> {
621    value_lists: &'a ValueListPool,
622    union_find: &'a UnionFind<Value>,
623}
624
625impl<'a> CtxEq<(Type, InstructionData), (Type, InstructionData)> for GVNContext<'a> {
626    fn ctx_eq(
627        &self,
628        (a_ty, a_inst): &(Type, InstructionData),
629        (b_ty, b_inst): &(Type, InstructionData),
630    ) -> bool {
631        a_ty == b_ty
632            && a_inst.eq(b_inst, self.value_lists, |value| {
633                self.union_find.find(value)
634            })
635    }
636}
637
638impl<'a> CtxHash<(Type, InstructionData)> for GVNContext<'a> {
639    fn ctx_hash<H: Hasher>(&self, state: &mut H, (ty, inst): &(Type, InstructionData)) {
640        std::hash::Hash::hash(&ty, state);
641        inst.hash(state, self.value_lists, |value| self.union_find.find(value));
642    }
643}
644
645/// Statistics collected during egraph-based processing.
646#[derive(Clone, Debug, Default)]
647pub(crate) struct Stats {
648    pub(crate) pure_inst: u64,
649    pub(crate) pure_inst_deduped: u64,
650    pub(crate) skeleton_inst: u64,
651    pub(crate) alias_analysis_removed: u64,
652    pub(crate) new_inst: u64,
653    pub(crate) union: u64,
654    pub(crate) subsume: u64,
655    pub(crate) remat: u64,
656    pub(crate) rewrite_rule_invoked: u64,
657    pub(crate) rewrite_depth_limit: u64,
658    pub(crate) elaborate_visit_node: u64,
659    pub(crate) elaborate_memoize_hit: u64,
660    pub(crate) elaborate_memoize_miss: u64,
661    pub(crate) elaborate_memoize_miss_remat: u64,
662    pub(crate) elaborate_licm_hoist: u64,
663    pub(crate) elaborate_func: u64,
664    pub(crate) elaborate_func_pre_insts: u64,
665    pub(crate) elaborate_func_post_insts: u64,
666}