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
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 #[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 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
195struct EagerSubgraph {
197 nodes: Vec<FileId>,
199 root: u32,
201 successors: Vec<Vec<u32>>,
203 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
255struct 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 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
325fn 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
349fn 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 fn graph(edges: &[&[u32]]) -> ModuleGraph {
407 graph_with_paths(edges, |i| PathBuf::from(format!("/p/m{i}.ts")))
408 }
409
410 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 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 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 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}