1use 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
26pub struct EntryLoadClosure {
27 pub eager: Vec<FileId>,
30 pub deferred: Vec<FileId>,
33 pub out_of_thread: Vec<FileId>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct DominatingImport {
41 pub importer: FileId,
43 pub target: FileId,
45 pub import_span_start: Option<u32>,
49 pub exclusive_modules: usize,
51 pub exclusive_weight: u64,
53}
54
55const NO_NODE: u32 = u32::MAX;
56
57impl ModuleGraph {
58 #[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 #[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 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
191struct EagerSubgraph {
193 nodes: Vec<FileId>,
195 root: u32,
197 successors: Vec<Vec<u32>>,
199 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
251struct 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 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
321fn 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
345fn 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 fn graph(edges: &[&[u32]]) -> ModuleGraph {
403 graph_with_paths(edges, |i| PathBuf::from(format!("/p/m{i}.ts")))
404 }
405
406 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 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 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 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}