Skip to main content

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;
7use turbo_rcstr::rcstr;
8
9use super::{JsValue, graph::VarGraph};
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 children 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                        rcstr!("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(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                            rcstr!("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                            rcstr!("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                        rcstr!("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(_, call))
176                if matches!(call.callee(), JsValue::Function(..)) =>
177            {
178                let (callee, args) = call.into_parts();
179                let JsValue::Function(function_nodes, func_ident, return_value) = callee else {
180                    unreachable!()
181                };
182                total_nodes -= 2; // Call + Function
183                if let Entry::Vacant(entry) = fun_args_values.lock().entry(func_ident) {
184                    // Return value will stay in total_nodes
185                    for arg in args.iter() {
186                        total_nodes -= arg.total_nodes();
187                    }
188                    entry.insert(args);
189                    work_queue_stack.push(Step::LeaveCall(func_ident));
190                    work_queue_stack.push(Step::Enter(*return_value));
191                } else {
192                    total_nodes -= return_value.total_nodes();
193                    for arg in args.iter() {
194                        total_nodes -= arg.total_nodes();
195                    }
196                    total_nodes += 1;
197                    done.push(JsValue::unknown(
198                        JsValue::call_from_parts(
199                            JsValue::Function(function_nodes, func_ident, return_value),
200                            args,
201                        ),
202                        true,
203                        rcstr!("recursive function call"),
204                    ));
205                }
206            }
207            // Leaving a function call evaluation
208            // - remove function arguments from the map
209            Step::LeaveCall(func_ident) => {
210                fun_args_values.lock().remove(&func_ident);
211            }
212            // Enter a function
213            // We don't want to process the function return value yet, this will happen after
214            // function calls
215            // - just put it into done
216            Step::Enter(func @ JsValue::Function(..)) => {
217                done.push(func);
218            }
219            // Enter a value
220            // - take and queue children for processing
221            // - on leave: insert children again and optimize
222            Step::Enter(mut val) => {
223                if !val.has_children() {
224                    visit(
225                        &mut total_nodes,
226                        &mut done,
227                        &mut work_queue_stack,
228                        visitor,
229                        val,
230                    )
231                    .await?;
232                } else {
233                    let i = work_queue_stack.len();
234                    work_queue_stack.push(Step::TemporarySlot);
235                    let mut has_early_children = false;
236                    val.for_each_early_children_mut(&mut |child| {
237                        has_early_children = true;
238                        work_queue_stack.push(Step::Enter(take(child)));
239                        false
240                    });
241                    if has_early_children {
242                        work_queue_stack[i] = Step::EarlyVisit(val);
243                    } else {
244                        val.for_each_children_mut(&mut |child| {
245                            work_queue_stack.push(Step::Enter(take(child)));
246                            false
247                        });
248                        work_queue_stack[i] = Step::Leave(val);
249                    }
250                }
251            }
252            // Early visit a value
253            // - reconstruct the value from early children
254            // - visit the value
255            // - insert late children and process for Leave
256            Step::EarlyVisit(mut val) => {
257                val.for_each_early_children_mut(&mut |child| {
258                    let val = done.pop().unwrap();
259                    *child = val;
260                    true
261                });
262                val.debug_assert_total_nodes_up_to_date();
263                total_nodes -= val.total_nodes();
264                if val.total_nodes() > LIMIT_NODE_SIZE {
265                    total_nodes += 1;
266                    done.push(JsValue::unknown_empty(true, rcstr!("node limit reached")));
267                    continue;
268                }
269
270                let (mut val, visit_modified) = early_visitor(val).await?;
271                val.debug_assert_total_nodes_up_to_date();
272                if visit_modified && val.total_nodes() > LIMIT_NODE_SIZE {
273                    total_nodes += 1;
274                    done.push(JsValue::unknown_empty(true, rcstr!("node limit reached")));
275                    continue;
276                }
277
278                let count = val.total_nodes();
279                if total_nodes + count > LIMIT_IN_PROGRESS_NODES {
280                    // There is always space for one more node since we just popped at least one
281                    // count
282                    total_nodes += 1;
283                    done.push(JsValue::unknown_empty(
284                        true,
285                        rcstr!("in progress nodes limit reached"),
286                    ));
287                    continue;
288                }
289                total_nodes += count;
290
291                if visit_modified {
292                    // When the early visitor has changed the value, we need to enter it again
293                    work_queue_stack.push(Step::Enter(val));
294                } else {
295                    // Otherwise we can just process the late children
296                    let i = work_queue_stack.len();
297                    work_queue_stack.push(Step::TemporarySlot);
298                    val.for_each_late_children_mut(&mut |child| {
299                        work_queue_stack.push(Step::Enter(take(child)));
300                        false
301                    });
302                    work_queue_stack[i] = Step::LeaveLate(val);
303                }
304            }
305            // Leave a value
306            Step::Leave(mut val) => {
307                val.for_each_children_mut(&mut |child| {
308                    let val = done.pop().unwrap();
309                    *child = val;
310                    true
311                });
312                val.debug_assert_total_nodes_up_to_date();
313
314                total_nodes -= val.total_nodes();
315
316                if val.total_nodes() > LIMIT_NODE_SIZE {
317                    total_nodes += 1;
318                    done.push(JsValue::unknown_empty(true, rcstr!("node limit reached")));
319                    continue;
320                }
321                val.normalize_shallow();
322
323                val.debug_assert_total_nodes_up_to_date();
324
325                total_nodes += val.total_nodes();
326                work_queue_stack.push(Step::Visit(val));
327            }
328            // Leave a value from EarlyVisit
329            Step::LeaveLate(mut val) => {
330                val.for_each_late_children_mut(&mut |child| {
331                    let val = done.pop().unwrap();
332                    *child = val;
333                    true
334                });
335                val.debug_assert_total_nodes_up_to_date();
336
337                total_nodes -= val.total_nodes();
338
339                if val.total_nodes() > LIMIT_NODE_SIZE {
340                    total_nodes += 1;
341                    done.push(JsValue::unknown_empty(true, rcstr!("node limit reached")));
342                    continue;
343                }
344                val.normalize_shallow();
345
346                val.debug_assert_total_nodes_up_to_date();
347
348                total_nodes += val.total_nodes();
349                work_queue_stack.push(Step::Visit(val));
350            }
351            // Visit a value with the visitor
352            // - visited value is put into done
353            Step::Visit(val) => {
354                visit(
355                    &mut total_nodes,
356                    &mut done,
357                    &mut work_queue_stack,
358                    visitor,
359                    val,
360                )
361                .await?;
362            }
363            Step::TemporarySlot => unreachable!(),
364        }
365
366        if steps > LIMIT_LINK_STEPS {
367            // Unroll the stack and apply steps that modify "global" state.
368            for step in work_queue_stack.into_iter().rev() {
369                match step {
370                    Step::LeaveVar(var) => {
371                        cycle_stack.remove(&var);
372                        if cycle_stack.is_empty() && fun_args_values.lock().is_empty() {
373                            var_cache.lock().insert(
374                                var,
375                                JsValue::unknown_empty(
376                                    true,
377                                    rcstr!("max number of linking steps reached"),
378                                ),
379                            );
380                        }
381                    }
382                    Step::LeaveCall(func_ident) => {
383                        fun_args_values.lock().remove(&func_ident);
384                    }
385                    _ => {}
386                }
387            }
388
389            tracing::trace!("link limit hit {}", steps);
390            return Ok((
391                JsValue::unknown_empty(true, rcstr!("max number of linking steps reached")),
392                steps,
393            ));
394        }
395    }
396
397    let final_value = done.pop().unwrap();
398
399    debug_assert!(work_queue_stack.is_empty());
400    debug_assert_eq!(total_nodes, final_value.total_nodes());
401
402    Ok((final_value, steps))
403}
404
405async fn visit<'a, F, RF>(
406    total_nodes: &mut u32,
407    done: &mut Vec<JsValue>,
408    work_queue_stack: &mut Vec<Step>,
409    visitor: &'a F,
410    val: JsValue,
411) -> Result<()>
412where
413    RF: 'a + Future<Output = Result<(JsValue, bool)>> + Send,
414    F: 'a + Fn(JsValue) -> RF + Sync,
415{
416    *total_nodes -= val.total_nodes();
417
418    let (mut val, visit_modified) = visitor(val).await?;
419    if visit_modified {
420        val.normalize_shallow();
421        #[cfg(debug_assertions)]
422        val.debug_assert_total_nodes_up_to_date();
423        if val.total_nodes() > LIMIT_NODE_SIZE {
424            *total_nodes += 1;
425            done.push(JsValue::unknown_empty(true, rcstr!("node limit reached")));
426            return Ok(());
427        }
428    }
429
430    let count = val.total_nodes();
431    if *total_nodes + count > LIMIT_IN_PROGRESS_NODES {
432        // There is always space for one more node since we just popped at least one
433        // count
434        *total_nodes += 1;
435        done.push(JsValue::unknown_empty(
436            true,
437            rcstr!("in progress nodes limit reached"),
438        ));
439        return Ok(());
440    }
441    *total_nodes += count;
442    if visit_modified {
443        work_queue_stack.push(Step::Enter(val));
444    } else {
445        done.push(val);
446    }
447    Ok(())
448}