luaur_analysis/methods/frontend_parse_graph.rs
1use crate::enums::mark::Mark;
2use crate::records::frontend::Frontend;
3use crate::records::source_node::SourceNode;
4use crate::records::type_check_limits::TypeCheckLimits;
5use crate::type_aliases::module_name_file_resolver::ModuleName;
6use alloc::vec::Vec;
7use luaur_common::macros::luau_assert::LUAU_ASSERT;
8use luaur_common::macros::luau_timetrace_argument::LUAU_TIMETRACE_ARGUMENT;
9use luaur_common::macros::luau_timetrace_scope::LUAU_TIMETRACE_SCOPE;
10use luaur_common::records::dense_hash_map::DenseHashMap;
11
12impl Frontend {
13 pub fn parse_graph(
14 &mut self,
15 build_queue: &mut Vec<ModuleName>,
16 root: &ModuleName,
17 limits: &TypeCheckLimits,
18 for_autocomplete: bool,
19 ) -> bool {
20 LUAU_TIMETRACE_SCOPE!("Frontend::parseGraph", "Frontend");
21 LUAU_TIMETRACE_ARGUMENT!("root", root.as_str());
22
23 let mut seen: DenseHashMap<*mut SourceNode, Mark> =
24 DenseHashMap::new(core::ptr::null_mut());
25 let mut stack: Vec<*mut SourceNode> = Vec::new();
26 let mut path: Vec<*mut SourceNode> = Vec::new();
27 let mut cyclic = false;
28
29 {
30 let (source_node, _) = self.get_source_node(root, limits);
31 if !source_node.is_null() {
32 stack.push(source_node);
33 }
34 }
35
36 while !stack.is_empty() {
37 let top = stack.pop().unwrap();
38
39 if top.is_null() {
40 // special marker for post-order processing
41 LUAU_ASSERT!(!path.is_empty());
42
43 let top = path.pop().unwrap();
44
45 // note: topseen ref gets invalidated in any seen[] access, beware - only one seen[] access per iteration!
46 let topseen = seen.get_or_insert(top);
47 LUAU_ASSERT!(*topseen == Mark::Temporary);
48 *topseen = Mark::Permanent;
49
50 build_queue.push(unsafe { (*top).name.clone() });
51
52 // at this point we know all valid dependencies are processed into SourceNodes
53 let require_set = unsafe { &(*top).require_set };
54 for dep in require_set.iter() {
55 if let Some(source_node_arc) = self.source_nodes.get(dep) {
56 let source_node_ref: *mut SourceNode =
57 source_node_arc.as_ref() as *const SourceNode as *mut SourceNode;
58 let dependents = unsafe { &mut (*source_node_ref).dependents };
59 dependents.insert(unsafe { (*top).name.clone() });
60 }
61 }
62 } else {
63 // note: topseen ref gets invalidated in any seen[] access, beware - only one seen[] access per iteration!
64 let topseen = seen.get_or_insert(top);
65
66 if *topseen != Mark::None {
67 cyclic |= *topseen == Mark::Temporary;
68 continue;
69 }
70
71 *topseen = Mark::Temporary;
72
73 // push marker for post-order processing
74 stack.push(core::ptr::null_mut());
75 path.push(top);
76
77 // push children
78 let require_set = unsafe { &(*top).require_set };
79 for dep in require_set.iter() {
80 // Resolve the already-known SourceNode pointer (if any) in a scope so the
81 // immutable borrow of `self.source_nodes` is released before the mutable
82 // `self.get_source_node` call below.
83 let known: Option<*mut SourceNode> = {
84 if let Some(source_node_arc) = self.source_nodes.get(dep) {
85 // this is a critical optimization: we do *not* traverse non-dirty subtrees.
86 // this relies on the fact that markDirty marks reverse-dependencies dirty as well
87 // thus if a node is not dirty, all its transitive deps aren't dirty, which means that they won't ever need
88 // to be built, *and* can't form a cycle with any nodes we did process.
89 if !source_node_arc.has_dirty_module(for_autocomplete) {
90 None
91 } else {
92 Some(source_node_arc.as_ref() as *const SourceNode
93 as *mut SourceNode)
94 }
95 } else {
96 // no SourceNode known yet; fall through to getSourceNode
97 Some(core::ptr::null_mut())
98 }
99 };
100
101 match known {
102 // dependency is known but not dirty: skip
103 None => continue,
104 // dependency is known and dirty
105 Some(ptr) if !ptr.is_null() => {
106 // This module might already be in the outside build queue
107 // Note: canSkip is not available in this context, so we skip this check
108
109 // note: this check is technically redundant *except* that getSourceNode has somewhat broken memoization
110 // calling getSourceNode twice in succession will reparse the file, since getSourceNode leaves dirty flag set
111 if seen.contains_key(&ptr) {
112 stack.push(ptr);
113 continue;
114 }
115 }
116 // dependency not yet resolved
117 Some(_) => {}
118 }
119
120 let (source_node, _) = self.get_source_node(dep, limits);
121 if !source_node.is_null() {
122 stack.push(source_node);
123
124 // note: this assignment is paired with .contains() check above and effectively deduplicates getSourceNode()
125 seen.try_insert(source_node, Mark::None);
126 }
127 }
128 }
129 }
130
131 cyclic
132 }
133}