From bb4137a33f42f7001875fb02383c7e0b3eb43e5e Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 17:19:55 +0100 Subject: [PATCH 1/9] feat: traversal extraction --- libs/@local/hashql/core/src/id/vec.rs | 5 + libs/@local/hashql/mir/src/body/place.rs | 6 + .../hashql/mir/src/pass/transform/mod.rs | 2 + .../pass/transform/traversal_extraction.rs | 243 ++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs diff --git a/libs/@local/hashql/core/src/id/vec.rs b/libs/@local/hashql/core/src/id/vec.rs index d828d0e7be4..3a898a88622 100644 --- a/libs/@local/hashql/core/src/id/vec.rs +++ b/libs/@local/hashql/core/src/id/vec.rs @@ -404,6 +404,11 @@ where self.raw.extend_from_slice(other.as_raw()); } + #[inline] + pub fn append(&mut self, other: &mut Self) { + self.raw.append(&mut other.raw); + } + pub fn into_iter_enumerated( self, ) -> impl DoubleEndedIterator + ExactSizeIterator { diff --git a/libs/@local/hashql/mir/src/body/place.rs b/libs/@local/hashql/mir/src/body/place.rs index 6de28794279..60ca2942f6a 100644 --- a/libs/@local/hashql/mir/src/body/place.rs +++ b/libs/@local/hashql/mir/src/body/place.rs @@ -402,6 +402,12 @@ impl<'heap> Place<'heap> { .map_or_else(|| decl[self.local].r#type, |projection| projection.r#type) } + pub fn type_id_unchecked(&self, decl: &LocalDecl<'heap>) -> TypeId { + self.projections + .last() + .map_or_else(|| decl.r#type, |projection| projection.r#type) + } + /// Returns a borrowed reference to this place. #[must_use] pub const fn as_ref(&self) -> PlaceRef<'heap, 'heap> { diff --git a/libs/@local/hashql/mir/src/pass/transform/mod.rs b/libs/@local/hashql/mir/src/pass/transform/mod.rs index c6819a45d9f..c3d44b39204 100644 --- a/libs/@local/hashql/mir/src/pass/transform/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/mod.rs @@ -12,6 +12,7 @@ mod inst_simplify; mod post_inline; mod pre_inline; mod ssa_repair; +mod traversal_extraction; pub use self::{ administrative_reduction::AdministrativeReduction, @@ -27,4 +28,5 @@ pub use self::{ post_inline::PostInline, pre_inline::PreInline, ssa_repair::SsaRepair, + traversal_extraction::{TraversalExtraction, Traversals}, }; diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs new file mode 100644 index 00000000000..36be5b8c509 --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs @@ -0,0 +1,243 @@ +use core::{alloc::Allocator, convert::Infallible}; + +use hashql_core::{heap::Heap, id::Id as _, span::SpanId}; + +use crate::{ + body::{ + Body, Source, + basic_block::{BasicBlock, BasicBlockId}, + local::{Local, LocalDecl, LocalVec}, + location::Location, + operand::Operand, + place::Place, + rvalue::RValue, + statement::{Assign, Statement, StatementKind}, + }, + context::MirContext, + pass::{Changed, TransformPass}, + visit::{self, VisitorMut, r#mut::filter}, +}; + +pub struct Traversals<'heap> { + source: Local, + derivations: LocalVec>, &'heap Heap>, +} + +impl<'heap> Traversals<'heap> { + fn with_capacity_in(source: Local, capacity: usize, heap: &'heap Heap) -> Self { + Self { + source, + derivations: LocalVec::with_capacity_in(capacity, heap), + } + } + + fn insert(&mut self, local: Local, place: Place<'heap>) { + debug_assert_eq!(place.local, self.source); + + self.derivations.insert(local, place); + } + + #[must_use] + #[inline] + pub fn lookup(&self, local: Local) -> Option<&Place<'heap>> { + self.derivations.lookup(local) + } +} + +struct TraversalExtractionVisitor<'heap, A: Allocator> { + target: Local, + target_decl: LocalDecl<'heap>, + + current_span: SpanId, + + total_locals: Local, + pending_locals: Vec, A>, + pending_statements: Vec, A>, + + traversals: Traversals<'heap>, + changed: Changed, +} + +impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap, A> { + type Filter = filter::Deep; + type Residual = Result; + type Result + = Result + where + T: 'heap; + + fn visit_operand(&mut self, _: Location, operand: &mut Operand<'heap>) -> Self::Result<()> { + let Some(place) = operand.as_place() else { + return Ok(()); + }; + + if place.local != self.target { + return Ok(()); + } + + let r#type = place.type_id_unchecked(&self.target_decl); + + // provision a new local + let new_local = self.total_locals.plus(self.pending_locals.len()); + + self.pending_locals.push(LocalDecl { + span: self.target_decl.span, + r#type, + name: None, + }); + self.pending_statements.push(Statement { + span: self.current_span, + kind: StatementKind::Assign(Assign { + lhs: Place::local(new_local), + rhs: RValue::Load(Operand::Place(*place)), + }), + }); + + // Replace the operand with the new local + *operand = Operand::Place(Place::local(new_local)); + + Ok(()) + } + + fn visit_rvalue(&mut self, location: Location, rvalue: &mut RValue<'heap>) -> Self::Result<()> { + // loads are handled by the statement_assign visitor and therefore not needed here + match rvalue { + RValue::Load(_) => return Ok(()), + RValue::Binary(_) + | RValue::Unary(_) + | RValue::Aggregate(_) + | RValue::Input(_) + | RValue::Apply(_) => {} + } + + visit::r#mut::walk_rvalue(self, location, rvalue) + } + + fn visit_statement_assign( + &mut self, + location: Location, + assign: &mut Assign<'heap>, + ) -> Self::Result<()> { + Ok(()) = visit::r#mut::walk_statement_assign(self, location, assign); + + let Assign { lhs, rhs } = assign; + + if !lhs.projections.is_empty() { + return Ok(()); + } + + let RValue::Load(Operand::Place(rhs)) = rhs else { + return Ok(()); + }; + + if rhs.local != self.target { + return Ok(()); + } + + // lhs is a traversal onto rhs (our target) + self.traversals.insert(lhs.local, *rhs); + + Ok(()) + } + + fn visit_statement( + &mut self, + location: Location, + statement: &mut Statement<'heap>, + ) -> Self::Result<()> { + self.current_span = statement.span; + + visit::r#mut::walk_statement(self, location, statement) + } + + fn visit_basic_block( + &mut self, + id: BasicBlockId, + BasicBlock { + params, + statements, + terminator, + }: &mut BasicBlock<'heap>, + ) -> Self::Result<()> { + let mut location = Location { + block: id, + statement_index: 0, + }; + + // We do not visit the basic block id here because it **cannot** be changed + self.visit_basic_block_params(location, params)?; + + location.statement_index += 1; + + while location.statement_index <= statements.len() { + // statement_index is 1 indexed, therefore the `<=` + let index = location.statement_index - 1; + + let statement = &mut statements[index]; + Ok(()) = self.visit_statement(location, statement); + + if self.pending_statements.is_empty() { + location.statement_index += 1; + continue; + } + + // We do not increment the counter on purpose, to allow us to traverse the new + // statements again. The second iteration will *not* add any new statements, and is + // therefore safe. + statements.splice(index..index, self.pending_statements.drain(..)); + self.changed = Changed::Yes; + } + + self.visit_terminator(location, terminator)?; + + Ok(()) + } +} + +pub struct TraversalExtraction<'heap, A: Allocator> { + alloc: A, + traversals: Option>, +} + +impl<'heap, A: Allocator> TraversalExtraction<'heap, A> { + pub const fn new_in(alloc: A) -> Self { + Self { + alloc, + traversals: None, + } + } + + pub const fn take_traversals(&mut self) -> Option> { + self.traversals.take() + } +} + +impl<'env, 'heap, A: Allocator> TransformPass<'env, 'heap> for TraversalExtraction<'heap, A> { + fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &mut Body<'heap>) -> Changed { + if !matches!(body.source, Source::GraphReadFilter(_)) { + self.traversals = None; + return Changed::No; + } + + debug_assert_eq!(body.args, 2); + // The second argument is the vertex, which we try to traverse; + let vertex = Local::new(1); + + let mut visitor = TraversalExtractionVisitor { + target: vertex, + target_decl: body.local_decls[vertex], + current_span: SpanId::SYNTHETIC, + total_locals: body.local_decls.bound(), + pending_locals: Vec::new_in(&self.alloc), + pending_statements: Vec::new_in(&self.alloc), + traversals: Traversals::with_capacity_in(vertex, body.local_decls.len(), context.heap), + changed: Changed::No, + }; + Ok(()) = visitor.visit_body_preserving_cfg(body); + + body.local_decls.extend(visitor.pending_locals); + + self.traversals = Some(visitor.traversals); + visitor.changed + } +} From f53e4067a323b4c14e67e3a24a3e389e5a51aa29 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 17:48:47 +0100 Subject: [PATCH 2/9] feat: traversal extraction (II) --- .../pass/transform/traversal_extraction.rs | 77 ++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs index 36be5b8c509..47fbdd74c7d 100644 --- a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs @@ -12,6 +12,7 @@ use crate::{ place::Place, rvalue::RValue, statement::{Assign, Statement, StatementKind}, + terminator::Terminator, }, context::MirContext, pass::{Changed, TransformPass}, @@ -51,7 +52,9 @@ struct TraversalExtractionVisitor<'heap, A: Allocator> { current_span: SpanId, total_locals: Local, + pending_locals: Vec, A>, + pending_locals_offset: usize, pending_statements: Vec, A>, traversals: Traversals<'heap>, @@ -77,21 +80,37 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap let r#type = place.type_id_unchecked(&self.target_decl); - // provision a new local - let new_local = self.total_locals.plus(self.pending_locals.len()); - - self.pending_locals.push(LocalDecl { - span: self.target_decl.span, - r#type, - name: None, - }); - self.pending_statements.push(Statement { - span: self.current_span, - kind: StatementKind::Assign(Assign { - lhs: Place::local(new_local), - rhs: RValue::Load(Operand::Place(*place)), - }), - }); + // Before we do anything, verify if we haven't already added a local with the same + // projection, inside the same basic block. + let new_local = if let Some(offset) = + (self.pending_locals_offset..self.pending_locals.len()).find(|&index| { + self.traversals + .lookup(self.total_locals.plus(self.pending_locals_offset + index)) + .is_some_and(|pending| pending.projections == place.projections) + }) { + // We already have a local with the same projection inside the same basic block that we + // can reuse. + self.total_locals.plus(offset) + } else { + // provision a new local + let new_local = self.total_locals.plus(self.pending_locals.len()); + self.traversals.insert(new_local, *place); + + self.pending_locals.push(LocalDecl { + span: self.target_decl.span, + r#type, + name: None, + }); + self.pending_statements.push(Statement { + span: self.current_span, + kind: StatementKind::Assign(Assign { + lhs: Place::local(new_local), + rhs: RValue::Load(Operand::Place(*place)), + }), + }); + + new_local + }; // Replace the operand with the new local *operand = Operand::Place(Place::local(new_local)); @@ -150,6 +169,15 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap visit::r#mut::walk_statement(self, location, statement) } + fn visit_terminator( + &mut self, + location: Location, + terminator: &mut Terminator<'heap>, + ) -> Self::Result<()> { + self.current_span = terminator.span; + visit::r#mut::walk_terminator(self, location, terminator) + } + fn visit_basic_block( &mut self, id: BasicBlockId, @@ -164,6 +192,8 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap statement_index: 0, }; + self.pending_locals_offset = self.pending_locals.len(); + // We do not visit the basic block id here because it **cannot** be changed self.visit_basic_block_params(location, params)?; @@ -176,20 +206,28 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap let statement = &mut statements[index]; Ok(()) = self.visit_statement(location, statement); + location.statement_index += 1; if self.pending_statements.is_empty() { - location.statement_index += 1; continue; } - // We do not increment the counter on purpose, to allow us to traverse the new - // statements again. The second iteration will *not* add any new statements, and is - // therefore safe. + // We increment the counter to the amount of statements that we are about to add, as we + // don't need to visit them again. These are only loads, which are recorded in the load + // traversal. + location.statement_index += self.pending_statements.len(); + statements.splice(index..index, self.pending_statements.drain(..)); self.changed = Changed::Yes; } self.visit_terminator(location, terminator)?; + #[expect(clippy::extend_with_drain, reason = "differing allocator")] + if !self.pending_statements.is_empty() { + statements.extend(self.pending_statements.drain(..)); + self.changed = Changed::Yes; + } + Ok(()) } } @@ -228,6 +266,7 @@ impl<'env, 'heap, A: Allocator> TransformPass<'env, 'heap> for TraversalExtracti target_decl: body.local_decls[vertex], current_span: SpanId::SYNTHETIC, total_locals: body.local_decls.bound(), + pending_locals_offset: 0, pending_locals: Vec::new_in(&self.alloc), pending_statements: Vec::new_in(&self.alloc), traversals: Traversals::with_capacity_in(vertex, body.local_decls.len(), context.heap), From 26660b9ede8cc68bc04846e7481cc6fffb533f04 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 17:55:05 +0100 Subject: [PATCH 3/9] chore: docs --- .../pass/transform/traversal_extraction.rs | 105 ++++++++++++++---- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs index 47fbdd74c7d..196449ba5a2 100644 --- a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs @@ -1,3 +1,55 @@ +//! Traversal extraction transformation pass. +//! +//! This pass extracts projections from a target local into separate bindings, creating explicit +//! intermediate assignments. It is the inverse of projection forwarding — rather than inlining +//! projections, it materializes them as distinct locals. +//! +//! # Purpose +//! +//! The primary use case is preparing graph read filters for entity traversal. When reading from +//! the graph, the filter body receives a vertex as its second argument (`Local::new(1)`). +//! Projections like `vertex.2.1` (accessing nested properties) need to be extracted so the graph +//! executor can track which paths through the vertex are actually accessed. +//! +//! # Algorithm +//! +//! The pass operates by: +//! +//! 1. Walking all operands in the MIR body +//! 2. For each place operand projecting from the target local, creating a new local and load +//! 3. Replacing the original operand with a reference to the new local +//! 4. Recording the projection path in a [`Traversals`] map for later consumption +//! +//! Deduplication is scoped to the current basic block — if the same projection appears multiple +//! times within a block, it reuses the existing extracted local rather than creating duplicates. +//! +//! Pre-existing loads (e.g., `b = a.2.1`) are detected via [`VisitorMut::visit_statement_assign`] +//! and recorded in the traversal map without generating new statements. +//! +//! # Example +//! +//! Before: +//! ```text +//! bb0: +//! _2 = input() +//! _3 = eq(_1.0.1, _2) +//! _4 = eq(_1.0.1, _1.2) +//! return and(_3, _4) +//! ``` +//! +//! After: +//! ```text +//! bb0: +//! _2 = input() +//! _5 = _1.0.1 +//! _3 = eq(_5, _2) +//! _6 = _1.2 +//! _4 = eq(_5, _6) +//! return and(_3, _4) +//! ``` +//! +//! The [`Traversals`] map records `_5 → _1.0.1` and `_6 → _1.2` for the graph executor to use. + use core::{alloc::Allocator, convert::Infallible}; use hashql_core::{heap::Heap, id::Id as _, span::SpanId}; @@ -19,6 +71,10 @@ use crate::{ visit::{self, VisitorMut, r#mut::filter}, }; +/// Maps extracted locals back to their original projection paths. +/// +/// This is the output of [`TraversalExtraction`], allowing consumers (such as the graph executor) +/// to determine which property paths were accessed on the source local. pub struct Traversals<'heap> { source: Local, derivations: LocalVec>, &'heap Heap>, @@ -38,6 +94,7 @@ impl<'heap> Traversals<'heap> { self.derivations.insert(local, place); } + /// Returns the original projection path for `local`, if it was extracted from the source. #[must_use] #[inline] pub fn lookup(&self, local: Local) -> Option<&Place<'heap>> { @@ -45,18 +102,28 @@ impl<'heap> Traversals<'heap> { } } +/// Visitor that extracts projections from a target local into separate bindings. struct TraversalExtractionVisitor<'heap, A: Allocator> { + /// The local we're extracting projections from (the vertex). target: Local, + /// Declaration of the target local, used to derive types for extracted locals. target_decl: LocalDecl<'heap>, + /// Span of the current statement/terminator being visited. current_span: SpanId, + /// Bound of existing locals before extraction (new locals start from here). total_locals: Local, + /// New local declarations to append to the body after visiting. pending_locals: Vec, A>, + /// Index into `pending_locals` marking the start of the current basic block's locals. + /// Used to scope deduplication to the current block. pending_locals_offset: usize, + /// New load statements to insert before the current statement. pending_statements: Vec, A>, + /// Accumulated traversal mappings. traversals: Traversals<'heap>, changed: Changed, } @@ -80,19 +147,15 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap let r#type = place.type_id_unchecked(&self.target_decl); - // Before we do anything, verify if we haven't already added a local with the same - // projection, inside the same basic block. + // Check if we already extracted this projection in the current basic block. let new_local = if let Some(offset) = (self.pending_locals_offset..self.pending_locals.len()).find(|&index| { self.traversals .lookup(self.total_locals.plus(self.pending_locals_offset + index)) .is_some_and(|pending| pending.projections == place.projections) }) { - // We already have a local with the same projection inside the same basic block that we - // can reuse. self.total_locals.plus(offset) } else { - // provision a new local let new_local = self.total_locals.plus(self.pending_locals.len()); self.traversals.insert(new_local, *place); @@ -112,21 +175,15 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap new_local }; - // Replace the operand with the new local *operand = Operand::Place(Place::local(new_local)); Ok(()) } fn visit_rvalue(&mut self, location: Location, rvalue: &mut RValue<'heap>) -> Self::Result<()> { - // loads are handled by the statement_assign visitor and therefore not needed here - match rvalue { - RValue::Load(_) => return Ok(()), - RValue::Binary(_) - | RValue::Unary(_) - | RValue::Aggregate(_) - | RValue::Input(_) - | RValue::Apply(_) => {} + // Skip loads — they're recorded by `visit_statement_assign` to avoid double-processing. + if matches!(rvalue, RValue::Load(_)) { + return Ok(()); } visit::r#mut::walk_rvalue(self, location, rvalue) @@ -153,7 +210,7 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap return Ok(()); } - // lhs is a traversal onto rhs (our target) + // Record pre-existing load as a traversal (e.g., `_2 = _1.0.1` already in the MIR). self.traversals.insert(lhs.local, *rhs); Ok(()) @@ -194,13 +251,12 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap self.pending_locals_offset = self.pending_locals.len(); - // We do not visit the basic block id here because it **cannot** be changed self.visit_basic_block_params(location, params)?; location.statement_index += 1; + // statement_index is 1-indexed (0 is block params). while location.statement_index <= statements.len() { - // statement_index is 1 indexed, therefore the `<=` let index = location.statement_index - 1; let statement = &mut statements[index]; @@ -211,9 +267,7 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap continue; } - // We increment the counter to the amount of statements that we are about to add, as we - // don't need to visit them again. These are only loads, which are recorded in the load - // traversal. + // Skip over the statements we're about to insert — they're already recorded. location.statement_index += self.pending_statements.len(); statements.splice(index..index, self.pending_statements.drain(..)); @@ -222,6 +276,7 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap self.visit_terminator(location, terminator)?; + // Insert any remaining statements from terminator operands at the block end. #[expect(clippy::extend_with_drain, reason = "differing allocator")] if !self.pending_statements.is_empty() { statements.extend(self.pending_statements.drain(..)); @@ -232,12 +287,18 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap } } +/// Extracts projections from the vertex local in graph read filter bodies. +/// +/// This pass only runs on [`Source::GraphReadFilter`] bodies. After running, call +/// [`take_traversals`](Self::take_traversals) to retrieve the mapping of extracted locals to +/// their original projection paths. pub struct TraversalExtraction<'heap, A: Allocator> { alloc: A, traversals: Option>, } impl<'heap, A: Allocator> TraversalExtraction<'heap, A> { + /// Creates a new pass using `alloc` for temporary allocations. pub const fn new_in(alloc: A) -> Self { Self { alloc, @@ -245,6 +306,9 @@ impl<'heap, A: Allocator> TraversalExtraction<'heap, A> { } } + /// Takes the traversal map from the last pass run. + /// + /// Returns [`None`] if the pass hasn't run or if the body wasn't a graph read filter. pub const fn take_traversals(&mut self) -> Option> { self.traversals.take() } @@ -258,7 +322,6 @@ impl<'env, 'heap, A: Allocator> TransformPass<'env, 'heap> for TraversalExtracti } debug_assert_eq!(body.args, 2); - // The second argument is the vertex, which we try to traverse; let vertex = Local::new(1); let mut visitor = TraversalExtractionVisitor { From c411b4ab13745497e6bc9422ce4a05d3b652ead2 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 18:53:55 +0100 Subject: [PATCH 4/9] chore: docs --- .../references/mir-builder-guide.md | 17 +++++++++++++++++ libs/@local/hashql/mir/src/body/place.rs | 1 + libs/@local/hashql/mir/src/builder/body.rs | 3 +++ .../mod.rs} | 0 4 files changed, 21 insertions(+) rename libs/@local/hashql/mir/src/pass/transform/{traversal_extraction.rs => traversal_extraction/mod.rs} (100%) diff --git a/.claude/skills/testing-hashql/references/mir-builder-guide.md b/.claude/skills/testing-hashql/references/mir-builder-guide.md index cbed9db8650..3e15a4256ec 100644 --- a/.claude/skills/testing-hashql/references/mir-builder-guide.md +++ b/.claude/skills/testing-hashql/references/mir-builder-guide.md @@ -70,6 +70,7 @@ The `` can be a numeric literal (`0`, `1`, `42`) or a variable identifier (` | `fn` | `Source::Closure` | Regular closures/functions | | `thunk` | `Source::Thunk` | Thunk bodies (zero-arg delayed computations) | | `[ctor sym::path]` | `Source::Ctor(sym)` | Constructor bodies (always inlined) | +| `[graph::read::filter]` | `Source::GraphReadFilter` | Graph read filter bodies (never inlined) | | `intrinsic` | `Source::Intrinsic` | Intrinsic bodies (never inlined) | ### Types @@ -224,6 +225,22 @@ let body = body!(interner, env; fn@0/0 -> Null { }); ``` +### Graph Read Filter + +Filter bodies for graph traversal. The first two declared locals become the function arguments (`_0` = env tuple, `_1` = vertex): + +```rust +let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (Int,), vertex: (Int, Int), result: Bool; + @proj vertex_field = vertex.0: Int; + + bb0() { + result = bin.== vertex_field 42; + return result; + } +}); +``` + ### Direct Function Calls Use a `DefId` variable directly: diff --git a/libs/@local/hashql/mir/src/body/place.rs b/libs/@local/hashql/mir/src/body/place.rs index 60ca2942f6a..79bb56a934c 100644 --- a/libs/@local/hashql/mir/src/body/place.rs +++ b/libs/@local/hashql/mir/src/body/place.rs @@ -402,6 +402,7 @@ impl<'heap> Place<'heap> { .map_or_else(|| decl[self.local].r#type, |projection| projection.r#type) } + #[must_use] pub fn type_id_unchecked(&self, decl: &LocalDecl<'heap>) -> TypeId { self.projections .last() diff --git a/libs/@local/hashql/mir/src/builder/body.rs b/libs/@local/hashql/mir/src/builder/body.rs index 4e1b4646bac..7544f792ae3 100644 --- a/libs/@local/hashql/mir/src/builder/body.rs +++ b/libs/@local/hashql/mir/src/builder/body.rs @@ -351,6 +351,9 @@ macro_rules! body { (@source fn) => { $crate::body::Source::Closure(hashql_hir::node::HirId::PLACEHOLDER, None) }; + (@source [graph::read::filter]) => { + $crate::body::Source::GraphReadFilter(hashql_hir::node::HirId::PLACEHOLDER) + }; (@source [ctor $name:expr]) => { $crate::body::Source::Ctor($name) }; diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs similarity index 100% rename from libs/@local/hashql/mir/src/pass/transform/traversal_extraction.rs rename to libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs From cb6232e855aa89051f3edd2f18cebf787c85acdb Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 19:11:25 +0100 Subject: [PATCH 5/9] feat: tests --- libs/@local/hashql/mir/src/body/place.rs | 13 + .../transform/traversal_extraction/mod.rs | 13 +- .../transform/traversal_extraction/tests.rs | 455 ++++++++++++++++++ .../duplicate_different_blocks.snap | 70 +++ .../duplicate_same_block_deduped.snap | 39 ++ .../mixed_statement_and_terminator.snap | 55 +++ .../multiple_distinct_projections.snap | 51 ++ .../nested_projection_extracted.snap | 31 ++ .../no_projections_from_target.snap | 27 ++ .../non_graph_filter_unchanged.snap | 25 + .../pre_existing_load_recorded.snap | 33 ++ .../projection_from_non_target_unchanged.snap | 27 ++ .../single_projection_extracted.snap | 31 ++ .../terminator_operand_extraction.snap | 25 + .../traversals_lookup_correct.snap | 42 ++ 15 files changed, 935 insertions(+), 2 deletions(-) create mode 100644 libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_same_block_deduped.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/mixed_statement_and_terminator.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/multiple_distinct_projections.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/nested_projection_extracted.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/no_projections_from_target.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/non_graph_filter_unchanged.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/pre_existing_load_recorded.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/projection_from_non_target_unchanged.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/single_projection_extracted.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/terminator_operand_extraction.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/traversals_lookup_correct.snap diff --git a/libs/@local/hashql/mir/src/body/place.rs b/libs/@local/hashql/mir/src/body/place.rs index 79bb56a934c..a84a5edd362 100644 --- a/libs/@local/hashql/mir/src/body/place.rs +++ b/libs/@local/hashql/mir/src/body/place.rs @@ -419,6 +419,19 @@ impl<'heap> Place<'heap> { } } +impl fmt::Display for Place<'_> { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { local, projections } = self; + fmt::Display::fmt(local, fmt)?; + + for projection in projections { + fmt::Display::fmt(&projection.kind, fmt)?; + } + + Ok(()) + } +} + /// A single projection step that navigates into structured data, carrying its result type. /// /// A [`Projection`] represents one step in navigating through structured data, combining diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs index 196449ba5a2..ae8e225273e 100644 --- a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs @@ -49,6 +49,8 @@ //! ``` //! //! The [`Traversals`] map records `_5 → _1.0.1` and `_6 → _1.2` for the graph executor to use. +#[cfg(test)] +mod tests; use core::{alloc::Allocator, convert::Infallible}; @@ -67,6 +69,7 @@ use crate::{ terminator::Terminator, }, context::MirContext, + intern::Interner, pass::{Changed, TransformPass}, visit::{self, VisitorMut, r#mut::filter}, }; @@ -103,7 +106,7 @@ impl<'heap> Traversals<'heap> { } /// Visitor that extracts projections from a target local into separate bindings. -struct TraversalExtractionVisitor<'heap, A: Allocator> { +struct TraversalExtractionVisitor<'env, 'heap, A: Allocator> { /// The local we're extracting projections from (the vertex). target: Local, /// Declaration of the target local, used to derive types for extracted locals. @@ -126,9 +129,10 @@ struct TraversalExtractionVisitor<'heap, A: Allocator> { /// Accumulated traversal mappings. traversals: Traversals<'heap>, changed: Changed, + interner: &'env Interner<'heap>, } -impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap, A> { +impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'_, 'heap, A> { type Filter = filter::Deep; type Residual = Result; type Result @@ -136,6 +140,10 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'heap where T: 'heap; + fn interner(&self) -> &Interner<'heap> { + self.interner + } + fn visit_operand(&mut self, _: Location, operand: &mut Operand<'heap>) -> Self::Result<()> { let Some(place) = operand.as_place() else { return Ok(()); @@ -334,6 +342,7 @@ impl<'env, 'heap, A: Allocator> TransformPass<'env, 'heap> for TraversalExtracti pending_statements: Vec::new_in(&self.alloc), traversals: Traversals::with_capacity_in(vertex, body.local_decls.len(), context.heap), changed: Changed::No, + interner: context.interner, }; Ok(()) = visitor.visit_body_preserving_cfg(body); diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs new file mode 100644 index 00000000000..f7791916252 --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs @@ -0,0 +1,455 @@ +#![expect(clippy::min_ident_chars, reason = "tests")] + +use alloc::alloc::Global; +use std::{io::Write as _, path::PathBuf}; + +use bstr::ByteVec as _; +use hashql_core::{ + heap::Heap, + pretty::Formatter, + r#type::{TypeFormatter, TypeFormatterOptions, environment::Environment}, +}; +use hashql_diagnostics::DiagnosticIssues; +use insta::{Settings, assert_snapshot}; + +use crate::{ + body::Body, + builder::body, + context::MirContext, + def::DefIdSlice, + intern::Interner, + pass::{TransformPass as _, transform::traversal_extraction::TraversalExtraction}, + pretty::TextFormat, +}; + +#[track_caller] +fn assert_traversal_pass<'heap>( + name: &'static str, + body: Body<'heap>, + mut context: 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"); + + let mut pass = TraversalExtraction::new_in(Global); + let changed = pass.run(&mut context, &mut bodies[0]); + + write!( + text_format.writer, + "\n\n{:=^50}\n\n", + format!(" Changed: {changed:?} ") + ) + .expect("infallible"); + + text_format + .format(DefIdSlice::from_raw(&bodies), &[]) + .expect("should be able to write bodies"); + + // Include traversals info if available + if let Some(traversals) = pass.take_traversals() { + write!(text_format.writer, "\n\n{:=^50}\n\n", " Traversals ").expect("infallible"); + + for local in bodies[0].local_decls.ids() { + if let Some(place) = traversals.lookup(local) { + writeln!(text_format.writer, "{local} → {place}").expect("infallible"); + } + } + } + + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let mut settings = Settings::clone_current(); + settings.set_snapshot_path(dir.join("tests/ui/pass/traversal_extraction")); + 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); +} + +#[test] +fn non_graph_filter_unchanged() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Regular fn body, not GraphReadFilter - should return Changed::No + let body = body!(interner, env; fn@0/2 -> Bool { + decl env: (), vertex: (Int, Int), result: Bool; + @proj vertex_0 = vertex.0: Int; + + bb0() { + result = bin.== vertex_0 42; + return result; + } + }); + + assert_traversal_pass( + "non_graph_filter_unchanged", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn no_projections_from_target() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // GraphReadFilter but no projections from vertex (_1) + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int), result: Bool; + + bb0() { + result = load true; + return result; + } + }); + + assert_traversal_pass( + "no_projections_from_target", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn single_projection_extracted() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Single projection from vertex.0 should be extracted + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int), result: Bool; + @proj vertex_0 = vertex.0: Int; + + bb0() { + result = bin.== vertex_0 42; + return result; + } + }); + + assert_traversal_pass( + "single_projection_extracted", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn nested_projection_extracted() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Nested projection vertex.0.1 should be extracted + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: ((Int, Int), Int), result: Bool; + @proj vertex_0 = vertex.0: (Int, Int), vertex_0_1 = vertex_0.1: Int; + + bb0() { + result = bin.== vertex_0_1 42; + return result; + } + }); + + assert_traversal_pass( + "nested_projection_extracted", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn duplicate_same_block_deduped() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Same projection used twice in one block - should reuse extracted local + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int), r1: Bool, r2: Bool, result: Bool; + @proj vertex_0 = vertex.0: Int; + + bb0() { + r1 = bin.== vertex_0 42; + r2 = bin.== vertex_0 100; + result = bin.& r1 r2; + return result; + } + }); + + assert_traversal_pass( + "duplicate_same_block_deduped", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn duplicate_different_blocks() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Same projection in different blocks - should create separate locals (no cross-block dedup) + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int), r1: Bool, r2: Bool; + @proj vertex_0 = vertex.0: Int; + + bb0() { + if true then bb1() else bb2(); + }, + bb1() { + r1 = bin.== vertex_0 42; + goto bb3(r1); + }, + bb2() { + r2 = bin.== vertex_0 100; + goto bb3(r2); + }, + bb3(r1) { + return r1; + } + }); + + assert_traversal_pass( + "duplicate_different_blocks", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn multiple_distinct_projections() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Multiple different projections - each gets its own extracted local + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int, Int), r1: Bool, r2: Bool, r3: Bool, result: Bool; + @proj vertex_0 = vertex.0: Int, vertex_1 = vertex.1: Int, vertex_2 = vertex.2: Int; + + bb0() { + r1 = bin.== vertex_0 1; + r2 = bin.== vertex_1 2; + r3 = bin.== vertex_2 3; + result = bin.& r1 r2; + result = bin.& result r3; + return result; + } + }); + + assert_traversal_pass( + "multiple_distinct_projections", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn pre_existing_load_recorded() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Pre-existing load statement should be recorded without generating new statements + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int), extracted: Int, result: Bool; + @proj vertex_0 = vertex.0: Int; + + bb0() { + extracted = load vertex_0; + result = bin.== extracted 42; + return result; + } + }); + + assert_traversal_pass( + "pre_existing_load_recorded", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn terminator_operand_extraction() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Projection used in terminator should be extracted at block end + let body = body!(interner, env; [graph::read::filter]@0/2 -> Int { + decl env: (), vertex: (Int, Int); + @proj vertex_0 = vertex.0: Int; + + bb0() { + return vertex_0; + } + }); + + assert_traversal_pass( + "terminator_operand_extraction", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn mixed_statement_and_terminator() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Projections in both statements and terminator + let body = body!(interner, env; [graph::read::filter]@0/2 -> Int { + decl env: (), vertex: (Int, Int), cond: Bool; + @proj vertex_0 = vertex.0: Int, vertex_1 = vertex.1: Int; + + bb0() { + cond = bin.== vertex_0 42; + if cond then bb1() else bb2(); + }, + bb1() { + return vertex_0; + }, + bb2() { + return vertex_1; + } + }); + + assert_traversal_pass( + "mixed_statement_and_terminator", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn projection_from_non_target_unchanged() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Projection from env (_0) should not be extracted - only vertex (_1) is target + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (Int, Int), vertex: (Int, Int), result: Bool; + @proj env_0 = env.0: Int; + + bb0() { + result = bin.== env_0 42; + return result; + } + }); + + assert_traversal_pass( + "projection_from_non_target_unchanged", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn traversals_lookup_correct() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Verify traversals.lookup() returns correct projection paths + let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool { + decl env: (), vertex: (Int, Int, Int), r1: Bool, r2: Bool, result: Bool; + @proj vertex_0 = vertex.0: Int, vertex_2 = vertex.2: Int; + + bb0() { + r1 = bin.== vertex_0 1; + r2 = bin.== vertex_2 3; + result = bin.& r1 r2; + return result; + } + }); + + assert_traversal_pass( + "traversals_lookup_correct", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap new file mode 100644 index 00000000000..b69d4195dd8 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap @@ -0,0 +1,70 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + + bb0(): { + %2 = 1 + + switchInt(%2) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %3 = %1.0 == 42 + + goto -> bb3(%3) + } + + bb2(): { + %4 = %1.0 == 100 + + goto -> bb3(%4) + } + + bb3(%3): { + return %3 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + let %5: Integer + let %6: Integer + + bb0(): { + %2 = 1 + + switchInt(%2) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %5 = %1.0 + %3 = %5 == 42 + + goto -> bb3(%3) + } + + bb2(): { + %6 = %1.0 + %4 = %6 == 100 + + goto -> bb3(%4) + } + + bb3(%3): { + return %3 + } +} + +=================== Traversals =================== + +%5 → %1.0 +%6 → %1.0 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_same_block_deduped.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_same_block_deduped.snap new file mode 100644 index 00000000000..58ba673793b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_same_block_deduped.snap @@ -0,0 +1,39 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + + bb0(): { + %2 = %1.0 == 42 + %3 = %1.0 == 100 + %4 = %2 & %3 + + return %4 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + let %5: Integer + + bb0(): { + %5 = %1.0 + %2 = %5 == 42 + %3 = %5 == 100 + %4 = %2 & %3 + + return %4 + } +} + +=================== Traversals =================== + +%5 → %1.0 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/mixed_statement_and_terminator.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/mixed_statement_and_terminator.snap new file mode 100644 index 00000000000..9e1f0561d97 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/mixed_statement_and_terminator.snap @@ -0,0 +1,55 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Integer { + let %2: Boolean + + bb0(): { + %2 = %1.0 == 42 + + switchInt(%2) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + return %1.0 + } + + bb2(): { + return %1.1 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Integer { + let %2: Boolean + let %3: Integer + let %4: Integer + let %5: Integer + + bb0(): { + %3 = %1.0 + %2 = %3 == 42 + + switchInt(%2) -> [0: bb2(), 1: bb1()] + } + + bb1(): { + %4 = %1.0 + + return %4 + } + + bb2(): { + %5 = %1.1 + + return %5 + } +} + +=================== Traversals =================== + +%3 → %1.0 +%4 → %1.0 +%5 → %1.1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/multiple_distinct_projections.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/multiple_distinct_projections.snap new file mode 100644 index 00000000000..13467e78d4e --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/multiple_distinct_projections.snap @@ -0,0 +1,51 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + let %5: Boolean + + bb0(): { + %2 = %1.0 == 1 + %3 = %1.1 == 2 + %4 = %1.2 == 3 + %5 = %2 & %3 + %5 = %5 & %4 + + return %5 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + let %5: Boolean + let %6: Integer + let %7: Integer + let %8: Integer + + bb0(): { + %6 = %1.0 + %2 = %6 == 1 + %7 = %1.1 + %3 = %7 == 2 + %8 = %1.2 + %4 = %8 == 3 + %5 = %2 & %3 + %5 = %5 & %4 + + return %5 + } +} + +=================== Traversals =================== + +%6 → %1.0 +%7 → %1.1 +%8 → %1.2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/nested_projection_extracted.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/nested_projection_extracted.snap new file mode 100644 index 00000000000..06da1f6fbf3 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/nested_projection_extracted.snap @@ -0,0 +1,31 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: ((Integer, Integer), Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = %1.0.1 == 42 + + return %2 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: ((Integer, Integer), Integer)) -> Boolean { + let %2: Boolean + let %3: Integer + + bb0(): { + %3 = %1.0.1 + %2 = %3 == 42 + + return %2 + } +} + +=================== Traversals =================== + +%3 → %1.0.1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/no_projections_from_target.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/no_projections_from_target.snap new file mode 100644 index 00000000000..526b9dbeffb --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/no_projections_from_target.snap @@ -0,0 +1,27 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = 1 + + return %2 + } +} + +================== Changed: No =================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = 1 + + return %2 + } +} + +=================== Traversals =================== diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/non_graph_filter_unchanged.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/non_graph_filter_unchanged.snap new file mode 100644 index 00000000000..87955e126ad --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/non_graph_filter_unchanged.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {closure@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = %1.0 == 42 + + return %2 + } +} + +================== Changed: No =================== + +fn {closure@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = %1.0 == 42 + + return %2 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/pre_existing_load_recorded.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/pre_existing_load_recorded.snap new file mode 100644 index 00000000000..99dfe813624 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/pre_existing_load_recorded.snap @@ -0,0 +1,33 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Integer + let %3: Boolean + + bb0(): { + %2 = %1.0 + %3 = %2 == 42 + + return %3 + } +} + +================== Changed: No =================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Integer + let %3: Boolean + + bb0(): { + %2 = %1.0 + %3 = %2 == 42 + + return %3 + } +} + +=================== Traversals =================== + +%2 → %1.0 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/projection_from_non_target_unchanged.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/projection_from_non_target_unchanged.snap new file mode 100644 index 00000000000..10ca4a6879b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/projection_from_non_target_unchanged.snap @@ -0,0 +1,27 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (Integer, Integer), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = %0.0 == 42 + + return %2 + } +} + +================== Changed: No =================== + +fn {graph::read::filter@4294967040}(%0: (Integer, Integer), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = %0.0 == 42 + + return %2 + } +} + +=================== Traversals =================== diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/single_projection_extracted.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/single_projection_extracted.snap new file mode 100644 index 00000000000..0b8e097b1cc --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/single_projection_extracted.snap @@ -0,0 +1,31 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + + bb0(): { + %2 = %1.0 == 42 + + return %2 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Integer + + bb0(): { + %3 = %1.0 + %2 = %3 == 42 + + return %2 + } +} + +=================== Traversals =================== + +%3 → %1.0 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/terminator_operand_extraction.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/terminator_operand_extraction.snap new file mode 100644 index 00000000000..f4a95e38a5c --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/terminator_operand_extraction.snap @@ -0,0 +1,25 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Integer { + bb0(): { + return %1.0 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Integer { + let %2: Integer + + bb0(): { + %2 = %1.0 + + return %2 + } +} + +=================== Traversals =================== + +%2 → %1.0 diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/traversals_lookup_correct.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/traversals_lookup_correct.snap new file mode 100644 index 00000000000..d701ba43613 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/traversals_lookup_correct.snap @@ -0,0 +1,42 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/traversal_extraction/tests.rs +expression: value +--- +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + + bb0(): { + %2 = %1.0 == 1 + %3 = %1.2 == 3 + %4 = %2 & %3 + + return %4 + } +} + +================== Changed: Yes ================== + +fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer, Integer)) -> Boolean { + let %2: Boolean + let %3: Boolean + let %4: Boolean + let %5: Integer + let %6: Integer + + bb0(): { + %5 = %1.0 + %2 = %5 == 1 + %6 = %1.2 + %3 = %6 == 3 + %4 = %2 & %3 + + return %4 + } +} + +=================== Traversals =================== + +%5 → %1.0 +%6 → %1.2 From 79d4d456ab685a3b567029c9de32f4d395bb0c9c Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 19:42:42 +0100 Subject: [PATCH 6/9] feat: integration --- .../suite/mir_pass_transform_post_inline.rs | 2 +- .../src/pass/transform/canonicalization.rs | 11 ++++ .../mir/src/pass/transform/post_inline.rs | 59 +++++++++++++++---- .../transform/traversal_extraction/mod.rs | 21 ++++++- 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_post_inline.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_post_inline.rs index fb9180ae1ac..1dc2b95be8d 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_post_inline.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_post_inline.rs @@ -54,7 +54,7 @@ pub(crate) fn mir_pass_transform_post_inline<'heap>( diagnostics: DiagnosticIssues::new(), }; - let mut pass = PostInline::new_in(&mut scratch); + let mut pass = PostInline::new_in(heap, &mut scratch); let _: Changed = pass.run( &mut context, &mut GlobalTransformState::new_in(&bodies, heap), diff --git a/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs b/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs index 72ea7b45d83..73adc9f2535 100644 --- a/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs +++ b/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs @@ -60,6 +60,17 @@ impl Canonicalization { Self { alloc, config } } + /// Returns a reference to the allocator used for temporary data structures. + pub const fn allocator(&self) -> &A { + &self.alloc + } + + /// Returns a mutable reference to the allocator, allowing callers to run additional + /// passes within the same allocator scope. + pub const fn allocator_mut(&mut self) -> &mut A { + &mut self.alloc + } + /// Runs a local transform pass on all unstable bodies. /// /// Only bodies in the `unstable` set are processed. The `state` slice is updated to track diff --git a/libs/@local/hashql/mir/src/pass/transform/post_inline.rs b/libs/@local/hashql/mir/src/pass/transform/post_inline.rs index ada19d62d56..bcdad82c183 100644 --- a/libs/@local/hashql/mir/src/pass/transform/post_inline.rs +++ b/libs/@local/hashql/mir/src/pass/transform/post_inline.rs @@ -1,20 +1,26 @@ //! Post-inlining optimization pass. //! -//! This module contains the [`PostInline`] pass, a thin wrapper around [`Canonicalization`] that -//! runs with settings tuned for post-inlining optimization. +//! Runs [`Canonicalization`] to clean up redundancy from inlining, then [`TraversalExtraction`] +//! to materialize vertex projections in graph read filter bodies. +//! +//! After running, call [`PostInline::finish`] to retrieve the [`Traversals`] maps. use core::alloc::Allocator; -use hashql_core::heap::BumpAllocator; +use hashql_core::heap::{BumpAllocator, Heap}; -use super::{Canonicalization, CanonicalizationConfig}; +use super::{Canonicalization, CanonicalizationConfig, TraversalExtraction, Traversals}; use crate::{ body::Body, context::MirContext, - def::DefIdSlice, - pass::{Changed, GlobalTransformPass, GlobalTransformState}, + def::{DefIdSlice, DefIdVec}, + pass::{Changed, GlobalTransformPass, GlobalTransformState, TransformPass as _}, }; +pub struct PostInlineResidual<'heap> { + pub traversals: DefIdVec>, &'heap Heap>, +} + /// Post-inlining optimization driver. /// /// A thin wrapper around [`Canonicalization`] configured for post-inlining optimization. By running @@ -28,32 +34,63 @@ use crate::{ /// more optimization opportunities that may require additional passes to fully resolve. /// /// See [`Canonicalization`] for details on the pass ordering and implementation. -pub struct PostInline { +pub struct PostInline<'heap, A: Allocator> { canonicalization: Canonicalization, + + traversals: DefIdVec>, &'heap Heap>, } -impl PostInline { +impl<'heap, A: BumpAllocator> PostInline<'heap, A> { /// Creates a new post-inlining pass with the given allocator. /// /// The allocator is used for temporary data structures within sub-passes and is reset /// between pass invocations. - pub const fn new_in(alloc: A) -> Self { + pub const fn new_in(heap: &'heap Heap, alloc: A) -> Self { Self { canonicalization: Canonicalization::new_in( CanonicalizationConfig { max_iterations: 16 }, alloc, ), + traversals: DefIdVec::new_in(heap), + } + } + + /// Consumes the pass and returns accumulated results. + /// + /// The returned [`PostInlineResidual`] contains traversal maps for each graph read filter + /// body processed during the pass run. + pub fn finish(self) -> PostInlineResidual<'heap> { + PostInlineResidual { + traversals: self.traversals, } } } -impl<'env, 'heap, A: BumpAllocator> GlobalTransformPass<'env, 'heap> for PostInline { +impl<'env, 'heap, A: BumpAllocator> GlobalTransformPass<'env, 'heap> for PostInline<'heap, A> { fn run( &mut self, context: &mut MirContext<'env, 'heap>, state: &mut GlobalTransformState<'_>, bodies: &mut DefIdSlice>, ) -> Changed { - self.canonicalization.run(context, state, bodies) + let mut changed = Changed::No; + changed |= self.canonicalization.run(context, state, bodies); + + self.canonicalization.allocator_mut().scoped(|alloc| { + let mut extraction = TraversalExtraction::new_in(alloc); + + for (id, body) in bodies.iter_enumerated_mut() { + let changed_body = extraction.run(context, body); + + if let Some(traversal) = extraction.take_traversals() { + self.traversals.insert(id, traversal); + } + + state.mark(id, changed_body); + changed |= changed_body; + } + }); + + changed } } diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs index ae8e225273e..ada963c4300 100644 --- a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs @@ -4,6 +4,21 @@ //! intermediate assignments. It is the inverse of projection forwarding — rather than inlining //! projections, it materializes them as distinct locals. //! +//! # Pipeline Integration +//! +//! Traversal extraction runs as the final phase of [`super::PostInline`], after +//! [`super::Canonicalization`] has cleaned up redundancy from inlining: +//! +//! ```text +//! Post-Inline +//! ├── Canonicalization (fixpoint loop) +//! └── TraversalExtraction (single pass) +//! ``` +//! +//! The pass only operates on [`Source::GraphReadFilter`] bodies; other body types are skipped +//! with [`Changed::No`]. This placement ensures canonicalization has already simplified the MIR +//! before extraction, minimizing the number of projections that need materialization. +//! //! # Purpose //! //! The primary use case is preparing graph read filters for entity traversal. When reading from @@ -76,10 +91,12 @@ use crate::{ /// Maps extracted locals back to their original projection paths. /// -/// This is the output of [`TraversalExtraction`], allowing consumers (such as the graph executor) -/// to determine which property paths were accessed on the source local. +/// Produced by [`TraversalExtraction`] and consumed by the graph executor to determine which +/// property paths were accessed on the vertex local. pub struct Traversals<'heap> { + /// The source local from which projections were extracted (typically the vertex, `_1`). source: Local, + /// Sparse map from extracted local to its original projection path. derivations: LocalVec>, &'heap Heap>, } From d82530eb3621ee579649c619d87baf3254cabe10 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 19:43:48 +0100 Subject: [PATCH 7/9] chore: fix benches --- libs/@local/hashql/mir/benches/interpret.rs | 2 +- libs/@local/hashql/mir/benches/transform.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/@local/hashql/mir/benches/interpret.rs b/libs/@local/hashql/mir/benches/interpret.rs index cc65af23087..0c62ba0eab1 100644 --- a/libs/@local/hashql/mir/benches/interpret.rs +++ b/libs/@local/hashql/mir/benches/interpret.rs @@ -78,7 +78,7 @@ fn create_fibonacci_body<'heap>( let _: Changed = inline.run(&mut context, &mut state.as_mut(), bodies_mut); scratch.reset(); - let mut post = PostInline::new_in(&mut scratch); + let mut post = PostInline::new_in(context.heap, &mut scratch); let _: Changed = post.run(&mut context, &mut state.as_mut(), bodies_mut); scratch.reset(); diff --git a/libs/@local/hashql/mir/benches/transform.rs b/libs/@local/hashql/mir/benches/transform.rs index 38d7e74e684..b99f2bcbef4 100644 --- a/libs/@local/hashql/mir/benches/transform.rs +++ b/libs/@local/hashql/mir/benches/transform.rs @@ -512,7 +512,8 @@ fn pipeline(criterion: &mut Criterion) { changed |= Inline::new_in(InlineConfig::default(), &mut *scratch) .run(context, &mut state, bodies); scratch.reset(); - changed |= PostInline::new_in(&mut *scratch).run(context, &mut state, bodies); + changed |= + PostInline::new_in(context.heap, &mut *scratch).run(context, &mut state, bodies); scratch.reset(); changed }); @@ -527,7 +528,8 @@ fn pipeline(criterion: &mut Criterion) { changed |= Inline::new_in(InlineConfig::default(), &mut *scratch) .run(context, &mut state, bodies); scratch.reset(); - changed |= PostInline::new_in(&mut *scratch).run(context, &mut state, bodies); + changed |= + PostInline::new_in(context.heap, &mut *scratch).run(context, &mut state, bodies); scratch.reset(); changed }); @@ -542,7 +544,8 @@ fn pipeline(criterion: &mut Criterion) { changed |= Inline::new_in(InlineConfig::default(), &mut *scratch) .run(context, &mut state, bodies); scratch.reset(); - changed |= PostInline::new_in(&mut *scratch).run(context, &mut state, bodies); + changed |= + PostInline::new_in(context.heap, &mut *scratch).run(context, &mut state, bodies); scratch.reset(); changed }); @@ -557,7 +560,8 @@ fn pipeline(criterion: &mut Criterion) { changed |= Inline::new_in(InlineConfig::default(), &mut *scratch) .run(context, &mut state, bodies); scratch.reset(); - changed |= PostInline::new_in(&mut *scratch).run(context, &mut state, bodies); + changed |= + PostInline::new_in(context.heap, &mut *scratch).run(context, &mut state, bodies); scratch.reset(); changed }); From 40f373eeb9e3235d338f26042c3fe610320bc28a Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 19:51:58 +0100 Subject: [PATCH 8/9] fix: review --- .../transform/traversal_extraction/mod.rs | 2 +- .../duplicate_different_blocks.snap | 42 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs index ada963c4300..0396e1090b4 100644 --- a/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/traversal_extraction/mod.rs @@ -176,7 +176,7 @@ impl<'heap, A: Allocator> VisitorMut<'heap> for TraversalExtractionVisitor<'_, ' let new_local = if let Some(offset) = (self.pending_locals_offset..self.pending_locals.len()).find(|&index| { self.traversals - .lookup(self.total_locals.plus(self.pending_locals_offset + index)) + .lookup(self.total_locals.plus(index)) .is_some_and(|pending| pending.projections == place.projections) }) { self.total_locals.plus(offset) diff --git a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap index b69d4195dd8..8913c267a70 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/traversal_extraction/duplicate_different_blocks.snap @@ -5,28 +5,25 @@ expression: value fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { let %2: Boolean let %3: Boolean - let %4: Boolean bb0(): { - %2 = 1 - - switchInt(%2) -> [0: bb2(), 1: bb1()] + switchInt(1) -> [0: bb2(), 1: bb1()] } bb1(): { - %3 = %1.0 == 42 + %2 = %1.0 == 42 - goto -> bb3(%3) + goto -> bb3(%2) } bb2(): { - %4 = %1.0 == 100 + %3 = %1.0 == 100 - goto -> bb3(%4) + goto -> bb3(%3) } - bb3(%3): { - return %3 + bb3(%2): { + return %2 } } @@ -35,36 +32,33 @@ fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { fn {graph::read::filter@4294967040}(%0: (), %1: (Integer, Integer)) -> Boolean { let %2: Boolean let %3: Boolean - let %4: Boolean + let %4: Integer let %5: Integer - let %6: Integer bb0(): { - %2 = 1 - - switchInt(%2) -> [0: bb2(), 1: bb1()] + switchInt(1) -> [0: bb2(), 1: bb1()] } bb1(): { - %5 = %1.0 - %3 = %5 == 42 + %4 = %1.0 + %2 = %4 == 42 - goto -> bb3(%3) + goto -> bb3(%2) } bb2(): { - %6 = %1.0 - %4 = %6 == 100 + %5 = %1.0 + %3 = %5 == 100 - goto -> bb3(%4) + goto -> bb3(%3) } - bb3(%3): { - return %3 + bb3(%2): { + return %2 } } =================== Traversals =================== +%4 → %1.0 %5 → %1.0 -%6 → %1.0 From aabfe01d2547be1d247717f926cee7d93b6dc565 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 27 Jan 2026 20:05:49 +0100 Subject: [PATCH 9/9] fix: export residual --- libs/@local/hashql/mir/src/pass/transform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@local/hashql/mir/src/pass/transform/mod.rs b/libs/@local/hashql/mir/src/pass/transform/mod.rs index c3d44b39204..cff6fb9c773 100644 --- a/libs/@local/hashql/mir/src/pass/transform/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/mod.rs @@ -25,7 +25,7 @@ pub use self::{ forward_substitution::ForwardSubstitution, inline::{Inline, InlineConfig, InlineCostEstimationConfig, InlineHeuristicsConfig}, inst_simplify::InstSimplify, - post_inline::PostInline, + post_inline::{PostInline, PostInlineResidual}, pre_inline::PreInline, ssa_repair::SsaRepair, traversal_extraction::{TraversalExtraction, Traversals},