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;
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 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 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 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 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 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; if let Entry::Vacant(entry) = fun_args_values.lock().entry(func_ident) {
184 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 Step::LeaveCall(func_ident) => {
210 fun_args_values.lock().remove(&func_ident);
211 }
212 Step::Enter(func @ JsValue::Function(..)) => {
217 done.push(func);
218 }
219 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 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 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 work_queue_stack.push(Step::Enter(val));
294 } else {
295 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 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 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 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 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 *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}