Skip to main content

fallow_graph/graph/
entry_load.rs

1//! Load closures of one entry module, split by when each module loads, and
2//! the single imports that dominate the eager closure.
3//!
4//! The eager closure follows only edges with a static, value-carrying symbol:
5//! the code that loads before the entry runs. The deferred closure adds
6//! `import()` and pattern edges, and the out-of-thread closure adds worker,
7//! fork and loader-hook edges. Type-only symbols never load a module, and a
8//! declaration file never loads.
9//!
10//! A dominating import is an edge `importer -> target` that is the only way
11//! into `target` from outside the part of the eager closure that `target`
12//! dominates. If that edge became an `import()`, the whole dominator subtree
13//! of `target` would leave the eager closure. Every result is a pure function
14//! of the edge set, and every list is ordered by `FileId`, so repeated runs
15//! give identical output.
16
17use fallow_types::discover::FileId;
18use fixedbitset::FixedBitSet;
19use rustc_hash::FxHashMap;
20
21use super::{ImportedSymbol, ModuleGraph, is_declaration_file_path};
22use fallow_types::extract::ImportLoadKind;
23
24/// The modules that one entry reaches, split by when they load.
25#[derive(Debug, Clone, Default, PartialEq, Eq)]
26pub struct EntryLoadClosure {
27    /// Modules that load before the entry runs, the entry included, in
28    /// ascending `FileId` order.
29    pub eager: Vec<FileId>,
30    /// Modules that load on demand on the same thread (`import()` or a lazy
31    /// pattern) and are not eager, in ascending `FileId` order.
32    pub deferred: Vec<FileId>,
33    /// Modules that only an out-of-thread load reaches (a worker, a fork, a
34    /// loader hook), in ascending `FileId` order.
35    pub out_of_thread: Vec<FileId>,
36}
37
38/// One import edge that alone keeps a subtree of the eager closure eager.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct DominatingImport {
41    /// The module that contains the import.
42    pub importer: FileId,
43    /// The imported module; the root of the subtree that the edge keeps eager.
44    pub target: FileId,
45    /// Byte offset of the first static, value-carrying binding on the edge.
46    /// `None` for an edge without a binding span, such as a re-export or an
47    /// eager glob match.
48    pub import_span_start: Option<u32>,
49    /// Modules that leave the eager closure if the edge becomes an `import()`.
50    pub exclusive_modules: usize,
51    /// Summed weight of those modules.
52    pub exclusive_weight: u64,
53}
54
55const NO_NODE: u32 = u32::MAX;
56
57impl ModuleGraph {
58    /// Split the modules that `entry` reaches by when they load.
59    ///
60    /// Returns an empty closure for an out-of-range `entry`.
61    #[must_use]
62    pub fn entry_load_closure(&self, entry: FileId) -> EntryLoadClosure {
63        if entry.0 as usize >= self.modules.len() {
64            return EntryLoadClosure::default();
65        }
66        let eager = self.symbol_closure(&[entry], ImportedSymbol::is_eager_value);
67        let eager_ids = set_members(&eager);
68        let same_thread = self.symbol_closure(&eager_ids, |symbol| {
69            !symbol.is_type_only && symbol.load_kind() != ImportLoadKind::OutOfThread
70        });
71        let same_thread_ids = set_members(&same_thread);
72        let everything = self.symbol_closure(&same_thread_ids, |symbol| !symbol.is_type_only);
73
74        let mut deferred = same_thread.clone();
75        deferred.difference_with(&eager);
76        let mut out_of_thread = everything;
77        out_of_thread.difference_with(&same_thread);
78
79        EntryLoadClosure {
80            eager: eager_ids,
81            deferred: set_members(&deferred),
82            out_of_thread: set_members(&out_of_thread),
83        }
84    }
85
86    /// The import edges that each keep a subtree of the eager closure eager,
87    /// ordered by exclusive weight (heaviest first), then exclusive module
88    /// count, then importer and target `FileId`.
89    ///
90    /// `eager` must be the eager closure of `entry` from
91    /// [`Self::entry_load_closure`]. `weight` gives the weight of one module,
92    /// for example its source size in bytes.
93    #[must_use]
94    pub fn eager_dominating_imports(
95        &self,
96        entry: FileId,
97        eager: &[FileId],
98        weight: impl Fn(FileId) -> u64,
99    ) -> Vec<DominatingImport> {
100        let subgraph = EagerSubgraph::new(self, entry, eager);
101        let Some(subgraph) = subgraph else {
102            return Vec::new();
103        };
104        let tree = DominatorTree::new(&subgraph);
105
106        let mut subtree_modules = vec![1_usize; subgraph.nodes.len()];
107        let mut subtree_weight: Vec<u64> = subgraph.nodes.iter().map(|&id| weight(id)).collect();
108        for &node in tree.reverse_postorder.iter().rev() {
109            let parent = tree.idom[node as usize];
110            if parent == node || parent == NO_NODE {
111                continue;
112            }
113            subtree_modules[parent as usize] += subtree_modules[node as usize];
114            subtree_weight[parent as usize] =
115                subtree_weight[parent as usize].saturating_add(subtree_weight[node as usize]);
116        }
117
118        let mut imports = Vec::new();
119        for (node, predecessors) in subgraph.predecessors.iter().enumerate() {
120            if node as u32 == subgraph.root {
121                continue;
122            }
123            let mut external = predecessors
124                .iter()
125                .filter(|(pred, _)| !tree.dominates(node as u32, *pred));
126            let (Some(&(importer, span)), None) = (external.next(), external.next()) else {
127                continue;
128            };
129            imports.push(DominatingImport {
130                importer: subgraph.nodes[importer as usize],
131                target: subgraph.nodes[node],
132                import_span_start: span,
133                exclusive_modules: subtree_modules[node],
134                exclusive_weight: subtree_weight[node],
135            });
136        }
137        imports.sort_by(|a, b| {
138            b.exclusive_weight
139                .cmp(&a.exclusive_weight)
140                .then_with(|| b.exclusive_modules.cmp(&a.exclusive_modules))
141                .then_with(|| a.importer.0.cmp(&b.importer.0))
142                .then_with(|| a.target.0.cmp(&b.target.0))
143        });
144        imports
145    }
146
147    /// Modules reachable from `seeds` over edges with at least one symbol
148    /// that `follows` accepts. The seeds are part of the result.
149    ///
150    /// A declaration file is never a target: even a value import of one
151    /// compiles away, so it and its own imports never load at runtime.
152    fn symbol_closure(
153        &self,
154        seeds: &[FileId],
155        follows: impl Fn(&ImportedSymbol) -> bool,
156    ) -> FixedBitSet {
157        let capacity = self.modules.len();
158        let mut visited = FixedBitSet::with_capacity(capacity);
159        let mut stack: Vec<FileId> = Vec::new();
160        for &seed in seeds {
161            let idx = seed.0 as usize;
162            if idx < capacity && !visited.contains(idx) {
163                visited.insert(idx);
164                stack.push(seed);
165            }
166        }
167        while let Some(current) = stack.pop() {
168            let range = self.modules[current.0 as usize].edge_range.clone();
169            for edge in &self.edges[range] {
170                let idx = edge.target.0 as usize;
171                if idx < capacity
172                    && !visited.contains(idx)
173                    && edge.symbols.iter().any(&follows)
174                    && !is_declaration_file_path(&self.modules[idx].path)
175                {
176                    visited.insert(idx);
177                    stack.push(edge.target);
178                }
179            }
180        }
181        visited
182    }
183}
184
185fn set_members(set: &FixedBitSet) -> Vec<FileId> {
186    set.ones()
187        .map(|idx| FileId(u32::try_from(idx).unwrap_or(u32::MAX)))
188        .collect()
189}
190
191/// The eager closure as a compact graph with local node indices.
192struct EagerSubgraph {
193    /// Local index to `FileId`, in ascending `FileId` order.
194    nodes: Vec<FileId>,
195    /// Local index of the entry.
196    root: u32,
197    /// Eager successors of each node, in ascending order.
198    successors: Vec<Vec<u32>>,
199    /// Eager predecessors of each node with the binding span of the edge.
200    predecessors: Vec<Vec<(u32, Option<u32>)>>,
201}
202
203impl EagerSubgraph {
204    fn new(graph: &ModuleGraph, entry: FileId, eager: &[FileId]) -> Option<Self> {
205        let local: FxHashMap<FileId, u32> = eager
206            .iter()
207            .enumerate()
208            .map(|(idx, &id)| (id, u32::try_from(idx).unwrap_or(NO_NODE)))
209            .collect();
210        let root = *local.get(&entry)?;
211        let mut successors = vec![Vec::new(); eager.len()];
212        let mut predecessors = vec![Vec::new(); eager.len()];
213        for (source_idx, &source) in eager.iter().enumerate() {
214            let Some(module) = graph.modules.get(source.0 as usize) else {
215                continue;
216            };
217            for edge in &graph.edges[module.edge_range.clone()] {
218                let Some(&target_idx) = local.get(&edge.target) else {
219                    continue;
220                };
221                let Some(symbol) = edge.symbols.iter().find(|s| s.is_eager_value()) else {
222                    continue;
223                };
224                if target_idx as usize == source_idx {
225                    continue;
226                }
227                let span = (symbol.import_span.end > symbol.import_span.start)
228                    .then_some(symbol.import_span.start);
229                let source_local = u32::try_from(source_idx).unwrap_or(NO_NODE);
230                successors[source_idx].push(target_idx);
231                predecessors[target_idx as usize].push((source_local, span));
232            }
233        }
234        for list in &mut successors {
235            list.sort_unstable();
236            list.dedup();
237        }
238        for list in &mut predecessors {
239            list.sort_unstable_by_key(|(pred, _)| *pred);
240            list.dedup_by_key(|(pred, _)| *pred);
241        }
242        Some(Self {
243            nodes: eager.to_vec(),
244            root,
245            successors,
246            predecessors,
247        })
248    }
249}
250
251/// Immediate dominators by the iterative Cooper, Harvey and Kennedy method,
252/// plus tree intervals for a constant-time dominance test.
253struct DominatorTree {
254    idom: Vec<u32>,
255    reverse_postorder: Vec<u32>,
256    entry_time: Vec<u32>,
257    exit_time: Vec<u32>,
258}
259
260impl DominatorTree {
261    fn new(graph: &EagerSubgraph) -> Self {
262        let count = graph.nodes.len();
263        let reverse_postorder = reverse_postorder(graph);
264        let mut order = vec![NO_NODE; count];
265        for (position, &node) in reverse_postorder.iter().enumerate() {
266            order[node as usize] = u32::try_from(position).unwrap_or(NO_NODE);
267        }
268
269        let mut idom = vec![NO_NODE; count];
270        idom[graph.root as usize] = graph.root;
271        let mut changed = true;
272        while changed {
273            changed = false;
274            for &node in reverse_postorder.iter().skip(1) {
275                let mut new_idom = NO_NODE;
276                for &(pred, _) in &graph.predecessors[node as usize] {
277                    if idom[pred as usize] == NO_NODE {
278                        continue;
279                    }
280                    new_idom = if new_idom == NO_NODE {
281                        pred
282                    } else {
283                        intersect(&idom, &order, pred, new_idom)
284                    };
285                }
286                if new_idom != NO_NODE && idom[node as usize] != new_idom {
287                    idom[node as usize] = new_idom;
288                    changed = true;
289                }
290            }
291        }
292
293        let (entry_time, exit_time) = tree_intervals(graph.root, &idom, &reverse_postorder);
294        Self {
295            idom,
296            reverse_postorder,
297            entry_time,
298            exit_time,
299        }
300    }
301
302    /// Whether `dominator` dominates `node` (every node dominates itself).
303    fn dominates(&self, dominator: u32, node: u32) -> bool {
304        let (d, n) = (dominator as usize, node as usize);
305        self.entry_time[d] <= self.entry_time[n] && self.exit_time[n] <= self.exit_time[d]
306    }
307}
308
309fn intersect(idom: &[u32], order: &[u32], mut left: u32, mut right: u32) -> u32 {
310    while left != right {
311        while order[left as usize] > order[right as usize] {
312            left = idom[left as usize];
313        }
314        while order[right as usize] > order[left as usize] {
315            right = idom[right as usize];
316        }
317    }
318    left
319}
320
321/// Reverse postorder of the nodes reachable from the root, by an iterative
322/// depth-first search that visits successors in ascending order.
323fn reverse_postorder(graph: &EagerSubgraph) -> Vec<u32> {
324    let mut visited = FixedBitSet::with_capacity(graph.nodes.len());
325    let mut postorder = Vec::with_capacity(graph.nodes.len());
326    let mut stack: Vec<(u32, usize)> = vec![(graph.root, 0)];
327    visited.insert(graph.root as usize);
328    while let Some((node, next_child)) = stack.last_mut() {
329        let children = &graph.successors[*node as usize];
330        if let Some(&child) = children.get(*next_child) {
331            *next_child += 1;
332            if !visited.contains(child as usize) {
333                visited.insert(child as usize);
334                stack.push((child, 0));
335            }
336        } else {
337            postorder.push(*node);
338            stack.pop();
339        }
340    }
341    postorder.reverse();
342    postorder
343}
344
345/// Pre- and post-order times of the dominator tree, for the interval test.
346fn tree_intervals(root: u32, idom: &[u32], reverse_postorder: &[u32]) -> (Vec<u32>, Vec<u32>) {
347    let count = idom.len();
348    let mut children = vec![Vec::new(); count];
349    for &node in reverse_postorder {
350        let parent = idom[node as usize];
351        if node != root && parent != NO_NODE {
352            children[parent as usize].push(node);
353        }
354    }
355    let mut entry_time = vec![u32::MAX; count];
356    let mut exit_time = vec![0_u32; count];
357    let mut clock = 0_u32;
358    let mut stack: Vec<(u32, usize)> = vec![(root, 0)];
359    entry_time[root as usize] = clock;
360    while let Some((node, next_child)) = stack.last_mut() {
361        if let Some(&child) = children[*node as usize].get(*next_child) {
362            *next_child += 1;
363            clock += 1;
364            entry_time[child as usize] = clock;
365            stack.push((child, 0));
366        } else {
367            clock += 1;
368            exit_time[*node as usize] = clock;
369            stack.pop();
370        }
371    }
372    (entry_time, exit_time)
373}
374
375#[cfg(test)]
376mod tests {
377    use std::path::PathBuf;
378
379    use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
380    use fallow_types::extract::{ImportInfo, ImportedName};
381
382    use crate::graph::ModuleGraph;
383    use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
384
385    fn import(to: u32, start: u32) -> ResolvedImport {
386        ResolvedImport {
387            info: ImportInfo {
388                source: format!("./m{to}"),
389                imported_name: ImportedName::SideEffect,
390                local_name: String::new(),
391                is_type_only: false,
392                is_type_only_star: false,
393                from_style: false,
394                span: oxc_span::Span::new(start, start + 10),
395                source_span: oxc_span::Span::default(),
396            },
397            target: ResolveResult::InternalModule(FileId(to)),
398        }
399    }
400
401    /// Static edges only; `edges[i]` lists the targets of module `i`.
402    fn graph(edges: &[&[u32]]) -> ModuleGraph {
403        graph_with_paths(edges, |i| PathBuf::from(format!("/p/m{i}.ts")))
404    }
405
406    /// [`graph`] where `path` names the file of module `i`.
407    fn graph_with_paths(edges: &[&[u32]], path: impl Fn(usize) -> PathBuf) -> ModuleGraph {
408        let files: Vec<DiscoveredFile> = (0..edges.len())
409            .map(|i| DiscoveredFile {
410                id: FileId(u32::try_from(i).unwrap_or(u32::MAX)),
411                path: path(i),
412                size_bytes: 10,
413            })
414            .collect();
415        let modules: Vec<ResolvedModule> = edges
416            .iter()
417            .enumerate()
418            .map(|(i, targets)| ResolvedModule {
419                file_id: FileId(u32::try_from(i).unwrap_or(u32::MAX)),
420                path: path(i),
421                resolved_imports: targets
422                    .iter()
423                    .zip(0_u32..)
424                    .map(|(&to, n)| import(to, n * 20))
425                    .collect(),
426                ..Default::default()
427            })
428            .collect();
429        let entry = vec![EntryPoint {
430            path: path(0),
431            source: EntryPointSource::PackageJsonMain,
432        }];
433        ModuleGraph::build(&modules, &entry, &files)
434    }
435
436    #[test]
437    fn a_cycle_back_into_a_subtree_does_not_hide_its_dominating_import() {
438        // m0 -> m1 -> m2 -> m1: the back edge m2 -> m1 comes from inside the
439        // subtree that m1 dominates, so m0 -> m1 still removes m1 and m2.
440        let graph = graph(&[&[1], &[2], &[1]]);
441        let closure = graph.entry_load_closure(FileId(0));
442        let imports = graph.eager_dominating_imports(FileId(0), &closure.eager, |_| 10);
443        let first = imports.first().expect("m0 -> m1 dominates the cycle");
444        assert_eq!((first.importer, first.target), (FileId(0), FileId(1)));
445        assert_eq!(first.exclusive_modules, 2);
446        assert_eq!(first.exclusive_weight, 20);
447    }
448
449    #[test]
450    fn a_diamond_has_no_single_dominating_import_for_the_shared_module() {
451        // m0 -> m1 -> m3 and m0 -> m2 -> m3: two eager importers keep m3.
452        let graph = graph(&[&[1, 2], &[3], &[3], &[]]);
453        let closure = graph.entry_load_closure(FileId(0));
454        let imports = graph.eager_dominating_imports(FileId(0), &closure.eager, |_| 10);
455        assert!(imports.iter().all(|import| import.target != FileId(3)));
456        assert_eq!(
457            imports.len(),
458            2,
459            "m0 -> m1 and m0 -> m2 each remove one module"
460        );
461    }
462
463    #[test]
464    fn a_declaration_file_never_loads_at_runtime() {
465        // m0 -> m1.d.ts -> m3 and m0 -> m2: a value import of a declaration
466        // file compiles away, so neither it nor its imports load.
467        let graph = graph_with_paths(&[&[1, 2], &[3], &[], &[]], |i| {
468            if i == 1 {
469                PathBuf::from("/p/m1.d.ts")
470            } else {
471                PathBuf::from(format!("/p/m{i}.ts"))
472            }
473        });
474        let closure = graph.entry_load_closure(FileId(0));
475        assert_eq!(closure.eager, vec![FileId(0), FileId(2)]);
476        assert!(closure.deferred.is_empty());
477        assert!(closure.out_of_thread.is_empty());
478        let imports = graph.eager_dominating_imports(FileId(0), &closure.eager, |_| 10);
479        assert!(imports.iter().all(|import| import.target != FileId(1)));
480    }
481
482    #[test]
483    fn an_out_of_range_entry_has_an_empty_closure() {
484        let graph = graph(&[&[]]);
485        assert_eq!(graph.entry_load_closure(FileId(9)).eager, Vec::new());
486    }
487}