turbopack_ecmascript/analyzer/builtin.rs
1use std::mem::take;
2
3use smallvec::SmallVec;
4use turbo_rcstr::rcstr;
5
6use super::{ConstantNumber, ConstantValue, JsValue, LogicalOperator, LogicalProperty, ObjectPart};
7use crate::analyzer::{Bump, BumpVec, JsValueUrlKind, Modified};
8
9/// Replaces some builtin values with their resulting values. Called early
10/// without lazy nested values. This allows to skip a lot of work to process the
11/// arguments.
12pub fn early_replace_builtin(value: &mut JsValue<'_>) -> Modified {
13 match value {
14 // matching calls like `callee(arg1, arg2, ...)`
15 JsValue::Call(_, call) => {
16 let (args, callee) = call.as_parts_mut();
17 let args_have_side_effects = || args.iter().any(|arg| arg.has_side_effects());
18 match callee {
19 // We don't know what the callee is, so we can early return
20 &mut JsValue::Unknown {
21 original_value: _,
22 reason: _,
23 has_side_effects,
24 } => {
25 let has_side_effects = has_side_effects || args_have_side_effects();
26 value.make_unknown(has_side_effects, rcstr!("unknown callee"));
27 Modified::Yes
28 }
29 // We known that these callee will lead to an error at runtime, so we can skip
30 // processing them
31 JsValue::Constant(_)
32 | JsValue::Url(_, _)
33 | JsValue::WellKnownObject(_)
34 | JsValue::Array { .. }
35 | JsValue::Object { .. }
36 | JsValue::Alternatives { .. }
37 | JsValue::Concat(_, _)
38 | JsValue::Add(_, _)
39 | JsValue::Not(_, _) => {
40 let has_side_effects = args_have_side_effects();
41 value.make_unknown(has_side_effects, rcstr!("non-function callee"));
42 Modified::Yes
43 }
44 _ => Modified::No,
45 }
46 }
47 // matching calls with this context like `obj.prop(arg1, arg2, ...)`
48 JsValue::MemberCall(_, call) => {
49 let (args, prop, obj) = call.as_parts_mut();
50 let args_have_side_effects = || args.iter().any(|arg| arg.has_side_effects());
51 match obj {
52 // We don't know what the callee is, so we can early return
53 &mut JsValue::Unknown {
54 original_value: _,
55 reason: _,
56 has_side_effects,
57 } => {
58 let side_effects =
59 has_side_effects || prop.has_side_effects() || args_have_side_effects();
60 value.make_unknown(side_effects, rcstr!("unknown callee object"));
61 Modified::Yes
62 }
63 // otherwise we need to look at the property
64 _ => match prop {
65 // We don't know what the property is, so we can early return
66 &mut JsValue::Unknown {
67 original_value: _,
68 reason: _,
69 has_side_effects,
70 } => {
71 let side_effects = has_side_effects || args_have_side_effects();
72 value.make_unknown(side_effects, rcstr!("unknown callee property"));
73 Modified::Yes
74 }
75 _ => Modified::No,
76 },
77 }
78 }
79 // matching property access like `obj.prop` when we don't know what the obj is.
80 // We can early return here
81 JsValue::Member(_, obj, prop) => {
82 if let JsValue::Unknown {
83 has_side_effects, ..
84 } = &**obj
85 {
86 let side_effects = *has_side_effects || prop.has_side_effects();
87 value.make_unknown(side_effects, rcstr!("unknown object"));
88 Modified::Yes
89 } else {
90 Modified::No
91 }
92 }
93 _ => Modified::No,
94 }
95}
96
97/// Replaces some builtin functions and values with their resulting values. In
98/// contrast to early_replace_builtin this has all inner values already
99/// processed.
100pub fn replace_builtin<'a>(arena: &'a Bump, value: &mut JsValue<'a>) -> Modified {
101 match value {
102 JsValue::Add(_, list) => {
103 // numeric addition
104 let mut sum = 0f64;
105 for arg in list {
106 let JsValue::Constant(ConstantValue::Num(num)) = arg else {
107 return Modified::No;
108 };
109 sum += num.0;
110 }
111 *value = JsValue::Constant(ConstantValue::Num(sum.into()));
112 Modified::Yes
113 }
114
115 // matching property access like `obj.prop`
116 // Accessing a property on something can be handled in some cases
117 JsValue::Member(_, obj, prop) => match &mut **obj {
118 // matching property access when obj is a bunch of alternatives
119 // like `(obj1 | obj2 | obj3).prop`
120 // We expand these to `obj1.prop | obj2.prop | obj3.prop`
121 JsValue::Alternatives {
122 total_nodes: _,
123 values,
124 logical_property: _,
125 } => {
126 *value = JsValue::alternatives(BumpVec::from_iter_in(
127 arena,
128 take(values)
129 .into_iter()
130 .map(|alt| JsValue::member(arena, alt, prop.clone_in(arena))),
131 ));
132 Modified::Yes
133 }
134 // matching property access on an array like `[1,2,3].prop` or `[1,2,3][1]`
135 &mut JsValue::Array {
136 ref mut items,
137 mutable,
138 ..
139 } => {
140 fn items_to_alternatives<'a>(
141 arena: &'a Bump,
142 items: &mut BumpVec<'a, JsValue<'a>>,
143 prop: &mut JsValue<'a>,
144 ) -> JsValue<'a> {
145 items.push(arena, JsValue::unknown(
146 JsValue::member(arena, JsValue::array(BumpVec::new()), take(prop)),
147 false,
148 rcstr!("unknown array prototype methods or values"),
149 ));
150 JsValue::alternatives(take(items))
151 }
152 match &mut **prop {
153 // accessing a numeric property on an array like `[1,2,3][1]`
154 // We can replace this with the value at the index
155 JsValue::Constant(ConstantValue::Num(num @ ConstantNumber(_))) => {
156 if let Some(index) = num.as_u32_index() {
157 if index < items.len() {
158 *value = items.swap_remove(index);
159 if mutable {
160 value.add_unknown_mutations(arena, true);
161 }
162 Modified::Yes
163 } else {
164 *value = JsValue::unknown(
165 JsValue::member(arena, take(&mut **obj), take(&mut **prop)),
166 false,
167 rcstr!("invalid index"),
168 );
169 Modified::Yes
170 }
171 } else {
172 value.make_unknown(false, rcstr!("non-num constant property on array"));
173 Modified::Yes
174 }
175 }
176 // accessing a non-numeric property on an array like `[1,2,3].length`
177 // We don't know what happens here
178 JsValue::Constant(_) => {
179 value.make_unknown(false, rcstr!("non-num constant property on array"));
180 Modified::Yes
181 }
182 // accessing multiple alternative properties on an array like `[1,2,3][(1 | 2 |
183 // prop3)]`
184 JsValue::Alternatives {
185 total_nodes: _,
186 values,
187 logical_property: _,
188 } => {
189 *value = JsValue::alternatives(BumpVec::from_iter_in(
190 arena,
191 take(values)
192 .into_iter()
193 .map(|alt| JsValue::member(arena, obj.clone_in(arena), alt)),
194 ));
195 Modified::Yes
196 }
197 // otherwise we can say that this might gives an item of the array
198 // but we also add an unknown value to the alternatives for other properties
199 _ => {
200 *value = items_to_alternatives(arena, items, prop);
201 Modified::Yes
202 }
203 }
204 }
205 // matching property access on an object like `{a: 1, b: 2}.a`
206 &mut JsValue::Object {
207 ref mut parts,
208 mutable,
209 ..
210 } => {
211 fn parts_to_alternatives<'a>(
212 arena: &'a Bump,
213 parts: impl IntoIterator<Item = ObjectPart<'a>>,
214 prop: &mut JsValue<'a>,
215 include_unknown: bool,
216 ) -> JsValue<'a> {
217 let parts = parts.into_iter();
218 let (lower, upper) = parts.size_hint();
219 let mut values = BumpVec::with_capacity_in(
220 arena, upper.unwrap_or(lower) + if include_unknown { 1 } else { 0 }
221 );
222 for part in parts {
223 match part {
224 ObjectPart::KeyValue(_, value) => {
225 values.push(arena, value);
226 }
227 ObjectPart::Spread(_) => {
228 values.push(arena, JsValue::unknown(
229 JsValue::member(
230 arena,
231 JsValue::object(BumpVec::from_iter_in(arena, [part])),
232 prop.clone_in(arena),
233 ),
234 true,
235 rcstr!("spread object"),
236 ));
237 }
238 }
239 }
240 if include_unknown {
241 values.push(arena, JsValue::unknown(
242 JsValue::member(
243 arena,
244 JsValue::object(BumpVec::new()),
245 take(prop),
246 ),
247 true,
248 rcstr!("unknown object prototype methods or values"),
249 ));
250 }
251 JsValue::alternatives(values)
252 }
253
254 /// Convert a list of potential values into
255 /// JsValue::Alternatives Optionally add a
256 /// unknown value to the alternatives for object prototype
257 /// methods
258 fn potential_values_to_alternatives<'a>(
259 arena: &'a Bump,
260 mut potential_values: SmallVec<[usize; 8]>,
261 parts: &mut BumpVec<'a, ObjectPart<'a>>,
262 prop: &mut JsValue<'a>,
263 include_unknown: bool,
264 ) -> JsValue<'a> {
265 // Note: potential_values are already in reverse order
266 let mut potential_values = take(parts)
267 .into_iter()
268 .enumerate()
269 .filter(|(i, _)| {
270 if potential_values.last() == Some(i) {
271 potential_values.pop();
272 true
273 } else {
274 false
275 }
276 })
277 .map(|(_, part)| part);
278 parts_to_alternatives(arena, &mut potential_values, prop, include_unknown)
279 }
280
281 match &mut **prop {
282 // matching constant string property access on an object like `{a: 1, b:
283 // 2}["a"]`
284 JsValue::Constant(ConstantValue::Str(_)) => {
285 let prop_str = prop.as_str().unwrap();
286 let mut potential_values: SmallVec<[usize; 8]> = SmallVec::new();
287 for (i, part) in parts.iter_mut().enumerate().rev() {
288 match part {
289 ObjectPart::KeyValue(key, val) => {
290 if let Some(key) = key.as_str() {
291 if key == prop_str {
292 if potential_values.is_empty() {
293 *value = take(val);
294 } else {
295 potential_values.push(i);
296 *value = potential_values_to_alternatives(
297 arena,
298 potential_values,
299 parts,
300 prop,
301 false,
302 );
303 }
304 if mutable {
305 value.add_unknown_mutations(arena, true);
306 }
307 return Modified::Yes;
308 }
309 } else {
310 potential_values.push(i);
311 }
312 }
313 ObjectPart::Spread(_) => {
314 value.make_unknown(true, rcstr!("spread object"));
315 return Modified::Yes;
316 }
317 }
318 }
319 if potential_values.is_empty() {
320 *value = JsValue::Constant(ConstantValue::Undefined);
321 } else {
322 *value = potential_values_to_alternatives(
323 arena,
324 potential_values,
325 parts,
326 prop,
327 true,
328 );
329 }
330 if mutable {
331 value.add_unknown_mutations(arena, true);
332 }
333 Modified::Yes
334 }
335 // matching multiple alternative properties on an object like `{a: 1, b: 2}[(a |
336 // b)]`
337 JsValue::Alternatives {
338 total_nodes: _,
339 values,
340 logical_property: _,
341 } => {
342 *value = JsValue::alternatives(BumpVec::from_iter_in(
343 arena,
344 take(values)
345 .into_iter()
346 .map(|alt| JsValue::member(arena, obj.clone_in(arena), alt)),
347 ));
348 Modified::Yes
349 }
350 _ => {
351 *value = parts_to_alternatives(arena, take(parts), prop, true);
352 Modified::Yes
353 }
354 }
355 }
356 _ => Modified::No,
357 },
358
359 JsValue::MemberCall(_, _) => {
360 // `into_parts` pops obj + prop off the tail of the underlying `Vec`, and the
361 // remaining `Vec` (owned, not reallocated) becomes `args`. We take the whole
362 // `value` because `MemberCallList` has no `Default` to move it out directly.
363 let JsValue::MemberCall(_, call) = take(value) else {
364 unreachable!()
365 };
366 let (mut obj, prop, args) = call.into_parts();
367 match &mut obj {
368 // matching calls on an array like `[1,2,3].concat([4,5,6])`
369 JsValue::Array { items, mutable, .. } => {
370 // matching cases where the property is a const string
371 if let Some(str) = prop.as_str() {
372 match str {
373 // The Array.prototype.concat method
374 "concat"
375 if args.iter().all(|arg| {
376 matches!(
377 arg,
378 JsValue::Array { .. }
379 | JsValue::Constant(_)
380 | JsValue::Url(_, JsValueUrlKind::Absolute)
381 | JsValue::Concat(..)
382 | JsValue::Add(..)
383 | JsValue::WellKnownObject(_)
384 | JsValue::WellKnownFunction(_)
385 | JsValue::Function(..)
386 )
387 }) => {
388 for arg in args {
389 match arg {
390 JsValue::Array {
391 items: inner,
392 mutable: inner_mutable,
393 ..
394 } => {
395 items.extend(arena, inner);
396 *mutable |= inner_mutable;
397 }
398 other @ (JsValue::Constant(_)
399 | JsValue::Url(_, JsValueUrlKind::Absolute)
400 | JsValue::Concat(..)
401 | JsValue::Add(..)
402 | JsValue::WellKnownObject(_)
403 | JsValue::WellKnownFunction(_)
404 | JsValue::Function(..)) => {
405 items.push(arena, other);
406 }
407 _ => {
408 unreachable!();
409 }
410 }
411 }
412 obj.update_total_nodes();
413 *value = obj;
414 return Modified::Yes;
415 }
416 // The Array.prototype.map method
417 "map" => {
418 if let Some(func) = args.first() {
419 *value = JsValue::array(BumpVec::from_iter_in(
420 arena,
421 take(items).into_iter().enumerate().map(|(i, item)| {
422 JsValue::call_from_iter(
423 arena,
424 func.clone_in(arena),
425 [
426 item,
427 JsValue::Constant(ConstantValue::Num(
428 (i as f64).into(),
429 )),
430 ],
431 )
432 }),
433 ));
434 return Modified::Yes;
435 }
436 }
437 _ => {}
438 }
439 }
440 }
441 // matching calls on multiple alternative objects like `(obj1 | obj2).prop(arg1,
442 // arg2, ...)`
443 JsValue::Alternatives {
444 total_nodes: _,
445 values,
446 logical_property: _,
447 } => {
448 *value = JsValue::alternatives(BumpVec::from_iter_in(
449 arena,
450 take(values).into_iter().map(|alt| {
451 JsValue::member_call_from_iter(
452 arena,
453 alt,
454 prop.clone_in(arena),
455 args.iter().map(|a| a.clone_in(arena)),
456 )
457 },
458 )));
459 return Modified::Yes;
460 }
461 _ => {}
462 }
463
464 // matching calls on strings like `"dayjs/locale/".concat(userLocale, ".js")`
465 if obj.is_string() == Some(true)
466 && let Some(str) = prop.as_str()
467 {
468 // The String.prototype.concat method
469 if str == "concat" {
470 let mut values = BumpVec::with_capacity_in(arena, 1 + args.len());
471 values.push(arena, obj);
472 values.extend(arena, args);
473
474 *value = JsValue::concat(values);
475 return Modified::Yes;
476 }
477 }
478
479 // without special handling, we convert it into a normal call like
480 // `(obj.prop)(arg1, arg2, ...)`.
481 //
482 // Pass-through path: `args` came from `MemberCallList::into_parts` which yields
483 // a `Vec` with `cap >= len + 2` (slack from the original layout). Re-wrapping it
484 // into a `JsValue::Call` only needs `+1` slot, which fits in the existing slack —
485 // no realloc. This is the original motivation for the `[args..., prop, obj]`
486 // tail layout.
487 *value = JsValue::call_from_parts(arena, JsValue::member(arena, obj, prop), args);
488 Modified::Yes
489 }
490 // match calls when the callee are multiple alternative functions like `(func1 |
491 // func2)(arg1, arg2, ...)`
492 JsValue::Call(_, call)
493 if matches!(call.callee(), JsValue::Alternatives { .. }) =>
494 {
495 // Take the whole `value` (not `call`) because `CallList` has no `Default`, then
496 // move the alternatives `values` out of the callee.
497 let JsValue::Call(_, call) = take(value) else {
498 unreachable!()
499 };
500 let (callee, args) = call.into_parts();
501 let JsValue::Alternatives { values, .. } = callee else {
502 unreachable!()
503 };
504 *value = JsValue::alternatives(BumpVec::from_iter_in(arena,
505 values
506 .into_iter()
507 .map(|alt| JsValue::call_from_iter(arena, alt, args.iter().map(|a| a.clone_in(arena)))),
508 ));
509 Modified::Yes
510 }
511 // match object literals
512 JsValue::Object { parts, mutable, .. }
513 // If the object contains any spread, we might be able to flatten that
514 if parts
515 .iter()
516 .any(|part| matches!(part, ObjectPart::Spread(JsValue::Object { .. })))
517 => {
518 let old_parts = take(parts);
519 for part in old_parts {
520 if let ObjectPart::Spread(JsValue::Object {
521 parts: inner_parts,
522 mutable: inner_mutable,
523 ..
524 }) = part
525 {
526 parts.extend(arena, inner_parts);
527 *mutable |= inner_mutable;
528 } else {
529 parts.push(arena, part);
530 }
531 }
532 value.update_total_nodes();
533 Modified::Yes
534 }
535 // match logical expressions like `a && b` or `a || b || c` or `a ?? b`
536 // Reduce logical expressions to their final value(s)
537 JsValue::Logical(..) => {
538 let JsValue::Logical(_, op, input_parts) = take(value) else {
539 unreachable!()
540 };
541 let len = input_parts.len();
542 let mut parts = BumpVec::<JsValue<'a>>::with_capacity_in(arena, len);
543 let mut part_properties = Vec::with_capacity(len);
544 for (i, part) in input_parts.into_iter().enumerate() {
545 // The last part is never skipped.
546 if i == len - 1 {
547 // We intentionally omit the part_properties for the last part.
548 // This isn't always needed so we only compute it when actually needed.
549 parts.push(arena, part);
550 break;
551 }
552 let property = match op {
553 LogicalOperator::And => part.is_truthy(),
554 LogicalOperator::Or => part.is_falsy(),
555 LogicalOperator::NullishCoalescing => part.is_nullish(),
556 };
557 // We might know at compile-time if a part is skipped or the final value.
558 match property {
559 Some(true) => {
560 // We known this part is skipped, so we can remove it.
561 continue;
562 }
563 Some(false) => {
564 // We known this part is the final value, so we can remove the rest.
565 part_properties.push(property);
566 parts.push(arena, part);
567 break;
568 }
569 None => {
570 // We don't know if this part is skipped or the final value, so we keep it.
571 part_properties.push(property);
572 parts.push(arena, part);
573 continue;
574 }
575 }
576 }
577 // If we reduced the expression to a single value, we can replace it.
578 if parts.len() == 1 {
579 *value = parts.pop().unwrap();
580 Modified::Yes
581 } else {
582 // If not, we know that it will be one of the remaining values.
583 let last_part = parts.last().unwrap();
584 let property = match op {
585 LogicalOperator::And => last_part.is_truthy(),
586 LogicalOperator::Or => last_part.is_falsy(),
587 LogicalOperator::NullishCoalescing => last_part.is_nullish(),
588 };
589 part_properties.push(property);
590 let (any_unset, all_set) =
591 part_properties
592 .iter()
593 .fold((false, true), |(any_unset, all_set), part| match part {
594 Some(true) => (any_unset, all_set),
595 Some(false) => (true, false),
596 None => (any_unset, false),
597 });
598 let property = match op {
599 LogicalOperator::Or => {
600 if any_unset {
601 Some(LogicalProperty::Truthy)
602 } else if all_set {
603 Some(LogicalProperty::Falsy)
604 } else {
605 None
606 }
607 }
608 LogicalOperator::And => {
609 if any_unset {
610 Some(LogicalProperty::Falsy)
611 } else if all_set {
612 Some(LogicalProperty::Truthy)
613 } else {
614 None
615 }
616 }
617 LogicalOperator::NullishCoalescing => {
618 if any_unset {
619 Some(LogicalProperty::NonNullish)
620 } else if all_set {
621 Some(LogicalProperty::Nullish)
622 } else {
623 None
624 }
625 }
626 };
627 if let Some(property) = property {
628 *value = JsValue::alternatives_with_additional_property(parts, property);
629 Modified::Yes
630 } else {
631 *value = JsValue::alternatives(parts);
632 Modified::Yes
633 }
634 }
635 }
636 JsValue::Tenary(_, test, cons, alt) => {
637 if test.is_truthy() == Some(true) {
638 *value = take(&mut **cons);
639 Modified::Yes
640 } else if test.is_falsy() == Some(true) {
641 *value = take(&mut **alt);
642 Modified::Yes
643 } else {
644 Modified::No
645 }
646 }
647 // match a binary operator like `a == b`
648 JsValue::Binary(..) => {
649 if let Some(v) = value.is_truthy() {
650 let v = if v {
651 ConstantValue::True
652 } else {
653 ConstantValue::False
654 };
655 *value = JsValue::Constant(v);
656 Modified::Yes
657 } else {
658 Modified::No
659 }
660 }
661 // match the not operator like `!a`
662 // Evaluate not when the inner value is truthy or falsy
663 JsValue::Not(_, inner) => match inner.is_truthy() {
664 Some(true) => {
665 *value = JsValue::Constant(ConstantValue::False);
666 Modified::Yes
667 }
668 Some(false) => {
669 *value = JsValue::Constant(ConstantValue::True);
670 Modified::Yes
671 }
672 None => Modified::No,
673 },
674
675 JsValue::Iterated(_, iterable) => {
676 if let JsValue::Array { items, mutable, .. } = &mut **iterable {
677 let mut new_value = JsValue::alternatives(take(items));
678 if *mutable {
679 new_value.add_unknown_mutations(arena, true);
680 }
681 *value = new_value;
682 Modified::Yes
683 } else {
684 Modified::No
685 }
686 }
687
688 JsValue::Awaited(_, operand) => {
689 if let JsValue::Promise(_, inner) = &mut **operand {
690 *value = take(&mut **inner);
691 Modified::Yes
692 } else {
693 *value = take(&mut **operand);
694 Modified::Yes
695 }
696 }
697
698 _ => Modified::No,
699 }
700}