From db91c17093e60fd29b81a3fe873aed556f985bad Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Thu, 18 Dec 2025 16:17:00 +0100 Subject: [PATCH 01/14] feat: create file --- libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs | 1 + libs/@local/hashql/mir/src/pass/transform/mod.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -0,0 +1 @@ + diff --git a/libs/@local/hashql/mir/src/pass/transform/mod.rs b/libs/@local/hashql/mir/src/pass/transform/mod.rs index c191f3a3040..35179d1d5bc 100644 --- a/libs/@local/hashql/mir/src/pass/transform/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/mod.rs @@ -3,6 +3,7 @@ mod dbe; mod dle; mod dse; pub mod error; +mod inst_simplify; mod sroa; mod ssa_repair; From 40eedbbc16966d947c714424f7e1af4b1f722593 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Thu, 18 Dec 2025 17:02:04 +0100 Subject: [PATCH 02/14] fix: doctests --- libs/@local/hashql/mir/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@local/hashql/mir/src/builder.rs b/libs/@local/hashql/mir/src/builder.rs index ac9d90b0d63..63130b64abc 100644 --- a/libs/@local/hashql/mir/src/builder.rs +++ b/libs/@local/hashql/mir/src/builder.rs @@ -113,7 +113,7 @@ macro_rules! scaffold { /// assert!(matches!(op![>], BinOp::Gt)); /// assert!(matches!(op![>=], BinOp::Gte)); /// -/// // Logical +/// // Bitwise /// assert!(matches!(op![&], BinOp::BitAnd)); /// assert!(matches!(op![|], BinOp::BitOr)); /// ``` From 39a1477125864ea16c5a7f5d14e5482916f7f430 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Thu, 18 Dec 2025 17:03:39 +0100 Subject: [PATCH 03/14] chore: reorder --- libs/@local/hashql/mir/src/builder.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/@local/hashql/mir/src/builder.rs b/libs/@local/hashql/mir/src/builder.rs index 63130b64abc..87d87dd14ee 100644 --- a/libs/@local/hashql/mir/src/builder.rs +++ b/libs/@local/hashql/mir/src/builder.rs @@ -105,6 +105,10 @@ macro_rules! scaffold { /// use hashql_mir::body::rvalue::BinOp; /// use hashql_mir::op; /// +/// // Bitwise +/// assert!(matches!(op![&], BinOp::BitAnd)); +/// assert!(matches!(op![|], BinOp::BitOr)); +/// /// // Comparison /// assert!(matches!(op![==], BinOp::Eq)); /// assert!(matches!(op![!=], BinOp::Ne)); @@ -112,10 +116,6 @@ macro_rules! scaffold { /// assert!(matches!(op![<=], BinOp::Lte)); /// assert!(matches!(op![>], BinOp::Gt)); /// assert!(matches!(op![>=], BinOp::Gte)); -/// -/// // Bitwise -/// assert!(matches!(op![&], BinOp::BitAnd)); -/// assert!(matches!(op![|], BinOp::BitOr)); /// ``` /// /// Arithmetic operators are also available (`op![+]`, `op![-]`, `op![*]`, `op![/]`), From 02078e60eec9c715698ff14ce1c2dc6ab37582a2 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 16:46:24 +0100 Subject: [PATCH 04/14] feat: checkpoint --- libs/@local/hashql/core/src/id/vec.rs | 21 ++ libs/@local/hashql/mir/src/body/place.rs | 4 +- .../src/pass/transform/inst_simplify/mod.rs | 281 ++++++++++++++++++ 3 files changed, 304 insertions(+), 2 deletions(-) diff --git a/libs/@local/hashql/core/src/id/vec.rs b/libs/@local/hashql/core/src/id/vec.rs index 294d5fd669a..fd6e02413c2 100644 --- a/libs/@local/hashql/core/src/id/vec.rs +++ b/libs/@local/hashql/core/src/id/vec.rs @@ -8,8 +8,10 @@ use core::{ marker::PhantomData, ops::{Deref, DerefMut}, }; +use std::alloc::AllocError; use super::{Id, slice::IdSlice}; +use crate::heap::{CloneIn, TryCloneIn}; /// A growable vector that uses typed IDs for indexing instead of raw `usize` values. /// @@ -558,3 +560,22 @@ where Self::new() } } + +impl TryCloneIn for IdVec +where + I: Id, + T: Clone, + A: Allocator, + B: Allocator, +{ + type Cloned = IdVec; + + #[inline] + fn try_clone_in(&self, allocator: B) -> Result { + self.raw.try_clone_in(allocator).map(IdVec::from_raw) + } + + fn try_clone_into(&self, into: &mut Self::Cloned, allocator: B) -> Result<(), AllocError> { + self.raw.try_clone_into(&mut into.raw, allocator) + } +} diff --git a/libs/@local/hashql/mir/src/body/place.rs b/libs/@local/hashql/mir/src/body/place.rs index 3b1ef99bc95..124b559d37f 100644 --- a/libs/@local/hashql/mir/src/body/place.rs +++ b/libs/@local/hashql/mir/src/body/place.rs @@ -12,7 +12,7 @@ use core::{ use hashql_core::{id, intern::Interned, symbol::Symbol, r#type::TypeId}; -use super::local::{Local, LocalDecl, LocalVec}; +use super::local::{Local, LocalDecl, LocalSlice, LocalVec}; use crate::intern::Interner; id::newtype!( @@ -378,7 +378,7 @@ impl<'heap> Place<'heap> { } /// Return the type of the place after applying all projections. - pub fn type_id(&self, decl: &LocalVec, A>) -> TypeId { + pub fn type_id(&self, decl: &LocalSlice>) -> TypeId { self.projections .last() .map_or_else(|| decl[self.local].r#type, |projection| projection.r#type) diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index 8b137891791..c0fced5b90f 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -1 +1,282 @@ +use core::convert::Infallible; +use std::alloc::Allocator; +use hashql_core::{ + heap::{BumpAllocator, CloneIn}, + id::IdVec, + r#type::{environment::Environment, kind::PrimitiveType}, +}; +use hashql_hir::node::operation::UnOp; + +use crate::{ + body::{ + Body, + constant::{Constant, Int}, + local::{LocalDecl, LocalSlice, LocalVec}, + location::Location, + operand::Operand, + place::Place, + rvalue::{BinOp, Binary, RValue, Unary}, + statement::Assign, + }, + context::MirContext, + intern::Interner, + pass::TransformPass, + visit::{self, VisitorMut, r#mut::filter}, +}; + +#[derive(Debug, Copy, Clone)] +enum OperandKind<'heap> { + Int(Int), + Place(Place<'heap>), + Other, +} + +pub struct InstSimplify { + alloc: A, +} + +impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for InstSimplify { + fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) { + self.alloc.reset(); + + let decl = body.local_decls.clone_in(&self.alloc); + + let mut visitor = InstSimplifyVisitor { + env: context.env, + interner: context.interner, + trampoline: None, + decl: &decl, + evaluated: IdVec::with_capacity_in(body.local_decls.len(), &self.alloc), + }; + visitor.visit_body_preserving_cfg(body); + } +} + +struct InstSimplifyVisitor<'env, 'heap, A: Allocator> { + env: &'env Environment<'heap>, + interner: &'env Interner<'heap>, + trampoline: Option>, + decl: &'env LocalSlice>, + evaluated: LocalVec, A>, +} + +impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { + fn try_eval(&self, operand: Operand<'heap>) -> OperandKind<'heap> { + if let Operand::Constant(Constant::Int(int)) = operand { + return OperandKind::Int(int); + } + + if let Operand::Place(place) = operand + && place.projections.is_empty() + && let Some(int) = self.evaluated[place.local] + { + return OperandKind::Int(int); + } + + if let Operand::Place(place) = operand { + return OperandKind::Place(place); + } + + OperandKind::Other + } + + fn eval_bin_op(lhs: Int, op: BinOp, rhs: Int) -> Int { + let lhs = lhs.as_int(); + let rhs = rhs.as_int(); + + let result = match op { + BinOp::BitAnd => lhs & rhs, + BinOp::BitOr => lhs | rhs, + BinOp::Eq => i128::from(lhs == rhs), + BinOp::Ne => i128::from(lhs != rhs), + BinOp::Lt => i128::from(lhs < rhs), + BinOp::Lte => i128::from(lhs <= rhs), + BinOp::Gt => i128::from(lhs > rhs), + BinOp::Gte => i128::from(lhs >= rhs), + }; + + Int::from(result) + } + + fn eval_un_op(op: UnOp, operand: Int) -> Int { + let value = operand.as_int(); + + let result = match op { + UnOp::Not => { + let Some(value) = operand.as_bool() else { + unreachable!("only boolean values can be negated"); + }; + + i128::from(!value) + } + UnOp::Neg => -value, + UnOp::BitNot => !value, + }; + + Int::from(result) + } + + #[expect(clippy::match_same_arms)] + fn simplify_bin_op_left( + &self, + lhs: Int, + op: BinOp, + rhs: Place<'heap>, + ) -> Option> { + let rhs_type = rhs.type_id(self.decl); + let is_bool = + self.env.r#type(rhs_type).kind.primitive().copied() == Some(PrimitiveType::Boolean); + + match (op, lhs.as_int()) { + // true && rhs => rhs + (BinOp::BitAnd, 1) if is_bool => Some(RValue::Load(Operand::Place(rhs))), + (BinOp::BitAnd, _) => None, + // 0 | rhs => rhs + (BinOp::BitOr, 0) => Some(RValue::Load(Operand::Place(rhs))), + (BinOp::BitOr, _) => None, + // 1 == rhs => rhs + (BinOp::Eq, 1) if is_bool => Some(RValue::Load(Operand::Place(rhs))), + // 0 == rhs => not(rhs) + (BinOp::Eq, 0) if is_bool => Some(RValue::Unary(Unary { + op: UnOp::Not, + operand: Operand::Place(rhs), + })), + (BinOp::Eq, _) => None, + // 0 != rhs => rhs + (BinOp::Ne, 0) if is_bool => Some(RValue::Load(Operand::Place(rhs))), + // 1 != rhs => not(rhs) + (BinOp::Ne, 1) if is_bool => Some(RValue::Unary(Unary { + op: UnOp::Not, + operand: Operand::Place(rhs), + })), + (BinOp::Ne, _) => None, + (BinOp::Lt, _) => None, + (BinOp::Lte, _) => None, + (BinOp::Gt, _) => None, + (BinOp::Gte, _) => None, + } + } + + #[expect(clippy::match_same_arms)] + fn simplify_bin_op_right( + &self, + lhs: Place<'heap>, + op: BinOp, + rhs: Int, + ) -> Option> { + let lhs_type = lhs.type_id(self.decl); + let is_bool = + self.env.r#type(lhs_type).kind.primitive().copied() == Some(PrimitiveType::Boolean); + + match (op, rhs.as_int()) { + // lhs && true => lhs + (BinOp::BitAnd, 1) if is_bool => Some(RValue::Load(Operand::Place(lhs))), + (BinOp::BitAnd, _) => None, + // lhs | 0 => lhs + (BinOp::BitOr, 0) => Some(RValue::Load(Operand::Place(lhs))), + (BinOp::BitOr, _) => None, + // lhs == 1 => lhs + (BinOp::Eq, 1) if is_bool => Some(RValue::Load(Operand::Place(lhs))), + // lhs == 0 => not(lhs) + (BinOp::Eq, 0) if is_bool => Some(RValue::Unary(Unary { + op: UnOp::Not, + operand: Operand::Place(lhs), + })), + (BinOp::Eq, _) => None, + // lhs != 0 => lhs == 1 => lhs + (BinOp::Ne, 0) if is_bool => Some(RValue::Load(Operand::Place(lhs))), + // lhs != 1 => lhs == 0 => not(lhs) + (BinOp::Ne, 1) if is_bool => Some(RValue::Unary(Unary { + op: UnOp::Not, + operand: Operand::Place(lhs), + })), + (BinOp::Ne, _) => None, + (BinOp::Lt, _) => None, + (BinOp::Lte, _) => None, + (BinOp::Gt, _) => None, + (BinOp::Gte, _) => None, + } + } +} + +impl<'heap, A: Allocator> VisitorMut<'heap> for InstSimplifyVisitor<'_, 'heap, A> { + type Filter = filter::Deep; + type Residual = Result; + type Result + = Result + where + T: 'heap; + + fn interner(&self) -> &Interner<'heap> { + self.interner + } + + fn visit_rvalue_binary( + &mut self, + _: Location, + Binary { op, left, right }: &mut Binary<'heap>, + ) -> Self::Result<()> { + // If both values are non-opaque (aka `Integer`) we evaluate them. + // Because we run after SROA, we can assume that the constants are already in place where + // they can be evaluated. + match (self.try_eval(*left), self.try_eval(*right)) { + (OperandKind::Int(lhs), OperandKind::Int(rhs)) => { + let result = Self::eval_bin_op(lhs, *op, rhs); + self.trampoline = Some(RValue::Load(Operand::Constant(Constant::Int(result)))); + } + (OperandKind::Place(lhs), OperandKind::Int(rhs)) => { + let result = self.simplify_bin_op_right(lhs, *op, rhs); + if let Some(result) = result { + self.trampoline = Some(result); + } + } + (OperandKind::Int(lhs), OperandKind::Place(rhs)) => { + let result = self.simplify_bin_op_left(lhs, *op, rhs); + if let Some(result) = result { + self.trampoline = Some(result); + } + } + _ => {} + } + + Ok(()) + } + + fn visit_rvalue_unary( + &mut self, + _: Location, + Unary { op, operand }: &mut Unary<'heap>, + ) -> Self::Result<()> { + if let OperandKind::Int(value) = self.try_eval(*operand) { + let result = Self::eval_un_op(*op, value); + self.trampoline = Some(RValue::Load(Operand::Constant(Constant::Int(result)))); + } + + Ok(()) + } + + fn visit_statement_assign( + &mut self, + location: Location, + assign: &mut Assign<'heap>, + ) -> Self::Result<()> { + debug_assert!(self.trampoline.is_none()); + + Ok(()) = visit::r#mut::walk_statement_assign(self, location, assign); + + let Some(trampoline) = self.trampoline.take() else { + return Ok(()); + }; + + if let RValue::Load(Operand::Constant(Constant::Int(int))) = trampoline + && assign.lhs.projections.is_empty() + { + self.evaluated.insert(assign.lhs.local, int); + } + + assign.rhs = trampoline; + + Ok(()) + } +} From 2286deb68b0b266788a3a50c227cab8cf30bb80a Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 16:50:24 +0100 Subject: [PATCH 05/14] fix: run in reverse postorder --- .../mir/src/pass/transform/inst_simplify/mod.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index c0fced5b90f..225e59512ee 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -2,7 +2,7 @@ use core::convert::Infallible; use std::alloc::Allocator; use hashql_core::{ - heap::{BumpAllocator, CloneIn}, + heap::{BumpAllocator, CloneIn, TransferInto}, id::IdVec, r#type::{environment::Environment, kind::PrimitiveType}, }; @@ -40,16 +40,22 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for InstSimplify< fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) { self.alloc.reset(); - let decl = body.local_decls.clone_in(&self.alloc); - let mut visitor = InstSimplifyVisitor { env: context.env, interner: context.interner, trampoline: None, - decl: &decl, + decl: &body.local_decls, evaluated: IdVec::with_capacity_in(body.local_decls.len(), &self.alloc), }; - visitor.visit_body_preserving_cfg(body); + + let reverse_postorder = body + .basic_blocks + .reverse_postorder() + .transfer_into(&self.alloc); + + for &mut id in reverse_postorder { + visitor.visit_basic_block(id, &mut body.basic_blocks.as_mut_preserving_cfg()[id]); + } } } From 84fc008bae950e39d9dd8ea2fd66322b1a5a3898 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 17:06:38 +0100 Subject: [PATCH 06/14] feat: place simplify --- .../src/pass/transform/inst_simplify/mod.rs | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index 225e59512ee..34a0610587d 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -1,8 +1,7 @@ -use core::convert::Infallible; -use std::alloc::Allocator; +use core::{alloc::Allocator, convert::Infallible}; use hashql_core::{ - heap::{BumpAllocator, CloneIn, TransferInto}, + heap::{BumpAllocator, TransferInto as _}, id::IdVec, r#type::{environment::Environment, kind::PrimitiveType}, }; @@ -137,9 +136,17 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { match (op, lhs.as_int()) { // true && rhs => rhs (BinOp::BitAnd, 1) if is_bool => Some(RValue::Load(Operand::Place(rhs))), + // false && rhs => false + (BinOp::BitAnd, 0) if is_bool => { + Some(RValue::Load(Operand::Constant(Constant::Int(false.into())))) + } (BinOp::BitAnd, _) => None, // 0 | rhs => rhs (BinOp::BitOr, 0) => Some(RValue::Load(Operand::Place(rhs))), + // true || rhs => true + (BinOp::BitOr, 1) if is_bool => { + Some(RValue::Load(Operand::Constant(Constant::Int(true.into())))) + } (BinOp::BitOr, _) => None, // 1 == rhs => rhs (BinOp::Eq, 1) if is_bool => Some(RValue::Load(Operand::Place(rhs))), @@ -178,9 +185,17 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { match (op, rhs.as_int()) { // lhs && true => lhs (BinOp::BitAnd, 1) if is_bool => Some(RValue::Load(Operand::Place(lhs))), + // rhs && false => false + (BinOp::BitAnd, 0) if is_bool => { + Some(RValue::Load(Operand::Constant(Constant::Int(false.into())))) + } (BinOp::BitAnd, _) => None, // lhs | 0 => lhs (BinOp::BitOr, 0) => Some(RValue::Load(Operand::Place(lhs))), + // rhs || 1 => true + (BinOp::BitOr, 1) if is_bool => { + Some(RValue::Load(Operand::Constant(Constant::Int(true.into())))) + } (BinOp::BitOr, _) => None, // lhs == 1 => lhs (BinOp::Eq, 1) if is_bool => Some(RValue::Load(Operand::Place(lhs))), @@ -204,6 +219,46 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { (BinOp::Gte, _) => None, } } + + #[expect(clippy::match_same_arms)] + fn simplify_bin_op_place( + lhs: Place<'heap>, + op: BinOp, + rhs: Place<'heap>, + ) -> Option> { + let is_same = lhs.local == rhs.local + && lhs.projections.len() == rhs.projections.len() + && lhs + .projections + .iter() + .zip(rhs.projections) + .all(|(lhs, rhs)| lhs.kind == rhs.kind); + + if !is_same { + return None; + } + + let bool = match op { + // x & x => x + BinOp::BitAnd => return Some(RValue::Load(Operand::Place(lhs))), + // x | x => x + BinOp::BitOr => return Some(RValue::Load(Operand::Place(lhs))), + // x == x => true + BinOp::Eq => true, + // x != x => false + BinOp::Ne => false, + // x < x => false + BinOp::Lt => false, + // x <= x => true + BinOp::Lte => true, + // x > x => false + BinOp::Gt => false, + // x >= x => true + BinOp::Gte => true, + }; + + Some(RValue::Load(Operand::Constant(Constant::Int(bool.into())))) + } } impl<'heap, A: Allocator> VisitorMut<'heap> for InstSimplifyVisitor<'_, 'heap, A> { @@ -243,6 +298,12 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for InstSimplifyVisitor<'_, 'heap, A self.trampoline = Some(result); } } + (OperandKind::Place(lhs), OperandKind::Place(rhs)) => { + let result = Self::simplify_bin_op_place(lhs, *op, rhs); + if let Some(result) = result { + self.trampoline = Some(result); + } + } _ => {} } From 38f09b3bca69641d86c73a19f777bfebebb99bf2 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 18:07:30 +0100 Subject: [PATCH 07/14] feat: propagate across edges --- .../hashql/mir/src/body/basic_blocks.rs | 2 +- libs/@local/hashql/mir/src/body/place.rs | 3 +- .../hashql/mir/src/body/terminator/mod.rs | 5 + .../src/pass/transform/inst_simplify/mod.rs | 95 ++++++++++++++++++- .../hashql/mir/src/pass/transform/mod.rs | 2 +- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/libs/@local/hashql/mir/src/body/basic_blocks.rs b/libs/@local/hashql/mir/src/body/basic_blocks.rs index 683e09c6ff2..5c21023614e 100644 --- a/libs/@local/hashql/mir/src/body/basic_blocks.rs +++ b/libs/@local/hashql/mir/src/body/basic_blocks.rs @@ -349,7 +349,7 @@ impl graph::Successors for BasicBlocks<'_> { impl graph::Predecessors for BasicBlocks<'_> { type PredIter<'this> - = impl ExactSizeIterator + DoubleEndedIterator + = impl ExactSizeIterator + DoubleEndedIterator + Clone where Self: 'this; diff --git a/libs/@local/hashql/mir/src/body/place.rs b/libs/@local/hashql/mir/src/body/place.rs index 124b559d37f..04dfba95f00 100644 --- a/libs/@local/hashql/mir/src/body/place.rs +++ b/libs/@local/hashql/mir/src/body/place.rs @@ -12,7 +12,7 @@ use core::{ use hashql_core::{id, intern::Interned, symbol::Symbol, r#type::TypeId}; -use super::local::{Local, LocalDecl, LocalSlice, LocalVec}; +use super::local::{Local, LocalDecl, LocalSlice}; use crate::intern::Interner; id::newtype!( @@ -378,6 +378,7 @@ impl<'heap> Place<'heap> { } /// Return the type of the place after applying all projections. + #[must_use] pub fn type_id(&self, decl: &LocalSlice>) -> TypeId { self.projections .last() diff --git a/libs/@local/hashql/mir/src/body/terminator/mod.rs b/libs/@local/hashql/mir/src/body/terminator/mod.rs index 1f861c46609..97a7e8a5386 100644 --- a/libs/@local/hashql/mir/src/body/terminator/mod.rs +++ b/libs/@local/hashql/mir/src/body/terminator/mod.rs @@ -165,6 +165,11 @@ pub enum TerminatorKind<'heap> { } impl<'heap> TerminatorKind<'heap> { + #[must_use] + pub const fn is_effectful(&self) -> bool { + matches!(self, Self::GraphRead(_)) + } + /// Returns an iterator over the basic block IDs of all possible successors. /// /// The returned iterator yields the [`BasicBlockId`] of each block that control diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index 34a0610587d..e0804eef953 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -1,7 +1,8 @@ use core::{alloc::Allocator, convert::Infallible}; use hashql_core::{ - heap::{BumpAllocator, TransferInto as _}, + graph::Predecessors as _, + heap::{BumpAllocator, Scratch, TransferInto as _}, id::IdVec, r#type::{environment::Environment, kind::PrimitiveType}, }; @@ -10,6 +11,7 @@ use hashql_hir::node::operation::UnOp; use crate::{ body::{ Body, + basic_block::BasicBlockId, constant::{Constant, Int}, local::{LocalDecl, LocalSlice, LocalVec}, location::Location, @@ -24,17 +26,97 @@ use crate::{ visit::{self, VisitorMut, r#mut::filter}, }; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] enum OperandKind<'heap> { Int(Int), Place(Place<'heap>), Other, } -pub struct InstSimplify { +impl OperandKind<'_> { + const fn as_int(&self) -> Option { + if let &OperandKind::Int(int) = self { + Some(int) + } else { + None + } + } +} + +pub struct InstSimplify { alloc: A, } +impl InstSimplify { + #[must_use] + pub fn new() -> Self { + Self { + alloc: Scratch::new(), + } + } +} + +impl Default for InstSimplify { + fn default() -> Self { + Self::new() + } +} + +impl InstSimplify { + pub const fn new_in(alloc: A) -> Self { + Self { alloc } + } + + fn propagate_block_params<'heap>( + args: &mut Vec, &A>, + visitor: &mut InstSimplifyVisitor<'_, 'heap, &A>, + body: &Body<'heap>, + id: BasicBlockId, + ) { + let pred = body.basic_blocks.predecessors(id); + + // If we have any predecessor that doesn't have explicit params (aka an effect) we cannot + // propagate anything + if pred + .clone() + .any(|pred| body.basic_blocks[pred].terminator.kind.is_effectful()) + { + return; + } + + // Check any predecessors of the basic block, if they all agree on the same value, we + // can copy their value. + let mut targets = pred + .flat_map(|pred| body.basic_blocks[pred].terminator.kind.successor_targets()) + .filter(|&target| target.block == id); + + // There's nothing to propagate (aka there are no targets) + let Some(first) = targets.next() else { + return; + }; + + // These are our reference values, all branches must have the same `Option` value, as us. + args.extend(first.args.iter().map(|&arg| visitor.try_eval(arg).as_int())); + + for target in targets { + debug_assert_eq!(args.len(), target.args.len()); + + for (lhs, &rhs) in args.iter_mut().zip(target.args.iter()) { + let rhs = visitor.try_eval(rhs).as_int(); + if *lhs != rhs { + *lhs = None; + } + } + } + + for (&local, constant) in body.basic_blocks[id].params.iter().zip(args.drain(..)) { + if let Some(constant) = constant { + visitor.evaluated.insert(local, constant); + } + } + } +} + impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for InstSimplify { fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) { self.alloc.reset(); @@ -52,8 +134,13 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for InstSimplify< .reverse_postorder() .transfer_into(&self.alloc); + let mut args = Vec::new_in(&self.alloc); + for &mut id in reverse_postorder { - visitor.visit_basic_block(id, &mut body.basic_blocks.as_mut_preserving_cfg()[id]); + Self::propagate_block_params(&mut args, &mut visitor, body, id); + + Ok(()) = + visitor.visit_basic_block(id, &mut body.basic_blocks.as_mut_preserving_cfg()[id]); } } } diff --git a/libs/@local/hashql/mir/src/pass/transform/mod.rs b/libs/@local/hashql/mir/src/pass/transform/mod.rs index 35179d1d5bc..5cc9a9f3622 100644 --- a/libs/@local/hashql/mir/src/pass/transform/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/mod.rs @@ -9,5 +9,5 @@ mod ssa_repair; pub use self::{ cfg_simplify::CfgSimplify, dbe::DeadBlockElimination, dle::DeadLocalElimination, - dse::DeadStoreElimination, sroa::Sroa, ssa_repair::SsaRepair, + dse::DeadStoreElimination, inst_simplify::InstSimplify, sroa::Sroa, ssa_repair::SsaRepair, }; From b64090fe9c37dd3f97ff27abada885b90c9b0688 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 18:22:03 +0100 Subject: [PATCH 08/14] chore: docs --- .../src/pass/transform/inst_simplify/mod.rs | 234 +++++++++++++++--- 1 file changed, 201 insertions(+), 33 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index e0804eef953..ce17aa727c2 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -1,3 +1,91 @@ +//! Instruction simplification pass. +//! +//! This pass performs local algebraic simplifications and constant folding on MIR instructions. +//! It operates on individual instructions, replacing complex expressions with simpler equivalents +//! when operands are constants or satisfy algebraic identities. +//! +//! # Simplifications +//! +//! The pass handles three categories of simplifications: +//! +//! ## Constant Folding +//! +//! When both operands of a binary operation or the operand of a unary operation are constants, +//! the operation is evaluated at compile time: +//! +//! - `const 2 + const 3` → `const 5` +//! - `!const true` → `const false` +//! +//! ## Algebraic Identities +//! +//! Operations with one constant operand may simplify based on algebraic laws: +//! +//! - **Identity elements**: `x && true` → `x`, `x | 0` → `x` +//! - **Annihilators**: `x && false` → `false`, `x || true` → `true` +//! - **Boolean equivalences**: `x == true` → `x`, `x == false` → `!x` +//! +//! ## Identical Operand Patterns +//! +//! When both operands reference the same place, certain operations have known results: +//! +//! - **Idempotence**: `x & x` → `x`, `x | x` → `x` +//! - **Reflexive comparisons**: `x == x` → `true`, `x < x` → `false` +//! +//! # Algorithm +//! +//! The pass operates in a single traversal over basic blocks in reverse postorder: +//! +//! 1. At each block entry, propagate constants through block parameters by checking if all +//! predecessor branches pass the same constant value +//! 2. Visit each instruction, applying simplifications when patterns match +//! 3. Track locals assigned constant values to enable further simplifications within the block +//! +//! The `evaluated` map tracks locals known to hold constant values. When a local is assigned +//! a constant (either directly or as the result of constant folding), it is recorded so that +//! subsequent uses can be treated as constants. +//! +//! # Example +//! +//! Before: +//! ```text +//! bb0: +//! _1 = const 1 +//! _2 = const 2 +//! _3 = _1 == _2 +//! _4 = _3 && const true +//! return _4 +//! ``` +//! +//! After: +//! ```text +//! bb0: +//! _1 = const 1 +//! _2 = const 2 +//! _3 = const false +//! _4 = const false +//! return _4 +//! ``` +//! +//! # Interaction with Other Passes +//! +//! This pass runs after [`Sroa`], which resolves places through the data dependency graph. SROA +//! ensures that operands are simplified to their canonical forms before [`InstSimplify`] runs, so +//! constants that flow through assignments or block parameters are already exposed. +//! +//! Block parameter propagation in this pass complements SROA: while SROA resolves structural +//! dependencies at the operand level, [`InstSimplify`] propagates constants discovered through +//! folding across block boundaries. +//! +//! # Limitations +//! +//! The pass does not perform fix-point iteration for loops. Constants propagated through +//! back-edges are not discovered because predecessors forming back-edges have not been visited +//! when the loop header is processed. Full loop-carried constant propagation would require +//! iterating until the `evaluated` map stabilizes, which is not implemented as the expected +//! benefit is low. +//! +//! [`Sroa`]: super::Sroa + use core::{alloc::Allocator, convert::Infallible}; use hashql_core::{ @@ -26,10 +114,17 @@ use crate::{ visit::{self, VisitorMut, r#mut::filter}, }; +/// Classification of an operand for simplification purposes. +/// +/// This enum categorizes operands into cases relevant for constant folding and algebraic +/// simplification. #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum OperandKind<'heap> { + /// A constant integer value, either from a literal or a local known to hold a constant. Int(Int), + /// A place reference that could not be resolved to a constant. Place(Place<'heap>), + /// An operand that cannot be simplified (e.g., non-integer constants). Other, } @@ -43,11 +138,15 @@ impl OperandKind<'_> { } } +/// Instruction simplification pass. +/// +/// Performs constant folding and algebraic simplification on MIR instructions. pub struct InstSimplify { alloc: A, } impl InstSimplify { + /// Creates a new [`InstSimplify`]. #[must_use] pub fn new() -> Self { Self { @@ -63,10 +162,28 @@ impl Default for InstSimplify { } impl InstSimplify { + /// Creates a new [`InstSimplify`] using the provided allocator. + #[must_use] pub const fn new_in(alloc: A) -> Self { Self { alloc } } + /// Propagates constant values through block parameters. + /// + /// For each block parameter, examines all predecessor branches that target this block. + /// If all predecessors pass the same constant value (or a local that evaluates to the + /// same constant), records that constant in `evaluated` for the block parameter. + /// + /// This enables constant folding for values that converge at control flow join points, + /// complementing SROA's structural resolution with runtime constant tracking. + /// + /// # Limitations + /// + /// - Predecessors with effectful terminators (e.g., `GraphRead`) are skipped entirely, as their + /// arguments are implicit rather than explicit. + /// - Back-edges from loops may not have been visited yet, so their contributions are based on + /// whatever constants were discovered in previous iterations (if any). Full loop-carried + /// propagation would require fix-point iteration. fn propagate_block_params<'heap>( args: &mut Vec, &A>, visitor: &mut InstSimplifyVisitor<'_, 'heap, &A>, @@ -75,8 +192,10 @@ impl InstSimplify { ) { let pred = body.basic_blocks.predecessors(id); - // If we have any predecessor that doesn't have explicit params (aka an effect) we cannot - // propagate anything + // Effectful terminators (like GraphRead) pass arguments implicitly, where they set the + // block param directly. We cannot inspect those values, so we conservatively skip + // propagation for blocks reachable from effectful predecessors (they have single + // successors). if pred .clone() .any(|pred| body.basic_blocks[pred].terminator.kind.is_effectful()) @@ -84,20 +203,24 @@ impl InstSimplify { return; } - // Check any predecessors of the basic block, if they all agree on the same value, we - // can copy their value. + // Collect all predecessor targets that branch to this block. A single predecessor + // may have multiple targets to us (e.g., a switch with two arms to the same block). let mut targets = pred .flat_map(|pred| body.basic_blocks[pred].terminator.kind.successor_targets()) .filter(|&target| target.block == id); - // There's nothing to propagate (aka there are no targets) let Some(first) = targets.next() else { + // No explicit targets means this block is only reachable via implicit edges + // (e.g., entry block or effectful continuations). Nothing to propagate. return; }; - // These are our reference values, all branches must have the same `Option` value, as us. + // Seed with the first target's argument values. Each position holds `Some(int)` if + // that argument evaluated to a constant, `None` otherwise. args.extend(first.args.iter().map(|&arg| visitor.try_eval(arg).as_int())); + // Check remaining targets for consensus. If any target passes a different value + // (or non-constant) for a parameter position, clear that position to `None`. for target in targets { debug_assert_eq!(args.len(), target.args.len()); @@ -109,6 +232,7 @@ impl InstSimplify { } } + // Record constants for block parameters where all predecessors agreed. for (&local, constant) in body.basic_blocks[id].params.iter().zip(args.drain(..)) { if let Some(constant) = constant { visitor.evaluated.insert(local, constant); @@ -145,15 +269,30 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for InstSimplify< } } +/// Visitor that applies instruction simplifications during MIR traversal. +/// +/// This visitor implements the core simplification logic. It uses a trampoline pattern to +/// communicate replacement [`RValue`]s from nested rvalue visitors back to the statement visitor, +/// since rvalue visitors only have access to the inner struct, not the enclosing `RValue` enum. struct InstSimplifyVisitor<'env, 'heap, A: Allocator> { env: &'env Environment<'heap>, interner: &'env Interner<'heap>, + + /// Temporary slot for rvalue visitors to request replacement of the enclosing [`RValue`]. trampoline: Option>, + decl: &'env LocalSlice>, + + /// Map from locals to their known constant values. + /// Populated when a local is assigned a constant (directly or via folding). evaluated: LocalVec, A>, } impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { + /// Attempts to evaluate an operand to a known constant or classify it for simplification. + /// + /// Returns `Int` if the operand is a constant integer or a local known to hold one, + /// `Place` if it's a non-constant place, or `Other` for operands that can't be simplified. fn try_eval(&self, operand: Operand<'heap>) -> OperandKind<'heap> { if let Operand::Constant(Constant::Int(int)) = operand { return OperandKind::Int(int); @@ -173,6 +312,7 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { OperandKind::Other } + /// Evaluates a binary operation on two constant integers. fn eval_bin_op(lhs: Int, op: BinOp, rhs: Int) -> Int { let lhs = lhs.as_int(); let rhs = rhs.as_int(); @@ -191,6 +331,7 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { Int::from(result) } + /// Evaluates a unary operation on a constant integer. fn eval_un_op(op: UnOp, operand: Int) -> Int { let value = operand.as_int(); @@ -209,6 +350,11 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { Int::from(result) } + /// Attempts to simplify a binary operation with a constant left operand and place right + /// operand. + /// + /// Handles identity elements, annihilators, and boolean equivalences where the constant + /// is on the left side of the operation. #[expect(clippy::match_same_arms)] fn simplify_bin_op_left( &self, @@ -221,31 +367,31 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { self.env.r#type(rhs_type).kind.primitive().copied() == Some(PrimitiveType::Boolean); match (op, lhs.as_int()) { - // true && rhs => rhs + // true && rhs => rhs (identity) (BinOp::BitAnd, 1) if is_bool => Some(RValue::Load(Operand::Place(rhs))), - // false && rhs => false + // false && rhs => false (annihilator) (BinOp::BitAnd, 0) if is_bool => { Some(RValue::Load(Operand::Constant(Constant::Int(false.into())))) } (BinOp::BitAnd, _) => None, - // 0 | rhs => rhs + // 0 | rhs => rhs (identity) (BinOp::BitOr, 0) => Some(RValue::Load(Operand::Place(rhs))), - // true || rhs => true + // true || rhs => true (annihilator) (BinOp::BitOr, 1) if is_bool => { Some(RValue::Load(Operand::Constant(Constant::Int(true.into())))) } (BinOp::BitOr, _) => None, - // 1 == rhs => rhs + // true == rhs => rhs (boolean equivalence) (BinOp::Eq, 1) if is_bool => Some(RValue::Load(Operand::Place(rhs))), - // 0 == rhs => not(rhs) + // false == rhs => !rhs (boolean equivalence) (BinOp::Eq, 0) if is_bool => Some(RValue::Unary(Unary { op: UnOp::Not, operand: Operand::Place(rhs), })), (BinOp::Eq, _) => None, - // 0 != rhs => rhs + // false != rhs => rhs (boolean equivalence) (BinOp::Ne, 0) if is_bool => Some(RValue::Load(Operand::Place(rhs))), - // 1 != rhs => not(rhs) + // true != rhs => !rhs (boolean equivalence) (BinOp::Ne, 1) if is_bool => Some(RValue::Unary(Unary { op: UnOp::Not, operand: Operand::Place(rhs), @@ -258,6 +404,11 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { } } + /// Attempts to simplify a binary operation with a place left operand and constant right + /// operand. + /// + /// Handles identity elements, annihilators, and boolean equivalences where the constant + /// is on the right side of the operation. #[expect(clippy::match_same_arms)] fn simplify_bin_op_right( &self, @@ -270,31 +421,31 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { self.env.r#type(lhs_type).kind.primitive().copied() == Some(PrimitiveType::Boolean); match (op, rhs.as_int()) { - // lhs && true => lhs + // lhs && true => lhs (identity) (BinOp::BitAnd, 1) if is_bool => Some(RValue::Load(Operand::Place(lhs))), - // rhs && false => false + // lhs && false => false (annihilator) (BinOp::BitAnd, 0) if is_bool => { Some(RValue::Load(Operand::Constant(Constant::Int(false.into())))) } (BinOp::BitAnd, _) => None, - // lhs | 0 => lhs + // lhs | 0 => lhs (identity) (BinOp::BitOr, 0) => Some(RValue::Load(Operand::Place(lhs))), - // rhs || 1 => true + // lhs || true => true (annihilator) (BinOp::BitOr, 1) if is_bool => { Some(RValue::Load(Operand::Constant(Constant::Int(true.into())))) } (BinOp::BitOr, _) => None, - // lhs == 1 => lhs + // lhs == true => lhs (boolean equivalence) (BinOp::Eq, 1) if is_bool => Some(RValue::Load(Operand::Place(lhs))), - // lhs == 0 => not(lhs) + // lhs == false => !lhs (boolean equivalence) (BinOp::Eq, 0) if is_bool => Some(RValue::Unary(Unary { op: UnOp::Not, operand: Operand::Place(lhs), })), (BinOp::Eq, _) => None, - // lhs != 0 => lhs == 1 => lhs + // lhs != false => lhs (boolean equivalence) (BinOp::Ne, 0) if is_bool => Some(RValue::Load(Operand::Place(lhs))), - // lhs != 1 => lhs == 0 => not(lhs) + // lhs != true => !lhs (boolean equivalence) (BinOp::Ne, 1) if is_bool => Some(RValue::Unary(Unary { op: UnOp::Not, operand: Operand::Place(lhs), @@ -307,12 +458,17 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { } } + /// Attempts to simplify a binary operation where both operands are the same place. + /// + /// Handles idempotent operations (`x & x`, `x | x`) and reflexive comparisons + /// (`x == x`, `x < x`, etc.) that have known results regardless of the actual value. #[expect(clippy::match_same_arms)] fn simplify_bin_op_place( lhs: Place<'heap>, op: BinOp, rhs: Place<'heap>, ) -> Option> { + // Check if both places refer to the same location (same local and projections). let is_same = lhs.local == rhs.local && lhs.projections.len() == rhs.projections.len() && lhs @@ -326,21 +482,21 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { } let bool = match op { - // x & x => x + // x & x => x (idempotent) BinOp::BitAnd => return Some(RValue::Load(Operand::Place(lhs))), - // x | x => x + // x | x => x (idempotent) BinOp::BitOr => return Some(RValue::Load(Operand::Place(lhs))), - // x == x => true + // x == x => true (reflexive) BinOp::Eq => true, - // x != x => false + // x != x => false (irreflexive) BinOp::Ne => false, - // x < x => false + // x < x => false (irreflexive) BinOp::Lt => false, - // x <= x => true + // x <= x => true (reflexive) BinOp::Lte => true, - // x > x => false + // x > x => false (irreflexive) BinOp::Gt => false, - // x >= x => true + // x >= x => true (reflexive) BinOp::Gte => true, }; @@ -365,9 +521,8 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for InstSimplifyVisitor<'_, 'heap, A _: Location, Binary { op, left, right }: &mut Binary<'heap>, ) -> Self::Result<()> { - // If both values are non-opaque (aka `Integer`) we evaluate them. - // Because we run after SROA, we can assume that the constants are already in place where - // they can be evaluated. + // Dispatch to the appropriate simplification based on operand classification. + // SROA has already resolved structural dependencies, so constants are directly visible. match (self.try_eval(*left), self.try_eval(*right)) { (OperandKind::Int(lhs), OperandKind::Int(rhs)) => { let result = Self::eval_bin_op(lhs, *op, rhs); @@ -417,18 +572,31 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for InstSimplifyVisitor<'_, 'heap, A ) -> Self::Result<()> { debug_assert!(self.trampoline.is_none()); + // Walk the RHS first, which may set `trampoline` if a simplification applies. Ok(()) = visit::r#mut::walk_statement_assign(self, location, assign); let Some(trampoline) = self.trampoline.take() else { return Ok(()); }; + // If the simplified RHS is a constant, record it for future simplifications. if let RValue::Load(Operand::Constant(Constant::Int(int))) = trampoline && assign.lhs.projections.is_empty() { self.evaluated.insert(assign.lhs.local, int); } + // If the simplified RHS is a place, and has a constant associated with it, record it + // forward for future simplifications, this is important in cases where we simplify into + // idempotent positions, given: `y = x & x`, we simplify to `y = x`. Therefore if `x` is + // already a constant, we can propagate it. + if let RValue::Load(Operand::Place(place)) = trampoline + && place.projections.is_empty() + && let Some(&Some(constant)) = self.evaluated.get(place.local) + { + self.evaluated.insert(assign.lhs.local, constant); + } + assign.rhs = trampoline; Ok(()) From af6544855a74e6b476bdbefa04abea1660b6d5d8 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 19:06:17 +0100 Subject: [PATCH 09/14] chore: docs --- .../src/pass/transform/inst_simplify/mod.rs | 2 + .../src/pass/transform/inst_simplify/tests.rs | 539 ++++++++++++++++++ .../tests/ui/pass/inst_simplify/.spec.toml | 1 + .../inst_simplify/annihilator-and-false.jsonc | 14 + .../inst_simplify/annihilator-or-true.jsonc | 14 + .../inst_simplify/chained-const-fold.jsonc | 14 + .../ui/pass/inst_simplify/const-fold-eq.jsonc | 10 + .../ui/pass/inst_simplify/const-fold-gt.jsonc | 10 + .../pass/inst_simplify/const-fold-gte.jsonc | 10 + .../ui/pass/inst_simplify/const-fold-lt.jsonc | 10 + .../pass/inst_simplify/const-fold-lte.jsonc | 10 + .../ui/pass/inst_simplify/const-fold-ne.jsonc | 10 + .../const-propagation-locals.jsonc | 14 + .../inst_simplify/identical-operand-eq.jsonc | 9 + .../inst_simplify/identical-operand-gt.jsonc | 9 + .../inst_simplify/identical-operand-gte.jsonc | 9 + .../inst_simplify/identical-operand-lt.jsonc | 9 + .../inst_simplify/identical-operand-lte.jsonc | 9 + .../inst_simplify/identical-operand-ne.jsonc | 9 + .../inst_simplify/identity-and-true.jsonc | 14 + .../inst_simplify/identity-or-false.jsonc | 14 + 21 files changed, 740 insertions(+) create mode 100644 libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/.spec.toml create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index ce17aa727c2..33d2b52ff19 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -85,6 +85,8 @@ //! benefit is low. //! //! [`Sroa`]: super::Sroa +#[cfg(test)] +mod tests; use core::{alloc::Allocator, convert::Infallible}; diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs new file mode 100644 index 00000000000..9faeb92bb40 --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs @@ -0,0 +1,539 @@ +//! Insta tests for `InstSimplify` operations not expressible in HashQL source. +//! +//! These tests cover bitwise operations on integers, unary operations, and block +//! parameter propagation which require direct MIR construction. Boolean logical +//! operations and comparisons are tested via compiletest. + +use std::path::PathBuf; + +use bstr::ByteVec as _; +use hashql_core::{ + pretty::Formatter, + r#type::{TypeBuilder, TypeFormatter, TypeFormatterOptions, environment::Environment}, +}; +use hashql_diagnostics::DiagnosticIssues; +use insta::{Settings, assert_snapshot}; + +use super::InstSimplify; +use crate::{ + body::Body, + builder::{op, scaffold}, + context::MirContext, + def::DefIdSlice, + pass::TransformPass as _, + pretty::TextFormat, +}; + +#[track_caller] +fn assert_inst_simplify_pass<'heap>( + name: &'static str, + body: Body<'heap>, + context: &mut MirContext<'_, 'heap>, +) { + let formatter = Formatter::new(context.heap); + let mut formatter = TypeFormatter::new( + &formatter, + context.env, + TypeFormatterOptions::terse().with_qualified_opaque_names(true), + ); + let mut text_format = TextFormat { + writer: Vec::new(), + indent: 4, + sources: (), + types: &mut formatter, + }; + + let mut bodies = [body]; + + text_format + .format(DefIdSlice::from_raw(&bodies), &[]) + .expect("should be able to write bodies"); + + text_format + .writer + .extend(b"\n\n------------------------------------\n\n"); + + InstSimplify::new().run(context, &mut bodies[0]); + + text_format + .format(DefIdSlice::from_raw(&bodies), &[]) + .expect("should be able to write bodies"); + + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let mut settings = Settings::clone_current(); + settings.set_snapshot_path(dir.join("tests/ui/pass/inst_simplify")); + settings.set_prepend_module_to_snapshot(false); + + let _drop = settings.bind_to_scope(); + + let value = text_format.writer.into_string_lossy(); + assert_snapshot!(name, value); +} + +// ============================================================================= +// Constant Folding (Bitwise on integers, Unary - not in source language) +// ============================================================================= + +/// Tests constant folding for bitwise AND on integers. +/// +/// ```text +/// bb0: +/// %result = 2 & 3 +/// return %result +/// ``` +#[test] +fn const_fold_bit_and() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); + let const_2 = builder.const_int(2); + let const_3 = builder.const_int(3); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.binary(const_2, op![&], const_3)) + .ret(result); + + let body = builder.finish(0, TypeBuilder::synthetic(&env).integer()); + + assert_inst_simplify_pass( + "const_fold_bit_and", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests constant folding for bitwise OR on integers. +/// +/// ```text +/// bb0: +/// %result = 2 | 1 +/// return %result +/// ``` +#[test] +fn const_fold_bit_or() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); + let const_2 = builder.const_int(2); + let const_1 = builder.const_int(1); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.binary(const_2, op![|], const_1)) + .ret(result); + + let body = builder.finish(0, TypeBuilder::synthetic(&env).integer()); + + assert_inst_simplify_pass( + "const_fold_bit_or", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests constant folding for unary NOT. +/// +/// ```text +/// bb0: +/// %result = !true +/// return %result +/// ``` +#[test] +fn const_fold_unary_not() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let result = builder.local("result", TypeBuilder::synthetic(&env).boolean()); + let const_true = builder.const_bool(true); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.unary(op![!], const_true)) + .ret(result); + + let body = builder.finish(0, TypeBuilder::synthetic(&env).boolean()); + + assert_inst_simplify_pass( + "const_fold_unary_not", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests constant folding for unary negation. +/// +/// ```text +/// bb0: +/// %result = -5 +/// return %result +/// ``` +#[test] +fn const_fold_unary_neg() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); + let const_5 = builder.const_int(5); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.unary(op![neg], const_5)) + .ret(result); + + let body = builder.finish(0, TypeBuilder::synthetic(&env).integer()); + + assert_inst_simplify_pass( + "const_fold_unary_neg", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +// ============================================================================= +// Bitwise Identity on Integers (x | 0 => x - not in source language) +// ============================================================================= + +/// Tests identity simplification for bitwise OR with zero. +/// +/// ```text +/// bb0: +/// %result = %x | 0 +/// return %result +/// ``` +#[test] +fn identity_bit_or_zero() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let x = builder.local("x", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); + let const_0 = builder.const_int(0); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.binary(x, op![|], const_0)) + .ret(result); + + let body = builder.finish(1, TypeBuilder::synthetic(&env).integer()); + + assert_inst_simplify_pass( + "identity_bit_or_zero", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +// ============================================================================= +// Identical Operand Patterns (BitAnd/BitOr on integers - not in source) +// ============================================================================= + +/// Tests idempotent simplification for bitwise AND with identical operands. +/// +/// ```text +/// bb0: +/// %result = %x & %x +/// return %result +/// ``` +#[test] +fn identical_operand_bit_and() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let x = builder.local("x", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.binary(x, op![&], x)) + .ret(result); + + let body = builder.finish(1, TypeBuilder::synthetic(&env).integer()); + + assert_inst_simplify_pass( + "identical_operand_bit_and", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests idempotent simplification for bitwise OR with identical operands. +/// +/// ```text +/// bb0: +/// %result = %x | %x +/// return %result +/// ``` +#[test] +fn identical_operand_bit_or() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let x = builder.local("x", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.binary(x, op![|], x)) + .ret(result); + + let body = builder.finish(1, TypeBuilder::synthetic(&env).integer()); + + assert_inst_simplify_pass( + "identical_operand_bit_or", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +// ============================================================================= +// Block Parameter Propagation (requires CFG control) +// ============================================================================= + +/// Tests constant propagation through block params with single predecessor. +/// +/// ```text +/// bb0: +/// goto bb1(5) +/// +/// bb1(%p): +/// %result = %p == 5 +/// return %result +/// ``` +#[test] +fn block_param_single_predecessor() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let param = builder.local("p", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).boolean()); + let const_5 = builder.const_int(5); + + let bb0 = builder.reserve_block([]); + let bb1 = builder.reserve_block([param.local]); + + builder.build_block(bb0).goto(bb1, [const_5]); + + builder + .build_block(bb1) + .assign_place(result, |rv| rv.binary(param, op![==], const_5)) + .ret(result); + + let body = builder.finish(0, TypeBuilder::synthetic(&env).boolean()); + + assert_inst_simplify_pass( + "block_param_single_predecessor", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests constant propagation when all predecessors agree on value. +/// +/// ```text +/// bb0: +/// switch %cond -> [0: bb1, 1: bb2] +/// +/// bb1: +/// goto bb3(42) +/// +/// bb2: +/// goto bb3(42) +/// +/// bb3(%p): +/// %result = %p == 42 +/// return %result +/// ``` +#[test] +fn block_param_predecessors_agree() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let cond = builder.local("cond", TypeBuilder::synthetic(&env).integer()); + let param = builder.local("p", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).boolean()); + let const_42 = builder.const_int(42); + + let bb0 = builder.reserve_block([]); + let bb1 = builder.reserve_block([]); + let bb2 = builder.reserve_block([]); + let bb3 = builder.reserve_block([param.local]); + + builder + .build_block(bb0) + .switch(cond, |switch| switch.case(0, bb1, []).case(1, bb2, [])); + + builder.build_block(bb1).goto(bb3, [const_42]); + builder.build_block(bb2).goto(bb3, [const_42]); + + builder + .build_block(bb3) + .assign_place(result, |rv| rv.binary(param, op![==], const_42)) + .ret(result); + + let body = builder.finish(1, TypeBuilder::synthetic(&env).boolean()); + + assert_inst_simplify_pass( + "block_param_predecessors_agree", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests no propagation when predecessors disagree on value. +/// +/// ```text +/// bb0: +/// switch %cond -> [0: bb1, 1: bb2] +/// +/// bb1: +/// goto bb3(1) +/// +/// bb2: +/// goto bb3(2) +/// +/// bb3(%p): +/// %result = %p == 1 +/// return %result +/// ``` +#[test] +fn block_param_predecessors_disagree() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let cond = builder.local("cond", TypeBuilder::synthetic(&env).integer()); + let param = builder.local("p", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).boolean()); + let const_1 = builder.const_int(1); + let const_2 = builder.const_int(2); + + let bb0 = builder.reserve_block([]); + let bb1 = builder.reserve_block([]); + let bb2 = builder.reserve_block([]); + let bb3 = builder.reserve_block([param.local]); + + builder + .build_block(bb0) + .switch(cond, |switch| switch.case(0, bb1, []).case(1, bb2, [])); + + builder.build_block(bb1).goto(bb3, [const_1]); + builder.build_block(bb2).goto(bb3, [const_2]); + + builder + .build_block(bb3) + .assign_place(result, |rv| rv.binary(param, op![==], const_1)) + .ret(result); + + let body = builder.finish(1, TypeBuilder::synthetic(&env).boolean()); + + assert_inst_simplify_pass( + "block_param_predecessors_disagree", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +// ============================================================================= +// Idempotent to Constant Forwarding (requires bitwise op) +// ============================================================================= + +/// Tests that idempotent simplification propagates constants through the result. +/// +/// ```text +/// bb0: +/// %x = 42 +/// %y = %x & %x +/// %result = %y == 42 +/// return %result +/// ``` +#[test] +fn idempotent_to_const_forwarding() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + + let x = builder.local("x", TypeBuilder::synthetic(&env).integer()); + let y = builder.local("y", TypeBuilder::synthetic(&env).integer()); + let result = builder.local("result", TypeBuilder::synthetic(&env).boolean()); + let const_42 = builder.const_int(42); + + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(x, |rv| rv.load(const_42)) + .assign_place(y, |rv| rv.binary(x, op![&], x)) + .assign_place(result, |rv| rv.binary(y, op![==], const_42)) + .ret(result); + + let body = builder.finish(0, TypeBuilder::synthetic(&env).boolean()); + + assert_inst_simplify_pass( + "idempotent_to_const_forwarding", + body, + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/.spec.toml b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/.spec.toml new file mode 100644 index 00000000000..86d52a6d712 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/.spec.toml @@ -0,0 +1 @@ +suite = "mir/pass/transform/inst-simplify" diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc new file mode 100644 index 00000000000..be58699384e --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Annihilator simplification: x && false => false +// x && false is always false regardless of x. +[ + "if", + { "#literal": true }, + [ + "let", + "x", + ["==", { "#literal": 1 }, { "#literal": 1 }], + ["&&", "x", { "#literal": false }] + ], + { "#literal": true } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc new file mode 100644 index 00000000000..f843e34b306 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Annihilator simplification: x || true => true +// x || true is always true regardless of x. +[ + "if", + { "#literal": true }, + [ + "let", + "x", + ["==", { "#literal": 1 }, { "#literal": 2 }], + ["||", "x", { "#literal": true }] + ], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc new file mode 100644 index 00000000000..639667c5952 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Chained constant folding where a folded result is used in another comparison +// (1 == 2) folds to false, then (false == false) folds to true. +[ + "if", + { "#literal": true }, + [ + "let", + "cmp", + ["==", { "#literal": 1 }, { "#literal": 2 }], + ["==", "cmp", { "#literal": false }] + ], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc new file mode 100644 index 00000000000..42680c65001 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Constant folding for equality comparison of two constants +// Before: _0 = (1 == 2) evaluates at runtime +// After: _0 = false (folded at compile time) +[ + "if", + { "#literal": true }, + ["==", { "#literal": 1 }, { "#literal": 2 }], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc new file mode 100644 index 00000000000..cc83a2ab0f3 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Constant folding for greater-than comparison +// Before: _0 = (10 > 5) evaluates at runtime +// After: _0 = true (folded at compile time) +[ + "if", + { "#literal": true }, + [">", { "#literal": 10 }, { "#literal": 5 }], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc new file mode 100644 index 00000000000..c6ed52f2868 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Constant folding for greater-than-or-equal comparison +// Before: _0 = (5 >= 5) evaluates at runtime +// After: _0 = true (folded at compile time) +[ + "if", + { "#literal": true }, + [">=", { "#literal": 5 }, { "#literal": 5 }], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc new file mode 100644 index 00000000000..87375c2544b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Constant folding for less-than comparison of two constants +// Before: _0 = (3 < 5) evaluates at runtime +// After: _0 = true (folded at compile time) +[ + "if", + { "#literal": true }, + ["<", { "#literal": 3 }, { "#literal": 5 }], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc new file mode 100644 index 00000000000..e5578c7bb80 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Constant folding for less-than-or-equal comparison +// Before: _0 = (5 <= 5) evaluates at runtime +// After: _0 = true (folded at compile time) +[ + "if", + { "#literal": true }, + ["<=", { "#literal": 5 }, { "#literal": 5 }], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc new file mode 100644 index 00000000000..f0645930359 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc @@ -0,0 +1,10 @@ +//@ run: pass +//@ description: Constant folding for inequality comparison of two constants +// Before: _0 = (1 != 2) evaluates at runtime +// After: _0 = true (folded at compile time) +[ + "if", + { "#literal": true }, + ["!=", { "#literal": 1 }, { "#literal": 2 }], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc new file mode 100644 index 00000000000..61ac977db3d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Constants assigned to locals are tracked and used in subsequent folding +// x = 5, y = 3, then x == y folds to false because both are known constants. +[ + "if", + { "#literal": true }, + [ + "let", + "x", + { "#literal": 5 }, + ["let", "y", { "#literal": 3 }, ["==", "x", "y"]] + ], + { "#literal": true } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc new file mode 100644 index 00000000000..761e68ea98b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc @@ -0,0 +1,9 @@ +//@ run: pass +//@ description: Identical operand simplification: x == x => true (reflexive) +// When both operands of equality are the same place, the result is always true. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, ["==", "x", "x"]], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc new file mode 100644 index 00000000000..6383f10d791 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc @@ -0,0 +1,9 @@ +//@ run: pass +//@ description: Identical operand simplification: x > x => false (irreflexive) +// A value is never greater than itself. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, [">", "x", "x"]], + { "#literal": true } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc new file mode 100644 index 00000000000..4ce823a67b4 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc @@ -0,0 +1,9 @@ +//@ run: pass +//@ description: Identical operand simplification: x >= x => true (reflexive) +// A value is always greater than or equal to itself. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, [">=", "x", "x"]], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc new file mode 100644 index 00000000000..83655717e65 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc @@ -0,0 +1,9 @@ +//@ run: pass +//@ description: Identical operand simplification: x < x => false (irreflexive) +// A value is never less than itself. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, ["<", "x", "x"]], + { "#literal": true } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc new file mode 100644 index 00000000000..44bdce60c86 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc @@ -0,0 +1,9 @@ +//@ run: pass +//@ description: Identical operand simplification: x <= x => true (reflexive) +// A value is always less than or equal to itself. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, ["<=", "x", "x"]], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc new file mode 100644 index 00000000000..5ce12283586 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc @@ -0,0 +1,9 @@ +//@ run: pass +//@ description: Identical operand simplification: x != x => false (irreflexive) +// When both operands of inequality are the same place, the result is always false. +[ + "if", + { "#literal": true }, + ["let", "x", { "#literal": 42 }, ["!=", "x", "x"]], + { "#literal": true } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc new file mode 100644 index 00000000000..ae7e07d0218 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Identity simplification: x && true => x +// The result of x && true is just x when x is boolean. +[ + "if", + { "#literal": true }, + [ + "let", + "x", + ["==", { "#literal": 1 }, { "#literal": 1 }], + ["&&", "x", { "#literal": true }] + ], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc new file mode 100644 index 00000000000..ae48f1d1fea --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Identity simplification: x || false => x +// The result of x || false is just x when x is boolean. +[ + "if", + { "#literal": true }, + [ + "let", + "x", + ["==", { "#literal": 1 }, { "#literal": 1 }], + ["||", "x", { "#literal": false }] + ], + { "#literal": false } +] From 760f90448e5f1178f791af3c90b9269ad535e9da Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 19:06:53 +0100 Subject: [PATCH 10/14] chore: docs --- .../hashql/mir/src/pass/transform/inst_simplify/tests.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs index 9faeb92bb40..4c0cbf6f4f5 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs @@ -1,9 +1,3 @@ -//! Insta tests for `InstSimplify` operations not expressible in HashQL source. -//! -//! These tests cover bitwise operations on integers, unary operations, and block -//! parameter propagation which require direct MIR construction. Boolean logical -//! operations and comparisons are tested via compiletest. - use std::path::PathBuf; use bstr::ByteVec as _; From a8e197cc31ea52dadd16ab8cfbc965d9c44e2c1d Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 19:32:03 +0100 Subject: [PATCH 11/14] feat: tests --- .../suite/mir_pass_transform_inst_simplify.rs | 123 +++ .../hashql/compiletest/src/suite/mod.rs | 8 +- libs/@local/hashql/core/src/id/vec.rs | 5 +- .../src/pass/transform/inst_simplify/mod.rs | 9 +- .../src/pass/transform/inst_simplify/tests.rs | 2 +- .../inst_simplify/annihilator-and-false.jsonc | 1 - .../annihilator-and-false.stdout | 41 + .../inst_simplify/annihilator-or-true.jsonc | 1 - .../inst_simplify/annihilator-or-true.stdout | 41 + .../block_param_predecessors_agree.snap | 51 ++ .../block_param_predecessors_disagree.snap | 51 ++ .../block_param_single_predecessor.snap | 35 + .../inst_simplify/chained-const-fold.jsonc | 1 - .../inst_simplify/chained-const-fold.stdout | 41 + .../ui/pass/inst_simplify/const-fold-eq.jsonc | 2 - .../pass/inst_simplify/const-fold-eq.stdout | 37 + .../ui/pass/inst_simplify/const-fold-gt.jsonc | 2 - .../pass/inst_simplify/const-fold-gt.stdout | 37 + .../pass/inst_simplify/const-fold-gte.jsonc | 2 - .../pass/inst_simplify/const-fold-gte.stdout | 37 + .../ui/pass/inst_simplify/const-fold-lt.jsonc | 2 - .../pass/inst_simplify/const-fold-lt.stdout | 37 + .../pass/inst_simplify/const-fold-lte.jsonc | 2 - .../pass/inst_simplify/const-fold-lte.stdout | 37 + .../ui/pass/inst_simplify/const-fold-ne.jsonc | 2 - .../pass/inst_simplify/const-fold-ne.stdout | 37 + .../const-propagation-locals.jsonc | 1 - .../const-propagation-locals.stdout | 45 + .../inst_simplify/const_fold_bit_and.snap | 25 + .../pass/inst_simplify/const_fold_bit_or.snap | 25 + .../inst_simplify/const_fold_unary_neg.snap | 25 + .../inst_simplify/const_fold_unary_not.snap | 25 + .../idempotent_to_const_forwarding.snap | 33 + .../inst_simplify/identical-operand-eq.jsonc | 1 - .../inst_simplify/identical-operand-eq.stdout | 41 + .../inst_simplify/identical-operand-gt.jsonc | 1 - .../inst_simplify/identical-operand-gt.stdout | 41 + .../inst_simplify/identical-operand-gte.jsonc | 1 - .../identical-operand-gte.stdout | 41 + .../inst_simplify/identical-operand-lt.jsonc | 1 - .../inst_simplify/identical-operand-lt.stdout | 41 + .../inst_simplify/identical-operand-lte.jsonc | 1 - .../identical-operand-lte.stdout | 41 + .../inst_simplify/identical-operand-ne.jsonc | 1 - .../inst_simplify/identical-operand-ne.stdout | 41 + .../identical_operand_bit_and.snap | 25 + .../identical_operand_bit_or.snap | 25 + .../inst_simplify/identity-and-true.jsonc | 1 - .../inst_simplify/identity-and-true.stdout | 41 + .../inst_simplify/identity-or-false.jsonc | 1 - .../inst_simplify/identity-or-false.stdout | 41 + .../inst_simplify/identity_bit_or_zero.snap | 25 + .../ui/pass/inst_simplify/showcase.aux.svg | 846 ++++++++++++++++++ .../ui/pass/inst_simplify/showcase.jsonc | 39 + .../ui/pass/inst_simplify/showcase.stdout | 61 ++ 55 files changed, 2149 insertions(+), 31 deletions(-) create mode 100644 libs/@local/hashql/compiletest/src/suite/mir_pass_transform_inst_simplify.rs create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_disagree.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_and.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_or.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_neg.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_and.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_or.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity_bit_or_zero.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_inst_simplify.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_inst_simplify.rs new file mode 100644 index 00000000000..d05bc9d5aca --- /dev/null +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_inst_simplify.rs @@ -0,0 +1,123 @@ +use std::io::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::{TransformPass as _, transform::InstSimplify}, +}; + +use super::{ + RunContext, Suite, SuiteDiagnostic, common::process_issues, + mir_pass_transform_sroa::mir_pass_transform_sroa, +}; +use crate::suite::{ + common::Header, + mir_pass_transform_cfg_simplify::mir_pass_transform_cfg_simplify_default_renderer, + mir_reify::{d2_output_enabled, mir_format_d2, mir_format_text, mir_spawn_d2}, +}; + +pub(crate) fn mir_pass_transform_inst_simplify<'heap>( + heap: &'heap Heap, + expr: Expr<'heap>, + interner: &Interner<'heap>, + render: impl FnOnce(&'heap Heap, &Environment<'heap>, DefId, &DefIdSlice>), + environment: &mut Environment<'heap>, + diagnostics: &mut Vec, +) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { + let (root, mut bodies, mut scratch) = + mir_pass_transform_sroa(heap, expr, interner, render, environment, diagnostics)?; + + let mut context = MirContext { + heap, + env: environment, + interner, + diagnostics: DiagnosticIssues::new(), + }; + + let mut pass = InstSimplify::new_in(&mut scratch); + for body in bodies.as_mut_slice() { + pass.run(&mut context, body); + } + + process_issues(diagnostics, context.diagnostics)?; + Ok((root, bodies, scratch)) +} + +pub(crate) struct MirPassTransformInstSimplify; + +impl Suite for MirPassTransformInstSimplify { + fn priority(&self) -> usize { + 1 + } + + fn name(&self) -> &'static str { + "mir/pass/transform/inst-simplify" + } + + fn description(&self) -> &'static str { + "Instruction Simplification in the MIR" + } + + fn secondary_file_extensions(&self) -> &[&str] { + &["svg"] + } + + 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); + + let (root, bodies, _) = mir_pass_transform_inst_simplify( + heap, + expr, + &interner, + mir_pass_transform_cfg_simplify_default_renderer( + &mut buffer, + d2.as_mut().map(|(writer, _)| writer), + ), + &mut environment, + diagnostics, + )?; + + let _ = writeln!(buffer, "\n{}\n", Header::new("MIR after InstSimplify")); + mir_format_text(heap, &environment, &mut buffer, root, &bodies); + + if let Some((mut writer, handle)) = d2 { + writeln!(writer, "final: 'MIR after InstSimplify' {{") + .expect("should be able to write to buffer"); + mir_format_d2(heap, &environment, &mut writer, root, &bodies); + writeln!(writer, "}}").expect("should be able to write to buffer"); + + 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 cb32ca0907c..33fb45b0a62 100644 --- a/libs/@local/hashql/compiletest/src/suite/mod.rs +++ b/libs/@local/hashql/compiletest/src/suite/mod.rs @@ -23,6 +23,7 @@ mod hir_reify; mod mir_pass_analysis_data_dependency; mod mir_pass_transform_cfg_simplify; mod mir_pass_transform_dse; +mod mir_pass_transform_inst_simplify; mod mir_pass_transform_sroa; mod mir_reify; mod parse_syntax_dump; @@ -55,8 +56,10 @@ use self::{ hir_lower_thunking::HirLowerThunkingSuite, hir_reify::HirReifySuite, mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency, mir_pass_transform_cfg_simplify::MirPassTransformCfgSimplify, - mir_pass_transform_dse::MirPassTransformDse, mir_pass_transform_sroa::MirPassTransformSroa, - mir_reify::MirReifySuite, parse_syntax_dump::ParseSyntaxDumpSuite, + mir_pass_transform_dse::MirPassTransformDse, + mir_pass_transform_inst_simplify::MirPassTransformInstSimplify, + mir_pass_transform_sroa::MirPassTransformSroa, mir_reify::MirReifySuite, + parse_syntax_dump::ParseSyntaxDumpSuite, }; use crate::executor::TrialError; @@ -153,6 +156,7 @@ const SUITES: &[&dyn Suite] = &[ &MirPassAnalysisDataDependency, &MirPassTransformCfgSimplify, &MirPassTransformDse, + &MirPassTransformInstSimplify, &MirPassTransformSroa, &MirReifySuite, &ParseSyntaxDumpSuite, diff --git a/libs/@local/hashql/core/src/id/vec.rs b/libs/@local/hashql/core/src/id/vec.rs index fd6e02413c2..2a4608895b3 100644 --- a/libs/@local/hashql/core/src/id/vec.rs +++ b/libs/@local/hashql/core/src/id/vec.rs @@ -1,6 +1,6 @@ use alloc::{alloc::Global, vec}; use core::{ - alloc::Allocator, + alloc::{AllocError, Allocator}, borrow::{Borrow, BorrowMut}, cmp::Ordering, fmt::{self, Debug}, @@ -8,10 +8,9 @@ use core::{ marker::PhantomData, ops::{Deref, DerefMut}, }; -use std::alloc::AllocError; use super::{Id, slice::IdSlice}; -use crate::heap::{CloneIn, TryCloneIn}; +use crate::heap::TryCloneIn; /// A growable vector that uses typed IDs for indexing instead of raw `usize` values. /// diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index 33d2b52ff19..59860910abd 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -302,7 +302,7 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { if let Operand::Place(place) = operand && place.projections.is_empty() - && let Some(int) = self.evaluated[place.local] + && let Some(&Some(int)) = self.evaluated.get(place.local) { return OperandKind::Int(int); } @@ -578,6 +578,13 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for InstSimplifyVisitor<'_, 'heap, A Ok(()) = visit::r#mut::walk_statement_assign(self, location, assign); let Some(trampoline) = self.trampoline.take() else { + // No simplification occurred, but we still need to track constants for propagation. + if let RValue::Load(Operand::Constant(Constant::Int(int))) = assign.rhs + && assign.lhs.projections.is_empty() + { + self.evaluated.insert(assign.lhs.local, int); + } + return Ok(()); }; diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs index 4c0cbf6f4f5..c7ee100a8b1 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs @@ -188,7 +188,7 @@ fn const_fold_unary_neg() { let env = Environment::new(&heap); let result = builder.local("result", TypeBuilder::synthetic(&env).integer()); - let const_5 = builder.const_int(5); + let const_5 = builder.const_int(-5); let bb0 = builder.reserve_block([]); builder diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc index be58699384e..177bf24e222 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Annihilator simplification: x && false => false -// x && false is always false regardless of x. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout new file mode 100644 index 00000000000..9caa65ae28a --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 1 + %2 = %1 & 0 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(1) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + %1 = 1 + %2 = 0 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc index f843e34b306..44e39fe89f1 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Annihilator simplification: x || true => true -// x || true is always true regardless of x. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout new file mode 100644 index 00000000000..659b50f806b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 2 + %2 = %1 | 1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + %1 = 0 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap new file mode 100644 index 00000000000..20b9a6549b4 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap @@ -0,0 +1,51 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}(%0: Integer) -> Boolean { + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(%0) -> [0: bb1(), 1: bb2()] + } + + bb1(): { + goto -> bb3(42) + } + + bb2(): { + goto -> bb3(42) + } + + bb3(%1): { + %2 = %1 == 42 + + return %2 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}(%0: Integer) -> Boolean { + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(%0) -> [0: bb1(), 1: bb2()] + } + + bb1(): { + goto -> bb3(42) + } + + bb2(): { + goto -> bb3(42) + } + + bb3(%1): { + %2 = 1 + + return %2 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_disagree.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_disagree.snap new file mode 100644 index 00000000000..12918418674 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_disagree.snap @@ -0,0 +1,51 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}(%0: Integer) -> Boolean { + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(%0) -> [0: bb1(), 1: bb2()] + } + + bb1(): { + goto -> bb3(1) + } + + bb2(): { + goto -> bb3(2) + } + + bb3(%1): { + %2 = %1 == 1 + + return %2 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}(%0: Integer) -> Boolean { + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(%0) -> [0: bb1(), 1: bb2()] + } + + bb1(): { + goto -> bb3(1) + } + + bb2(): { + goto -> bb3(2) + } + + bb3(%1): { + %2 = %1 == 1 + + return %2 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap new file mode 100644 index 00000000000..a7f60fc8baf --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap @@ -0,0 +1,35 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}() -> Boolean { + let %0: Integer + let %1: Boolean + + bb0(): { + goto -> bb1(5) + } + + bb1(%0): { + %1 = %0 == 5 + + return %1 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}() -> Boolean { + let %0: Integer + let %1: Boolean + + bb0(): { + goto -> bb1(5) + } + + bb1(%0): { + %1 = 1 + + return %1 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc index 639667c5952..9756ece3742 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Chained constant folding where a folded result is used in another comparison -// (1 == 2) folds to false, then (false == false) folds to true. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout new file mode 100644 index 00000000000..df3c51fa9a1 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 2 + %2 = %1 == 0 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + %1 = 0 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc index 42680c65001..f346c468d7b 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.jsonc @@ -1,7 +1,5 @@ //@ run: pass //@ description: Constant folding for equality comparison of two constants -// Before: _0 = (1 == 2) evaluates at runtime -// After: _0 = false (folded at compile time) [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout new file mode 100644 index 00000000000..99d9962fab4 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout @@ -0,0 +1,37 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 2 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + %1 = 0 + %0 = %1 + + return %1 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc index cc83a2ab0f3..45d064da948 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.jsonc @@ -1,7 +1,5 @@ //@ run: pass //@ description: Constant folding for greater-than comparison -// Before: _0 = (10 > 5) evaluates at runtime -// After: _0 = true (folded at compile time) [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout new file mode 100644 index 00000000000..6119322d227 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout @@ -0,0 +1,37 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 10 > 5 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + %1 = 1 + %0 = %1 + + return %1 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc index c6ed52f2868..d9b2590d070 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.jsonc @@ -1,7 +1,5 @@ //@ run: pass //@ description: Constant folding for greater-than-or-equal comparison -// Before: _0 = (5 >= 5) evaluates at runtime -// After: _0 = true (folded at compile time) [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout new file mode 100644 index 00000000000..800a6493c6b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout @@ -0,0 +1,37 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 5 >= 5 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + %1 = 1 + %0 = %1 + + return %1 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc index 87375c2544b..941437dbfeb 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.jsonc @@ -1,7 +1,5 @@ //@ run: pass //@ description: Constant folding for less-than comparison of two constants -// Before: _0 = (3 < 5) evaluates at runtime -// After: _0 = true (folded at compile time) [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout new file mode 100644 index 00000000000..dfd17f5efb1 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout @@ -0,0 +1,37 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 3 < 5 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + %1 = 1 + %0 = %1 + + return %1 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc index e5578c7bb80..18875d0aa49 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.jsonc @@ -1,7 +1,5 @@ //@ run: pass //@ description: Constant folding for less-than-or-equal comparison -// Before: _0 = (5 <= 5) evaluates at runtime -// After: _0 = true (folded at compile time) [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout new file mode 100644 index 00000000000..31efd47a78a --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout @@ -0,0 +1,37 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 5 <= 5 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + %1 = 1 + %0 = %1 + + return %1 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc index f0645930359..667c6e90c72 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.jsonc @@ -1,7 +1,5 @@ //@ run: pass //@ description: Constant folding for inequality comparison of two constants -// Before: _0 = (1 != 2) evaluates at runtime -// After: _0 = true (folded at compile time) [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout new file mode 100644 index 00000000000..83f22bbc9be --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout @@ -0,0 +1,37 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 != 2 + + goto -> bb3(%1) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#1}() -> Boolean { + let %0: Boolean + let %1: Boolean + + bb0(): { + %1 = 1 + %0 = %1 + + return %1 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc index 61ac977db3d..d2eaa8e24ec 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Constants assigned to locals are tracked and used in subsequent folding -// x = 5, y = 3, then x == y folds to false because both are known constants. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout new file mode 100644 index 00000000000..f1497ac6368 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout @@ -0,0 +1,45 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#3}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Integer + let %3: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 5 + %2 = 3 + %3 = %1 == %2 + + goto -> bb3(%3) + } + + bb2(): { + goto -> bb3(1) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#3}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Integer + let %3: Boolean + + bb0(): { + %1 = 5 + %2 = 3 + %3 = 0 + %0 = %3 + + return %3 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_and.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_and.snap new file mode 100644 index 00000000000..bbc9335b167 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_and.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}() -> Integer { + let %0: Integer + + bb0(): { + %0 = 2 & 3 + + return %0 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}() -> Integer { + let %0: Integer + + bb0(): { + %0 = 2 + + return %0 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_or.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_or.snap new file mode 100644 index 00000000000..40e2c225d4e --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_bit_or.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}() -> Integer { + let %0: Integer + + bb0(): { + %0 = 2 | 1 + + return %0 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}() -> Integer { + let %0: Integer + + bb0(): { + %0 = 3 + + return %0 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_neg.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_neg.snap new file mode 100644 index 00000000000..71f0e8c78ab --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_neg.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}() -> Integer { + let %0: Integer + + bb0(): { + %0 = --5 + + return %0 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}() -> Integer { + let %0: Integer + + bb0(): { + %0 = 5 + + return %0 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap new file mode 100644 index 00000000000..a6f3582d2c9 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}() -> Boolean { + let %0: Boolean + + bb0(): { + %0 = !1 + + return %0 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}() -> Boolean { + let %0: Boolean + + bb0(): { + %0 = 0 + + return %0 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap new file mode 100644 index 00000000000..85acef647b9 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap @@ -0,0 +1,33 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}() -> Boolean { + let %0: Integer + let %1: Integer + let %2: Boolean + + bb0(): { + %0 = 42 + %1 = %0 & %0 + %2 = %1 == 42 + + return %2 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}() -> Boolean { + let %0: Integer + let %1: Integer + let %2: Boolean + + bb0(): { + %0 = 42 + %1 = 42 + %2 = 1 + + return %2 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc index 761e68ea98b..7dffb0edee7 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identical operand simplification: x == x => true (reflexive) -// When both operands of equality are the same place, the result is always true. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout new file mode 100644 index 00000000000..47b282f0974 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = %1 == %1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + %1 = 42 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc index 6383f10d791..c59ca1066bb 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identical operand simplification: x > x => false (irreflexive) -// A value is never greater than itself. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout new file mode 100644 index 00000000000..87ce2f08dd9 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = %1 > %1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(1) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + %1 = 42 + %2 = 0 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc index 4ce823a67b4..20eda452631 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identical operand simplification: x >= x => true (reflexive) -// A value is always greater than or equal to itself. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout new file mode 100644 index 00000000000..5e8779c91ff --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = %1 >= %1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + %1 = 42 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc index 83655717e65..b404172f076 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identical operand simplification: x < x => false (irreflexive) -// A value is never less than itself. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout new file mode 100644 index 00000000000..dcc74bb2a59 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = %1 < %1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(1) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + %1 = 42 + %2 = 0 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc index 44bdce60c86..66ac9a88d7f 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identical operand simplification: x <= x => true (reflexive) -// A value is always less than or equal to itself. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout new file mode 100644 index 00000000000..a582e4b8621 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = %1 <= %1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + %1 = 42 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc index 5ce12283586..5d8f4db284c 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identical operand simplification: x != x => false (irreflexive) -// When both operands of inequality are the same place, the result is always false. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout new file mode 100644 index 00000000000..f0ee0186aab --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 42 + %2 = %1 != %1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(1) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Integer + let %2: Boolean + + bb0(): { + %1 = 42 + %2 = 0 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_and.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_and.snap new file mode 100644 index 00000000000..19728b0aa72 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_and.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}(%0: Integer) -> Integer { + let %1: Integer + + bb0(): { + %1 = %0 & %0 + + return %1 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}(%0: Integer) -> Integer { + let %1: Integer + + bb0(): { + %1 = %0 + + return %1 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_or.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_or.snap new file mode 100644 index 00000000000..a94d4c7eb87 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical_operand_bit_or.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}(%0: Integer) -> Integer { + let %1: Integer + + bb0(): { + %1 = %0 | %0 + + return %1 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}(%0: Integer) -> Integer { + let %1: Integer + + bb0(): { + %1 = %0 + + return %1 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc index ae7e07d0218..ec064b599d7 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identity simplification: x && true => x -// The result of x && true is just x when x is boolean. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout new file mode 100644 index 00000000000..f66fb763a75 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 1 + %2 = %1 & 1 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + %1 = 1 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc index ae48f1d1fea..8e0045398ea 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.jsonc @@ -1,6 +1,5 @@ //@ run: pass //@ description: Identity simplification: x || false => x -// The result of x || false is just x when x is boolean. [ "if", { "#literal": true }, diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout new file mode 100644 index 00000000000..81aae683325 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout @@ -0,0 +1,41 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 1 + %2 = %1 | 0 + + goto -> bb3(%2) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#2}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + + bb0(): { + %1 = 1 + %2 = 1 + %0 = %2 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity_bit_or_zero.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity_bit_or_zero.snap new file mode 100644 index 00000000000..fb533c0d2eb --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity_bit_or_zero.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/inst_simplify/tests.rs +expression: value +--- +fn {intrinsic#4294967040}(%0: Integer) -> Integer { + let %1: Integer + + bb0(): { + %1 = %0 | 0 + + return %1 + } +} + +------------------------------------ + +fn {intrinsic#4294967040}(%0: Integer) -> Integer { + let %1: Integer + + bb0(): { + %1 = %0 + + return %1 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg new file mode 100644 index 00000000000..5d18449c258 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg @@ -0,0 +1,846 @@ +Initial MIRMIR after InstSimplify

thunk {thunk#7}() -> Boolean

+

thunk {thunk#7}() -> Boolean

+
bb0()
MIR
TswitchInt(1)
bb2()
MIR
Tgoto
bb1()
MIR
0%1 = 1 == 2
1%2 = 3 < 5
2%3 = %2 & 1
3%4 = 42
4%5 = %4 == %4
5%6 = %1 | %3
6%7 = %6 & %5
Tgoto
bb3(%0)
MIR
Treturn %0
bb0()
MIR
0%1 = 0
1%2 = 1
2%3 = 1
3%4 = 42
4%5 = 1
5%6 = 1
6%7 = 1
7%0 = %7
Treturn %7
()0()1(%7)(0) + + + + + +
diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc new file mode 100644 index 00000000000..9e6387c3925 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc @@ -0,0 +1,39 @@ +//@ run: pass +//@ description: Showcase of InstSimplify transformations through the pipeline +//@ suite#d2: true +// This test demonstrates multiple simplification patterns working together: +// - Constant folding: 1 == 2 => false, 3 < 5 => true +// - Chained propagation: folded results feed into subsequent operations +// - Identity: result && true => result +// - Identical operands: x == x => true +[ + "if", + { "#literal": true }, + [ + "let", + "a", + ["==", { "#literal": 1 }, { "#literal": 2 }], + [ + "let", + "b", + ["<", { "#literal": 3 }, { "#literal": 5 }], + [ + "let", + "c", + ["&&", "b", { "#literal": true }], + [ + "let", + "x", + { "#literal": 42 }, + [ + "let", + "d", + ["==", "x", "x"], + ["&&", ["||", "a", "c"], "d"] + ] + ] + ] + ] + ], + { "#literal": false } +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout new file mode 100644 index 00000000000..04d56dc87fc --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout @@ -0,0 +1,61 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +*thunk {thunk#7}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + let %3: Boolean + let %4: Integer + let %5: Boolean + let %6: Boolean + let %7: Boolean + + bb0(): { + switchInt(1) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %1 = 1 == 2 + %2 = 3 < 5 + %3 = %2 & 1 + %4 = 42 + %5 = %4 == %4 + %6 = %1 | %3 + %7 = %6 & %5 + + goto -> bb3(%7) + } + + bb2(): { + goto -> bb3(0) + } + + bb3(%0): { + return %0 + } +} +════ MIR after InstSimplify ════════════════════════════════════════════════════ + +*thunk {thunk#7}() -> Boolean { + let %0: Boolean + let %1: Boolean + let %2: Boolean + let %3: Boolean + let %4: Integer + let %5: Boolean + let %6: Boolean + let %7: Boolean + + bb0(): { + %1 = 0 + %2 = 1 + %3 = 1 + %4 = 42 + %5 = 1 + %6 = 1 + %7 = 1 + %0 = %7 + + return %7 + } +} \ No newline at end of file From fff423780f9d03c5702577ba0a84cc2f732fa218 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 19:43:39 +0100 Subject: [PATCH 12/14] fix: format --- .../hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc index 9e6387c3925..eed66ef97bb 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.jsonc @@ -25,12 +25,7 @@ "let", "x", { "#literal": 42 }, - [ - "let", - "d", - ["==", "x", "x"], - ["&&", ["||", "a", "c"], "d"] - ] + ["let", "d", ["==", "x", "x"], ["&&", ["||", "a", "c"], "d"]] ] ] ] From ab31a50461297d6c98476437d01d7055a5596aa8 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 20:15:40 +0100 Subject: [PATCH 13/14] chore: benchmarks --- .../src/suite/mir_pass_transform_dse.rs | 5 +- libs/@local/hashql/mir/benches/transform.rs | 83 ++- .../hashql/mir/src/pass/transform/dbe/mod.rs | 5 +- .../mir/tests/ui/pass/dse/showcase.aux.svg | 570 +++++++++--------- .../mir/tests/ui/pass/dse/showcase.stdout | 11 +- 5 files changed, 370 insertions(+), 304 deletions(-) diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_dse.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_dse.rs index 38ea2439688..88087d9c208 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_dse.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_dse.rs @@ -16,7 +16,7 @@ use hashql_mir::{ use super::{ RunContext, Suite, SuiteDiagnostic, common::process_issues, - mir_pass_transform_sroa::mir_pass_transform_sroa, + mir_pass_transform_inst_simplify::mir_pass_transform_inst_simplify, }; use crate::suite::{ common::Header, @@ -33,7 +33,7 @@ pub(crate) fn mir_pass_transform_dse<'heap>( diagnostics: &mut Vec, ) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { let (root, mut bodies, mut scratch) = - mir_pass_transform_sroa(heap, expr, interner, render, environment, diagnostics)?; + mir_pass_transform_inst_simplify(heap, expr, interner, render, environment, diagnostics)?; let mut context = MirContext { heap, @@ -42,6 +42,7 @@ pub(crate) fn mir_pass_transform_dse<'heap>( diagnostics: DiagnosticIssues::new(), }; + // CFG -> SROA -> Inst -> DSE let mut pass = DeadStoreElimination::new_in(&mut scratch); for body in bodies.as_mut_slice() { pass.run(&mut context, body); diff --git a/libs/@local/hashql/mir/benches/transform.rs b/libs/@local/hashql/mir/benches/transform.rs index d3ea26c06aa..ff96f7da621 100644 --- a/libs/@local/hashql/mir/benches/transform.rs +++ b/libs/@local/hashql/mir/benches/transform.rs @@ -1,7 +1,8 @@ #![expect( clippy::min_ident_chars, clippy::many_single_char_names, - clippy::significant_drop_tightening + clippy::significant_drop_tightening, + clippy::similar_names )] use core::hint::black_box; @@ -20,7 +21,7 @@ use hashql_mir::{ op, pass::{ TransformPass, - transform::{CfgSimplify, DeadStoreElimination, Sroa}, + transform::{CfgSimplify, DeadStoreElimination, InstSimplify, Sroa}, }, }; @@ -151,6 +152,62 @@ fn create_dead_store_cfg<'heap>( builder.finish(0, TypeBuilder::synthetic(env).integer()) } +/// Creates a body with patterns that `InstSimplify` can optimize. +/// +/// Structure: +/// ```text +/// bb0: +/// a = 1 == 2 // const fold -> false +/// b = 3 < 5 // const fold -> true +/// c = b && true // identity -> b +/// x = 42 +/// y = x & x // idempotent -> x +/// d = x == x // identical operand -> true +/// e = a || c // const fold (a=false, c=true) -> true +/// f = e && d // const fold -> true +/// return f +/// ``` +fn create_inst_simplify_cfg<'heap>( + env: &Environment<'heap>, + interner: &Interner<'heap>, +) -> Body<'heap> { + let mut builder = BodyBuilder::new(interner); + let int_ty = TypeBuilder::synthetic(env).integer(); + let bool_ty = TypeBuilder::synthetic(env).boolean(); + + let a = builder.local("a", bool_ty); + let b = builder.local("b", bool_ty); + let c = builder.local("c", bool_ty); + let x = builder.local("x", int_ty); + let y = builder.local("y", int_ty); + let d = builder.local("d", bool_ty); + let e = builder.local("e", bool_ty); + let f = builder.local("f", bool_ty); + + let const_1 = builder.const_int(1); + let const_2 = builder.const_int(2); + let const_3 = builder.const_int(3); + let const_5 = builder.const_int(5); + let const_42 = builder.const_int(42); + let const_true = builder.const_bool(true); + + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(a, |rv| rv.binary(const_1, op![==], const_2)) + .assign_place(b, |rv| rv.binary(const_3, op![<], const_5)) + .assign_place(c, |rv| rv.binary(b, op![&], const_true)) + .assign_place(x, |rv| rv.load(const_42)) + .assign_place(y, |rv| rv.binary(x, op![&], x)) + .assign_place(d, |rv| rv.binary(x, op![==], x)) + .assign_place(e, |rv| rv.binary(a, op![|], c)) + .assign_place(f, |rv| rv.binary(e, op![&], d)) + .ret(f); + + builder.finish(0, TypeBuilder::synthetic(env).boolean()) +} + /// Creates a larger CFG with multiple branches and join points for more realistic benchmarking. /// /// Structure: @@ -359,6 +416,23 @@ fn dse(criterion: &mut Criterion) { }); } +fn inst_simplify(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("inst_simplify"); + + group.bench_function("foldable", |bencher| { + run(bencher, create_inst_simplify_cfg, InstSimplify::new()); + }); + group.bench_function("linear", |bencher| { + run(bencher, create_linear_cfg, InstSimplify::new()); + }); + group.bench_function("diamond", |bencher| { + run(bencher, create_diamond_cfg, InstSimplify::new()); + }); + group.bench_function("complex", |bencher| { + run(bencher, create_complex_cfg, InstSimplify::new()); + }); +} + fn pipeline(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("pipeline"); @@ -368,6 +442,7 @@ fn pipeline(criterion: &mut Criterion) { run_bencher(bencher, create_linear_cfg, |context, body| { CfgSimplify::new_in(&mut scratch).run(context, body); Sroa::new_in(&mut scratch).run(context, body); + InstSimplify::new().run(context, body); DeadStoreElimination::new_in(&mut scratch).run(context, body); }); }); @@ -377,6 +452,7 @@ fn pipeline(criterion: &mut Criterion) { run_bencher(bencher, create_diamond_cfg, |context, body| { CfgSimplify::new_in(&mut scratch).run(context, body); Sroa::new_in(&mut scratch).run(context, body); + InstSimplify::new().run(context, body); DeadStoreElimination::new_in(&mut scratch).run(context, body); }); }); @@ -386,10 +462,11 @@ fn pipeline(criterion: &mut Criterion) { run_bencher(bencher, create_complex_cfg, |context, body| { CfgSimplify::new_in(&mut scratch).run(context, body); Sroa::new_in(&mut scratch).run(context, body); + InstSimplify::new().run(context, body); DeadStoreElimination::new_in(&mut scratch).run(context, body); }); }); } -criterion_group!(benches, cfg_simplify, sroa, dse, pipeline); +criterion_group!(benches, cfg_simplify, sroa, dse, inst_simplify, pipeline); criterion_main!(benches); diff --git a/libs/@local/hashql/mir/src/pass/transform/dbe/mod.rs b/libs/@local/hashql/mir/src/pass/transform/dbe/mod.rs index f0ac29b8d96..2a87da7f3c3 100644 --- a/libs/@local/hashql/mir/src/pass/transform/dbe/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/dbe/mod.rs @@ -139,9 +139,8 @@ impl<'env, 'heap, A: BumpAllocator> TransformPass<'env, 'heap> for DeadBlockElim write_index.increment_by(1); } - body.basic_blocks - .as_mut_preserving_cfg() - .truncate(write_index); + // We **remove** the blocks, so cfg must be invalidated. + body.basic_blocks.as_mut().truncate(write_index); } } diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg index bdd43a6727c..67bfedd1776 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg @@ -1,20 +1,20 @@ -Initial MIRMIR after DSE

thunk {thunk#7}() -> Integer

-

thunk {thunk#7}() -> Integer

-
bb0()
MIR
TswitchInt(1)
bb5()
MIR
Tgoto
bb1()
MIR
0%1 = 10
1%2 = 20
2%3 = %1 == %2
3%4 = %1 < %2
4%5 = %1 > %2
5%6 = 999
TswitchInt(%3)
bb3()
MIR
Tgoto
bb2()
MIR
Tgoto
bb4(%7)
MIR
Tgoto
bb6(%0)
MIR
Treturn %0
bb0()
MIR
0%0 = 10 == 20
TswitchInt(%0)
bb1(%1)
MIR
Treturn %1
()0()1()0()1(1)(0)(%7)(0)(0)0(1)1 - +Initial MIRMIR after DSE

thunk {thunk#7}() -> Integer

+

thunk {thunk#7}() -> Integer

+
bb0()
MIR
TswitchInt(1)
bb5()
MIR
Tgoto
bb1()
MIR
0%1 = 10
1%2 = 20
2%3 = %1 == %2
3%4 = %1 < %2
4%5 = %1 > %2
5%6 = 999
TswitchInt(%3)
bb3()
MIR
Tgoto
bb2()
MIR
Tgoto
bb4(%7)
MIR
Tgoto
bb6(%0)
MIR
Treturn %0
bb0()
MIR
Treturn 0
()0()1()0()1(1)(0)(%7)(0) + @@ -847,6 +847,4 @@ - -
diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout index 4eac94e6d7b..cc7d57d855b 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout @@ -48,16 +48,7 @@ ════ MIR after DSE ═════════════════════════════════════════════════════════════ *thunk {thunk#7}() -> Integer { - let %0: Boolean - let %1: Integer - bb0(): { - %0 = 10 == 20 - - switchInt(%0) -> [0: bb1(0), 1: bb1(1)] - } - - bb1(%1): { - return %1 + return 0 } } \ No newline at end of file From 08246f0f4f45816525af758b62a0b3f6d9699380 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Sun, 21 Dec 2025 21:22:05 +0100 Subject: [PATCH 14/14] chore: snapshots --- .../mir/tests/ui/pass/dse/showcase.aux.svg | 570 +++++++++--------- .../mir/tests/ui/pass/dse/showcase.stdout | 11 +- 2 files changed, 296 insertions(+), 285 deletions(-) diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg index 67bfedd1776..c5ce3a75f53 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg @@ -1,20 +1,20 @@ -Initial MIRMIR after DSE

thunk {thunk#7}() -> Integer

-

thunk {thunk#7}() -> Integer

-
bb0()
MIR
TswitchInt(1)
bb5()
MIR
Tgoto
bb1()
MIR
0%1 = 10
1%2 = 20
2%3 = %1 == %2
3%4 = %1 < %2
4%5 = %1 > %2
5%6 = 999
TswitchInt(%3)
bb3()
MIR
Tgoto
bb2()
MIR
Tgoto
bb4(%7)
MIR
Tgoto
bb6(%0)
MIR
Treturn %0
bb0()
MIR
Treturn 0
()0()1()0()1(1)(0)(%7)(0) - +Initial MIRMIR after DSE

thunk {thunk#7}() -> Integer

+

thunk {thunk#7}() -> Integer

+
bb0()
MIR
TswitchInt(1)
bb5()
MIR
Tgoto
bb1()
MIR
0%1 = 10
1%2 = 20
2%3 = %1 == %2
3%4 = %1 < %2
4%5 = %1 > %2
5%6 = 999
TswitchInt(%3)
bb3()
MIR
Tgoto
bb2()
MIR
Tgoto
bb4(%7)
MIR
Tgoto
bb6(%0)
MIR
Treturn %0
bb0()
MIR
0%0 = 0
TswitchInt(%0)
bb1(%1)
MIR
Treturn %1
()0()1()0()1(1)(0)(%7)(0)(0)0(1)1 + @@ -847,4 +847,6 @@ + +
diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout index cc7d57d855b..eb3cbefe936 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout @@ -48,7 +48,16 @@ ════ MIR after DSE ═════════════════════════════════════════════════════════════ *thunk {thunk#7}() -> Integer { + let %0: Boolean + let %1: Integer + bb0(): { - return 0 + %0 = 0 + + switchInt(%0) -> [0: bb1(0), 1: bb1(1)] + } + + bb1(%1): { + return %1 } } \ No newline at end of file