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/mod.rs b/libs/@local/hashql/mir/src/body/mod.rs index 55e03bfc477..74d229d8a35 100644 --- a/libs/@local/hashql/mir/src/body/mod.rs +++ b/libs/@local/hashql/mir/src/body/mod.rs @@ -104,6 +104,13 @@ 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. /// /// This [`SpanId`] tracks the source location of the function, closure, 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/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 new file mode 100644 index 00000000000..387e778b70a --- /dev/null +++ b/libs/@local/hashql/mir/src/pass/analysis/callgraph/mod.rs @@ -0,0 +1,240 @@ +//! 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 resulting 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 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: +//! +//! 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 +//! +//! # Direct vs Indirect Calls +//! +//! Only *direct* calls are tracked as [`CallKind::Apply`] — those where the callee [`DefId`] +//! 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; + +use alloc::alloc::Global; +use core::{alloc::Allocator, fmt}; + +use hashql_core::{ + graph::{LinkedGraph, NodeId}, + id::Id as _, +}; + +use crate::{ + body::{ + Body, + location::Location, + place::{PlaceContext, PlaceReadContext}, + rvalue::Apply, + terminator::{GraphReadBody, GraphReadLocation}, + }, + context::MirContext, + def::{DefId, DefIdSlice}, + pass::AnalysisPass, + visit::Visitor, +}; + +/// Classification of [`DefId`] references in the call graph. +/// +/// 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`]. + /// + /// 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 [`GraphReadLocation`]. + /// + /// Created when a [`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 assignments, and indirect call + /// targets. For indirect calls, this edge appears at the assignment site, not the call site. + Opaque, +} + +/// 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 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 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, |_, _| ()); + + Self { inner: graph } + } +} + +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()); + + 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 [`CallGraph`] from MIR bodies. +/// +/// 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`. + 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, _: &mut MirContext<'env, 'heap>, body: &Body<'heap>) { + let mut visitor = CallGraphVisitor { + kind: CallKind::Opaque, + 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, + caller: DefId, + graph: &'graph mut CallGraph, +} + +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.caller.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_eq!(self.kind, CallKind::Opaque); + self.kind = CallKind::Apply(location); + self.visit_operand(location, function)?; + self.kind = CallKind::Opaque; + + for argument in arguments.iter() { + self.visit_operand(location, argument)?; + } + + 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/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/src/pass/analysis/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/mod.rs index 6c68b0addbf..ed904fa50ad 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/mod.rs @@ -1,5 +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)?; 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..95a61384fe8 --- /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 @ 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 new file mode 100644 index 00000000000..6cdece621b1 --- /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 @ 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 new file mode 100644 index 00000000000..7a9c3dc836e --- /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 @ bb0: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..3dabf772f4b --- /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 @ 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 new file mode 100644 index 00000000000..0b1d9a90eeb --- /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 @ bb0:1]