turbopack_ecmascript/analyzer/
linker.rs

1use 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    /// Take all chlidren out of the value (replacing temporarily with unknown) and queue them
44    /// for processing using individual `Enter`s.
45    Enter(JsValue),
46    /// Pop however many children there are from `done` and reinsert them into the value
47    Leave(JsValue),
48    /// Remove the variable from `cycle_stack` which detects e.g. circular reassignments
49    LeaveVar(Id),
50    LeaveLate(JsValue),
51    /// Call the visitor callbacks, and requeue the value for further processing if it changed.
52    Visit(JsValue),
53    EarlyVisit(JsValue),
54    /// Remove the call from `fun_args_values`
55    LeaveCall(u32),
56    /// Placeholder that is used to momentarily reserve a slot that is only filled after
57    /// pushing some more steps
58    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}
75// If a variable was already visited in this linking call, don't visit it again.
76
77pub(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    // Tracks the number of nodes in the queue and done combined
94    let mut total_nodes = 0;
95    let mut cycle_stack: FxHashSet<Id> = FxHashSet::default();
96    // Tracks the number linking steps so far
97    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            // Enter a variable
107            // - replace it with value from graph
108            // - process value
109            // - (Step::LeaveVar potentially caches the value)
110            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            // Leave a variable
141            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            // Enter a function argument
148            // We want to replace the argument with the value from the function call
149            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            // Visit a function call
172            // This need special handling, since we want to replace the function call and process
173            // the function return value after that.
174            Step::Visit(JsValue::Call(
175                _,
176                box JsValue::Function(_, func_ident, return_value),
177                args,
178            )) => {
179                total_nodes -= 2; // Call + Function
180                if let Entry::Vacant(entry) = fun_args_values.lock().entry(func_ident) {
181                    // Return value will stay in total_nodes
182                    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            // Leaving a function call evaluation
202            // - remove function arguments from the map
203            Step::LeaveCall(func_ident) => {
204                fun_args_values.lock().remove(&func_ident);
205            }
206            // Enter a function
207            // We don't want to process the function return value yet, this will happen after
208            // function calls
209            // - just put it into done
210            Step::Enter(func @ JsValue::Function(..)) => {
211                done.push(func);
212            }
213            // Enter a value
214            // - take and queue children for processing
215            // - on leave: insert children again and optimize
216            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            // Early visit a value
247            // - reconstruct the value from early children
248            // - visit the value
249            // - insert late children and process for Leave
250            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                    // There is always space for one more node since we just popped at least one
275                    // count
276                    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                    // When the early visitor has changed the value, we need to enter it again
287                    work_queue_stack.push(Step::Enter(val));
288                } else {
289                    // Otherwise we can just process the late children
290                    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            // Leave a value
300            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            // Leave a value from EarlyVisit
323            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            // Visit a value with the visitor
346            // - visited value is put into done
347            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            // Unroll the stack and apply steps that modify "global" state.
362            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        // There is always space for one more node since we just popped at least one
424        // count
425        *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}