Skip to main content

nodejs/
slots.rs

1//! Which locals can live in fusevm frame slots instead of the host's scope
2//! chain.
3//!
4//! Every identifier in a node-js program is a name lookup: the compiler emits
5//! `CallBuiltin(GETLOCAL)` with the name as a string constant, and the host pops
6//! it off the VM stack, borrows the thread-local `JsHost`, walks the
7//! `Rc<RefCell<EnvData>>` chain and hashes the name in each scope. An empty
8//! `for (let i = 0; i < 5_000_000; i++) {}` costs four of those round-trips per
9//! iteration — 178 ns against node's 6 ns — and none of them is JIT-able, since
10//! fusevm's block tier declines any region containing a `CallBuiltin`.
11//!
12//! A local that no closure can reach does not need a scope entry at all: it can
13//! live in the frame slot vector fusevm already keeps, addressed by index
14//! (`Op::GetSlot` / `Op::SetSlot`). This module decides which names qualify.
15//! The shape of the analysis follows pythonrs's `fn_slots_allowed` /
16//! `fn_slots`, which does the same job against the same VM.
17//!
18//! The rules are deliberately conservative, because a name that is slotted in
19//! one place and looked up by name in another is a silent wrong answer:
20//!
21//! 1. **A name reachable from another chunk keeps its binding.** A nested
22//!    function, arrow or class body compiles to its own chunk and resolves what
23//!    it captures through the environment chain; a `try` block is likewise its
24//!    own chunk on its own VM frame, so a slot written outside it is invisible
25//!    inside. Every identifier mentioned in either is therefore off the table —
26//!    but only those identifiers, not the whole chunk, so a loop counter still
27//!    gets a slot in a file that also defines a callback. A direct `eval` can
28//!    name anything, so it disables the chunk outright.
29//! 2. **One declaration per name.** Shadowing (`let x` in two sibling blocks) is
30//!    exactly where a flat name→slot table would be wrong, so a name declared
31//!    more than once in the chunk is left alone rather than scope-tracked.
32//! 3. **No read before the declaration, in source order.** This is what keeps
33//!    the temporal dead zone and `var` hoisting behaving as they do today: a
34//!    slot reads as `undefined` before its first write, which is neither node's
35//!    `ReferenceError` for a `let` nor what node-js currently answers. A name
36//!    whose first mention is a read stays on the name path, where those answers
37//!    come from. Source order is a conservative stand-in for execution order —
38//!    a loop can re-enter a block, but it cannot reach a statement the source
39//!    has not introduced yet.
40//! 4. **Simple identifiers only.** Destructuring targets, `delete x`, and
41//!    anything that is not an `Expr::Ident` bind through the host.
42//! 5. **At the top level of a script, `let`/`const` only.** A top-level `var` is
43//!    a property of the global object (`var g = 1; globalThis.g` is `1` on node
44//!    v26.7.0), so it has to stay a real binding.
45
46use crate::ast::{DeclKind, Expr, Param, Prop, Stmt, StmtKind, SwitchCase};
47use rustc_hash::{FxHashMap, FxHashSet};
48
49/// Name → frame slot for one chunk. Empty when the chunk is not eligible.
50pub type SlotTable = FxHashMap<String, u16>;
51
52/// What the compiler needs to know about a chunk's locals.
53#[derive(Default)]
54pub struct Plan {
55    /// The slotted names and their frame indices.
56    pub table: SlotTable,
57    /// Of those, the ones provably holding a Number, so `++`/`--` can be a
58    /// native add instead of a `NUM_STEP` call into the host.
59    pub numeric: FxHashSet<String>,
60    /// Of those, the ones declared `const`. A slotted binding never reaches the
61    /// host's scope chain, so the host's immutable-binding check cannot see it;
62    /// the compiler rejects the assignment instead, which costs nothing at run
63    /// time and is exact (see `Planner::consts`).
64    pub consts: FxHashSet<String>,
65}
66
67/// Is this initializer a literal Number? `-1` reaches the compiler as a unary
68/// negation of `1`, so it counts too.
69fn is_number_literal(e: &Expr) -> bool {
70    match e {
71        Expr::Number(_) => true,
72        Expr::Unary(crate::ast::UnOp::Neg, inner) => matches!(**inner, Expr::Number(_)),
73        _ => false,
74    }
75}
76
77/// Plan the slots for a chunk: `params` are bound into the environment by the
78/// caller before the chunk runs (the compiler emits a prologue that copies each
79/// into its slot), `body` is the statement list about to be compiled, and
80/// `top_level` marks a script/module body, where `var` stays a global.
81pub fn plan(params: &[Param], body: &[Stmt], top_level: bool) -> Plan {
82    if !chunk_is_eligible(body) {
83        return Plan::default();
84    }
85    // Anything a nested chunk can name resolves through the environment, so it
86    // cannot move into this frame's slots.
87    let mut escaping = FxHashSet::default();
88    for s in body {
89        collect_escaping_stmt(s, &mut escaping);
90    }
91    let mut p = Planner {
92        candidates: SlotTable::default(),
93        rejected: escaping,
94        numeric: FxHashSet::default(),
95        consts: FxHashSet::default(),
96        top_level,
97        next: 0,
98    };
99    // Parameters are declared and assigned before the first statement runs.
100    // Their incoming type is whatever the caller passed, so they are never in
101    // the numeric set.
102    for name in param_names(params) {
103        p.declare(&name);
104    }
105    for s in body {
106        p.walk_stmt(s);
107    }
108    for name in p.rejected {
109        p.candidates.remove(&name);
110        p.numeric.remove(&name);
111        p.consts.remove(&name);
112    }
113    Plan {
114        table: p.candidates,
115        numeric: p.numeric,
116        consts: p.consts,
117    }
118}
119
120/// The simple identifier parameters, in order. A destructuring or rest pattern
121/// is bound by the body prologue, so it is not seeded here.
122pub fn param_names(params: &[Param]) -> Vec<String> {
123    params
124        .iter()
125        .filter(|p| !p.rest)
126        .filter_map(|p| match &p.pattern {
127            Expr::Ident(n) => Some(n.clone()),
128            _ => None,
129        })
130        .collect()
131}
132
133/// Is this chunk's frame stable enough to hold slots at all? Two things say no
134/// for the whole chunk rather than for one name: a direct `eval`, which can read
135/// or write any binding by a name only known at run time, and a `yield`/`await`
136/// at this chunk's own level, which suspends the frame the slots live in.
137/// (Nested function bodies are their own chunks; what they contain is their
138/// business, and what they NAME is handled per-name by `collect_escaping_*`.)
139fn chunk_is_eligible(body: &[Stmt]) -> bool {
140    !mentions_eval_stmts(body) && body.iter().all(stmt_slot_safe)
141}
142
143/// `eval` named anywhere, at any depth — including inside a nested function,
144/// which can be handed this frame's environment.
145fn mentions_eval_stmts(body: &[Stmt]) -> bool {
146    let mut names = FxHashSet::default();
147    for s in body {
148        collect_all_idents_stmt(s, &mut names);
149    }
150    names.contains("eval")
151}
152
153/// A `try` runs as its own chunk on its own frame; `yield`/`await` suspend the
154/// frame; `delete x` removes a binding a slot does not have.
155fn stmt_slot_safe(s: &Stmt) -> bool {
156    match &s.kind {
157        // A `try` compiles to sub-chunks; the names they touch are handled by
158        // `collect_escaping_stmt`, so the statement itself is no obstacle.
159        StmtKind::Try {
160            block,
161            handler,
162            finalizer,
163        } => {
164            block.iter().all(stmt_slot_safe)
165                && handler
166                    .as_ref()
167                    .map_or(true, |(_, b)| b.iter().all(stmt_slot_safe))
168                && finalizer
169                    .as_ref()
170                    .map_or(true, |b| b.iter().all(stmt_slot_safe))
171        }
172        StmtKind::Expr(e) | StmtKind::Throw(e) => expr_slot_safe(e),
173        StmtKind::Return(e) => e.as_ref().map_or(true, expr_slot_safe),
174        StmtKind::Decl { decls, .. } => decls
175            .iter()
176            .all(|d| d.init.as_ref().map_or(true, expr_slot_safe)),
177        StmtKind::Block(body) => body.iter().all(stmt_slot_safe),
178        StmtKind::If { test, cons, alt } => {
179            expr_slot_safe(test)
180                && stmt_slot_safe(cons)
181                && alt.as_deref().map_or(true, stmt_slot_safe)
182        }
183        StmtKind::While { test, body } | StmtKind::DoWhile { body, test } => {
184            expr_slot_safe(test) && stmt_slot_safe(body)
185        }
186        StmtKind::For {
187            init,
188            test,
189            update,
190            body,
191        } => {
192            init.as_deref().map_or(true, stmt_slot_safe)
193                && test.as_ref().map_or(true, expr_slot_safe)
194                && update.as_ref().map_or(true, expr_slot_safe)
195                && stmt_slot_safe(body)
196        }
197        StmtKind::ForOf {
198            target,
199            iter,
200            body,
201            is_await,
202            ..
203        } => !*is_await && expr_slot_safe(target) && expr_slot_safe(iter) && stmt_slot_safe(body),
204        StmtKind::ForIn {
205            target,
206            object,
207            body,
208            ..
209        } => expr_slot_safe(target) && expr_slot_safe(object) && stmt_slot_safe(body),
210        StmtKind::Switch { disc, cases } => {
211            expr_slot_safe(disc)
212                && cases.iter().all(|c: &SwitchCase| {
213                    c.test.as_ref().map_or(true, expr_slot_safe)
214                        && c.body.iter().all(stmt_slot_safe)
215                })
216        }
217        StmtKind::Labeled { body, .. } => stmt_slot_safe(body),
218        StmtKind::Break(_) | StmtKind::Continue(_) | StmtKind::Empty => true,
219        // A nested function or class body is its own chunk with its own frame:
220        // nothing in it can unsettle this one. What it NAMES is refused per
221        // name by `collect_escaping_stmt`.
222        StmtKind::FuncDecl { .. } | StmtKind::ClassDecl(_) => true,
223    }
224}
225
226fn expr_slot_safe(e: &Expr) -> bool {
227    let all = |xs: &[Expr]| xs.iter().all(expr_slot_safe);
228    match e {
229        Expr::Yield { .. } | Expr::Await(_) => false,
230        // `delete x` is a binding operation a slot cannot express — the NAME is
231        // refused in the walk, the statement holding it is fine.
232        Expr::Unary(_, inner) | Expr::Spread(inner) => expr_slot_safe(inner),
233        Expr::Template { exprs, .. } => all(exprs),
234        Expr::TaggedTemplate { tag, exprs, .. } => expr_slot_safe(tag) && all(exprs),
235        Expr::Array(items) | Expr::Sequence(items) => all(items),
236        Expr::Object(props) => props.iter().all(|p| match p {
237            Prop::KeyValue { key, value, .. } => expr_slot_safe(key) && expr_slot_safe(value),
238            Prop::Spread(x) => expr_slot_safe(x),
239            // An accessor is a nested function: its names escape, it is not a
240            // reason to give up on the chunk.
241            Prop::Accessor { .. } => true,
242        }),
243        Expr::Logical(_, l, r) | Expr::Binary(_, l, r) => expr_slot_safe(l) && expr_slot_safe(r),
244        Expr::Conditional { test, cons, alt } => {
245            expr_slot_safe(test) && expr_slot_safe(cons) && expr_slot_safe(alt)
246        }
247        Expr::Assign { target, value } => expr_slot_safe(target) && expr_slot_safe(value),
248        Expr::Update { target, .. } => expr_slot_safe(target),
249        Expr::Call { func, args, .. } | Expr::New { callee: func, args } => {
250            expr_slot_safe(func) && all(args)
251        }
252        Expr::Member { object, .. } => expr_slot_safe(object),
253        Expr::Index { object, index, .. } => expr_slot_safe(object) && expr_slot_safe(index),
254        // Its own chunk, its own frame; `collect_escaping_expr` takes the names.
255        Expr::Function { .. } | Expr::Class(_) => true,
256        _ => true,
257    }
258}
259
260struct Planner {
261    candidates: SlotTable,
262    rejected: FxHashSet<String>,
263    /// Slots whose value is provably a JS Number at every point: declared from
264    /// a numeric literal and written afterwards only by `++`/`--`, which on a
265    /// Number yields a Number. `i++` on one of these needs no `NUM_STEP` call
266    /// into the host to do `ToNumeric` and stay BigInt-aware.
267    numeric: FxHashSet<String>,
268    /// Slotted names declared `const`. A slotted name is declared exactly once
269    /// in the chunk (rule 2), is unreachable from any other chunk (rule 1) and
270    /// is a simple identifier (rule 4), so a store to one is UNAMBIGUOUSLY a
271    /// store to that const binding and the compiler can reject it outright.
272    consts: FxHashSet<String>,
273    top_level: bool,
274    next: u16,
275}
276
277impl Planner {
278    /// A declaration in source order: the first one claims a slot, a second one
279    /// (rule 2) gives the name back to the host.
280    fn declare(&mut self, name: &str) {
281        if self.rejected.contains(name) {
282            return;
283        }
284        if self.candidates.contains_key(name) {
285            self.reject(name);
286            return;
287        }
288        // fusevm addresses slots with a `u16`; a chunk with more locals than
289        // that keeps the rest on the name path.
290        if self.next == u16::MAX {
291            self.reject(name);
292            return;
293        }
294        self.candidates.insert(name.to_string(), self.next);
295        self.next += 1;
296    }
297
298    fn reject(&mut self, name: &str) {
299        self.rejected.insert(name.to_string());
300    }
301
302    /// A mention of `name` that is not its declaration (rule 3): if the name has
303    /// no slot yet, its first mention is a read, so it never gets one.
304    fn mention(&mut self, name: &str) {
305        if !self.candidates.contains_key(name) {
306            self.reject(name);
307        }
308    }
309
310    /// The declaration target of a `let`/`var`/`for`-head binding.
311    fn declare_target(&mut self, target: &Expr, kind: Option<DeclKind>) {
312        self.declare_target_init(target, kind, None)
313    }
314
315    /// As `declare_target`, plus the initializer, which decides whether the
316    /// binding starts out a Number (see `Planner::numeric`).
317    fn declare_target_init(&mut self, target: &Expr, kind: Option<DeclKind>, init: Option<&Expr>) {
318        let Expr::Ident(n) = target else {
319            // A destructuring pattern binds through the host; every name in it
320            // is off the table.
321            self.reject_names_in(target);
322            return;
323        };
324        match kind {
325            // Rule 5: a top-level `var` is a global-object property.
326            Some(DeclKind::Var) if self.top_level => self.reject(n),
327            Some(k) => {
328                self.declare(n);
329                if init.is_some_and(is_number_literal) {
330                    self.numeric.insert(n.clone());
331                }
332                if k == DeclKind::Const {
333                    self.consts.insert(n.clone());
334                }
335            }
336            // `for (x of …)` with no declaration keyword assigns an existing
337            // binding — that is a mention, not a declaration.
338            None => self.mention(n),
339        }
340    }
341
342    fn reject_names_in(&mut self, e: &Expr) {
343        let mut names = Vec::new();
344        collect_idents(e, &mut names);
345        for n in names {
346            self.reject(&n);
347        }
348    }
349
350    fn walk_stmt(&mut self, s: &Stmt) {
351        match &s.kind {
352            StmtKind::Decl { kind, decls } => {
353                for d in decls {
354                    // The initializer is evaluated BEFORE the binding exists.
355                    if let Some(init) = &d.init {
356                        self.walk_expr(init);
357                    }
358                    match &d.init {
359                        // `let x;` with no initializer leaves the binding
360                        // unassigned, which is exactly the read-before-write
361                        // case slots cannot answer for.
362                        None => self.reject_names_in(&d.target),
363                        Some(init) => self.declare_target_init(&d.target, Some(*kind), Some(init)),
364                    }
365                }
366            }
367            StmtKind::Expr(e) | StmtKind::Throw(e) => self.walk_expr(e),
368            StmtKind::Return(e) => {
369                if let Some(e) = e {
370                    self.walk_expr(e);
371                }
372            }
373            StmtKind::Block(body) => {
374                for s in body {
375                    self.walk_stmt(s);
376                }
377            }
378            StmtKind::If { test, cons, alt } => {
379                self.walk_expr(test);
380                self.walk_stmt(cons);
381                if let Some(alt) = alt {
382                    self.walk_stmt(alt);
383                }
384            }
385            StmtKind::While { test, body } => {
386                self.walk_expr(test);
387                self.walk_stmt(body);
388            }
389            StmtKind::DoWhile { body, test } => {
390                self.walk_stmt(body);
391                self.walk_expr(test);
392            }
393            StmtKind::For {
394                init,
395                test,
396                update,
397                body,
398            } => {
399                if let Some(init) = init {
400                    self.walk_stmt(init);
401                }
402                if let Some(test) = test {
403                    self.walk_expr(test);
404                }
405                self.walk_stmt(body);
406                if let Some(update) = update {
407                    self.walk_expr(update);
408                }
409            }
410            StmtKind::ForOf {
411                decl_kind,
412                target,
413                iter,
414                body,
415                ..
416            } => {
417                self.walk_expr(iter);
418                self.declare_target(target, *decl_kind);
419                self.walk_stmt(body);
420            }
421            StmtKind::ForIn {
422                decl_kind,
423                target,
424                object,
425                body,
426            } => {
427                self.walk_expr(object);
428                self.declare_target(target, *decl_kind);
429                self.walk_stmt(body);
430            }
431            StmtKind::Switch { disc, cases } => {
432                self.walk_expr(disc);
433                for c in cases {
434                    if let Some(t) = &c.test {
435                        self.walk_expr(t);
436                    }
437                    for s in &c.body {
438                        self.walk_stmt(s);
439                    }
440                }
441            }
442            StmtKind::Labeled { body, .. } => self.walk_stmt(body),
443            // Refused by `chunk_is_eligible` before the walk starts.
444            StmtKind::Try { .. } | StmtKind::FuncDecl { .. } | StmtKind::ClassDecl(_) => {}
445            StmtKind::Break(_) | StmtKind::Continue(_) | StmtKind::Empty => {}
446        }
447    }
448
449    fn walk_expr(&mut self, e: &Expr) {
450        match e {
451            Expr::Ident(n) => self.mention(n),
452            // An assignment to a name that has no slot yet is a plain store to
453            // whatever binding exists — a mention, not a declaration.
454            Expr::Assign { target, value } => {
455                self.walk_expr(value);
456                match &**target {
457                    // A plain store of an arbitrary value: whatever the slot
458                    // held, it is not provably a Number after this.
459                    Expr::Ident(n) => {
460                        self.numeric.remove(n);
461                        self.mention(n);
462                    }
463                    other => self.walk_expr(other),
464                }
465            }
466            Expr::Update { target, .. } => self.walk_expr(target),
467            // `delete x` asks the host to remove a binding; a slot has none.
468            Expr::Unary(crate::ast::UnOp::Delete, inner) => {
469                if let Expr::Ident(n) = &**inner {
470                    self.reject(n);
471                } else {
472                    self.walk_expr(inner);
473                }
474            }
475            Expr::Unary(_, inner) | Expr::Spread(inner) | Expr::Await(inner) => {
476                self.walk_expr(inner)
477            }
478            Expr::Yield { arg: Some(a), .. } => self.walk_expr(a),
479            Expr::Template { exprs, .. } => self.walk_all(exprs),
480            Expr::TaggedTemplate { tag, exprs, .. } => {
481                self.walk_expr(tag);
482                self.walk_all(exprs);
483            }
484            Expr::Array(items) | Expr::Sequence(items) => self.walk_all(items),
485            Expr::Object(props) => {
486                for p in props {
487                    match p {
488                        Prop::KeyValue { key, value, .. } => {
489                            self.walk_expr(key);
490                            self.walk_expr(value);
491                        }
492                        Prop::Spread(x) => self.walk_expr(x),
493                        Prop::Accessor { key, func, .. } => {
494                            self.walk_expr(key);
495                            self.walk_expr(func);
496                        }
497                    }
498                }
499            }
500            Expr::Logical(_, l, r) | Expr::Binary(_, l, r) => {
501                self.walk_expr(l);
502                self.walk_expr(r);
503            }
504            Expr::Conditional { test, cons, alt } => {
505                self.walk_expr(test);
506                self.walk_expr(cons);
507                self.walk_expr(alt);
508            }
509            Expr::Call { func, args, .. } | Expr::New { callee: func, args } => {
510                self.walk_expr(func);
511                self.walk_all(args);
512            }
513            Expr::Member { object, .. } => self.walk_expr(object),
514            Expr::Index { object, index, .. } => {
515                self.walk_expr(object);
516                self.walk_expr(index);
517            }
518            // A nested function or class body is refused by `chunk_is_eligible`,
519            // so anything it names is already out of reach here.
520            Expr::Function { .. } | Expr::Class(_) => {}
521            _ => {}
522        }
523    }
524
525    fn walk_all(&mut self, items: &[Expr]) {
526        for e in items {
527            self.walk_expr(e);
528        }
529    }
530}
531
532// ── names another chunk can reach ────────────────────────────────────────────
533//
534// A nested function, arrow, class body or `try` part compiles to a chunk of its
535// own and resolves every name it uses through the environment chain. Whatever
536// those chunks mention therefore has to stay a real binding — and only that,
537// which is what lets a counting loop keep its slots in a file that also passes
538// a callback to `map`.
539
540fn collect_escaping_stmt(s: &Stmt, out: &mut FxHashSet<String>) {
541    match &s.kind {
542        // The declaration's own name is bound by the hoisting pass, by name.
543        StmtKind::FuncDecl { name, .. } => {
544            out.insert(name.clone());
545            collect_all_idents_stmt(s, out);
546        }
547        StmtKind::ClassDecl(c) => {
548            if let Some(n) = &c.name {
549                out.insert(n.clone());
550            }
551            collect_all_idents_stmt(s, out);
552        }
553        StmtKind::Try {
554            block,
555            handler,
556            finalizer,
557        } => {
558            for st in block {
559                collect_all_idents_stmt(st, out);
560            }
561            if let Some((bind, body)) = handler {
562                if let Some(p) = bind {
563                    collect_all_idents_expr(p, out);
564                }
565                for st in body {
566                    collect_all_idents_stmt(st, out);
567                }
568            }
569            if let Some(body) = finalizer {
570                for st in body {
571                    collect_all_idents_stmt(st, out);
572                }
573            }
574        }
575        StmtKind::Expr(e) | StmtKind::Throw(e) => collect_escaping_expr(e, out),
576        StmtKind::Return(e) => {
577            if let Some(e) = e {
578                collect_escaping_expr(e, out);
579            }
580        }
581        StmtKind::Decl { decls, .. } => {
582            for d in decls {
583                if let Some(init) = &d.init {
584                    collect_escaping_expr(init, out);
585                }
586            }
587        }
588        StmtKind::Block(body) => {
589            for st in body {
590                collect_escaping_stmt(st, out);
591            }
592        }
593        StmtKind::If { test, cons, alt } => {
594            collect_escaping_expr(test, out);
595            collect_escaping_stmt(cons, out);
596            if let Some(alt) = alt {
597                collect_escaping_stmt(alt, out);
598            }
599        }
600        StmtKind::While { test, body } | StmtKind::DoWhile { body, test } => {
601            collect_escaping_expr(test, out);
602            collect_escaping_stmt(body, out);
603        }
604        StmtKind::For {
605            init,
606            test,
607            update,
608            body,
609        } => {
610            if let Some(init) = init {
611                collect_escaping_stmt(init, out);
612            }
613            if let Some(test) = test {
614                collect_escaping_expr(test, out);
615            }
616            if let Some(update) = update {
617                collect_escaping_expr(update, out);
618            }
619            collect_escaping_stmt(body, out);
620        }
621        StmtKind::ForOf {
622            target, iter, body, ..
623        } => {
624            collect_escaping_expr(target, out);
625            collect_escaping_expr(iter, out);
626            collect_escaping_stmt(body, out);
627        }
628        StmtKind::ForIn {
629            target,
630            object,
631            body,
632            ..
633        } => {
634            collect_escaping_expr(target, out);
635            collect_escaping_expr(object, out);
636            collect_escaping_stmt(body, out);
637        }
638        StmtKind::Switch { disc, cases } => {
639            collect_escaping_expr(disc, out);
640            for c in cases {
641                if let Some(t) = &c.test {
642                    collect_escaping_expr(t, out);
643                }
644                for st in &c.body {
645                    collect_escaping_stmt(st, out);
646                }
647            }
648        }
649        StmtKind::Labeled { body, .. } => collect_escaping_stmt(body, out),
650        StmtKind::Break(_) | StmtKind::Continue(_) | StmtKind::Empty => {}
651    }
652}
653
654fn collect_escaping_expr(e: &Expr, out: &mut FxHashSet<String>) {
655    match e {
656        // Here is the boundary: everything named inside a nested function or
657        // class body is resolved from the environment when that chunk runs.
658        Expr::Function { .. } | Expr::Class(_) => collect_all_idents_expr(e, out),
659        Expr::Ident(_) | Expr::Null | Expr::Undefined | Expr::True | Expr::False => {}
660        Expr::Unary(_, x) | Expr::Spread(x) | Expr::Await(x) | Expr::Member { object: x, .. } => {
661            collect_escaping_expr(x, out)
662        }
663        Expr::Yield { arg: Some(a), .. } => collect_escaping_expr(a, out),
664        Expr::Template { exprs, .. } => exprs.iter().for_each(|x| collect_escaping_expr(x, out)),
665        Expr::TaggedTemplate { tag, exprs, .. } => {
666            collect_escaping_expr(tag, out);
667            exprs.iter().for_each(|x| collect_escaping_expr(x, out));
668        }
669        Expr::Array(items) | Expr::Sequence(items) => {
670            items.iter().for_each(|x| collect_escaping_expr(x, out))
671        }
672        Expr::Object(props) => {
673            for p in props {
674                match p {
675                    Prop::KeyValue { key, value, .. } => {
676                        collect_escaping_expr(key, out);
677                        collect_escaping_expr(value, out);
678                    }
679                    Prop::Spread(x) => collect_escaping_expr(x, out),
680                    Prop::Accessor { key, func, .. } => {
681                        collect_escaping_expr(key, out);
682                        collect_all_idents_expr(func, out);
683                    }
684                }
685            }
686        }
687        Expr::Logical(_, l, r) | Expr::Binary(_, l, r) => {
688            collect_escaping_expr(l, out);
689            collect_escaping_expr(r, out);
690        }
691        Expr::Conditional { test, cons, alt } => {
692            collect_escaping_expr(test, out);
693            collect_escaping_expr(cons, out);
694            collect_escaping_expr(alt, out);
695        }
696        Expr::Assign { target, value } => {
697            collect_escaping_expr(target, out);
698            collect_escaping_expr(value, out);
699        }
700        Expr::Update { target, .. } => collect_escaping_expr(target, out),
701        Expr::Call { func, args, .. } | Expr::New { callee: func, args } => {
702            collect_escaping_expr(func, out);
703            args.iter().for_each(|x| collect_escaping_expr(x, out));
704        }
705        Expr::Index { object, index, .. } => {
706            collect_escaping_expr(object, out);
707            collect_escaping_expr(index, out);
708        }
709        _ => {}
710    }
711}
712
713/// Every identifier anywhere in a statement, nested bodies included.
714fn collect_all_idents_stmt(s: &Stmt, out: &mut FxHashSet<String>) {
715    match &s.kind {
716        StmtKind::FuncDecl { params, body, .. } => {
717            for p in params {
718                collect_all_idents_expr(&p.pattern, out);
719                if let Some(d) = &p.default {
720                    collect_all_idents_expr(d, out);
721                }
722            }
723            body.iter().for_each(|st| collect_all_idents_stmt(st, out));
724        }
725        StmtKind::ClassDecl(c) => collect_class_idents(c, out),
726        StmtKind::Expr(e) | StmtKind::Throw(e) => collect_all_idents_expr(e, out),
727        StmtKind::Return(e) => {
728            if let Some(e) = e {
729                collect_all_idents_expr(e, out);
730            }
731        }
732        StmtKind::Decl { decls, .. } => {
733            for d in decls {
734                collect_all_idents_expr(&d.target, out);
735                if let Some(init) = &d.init {
736                    collect_all_idents_expr(init, out);
737                }
738            }
739        }
740        StmtKind::Block(body) => body.iter().for_each(|st| collect_all_idents_stmt(st, out)),
741        StmtKind::If { test, cons, alt } => {
742            collect_all_idents_expr(test, out);
743            collect_all_idents_stmt(cons, out);
744            if let Some(alt) = alt {
745                collect_all_idents_stmt(alt, out);
746            }
747        }
748        StmtKind::While { test, body } | StmtKind::DoWhile { body, test } => {
749            collect_all_idents_expr(test, out);
750            collect_all_idents_stmt(body, out);
751        }
752        StmtKind::For {
753            init,
754            test,
755            update,
756            body,
757        } => {
758            if let Some(init) = init {
759                collect_all_idents_stmt(init, out);
760            }
761            if let Some(test) = test {
762                collect_all_idents_expr(test, out);
763            }
764            if let Some(update) = update {
765                collect_all_idents_expr(update, out);
766            }
767            collect_all_idents_stmt(body, out);
768        }
769        StmtKind::ForOf {
770            target, iter, body, ..
771        } => {
772            collect_all_idents_expr(target, out);
773            collect_all_idents_expr(iter, out);
774            collect_all_idents_stmt(body, out);
775        }
776        StmtKind::ForIn {
777            target,
778            object,
779            body,
780            ..
781        } => {
782            collect_all_idents_expr(target, out);
783            collect_all_idents_expr(object, out);
784            collect_all_idents_stmt(body, out);
785        }
786        StmtKind::Switch { disc, cases } => {
787            collect_all_idents_expr(disc, out);
788            for c in cases {
789                if let Some(t) = &c.test {
790                    collect_all_idents_expr(t, out);
791                }
792                c.body
793                    .iter()
794                    .for_each(|st| collect_all_idents_stmt(st, out));
795            }
796        }
797        StmtKind::Labeled { body, .. } => collect_all_idents_stmt(body, out),
798        StmtKind::Try {
799            block,
800            handler,
801            finalizer,
802        } => {
803            block.iter().for_each(|st| collect_all_idents_stmt(st, out));
804            if let Some((bind, body)) = handler {
805                if let Some(p) = bind {
806                    collect_all_idents_expr(p, out);
807                }
808                body.iter().for_each(|st| collect_all_idents_stmt(st, out));
809            }
810            if let Some(body) = finalizer {
811                body.iter().for_each(|st| collect_all_idents_stmt(st, out));
812            }
813        }
814        StmtKind::Break(_) | StmtKind::Continue(_) | StmtKind::Empty => {}
815    }
816}
817
818fn collect_class_idents(c: &crate::ast::ClassNode, out: &mut FxHashSet<String>) {
819    if let Some(p) = &c.parent {
820        collect_all_idents_expr(p, out);
821    }
822    for m in &c.members {
823        collect_all_idents_expr(&m.key, out);
824        for p in &m.params {
825            collect_all_idents_expr(&p.pattern, out);
826            if let Some(d) = &p.default {
827                collect_all_idents_expr(d, out);
828            }
829        }
830        m.body
831            .iter()
832            .for_each(|st| collect_all_idents_stmt(st, out));
833        if let Some(init) = &m.field_init {
834            collect_all_idents_expr(init, out);
835        }
836    }
837}
838
839fn collect_all_idents_expr(e: &Expr, out: &mut FxHashSet<String>) {
840    let all = |xs: &[Expr], out: &mut FxHashSet<String>| {
841        xs.iter().for_each(|x| collect_all_idents_expr(x, out))
842    };
843    match e {
844        Expr::Ident(n) => {
845            out.insert(n.clone());
846        }
847        Expr::Class(c) => collect_class_idents(c, out),
848        Expr::Function { params, body, .. } => {
849            for p in params {
850                collect_all_idents_expr(&p.pattern, out);
851                if let Some(d) = &p.default {
852                    collect_all_idents_expr(d, out);
853                }
854            }
855            match body {
856                crate::ast::FnBody::Block(stmts) => {
857                    stmts.iter().for_each(|st| collect_all_idents_stmt(st, out))
858                }
859                crate::ast::FnBody::Expr(x) => collect_all_idents_expr(x, out),
860            }
861        }
862        Expr::Unary(_, x) | Expr::Spread(x) | Expr::Await(x) | Expr::Member { object: x, .. } => {
863            collect_all_idents_expr(x, out)
864        }
865        Expr::Yield { arg: Some(a), .. } => collect_all_idents_expr(a, out),
866        Expr::Template { exprs, .. } => all(exprs, out),
867        Expr::TaggedTemplate { tag, exprs, .. } => {
868            collect_all_idents_expr(tag, out);
869            all(exprs, out);
870        }
871        Expr::Array(items) | Expr::Sequence(items) => all(items, out),
872        Expr::Object(props) => {
873            for p in props {
874                match p {
875                    Prop::KeyValue { key, value, .. } => {
876                        collect_all_idents_expr(key, out);
877                        collect_all_idents_expr(value, out);
878                    }
879                    Prop::Spread(x) => collect_all_idents_expr(x, out),
880                    Prop::Accessor { key, func, .. } => {
881                        collect_all_idents_expr(key, out);
882                        collect_all_idents_expr(func, out);
883                    }
884                }
885            }
886        }
887        Expr::Logical(_, l, r) | Expr::Binary(_, l, r) => {
888            collect_all_idents_expr(l, out);
889            collect_all_idents_expr(r, out);
890        }
891        Expr::Conditional { test, cons, alt } => {
892            collect_all_idents_expr(test, out);
893            collect_all_idents_expr(cons, out);
894            collect_all_idents_expr(alt, out);
895        }
896        Expr::Assign { target, value } => {
897            collect_all_idents_expr(target, out);
898            collect_all_idents_expr(value, out);
899        }
900        Expr::Update { target, .. } => collect_all_idents_expr(target, out),
901        Expr::Call { func, args, .. } | Expr::New { callee: func, args } => {
902            collect_all_idents_expr(func, out);
903            all(args, out);
904        }
905        Expr::Index { object, index, .. } => {
906            collect_all_idents_expr(object, out);
907            collect_all_idents_expr(index, out);
908        }
909        _ => {}
910    }
911}
912
913/// Every identifier appearing in a (possibly destructuring) target expression.
914fn collect_idents(e: &Expr, out: &mut Vec<String>) {
915    match e {
916        Expr::Ident(n) => out.push(n.clone()),
917        Expr::Array(items) | Expr::Sequence(items) => {
918            for x in items {
919                collect_idents(x, out);
920            }
921        }
922        Expr::Object(props) => {
923            for p in props {
924                match p {
925                    Prop::KeyValue { value, .. } => collect_idents(value, out),
926                    Prop::Spread(x) => collect_idents(x, out),
927                    Prop::Accessor { .. } => {}
928                }
929            }
930        }
931        Expr::Spread(inner) => collect_idents(inner, out),
932        Expr::Assign { target, .. } => collect_idents(target, out),
933        _ => {}
934    }
935}