diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_administrative_reduction.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_administrative_reduction.rs index cc1f17983cb..35c352154c8 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_administrative_reduction.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_administrative_reduction.rs @@ -3,6 +3,7 @@ use std::io::Write as _; use hashql_ast::node::expr::Expr; use hashql_core::{ heap::{Heap, Scratch}, + id::IdVec, r#type::environment::Environment, }; use hashql_diagnostics::DiagnosticIssues; @@ -11,7 +12,9 @@ use hashql_mir::{ context::MirContext, def::{DefId, DefIdSlice, DefIdVec}, intern::Interner, - pass::{Changed, GlobalTransformPass as _, transform::AdministrativeReduction}, + pass::{ + Changed, GlobalTransformPass as _, GlobalTransformState, transform::AdministrativeReduction, + }, }; use super::{ @@ -43,7 +46,12 @@ pub(crate) fn mir_pass_transform_administrative_reduction<'heap>( }; let mut pass = AdministrativeReduction::new_in(&mut scratch); - let _: Changed = pass.run(&mut context, &mut bodies); + let mut changed = IdVec::from_domain(Changed::No, &bodies); + let _: Changed = pass.run( + &mut context, + &mut GlobalTransformState::new(&mut changed), + &mut bodies, + ); process_issues(diagnostics, context.diagnostics)?; Ok((root, bodies, scratch)) diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inlining.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inlining.rs new file mode 100644 index 00000000000..904f54d7799 --- /dev/null +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inlining.rs @@ -0,0 +1,276 @@ +use std::io::{self, Write as _}; + +use hashql_ast::node::expr::Expr; +use hashql_core::{ + heap::{Heap, Scratch}, + r#type::environment::Environment, +}; +use hashql_diagnostics::DiagnosticIssues; +use hashql_mir::{ + body::Body, + context::MirContext, + def::{DefId, DefIdSlice, DefIdVec}, + intern::Interner, + pass::{Changed, GlobalTransformPass as _, GlobalTransformState, transform::PreInlining}, +}; + +use super::{RunContext, Suite, SuiteDiagnostic, common::process_issues, mir_reify::mir_reify}; +use crate::suite::{ + common::Header, + mir_reify::{d2_output_enabled, mir_format_d2, mir_format_text, mir_spawn_d2}, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) struct Stage { + id: &'static str, + title: &'static str, +} + +pub(crate) struct RenderContext<'env, 'heap> { + pub heap: &'heap Heap, + pub env: &'env Environment<'heap>, + pub stage: Stage, + pub root: DefId, +} + +pub(crate) trait MirRenderer { + fn render<'heap>( + &mut self, + context: &mut RenderContext<'_, 'heap>, + bodies: &DefIdSlice>, + ); +} + +impl MirRenderer for &mut R +where + R: MirRenderer, +{ + fn render<'heap>( + &mut self, + context: &mut RenderContext<'_, 'heap>, + bodies: &DefIdSlice>, + ) { + R::render(self, context, bodies); + } +} + +impl MirRenderer for Option +where + R: MirRenderer, +{ + fn render<'heap>( + &mut self, + context: &mut RenderContext<'_, 'heap>, + bodies: &DefIdSlice>, + ) { + if let Some(renderer) = self.as_mut() { + renderer.render(context, bodies); + } + } +} + +pub(crate) struct TextRenderer { + inner: W, + index: usize, +} + +impl TextRenderer { + pub(crate) const fn new(inner: W) -> Self { + Self { inner, index: 0 } + } +} + +impl MirRenderer for TextRenderer +where + W: io::Write, +{ + fn render<'heap>( + &mut self, + RenderContext { + heap, + env, + stage: Stage { + id: _, + title: header, + }, + root, + }: &mut RenderContext<'_, 'heap>, + bodies: &DefIdSlice>, + ) { + if self.index > 0 { + write!(self.inner, "\n\n").expect("should be able to write to buffer"); + } + + writeln!(self.inner, "{}\n", Header::new(header)) + .expect("should be able to write to buffer"); + mir_format_text(heap, env, &mut self.inner, *root, bodies); + self.index += 1; + } +} + +pub(crate) struct D2Renderer { + inner: W, +} + +impl D2Renderer { + pub(crate) const fn new(inner: W) -> Self { + Self { inner } + } +} + +impl MirRenderer for D2Renderer +where + W: io::Write, +{ + fn render<'heap>( + &mut self, + RenderContext { + heap, + env, + stage: Stage { id, title: header }, + root, + }: &mut RenderContext<'_, 'heap>, + bodies: &DefIdSlice>, + ) { + writeln!(self.inner, "{id}: '{header}' {{").expect("should be able to write to buffer"); + mir_format_d2(heap, env, &mut self.inner, *root, bodies); + writeln!(self.inner, "}}").expect("should be able to write to buffer"); + } +} + +impl MirRenderer for (A, B) +where + A: MirRenderer, + B: MirRenderer, +{ + #[expect(clippy::min_ident_chars)] + fn render<'heap>( + &mut self, + context: &mut RenderContext<'_, 'heap>, + bodies: &DefIdSlice>, + ) { + let (a, b) = self; + + a.render(context, bodies); + b.render(context, bodies); + } +} + +pub(crate) fn mir_pass_transform_pre_inlining<'heap>( + heap: &'heap Heap, + expr: Expr<'heap>, + interner: &Interner<'heap>, + mut render: impl MirRenderer, + environment: &mut Environment<'heap>, + diagnostics: &mut Vec, +) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { + let (root, mut bodies) = mir_reify(heap, expr, interner, environment, diagnostics)?; + + render.render( + &mut RenderContext { + heap, + env: environment, + stage: Stage { + id: "initial", + title: "Initial MIR", + }, + root, + }, + &bodies, + ); + + let mut context = MirContext { + heap, + env: environment, + interner, + diagnostics: DiagnosticIssues::new(), + }; + let mut scratch = Scratch::new(); + + let mut pass = PreInlining::new_in(&mut scratch); + let _: Changed = pass.run( + &mut context, + &mut GlobalTransformState::new_in(&bodies, heap), + &mut bodies, + ); + + process_issues(diagnostics, context.diagnostics)?; + + render.render( + &mut RenderContext { + heap, + env: environment, + stage: Stage { + id: "pre-inlining", + title: "Pre-inlining MIR", + }, + root, + }, + &bodies, + ); + + Ok((root, bodies, scratch)) +} + +pub(crate) struct MirPassTransformPreInlining; + +impl Suite for MirPassTransformPreInlining { + fn priority(&self) -> usize { + 1 + } + + fn secondary_file_extensions(&self) -> &[&str] { + &["svg"] + } + + fn name(&self) -> &'static str { + "mir/pass/transform/pre-inlining" + } + + fn description(&self) -> &'static str { + "Pre-inlining transformations in the MIR" + } + + fn run<'heap>( + &self, + RunContext { + heap, + diagnostics, + suite_directives, + reports, + secondary_outputs, + .. + }: RunContext<'_, 'heap>, + expr: Expr<'heap>, + ) -> Result { + let mut environment = Environment::new(heap); + let interner = Interner::new(heap); + + let mut buffer = Vec::new(); + let mut d2 = d2_output_enabled(self, suite_directives, reports).then(mir_spawn_d2); + + mir_pass_transform_pre_inlining( + heap, + expr, + &interner, + ( + TextRenderer::new(&mut buffer), + d2.as_mut().map(|(writer, _)| D2Renderer::new(writer)), + ), + &mut environment, + diagnostics, + )?; + + if let Some((mut writer, handle)) = d2 { + writer.flush().expect("should be able to write to buffer"); + drop(writer); + + let diagram = handle.join().expect("should be able to join handle"); + let diagram = String::from_utf8_lossy_owned(diagram); + + secondary_outputs.insert("svg", diagram); + } + + Ok(String::from_utf8_lossy_owned(buffer)) + } +} diff --git a/libs/@local/hashql/compiletest/src/suite/mod.rs b/libs/@local/hashql/compiletest/src/suite/mod.rs index 59c52e77412..94049a01e68 100644 --- a/libs/@local/hashql/compiletest/src/suite/mod.rs +++ b/libs/@local/hashql/compiletest/src/suite/mod.rs @@ -26,6 +26,7 @@ mod mir_pass_transform_cfg_simplify; mod mir_pass_transform_dse; mod mir_pass_transform_forward_substitution; mod mir_pass_transform_inst_simplify; +mod mir_pass_transform_pre_inlining; mod mir_reify; mod parse_syntax_dump; @@ -60,7 +61,8 @@ use self::{ mir_pass_transform_cfg_simplify::MirPassTransformCfgSimplify, mir_pass_transform_dse::MirPassTransformDse, mir_pass_transform_forward_substitution::MirPassTransformForwardSubstitution, - mir_pass_transform_inst_simplify::MirPassTransformInstSimplify, mir_reify::MirReifySuite, + mir_pass_transform_inst_simplify::MirPassTransformInstSimplify, + mir_pass_transform_pre_inlining::MirPassTransformPreInlining, mir_reify::MirReifySuite, parse_syntax_dump::ParseSyntaxDumpSuite, }; use crate::executor::TrialError; @@ -161,6 +163,7 @@ const SUITES: &[&dyn Suite] = &[ &MirPassTransformDse, &MirPassTransformForwardSubstitution, &MirPassTransformInstSimplify, + &MirPassTransformPreInlining, &MirReifySuite, &ParseSyntaxDumpSuite, ]; diff --git a/libs/@local/hashql/mir/benches/transform.rs b/libs/@local/hashql/mir/benches/transform.rs index fd36598ee20..19296ac7ef3 100644 --- a/libs/@local/hashql/mir/benches/transform.rs +++ b/libs/@local/hashql/mir/benches/transform.rs @@ -5,11 +5,12 @@ clippy::similar_names )] -use core::{cmp, hint::black_box}; +use core::hint::black_box; use codspeed_criterion_compat::{BatchSize, Bencher, Criterion, criterion_group, criterion_main}; use hashql_core::{ heap::{Heap, ResetAllocator as _, Scratch}, + id::IdSlice, r#type::{TypeBuilder, environment::Environment}, }; use hashql_diagnostics::DiagnosticIssues; @@ -17,11 +18,14 @@ use hashql_mir::{ body::Body, builder::BodyBuilder, context::MirContext, + def::DefId, intern::Interner, op, pass::{ - TransformPass, - transform::{CfgSimplify, DeadStoreElimination, ForwardSubstitution, InstSimplify}, + GlobalTransformPass as _, GlobalTransformState, TransformPass, + transform::{ + CfgSimplify, DeadStoreElimination, ForwardSubstitution, InstSimplify, PreInlining, + }, }, }; @@ -64,7 +68,9 @@ fn create_linear_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap> .assign_place(z, |rv| rv.binary(y, op![==], const_3)) .ret(z); - builder.finish(0, TypeBuilder::synthetic(env).integer()) + let mut body = builder.finish(0, TypeBuilder::synthetic(env).integer()); + body.id = DefId::new(0); + body } /// Creates a branching CFG body with a diamond pattern for benchmarking. @@ -113,7 +119,9 @@ fn create_diamond_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap .assign_place(result, |rv| rv.load(p)) .ret(result); - builder.finish(1, TypeBuilder::synthetic(env).integer()) + let mut body = builder.finish(1, TypeBuilder::synthetic(env).integer()); + body.id = DefId::new(0); + body } /// Creates a body with dead code for dead store elimination benchmarking. @@ -149,7 +157,9 @@ fn create_dead_store_cfg<'heap>( .assign_place(y, |rv| rv.binary(x, op![==], const_2)) .ret(y); - builder.finish(0, TypeBuilder::synthetic(env).integer()) + let mut body = builder.finish(0, TypeBuilder::synthetic(env).integer()); + body.id = DefId::new(0); + body } /// Creates a body with patterns that `InstSimplify` can optimize. @@ -205,7 +215,9 @@ fn create_inst_simplify_cfg<'heap>( .assign_place(f, |rv| rv.binary(e, op![&], d)) .ret(f); - builder.finish(0, TypeBuilder::synthetic(env).boolean()) + let mut body = builder.finish(0, TypeBuilder::synthetic(env).boolean()); + body.id = DefId::new(0); + body } /// Creates a larger CFG with multiple branches and join points for more realistic benchmarking. @@ -440,54 +452,39 @@ fn pipeline(criterion: &mut Criterion) { let mut scratch = Scratch::new(); run_bencher(bencher, create_linear_cfg, |context, body| { - let mut changed = CfgSimplify::new_in(&mut scratch).run(context, body); - changed = cmp::max( - changed, - ForwardSubstitution::new_in(&mut scratch).run(context, body), - ); - changed = cmp::max(changed, InstSimplify::new().run(context, body)); - changed = cmp::max( - changed, - DeadStoreElimination::new_in(&mut scratch).run(context, body), - ); - - changed + let bodies = IdSlice::from_raw_mut(core::slice::from_mut(body)); + + PreInlining::new_in(&mut scratch).run( + context, + &mut GlobalTransformState::new_in(bodies, context.heap), + bodies, + ) }); }); group.bench_function("diamond", |bencher| { let mut scratch = Scratch::new(); run_bencher(bencher, create_diamond_cfg, |context, body| { - let mut changed = CfgSimplify::new_in(&mut scratch).run(context, body); - changed = cmp::max( - changed, - ForwardSubstitution::new_in(&mut scratch).run(context, body), - ); - changed = cmp::max(changed, InstSimplify::new().run(context, body)); - changed = cmp::max( - changed, - DeadStoreElimination::new_in(&mut scratch).run(context, body), - ); - - changed + let bodies = IdSlice::from_raw_mut(core::slice::from_mut(body)); + + PreInlining::new_in(&mut scratch).run( + context, + &mut GlobalTransformState::new_in(bodies, context.heap), + bodies, + ) }); }); group.bench_function("complex", |bencher| { let mut scratch = Scratch::new(); run_bencher(bencher, create_complex_cfg, |context, body| { - let mut changed = CfgSimplify::new_in(&mut scratch).run(context, body); - changed = cmp::max( - changed, - ForwardSubstitution::new_in(&mut scratch).run(context, body), - ); - changed = cmp::max(changed, InstSimplify::new().run(context, body)); - changed = cmp::max( - changed, - DeadStoreElimination::new_in(&mut scratch).run(context, body), - ); - - changed + let bodies = IdSlice::from_raw_mut(core::slice::from_mut(body)); + + PreInlining::new_in(&mut scratch).run( + context, + &mut GlobalTransformState::new_in(bodies, context.heap), + bodies, + ) }); }); } diff --git a/libs/@local/hashql/mir/src/pass/mod.rs b/libs/@local/hashql/mir/src/pass/mod.rs index 2d5885332ce..00492cea510 100644 --- a/libs/@local/hashql/mir/src/pass/mod.rs +++ b/libs/@local/hashql/mir/src/pass/mod.rs @@ -17,9 +17,18 @@ //! - [`analysis`]: Static analysis infrastructure including dataflow analysis framework //! - [`transform`]: MIR transformation passes -use core::ops::{BitOr, BitOrAssign}; +use core::{ + alloc::Allocator, + ops::{BitOr, BitOrAssign}, +}; -use crate::{body::Body, context::MirContext, def::DefIdSlice}; +use hashql_core::heap::BumpAllocator; + +use crate::{ + body::Body, + context::MirContext, + def::{DefId, DefIdSlice, DefIdVec}, +}; pub mod analysis; pub mod transform; @@ -175,6 +184,118 @@ pub trait TransformPass<'env, 'heap> { } } +/// Owned storage for tracking per-body change status during global transformations. +/// +/// This type owns the underlying [`DefIdVec`] that tracks which bodies have been modified. +/// Use this when you need the change-tracking state to outlive a single pass invocation, +/// such as when running multiple passes in a fixpoint loop. +/// +/// For arena-allocated or short-lived state, prefer [`GlobalTransformState::new_in`] which +/// allocates directly from a bump allocator. +/// +/// # Example +/// +/// ```ignore +/// let mut state = OwnedGlobalTransformState::new_in(bodies, Global); +/// +/// loop { +/// let changed = pass.run(context, &mut state.as_mut(), bodies); +/// if changed == Changed::No { +/// break; +/// } +/// } +/// ``` +pub struct OwnedGlobalTransformState { + changed: DefIdVec, +} + +impl OwnedGlobalTransformState { + /// Creates a new owned state initialized to [`Changed::No`] for all bodies. + /// + /// The `bodies` parameter is used only to determine the domain size; the actual + /// body contents are not accessed. + pub fn new_in(bodies: &DefIdSlice, alloc: A) -> Self { + Self { + changed: DefIdVec::from_domain_in(Changed::No, bodies, alloc), + } + } + + /// Returns a borrowed [`GlobalTransformState`] view of this owned state. + /// + /// This allows passing the state to [`GlobalTransformPass::run`] while retaining + /// ownership for subsequent iterations. + pub fn as_mut(&mut self) -> GlobalTransformState<'_> { + GlobalTransformState::new(&mut self.changed) + } +} + +/// Tracks per-body change status during a [`GlobalTransformPass`] execution. +/// +/// This type provides a borrowed view into change-tracking storage, allowing passes to +/// record which bodies they modified. The storage can come from either: +/// +/// - An [`OwnedGlobalTransformState`] via [`as_mut`](OwnedGlobalTransformState::as_mut) +/// - A bump allocator via [`new_in`](Self::new_in) +/// - An existing mutable slice via [`new`](Self::new) +/// +/// # Usage in Passes +/// +/// Global passes receive this as a parameter and should call [`mark`](Self::mark) whenever +/// they modify a body: +/// +/// ```ignore +/// fn run( +/// &mut self, +/// context: &mut MirContext<'env, 'heap>, +/// state: &mut GlobalTransformState<'_>, +/// bodies: &mut DefIdSlice>, +/// ) -> Changed { +/// for (id, body) in bodies.iter_enumerated_mut() { +/// if self.transform(body) { +/// state.mark(id, Changed::Yes); +/// } +/// } +/// Changed::Yes +/// } +/// ``` +pub struct GlobalTransformState<'ctx> { + changed: &'ctx mut DefIdSlice, +} + +impl<'ctx> GlobalTransformState<'ctx> { + /// Creates a new state from an existing mutable slice. + /// + /// The slice must be pre-sized to match the number of bodies being processed. + pub const fn new(changed: &'ctx mut DefIdSlice) -> Self { + Self { changed } + } + + /// Creates a new state by allocating from a bump allocator. + /// + /// The allocated storage is initialized to [`Changed::No`] for all bodies. The `bodies` + /// parameter is used only to determine the domain size; the actual body contents are + /// not accessed. + /// + /// This is useful when the state only needs to live for a single pass invocation and + /// can be discarded when the allocator is reset. + pub fn new_in(bodies: &DefIdSlice, alloc: &'ctx A) -> Self { + let uninit_slice = alloc.allocate_slice_uninit(bodies.len()); + let changed = uninit_slice.write_filled(Changed::No); + + Self { + changed: DefIdSlice::from_raw_mut(changed), + } + } + + /// Records that the body with the given [`DefId`] has changed. + /// + /// This uses `|=` semantics, so marking a body as [`Changed::Yes`] will not be + /// downgraded by a subsequent [`Changed::No`] mark. + pub fn mark(&mut self, id: DefId, changed: Changed) { + self.changed[id] |= changed; + } +} + /// A global transformation pass over MIR. /// /// Unlike [`TransformPass`] which operates on a single [`Body`], global passes have access to @@ -217,6 +338,7 @@ pub trait GlobalTransformPass<'env, 'heap> { fn run( &mut self, context: &mut MirContext<'env, 'heap>, + state: &mut GlobalTransformState<'_>, bodies: &mut DefIdSlice>, ) -> Changed; diff --git a/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/mod.rs b/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/mod.rs index bd81cfb4c64..8117fa4d7fa 100644 --- a/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/mod.rs @@ -71,7 +71,7 @@ use crate::{ context::MirContext, def::{DefId, DefIdSlice, DefIdVec}, pass::{ - Changed, GlobalTransformPass, TransformPass, analysis::CallGraph, + Changed, GlobalTransformPass, GlobalTransformState, TransformPass, analysis::CallGraph, transform::copy_propagation::propagate_block_params, }, visit::VisitorMut as _, @@ -116,6 +116,10 @@ impl Reducable { fn contains(&self, id: DefId) -> bool { self.inner.contains(id) } + + fn is_empty(&self) -> bool { + self.inner.is_empty() + } } /// Pre-allocated scratch space reused across per-body transformations. @@ -195,13 +199,18 @@ impl<'env, 'heap, A: ResetAllocator> GlobalTransformPass<'env, 'heap> fn run( &mut self, context: &mut MirContext<'env, 'heap>, + state: &mut GlobalTransformState<'_>, bodies: &mut DefIdSlice>, ) -> Changed { self.alloc.reset(); + let mut reducable = Reducable::new(bodies, &self.alloc); + if reducable.is_empty() { + return Changed::No; + } + // Build the call graph (edges: caller → callee) and seed the reducibility set. let callgraph = CallGraph::analyze_in(bodies, &self.alloc); - let mut reducable = Reducable::new(bodies, &self.alloc); // Compute DFS postorder over the call graph. Since edges go caller → callee, postorder // yields callees before callers. This ensures that when we process a caller, all its @@ -229,6 +238,7 @@ impl<'env, 'heap, A: ResetAllocator> GlobalTransformPass<'env, 'heap> let body_changed = pass.run(context, body); changed |= body_changed; + state.mark(id, body_changed); // If this body was transformed and wasn't already reducible, reclassify it. // This enables callers (processed later in postorder) to reduce calls to this body. diff --git a/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/tests.rs b/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/tests.rs index e565d1fb8fd..b9c4d372484 100644 --- a/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/tests.rs +++ b/libs/@local/hashql/mir/src/pass/transform/administrative_reduction/tests.rs @@ -18,7 +18,7 @@ use crate::{ context::MirContext, def::{DefId, DefIdSlice}, intern::Interner, - pass::{Changed, GlobalTransformPass as _}, + pass::{Changed, GlobalTransformPass as _, GlobalTransformState}, pretty::TextFormat, }; @@ -228,7 +228,11 @@ fn self_recursion_blocked() { let mut bodies = [body]; let mut pass = AdministrativeReduction::new_in(Scratch::new()); - let changed = pass.run(&mut context, DefIdSlice::from_raw_mut(&mut bodies)); + let changed = pass.run( + &mut context, + &mut GlobalTransformState::new(DefIdSlice::from_raw_mut(&mut [Changed::No])), + DefIdSlice::from_raw_mut(&mut bodies), + ); assert_eq!(changed, Changed::No); } @@ -261,7 +265,11 @@ fn assert_admin_reduction_pass<'heap>( .expect("should be able to write bodies"); let mut pass = AdministrativeReduction::new_in(Scratch::new()); - let changed = pass.run(context, DefIdSlice::from_raw_mut(bodies)); + let changed = pass.run( + context, + &mut GlobalTransformState::new_in(DefIdSlice::from_raw(bodies), context.heap), + DefIdSlice::from_raw_mut(bodies), + ); write!( text_format.writer, diff --git a/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs index 3e545a6d420..3c986418d26 100644 --- a/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs @@ -412,18 +412,28 @@ impl CfgSimplify { body: &mut Body<'heap>, id: BasicBlockId, ) -> bool { + let kind = &body.basic_blocks[id].terminator.kind; + match kind { + &TerminatorKind::Goto(_) | TerminatorKind::SwitchInt(_) => {} + TerminatorKind::Return(_) + | TerminatorKind::GraphRead(_) + | TerminatorKind::Unreachable => return false, + } + // Snapshot reachable blocks before modification to detect newly dead blocks. + // This is done *after* we check the terminator, to ensure that we don't recompute postorder + // if we don't need to. let previous_reverse_postorder = body .basic_blocks .reverse_postorder() .transfer_into(&self.alloc); - let changed = match &body.basic_blocks[id].terminator.kind { + let changed = match kind { &TerminatorKind::Goto(goto) => Self::simplify_goto(body, id, goto), TerminatorKind::SwitchInt(_) => Self::simplify_switch_int(context, body, id), TerminatorKind::Return(_) | TerminatorKind::GraphRead(_) - | TerminatorKind::Unreachable => false, + | TerminatorKind::Unreachable => unreachable!(), }; if changed { diff --git a/libs/@local/hashql/mir/src/pass/transform/mod.rs b/libs/@local/hashql/mir/src/pass/transform/mod.rs index 1ba3a54b014..8fd04d8dde9 100644 --- a/libs/@local/hashql/mir/src/pass/transform/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/mod.rs @@ -7,11 +7,12 @@ mod dse; pub mod error; mod forward_substitution; mod inst_simplify; +mod pre_inlining; mod ssa_repair; pub use self::{ administrative_reduction::AdministrativeReduction, cfg_simplify::CfgSimplify, copy_propagation::CopyPropagation, dbe::DeadBlockElimination, dle::DeadLocalElimination, dse::DeadStoreElimination, forward_substitution::ForwardSubstitution, - inst_simplify::InstSimplify, ssa_repair::SsaRepair, + inst_simplify::InstSimplify, pre_inlining::PreInlining, ssa_repair::SsaRepair, }; diff --git a/libs/@local/hashql/mir/src/pass/transform/pre_inlining.rs b/libs/@local/hashql/mir/src/pass/transform/pre_inlining.rs new file mode 100644 index 00000000000..6f179200c6c --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/transform/pre_inlining.rs @@ -0,0 +1,262 @@ +//! Pre-inlining optimization pass. +//! +//! This module contains the [`PreInlining`] pass, which runs a fixpoint loop of local and global +//! transformations to optimize MIR bodies before inlining occurs. + +use alloc::alloc::Global; +use core::alloc::Allocator; + +use hashql_core::{heap::ResetAllocator, id::bit_vec::DenseBitSet}; + +use super::{ + AdministrativeReduction, CfgSimplify, DeadStoreElimination, ForwardSubstitution, InstSimplify, +}; +use crate::{ + body::Body, + context::MirContext, + def::{DefId, DefIdSlice, DefIdVec}, + pass::{ + Changed, GlobalTransformPass, GlobalTransformState, TransformPass, + transform::CopyPropagation, + }, +}; + +/// Pre-inlining optimization driver. +/// +/// This pass orchestrates a sequence of local and global transformations in a fixpoint loop, +/// preparing MIR bodies for inlining. By running these optimizations before inlining, we ensure +/// that: +/// +/// - Inlined code is already simplified, reducing work after inlining +/// - Call sites see optimized callees, enabling better inlining decisions +/// - The overall MIR size is reduced before the potential code explosion from inlining +/// +/// # Pass Ordering +/// +/// The pass ordering is carefully chosen so each pass feeds the next with new opportunities: +/// +/// 1. **Administrative reduction** - Removes structural clutter and normalizes shape +/// 2. **Instruction simplification** - Constant folding and algebraic simplification +/// 3. **Value propagation** (FS/CP alternating) - Propagates values through the code +/// 4. **Dead store elimination** - Removes stores made dead by propagation +/// 5. **CFG simplification** - Cleans up control flow after local changes +/// +/// # Implementation Notes +/// +/// This pass manages its own per-body change tracking and does not populate the caller-provided +/// [`GlobalTransformState`]. Callers receive a combined [`Changed`] result indicating whether any +/// body was modified. +pub struct PreInlining { + alloc: A, +} + +impl PreInlining { + /// Creates a new pre-inlining pass with the given allocator. + /// + /// The allocator is used for temporary data structures within sub-passes and is reset + /// between pass invocations. + pub const fn new_in(alloc: A) -> Self { + Self { alloc } + } + + /// Runs a local transform pass on all unstable bodies. + /// + /// Only bodies in the `unstable` set are processed. The `state` slice is updated to track + /// which bodies were modified. + fn run_local_pass<'env, 'heap>( + context: &mut MirContext<'env, 'heap>, + bodies: &mut DefIdSlice>, + mut pass: impl TransformPass<'env, 'heap>, + unstable: &DenseBitSet, + state: &mut DefIdSlice, + ) -> Changed { + let mut changed = Changed::No; + + for (id, body) in bodies.iter_enumerated_mut() { + if !unstable.contains(id) { + continue; + } + + let result = pass.run(context, body); + changed |= result; + state[id] |= result; + } + + changed + } + + /// Runs a global transform pass on all bodies. + /// + /// Unlike local passes, global passes have access to all bodies and can perform + /// inter-procedural transformations. The `state` slice is updated by the pass to track + /// which bodies were modified. + fn run_global_pass<'env, 'heap>( + context: &mut MirContext<'env, 'heap>, + bodies: &mut DefIdSlice>, + mut pass: impl GlobalTransformPass<'env, 'heap>, + + state: &mut DefIdSlice, + ) -> Changed { + pass.run(context, &mut GlobalTransformState::new(state), bodies) + } + + fn copy_propagation<'heap>( + &mut self, + context: &mut MirContext<'_, 'heap>, + bodies: &mut DefIdSlice>, + unstable: &DenseBitSet, + state: &mut DefIdSlice, + ) -> Changed { + let pass = CopyPropagation::new_in(&mut self.alloc); + Self::run_local_pass(context, bodies, pass, unstable, state) + } + + fn cfg_simplify<'heap>( + &mut self, + context: &mut MirContext<'_, 'heap>, + bodies: &mut DefIdSlice>, + unstable: &DenseBitSet, + state: &mut DefIdSlice, + ) -> Changed { + let pass = CfgSimplify::new_in(&mut self.alloc); + Self::run_local_pass(context, bodies, pass, unstable, state) + } + + fn inst_simplify<'heap>( + &mut self, + context: &mut MirContext<'_, 'heap>, + bodies: &mut DefIdSlice>, + unstable: &DenseBitSet, + state: &mut DefIdSlice, + ) -> Changed { + let pass = InstSimplify::new_in(&mut self.alloc); + Self::run_local_pass(context, bodies, pass, unstable, state) + } + + fn forward_substitution<'heap>( + &mut self, + context: &mut MirContext<'_, 'heap>, + bodies: &mut DefIdSlice>, + unstable: &DenseBitSet, + state: &mut DefIdSlice, + ) -> Changed { + let pass = ForwardSubstitution::new_in(&mut self.alloc); + Self::run_local_pass(context, bodies, pass, unstable, state) + } + + fn administrative_reduction<'heap>( + &mut self, + context: &mut MirContext<'_, 'heap>, + bodies: &mut DefIdSlice>, + + state: &mut DefIdSlice, + ) -> Changed { + let pass = AdministrativeReduction::new_in(&mut self.alloc); + Self::run_global_pass(context, bodies, pass, state) + } + + fn dse<'heap>( + &mut self, + context: &mut MirContext<'_, 'heap>, + bodies: &mut DefIdSlice>, + unstable: &DenseBitSet, + state: &mut DefIdSlice, + ) -> Changed { + let pass = DeadStoreElimination::new_in(&mut self.alloc); + Self::run_local_pass(context, bodies, pass, unstable, state) + } +} + +const MAX_ITERATIONS: usize = 16; + +impl<'env, 'heap, A: ResetAllocator> GlobalTransformPass<'env, 'heap> for PreInlining { + #[expect(clippy::integer_division_remainder_used)] + fn run( + &mut self, + context: &mut MirContext<'env, 'heap>, + _: &mut GlobalTransformState<'_>, + bodies: &mut DefIdSlice>, + ) -> Changed { + self.alloc.reset(); + + // We would be able to move this to the scratch space, if we only had proper checkpointing + // support. + let mut state = DefIdVec::from_domain_in(Changed::No, bodies, Global); + let mut unstable = DenseBitSet::new_filled(bodies.len()); + + // Pre-pass: run CP + CFG once before the fixpoint loop. + // + // Both passes are cheap and effective on obvious cases (e.g., `if true { ... } else { ... + // }`). CP exposes constant conditions; CFG then prunes unreachable blocks and + // merges straight-line code. This shrinks the MIR upfront so more expensive passes + // run on smaller, cleaner bodies. + let mut global_changed = Changed::No; + global_changed |= self.copy_propagation(context, bodies, &unstable, &mut state); + global_changed |= self.cfg_simplify(context, bodies, &unstable, &mut state); + + let mut iter = 0; + loop { + if iter >= MAX_ITERATIONS { + break; + } + + // Reset per-iteration state to track which bodies change in this iteration only. + state.as_raw_mut().fill(Changed::No); + + // The pass ordering is chosen so each pass feeds the next with new opportunities: + // + // 1. AR: Removes structural clutter (unnecessary wrappers, trivial blocks/calls) and + // normalizes shape, exposing simpler instructions for later passes. + // 2. IS: Simplifies individual instructions (constant folding, algebraic + // simplification) given the cleaner structure, producing canonical RHS values ideal + // for propagation. + // 3. FS / CP: Propagates values through the code, eliminating temporaries. After + // propagation, many stores become unused. + // 4. DSE: Removes stores made dead by propagation. Dropping these often empties blocks. + // 5. CS: Cleans up CFG after local changes (empty blocks, unconditional edges), + // producing a minimal CFG that maximizes the next iteration's effectiveness. + + let mut changed = Changed::No; + changed |= self.administrative_reduction(context, bodies, &mut state); + changed |= self.inst_simplify(context, bodies, &unstable, &mut state); + + // FS vs CP strategy: ForwardSubstitution is more powerful but expensive; + // CopyPropagation is cheaper but weaker. We start with FS (iter=0) to + // aggressively expose the biggest opportunities early when there's most + // redundancy. Subsequent iterations alternate: CP maintains propagation + // cheaply, while periodic FS picks up deeper opportunities. + changed |= if iter % 2 == 0 { + self.forward_substitution(context, bodies, &unstable, &mut state) + } else { + self.copy_propagation(context, bodies, &unstable, &mut state) + }; + + changed |= self.dse(context, bodies, &unstable, &mut state); + changed |= self.cfg_simplify(context, bodies, &unstable, &mut state); + + global_changed |= changed; + if changed == Changed::No { + break; + } + + // Update the unstable set based on this iteration's results. Bodies that had no changes + // are removed (monotonically decreasing), but global passes may re-add bodies by + // creating new optimization opportunities in previously stable functions. + for (id, &changed) in state.iter_enumerated() { + if changed == Changed::No { + unstable.remove(id); + } else { + unstable.insert(id); + } + } + + if unstable.is_empty() { + break; + } + + iter += 1; + } + + global_changed + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/.spec.toml b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/.spec.toml new file mode 100644 index 00000000000..e9b97f05fab --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/.spec.toml @@ -0,0 +1 @@ +suite = "mir/pass/transform/pre-inlining" diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/basic-constant-folding.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/basic-constant-folding.jsonc new file mode 100644 index 00000000000..3b766c53ae4 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/basic-constant-folding.jsonc @@ -0,0 +1,5 @@ +//@ run: pass +//@ description: Pre-pass CP + CFG folds constant branch and simplifies control flow +// The `if true` should be folded away by the initial CP + CFG simplification pass, +// leaving only the "then" branch. +["if", { "#literal": true }, { "#literal": "then" }, { "#literal": "else" }] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/basic-constant-folding.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/basic-constant-folding.stdout new file mode 100644 index 00000000000..2c28db7b180 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/basic-constant-folding.stdout @@ -0,0 +1,29 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#0}() -> String { + let %0: String + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + goto -> bb3("then") + } + + bb2(): { + goto -> bb3("else") + } + + bb3(%0): { + return %0 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +*thunk {thunk#0}() -> String { + bb0(): { + return "then" + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/chain-simplification.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/chain-simplification.jsonc new file mode 100644 index 00000000000..4ea85915175 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/chain-simplification.jsonc @@ -0,0 +1,30 @@ +//@ run: pass +//@ description: Cascading conditionals require multiple fixpoint iterations to fully simplify +// Each conditional depends on the result of the previous one being simplified. +// The loop order is: AR → InstSimplify → FS/CP → DSE → CFG +// Since FS runs before CFG, each iteration can only propagate values that were +// already constants at the start of that iteration: +// Iter 1: FS propagates `a = true`, CFG folds first if → `b = true` +// Iter 2: FS propagates `b = true`, CFG folds second if → `c = true` +// Iter 3: FS propagates `c = true`, CFG folds final if → "yes" +[ + "if", + { "#literal": true }, + [ + "let", + "a", + { "#literal": true }, + [ + "let", + "b", + ["if", "a", { "#literal": true }, { "#literal": false }], + [ + "let", + "c", + ["if", "b", { "#literal": true }, { "#literal": false }], + ["if", "c", { "#literal": "yes" }, { "#literal": "no" }] + ] + ] + ], + { "#literal": 0 } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/chain-simplification.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/chain-simplification.stdout new file mode 100644 index 00000000000..f11d588671d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/chain-simplification.stdout @@ -0,0 +1,71 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#4}() -> String | Integer { + let %0: String | Integer + let %1: Boolean + let %2: Boolean + let %3: Boolean + let %4: String + + bb0(): { + switchInt(1) -> [0: bb11(), 1: bb1()] + } + + bb1(): { + %1 = 1 + + switchInt(%1) -> [0: bb3(), 1: bb2()] + } + + bb2(): { + goto -> bb4(1) + } + + bb3(): { + goto -> bb4(0) + } + + bb4(%2): { + switchInt(%2) -> [0: bb6(), 1: bb5()] + } + + bb5(): { + goto -> bb7(1) + } + + bb6(): { + goto -> bb7(0) + } + + bb7(%3): { + switchInt(%3) -> [0: bb9(), 1: bb8()] + } + + bb8(): { + goto -> bb10("yes") + } + + bb9(): { + goto -> bb10("no") + } + + bb10(%4): { + goto -> bb12(%4) + } + + bb11(): { + goto -> bb12(0) + } + + bb12(%0): { + return %0 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +*thunk {thunk#4}() -> String | Integer { + bb0(): { + return "yes" + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/closure-with-dead-branch.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/closure-with-dead-branch.jsonc new file mode 100644 index 00000000000..7051e025587 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/closure-with-dead-branch.jsonc @@ -0,0 +1,15 @@ +//@ run: pass +//@ description: Administrative reduction inlines closure, CFG simplifies dead branch +// A closure that returns a constant is defined and called with a constant condition. +// AR inlines the closure, then CP + CFG simplifies the constant branch. +[ + "let", + "identity", + ["fn", { "#tuple": [] }, { "#struct": { "x": "Boolean" } }, "_", "x"], + [ + "if", + ["identity", { "#literal": true }], + { "#literal": "yes" }, + { "#literal": "no" } + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/closure-with-dead-branch.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/closure-with-dead-branch.stdout new file mode 100644 index 00000000000..3c0cc3a0675 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/closure-with-dead-branch.stdout @@ -0,0 +1,86 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#4}(%0: (), %1: Boolean) -> Boolean { + bb0(): { + return %1 + } +} + +thunk identity:0() -> (Boolean) -> Boolean { + let %0: (Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#4} as FnPtr), %1) + + return %0 + } +} + +thunk {thunk#2}() -> Boolean { + let %0: (Boolean) -> Boolean + let %1: Boolean + + bb0(): { + %0 = apply (identity:0 as FnPtr) + %1 = apply %0.0 %0.1 1 + + return %1 + } +} + +*thunk {thunk#3}() -> String { + let %0: Boolean + let %1: String + + bb0(): { + %0 = apply ({thunk#2} as FnPtr) + + switchInt(%0) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + goto -> bb3("yes") + } + + bb2(): { + goto -> bb3("no") + } + + bb3(%1): { + return %1 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {closure#4}(%0: (), %1: Boolean) -> Boolean { + bb0(): { + return %1 + } +} + +thunk identity:0() -> (Boolean) -> Boolean { + let %0: (Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#4} as FnPtr), %1) + + return %0 + } +} + +thunk {thunk#2}() -> Boolean { + bb0(): { + return 1 + } +} + +*thunk {thunk#3}() -> String { + bb0(): { + return "yes" + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/dead-code-after-propagation.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/dead-code-after-propagation.jsonc new file mode 100644 index 00000000000..3ce016a7d2e --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/dead-code-after-propagation.jsonc @@ -0,0 +1,11 @@ +//@ run: pass +//@ description: Forward substitution propagates value, DSE eliminates dead store +// The value of `x` is propagated to the return, making `dead` unused. +// FS propagates `x` into the result, then DSE removes the dead assignment to `dead`. +// Wrapped in `if true` to keep as a single body rather than separate thunks. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, ["let", "dead", { "#literal": 100 }, "x"]], + { "#literal": 0 } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/dead-code-after-propagation.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/dead-code-after-propagation.stdout new file mode 100644 index 00000000000..5fb3924f24d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/dead-code-after-propagation.stdout @@ -0,0 +1,34 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Integer { + let %0: Integer + let %1: Integer + let %2: Integer + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = 100 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Integer { + bb0(): { + return 42 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/inst-simplify-with-propagation.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/inst-simplify-with-propagation.jsonc new file mode 100644 index 00000000000..2290fe08fbf --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/inst-simplify-with-propagation.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Instruction simplification combined with value propagation +// A let binding stores a value that is used in a comparison. +// InstSimplify can fold the comparison after FS propagates the constant. +[ + "let", + "x", + { "#literal": true }, + ["if", "x", { "#literal": "was-true" }, { "#literal": "was-false" }] +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/inst-simplify-with-propagation.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/inst-simplify-with-propagation.stdout new file mode 100644 index 00000000000..896c41dacba --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/inst-simplify-with-propagation.stdout @@ -0,0 +1,44 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +thunk x:0() -> Boolean { + bb0(): { + return 1 + } +} + +*thunk {thunk#1}() -> String { + let %0: Boolean + let %1: String + + bb0(): { + %0 = apply (x:0 as FnPtr) + + switchInt(%0) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + goto -> bb3("was-true") + } + + bb2(): { + goto -> bb3("was-false") + } + + bb3(%1): { + return %1 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +thunk x:0() -> Boolean { + bb0(): { + return 1 + } +} + +*thunk {thunk#1}() -> String { + bb0(): { + return "was-true" + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-if-constant.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-if-constant.jsonc new file mode 100644 index 00000000000..45555c5c50a --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-if-constant.jsonc @@ -0,0 +1,20 @@ +//@ run: pass +//@ description: Nested constant conditionals are fully simplified +// Multiple levels of constant if-then-else that should all fold away. +// Tests that CFG simplification works across multiple iterations. +[ + "if", + { "#literal": true }, + [ + "if", + { "#literal": false }, + { "#literal": "unreachable" }, + [ + "if", + { "#literal": true }, + { "#literal": "result" }, + { "#literal": "also-unreachable" } + ] + ], + { "#literal": "outer-else" } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-if-constant.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-if-constant.stdout new file mode 100644 index 00000000000..de1185c8bc2 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-if-constant.stdout @@ -0,0 +1,55 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> String { + let %0: String + let %1: String + let %2: String + + bb0(): { + switchInt(1) -> [0: bb8(), 1: bb1()] + } + + bb1(): { + switchInt(0) -> [0: bb2(), 1: bb5()] + } + + bb2(): { + switchInt(1) -> [0: bb4(), 1: bb3()] + } + + bb3(): { + goto -> bb6("result") + } + + bb4(): { + goto -> bb6("also-unreachable") + } + + bb5(): { + goto -> bb7("unreachable") + } + + bb6(%2): { + goto -> bb7(%2) + } + + bb7(%1): { + goto -> bb9(%1) + } + + bb8(): { + goto -> bb9("outer-else") + } + + bb9(%0): { + return %0 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> String { + bb0(): { + return "result" + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-let-cleanup.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-let-cleanup.jsonc new file mode 100644 index 00000000000..1c73cbcf532 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-let-cleanup.jsonc @@ -0,0 +1,26 @@ +//@ run: pass +//@ description: Nested let bindings with dead stores are cleaned up by CP + DSE + CFG +// Multiple nested let bindings where some values are unused. +// CP propagates used values, DSE removes dead stores, CFG cleans up the result. +// Wrapped in `if true` to keep as a single body rather than separate thunks. +[ + "if", + { "#literal": true }, + [ + "let", + "a", + { "#literal": 1 }, + [ + "let", + "b", + { "#literal": 2 }, + [ + "let", + "c", + { "#literal": 3 }, + ["let", "unused1", "a", ["let", "unused2", "b", "c"]] + ] + ] + ], + { "#literal": 0 } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-let-cleanup.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-let-cleanup.stdout new file mode 100644 index 00000000000..152ef88be46 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/nested-let-cleanup.stdout @@ -0,0 +1,36 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#5}() -> Integer { + let %0: Integer + let %1: Integer + let %2: Integer + let %3: Integer + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 + %2 = 2 + %3 = 3 + + goto -> bb3(%3) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +*thunk {thunk#5}() -> Integer { + bb0(): { + return 3 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/thunk-with-dead-code.jsonc b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/thunk-with-dead-code.jsonc new file mode 100644 index 00000000000..7f027fec2e2 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/thunk-with-dead-code.jsonc @@ -0,0 +1,20 @@ +//@ run: pass +//@ description: Thunk inlining combined with dead code elimination +// A thunk (zero-argument closure) is called, and its result used in a branch. +// AR inlines the thunk, then the constant propagates and dead code is eliminated. +[ + "let", + "get_flag", + ["fn", { "#tuple": [] }, { "#struct": {} }, "_", { "#literal": true }], + [ + "let", + "flag", + ["get_flag"], + [ + "let", + "unused", + { "#literal": "dead" }, + ["if", "flag", { "#literal": "active" }, { "#literal": "inactive" }] + ] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/thunk-with-dead-code.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/thunk-with-dead-code.stdout new file mode 100644 index 00000000000..76085b26b55 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inlining/thunk-with-dead-code.stdout @@ -0,0 +1,98 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#4}(%0: ()) -> Boolean { + bb0(): { + return 1 + } +} + +thunk get_flag:0() -> () -> Boolean { + let %0: () -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#4} as FnPtr), %1) + + return %0 + } +} + +thunk flag:0() -> Boolean { + let %0: () -> Boolean + let %1: Boolean + + bb0(): { + %0 = apply (get_flag:0 as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk unused:0() -> String { + bb0(): { + return "dead" + } +} + +*thunk {thunk#3}() -> String { + let %0: Boolean + let %1: String + + bb0(): { + %0 = apply (flag:0 as FnPtr) + + switchInt(%0) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + goto -> bb3("active") + } + + bb2(): { + goto -> bb3("inactive") + } + + bb3(%1): { + return %1 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {closure#4}(%0: ()) -> Boolean { + bb0(): { + return 1 + } +} + +thunk get_flag:0() -> () -> Boolean { + let %0: () -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#4} as FnPtr), %1) + + return %0 + } +} + +thunk flag:0() -> Boolean { + bb0(): { + return 1 + } +} + +thunk unused:0() -> String { + bb0(): { + return "dead" + } +} + +*thunk {thunk#3}() -> String { + bb0(): { + return "active" + } +} \ No newline at end of file