turbopack_ecmascript/analyzer/
linker.rs1use std::{collections::hash_map::Entry, fmt::Display, future::Future, mem::take};
2
3use anyhow::Result;
4use parking_lot::Mutex;
5use rustc_hash::{FxHashMap, FxHashSet};
6use swc_core::ecma::ast::Id;
7
8use super::{JsValue, graph::VarGraph};
9
10pub async fn link<'a, B, RB, F, RF>(
11 graph: &VarGraph,
12 mut val: JsValue,
13 early_visitor: &B,
14 visitor: &F,
15 fun_args_values: &Mutex<FxHashMap<u32, Vec<JsValue>>>,
16 var_cache: &Mutex<FxHashMap<Id, JsValue>>,
17) -> Result<(JsValue, u32)>
18where
19 RB: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
20 B: 'a + Fn(JsValue) -> RB + Sync,
21 RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
22 F: 'a + Fn(JsValue) -> RF + Sync,
23{
24 val.normalize();
25 let (val, steps) = link_internal_iterative(
26 graph,
27 val,
28 early_visitor,
29 visitor,
30 fun_args_values,
31 var_cache,
32 )
33 .await?;
34 Ok((val, steps))
35}
36
37const LIMIT_NODE_SIZE: u32 = 100;
38const LIMIT_IN_PROGRESS_NODES: u32 = 500;
39const LIMIT_LINK_STEPS: u32 = 1500;
40
41#[derive(Debug, Hash, Clone, Eq, PartialEq)]
42enum Step {
43 Enter(JsValue),
46 Leave(JsValue),
48 LeaveVar(Id),
50 LeaveLate(JsValue),
51 Visit(JsValue),
53 EarlyVisit(JsValue),
54 LeaveCall(u32),
56 TemporarySlot,
59}
60
61impl Display for Step {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Step::Enter(val) => write!(f, "Enter({val})"),
65 Step::EarlyVisit(val) => write!(f, "EarlyVisit({val})"),
66 Step::Leave(val) => write!(f, "Leave({val})"),
67 Step::LeaveVar(var) => write!(f, "LeaveVar({var:?})"),
68 Step::LeaveLate(val) => write!(f, "LeaveLate({val})"),
69 Step::Visit(val) => write!(f, "Visit({val})"),
70 Step::LeaveCall(func_ident) => write!(f, "LeaveCall({func_ident})"),
71 Step::TemporarySlot => write!(f, "TemporarySlot"),
72 }
73 }
74}
75pub(crate) async fn link_internal_iterative<'a, B, RB, F, RF>(
78 graph: &'a VarGraph,
79 val: JsValue,
80 early_visitor: &'a B,
81 visitor: &'a F,
82 fun_args_values: &Mutex<FxHashMap<u32, Vec<JsValue>>>,
83 var_cache: &Mutex<FxHashMap<Id, JsValue>>,
84) -> Result<(JsValue, u32)>
85where
86 RB: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
87 B: 'a + Fn(JsValue) -> RB + Sync,
88 RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
89 F: 'a + Fn(JsValue) -> RF + Sync,
90{
91 let mut work_queue_stack: Vec<Step> = Vec::new();
92 let mut done: Vec<JsValue> = Vec::new();
93 let mut total_nodes = 0;
95 let mut cycle_stack: FxHashSet<Id> = FxHashSet::default();
96 let mut steps = 0;
98
99 total_nodes += val.total_nodes();
100 work_queue_stack.push(Step::Enter(val));
101
102 while let Some(step) = work_queue_stack.pop() {
103 steps += 1;
104
105 match step {
106 Step::Enter(JsValue::Variable(var)) => {
111 if cycle_stack.contains(&var) {
112 done.push(JsValue::unknown(
113 JsValue::Variable(var.clone()),
114 false,
115 "circular variable reference",
116 ));
117 } else {
118 total_nodes -= 1;
119 let var_cache_lock = (cycle_stack.is_empty()
120 && fun_args_values.lock().is_empty())
121 .then(|| var_cache.lock());
122 if let Some(val) = var_cache_lock.as_deref().and_then(|cache| cache.get(&var)) {
123 total_nodes += val.total_nodes();
124 done.push(val.clone());
125 } else if let Some(val) = graph.values.get(&var) {
126 cycle_stack.insert(var.clone());
127 work_queue_stack.push(Step::LeaveVar(var));
128 total_nodes += val.total_nodes();
129 work_queue_stack.push(Step::Enter(val.clone()));
130 } else {
131 total_nodes += 1;
132 done.push(JsValue::unknown(
133 JsValue::Variable(var.clone()),
134 false,
135 "no value of this variable analysed",
136 ));
137 }
138 };
139 }
140 Step::LeaveVar(var) => {
142 cycle_stack.remove(&var);
143 if cycle_stack.is_empty() && fun_args_values.lock().is_empty() {
144 var_cache.lock().insert(var, done.last().unwrap().clone());
145 }
146 }
147 Step::Enter(JsValue::Argument(func_ident, index)) => {
150 total_nodes -= 1;
151 if let Some(args) = fun_args_values.lock().get(&func_ident) {
152 if let Some(val) = args.get(index) {
153 total_nodes += val.total_nodes();
154 done.push(val.clone());
155 } else {
156 total_nodes += 1;
157 done.push(JsValue::unknown_empty(
158 false,
159 "unknown function argument (out of bounds)",
160 ));
161 }
162 } else {
163 total_nodes += 1;
164 done.push(JsValue::unknown(
165 JsValue::Argument(func_ident, index),
166 false,
167 "function calls are not analysed yet",
168 ));
169 }
170 }
171 Step::Visit(JsValue::Call(
175 _,
176 box JsValue::Function(_, func_ident, return_value),
177 args,
178 )) => {
179 total_nodes -= 2; if let Entry::Vacant(entry) = fun_args_values.lock().entry(func_ident) {
181 for arg in args.iter() {
183 total_nodes -= arg.total_nodes();
184 }
185 entry.insert(args);
186 work_queue_stack.push(Step::LeaveCall(func_ident));
187 work_queue_stack.push(Step::Enter(*return_value));
188 } else {
189 total_nodes -= return_value.total_nodes();
190 for arg in args.iter() {
191 total_nodes -= arg.total_nodes();
192 }
193 total_nodes += 1;
194 done.push(JsValue::unknown(
195 JsValue::call(Box::new(JsValue::function(func_ident, return_value)), args),
196 true,
197 "recursive function call",
198 ));
199 }
200 }
201 Step::LeaveCall(func_ident) => {
204 fun_args_values.lock().remove(&func_ident);
205 }
206 Step::Enter(func @ JsValue::Function(..)) => {
211 done.push(func);
212 }
213 Step::Enter(mut val) => {
217 if !val.has_children() {
218 visit(
219 &mut total_nodes,
220 &mut done,
221 &mut work_queue_stack,
222 visitor,
223 val,
224 )
225 .await?;
226 } else {
227 let i = work_queue_stack.len();
228 work_queue_stack.push(Step::TemporarySlot);
229 let mut has_early_children = false;
230 val.for_each_early_children_mut(&mut |child| {
231 has_early_children = true;
232 work_queue_stack.push(Step::Enter(take(child)));
233 false
234 });
235 if has_early_children {
236 work_queue_stack[i] = Step::EarlyVisit(val);
237 } else {
238 val.for_each_children_mut(&mut |child| {
239 work_queue_stack.push(Step::Enter(take(child)));
240 false
241 });
242 work_queue_stack[i] = Step::Leave(val);
243 }
244 }
245 }
246 Step::EarlyVisit(mut val) => {
251 val.for_each_early_children_mut(&mut |child| {
252 let val = done.pop().unwrap();
253 *child = val;
254 true
255 });
256 val.debug_assert_total_nodes_up_to_date();
257 total_nodes -= val.total_nodes();
258 if val.total_nodes() > LIMIT_NODE_SIZE {
259 total_nodes += 1;
260 done.push(JsValue::unknown_empty(true, "node limit reached"));
261 continue;
262 }
263
264 let (mut val, visit_modified) = early_visitor(val).await?;
265 val.debug_assert_total_nodes_up_to_date();
266 if visit_modified && val.total_nodes() > LIMIT_NODE_SIZE {
267 total_nodes += 1;
268 done.push(JsValue::unknown_empty(true, "node limit reached"));
269 continue;
270 }
271
272 let count = val.total_nodes();
273 if total_nodes + count > LIMIT_IN_PROGRESS_NODES {
274 total_nodes += 1;
277 done.push(JsValue::unknown_empty(
278 true,
279 "in progress nodes limit reached",
280 ));
281 continue;
282 }
283 total_nodes += count;
284
285 if visit_modified {
286 work_queue_stack.push(Step::Enter(val));
288 } else {
289 let i = work_queue_stack.len();
291 work_queue_stack.push(Step::TemporarySlot);
292 val.for_each_late_children_mut(&mut |child| {
293 work_queue_stack.push(Step::Enter(take(child)));
294 false
295 });
296 work_queue_stack[i] = Step::LeaveLate(val);
297 }
298 }
299 Step::Leave(mut val) => {
301 val.for_each_children_mut(&mut |child| {
302 let val = done.pop().unwrap();
303 *child = val;
304 true
305 });
306 val.debug_assert_total_nodes_up_to_date();
307
308 total_nodes -= val.total_nodes();
309
310 if val.total_nodes() > LIMIT_NODE_SIZE {
311 total_nodes += 1;
312 done.push(JsValue::unknown_empty(true, "node limit reached"));
313 continue;
314 }
315 val.normalize_shallow();
316
317 val.debug_assert_total_nodes_up_to_date();
318
319 total_nodes += val.total_nodes();
320 work_queue_stack.push(Step::Visit(val));
321 }
322 Step::LeaveLate(mut val) => {
324 val.for_each_late_children_mut(&mut |child| {
325 let val = done.pop().unwrap();
326 *child = val;
327 true
328 });
329 val.debug_assert_total_nodes_up_to_date();
330
331 total_nodes -= val.total_nodes();
332
333 if val.total_nodes() > LIMIT_NODE_SIZE {
334 total_nodes += 1;
335 done.push(JsValue::unknown_empty(true, "node limit reached"));
336 continue;
337 }
338 val.normalize_shallow();
339
340 val.debug_assert_total_nodes_up_to_date();
341
342 total_nodes += val.total_nodes();
343 work_queue_stack.push(Step::Visit(val));
344 }
345 Step::Visit(val) => {
348 visit(
349 &mut total_nodes,
350 &mut done,
351 &mut work_queue_stack,
352 visitor,
353 val,
354 )
355 .await?;
356 }
357 Step::TemporarySlot => unreachable!(),
358 }
359
360 if steps > LIMIT_LINK_STEPS {
361 for step in work_queue_stack.into_iter().rev() {
363 match step {
364 Step::LeaveVar(var) => {
365 cycle_stack.remove(&var);
366 if cycle_stack.is_empty() && fun_args_values.lock().is_empty() {
367 var_cache.lock().insert(
368 var,
369 JsValue::unknown_empty(true, "max number of linking steps reached"),
370 );
371 }
372 }
373 Step::LeaveCall(func_ident) => {
374 fun_args_values.lock().remove(&func_ident);
375 }
376 _ => {}
377 }
378 }
379
380 tracing::trace!("link limit hit {}", steps);
381 return Ok((
382 JsValue::unknown_empty(true, "max number of linking steps reached"),
383 steps,
384 ));
385 }
386 }
387
388 let final_value = done.pop().unwrap();
389
390 debug_assert!(work_queue_stack.is_empty());
391 debug_assert_eq!(total_nodes, final_value.total_nodes());
392
393 Ok((final_value, steps))
394}
395
396async fn visit<'a, F, RF>(
397 total_nodes: &mut u32,
398 done: &mut Vec<JsValue>,
399 work_queue_stack: &mut Vec<Step>,
400 visitor: &'a F,
401 val: JsValue,
402) -> Result<()>
403where
404 RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
405 F: 'a + Fn(JsValue) -> RF + Sync,
406{
407 *total_nodes -= val.total_nodes();
408
409 let (mut val, visit_modified) = visitor(val).await?;
410 if visit_modified {
411 val.normalize_shallow();
412 #[cfg(debug_assertions)]
413 val.debug_assert_total_nodes_up_to_date();
414 if val.total_nodes() > LIMIT_NODE_SIZE {
415 *total_nodes += 1;
416 done.push(JsValue::unknown_empty(true, "node limit reached"));
417 return Ok(());
418 }
419 }
420
421 let count = val.total_nodes();
422 if *total_nodes + count > LIMIT_IN_PROGRESS_NODES {
423 *total_nodes += 1;
426 done.push(JsValue::unknown_empty(
427 true,
428 "in progress nodes limit reached",
429 ));
430 return Ok(());
431 }
432 *total_nodes += count;
433 if visit_modified {
434 work_queue_stack.push(Step::Enter(val));
435 } else {
436 done.push(val);
437 }
438 Ok(())
439}