// ============ Run All Optimizations ============
///|
/// Optimization level
/// - 0: No optimization
/// - 1: Basic optimizations (constant folding, copy propagation, CSE, DCE)
/// - 2: Default optimizations (includes control flow optimizations)
/// - 3: Aggressive optimizations (includes loop optimizations)
pub(all) enum OptLevel {
O0 // No optimization
O1 // Basic optimizations
O2 // Default optimizations
O3 // Aggressive optimizations
}
///|
/// Parse optimization level from integer
pub fn OptLevel::from_int(n : Int) -> OptLevel {
match n {
0 => O0
1 => O1
3 => O3
_ => O2 // Default
}
}
///|
/// Count IR instructions in a function.
pub fn instruction_count(func : Function) -> Int {
let mut count = 0
for block in func.blocks {
count = count + block.instructions.length()
}
count
}
///|
fn run_opt_pass(
func : Function,
pass_name : String,
pass_fn : (Function) -> OptResult,
) -> OptResult {
if !perf_enabled() {
return pass_fn(func)
}
let before = instruction_count(func)
let tick = perf_tick_now()
let result = pass_fn(func)
let duration_us = perf_elapsed_us(tick)
let after = instruction_count(func)
perf_record_ir_pass(pass_name, before, after, result.changed, duration_us)
result
}
///|
fn egraph_limits_for_function(func : Function) -> @egraph.SaturationLimits {
let insts = instruction_count(func)
if insts >= 600 {
// An e-class starts with one node, so a limit of one would disable all
// rewrites. Allow one match and one additional node instead.
SaturationLimits(1, 2)
} else if insts >= 300 {
// Medium functions retain a little more local exploration while still
// bounding matches and e-class growth per work item.
SaturationLimits(2, 3)
} else {
@egraph.SaturationLimits::cranelift_default()
}
}
///|
fn run_egraph_pass(func : Function) -> Bool {
let limits = egraph_limits_for_function(func)
// Keep to a compact Cranelift-like core rewrite set for predictable compile
// time at every function size; the limits above scale the work per e-class.
let ruleset = @egraph.get_compact_ruleset()
if !perf_enabled() {
return optimize_function_with_stats_with_limits_and_ruleset(
func, limits, ruleset,
).changed
}
let before = instruction_count(func)
let tick = perf_tick_now()
let stats = optimize_function_with_stats_with_limits_and_ruleset(
func, limits, ruleset,
)
let duration_us = perf_elapsed_us(tick)
let after = instruction_count(func)
perf_record_ir_pass(
"egraph",
before,
after,
stats.changed,
duration_us,
egraph_classes=Some(stats.total_classes),
egraph_nodes=Some(stats.total_nodes),
egraph_rule_apps=Some(stats.total_rule_applications),
)
stats.changed
}
///|
fn gvn_work_budget_for_function(func : Function) -> Int {
let insts = instruction_count(func)
if insts > 300 {
300
} else {
insts
}
}
///|
fn run_gvn_pass(func : Function) -> OptResult {
let before = instruction_count(func)
let budget = gvn_work_budget_for_function(func)
let perf_on = perf_enabled()
let tick = if perf_on { perf_tick_now() } else { 0L }
let stats = if budget == before {
cse_gvn_global(func)
} else {
run_global_value_numbering_with_budget(func, budget)
}
if perf_on {
perf_record_ir_pass(
"cse_gvn_global",
before,
instruction_count(func),
stats.result.changed,
perf_elapsed_us(tick),
work_done=Some(stats.work_done),
budget_exhausted=Some(stats.budget_exhausted),
)
}
stats.result
}
///|
fn run_backend_mandatory_cleanup(func : Function) -> OptResult {
let result = OptResult::OptResult()
let dce_result = run_opt_pass(func, "dce", eliminate_dead_code)
if dce_result.changed {
result.mark_changed()
}
let cbpe_result = run_opt_pass(
func, "const_block_param_elim", eliminate_constant_block_params,
)
if cbpe_result.changed {
result.mark_changed()
}
let dbpe_result = run_opt_pass(
func, "dead_block_param_elim", eliminate_dead_block_params,
)
if dbpe_result.changed {
result.mark_changed()
}
result
}
///|
fn run_optimized_mandatory_pre_cleanup(func : Function) -> OptResult {
let result = OptResult::OptResult()
let backend_result = run_backend_mandatory_cleanup(func)
if backend_result.changed {
result.mark_changed()
}
let cf_result = run_opt_pass(func, "const_fold", fold_constants)
if cf_result.changed {
result.mark_changed()
}
let alias_result = run_opt_pass(func, "alias_canon", canonicalize_aliases)
if alias_result.changed {
result.mark_changed()
}
let cleanup_result = run_opt_pass(func, "dce", eliminate_dead_code)
if cleanup_result.changed {
result.mark_changed()
}
result
}
///|
fn run_mandatory_final_cleanup(func : Function) -> OptResult {
let result = OptResult::OptResult()
let alias_result = run_opt_pass(func, "alias_canon", canonicalize_aliases)
if alias_result.changed {
result.mark_changed()
}
let dce_result = run_opt_pass(func, "dce", eliminate_dead_code)
if dce_result.changed {
result.mark_changed()
}
let bs_result = run_opt_pass(func, "simplify_branches", simplify_branches)
if bs_result.changed {
result.mark_changed()
}
let uce_result = run_opt_pass(
func, "eliminate_unreachable", eliminate_unreachable_code,
)
if uce_result.changed {
result.mark_changed()
}
let bm_result = run_opt_pass(func, "merge_blocks", merge_blocks)
if bm_result.changed {
result.mark_changed()
}
let jt_result = run_opt_pass(func, "thread_jumps", thread_jumps)
if jt_result.changed {
result.mark_changed()
}
result
}
///|
fn optimize_cranelift_style(
func : Function,
run_egraph? : Bool = true,
) -> OptResult {
let result = OptResult::OptResult()
// Mandatory passes are independent of function size and run before budgets
// are selected, so dead and aliased IR does not consume expensive work.
let pre_cleanup = run_optimized_mandatory_pre_cleanup(func)
if pre_cleanup.changed {
result.mark_changed()
}
if run_egraph && run_egraph_pass(func) {
result.mark_changed()
}
let gvn_result = run_gvn_pass(func)
if gvn_result.changed {
result.mark_changed()
}
// Budgeted passes may stop after a safe partial rewrite. Re-establish the
// canonical IR form unconditionally afterward.
let final_cleanup = run_mandatory_final_cleanup(func)
if final_cleanup.changed {
result.mark_changed()
}
result
}
///|
/// Run optimizations based on level
pub fn optimize_with_level(func : Function, level : OptLevel) -> OptResult {
// NOTE: Even at O0 we still need a small set of IR cleanups to keep the
// backend (lowering/regalloc) robust. These are semantics-preserving and
// align with Cranelift's "mandatory" pre-optimization / canonicalization
// philosophy.
if level is O0 {
return run_backend_mandatory_cleanup(func)
}
let result = match level {
O1 => optimize_o1(func)
O2 => optimize(func)
O3 => optimize_o3(func)
O0 => OptResult()
}
result
}
///|
/// O1: Basic optimizations only
fn optimize_o1(func : Function) -> OptResult {
optimize_cranelift_style(func, run_egraph=true)
}
///|
/// Run default optimizations (Cranelift-aligned O2 path).
pub fn optimize(func : Function) -> OptResult {
optimize_cranelift_style(func, run_egraph=true)
}
///|
/// O3: Aggressive optimizations including loop optimizations
fn optimize_o3(func : Function) -> OptResult {
let result = OptResult::OptResult()
// First run O2 optimizations
let o2_result = optimize(func)
if o2_result.changed {
result.mark_changed()
}
// Then apply loop optimizations
let licm_result = run_opt_pass(func, "loop_licm", hoist_loop_invariants)
if licm_result.changed {
result.mark_changed()
}
let unroll_result = run_opt_pass(func, "loop_unroll", unroll_counted_loops)
if unroll_result.changed {
result.mark_changed()
}
let sr_result = run_opt_pass(func, "loop_strength_reduce", reduce_strength)
if sr_result.changed {
result.mark_changed()
}
// Run O2 again to clean up
let cleanup_result = optimize(func)
if cleanup_result.changed {
result.mark_changed()
}
result
}