use crate::FxHashSet;
use crate::alias_analysis::{AliasAnalysis, LastStores, OptResult};
use crate::ctxhash::{CtxEq, CtxHash, NullCtx};
use crate::cursor::{Cursor, CursorPosition, FuncCursor};
use crate::dominator_tree::DominatorTree;
use crate::egraph::elaborate::Elaborator;
use crate::flowgraph::ControlFlowGraph;
use crate::inst_predicates::{is_mergeable_for_egraph, is_pure_for_egraph};
use crate::ir::{
Block, DataFlowGraph, Function, Inst, InstructionData, Type, Value, ValueDef, ValueListPool,
};
use crate::loop_analysis::LoopAnalysis;
use crate::opts::IsleContext;
use crate::opts::generated_code::SkeletonInstSimplification;
use crate::scoped_hash_map::{Entry as ScopedEntry, ScopedHashMap};
use crate::take_and_replace::TakeAndReplace;
use crate::trace;
use alloc::{vec, vec::Vec};
use core::cmp::Ordering;
use core::hash::Hasher;
use cranelift_control::ControlPlane;
use cranelift_entity::packed_option::ReservedValue;
use cranelift_entity::{EntitySet, SecondaryMap};
use smallvec::SmallVec;
mod cost;
mod elaborate;
struct EgraphBlockIter<'a> {
domtree: &'a DominatorTree,
stack: Vec<Block>,
children: SmallVec<[Block; 8]>,
}
impl<'a> EgraphBlockIter<'a> {
fn new(domtree: &'a DominatorTree) -> Self {
let mut iter = Self {
domtree,
stack: Vec::new(),
children: SmallVec::new(),
};
if let Some(&root) = domtree.cfg_postorder().last() {
iter.stack.push(root);
}
iter
}
}
impl Iterator for EgraphBlockIter<'_> {
type Item = Block;
fn next(&mut self) -> Option<Block> {
let block = self.stack.pop()?;
self.children.clear();
self.children.extend(self.domtree.children(block));
self.stack.extend(self.children.iter().rev().copied());
Some(block)
}
}
pub struct EgraphPass<'a> {
func: &'a mut Function,
domtree: &'a DominatorTree,
alias_analysis: &'a mut AliasAnalysis<'a>,
loop_analysis: &'a LoopAnalysis,
ctrl_plane: &'a mut ControlPlane,
cfg: &'a mut ControlFlowGraph,
remat_values: FxHashSet<Value>,
pub(crate) stats: Stats,
}
const MATCHES_LIMIT: usize = 5;
const ECLASS_ENODE_LIMIT: usize = 5;
pub(crate) const EXTRACTOR_FUEL: u32 = 500;
pub(crate) struct OptimizeCtx<'opt, 'analysis>
where
'analysis: 'opt,
{
pub(crate) func: &'opt mut Function,
pub(crate) value_to_opt_value: &'opt mut SecondaryMap<Value, Value>,
available_block: &'opt mut SecondaryMap<Value, Block>,
eclass_size: &'opt mut SecondaryMap<Value, u8>,
pub(crate) gvn_map: &'opt mut ScopedHashMap<(Type, InstructionData), Option<Value>>,
pub(crate) gvn_map_blocks: &'opt Vec<Block>,
pub(crate) remat_values: &'opt mut FxHashSet<Value>,
pub(crate) stats: &'opt mut Stats,
domtree: &'opt DominatorTree,
pub(crate) alias_analysis: &'opt mut AliasAnalysis<'analysis>,
pub(crate) alias_analysis_state: &'opt mut LastStores,
ctrl_plane: &'opt mut ControlPlane,
pub(crate) rewrite_depth: usize,
pub(crate) extractor_fuel: u32,
pub(crate) subsume_values: FxHashSet<Value>,
optimized_values: SmallVec<[Value; MATCHES_LIMIT]>,
optimized_insts: SmallVec<[SkeletonInstSimplification; MATCHES_LIMIT]>,
}
pub(crate) enum NewOrExistingInst {
New(InstructionData, Type),
Existing(Inst),
}
impl NewOrExistingInst {
fn get_inst_key<'a>(&'a self, dfg: &'a DataFlowGraph) -> (Type, InstructionData) {
match self {
NewOrExistingInst::New(data, ty) => (*ty, *data),
NewOrExistingInst::Existing(inst) => {
let ty = dfg.ctrl_typevar(*inst);
(ty, dfg.insts[*inst])
}
}
}
}
impl<'opt, 'analysis> OptimizeCtx<'opt, 'analysis>
where
'analysis: 'opt,
{
pub(crate) fn insert_pure_enode(&mut self, inst: NewOrExistingInst) -> Value {
let gvn_context = GVNContext {
value_lists: &self.func.dfg.value_lists,
};
self.stats.pure_inst += 1;
if let NewOrExistingInst::New(..) = inst {
self.stats.new_inst += 1;
}
if let Some(&Some(orig_result)) = self
.gvn_map
.get(&gvn_context, &inst.get_inst_key(&self.func.dfg))
{
self.stats.pure_inst_deduped += 1;
if let NewOrExistingInst::Existing(inst) = inst {
debug_assert_eq!(self.func.dfg.inst_results(inst).len(), 1);
let result = self.func.dfg.first_result(inst);
self.value_to_opt_value[result] = orig_result;
self.available_block[result] = self.available_block[orig_result];
}
orig_result
} else {
let (inst, result, ty) = match inst {
NewOrExistingInst::New(data, typevar) => {
self.stats.pure_inst_insert_new += 1;
let inst = self.func.dfg.make_inst(data);
self.func.dfg.make_inst_results(inst, typevar);
let result = self.func.dfg.first_result(inst);
(inst, result, typevar)
}
NewOrExistingInst::Existing(inst) => {
self.stats.pure_inst_insert_orig += 1;
let result = self.func.dfg.first_result(inst);
let ty = self.func.dfg.ctrl_typevar(inst);
(inst, result, ty)
}
};
self.available_block[result] = self.get_available_block(inst);
let opt_value = self.optimize_pure_enode(inst);
log::trace!("optimizing inst {inst} orig result {result} gave {opt_value}");
let gvn_context = GVNContext {
value_lists: &self.func.dfg.value_lists,
};
log::trace!(
"value {} is available at {}",
opt_value,
self.available_block[opt_value]
);
let depth = self.depth_of_block_in_gvn_map(self.available_block[opt_value]);
self.gvn_map.insert_with_depth(
&gvn_context,
(ty, self.func.dfg.insts[inst]),
Some(opt_value),
depth,
);
self.value_to_opt_value[result] = opt_value;
opt_value
}
}
fn get_available_block(&self, inst: Inst) -> Block {
debug_assert!(is_pure_for_egraph(self.func, inst));
self.func.dfg.insts[inst]
.arguments(&self.func.dfg.value_lists)
.iter()
.map(|&v| {
let block = self.available_block[v];
debug_assert!(!block.is_reserved_value());
block
})
.max_by(|&x, &y| {
if self.domtree.block_dominates(x, y) {
Ordering::Less
} else {
debug_assert!(self.domtree.block_dominates(y, x));
Ordering::Greater
}
})
.unwrap_or(self.func.layout.entry_block().unwrap())
}
fn depth_of_block_in_gvn_map(&self, block: Block) -> usize {
log::trace!(
"finding depth of available block {} in domtree stack: {:?}",
block,
self.gvn_map_blocks
);
self.gvn_map_blocks
.iter()
.enumerate()
.rev()
.find(|&(_, b)| *b == block)
.unwrap()
.0
}
fn optimize_pure_enode(&mut self, inst: Inst) -> Value {
let orig_value = self.func.dfg.first_result(inst);
let mut guard = TakeAndReplace::new(self, |x| &mut x.optimized_values);
let (ctx, optimized_values) = guard.get();
const REWRITE_LIMIT: usize = 5;
if ctx.rewrite_depth >= REWRITE_LIMIT {
ctx.stats.rewrite_depth_limit += 1;
return orig_value;
}
ctx.rewrite_depth += 1;
trace!("Incrementing rewrite depth; now {}", ctx.rewrite_depth);
trace!("Calling into ISLE with original value {}", orig_value);
ctx.stats.rewrite_rule_invoked += 1;
debug_assert!(optimized_values.is_empty());
crate::opts::generated_code::constructor_simplify(
&mut IsleContext { ctx },
orig_value,
optimized_values,
);
ctx.stats.rewrite_rule_results += optimized_values.len() as u64;
ctx.ctrl_plane.shuffle(optimized_values);
let num_matches = optimized_values.len();
if num_matches > MATCHES_LIMIT {
trace!(
"Reached maximum matches limit; too many optimized values \
({num_matches} > {MATCHES_LIMIT}); ignoring rest.",
);
optimized_values.truncate(MATCHES_LIMIT);
}
optimized_values.sort_unstable();
optimized_values.dedup();
trace!(" -> returned from ISLE: {orig_value} -> {optimized_values:?}");
let result_value = if let Some(&subsuming_value) = optimized_values
.iter()
.find(|&value| ctx.subsume_values.contains(value))
{
optimized_values.clear();
ctx.stats.pure_inst_subsume += 1;
subsuming_value
} else {
let mut union_value = orig_value;
let mut eclass_size = ctx.eclass_size[orig_value] + 1;
for optimized_value in optimized_values.drain(..) {
trace!(
"Returned from ISLE for {}, got {:?}",
orig_value, optimized_value
);
if optimized_value == orig_value {
trace!(" -> same as orig value; skipping");
ctx.stats.pure_inst_rewrite_to_self += 1;
continue;
}
let rhs_eclass_size = ctx.eclass_size[optimized_value] + 1;
if usize::from(eclass_size) + usize::from(rhs_eclass_size) > ECLASS_ENODE_LIMIT {
trace!(" -> reached eclass size limit");
ctx.stats.eclass_size_limit += 1;
break;
}
let old_union_value = union_value;
union_value = ctx.func.dfg.union(old_union_value, optimized_value);
eclass_size += rhs_eclass_size;
ctx.eclass_size[union_value] = eclass_size - 1;
ctx.stats.union += 1;
trace!(" -> union: now {}", union_value);
ctx.available_block[union_value] =
ctx.merge_availability(old_union_value, optimized_value);
}
union_value
};
ctx.rewrite_depth -= 1;
trace!("Decrementing rewrite depth; now {}", ctx.rewrite_depth);
if ctx.rewrite_depth == 0 {
ctx.subsume_values.clear();
}
debug_assert!(ctx.optimized_values.is_empty());
result_value
}
fn merge_availability(&self, a: Value, b: Value) -> Block {
let a = self.available_block[a];
let b = self.available_block[b];
if self.domtree.block_dominates(a, b) {
a
} else {
b
}
}
fn optimize_skeleton_inst(
&mut self,
inst: Inst,
block: Block,
) -> Option<SkeletonInstSimplification> {
self.stats.skeleton_inst += 1;
if let Some(cmd) = self.simplify_skeleton_inst(inst) {
self.stats.skeleton_inst_simplified += 1;
return Some(cmd);
}
if is_mergeable_for_egraph(self.func, inst) {
let result = self.func.dfg.inst_results(inst).get(0).copied();
trace!(" -> mergeable side-effecting op {}", inst);
let ty = self.func.dfg.ctrl_typevar(inst);
match self
.gvn_map
.entry(&NullCtx, (ty, self.func.dfg.insts[inst]))
{
ScopedEntry::Occupied(o) => {
let orig_result = *o.get();
match (result, orig_result) {
(Some(result), Some(orig_result)) => {
self.stats.skeleton_inst_gvn += 1;
self.value_to_opt_value[result] = orig_result;
self.available_block[result] = self.available_block[orig_result];
trace!(" -> merges result {} to {}", result, orig_result);
}
(None, None) => {
self.stats.skeleton_inst_gvn += 1;
trace!(" -> merges with dominating instruction");
}
(_, _) => unreachable!(),
}
Some(SkeletonInstSimplification::Remove)
}
ScopedEntry::Vacant(v) => {
if let Some(result) = result {
self.value_to_opt_value[result] = result;
self.available_block[result] = block;
}
v.insert(result);
trace!(" -> inserts as new (no GVN)");
None
}
}
}
else {
match self
.alias_analysis
.process_inst(self.func, self.alias_analysis_state, inst)
{
OptResult::AliasedLoad(new_result) => {
self.stats.alias_analysis_removed_load += 1;
let result = self.func.dfg.first_result(inst);
trace!(
" -> inst {} has result {} replaced with {}",
inst, result, new_result
);
self.value_to_opt_value[result] = new_result;
self.available_block[result] = self.available_block[new_result];
Some(SkeletonInstSimplification::Remove)
}
OptResult::IdempotentStore => {
self.stats.alias_analysis_removed_store += 1;
Some(SkeletonInstSimplification::Remove)
}
OptResult::None => {
for &result in self.func.dfg.inst_results(inst) {
self.value_to_opt_value[result] = result;
self.available_block[result] = block;
}
None
}
}
}
}
fn simplify_skeleton_inst(&mut self, inst: Inst) -> Option<SkeletonInstSimplification> {
let mut guard = TakeAndReplace::new(self, |x| &mut x.optimized_insts);
let (ctx, optimized_insts) = guard.get();
crate::opts::generated_code::constructor_simplify_skeleton(
&mut IsleContext { ctx },
inst,
optimized_insts,
);
let simplifications_len = optimized_insts.len();
log::trace!(" -> simplify_skeleton: yielded {simplifications_len} simplification(s)");
if simplifications_len > MATCHES_LIMIT {
log::trace!(" too many candidate simplifications; truncating to {MATCHES_LIMIT}");
optimized_insts.truncate(MATCHES_LIMIT);
}
let mut best = None;
let mut best_cost = cost::Cost::of_skeleton_op(
ctx.func.dfg.insts[inst].opcode(),
ctx.func.dfg.inst_args(inst).len(),
);
while let Some(simplification) = optimized_insts.pop() {
let (new_inst, new_val) = match simplification {
SkeletonInstSimplification::Remove => {
log::trace!(" -> simplify_skeleton: remove inst");
debug_assert!(ctx.func.dfg.inst_results(inst).is_empty());
return Some(simplification);
}
SkeletonInstSimplification::RemoveWithVal { val } => {
log::trace!(" -> simplify_skeleton: remove inst and use {val} as its result");
if cfg!(debug_assertions) {
let results = ctx.func.dfg.inst_results(inst);
debug_assert_eq!(results.len(), 1);
debug_assert_eq!(
ctx.func.dfg.value_type(results[0]),
ctx.func.dfg.value_type(val),
);
}
return Some(simplification);
}
SkeletonInstSimplification::Replace { inst } => {
log::trace!(
" -> simplify_skeleton: replace inst with {inst}: {}",
ctx.func.dfg.display_inst(inst)
);
(inst, None)
}
SkeletonInstSimplification::ReplaceWithVal { inst, val } => {
log::trace!(
" -> simplify_skeleton: replace inst with {val} and {inst}: {}",
ctx.func.dfg.display_inst(inst)
);
(inst, Some(val))
}
SkeletonInstSimplification::ReplaceBranchCond { cond } => {
log::trace!(" -> simplify_skeleton: replace `brif` cond with {cond}");
return Some(SkeletonInstSimplification::ReplaceBranchCond { cond });
}
};
if cfg!(debug_assertions) {
let old_vals = ctx.func.dfg.inst_results(inst);
let new_vals = if let Some(val) = new_val.as_ref() {
core::slice::from_ref(val)
} else {
ctx.func.dfg.inst_results(new_inst)
};
debug_assert_eq!(
old_vals.len(),
new_vals.len(),
"skeleton simplification should result in the same number of result values",
);
for (old_val, new_val) in old_vals.iter().zip(new_vals) {
let old_ty = ctx.func.dfg.value_type(*old_val);
let new_ty = ctx.func.dfg.value_type(*new_val);
debug_assert_eq!(
old_ty, new_ty,
"skeleton simplification should result in values of the correct type",
);
}
}
let cost = cost::Cost::of_skeleton_op(
ctx.func.dfg.insts[new_inst].opcode(),
ctx.func.dfg.inst_args(new_inst).len(),
);
if cost < best_cost {
best = Some(simplification);
best_cost = cost;
}
}
best
}
}
impl<'a> EgraphPass<'a> {
pub fn new(
func: &'a mut Function,
domtree: &'a DominatorTree,
loop_analysis: &'a LoopAnalysis,
alias_analysis: &'a mut AliasAnalysis<'a>,
ctrl_plane: &'a mut ControlPlane,
cfg: &'a mut ControlFlowGraph,
) -> Self {
Self {
func,
domtree,
loop_analysis,
alias_analysis,
ctrl_plane,
cfg,
stats: Stats::default(),
remat_values: FxHashSet::default(),
}
}
pub fn run(&mut self) {
self.remove_pure_and_optimize();
trace!("egraph built:\n{}\n", self.func.display());
if cfg!(feature = "trace-log") {
for (value, def) in self.func.dfg.values_and_defs() {
trace!(" -> {} = {:?}", value, def);
match def {
ValueDef::Result(i, 0) => {
trace!(" -> {} = {:?}", i, self.func.dfg.insts[i]);
}
_ => {}
}
}
}
self.cfg.compute(self.func);
let reachable_blocks = self.find_reachable_blocks();
crate::unreachable_code::eliminate_unreachable_code(self.func, self.cfg, |block| {
reachable_blocks.contains(block)
});
self.elaborate();
log::trace!("stats: {:#?}", self.stats);
}
fn remove_pure_and_optimize(&mut self) {
let mut cursor = FuncCursor::new(self.func);
let mut value_to_opt_value: SecondaryMap<Value, Value> =
SecondaryMap::with_default(Value::reserved_value());
let mut gvn_map: ScopedHashMap<(Type, InstructionData), Option<Value>> =
ScopedHashMap::with_capacity(cursor.func.dfg.num_values());
let mut gvn_map_blocks: Vec<Block> = vec![];
let mut available_block: SecondaryMap<Value, Block> =
SecondaryMap::with_default(Block::reserved_value());
let mut eclass_size: SecondaryMap<Value, u8> = SecondaryMap::with_default(0);
available_block.resize(cursor.func.dfg.num_values());
let domtree = self.domtree;
for block in EgraphBlockIter::new(domtree) {
while gvn_map_blocks
.last()
.is_some_and(|&dom| domtree.idom(block) != Some(dom))
{
gvn_map_blocks.pop();
gvn_map.decrement_depth();
}
gvn_map.increment_depth();
gvn_map_blocks.push(block);
debug_assert_eq!(gvn_map_blocks, {
let mut b = Some(block);
let mut v = core::iter::from_fn(move || {
let block = b;
b = b.map(|b| domtree.idom(b))?;
block
})
.collect::<Vec<_>>();
v.reverse();
v
});
trace!("Processing block {}", block);
cursor.set_position(CursorPosition::Before(block));
let mut alias_analysis_state = self.alias_analysis.block_starting_state(block);
for ¶m in cursor.func.dfg.block_params(block) {
trace!("creating initial singleton eclass for blockparam {}", param);
value_to_opt_value[param] = param;
available_block[param] = block;
}
while let Some(inst) = cursor.next_inst() {
trace!(
"Processing inst {inst}: {}",
cursor.func.dfg.display_inst(inst),
);
cursor.func.dfg.map_inst_values(inst, |arg| {
let new_value = value_to_opt_value[arg];
trace!("rewriting arg {} of inst {} to {}", arg, inst, new_value);
debug_assert_ne!(
new_value,
Value::reserved_value(),
"rewriting arg {arg} of {inst} to {new_value}, but \
{new_value} == Value::reserved_value()"
);
new_value
});
let mut ctx = OptimizeCtx {
func: cursor.func,
value_to_opt_value: &mut value_to_opt_value,
gvn_map: &mut gvn_map,
gvn_map_blocks: &mut gvn_map_blocks,
available_block: &mut available_block,
eclass_size: &mut eclass_size,
rewrite_depth: 0,
extractor_fuel: EXTRACTOR_FUEL,
subsume_values: FxHashSet::default(),
remat_values: &mut self.remat_values,
stats: &mut self.stats,
domtree: &self.domtree,
alias_analysis: self.alias_analysis,
alias_analysis_state: &mut alias_analysis_state,
ctrl_plane: self.ctrl_plane,
optimized_values: Default::default(),
optimized_insts: Default::default(),
};
if is_pure_for_egraph(ctx.func, inst) {
let inst = NewOrExistingInst::Existing(inst);
ctx.insert_pure_enode(inst);
cursor.remove_inst_and_step_back();
} else {
if let Some(cmd) = ctx.optimize_skeleton_inst(inst, block) {
Self::execute_skeleton_inst_simplification(
cmd,
&mut cursor,
&mut value_to_opt_value,
inst,
);
}
}
}
}
}
fn find_reachable_blocks(&self) -> EntitySet<Block> {
let mut reachable = EntitySet::<Block>::with_capacity(self.func.dfg.num_blocks());
let entry = self.func.layout.entry_block().unwrap();
let mut stack = vec![entry];
reachable.insert(entry);
while let Some(block) = stack.pop() {
for successor in self.cfg.succ_iter(block) {
if reachable.insert(successor) {
stack.push(successor);
}
}
}
reachable
}
fn execute_skeleton_inst_simplification(
simplification: SkeletonInstSimplification,
cursor: &mut FuncCursor,
value_to_opt_value: &mut SecondaryMap<Value, Value>,
old_inst: Inst,
) {
let mut forward_val = |cursor: &mut FuncCursor, old_val, new_val| {
cursor.func.dfg.change_to_alias(old_val, new_val);
value_to_opt_value[old_val] = new_val;
};
let (new_inst, new_val) = match simplification {
SkeletonInstSimplification::Remove => {
cursor.remove_inst_and_step_back();
return;
}
SkeletonInstSimplification::RemoveWithVal { val } => {
cursor.remove_inst_and_step_back();
let old_val = cursor.func.dfg.first_result(old_inst);
cursor.func.dfg.detach_inst_results(old_inst);
forward_val(cursor, old_val, val);
return;
}
SkeletonInstSimplification::Replace { inst } => (inst, None),
SkeletonInstSimplification::ReplaceWithVal { inst, val } => (inst, Some(val)),
SkeletonInstSimplification::ReplaceBranchCond { cond } => {
debug_assert_eq!(
cursor.func.dfg.insts[old_inst].opcode(),
crate::ir::Opcode::Brif,
);
cursor.func.dfg.inst_args_mut(old_inst)[0] = cond;
return;
}
};
cursor.replace_inst(new_inst);
let mut i = 0;
let mut next_new_val = |dfg: &crate::ir::DataFlowGraph| -> Value {
if let Some(val) = new_val {
val
} else {
let val = dfg.inst_results(new_inst)[i];
i += 1;
val
}
};
for i in 0..cursor.func.dfg.inst_results(old_inst).len() {
let old_val = cursor.func.dfg.inst_results(old_inst)[i];
let new_val = next_new_val(&cursor.func.dfg);
forward_val(cursor, old_val, new_val);
}
cursor.goto_inst(new_inst);
}
fn elaborate(&mut self) {
let mut elaborator = Elaborator::new(
self.func,
&self.domtree,
self.loop_analysis,
&self.remat_values,
&mut self.stats,
self.ctrl_plane,
);
elaborator.elaborate();
self.check_post_egraph();
}
#[cfg(debug_assertions)]
fn check_post_egraph(&self) {
for block in self.func.layout.blocks() {
for inst in self.func.layout.block_insts(block) {
self.func
.dfg
.inst_values(inst)
.for_each(|arg| match self.func.dfg.value_def(arg) {
ValueDef::Result(i, _) => {
debug_assert!(self.func.layout.inst_block(i).is_some());
}
ValueDef::Union(..) => {
panic!("egraph union node {arg} still reachable at {inst}!");
}
_ => {}
})
}
}
}
#[cfg(not(debug_assertions))]
fn check_post_egraph(&self) {}
}
struct GVNContext<'a> {
value_lists: &'a ValueListPool,
}
impl<'a> CtxEq<(Type, InstructionData), (Type, InstructionData)> for GVNContext<'a> {
fn ctx_eq(
&self,
(a_ty, a_inst): &(Type, InstructionData),
(b_ty, b_inst): &(Type, InstructionData),
) -> bool {
a_ty == b_ty && a_inst.eq(b_inst, self.value_lists)
}
}
impl<'a> CtxHash<(Type, InstructionData)> for GVNContext<'a> {
fn ctx_hash<H: Hasher>(&self, state: &mut H, (ty, inst): &(Type, InstructionData)) {
core::hash::Hash::hash(&ty, state);
inst.hash(state, self.value_lists);
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Stats {
pub(crate) pure_inst: u64,
pub(crate) pure_inst_deduped: u64,
pub(crate) pure_inst_subsume: u64,
pub(crate) pure_inst_rewrite_to_self: u64,
pub(crate) pure_inst_insert_orig: u64,
pub(crate) pure_inst_insert_new: u64,
pub(crate) skeleton_inst: u64,
pub(crate) skeleton_inst_simplified: u64,
pub(crate) skeleton_inst_gvn: u64,
pub(crate) alias_analysis_removed_load: u64,
pub(crate) alias_analysis_removed_store: u64,
pub(crate) new_inst: u64,
pub(crate) union: u64,
pub(crate) subsume: u64,
pub(crate) remat: u64,
pub(crate) rewrite_rule_invoked: u64,
pub(crate) rewrite_rule_results: u64,
pub(crate) rewrite_depth_limit: u64,
pub(crate) rewrite_fuel_exhausted: u64,
pub(crate) elaborate_visit_node: u64,
pub(crate) elaborate_memoize_hit: u64,
pub(crate) elaborate_memoize_miss: u64,
pub(crate) elaborate_remat: u64,
pub(crate) elaborate_licm_hoist: u64,
pub(crate) elaborate_func: u64,
pub(crate) elaborate_func_pre_insts: u64,
pub(crate) elaborate_func_post_insts: u64,
pub(crate) eclass_size_limit: u64,
}