From d54f0ab6ecf90cc0fcaec9fdf1bb48fc9548bce6 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 23 Dec 2025 12:55:52 +0100 Subject: [PATCH 1/5] chore: checkpoint --- .../mir/src/pass/analysis/callgraph/mod.rs | 40 +++++++++++++++++++ .../hashql/mir/src/pass/analysis/mod.rs | 1 + 2 files changed, 41 insertions(+) create mode 100644 libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs diff --git a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs new file mode 100644 index 00000000000..665a42cf4cc --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs @@ -0,0 +1,40 @@ +use alloc::alloc::Global; +use core::alloc::Allocator; + +use hashql_core::graph::LinkedGraph; + +use crate::{body::Body, context::MirContext, def::DefIdSlice, pass::AnalysisPass}; + +enum CallKind { + Invoke, + Opaque, +} + +struct EdgeData { + kind: CallKind, +} + +pub struct CallGraph { + inner: LinkedGraph<(), EdgeData, A>, +} + +impl CallGraph { + pub fn new_in(domain: &DefIdSlice, alloc: A) -> Self { + let mut graph = LinkedGraph::new_in(alloc); + for _ in domain { + graph.add_node(()); + } + + Self { inner: graph } + } +} + +pub struct CallGraphAnalysis<'graph, A: Allocator = Global> { + graph: &'graph mut CallGraph, +} + +impl<'env, 'heap, A: Allocator> AnalysisPass<'env, 'heap> for CallGraphAnalysis<'_, A> { + fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &Body<'heap>) { + todo!() + } +} diff --git a/libs/@local/hashql/mir/src/pass/analysis/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/mod.rs index 6c68b0addbf..534ef8e50bb 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/mod.rs @@ -1,3 +1,4 @@ +mod callgraph; mod data_dependency; pub mod dataflow; pub use data_dependency::{ From 803b0d2021391fcde6506c73416f9b3a2e70b5fb Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 23 Dec 2025 18:14:48 +0100 Subject: [PATCH 2/5] feat: great callgraph (WIP) --- .../mir/src/pass/analysis/callgraph/mod.rs | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs index 665a42cf4cc..24217b670bf 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs @@ -1,29 +1,34 @@ use alloc::alloc::Global; use core::alloc::Allocator; -use hashql_core::graph::LinkedGraph; +use hashql_core::{ + graph::{LinkedGraph, NodeId}, + id::Id, +}; -use crate::{body::Body, context::MirContext, def::DefIdSlice, pass::AnalysisPass}; +use crate::{ + body::{Body, location::Location, rvalue::Apply}, + context::MirContext, + def::{DefId, DefIdSlice}, + pass::AnalysisPass, + visit::Visitor, +}; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] enum CallKind { - Invoke, + Apply(Location), + Filter(Location), Opaque, } -struct EdgeData { - kind: CallKind, -} - pub struct CallGraph { - inner: LinkedGraph<(), EdgeData, A>, + inner: LinkedGraph<(), CallKind, A>, } impl CallGraph { pub fn new_in(domain: &DefIdSlice, alloc: A) -> Self { let mut graph = LinkedGraph::new_in(alloc); - for _ in domain { - graph.add_node(()); - } + graph.derive(domain, |_, _| ()); Self { inner: graph } } @@ -38,3 +43,41 @@ impl<'env, 'heap, A: Allocator> AnalysisPass<'env, 'heap> for CallGraphAnalysis< todo!() } } + +struct CallGraphVisitor<'graph, A: Allocator = Global> { + kind: CallKind, + current: DefId, + graph: &'graph mut CallGraph, +} + +impl<'heap, A: Allocator> Visitor<'heap> for CallGraphVisitor<'_, A> { + type Result = Result<(), !>; + + fn visit_def_id(&mut self, location: Location, def_id: DefId) -> Self::Result { + let source = NodeId::from_usize(self.current.as_usize()); + let target = NodeId::from_usize(def_id.as_usize()); + + self.graph.inner.add_edge(source, target, self.kind); + Ok(()) + } + + fn visit_rvalue_apply( + &mut self, + location: Location, + Apply { + function, + arguments, + }: &Apply<'heap>, + ) -> Self::Result { + debug_assert!(self.kind.is_none()); + self.kind = Some(Place::Function); + self.visit_operand(location, function)?; + self.kind = None; + + for argument in arguments.iter() { + self.visit_operand(location, argument)?; + } + + Ok(()) + } +} From 0e46b6af3d64dfd1810077e983e21a63b43447e4 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 23 Dec 2025 19:38:55 +0100 Subject: [PATCH 3/5] feat: callgraph impl --- libs/@local/hashql/mir/src/body/mod.rs | 2 + libs/@local/hashql/mir/src/builder.rs | 1 + .../mir/src/pass/analysis/callgraph/mod.rs | 64 ++++++++++++++++--- .../hashql/mir/src/pass/analysis/mod.rs | 5 +- libs/@local/hashql/mir/src/reify/mod.rs | 7 +- libs/@local/hashql/mir/src/visit/mut.rs | 4 ++ libs/@local/hashql/mir/src/visit/ref.rs | 2 + 7 files changed, 69 insertions(+), 16 deletions(-) diff --git a/libs/@local/hashql/mir/src/body/mod.rs b/libs/@local/hashql/mir/src/body/mod.rs index 55e03bfc477..cbe59489956 100644 --- a/libs/@local/hashql/mir/src/body/mod.rs +++ b/libs/@local/hashql/mir/src/body/mod.rs @@ -104,6 +104,8 @@ pub enum Source<'heap> { /// usage and improve interning efficiency while maintaining sufficient debugging information. #[derive(Debug, Clone)] pub struct Body<'heap> { + pub id: DefId, + /// The source location span for this entire body. /// /// This [`SpanId`] tracks the source location of the function, closure, diff --git a/libs/@local/hashql/mir/src/builder.rs b/libs/@local/hashql/mir/src/builder.rs index 87d87dd14ee..de8668d064a 100644 --- a/libs/@local/hashql/mir/src/builder.rs +++ b/libs/@local/hashql/mir/src/builder.rs @@ -324,6 +324,7 @@ impl<'env, 'heap> BodyBuilder<'env, 'heap> { ); Body { + id: DefId::MAX, span: SpanId::SYNTHETIC, return_type: return_ty, source: Source::Intrinsic(DefId::MAX), diff --git a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs index 24217b670bf..5dbe23153d2 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs @@ -3,11 +3,17 @@ use core::alloc::Allocator; use hashql_core::{ graph::{LinkedGraph, NodeId}, - id::Id, + id::Id as _, }; use crate::{ - body::{Body, location::Location, rvalue::Apply}, + body::{ + Body, + location::Location, + place::{PlaceContext, PlaceReadContext}, + rvalue::Apply, + terminator::{GraphReadBody, GraphReadLocation}, + }, context::MirContext, def::{DefId, DefIdSlice}, pass::AnalysisPass, @@ -15,9 +21,9 @@ use crate::{ }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum CallKind { +pub enum CallKind { Apply(Location), - Filter(Location), + Filter(GraphReadLocation), Opaque, } @@ -25,6 +31,12 @@ pub struct CallGraph { inner: LinkedGraph<(), CallKind, A>, } +impl CallGraph { + pub fn new(domain: &DefIdSlice) -> Self { + Self::new_in(domain, Global) + } +} + impl CallGraph { pub fn new_in(domain: &DefIdSlice, alloc: A) -> Self { let mut graph = LinkedGraph::new_in(alloc); @@ -38,9 +50,20 @@ pub struct CallGraphAnalysis<'graph, A: Allocator = Global> { graph: &'graph mut CallGraph, } +impl<'graph, A: Allocator> CallGraphAnalysis<'graph, A> { + pub const fn new(graph: &'graph mut CallGraph) -> Self { + Self { graph } + } +} + impl<'env, 'heap, A: Allocator> AnalysisPass<'env, 'heap> for CallGraphAnalysis<'_, A> { - fn run(&mut self, context: &mut MirContext<'env, 'heap>, body: &Body<'heap>) { - todo!() + fn run(&mut self, _: &mut MirContext<'env, 'heap>, body: &Body<'heap>) { + let mut visitor = CallGraphVisitor { + kind: CallKind::Opaque, + current: body.id, + graph: self.graph, + }; + Ok(()) = visitor.visit_body(body); } } @@ -53,7 +76,7 @@ struct CallGraphVisitor<'graph, A: Allocator = Global> { impl<'heap, A: Allocator> Visitor<'heap> for CallGraphVisitor<'_, A> { type Result = Result<(), !>; - fn visit_def_id(&mut self, location: Location, def_id: DefId) -> Self::Result { + fn visit_def_id(&mut self, _: Location, def_id: DefId) -> Self::Result { let source = NodeId::from_usize(self.current.as_usize()); let target = NodeId::from_usize(def_id.as_usize()); @@ -69,10 +92,10 @@ impl<'heap, A: Allocator> Visitor<'heap> for CallGraphVisitor<'_, A> { arguments, }: &Apply<'heap>, ) -> Self::Result { - debug_assert!(self.kind.is_none()); - self.kind = Some(Place::Function); + debug_assert_eq!(self.kind, CallKind::Opaque); + self.kind = CallKind::Apply(location); self.visit_operand(location, function)?; - self.kind = None; + self.kind = CallKind::Opaque; for argument in arguments.iter() { self.visit_operand(location, argument)?; @@ -80,4 +103,25 @@ impl<'heap, A: Allocator> Visitor<'heap> for CallGraphVisitor<'_, A> { Ok(()) } + + fn visit_graph_read_body( + &mut self, + location: GraphReadLocation, + body: &GraphReadBody, + ) -> Self::Result { + match body { + GraphReadBody::Filter(func, env) => { + debug_assert_eq!(self.kind, CallKind::Opaque); + self.kind = CallKind::Filter(location); + self.visit_def_id(location.base, *func)?; + self.kind = CallKind::Opaque; + + self.visit_local( + location.base, + PlaceContext::Read(PlaceReadContext::Load), + *env, + ) + } + } + } } diff --git a/libs/@local/hashql/mir/src/pass/analysis/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/mod.rs index 534ef8e50bb..ed904fa50ad 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/mod.rs @@ -1,6 +1,7 @@ mod callgraph; mod data_dependency; pub mod dataflow; -pub use data_dependency::{ - DataDependencyAnalysis, DataDependencyGraph, TransientDataDependencyGraph, +pub use self::{ + callgraph::{CallGraph, CallGraphAnalysis, CallKind}, + data_dependency::{DataDependencyAnalysis, DataDependencyGraph, TransientDataDependencyGraph}, }; diff --git a/libs/@local/hashql/mir/src/reify/mod.rs b/libs/@local/hashql/mir/src/reify/mod.rs index 2c46347b540..3e5532869cc 100644 --- a/libs/@local/hashql/mir/src/reify/mod.rs +++ b/libs/@local/hashql/mir/src/reify/mod.rs @@ -271,16 +271,15 @@ impl<'ctx, 'mir, 'hir, 'env, 'heap> Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { &mut self.blocks, ); - let block = Body { + self.context.bodies.push_with(|id| Body { + id, span, return_type: returns, source, local_decls: self.local_decls, basic_blocks: BasicBlocks::new(self.blocks), args, - }; - - self.context.bodies.push(block) + }) } /// Lowers a closure to a MIR body with proper capture handling. diff --git a/libs/@local/hashql/mir/src/visit/mut.rs b/libs/@local/hashql/mir/src/visit/mut.rs index 19415647b98..7bde88a452e 100644 --- a/libs/@local/hashql/mir/src/visit/mut.rs +++ b/libs/@local/hashql/mir/src/visit/mut.rs @@ -610,6 +610,7 @@ pub fn walk_params<'heap, T: VisitorMut<'heap> + ?Sized>( pub fn walk_body<'heap, T: VisitorMut<'heap> + ?Sized>( visitor: &mut T, Body { + id: _, span, return_type: r#type, source, @@ -618,6 +619,7 @@ pub fn walk_body<'heap, T: VisitorMut<'heap> + ?Sized>( args: _, }: &mut Body<'heap>, ) -> T::Result<()> { + // We do not visit the `DefId` here, as it doesn't make sense. visitor.visit_span(span)?; visitor.visit_type_id(r#type)?; visitor.visit_source(source)?; @@ -636,6 +638,7 @@ pub fn walk_body<'heap, T: VisitorMut<'heap> + ?Sized>( pub fn walk_body_preserving_cfg<'heap, T: VisitorMut<'heap> + ?Sized>( visitor: &mut T, Body { + id: _, span, return_type: r#type, source, @@ -644,6 +647,7 @@ pub fn walk_body_preserving_cfg<'heap, T: VisitorMut<'heap> + ?Sized>( args: _, }: &mut Body<'heap>, ) -> T::Result<()> { + // We do not visit the `DefId` here, as it doesn't make sense. visitor.visit_span(span)?; visitor.visit_type_id(r#type)?; visitor.visit_source(source)?; diff --git a/libs/@local/hashql/mir/src/visit/ref.rs b/libs/@local/hashql/mir/src/visit/ref.rs index 0e20958e69c..9c7c5886309 100644 --- a/libs/@local/hashql/mir/src/visit/ref.rs +++ b/libs/@local/hashql/mir/src/visit/ref.rs @@ -362,6 +362,7 @@ pub fn walk_params<'heap, T: Visitor<'heap> + ?Sized>( pub fn walk_body<'heap, T: Visitor<'heap> + ?Sized>( visitor: &mut T, Body { + id: _, span, return_type: r#type, source, @@ -370,6 +371,7 @@ pub fn walk_body<'heap, T: Visitor<'heap> + ?Sized>( args: _, }: &Body<'heap>, ) -> T::Result { + // We do not visit the `DefId` here, as it doesn't make sense. visitor.visit_span(*span)?; visitor.visit_type_id(*r#type)?; visitor.visit_source(source)?; From 0f207c79fe0c0e29d4dd291e6c9478fe8088ca5a Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 23 Dec 2025 20:05:34 +0100 Subject: [PATCH 4/5] test: impl --- libs/@local/hashql/mir/src/body/mod.rs | 5 + .../mir/src/pass/analysis/callgraph/mod.rs | 100 ++++- .../mir/src/pass/analysis/callgraph/tests.rs | 354 ++++++++++++++++++ .../callgraph/apply_with_fn_argument.snap | 6 + .../tests/ui/pass/callgraph/call_chain.snap | 6 + .../tests/ui/pass/callgraph/direct_apply.snap | 5 + .../callgraph/indirect_call_via_local.snap | 5 + .../ui/pass/callgraph/multiple_calls.snap | 6 + .../ui/pass/callgraph/recursive_call.snap | 5 + 9 files changed, 485 insertions(+), 7 deletions(-) create mode 100644 libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs create mode 100644 libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/callgraph/indirect_call_via_local.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap create mode 100644 libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap diff --git a/libs/@local/hashql/mir/src/body/mod.rs b/libs/@local/hashql/mir/src/body/mod.rs index cbe59489956..74d229d8a35 100644 --- a/libs/@local/hashql/mir/src/body/mod.rs +++ b/libs/@local/hashql/mir/src/body/mod.rs @@ -104,6 +104,11 @@ pub enum Source<'heap> { /// usage and improve interning efficiency while maintaining sufficient debugging information. #[derive(Debug, Clone)] pub struct Body<'heap> { + /// The unique identifier for this body. + /// + /// This [`DefId`] serves as a stable reference to this body within a collection of bodies, + /// enabling cross-body analyses like call graphs. The `DefId` is assigned during lowering + /// and corresponds to the body's index in the global definition table. pub id: DefId, /// The source location span for this entire body. diff --git a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs index 5dbe23153d2..65ee4e9b330 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs @@ -1,5 +1,42 @@ +//! Call graph analysis for MIR. +//! +//! This module provides [`CallGraphAnalysis`], a pass that constructs a [`CallGraph`] representing +//! function call relationships between [`DefId`]s in the MIR. The graph can be used for call site +//! enumeration, reachability analysis, and optimization decisions. +//! +//! # Graph Structure +//! +//! The call graph uses [`DefId`]s as nodes and tracks references between them as directed edges. +//! An edge from `A` to `B` means "the MIR body of A references B", annotated with a [`CallKind`] +//! describing how the reference occurs: +//! +//! - [`CallKind::Apply`]: Direct function application at a specific location +//! - [`CallKind::Filter`]: Graph-read filter function call +//! - [`CallKind::Opaque`]: Any other reference (types, constants, function pointers, etc.) +//! +//! # Usage Pattern +//! +//! Unlike [`DataDependencyAnalysis`] which is per-body, [`CallGraphAnalysis`] operates on a shared +//! [`CallGraph`] across multiple bodies. The caller must: +//! +//! 1. Create a [`CallGraph`] with a domain containing all [`DefId`]s that may appear +//! 2. Run [`CallGraphAnalysis`] on each body to populate edges +//! 3. Query the resulting graph +//! +//! # Limitations +//! +//! Only *direct* calls are tracked as [`CallKind::Apply`] — those where the callee [`DefId`] +//! appears syntactically in the function operand. Indirect calls through locals or function +//! pointers appear as [`CallKind::Opaque`] edges at the point where the [`DefId`] is referenced, +//! not at the call site. +//! +//! [`DataDependencyAnalysis`]: super::DataDependencyAnalysis + +#[cfg(test)] +mod tests; + use alloc::alloc::Global; -use core::alloc::Allocator; +use core::{alloc::Allocator, fmt}; use hashql_core::{ graph::{LinkedGraph, NodeId}, @@ -20,24 +57,43 @@ use crate::{ visit::Visitor, }; +/// Classification of [`DefId`] references in the call graph. +/// +/// Each edge in the [`CallGraph`] is annotated with a `CallKind` to distinguish actual call sites +/// from other kinds of references. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum CallKind { + /// Direct function application at the given MIR location. + /// + /// The [`DefId`] appears syntactically as the function operand in an [`Apply`] rvalue. Apply(Location), + + /// Graph-read filter function call at the given location. + /// + /// The [`DefId`] is the filter function in a [`GraphReadBody::Filter`] terminator. Filter(GraphReadLocation), + + /// Any other reference to a [`DefId`]. + /// + /// Includes type references, constant uses, function pointer initialization, and other + /// non-call references. Opaque, } +/// A global call graph over [`DefId`]s. pub struct CallGraph { inner: LinkedGraph<(), CallKind, A>, } impl CallGraph { + /// Creates a new call graph using the global allocator. pub fn new(domain: &DefIdSlice) -> Self { Self::new_in(domain, Global) } } impl CallGraph { + /// Creates a new call graph using the specified allocator. pub fn new_in(domain: &DefIdSlice, alloc: A) -> Self { let mut graph = LinkedGraph::new_in(alloc); graph.derive(domain, |_, _| ()); @@ -46,11 +102,39 @@ impl CallGraph { } } +impl fmt::Display for CallGraph { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + for edge in self.inner.edges() { + let source = DefId::from_usize(edge.source().as_usize()); + let target = DefId::from_usize(edge.target().as_usize()); + + #[expect(clippy::use_debug)] + match edge.data { + CallKind::Apply(location) => { + writeln!(fmt, "@{source} -> @{target} [Apply @ {location:?}]")?; + } + CallKind::Filter(location) => { + writeln!(fmt, "@{source} -> @{target} [Filter @ {location:?}]")?; + } + CallKind::Opaque => { + writeln!(fmt, "@{source} -> @{target} [Opaque]")?; + } + } + } + + Ok(()) + } +} + +/// Analysis pass that populates a shared [`CallGraph`] from MIR bodies. +/// +/// This pass traverses a MIR body and records edges for each [`DefId`] reference encountered. pub struct CallGraphAnalysis<'graph, A: Allocator = Global> { graph: &'graph mut CallGraph, } impl<'graph, A: Allocator> CallGraphAnalysis<'graph, A> { + /// Creates a new analysis pass that will populate the given graph. pub const fn new(graph: &'graph mut CallGraph) -> Self { Self { graph } } @@ -60,16 +144,18 @@ impl<'env, 'heap, A: Allocator> AnalysisPass<'env, 'heap> for CallGraphAnalysis< fn run(&mut self, _: &mut MirContext<'env, 'heap>, body: &Body<'heap>) { let mut visitor = CallGraphVisitor { kind: CallKind::Opaque, - current: body.id, + caller: body.id, graph: self.graph, }; + Ok(()) = visitor.visit_body(body); } } +/// Visitor that collects call edges during MIR traversal. struct CallGraphVisitor<'graph, A: Allocator = Global> { kind: CallKind, - current: DefId, + caller: DefId, graph: &'graph mut CallGraph, } @@ -77,7 +163,7 @@ impl<'heap, A: Allocator> Visitor<'heap> for CallGraphVisitor<'_, A> { type Result = Result<(), !>; fn visit_def_id(&mut self, _: Location, def_id: DefId) -> Self::Result { - let source = NodeId::from_usize(self.current.as_usize()); + let source = NodeId::from_usize(self.caller.as_usize()); let target = NodeId::from_usize(def_id.as_usize()); self.graph.inner.add_edge(source, target, self.kind); @@ -110,16 +196,16 @@ impl<'heap, A: Allocator> Visitor<'heap> for CallGraphVisitor<'_, A> { body: &GraphReadBody, ) -> Self::Result { match body { - GraphReadBody::Filter(func, env) => { + &GraphReadBody::Filter(func, env) => { debug_assert_eq!(self.kind, CallKind::Opaque); self.kind = CallKind::Filter(location); - self.visit_def_id(location.base, *func)?; + self.visit_def_id(location.base, func)?; self.kind = CallKind::Opaque; self.visit_local( location.base, PlaceContext::Read(PlaceReadContext::Load), - *env, + env, ) } } diff --git a/libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs b/libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs new file mode 100644 index 00000000000..e3098d8b543 --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs @@ -0,0 +1,354 @@ +#![expect(clippy::similar_names, reason = "tests")] +use std::path::PathBuf; + +use hashql_core::r#type::{TypeBuilder, environment::Environment}; +use hashql_diagnostics::DiagnosticIssues; +use insta::{Settings, assert_snapshot}; + +use super::{CallGraph, CallGraphAnalysis}; +use crate::{ + body::{Body, operand::Operand}, + builder::{BodyBuilder, scaffold}, + context::MirContext, + def::DefId, + pass::AnalysisPass as _, +}; + +#[track_caller] +fn assert_callgraph<'heap>( + name: &'static str, + bodies: &[Body<'heap>], + context: &mut MirContext<'_, 'heap>, +) { + let mut graph = CallGraph::new(crate::def::DefIdSlice::from_raw(bodies)); + + for body in bodies { + let mut analysis = CallGraphAnalysis::new(&mut graph); + analysis.run(context, body); + } + + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let mut settings = Settings::clone_current(); + settings.set_snapshot_path(dir.join("tests/ui/pass/callgraph")); + settings.set_prepend_module_to_snapshot(false); + + let _drop = settings.bind_to_scope(); + + assert_snapshot!(name, format!("{graph}")); +} + +/// Tests that a direct function application creates an Apply edge. +/// +/// ```text +/// @0: +/// _0 = apply(@1, []) +/// return _0 +/// ``` +#[test] +fn direct_apply() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + let ty = TypeBuilder::synthetic(&env).integer(); + + let result = builder.local("result", ty); + + let caller_id = DefId::new(0); + let callee_id = DefId::new(1); + let callee_fn = builder.const_fn(callee_id); + + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(result, |rv| rv.apply(callee_fn, [] as [Operand<'_>; 0])) + .ret(result); + + let mut caller = builder.finish(0, ty); + caller.id = caller_id; + + // Create a dummy body for the callee so the domain includes it + let mut builder = BodyBuilder::new(&interner); + let ret = builder.local("ret", ty); + let bb = builder.reserve_block([]); + builder.build_block(bb).ret(ret); + let mut callee = builder.finish(0, ty); + callee.id = callee_id; + + assert_callgraph( + "direct_apply", + &[caller, callee], + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests that function arguments also get visited as Opaque if they contain [`DefId`]. +/// +/// ```text +/// @0: +/// _0 = apply(@1, [@2]) // @1 is Apply, @2 is Opaque (passed as argument) +/// return _0 +/// ``` +#[test] +fn apply_with_fn_argument() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + let ty = TypeBuilder::synthetic(&env).integer(); + + let caller_id = DefId::new(0); + let callee_id = DefId::new(1); + let arg_fn_id = DefId::new(2); + + let result = builder.local("result", ty); + let callee_fn = builder.const_fn(callee_id); + let arg_fn = builder.const_fn(arg_fn_id); + + let bb0 = builder.reserve_block([]); + builder + .build_block(bb0) + .assign_place(result, |rv| rv.apply(callee_fn, [arg_fn])) + .ret(result); + + let mut caller = builder.finish(0, ty); + caller.id = caller_id; + + // Dummy body for callee + let mut builder = BodyBuilder::new(&interner); + let ret = builder.local("ret", ty); + let bb = builder.reserve_block([]); + builder.build_block(bb).ret(ret); + let mut callee = builder.finish(0, ty); + callee.id = callee_id; + + // Dummy body for arg_fn + let mut builder = BodyBuilder::new(&interner); + let ret = builder.local("ret", ty); + let bb = builder.reserve_block([]); + builder.build_block(bb).ret(ret); + let mut arg_body = builder.finish(0, ty); + arg_body.id = arg_fn_id; + + assert_callgraph( + "apply_with_fn_argument", + &[caller, callee, arg_body], + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests that multiple calls from the same body create multiple edges. +/// +/// ```text +/// @0: +/// _0 = apply(@1, []) +/// _1 = apply(@2, []) +/// return _1 +/// ``` +#[test] +fn multiple_calls() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + let ty = TypeBuilder::synthetic(&env).integer(); + + let caller_id = DefId::new(0); + let callee1_id = DefId::new(1); + let callee2_id = DefId::new(2); + + let x = builder.local("x", ty); + let y = builder.local("y", ty); + let fn1 = builder.const_fn(callee1_id); + let fn2 = builder.const_fn(callee2_id); + + let bb0 = builder.reserve_block([]); + builder + .build_block(bb0) + .assign_place(x, |rv| rv.apply(fn1, [] as [Operand<'_>; 0])) + .assign_place(y, |rv| rv.apply(fn2, [] as [Operand<'_>; 0])) + .ret(y); + + let mut caller = builder.finish(0, ty); + caller.id = caller_id; + + // Dummy body 1 + let mut builder = BodyBuilder::new(&interner); + let ret = builder.local("ret", ty); + let bb = builder.reserve_block([]); + builder.build_block(bb).ret(ret); + let mut body1 = builder.finish(0, ty); + body1.id = callee1_id; + + // Dummy body 2 + let mut builder = BodyBuilder::new(&interner); + let ret = builder.local("ret", ty); + let bb = builder.reserve_block([]); + builder.build_block(bb).ret(ret); + let mut body2 = builder.finish(0, ty); + body2.id = callee2_id; + + assert_callgraph( + "multiple_calls", + &[caller, body1, body2], + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests call chain across multiple bodies. +/// +/// ```text +/// @0 calls @1 +/// @1 calls @2 +/// ``` +#[test] +fn call_chain() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + let ty = TypeBuilder::synthetic(&env).integer(); + + let outer_id = DefId::new(0); + let middle_id = DefId::new(1); + let leaf_id = DefId::new(2); + + // Outer body: calls middle + let x = builder.local("x", ty); + let middle_fn = builder.const_fn(middle_id); + let bb0 = builder.reserve_block([]); + builder + .build_block(bb0) + .assign_place(x, |rv| rv.apply(middle_fn, [] as [Operand<'_>; 0])) + .ret(x); + let mut outer = builder.finish(0, ty); + outer.id = outer_id; + + // Middle body: calls leaf + let mut builder = BodyBuilder::new(&interner); + let y = builder.local("y", ty); + let leaf_fn = builder.const_fn(leaf_id); + let bb1 = builder.reserve_block([]); + builder + .build_block(bb1) + .assign_place(y, |rv| rv.apply(leaf_fn, [] as [Operand<'_>; 0])) + .ret(y); + let mut middle = builder.finish(0, ty); + middle.id = middle_id; + + // Leaf body: no calls + let mut builder = BodyBuilder::new(&interner); + let z = builder.local("z", ty); + let bb2 = builder.reserve_block([]); + builder.build_block(bb2).ret(z); + let mut leaf = builder.finish(0, ty); + leaf.id = leaf_id; + + assert_callgraph( + "call_chain", + &[outer, middle, leaf], + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests recursive call (self-reference). +/// +/// ```text +/// @0 calls @0 +/// ``` +#[test] +fn recursive_call() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + let ty = TypeBuilder::synthetic(&env).integer(); + + let recursive_id = DefId::new(0); + + let x = builder.local("x", ty); + let self_fn = builder.const_fn(recursive_id); + let bb0 = builder.reserve_block([]); + + builder + .build_block(bb0) + .assign_place(x, |rv| rv.apply(self_fn, [] as [Operand<'_>; 0])) + .ret(x); + + let mut body = builder.finish(0, ty); + body.id = recursive_id; + + assert_callgraph( + "recursive_call", + &[body], + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +/// Tests that indirect calls (via local) are tracked as Opaque at assignment, not Apply. +/// +/// ```text +/// @0: +/// _0 = @1 // Opaque edge here +/// _1 = apply(_0) // No edge here (function is a local, not a DefId) +/// return _1 +/// ``` +#[test] +fn indirect_call_via_local() { + scaffold!(heap, interner, builder); + let env = Environment::new(&heap); + let ty = TypeBuilder::synthetic(&env).integer(); + let fn_ty = TypeBuilder::synthetic(&env).unknown(); + + let caller_id = DefId::new(0); + let callee_id = DefId::new(1); + + let func_local = builder.local("func", fn_ty); + let result = builder.local("result", ty); + let fn_const = builder.const_fn(callee_id); + + let bb0 = builder.reserve_block([]); + builder + .build_block(bb0) + .assign_place(func_local, |rv| rv.load(fn_const)) + .assign_place(result, |rv| rv.apply(func_local, [] as [Operand<'_>; 0])) + .ret(result); + + let mut caller = builder.finish(0, ty); + caller.id = caller_id; + + // Dummy callee body + let mut builder = BodyBuilder::new(&interner); + let ret = builder.local("ret", ty); + let bb = builder.reserve_block([]); + builder.build_block(bb).ret(ret); + let mut callee = builder.finish(0, ty); + callee.id = callee_id; + + assert_callgraph( + "indirect_call_via_local", + &[caller, callee], + &mut MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap new file mode 100644 index 00000000000..b5fe55434cb --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap @@ -0,0 +1,6 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs +expression: "format!(\"{graph}\")" +--- +@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@0 -> @2 [Opaque] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap new file mode 100644 index 00000000000..15f324035d6 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap @@ -0,0 +1,6 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs +expression: "format!(\"{graph}\")" +--- +@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@1 -> @2 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap new file mode 100644 index 00000000000..f670555b638 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap @@ -0,0 +1,5 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs +expression: "format!(\"{graph}\")" +--- +@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/indirect_call_via_local.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/indirect_call_via_local.snap new file mode 100644 index 00000000000..707dd4db16d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/indirect_call_via_local.snap @@ -0,0 +1,5 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs +expression: "format!(\"{graph}\")" +--- +@0 -> @1 [Opaque] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap new file mode 100644 index 00000000000..fdc54dfffe5 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap @@ -0,0 +1,6 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs +expression: "format!(\"{graph}\")" +--- +@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@0 -> @2 [Apply @ Location { block: BasicBlockId(0), statement_index: 2 }] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap new file mode 100644 index 00000000000..30a22101d28 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap @@ -0,0 +1,5 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs +expression: "format!(\"{graph}\")" +--- +@0 -> @0 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] From 9d0c0eddfed119f84043b2e4fa77f0ad609ad18d Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Tue, 23 Dec 2025 22:15:02 +0100 Subject: [PATCH 5/5] chore: docs --- libs/@local/hashql/mir/src/body/location.rs | 13 +++ .../hashql/mir/src/body/terminator/graph.rs | 13 +++ .../mir/src/pass/analysis/callgraph/mod.rs | 81 ++++++++++++------- .../callgraph/apply_with_fn_argument.snap | 2 +- .../tests/ui/pass/callgraph/call_chain.snap | 4 +- .../tests/ui/pass/callgraph/direct_apply.snap | 2 +- .../ui/pass/callgraph/multiple_calls.snap | 4 +- .../ui/pass/callgraph/recursive_call.snap | 2 +- 8 files changed, 87 insertions(+), 34 deletions(-) diff --git a/libs/@local/hashql/mir/src/body/location.rs b/libs/@local/hashql/mir/src/body/location.rs index 69e85588f08..730321c0ce8 100644 --- a/libs/@local/hashql/mir/src/body/location.rs +++ b/libs/@local/hashql/mir/src/body/location.rs @@ -1,3 +1,5 @@ +use core::{fmt, fmt::Display}; + use super::basic_block::BasicBlockId; /// A precise location identifying a specific statement within the MIR control flow graph. @@ -31,3 +33,14 @@ impl Location { statement_index: usize::MAX, }; } + +impl Display for Location { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + block, + statement_index, + } = self; + + write!(fmt, "bb{block}:{statement_index}") + } +} diff --git a/libs/@local/hashql/mir/src/body/terminator/graph.rs b/libs/@local/hashql/mir/src/body/terminator/graph.rs index 7e24760d004..86cebadc62c 100644 --- a/libs/@local/hashql/mir/src/body/terminator/graph.rs +++ b/libs/@local/hashql/mir/src/body/terminator/graph.rs @@ -4,6 +4,8 @@ //! the HashQL graph store. They provide structured access to graph data with //! control flow implications based on query results. +use core::{fmt, fmt::Display}; + use hashql_core::heap; use crate::{ @@ -33,6 +35,17 @@ pub struct GraphReadLocation { pub graph_read_index: usize, } +impl Display for GraphReadLocation { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + base, + graph_read_index, + } = self; + + write!(fmt, "{base}:{graph_read_index}") + } +} + /// The starting point for a graph read operation. /// /// Determines where the query begins in the bi-temporal graph. The head diff --git a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs index 65ee4e9b330..387e778b70a 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs @@ -1,8 +1,8 @@ //! Call graph analysis for MIR. //! //! This module provides [`CallGraphAnalysis`], a pass that constructs a [`CallGraph`] representing -//! function call relationships between [`DefId`]s in the MIR. The graph can be used for call site -//! enumeration, reachability analysis, and optimization decisions. +//! function call relationships between [`DefId`]s in the MIR. The resulting graph can be used for +//! call site enumeration, reachability analysis, and optimization decisions. //! //! # Graph Structure //! @@ -10,27 +10,36 @@ //! An edge from `A` to `B` means "the MIR body of A references B", annotated with a [`CallKind`] //! describing how the reference occurs: //! -//! - [`CallKind::Apply`]: Direct function application at a specific location -//! - [`CallKind::Filter`]: Graph-read filter function call -//! - [`CallKind::Opaque`]: Any other reference (types, constants, function pointers, etc.) +//! - [`CallKind::Apply`]: Direct function application via an [`Apply`] rvalue +//! - [`CallKind::Filter`]: Graph-read filter function in a [`GraphReadBody::Filter`] terminator +//! - [`CallKind::Opaque`]: Any other reference (types, constants, function pointers) +//! +//! For example, given a body `@0` containing `_1 = @1(_2)`: +//! - An edge `@0 → @1` is created with kind [`CallKind::Apply`] //! //! # Usage Pattern //! //! Unlike [`DataDependencyAnalysis`] which is per-body, [`CallGraphAnalysis`] operates on a shared -//! [`CallGraph`] across multiple bodies. The caller must: +//! [`CallGraph`] across multiple bodies: //! //! 1. Create a [`CallGraph`] with a domain containing all [`DefId`]s that may appear //! 2. Run [`CallGraphAnalysis`] on each body to populate edges //! 3. Query the resulting graph //! -//! # Limitations +//! # Direct vs Indirect Calls //! //! Only *direct* calls are tracked as [`CallKind::Apply`] — those where the callee [`DefId`] -//! appears syntactically in the function operand. Indirect calls through locals or function -//! pointers appear as [`CallKind::Opaque`] edges at the point where the [`DefId`] is referenced, -//! not at the call site. +//! appears syntactically as the function operand. Indirect calls through locals (e.g., +//! `_1 = @fn; _2 = _1(...)`) produce an [`Opaque`] edge at the assignment site, not an +//! [`Apply`] edge at the call site. +//! +//! This is intentional: the analysis is designed to run after SROA, which propagates function +//! references through locals, eliminating most indirect call patterns. //! +//! [`Opaque`]: CallKind::Opaque //! [`DataDependencyAnalysis`]: super::DataDependencyAnalysis +//! [`Apply`]: crate::body::rvalue::Apply +//! [`GraphReadBody::Filter`]: crate::body::terminator::GraphReadBody::Filter #[cfg(test)] mod tests; @@ -59,41 +68,58 @@ use crate::{ /// Classification of [`DefId`] references in the call graph. /// -/// Each edge in the [`CallGraph`] is annotated with a `CallKind` to distinguish actual call sites -/// from other kinds of references. +/// Each edge in the [`CallGraph`] is annotated with a `CallKind` to distinguish direct call sites +/// from other kinds of references. This enables consumers to differentiate between actual function +/// invocations and incidental references. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum CallKind { - /// Direct function application at the given MIR location. + /// Direct function application at the given MIR [`Location`]. /// - /// The [`DefId`] appears syntactically as the function operand in an [`Apply`] rvalue. + /// Created when a [`DefId`] appears syntactically as the function operand in an [`Apply`] + /// rvalue. The location identifies the exact statement where the call occurs. + /// + /// [`Apply`]: crate::body::rvalue::Apply Apply(Location), - /// Graph-read filter function call at the given location. + /// Graph-read filter function call at the given [`GraphReadLocation`]. + /// + /// Created when a [`DefId`] is the filter function in a [`GraphReadBody::Filter`] terminator. /// - /// The [`DefId`] is the filter function in a [`GraphReadBody::Filter`] terminator. + /// [`GraphReadBody::Filter`]: crate::body::terminator::GraphReadBody::Filter Filter(GraphReadLocation), /// Any other reference to a [`DefId`]. /// - /// Includes type references, constant uses, function pointer initialization, and other - /// non-call references. + /// Includes type references, constant uses, function pointer assignments, and indirect call + /// targets. For indirect calls, this edge appears at the assignment site, not the call site. Opaque, } -/// A global call graph over [`DefId`]s. +/// A directed graph of [`DefId`] references across MIR bodies. +/// +/// Nodes correspond to [`DefId`]s and edges represent references from one definition to another, +/// annotated with [`CallKind`] to distinguish call sites from other reference types. +/// +/// The graph is populated by running [`CallGraphAnalysis`] on each MIR body. Multiple bodies +/// can contribute edges to the same graph, building up a complete picture of inter-procedural +/// references. pub struct CallGraph { inner: LinkedGraph<(), CallKind, A>, } impl CallGraph { - /// Creates a new call graph using the global allocator. + /// Creates a new call graph with the given `domain` of [`DefId`]s. + /// + /// All [`DefId`]s that may appear as edge endpoints must be present in the domain. pub fn new(domain: &DefIdSlice) -> Self { Self::new_in(domain, Global) } } impl CallGraph { - /// Creates a new call graph using the specified allocator. + /// Creates a new call graph with the given `domain` using the specified `alloc`ator. + /// + /// All [`DefId`]s that may appear as edge endpoints must be present in the domain. pub fn new_in(domain: &DefIdSlice, alloc: A) -> Self { let mut graph = LinkedGraph::new_in(alloc); graph.derive(domain, |_, _| ()); @@ -108,13 +134,12 @@ impl fmt::Display for CallGraph { let source = DefId::from_usize(edge.source().as_usize()); let target = DefId::from_usize(edge.target().as_usize()); - #[expect(clippy::use_debug)] match edge.data { CallKind::Apply(location) => { - writeln!(fmt, "@{source} -> @{target} [Apply @ {location:?}]")?; + writeln!(fmt, "@{source} -> @{target} [Apply @ {location}]")?; } CallKind::Filter(location) => { - writeln!(fmt, "@{source} -> @{target} [Filter @ {location:?}]")?; + writeln!(fmt, "@{source} -> @{target} [Filter @ {location}]")?; } CallKind::Opaque => { writeln!(fmt, "@{source} -> @{target} [Opaque]")?; @@ -126,15 +151,17 @@ impl fmt::Display for CallGraph { } } -/// Analysis pass that populates a shared [`CallGraph`] from MIR bodies. +/// Analysis pass that populates a [`CallGraph`] from MIR bodies. /// -/// This pass traverses a MIR body and records edges for each [`DefId`] reference encountered. +/// This pass traverses a MIR body and records an edge for each [`DefId`] reference encountered, +/// annotated with the appropriate [`CallKind`]. Run this pass on each body to build a complete +/// inter-procedural call graph. pub struct CallGraphAnalysis<'graph, A: Allocator = Global> { graph: &'graph mut CallGraph, } impl<'graph, A: Allocator> CallGraphAnalysis<'graph, A> { - /// Creates a new analysis pass that will populate the given graph. + /// Creates a new analysis pass that will populate the given `graph`. pub const fn new(graph: &'graph mut CallGraph) -> Self { Self { graph } } diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap index b5fe55434cb..95a61384fe8 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/apply_with_fn_argument.snap @@ -2,5 +2,5 @@ source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs expression: "format!(\"{graph}\")" --- -@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@0 -> @1 [Apply @ bb0:1] @0 -> @2 [Opaque] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap index 15f324035d6..6cdece621b1 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/call_chain.snap @@ -2,5 +2,5 @@ source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs expression: "format!(\"{graph}\")" --- -@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] -@1 -> @2 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@0 -> @1 [Apply @ bb0:1] +@1 -> @2 [Apply @ bb0:1] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap index f670555b638..7a9c3dc836e 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/direct_apply.snap @@ -2,4 +2,4 @@ source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs expression: "format!(\"{graph}\")" --- -@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@0 -> @1 [Apply @ bb0:1] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap index fdc54dfffe5..3dabf772f4b 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/multiple_calls.snap @@ -2,5 +2,5 @@ source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs expression: "format!(\"{graph}\")" --- -@0 -> @1 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] -@0 -> @2 [Apply @ Location { block: BasicBlockId(0), statement_index: 2 }] +@0 -> @1 [Apply @ bb0:1] +@0 -> @2 [Apply @ bb0:2] diff --git a/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap b/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap index 30a22101d28..0b1d9a90eeb 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/callgraph/recursive_call.snap @@ -2,4 +2,4 @@ source: libs/@local/hashql/mir/src/pass/analysis/callgraph/tests.rs expression: "format!(\"{graph}\")" --- -@0 -> @0 [Apply @ Location { block: BasicBlockId(0), statement_index: 1 }] +@0 -> @0 [Apply @ bb0:1]