swc_ecma_transforms_optimization/simplify/inlining/
scope.rs

1#![allow(dead_code)]
2
3use std::{
4    borrow::Cow,
5    cell::{Cell, RefCell},
6    collections::VecDeque,
7};
8
9use indexmap::map::{Entry, IndexMap};
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11use swc_ecma_ast::*;
12use swc_ecma_transforms_base::ext::ExprRefExt;
13use tracing::{span, Level};
14
15use super::{Inlining, Phase};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ScopeKind {
19    /// If / Switch
20    Cond,
21    Loop,
22    Block,
23    Fn {
24        named: bool,
25    },
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(super) enum VarType {
30    Param,
31    Var(VarDeclKind),
32}
33
34impl Default for ScopeKind {
35    fn default() -> Self {
36        Self::Fn { named: false }
37    }
38}
39
40impl Inlining<'_> {
41    pub(super) fn with_child<F>(&mut self, kind: ScopeKind, op: F)
42    where
43        F: for<'any> FnOnce(&mut Inlining<'any>),
44    {
45        let (unresolved_usages, bindings) = {
46            let mut child = Inlining {
47                phase: self.phase,
48                is_first_run: self.is_first_run,
49                changed: false,
50                scope: Scope::new(Some(&self.scope), kind),
51                var_decl_kind: VarDeclKind::Var,
52                ident_type: self.ident_type,
53                pat_mode: self.pat_mode,
54                in_test: self.in_test,
55                pass: self.pass,
56            };
57
58            op(&mut child);
59
60            self.changed |= child.changed;
61
62            (child.scope.unresolved_usages, child.scope.bindings)
63        };
64
65        tracing::trace!("propagating variables");
66
67        self.scope.unresolved_usages.extend(unresolved_usages);
68
69        if !matches!(kind, ScopeKind::Fn { .. }) {
70            let v = bindings;
71
72            for (id, v) in v.into_iter().filter_map(|(id, v)| {
73                if v.kind == VarType::Var(VarDeclKind::Var) {
74                    Some((id, v))
75                } else {
76                    None
77                }
78            }) {
79                let v: VarInfo = v;
80
81                tracing::trace!("Hoisting a variable {:?}", id);
82
83                if self.scope.unresolved_usages.contains(&id) {
84                    v.inline_prevented.set(true)
85                }
86
87                v.hoisted.set(true);
88
89                *v.value.borrow_mut() = None;
90                v.is_undefined.set(false);
91                self.scope.bindings.insert(id, v);
92            }
93        }
94    }
95
96    /// Note: this method stores the value only if init is [Cow::Owned] or it's
97    /// [Expr::Ident] or [Expr::Lit].
98    pub(super) fn declare(
99        &mut self,
100        id: Id,
101        init: Option<Cow<Expr>>,
102        is_change: bool,
103        kind: VarType,
104    ) {
105        tracing::trace!(
106            "({}, {:?}) declare({})",
107            self.scope.depth(),
108            self.phase,
109            id.0
110        );
111
112        let init = init.map(|cow| match cow {
113            Cow::Owned(v) => Cow::Owned(v),
114            Cow::Borrowed(b) => match b {
115                Expr::Ident(..) | Expr::Lit(..) => Cow::Owned(b.clone()),
116                _ => Cow::Borrowed(b),
117            },
118        });
119
120        let is_undefined = self.var_decl_kind == VarDeclKind::Var
121            && !is_change
122            && init.is_none()
123            && self.phase == Phase::Inlining;
124
125        let mut alias_of = None;
126
127        let value_idx = match init.as_deref() {
128            Some(Expr::Ident(vi)) => {
129                if let Some((value_idx, value_var)) = self.scope.idx_val(&vi.to_id()) {
130                    alias_of = Some(value_var.kind);
131                    Some((value_idx, vi.to_id()))
132                } else {
133                    None
134                }
135            }
136            _ => None,
137        };
138
139        let is_inline_prevented = self.scope.should_prevent_inline_because_of_scope(&id)
140            || match init {
141                Some(ref e) => self.scope.is_inline_prevented(e),
142                _ => false,
143            };
144
145        if is_inline_prevented {
146            tracing::trace!("\tdeclare: Inline prevented: {:?}", id)
147        }
148        if is_undefined {
149            tracing::trace!("\tdeclare: {:?} is undefined", id);
150        }
151
152        let idx = match self.scope.bindings.entry(id.clone()) {
153            Entry::Occupied(mut e) => {
154                e.get().is_undefined.set(is_undefined);
155                e.get().read_cnt.set(0);
156                e.get().read_from_nested_scope.set(false);
157
158                match init {
159                    Some(Cow::Owned(v)) => e.get_mut().value = RefCell::new(Some(v)),
160                    None => e.get_mut().value = RefCell::new(None),
161                    _ => {}
162                }
163
164                e.get().inline_prevented.set(is_inline_prevented);
165                e.index()
166            }
167            Entry::Vacant(e) => {
168                let idx = e.index();
169                e.insert(VarInfo {
170                    kind,
171                    alias_of,
172                    read_from_nested_scope: Cell::new(false),
173                    read_cnt: Cell::new(0),
174                    inline_prevented: Cell::new(is_inline_prevented),
175                    is_undefined: Cell::new(is_undefined),
176                    value: RefCell::new(match init {
177                        Some(Cow::Owned(v)) => Some(v),
178                        _ => None,
179                    }),
180                    this_sensitive: Cell::new(false),
181                    hoisted: Cell::new(false),
182                });
183                idx
184            }
185        };
186
187        {
188            let mut cur = Some(&self.scope);
189            while let Some(scope) = cur {
190                if let ScopeKind::Fn { .. } = scope.kind {
191                    break;
192                }
193
194                if scope.kind == ScopeKind::Loop {
195                    tracing::trace!("preventing inline as it's declared in a loop");
196                    self.scope.prevent_inline(&id);
197                    break;
198                }
199
200                cur = scope.parent;
201            }
202        }
203
204        //
205        let barrier_works = match kind {
206            VarType::Param => false,
207            _ if alias_of == Some(VarType::Param) => false,
208            _ => true,
209        };
210
211        if barrier_works {
212            if let Some((value_idx, vi)) = value_idx {
213                tracing::trace!("\tdeclare: {} -> {}", idx, value_idx);
214
215                let barrier_exists = (|| {
216                    for &blocker in self.scope.inline_barriers.borrow().iter() {
217                        if (value_idx <= blocker && blocker <= idx)
218                            || (idx <= blocker && blocker <= value_idx)
219                        {
220                            return true;
221                        }
222                    }
223
224                    false
225                })();
226
227                if value_idx > idx || barrier_exists {
228                    tracing::trace!("Variable use before declaration: {:?}", id);
229                    self.scope.prevent_inline(&id);
230                    self.scope.prevent_inline(&vi)
231                }
232            } else {
233                tracing::trace!("\tdeclare: value idx is none");
234            }
235        }
236    }
237}
238
239#[derive(Debug, Default)]
240pub(super) struct Scope<'a> {
241    pub parent: Option<&'a Scope<'a>>,
242    pub kind: ScopeKind,
243
244    inline_barriers: RefCell<VecDeque<usize>>,
245    bindings: IndexMap<Id, VarInfo, FxBuildHasher>,
246    unresolved_usages: FxHashSet<Id>,
247
248    /// Simple optimization. We don't need complex scope analysis.
249    pub constants: FxHashMap<Id, Option<Expr>>,
250}
251
252impl<'a> Scope<'a> {
253    pub fn new(parent: Option<&'a Scope<'a>>, kind: ScopeKind) -> Self {
254        Self {
255            parent,
256            kind,
257            ..Default::default()
258        }
259    }
260
261    pub fn depth(&self) -> usize {
262        match self.parent {
263            None => 0,
264            Some(p) => p.depth() + 1,
265        }
266    }
267
268    fn should_prevent_inline_because_of_scope(&self, id: &Id) -> bool {
269        if self.unresolved_usages.contains(id) {
270            return true;
271        }
272
273        match self.parent {
274            None => false,
275            Some(p) => {
276                if p.should_prevent_inline_because_of_scope(id) {
277                    return true;
278                }
279
280                if let Some(v) = p.find_binding(id) {
281                    if v.hoisted.get() && v.is_inline_prevented() {
282                        return true;
283                    }
284                }
285
286                false
287            }
288        }
289    }
290
291    /// True if the returned scope is self
292    fn scope_for(&self, id: &Id) -> (&Scope, bool) {
293        if self.constants.contains_key(id) {
294            return (self, true);
295        }
296        if self.find_binding_from_current(id).is_some() {
297            return (self, true);
298        }
299
300        match self.parent {
301            None => (self, true),
302            Some(p) => {
303                let (s, _) = p.scope_for(id);
304                (s, false)
305            }
306        }
307    }
308
309    pub fn read_cnt(&self, id: &Id) -> Option<usize> {
310        if let Some(var) = self.find_binding(id) {
311            return Some(var.read_cnt.get());
312        }
313
314        None
315    }
316
317    fn read_prevents_inlining(&self, id: &Id) -> bool {
318        tracing::trace!("read_prevents_inlining({:?})", id);
319
320        if let Some(v) = self.find_binding(id) {
321            match v.kind {
322                // Reading parameter is ok.
323                VarType::Param => return false,
324                VarType::Var(VarDeclKind::Let) | VarType::Var(VarDeclKind::Const) => return false,
325                _ => {}
326            }
327
328            // If it's already hoisted, it means that it is already processed by child
329            // scope.
330            if v.hoisted.get() {
331                return false;
332            }
333        }
334
335        {
336            let mut cur = Some(self);
337
338            while let Some(scope) = cur {
339                let found = scope.find_binding_from_current(id).is_some();
340
341                if found {
342                    tracing::trace!("found");
343                    break;
344                }
345                tracing::trace!("({}): {}: kind = {:?}", scope.depth(), id.0, scope.kind);
346
347                match scope.kind {
348                    ScopeKind::Fn { .. } => {
349                        tracing::trace!(
350                            "{}: variable access from a nested function detected",
351                            id.0
352                        );
353                        return true;
354                    }
355                    ScopeKind::Loop | ScopeKind::Cond => {
356                        return true;
357                    }
358                    _ => {}
359                }
360                cur = scope.parent;
361            }
362        }
363
364        false
365    }
366
367    pub fn add_read(&mut self, id: &Id) {
368        if self.read_prevents_inlining(id) {
369            tracing::trace!("prevent inlining because of read: {}", id.0);
370
371            self.prevent_inline(id)
372        }
373
374        if id.0 == "arguments" {
375            self.prevent_inline_of_params();
376        }
377
378        if let Some(var_info) = self.find_binding(id) {
379            var_info.read_cnt.set(var_info.read_cnt.get() + 1);
380            if var_info.hoisted.get() {
381                var_info.inline_prevented.set(true);
382            }
383        } else {
384            tracing::trace!("({}): Unresolved usage.: {:?}", self.depth(), id);
385            self.unresolved_usages.insert(id.clone());
386        }
387
388        let (scope, is_self) = self.scope_for(id);
389        if !is_self {
390            if let Some(var_info) = scope.find_binding_from_current(id) {
391                var_info.read_from_nested_scope.set(true);
392            }
393        }
394    }
395
396    fn write_prevents_inline(&self, id: &Id) -> bool {
397        tracing::trace!("write_prevents_inline({})", id.0);
398
399        {
400            let mut cur = Some(self);
401
402            while let Some(scope) = cur {
403                let found = scope.find_binding_from_current(id).is_some();
404
405                if found {
406                    break;
407                }
408                tracing::trace!("({}): {}: kind = {:?}", scope.depth(), id.0, scope.kind);
409
410                match scope.kind {
411                    ScopeKind::Fn { .. } => {
412                        tracing::trace!(
413                            "{}: variable access from a nested function detected",
414                            id.0
415                        );
416                        return true;
417                    }
418                    ScopeKind::Loop | ScopeKind::Cond => {
419                        return true;
420                    }
421                    _ => {}
422                }
423                cur = scope.parent;
424            }
425        }
426
427        false
428    }
429
430    pub fn add_write(&mut self, id: &Id, force_no_inline: bool) {
431        let _tracing = span!(
432            Level::DEBUG,
433            "add_write",
434            force_no_inline = force_no_inline,
435            id = &*format!("{id:?}")
436        )
437        .entered();
438
439        if self.write_prevents_inline(id) {
440            tracing::trace!("prevent inlining because of write: {}", id.0);
441
442            self.prevent_inline(id)
443        }
444
445        if id.0 == "arguments" {
446            self.prevent_inline_of_params();
447        }
448
449        let (scope, is_self) = self.scope_for(id);
450
451        if let Some(var_info) = scope.find_binding_from_current(id) {
452            var_info.is_undefined.set(false);
453
454            if !is_self || force_no_inline {
455                self.prevent_inline(id)
456            }
457        } else if self.has_constant(id) {
458            // noop
459        } else {
460            tracing::trace!(
461                "({}): Unresolved. (scope = ({})): {:?}",
462                self.depth(),
463                scope.depth(),
464                id
465            );
466            self.bindings.insert(
467                id.clone(),
468                VarInfo {
469                    kind: VarType::Var(VarDeclKind::Var),
470                    alias_of: None,
471                    read_from_nested_scope: Cell::new(false),
472                    read_cnt: Cell::new(0),
473                    inline_prevented: Cell::new(force_no_inline),
474                    value: RefCell::new(None),
475                    is_undefined: Cell::new(false),
476                    this_sensitive: Cell::new(false),
477                    hoisted: Cell::new(false),
478                },
479            );
480        }
481    }
482
483    pub fn find_binding(&self, id: &Id) -> Option<&VarInfo> {
484        if let Some(e) = self.find_binding_from_current(id) {
485            return Some(e);
486        }
487
488        self.parent.and_then(|parent| parent.find_binding(id))
489    }
490
491    /// Searches only for current scope.
492    fn idx_val(&self, id: &Id) -> Option<(usize, &VarInfo)> {
493        self.bindings.iter().enumerate().find_map(
494            |(idx, (k, v))| {
495                if k == id {
496                    Some((idx, v))
497                } else {
498                    None
499                }
500            },
501        )
502    }
503
504    pub fn find_binding_from_current(&self, id: &Id) -> Option<&VarInfo> {
505        let (_, v) = self.idx_val(id)?;
506
507        Some(v)
508    }
509
510    fn has_constant(&self, id: &Id) -> bool {
511        if self.constants.contains_key(id) {
512            return true;
513        }
514
515        self.parent
516            .map(|parent| parent.has_constant(id))
517            .unwrap_or(false)
518    }
519
520    pub fn find_constant(&self, id: &Id) -> Option<&Expr> {
521        if let Some(Some(e)) = self.constants.get(id) {
522            return Some(e);
523        }
524
525        self.parent.and_then(|parent| parent.find_constant(id))
526    }
527
528    pub fn mark_this_sensitive(&self, callee: &Expr) {
529        if let Expr::Ident(ref i) = callee {
530            if let Some(v) = self.find_binding(&i.to_id()) {
531                v.this_sensitive.set(true);
532            }
533        }
534    }
535
536    pub fn store_inline_barrier(&self, phase: Phase) {
537        tracing::trace!("store_inline_barrier({:?})", phase);
538
539        match phase {
540            Phase::Analysis => {
541                let idx = self.bindings.len();
542                self.inline_barriers.borrow_mut().push_back(idx);
543            }
544            Phase::Inlining => {
545                //if let Some(idx) =
546                // self.inline_barriers.borrow_mut().pop_front() {
547                //    for i in 0..idx {
548                //        if let Some((id, _)) = self.bindings.get_index(i) {
549                //            self.prevent_inline(id);
550                //        }
551                //    }
552                //}
553            }
554        }
555
556        match self.parent {
557            None => {}
558            Some(p) => p.store_inline_barrier(phase),
559        }
560    }
561
562    fn prevent_inline_of_params(&self) {
563        for (_, v) in self.bindings.iter() {
564            let v: &VarInfo = v;
565
566            if v.is_param() {
567                v.inline_prevented.set(true);
568            }
569        }
570
571        if let ScopeKind::Fn { .. } = self.kind {
572            return;
573        }
574
575        if let Some(p) = self.parent {
576            p.prevent_inline_of_params()
577        }
578    }
579
580    pub fn prevent_inline(&self, id: &Id) {
581        tracing::trace!("({}) Prevent inlining: {:?}", self.depth(), id);
582
583        if let Some(v) = self.find_binding_from_current(id) {
584            v.inline_prevented.set(true);
585        }
586
587        for (_, v) in self.bindings.iter() {
588            if let Some(Expr::Ident(i)) = v.value.borrow().as_ref() {
589                if i.sym == id.0 && i.ctxt == id.1 {
590                    v.inline_prevented.set(true);
591                }
592            }
593        }
594
595        match self.parent {
596            None => {}
597            Some(p) => p.prevent_inline(id),
598        }
599    }
600
601    pub fn is_inline_prevented(&self, e: &Expr) -> bool {
602        match e {
603            Expr::Ident(ref ri) => {
604                if let Some(v) = self.find_binding_from_current(&ri.to_id()) {
605                    return v.inline_prevented.get();
606                }
607            }
608            Expr::Member(MemberExpr {
609                obj: right_expr, ..
610            }) if right_expr.is_ident() => {
611                let ri = right_expr.as_ident().unwrap();
612
613                if let Some(v) = self.find_binding_from_current(&ri.to_id()) {
614                    return v.inline_prevented.get();
615                }
616            }
617            Expr::Update(..) => return true,
618
619            // TODO: Remove this
620            Expr::Paren(..) | Expr::Call(..) | Expr::New(..) => return true,
621
622            _ => {}
623        }
624
625        match self.parent {
626            None => false,
627            Some(p) => p.is_inline_prevented(e),
628        }
629    }
630
631    pub fn has_same_this(&self, id: &Id, init: Option<&Expr>) -> bool {
632        if let Some(v) = self.find_binding(id) {
633            if v.this_sensitive.get() {
634                if let Some(&Expr::Member(..)) = init {
635                    return false;
636                }
637            }
638        }
639
640        true
641    }
642}
643
644#[derive(Debug)]
645pub(super) struct VarInfo {
646    pub kind: VarType,
647
648    alias_of: Option<VarType>,
649
650    read_from_nested_scope: Cell<bool>,
651    read_cnt: Cell<usize>,
652
653    inline_prevented: Cell<bool>,
654    this_sensitive: Cell<bool>,
655
656    pub value: RefCell<Option<Expr>>,
657    pub is_undefined: Cell<bool>,
658
659    hoisted: Cell<bool>,
660}
661
662impl VarInfo {
663    pub fn is_param(&self) -> bool {
664        self.kind == VarType::Param
665    }
666
667    pub fn is_inline_prevented(&self) -> bool {
668        if self.inline_prevented.get() {
669            return true;
670        }
671
672        if self.this_sensitive.get() {
673            if let Some(Expr::Member(..)) = *self.value.borrow() {
674                return true;
675            }
676        }
677
678        false
679    }
680}