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};
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 Enter(JsValue),
47 Leave(JsValue),
49 LeaveVar(Id),
51 LeaveLate(JsValue),
52 Visit(JsValue),
54 EarlyVisit(JsValue),
55 LeaveCall(u32),
57 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}
76pub(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 let mut total_nodes = 0;
96 let mut cycle_stack: FxHashSet<Id> = FxHashSet::default();
97 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 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 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 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 Step::Visit(JsValue::Call(
176 _,
177 box JsValue::Function(function_nodes, func_ident, return_value),
178 args,
179 )) => {
180 total_nodes -= 2; if let Entry::Vacant(entry) = fun_args_values.lock().entry(func_ident) {
182 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 Step::LeaveCall(func_ident) => {
208 fun_args_values.lock().remove(&func_ident);
209 }
210 Step::Enter(func @ JsValue::Function(..)) => {
215 done.push(func);
216 }
217 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 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 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 work_queue_stack.push(Step::Enter(val));
292 } else {
293 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 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 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 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 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 *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}