Skip to main content

nodejs/
capture.rs

1//! Does a piece of a program hold on to the scope it runs in?
2//!
3//! Two lowerings in `compiler.rs` exist only to serve code that captures an
4//! environment, and both cost a heap allocation every time control passes them:
5//!
6//! * a `for (let i = …)` head is re-bound per iteration (ForBodyEvaluation's
7//!   CreatePerIterationEnvironment), so a closure made in one pass keeps that
8//!   pass's value — `COPY_SCOPE` clones the whole scope on every iteration;
9//! * a `{ … }` block opens a scope for its lexical declarations.
10//!
11//! When nothing in the subtree can make a closure, the per-iteration copy is
12//! unobservable: no one can ever hold a reference to the iteration's bindings,
13//! so one binding mutated in place gives the same answers. A profile of
14//! `for (let i = 0; i < 5_000_000; i++) s += i % 7;` spent 17% of its samples in
15//! `copy_scope` and the `EnvData` allocate/free traffic under it, for copies
16//! that nothing could observe.
17//!
18//! This module answers the question conservatively: it says "captures" for
19//! anything that makes a function, a class (its methods are functions), or a
20//! direct `eval` (which can both make closures and declare into the caller's
21//! scope). Every match here is exhaustive — a new AST node has to be classified
22//! deliberately rather than defaulting into the fast path.
23
24use crate::ast::{Expr, Prop, Stmt, StmtKind, SwitchCase};
25
26/// True if evaluating `s` can create something that outlives it holding the
27/// current scope.
28pub fn stmt_captures(s: &Stmt) -> bool {
29    match &s.kind {
30        // A function or class body is exactly the thing that captures.
31        StmtKind::FuncDecl { .. } | StmtKind::ClassDecl(_) => true,
32
33        StmtKind::Expr(e) | StmtKind::Throw(e) => expr_captures(e),
34        StmtKind::Return(e) => e.as_ref().is_some_and(expr_captures),
35        StmtKind::Decl { decls, .. } => decls
36            .iter()
37            .any(|d| expr_captures(&d.target) || d.init.as_ref().is_some_and(expr_captures)),
38        StmtKind::Block(body) => body.iter().any(stmt_captures),
39        StmtKind::If { test, cons, alt } => {
40            expr_captures(test) || stmt_captures(cons) || alt.as_deref().is_some_and(stmt_captures)
41        }
42        StmtKind::While { test, body } | StmtKind::DoWhile { body, test } => {
43            expr_captures(test) || stmt_captures(body)
44        }
45        StmtKind::For {
46            init,
47            test,
48            update,
49            body,
50        } => {
51            init.as_deref().is_some_and(stmt_captures)
52                || test.as_ref().is_some_and(expr_captures)
53                || update.as_ref().is_some_and(expr_captures)
54                || stmt_captures(body)
55        }
56        StmtKind::ForOf {
57            target, iter, body, ..
58        } => expr_captures(target) || expr_captures(iter) || stmt_captures(body),
59        StmtKind::ForIn {
60            target,
61            object,
62            body,
63            ..
64        } => expr_captures(target) || expr_captures(object) || stmt_captures(body),
65        StmtKind::Switch { disc, cases } => expr_captures(disc) || cases.iter().any(case_captures),
66        StmtKind::Labeled { body, .. } => stmt_captures(body),
67        StmtKind::Try {
68            block,
69            handler,
70            finalizer,
71        } => {
72            block.iter().any(stmt_captures)
73                || handler.as_ref().is_some_and(|(param, body)| {
74                    param.as_ref().is_some_and(expr_captures) || body.iter().any(stmt_captures)
75                })
76                || finalizer
77                    .as_ref()
78                    .is_some_and(|body| body.iter().any(stmt_captures))
79        }
80        StmtKind::Break(_) | StmtKind::Continue(_) | StmtKind::Empty => false,
81    }
82}
83
84fn case_captures(c: &SwitchCase) -> bool {
85    c.test.as_ref().is_some_and(expr_captures) || c.body.iter().any(stmt_captures)
86}
87
88/// True if evaluating `e` can create something that outlives it holding the
89/// current scope.
90pub fn expr_captures(e: &Expr) -> bool {
91    match e {
92        // The three that capture.
93        Expr::Function { .. } | Expr::Class(_) => true,
94        // Direct `eval` sees — and can close over, or declare into — the scope
95        // it is called from, so a scope it can reach must stay a real scope.
96        // Any other use of the name (`const f = eval;`) is caught here too:
97        // the check is on the identifier, not on the call shape.
98        Expr::Ident(n) => n == "eval",
99
100        Expr::Null
101        | Expr::Undefined
102        | Expr::Hole
103        | Expr::True
104        | Expr::False
105        | Expr::Number(_)
106        | Expr::BigInt(_)
107        | Expr::Regex(_, _)
108        | Expr::Str(_)
109        | Expr::This
110        | Expr::Super
111        | Expr::NewTarget => false,
112
113        Expr::Template { exprs, .. } => exprs.iter().any(expr_captures),
114        Expr::TaggedTemplate { tag, exprs, .. } => {
115            expr_captures(tag) || exprs.iter().any(expr_captures)
116        }
117        Expr::Yield { arg, .. } => arg.as_deref().is_some_and(expr_captures),
118        Expr::Await(inner) | Expr::Spread(inner) | Expr::Unary(_, inner) => expr_captures(inner),
119        Expr::Array(items) | Expr::Sequence(items) => items.iter().any(expr_captures),
120        Expr::Object(props) => props.iter().any(prop_captures),
121        Expr::Logical(_, l, r) | Expr::Binary(_, l, r) => expr_captures(l) || expr_captures(r),
122        Expr::Conditional { test, cons, alt } => {
123            expr_captures(test) || expr_captures(cons) || expr_captures(alt)
124        }
125        Expr::Assign { target, value, .. } => expr_captures(target) || expr_captures(value),
126        Expr::Update { target, .. } => expr_captures(target),
127        Expr::Call { func, args, .. } | Expr::New { callee: func, args } => {
128            expr_captures(func) || args.iter().any(expr_captures)
129        }
130        Expr::Member { object, .. } => expr_captures(object),
131        Expr::Index { object, index, .. } => expr_captures(object) || expr_captures(index),
132    }
133}
134
135fn prop_captures(p: &Prop) -> bool {
136    match p {
137        Prop::KeyValue { key, value, .. } => expr_captures(key) || expr_captures(value),
138        Prop::Spread(e) => expr_captures(e),
139        // An accessor's `func` is an `Expr::Function`, so this arm is `true`;
140        // it is spelled out rather than assumed.
141        Prop::Accessor { key, func, .. } => expr_captures(key) || expr_captures(func),
142    }
143}
144
145/// Does this statement list declare anything that needs a block scope of its
146/// own? `let` / `const` / `class` / a hoisted `function` bind into the block;
147/// `var` does not (it lands in the enclosing function's base env). A block that
148/// binds nothing needs no scope at all, so `{ …; }` inside a hot loop stops
149/// allocating and freeing an `EnvData` per pass.
150///
151/// Direct `eval` forces a scope: `eval("let x = 1")` declares into the running
152/// block, and with no block open that binding would escape to the function.
153pub fn block_needs_scope(body: &[Stmt]) -> bool {
154    body.iter().any(|s| match &s.kind {
155        StmtKind::Decl { kind, .. } => !matches!(kind, crate::ast::DeclKind::Var),
156        StmtKind::FuncDecl { .. } | StmtKind::ClassDecl(_) => true,
157        // Only a DIRECT `eval` in this block's own statements can declare into
158        // it; a nested block or function has its own scope to declare into.
159        StmtKind::Expr(e) => mentions_eval(e),
160        _ => false,
161    })
162}
163
164/// `eval` named anywhere in this expression (see [`block_needs_scope`]).
165fn mentions_eval(e: &Expr) -> bool {
166    match e {
167        Expr::Ident(n) => n == "eval",
168        Expr::Call { func, args, .. } | Expr::New { callee: func, args } => {
169            mentions_eval(func) || args.iter().any(mentions_eval)
170        }
171        Expr::Assign { target, value, .. } => mentions_eval(target) || mentions_eval(value),
172        Expr::Sequence(items) => items.iter().any(mentions_eval),
173        Expr::Logical(_, l, r) | Expr::Binary(_, l, r) => mentions_eval(l) || mentions_eval(r),
174        Expr::Conditional { test, cons, alt } => {
175            mentions_eval(test) || mentions_eval(cons) || mentions_eval(alt)
176        }
177        Expr::Unary(_, inner) | Expr::Await(inner) | Expr::Spread(inner) => mentions_eval(inner),
178        _ => false,
179    }
180}