Skip to main content

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}