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