Skip to main content

backhopper_xref_graph/
graph.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! The aggregate `CallGraph` type.
6//!
7//! Mode (`Functions` vs `Modules`) and phase (`Building` vs `Built`) are
8//! lifted to the type system: `exports_not_used` is a method on
9//! `CallGraph<Functions, Built>` only, so a misuse against a modules-mode
10//! or under-construction graph fails at compile time.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::fmt;
14use std::marker::PhantomData;
15
16use backhopper_core::{ApplicationName, BehaviourName, Mfa, ModuleName};
17use serde::{Deserialize, Serialize};
18
19use crate::call::{FunctionSig, UnresolvedCallSite};
20use crate::loc::Loc;
21use crate::relation::{Relation, VertexSet};
22use crate::state::{Building, Built, Functions, Mode, Phase};
23use crate::vertex::Vertex;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27#[non_exhaustive]
28pub enum DeprecationTier {
29    Now,
30    Next,
31    Eventual,
32}
33
34impl fmt::Display for DeprecationTier {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        let s = match self {
37            Self::Now => "now",
38            Self::Next => "next_release",
39            Self::Eventual => "eventual",
40        };
41        f.write_str(s)
42    }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct Deprecation {
47    pub tier: DeprecationTier,
48    pub message: Option<String>,
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Eq)]
52pub struct ModuleSummary {
53    pub application: Option<ApplicationName>,
54    pub exports: BTreeSet<FunctionSig>,
55    pub locals: BTreeSet<FunctionSig>,
56    pub on_load: Option<FunctionSig>,
57    pub behaviours: BTreeSet<BehaviourName>,
58    pub callbacks_required: BTreeSet<FunctionSig>,
59    pub callbacks_optional: BTreeSet<FunctionSig>,
60}
61
62#[derive(Debug, Clone)]
63pub struct CallGraph<M: Mode, P: Phase = Built> {
64    mode: PhantomData<M>,
65    phase: PhantomData<P>,
66
67    modules: BTreeMap<ModuleName, ModuleSummary>,
68    def_at: BTreeMap<Mfa, Loc>,
69    deprecated: BTreeMap<Mfa, Deprecation>,
70    on_load_functions: BTreeSet<Mfa>,
71
72    local_calls: Relation,
73    external_calls: Relation,
74    module_edges: Relation,
75    application_edges: Relation,
76    behaviour_edges: Relation,
77
78    unresolved: BTreeSet<UnresolvedCallSite>,
79    builtins: VertexSet,
80}
81
82impl<M: Mode, P: Phase> CallGraph<M, P> {
83    fn empty() -> Self {
84        Self {
85            mode: PhantomData,
86            phase: PhantomData,
87            modules: BTreeMap::new(),
88            def_at: BTreeMap::new(),
89            deprecated: BTreeMap::new(),
90            on_load_functions: BTreeSet::new(),
91            local_calls: Relation::new(),
92            external_calls: Relation::new(),
93            module_edges: Relation::new(),
94            application_edges: Relation::new(),
95            behaviour_edges: Relation::new(),
96            unresolved: BTreeSet::new(),
97            builtins: VertexSet::new(),
98        }
99    }
100}
101
102impl<M: Mode> CallGraph<M, Building> {
103    pub fn new() -> Self {
104        Self::empty()
105    }
106
107    pub fn set_builtins(&mut self, b: VertexSet) -> &mut Self {
108        self.builtins = b;
109        self
110    }
111
112    pub fn insert_module(&mut self, name: ModuleName, summary: ModuleSummary) {
113        let app_vertex = summary.application.clone().map(Vertex::Application);
114        for b in &summary.behaviours {
115            self.behaviour_edges
116                .insert(Vertex::Module(name.clone()), Vertex::Behaviour(b.clone()));
117            if let Some(a) = &app_vertex {
118                self.application_edges
119                    .insert(a.clone(), Vertex::Behaviour(b.clone()));
120            }
121        }
122        self.modules.insert(name, summary);
123    }
124
125    pub fn record_definition(&mut self, mfa: Mfa, loc: Loc) {
126        self.def_at.insert(mfa, loc);
127    }
128
129    pub fn record_on_load(&mut self, mfa: Mfa) {
130        self.on_load_functions.insert(mfa);
131    }
132
133    pub fn record_deprecation(&mut self, mfa: Mfa, d: Deprecation) {
134        self.deprecated.insert(mfa, d);
135    }
136
137    pub fn insert_local_call(&mut self, caller: Mfa, callee: Mfa) {
138        self.local_calls
139            .insert(Vertex::Function(caller), Vertex::Function(callee));
140    }
141
142    pub fn insert_external_call(&mut self, caller: Mfa, callee: Mfa) {
143        let caller_mod = caller.module.clone();
144        let callee_mod = callee.module.clone();
145        self.external_calls
146            .insert(Vertex::Function(caller), Vertex::Function(callee));
147        if caller_mod != callee_mod {
148            let caller_app = self
149                .modules
150                .get(&caller_mod)
151                .and_then(|m| m.application.clone());
152            let callee_app = self
153                .modules
154                .get(&callee_mod)
155                .and_then(|m| m.application.clone());
156            self.module_edges
157                .insert(Vertex::Module(caller_mod), Vertex::Module(callee_mod));
158            if let (Some(a), Some(b)) = (caller_app, callee_app)
159                && a != b
160            {
161                self.application_edges
162                    .insert(Vertex::Application(a), Vertex::Application(b));
163            }
164        }
165    }
166
167    pub fn insert_unresolved(&mut self, site: UnresolvedCallSite) {
168        self.unresolved.insert(site);
169    }
170
171    pub fn finish(self) -> CallGraph<M, Built> {
172        CallGraph {
173            mode: PhantomData,
174            phase: PhantomData,
175            modules: self.modules,
176            def_at: self.def_at,
177            deprecated: self.deprecated,
178            on_load_functions: self.on_load_functions,
179            local_calls: self.local_calls,
180            external_calls: self.external_calls,
181            module_edges: self.module_edges,
182            application_edges: self.application_edges,
183            behaviour_edges: self.behaviour_edges,
184            unresolved: self.unresolved,
185            builtins: self.builtins,
186        }
187    }
188}
189
190impl<M: Mode> Default for CallGraph<M, Building> {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196impl<M: Mode> CallGraph<M, Built> {
197    pub fn modules(&self) -> impl Iterator<Item = (&ModuleName, &ModuleSummary)> {
198        self.modules.iter()
199    }
200
201    pub fn module(&self, name: &ModuleName) -> Option<&ModuleSummary> {
202        self.modules.get(name)
203    }
204
205    pub fn def_at(&self, mfa: &Mfa) -> Option<&Loc> {
206        self.def_at.get(mfa)
207    }
208
209    pub fn deprecation(&self, mfa: &Mfa) -> Option<&Deprecation> {
210        self.deprecated.get(mfa)
211    }
212
213    pub fn deprecated_mfas(&self) -> impl Iterator<Item = (&Mfa, &Deprecation)> {
214        self.deprecated.iter()
215    }
216
217    pub fn on_load_functions(&self) -> &BTreeSet<Mfa> {
218        &self.on_load_functions
219    }
220
221    pub fn builtins(&self) -> &VertexSet {
222        &self.builtins
223    }
224
225    pub fn module_edges(&self) -> &Relation {
226        &self.module_edges
227    }
228
229    pub fn application_edges(&self) -> &Relation {
230        &self.application_edges
231    }
232
233    pub fn behaviour_edges(&self) -> &Relation {
234        &self.behaviour_edges
235    }
236
237    pub fn unresolved_calls(&self) -> &BTreeSet<UnresolvedCallSite> {
238        &self.unresolved
239    }
240
241    pub fn defined_functions(&self) -> VertexSet {
242        self.modules
243            .iter()
244            .flat_map(|(m, s)| {
245                s.exports
246                    .iter()
247                    .chain(s.locals.iter())
248                    .map(move |f| function_vertex(m, f))
249            })
250            .collect()
251    }
252}
253
254fn function_vertex(module: &ModuleName, sig: &FunctionSig) -> Vertex {
255    Vertex::Function(Mfa::new(module.clone(), sig.name.clone(), sig.arity))
256}
257
258impl CallGraph<Functions, Built> {
259    pub fn local_calls(&self) -> &Relation {
260        &self.local_calls
261    }
262
263    pub fn external_calls(&self) -> &Relation {
264        &self.external_calls
265    }
266
267    pub fn all_calls(&self) -> Relation {
268        self.local_calls.union(&self.external_calls)
269    }
270}