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};
9use crate::analyzer::graph::VarMeta;
10
11pub async fn link<'a, B, RB, F, RF>(
12    graph: &VarGraph,
13    mut val: JsValue,
14    early_visitor: &B,
15    visitor: &F,
16    fun_args_values: &Mutex<FxHashMap<u32, Vec<JsValue>>>,
17    var_cache: &Mutex<FxHashMap<Id, JsValue>>,
18) -> Result<(JsValue, u32)>
19where
20    RB: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
21    B: 'a + Fn(JsValue) -> RB + Sync,
22    RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
23    F: 'a + Fn(JsValue) -> RF + Sync,
24{
25    val.normalize();
26    let (val, steps) = link_internal_iterative(
27        graph,
28        val,
29        early_visitor,
30        visitor,
31        fun_args_values,
32        var_cache,
33    )
34    .await?;
35    Ok((val, steps))
36}
37
38const LIMIT_NODE_SIZE: u32 = 100;
39const LIMIT_IN_PROGRESS_NODES: u32 = 500;
40const LIMIT_LINK_STEPS: u32 = 1500;
41
42#[derive(Debug, Hash, Clone, Eq, PartialEq)]
43enum Step {
44    /// Take all chlidren out of the value (replacing temporarily with unknown) and queue them
45    /// for processing using individual `Enter`s.
46    Enter(JsValue),
47    /// Pop however many children there are from `done` and reinsert them into the value
48    Leave(JsValue),
49    /// Remove the variable from `cycle_stack` which detects e.g. circular reassignments
50    LeaveVar(Id),
51    LeaveLate(JsValue),
52    /// Call the visitor callbacks, and requeue the value for further processing if it changed.
53    Visit(JsValue),
54    EarlyVisit(JsValue),
55    /// Remove the call from `fun_args_values`
56    LeaveCall(u32),
57    /// Placeholder that is used to momentarily reserve a slot that is only filled after
58    /// pushing some more steps
59    TemporarySlot,
60}
61
62impl Display for Step {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Step::Enter(val) => write!(f, "Enter({val})"),
66            Step::EarlyVisit(val) => write!(f, "EarlyVisit({val})"),
67            Step::Leave(val) => write!(f, "Leave({val})"),
68            Step::LeaveVar(var) => write!(f, "LeaveVar({var:?})"),
69            Step::LeaveLate(val) => write!(f, "LeaveLate({val})"),
70            Step::Visit(val) => write!(f, "Visit({val})"),
71            Step::LeaveCall(func_ident) => write!(f, "LeaveCall({func_ident})"),
72            Step::TemporarySlot => write!(f, "TemporarySlot"),
73        }
74    }
75}
76// If a variable was already visited in this linking call, don't visit it again.
77
78pub(crate) async fn link_internal_iterative<'a, B, RB, F, RF>(
79    graph: &'a VarGraph,
80    val: JsValue,
81    early_visitor: &'a B,
82    visitor: &'a F,
83    fun_args_values: &Mutex<FxHashMap<u32, Vec<JsValue>>>,
84    var_cache: &Mutex<FxHashMap<Id, JsValue>>,
85) -> Result<(JsValue, u32)>
86where
87    RB: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
88    B: 'a + Fn(JsValue) -> RB + Sync,
89    RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
90    F: 'a + Fn(JsValue) -> RF + Sync,
91{
92    let mut work_queue_stack: Vec<Step> = Vec::new();
93    let mut done: Vec<JsValue> = Vec::new();
94    // Tracks the number of nodes in the queue and done combined
95    let mut total_nodes = 0;
96    let mut cycle_stack: FxHashSet<Id> = FxHashSet::default();
97    // Tracks the number linking steps so far
98    let mut steps = 0;
99
100    total_nodes += val.total_nodes();
101    work_queue_stack.push(Step::Enter(val));
102
103    while let Some(step) = work_queue_stack.pop() {
104        steps += 1;
105
106        match step {
107            // Enter a variable
108            // - replace it with value from graph
109            // - process value
110            // - (Step::LeaveVar potentially caches the value)
111            Step::Enter(JsValue::Variable(var)) => {
112                if cycle_stack.contains(&var) {
113                    done.push(JsValue::unknown(
114                        JsValue::Variable(var.clone()),
115                        false,
116                        "circular variable reference",
117                    ));
118                } else {
119                    total_nodes -= 1;
120                    let var_cache_lock = (cycle_stack.is_empty()
121                        && fun_args_values.lock().is_empty())
122                    .then(|| var_cache.lock());
123                    if let Some(val) = var_cache_lock.as_deref().and_then(|cache| cache.get(&var)) {
124                        total_nodes += val.total_nodes();
125                        done.push(val.clone());
126                    } else if let Some(VarMeta { value, .. }) = graph.values.get(&var) {
127                        cycle_stack.insert(var.clone());
128                        work_queue_stack.push(Step::LeaveVar(var));
129                        total_nodes += value.total_nodes();
130                        work_queue_stack.push(Step::Enter(value.clone()));
131                    } else {
132                        total_nodes += 1;
133                        done.push(JsValue::unknown(
134                            JsValue::Variable(var.clone()),
135                            false,
136                            "no value of this variable analyzed",
137                        ));
138                    }
139                };
140            }
141            // Leave a variable
142            Step::LeaveVar(var) => {
143                cycle_stack.remove(&var);
144                if cycle_stack.is_empty() && fun_args_values.lock().is_empty() {
145                    var_cache.lock().insert(var, done.last().unwrap().clone());
146                }
147            }
148            // Enter a function argument
149            // We want to replace the argument with the value from the function call
150            Step::Enter(JsValue::Argument(func_ident, index)) => {
151                total_nodes -= 1;
152                if let Some(args) = fun_args_values.lock().get(&func_ident) {
153                    if let Some(val) = args.get(index) {
154                        total_nodes += val.total_nodes();
155                        done.push(val.clone());
156                    } else {
157                        total_nodes += 1;
158                        done.push(JsValue::unknown_empty(
159                            false,
160                            "unknown function argument (out of bounds)",
161                        ));
162                    }
163                } else {
164                    total_nodes += 1;
165                    done.push(JsValue::unknown(
166                        JsValue::Argument(func_ident, index),
167                        false,
168                        "function calls are not analyzed yet",
169                    ));
170                }
171            }
172            // Visit a function call
173            // This need special handling, since we want to replace the function call and process
174            // the function return value after that.
175            Step::Visit(JsValue::Call(
176                _,
177                box JsValue::Function(function_nodes, func_ident, return_value),
178                args,
179            )) => {
180                total_nodes -= 2; // Call + Function
181                if let Entry::Vacant(entry) = fun_args_values.lock().entry(func_ident) {
182                    // Return value will stay in total_nodes
183                    for arg in args.iter() {
184                        total_nodes -= arg.total_nodes();
185                    }
186                    entry.insert(args);
187                    work_queue_stack.push(Step::LeaveCall(func_ident));
188                    work_queue_stack.push(Step::Enter(*return_value));
189                } else {
190                    total_nodes -= return_value.total_nodes();
191                    for arg in args.iter() {
192                        total_nodes -= arg.total_nodes();
193                    }
194                    total_nodes += 1;
195                    done.push(JsValue::unknown(
196                        JsValue::call(
197                            Box::new(JsValue::Function(function_nodes, func_ident, return_value)),
198                            args,
199                        ),
200                        true,
201                        "recursive function call",
202                    ));
203                }
204            }
205            // Leaving a function call evaluation
206            // - remove function arguments from the map
207            Step::LeaveCall(func_ident) => {
208                fun_args_values.lock().remove(&func_ident);
209            }
210            // Enter a function
211            // We don't want to process the function return value yet, this will happen after
212            // function calls
213            // - just put it into done
214            Step::Enter(func @ JsValue::Function(..)) => {
215                done.push(func);
216            }
217            // Enter a value
218            // - take and queue children for processing
219            // - on leave: insert children again and optimize
220            Step::Enter(mut val) => {
221                if !val.has_children() {
222                    visit(
223                        &mut total_nodes,
224                        &mut done,
225                        &mut work_queue_stack,
226                        visitor,
227                        val,
228                    )
229                    .await?;
230                } else {
231                    let i = work_queue_stack.len();
232                    work_queue_stack.push(Step::TemporarySlot);
233                    let mut has_early_children = false;
234                    val.for_each_early_children_mut(&mut |child| {
235                        has_early_children = true;
236                        work_queue_stack.push(Step::Enter(take(child)));
237                        false
238                    });
239                    if has_early_children {
240                        work_queue_stack[i] = Step::EarlyVisit(val);
241                    } else {
242                        val.for_each_children_mut(&mut |child| {
243                            work_queue_stack.push(Step::Enter(take(child)));
244                            false
245                        });
246                        work_queue_stack[i] = Step::Leave(val);
247                    }
248                }
249            }
250            // Early visit a value
251            // - reconstruct the value from early children
252            // - visit the value
253            // - insert late children and process for Leave
254            Step::EarlyVisit(mut val) => {
255                val.for_each_early_children_mut(&mut |child| {
256                    let val = done.pop().unwrap();
257                    *child = val;
258                    true
259                });
260                val.debug_assert_total_nodes_up_to_date();
261                total_nodes -= val.total_nodes();
262                if val.total_nodes() > LIMIT_NODE_SIZE {
263                    total_nodes += 1;
264                    done.push(JsValue::unknown_empty(true, "node limit reached"));
265                    continue;
266                }
267
268                let (mut val, visit_modified) = early_visitor(val).await?;
269                val.debug_assert_total_nodes_up_to_date();
270                if visit_modified && val.total_nodes() > LIMIT_NODE_SIZE {
271                    total_nodes += 1;
272                    done.push(JsValue::unknown_empty(true, "node limit reached"));
273                    continue;
274                }
275
276                let count = val.total_nodes();
277                if total_nodes + count > LIMIT_IN_PROGRESS_NODES {
278                    // There is always space for one more node since we just popped at least one
279                    // count
280                    total_nodes += 1;
281                    done.push(JsValue::unknown_empty(
282                        true,
283                        "in progress nodes limit reached",
284                    ));
285                    continue;
286                }
287                total_nodes += count;
288
289                if visit_modified {
290                    // When the early visitor has changed the value, we need to enter it again
291                    work_queue_stack.push(Step::Enter(val));
292                } else {
293                    // Otherwise we can just process the late children
294                    let i = work_queue_stack.len();
295                    work_queue_stack.push(Step::TemporarySlot);
296                    val.for_each_late_children_mut(&mut |child| {
297                        work_queue_stack.push(Step::Enter(take(child)));
298                        false
299                    });
300                    work_queue_stack[i] = Step::LeaveLate(val);
301                }
302            }
303            // Leave a value
304            Step::Leave(mut val) => {
305                val.for_each_children_mut(&mut |child| {
306                    let val = done.pop().unwrap();
307                    *child = val;
308                    true
309                });
310                val.debug_assert_total_nodes_up_to_date();
311
312                total_nodes -= val.total_nodes();
313
314                if val.total_nodes() > LIMIT_NODE_SIZE {
315                    total_nodes += 1;
316                    done.push(JsValue::unknown_empty(true, "node limit reached"));
317                    continue;
318                }
319                val.normalize_shallow();
320
321                val.debug_assert_total_nodes_up_to_date();
322
323                total_nodes += val.total_nodes();
324                work_queue_stack.push(Step::Visit(val));
325            }
326            // Leave a value from EarlyVisit
327            Step::LeaveLate(mut val) => {
328                val.for_each_late_children_mut(&mut |child| {
329                    let val = done.pop().unwrap();
330                    *child = val;
331                    true
332                });
333                val.debug_assert_total_nodes_up_to_date();
334
335                total_nodes -= val.total_nodes();
336
337                if val.total_nodes() > LIMIT_NODE_SIZE {
338                    total_nodes += 1;
339                    done.push(JsValue::unknown_empty(true, "node limit reached"));
340                    continue;
341                }
342                val.normalize_shallow();
343
344                val.debug_assert_total_nodes_up_to_date();
345
346                total_nodes += val.total_nodes();
347                work_queue_stack.push(Step::Visit(val));
348            }
349            // Visit a value with the visitor
350            // - visited value is put into done
351            Step::Visit(val) => {
352                visit(
353                    &mut total_nodes,
354                    &mut done,
355                    &mut work_queue_stack,
356                    visitor,
357                    val,
358                )
359                .await?;
360            }
361            Step::TemporarySlot => unreachable!(),
362        }
363
364        if steps > LIMIT_LINK_STEPS {
365            // Unroll the stack and apply steps that modify "global" state.
366            for step in work_queue_stack.into_iter().rev() {
367                match step {
368                    Step::LeaveVar(var) => {
369                        cycle_stack.remove(&var);
370                        if cycle_stack.is_empty() && fun_args_values.lock().is_empty() {
371                            var_cache.lock().insert(
372                                var,
373                                JsValue::unknown_empty(true, "max number of linking steps reached"),
374                            );
375                        }
376                    }
377                    Step::LeaveCall(func_ident) => {
378                        fun_args_values.lock().remove(&func_ident);
379                    }
380                    _ => {}
381                }
382            }
383
384            tracing::trace!("link limit hit {}", steps);
385            return Ok((
386                JsValue::unknown_empty(true, "max number of linking steps reached"),
387                steps,
388            ));
389        }
390    }
391
392    let final_value = done.pop().unwrap();
393
394    debug_assert!(work_queue_stack.is_empty());
395    debug_assert_eq!(total_nodes, final_value.total_nodes());
396
397    Ok((final_value, steps))
398}
399
400async fn visit<'a, F, RF>(
401    total_nodes: &mut u32,
402    done: &mut Vec<JsValue>,
403    work_queue_stack: &mut Vec<Step>,
404    visitor: &'a F,
405    val: JsValue,
406) -> Result<()>
407where
408    RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
409    F: 'a + Fn(JsValue) -> RF + Sync,
410{
411    *total_nodes -= val.total_nodes();
412
413    let (mut val, visit_modified) = visitor(val).await?;
414    if visit_modified {
415        val.normalize_shallow();
416        #[cfg(debug_assertions)]
417        val.debug_assert_total_nodes_up_to_date();
418        if val.total_nodes() > LIMIT_NODE_SIZE {
419            *total_nodes += 1;
420            done.push(JsValue::unknown_empty(true, "node limit reached"));
421            return Ok(());
422        }
423    }
424
425    let count = val.total_nodes();
426    if *total_nodes + count > LIMIT_IN_PROGRESS_NODES {
427        // There is always space for one more node since we just popped at least one
428        // count
429        *total_nodes += 1;
430        done.push(JsValue::unknown_empty(
431            true,
432            "in progress nodes limit reached",
433        ));
434        return Ok(());
435    }
436    *total_nodes += count;
437    if visit_modified {
438        work_queue_stack.push(Step::Enter(val));
439    } else {
440        done.push(val);
441    }
442    Ok(())
443}