Skip to main content

nodejs/
compiler.rs

1//! Lower the JavaScript AST to `fusevm::Chunk`.
2//!
3//! Native fusevm ops carry arithmetic (`+ - * / % **`), the relational
4//! comparisons (`< <= > >=`) and boolean short-circuit so the JIT can trace
5//! them; the strict numeric hook (host) supplies JS semantics for non-numeric
6//! operands (string concat, coercion). Everything JS-specific — name access,
7//! member/index access, calls, object/array construction, iteration — lowers to
8//! a `CallBuiltin` that lands in `builtins.rs`.
9//!
10//! Conditions are normalized through the `TRUTHY` builtin before a native
11//! `JumpIfFalse`, because JS truthiness differs from fusevm's default numeric
12//! truthiness. Compiler-internal name strings travel as native `Value::Str`
13//! constants; JS-level strings are always heap objects built by `MKSTR`.
14
15use crate::ast::*;
16use crate::host::{binop as bop, member, ops, unop, unwind, FuncDef, ParamSlot, TryDef};
17use fusevm::{Chunk, ChunkBuilder, Op, Value};
18
19/// A compiled program: the top-level chunk plus the function template table and
20/// the try-block table.
21#[derive(Default)]
22pub struct Program {
23    pub main: Chunk,
24    pub functions: Vec<(String, FuncDef)>,
25    pub tries: Vec<TryDef>,
26    /// Whether the program's own top level is strict (`'use strict'` as its
27    /// first statement). A FUNCTION carries its strictness in its `FuncDef`;
28    /// the top level had nowhere to put it, so the module frame stayed sloppy
29    /// and a refused write never threw there even though the compiler had
30    /// already emitted the strict ASSIGNMENT opcodes.
31    pub strict: bool,
32    /// The text the program was parsed from, which every function's `span`
33    /// indexes. Installed on the host by `load_merged`.
34    pub source: Option<std::sync::Arc<str>>,
35}
36
37/// Rebase every func-id and try-id reference so its ids sit above those already
38/// loaded on the host (needed only for incremental loading; a no-op for a single
39/// run).
40pub fn rebase_program(prog: &mut Program, func_off: usize, try_off: usize) {
41    if func_off == 0 && try_off == 0 {
42        return;
43    }
44    rebase_chunk(&mut prog.main, func_off, try_off);
45    for (_, f) in &mut prog.functions {
46        rebase_chunk(&mut f.chunk, func_off, try_off);
47    }
48    for t in &mut prog.tries {
49        rebase_chunk(&mut t.block, func_off, try_off);
50        if let Some((_, hb)) = &mut t.handler {
51            rebase_chunk(hb, func_off, try_off);
52        }
53        if let Some(f) = &mut t.finalizer {
54            rebase_chunk(f, func_off, try_off);
55        }
56    }
57}
58
59fn rebase_chunk(chunk: &mut Chunk, func_off: usize, try_off: usize) {
60    for i in 1..chunk.ops.len() {
61        let off = match chunk.ops[i] {
62            Op::CallBuiltin(id, _) if id == ops::MKFUNC => func_off,
63            Op::CallBuiltin(id, 4) if id == ops::MKCLASS => func_off,
64            Op::CallBuiltin(id, 1) if id == ops::TRY => try_off,
65            _ => continue,
66        };
67        if off == 0 {
68            continue;
69        }
70        if let Op::LoadInt(v) = &mut chunk.ops[i - 1] {
71            *v += off as i64;
72        }
73    }
74    for sub in &mut chunk.sub_chunks {
75        rebase_chunk(sub, func_off, try_off);
76    }
77}
78
79/// The binding scope a declaration keyword introduces.
80fn bind_mode(kind: DeclKind) -> BindMode {
81    match kind {
82        DeclKind::Var => BindMode::Var,
83        DeclKind::Let => BindMode::Lexical,
84        DeclKind::Const => BindMode::Const,
85    }
86}
87
88/// How a binding site introduces its name.
89#[derive(Clone, Copy, PartialEq, Eq)]
90enum BindMode {
91    /// Plain assignment to an existing binding (`x = 1`, a for-of head without
92    /// `let`/`const`/`var`).
93    Assign,
94    /// `let`/`class`: bound in the innermost BLOCK scope.
95    Lexical,
96    /// `const`: block-scoped like `Lexical`, but IMMUTABLE — a later assignment
97    /// to the name throws `TypeError: Assignment to constant variable.`
98    Const,
99    /// `var` / a hoisted function declaration: bound at FUNCTION scope.
100    Var,
101}
102
103/// Break/continue jump fixups for a loop or switch.
104struct LoopCtx {
105    breaks: Vec<usize>,
106    continues: Vec<usize>,
107    /// Block-scope depth the `break` target expects; a `break` from inside nested
108    /// blocks pops back down to it first.
109    break_depth: usize,
110    /// Block-scope depth the `continue` target expects.
111    continue_depth: usize,
112    /// Number of iterators on the VM stack inside this loop's body.
113    iter_depth: usize,
114    /// Whether `continue` binds here (true for loops, false for `switch`).
115    catches_continue: bool,
116    /// The source label attached to this loop/block, if any (`outer: for …`),
117    /// so labeled `break outer` / `continue outer` can target it directly.
118    label: Option<String>,
119}
120
121#[derive(Default)]
122pub struct Compiler {
123    /// Pending short-circuit jumps for the optional chain being lowered, one
124    /// frame per chain.
125    ///
126    /// `?.` short-circuits the WHOLE chain to its right, not just its own link:
127    /// `o.a?.b.c` is `undefined` when `o.a` is nullish, and never reads `.c`
128    /// off it. Each `?.` therefore parks its jump here and the chain's ROOT
129    /// patches every one of them to the end. An empty stack means no chain is
130    /// open, so a `?.` outside one patches itself as before.
131    opt_chain: Vec<Vec<usize>>,
132    /// Compiling a Script whose COMPLETION VALUE is observable — what `eval`
133    /// returns. Every expression statement then stores into `.completion`
134    /// instead of discarding its value, which is how the "last non-empty
135    /// completion" rule (14.x, `UpdateEmpty`) falls out without threading a
136    /// value through each statement form. Cleared inside a nested function
137    /// body, whose statements are not the script's.
138    completion: bool,
139    functions: Vec<(String, FuncDef)>,
140    tries: Vec<TryDef>,
141    loops: Vec<LoopCtx>,
142    tmp: usize,
143    /// A label seen immediately before a loop, consumed by that loop's `LoopCtx`
144    /// (`outer: for (…)`); `None` once claimed.
145    pending_label: Option<String>,
146    /// Source text of the expression an object pattern is being destructured
147    /// FROM, set by the declaration or assignment site. Node names it in the
148    /// error a nullish source raises — `Cannot destructure property 'w' of 'v'
149    /// as it is null` — and the pattern compiler only has the VALUE.
150    destructure_src: Option<String>,
151    /// Whether `destructure_src` came from a DECLARATION's initializer rather
152    /// than from an assignment target. Node names the source in a
153    /// not-iterable error only for a declaration: `const [x] = o` is `o is not
154    /// iterable`, while `[y] = o` reports the TYPE.
155    destructure_is_decl: bool,
156    /// Emit per-statement `DBG_LINE` markers for the DAP debugger (`node --dap`).
157    debug: bool,
158    /// Index into `loops` of the first loop opened by the chunk being emitted.
159    /// A `break`/`continue` targeting a loop BELOW this index leaves the current
160    /// chunk (a `try` body is compiled as its own chunk), so it cannot be a plain
161    /// jump and is raised as a signal instead.
162    chunk_loop_base: usize,
163    /// Whether this chunk contains a signal-raising `break`/`continue`, so loops
164    /// in it must re-dispatch a still-pending signal when they exit.
165    chunk_signals: bool,
166    /// Number of block scopes open at the current emission point, so a jump out of
167    /// them can pop exactly the right number.
168    scope_depth: usize,
169    /// True while compiling an `async function*` body, where `yield*` must drive
170    /// the delegate through the ASYNC iteration protocol.
171    in_async_generator: bool,
172    /// Number of for-of/for-in iterators parked on the VM stack at this point. A
173    /// `break`/`continue` that leaves such a loop must close and drop its iterator,
174    /// otherwise the enclosing loop's `FORITER` would peek at the wrong one.
175    iter_depth: usize,
176    /// Whether the code being emitted is in STRICT mode — a `'use strict'`
177    /// directive prologue on the program or an enclosing function body, or a
178    /// class body (which is strict unconditionally). The only difference it
179    /// makes here is `PutValue` on an unresolvable reference: strict code throws
180    /// `ReferenceError` where sloppy code creates a global.
181    strict: bool,
182    /// Callee SOURCE TEXT per call op of the chunk being emitted, handed to the
183    /// host when the chunk is built so a failed call can name the callee the way
184    /// the source wrote it. Saved and restored around every nested chunk.
185    call_sites: Vec<(usize, String)>,
186    /// Parked-iterator depth per `yield` op of the chunk being emitted, so an
187    /// injected `.return()`/`.throw()` can close the `for…of` / `yield*`
188    /// iterators the halt would otherwise abandon.
189    yield_sites: Vec<(usize, usize)>,
190    /// Locals of the chunk being emitted that live in fusevm frame slots rather
191    /// than the host's scope chain — see [`crate::slots`]. Empty for a chunk the
192    /// analysis refused, so `slot_of` answering `None` is the old path.
193    slots: crate::slots::Plan,
194    /// Number of tagged-template sites emitted so far in this compilation, so
195    /// each site carries an ordinal the runtime can cache its template object
196    /// under. Monotonic across the whole compilation rather than per chunk: two
197    /// textually identical arrow bodies are separate chunks, and this operand is
198    /// what makes their bytecode — and therefore their chunk hashes — differ.
199    tmpl_sites: u64,
200}
201
202// ── early errors: duplicate lexical declarations ─────────────────────────────
203
204/// Reject a duplicate lexical declaration before anything runs, as node does.
205///
206/// `let a = 1; let a = 2;` is a SyntaxError at PARSE time in node, and this
207/// engine ran it — the second declaration simply won. That is the gap that lets
208/// a genuine double-declaration bug through silently, and it bit three test
209/// files in this repo whose collisions node rejected and this accepted.
210///
211/// Deliberately narrow, since a false positive REJECTS a program that works:
212/// only the three collisions the spec is unambiguous about are reported —
213/// two lexical declarations of one name in the same statement list, a lexical
214/// name that a `var` in the same subtree hoists onto, and a lexical name
215/// colliding with a function declaration beside it. Repeated `var`s, and the
216/// same name in nested scopes, stay legal.
217/// The names strict code may not BIND or ASSIGN to (13.1.1, 14.3.1.1).
218const RESERVED_IN_STRICT: [&str; 2] = ["eval", "arguments"];
219
220/// `SyntaxError: Unexpected eval or arguments in strict mode` — raised for a
221/// binding, a parameter, an assignment target and an update target alike.
222fn strict_reserved_error() -> String {
223    "SyntaxError: Unexpected eval or arguments in strict mode".to_string()
224}
225
226pub fn check_early_errors(stmts: &[Stmt]) -> Result<(), String> {
227    let mut lexical: Vec<String> = Vec::new();
228    let mut functions: Vec<String> = Vec::new();
229    for st in stmts {
230        match &st.kind {
231            StmtKind::Decl { kind, decls } if !matches!(kind, DeclKind::Var) => {
232                for d in decls {
233                    let mut names = Vec::new();
234                    pattern_names(&d.target, &mut names);
235                    for n in names {
236                        if lexical.contains(&n) {
237                            return Err(already_declared(&n));
238                        }
239                        lexical.push(n);
240                    }
241                }
242            }
243            StmtKind::ClassDecl(c) => {
244                if let Some(n) = &c.name {
245                    if lexical.contains(n) {
246                        return Err(already_declared(n));
247                    }
248                    lexical.push(n.clone());
249                }
250            }
251            StmtKind::FuncDecl { name, .. } => functions.push(name.clone()),
252            _ => {}
253        }
254    }
255    // A function declaration and a lexical binding of the same name cannot
256    // share a scope, whichever order they appear in.
257    for f in &functions {
258        if lexical.contains(f) {
259            return Err(already_declared(f));
260        }
261    }
262    // A `var` anywhere below hoists PAST any block between it and its function
263    // scope, so it collides with a lexical name declared here.
264    let mut vars: Vec<String> = Vec::new();
265    for st in stmts {
266        collect_var_names(st, &mut vars);
267    }
268    for n in &lexical {
269        if vars.contains(n) {
270            return Err(already_declared(n));
271        }
272    }
273    // Each nested statement list is its own scope.
274    for st in stmts {
275        check_nested(&st.kind)?;
276    }
277    Ok(())
278}
279
280fn already_declared(name: &str) -> String {
281    format!("SyntaxError: Identifier '{name}' has already been declared")
282}
283
284/// Recurse into the statement lists that form their own scopes. A function
285/// BODY is checked when that function is compiled, so the walk does not
286/// descend into one here.
287fn check_nested(k: &StmtKind) -> Result<(), String> {
288    let one = |s: &Stmt| check_nested(&s.kind);
289    match k {
290        StmtKind::Block(b) => check_early_errors(b),
291        StmtKind::If { cons, alt, .. } => {
292            one(cons)?;
293            match alt {
294                Some(a) => one(a),
295                None => Ok(()),
296            }
297        }
298        StmtKind::While { body, .. }
299        | StmtKind::DoWhile { body, .. }
300        | StmtKind::Labeled { body, .. }
301        | StmtKind::ForOf { body, .. }
302        | StmtKind::ForIn { body, .. } => one(body),
303        StmtKind::For { body, .. } => one(body),
304        StmtKind::Try {
305            block,
306            handler,
307            finalizer,
308        } => {
309            check_early_errors(block)?;
310            if let Some((_, h)) = handler {
311                check_early_errors(h)?;
312            }
313            match finalizer {
314                Some(f) => check_early_errors(f),
315                None => Ok(()),
316            }
317        }
318        StmtKind::Switch { cases, .. } => {
319            // Every case shares ONE block scope, so their statements are
320            // checked together rather than case by case.
321            let all: Vec<Stmt> = cases.iter().flat_map(|c| c.body.clone()).collect();
322            check_early_errors(&all)
323        }
324        _ => Ok(()),
325    }
326}
327
328/// Compile a parsed program. `debug` enables per-statement DAP line markers.
329pub fn compile(stmts: &[Stmt], debug: bool) -> Result<Program, String> {
330    let mut c = Compiler {
331        opt_chain: Vec::new(),
332        debug,
333        // Under `--dap` the debugger reads scopes by name out of the host, and a
334        // slot has no name, so a debug run keeps every local a binding.
335        slots: if debug {
336            Default::default()
337        } else {
338            crate::slots::plan(&[], stmts, true)
339        },
340        strict: has_use_strict(stmts),
341        ..Default::default()
342    };
343    check_early_errors(stmts)?;
344    let mut b = ChunkBuilder::new();
345    // Hoist function declarations to the top (JS function hoisting).
346    c.hoist_vars(&mut b, stmts)?;
347    c.hoist_lexical(&mut b, stmts);
348    c.hoist_funcs(&mut b, stmts)?;
349    c.compile_stmts(&mut b, stmts)?;
350    Ok(Program {
351        main: c.finish_chunk(b),
352        functions: c.functions,
353        tries: c.tries,
354        strict: c.strict,
355        source: None,
356    })
357}
358
359/// Compile leaving the value of the final top-level expression statement on the
360/// stack (the program's completion value), for `eval`/`vm.runInThisContext`. A
361/// non-expression final statement leaves nothing (→ `undefined`).
362pub fn compile_completion(stmts: &[Stmt], debug: bool) -> Result<Program, String> {
363    compile_completion_strict(stmts, debug, false)
364}
365
366/// As [`compile_completion`], but with the CALLER's strictness folded in.
367///
368/// A direct `eval` inherits it (19.2.1.1 step 10), which decides every strict
369/// early error inside the evaluated source: `eval('delete x')` in strict code
370/// is a SyntaxError, and compiling the source on its own could not see that.
371pub fn compile_completion_strict(
372    stmts: &[Stmt],
373    debug: bool,
374    caller_strict: bool,
375) -> Result<Program, String> {
376    let mut c = Compiler {
377        opt_chain: Vec::new(),
378        debug,
379        strict: caller_strict || has_use_strict(stmts),
380        ..Default::default()
381    };
382    let mut b = ChunkBuilder::new();
383    // The completion register, declared before anything can write it. A name no
384    // source text can spell, like the `.param<n>` slots a destructured
385    // parameter uses.
386    c.name_const(&mut b, COMPLETION_SLOT);
387    b.emit(Op::LoadUndef, 0);
388    b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
389    b.emit(Op::Pop, 0);
390    c.completion = true;
391    c.hoist_vars(&mut b, stmts)?;
392    c.hoist_funcs(&mut b, stmts)?;
393    c.compile_stmts(&mut b, stmts)?;
394    c.completion = false;
395    c.name_const(&mut b, COMPLETION_SLOT);
396    b.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0);
397    Ok(Program {
398        main: c.finish_chunk(b),
399        functions: c.functions,
400        tries: c.tries,
401        strict: c.strict,
402        source: None,
403    })
404}
405
406impl Compiler {
407    /// `UpdateEmpty(result, undefined)` — the step `if`, every loop, `try` and
408    /// `switch` apply to their own completion (14.6.7, 14.7.x, 14.11.x,
409    /// 14.15.3). Each therefore always produces a VALUE: `1; if(0){2}` is
410    /// `undefined`, not 1, and a `break` out of a loop body discards what
411    /// earlier iterations accumulated. A block, a labelled statement, `;` and
412    /// every declaration propagate empty instead and are not reset here.
413    fn reset_completion(&mut self, b: &mut ChunkBuilder, line: u32) {
414        if !self.completion {
415            return;
416        }
417        self.name_const(b, COMPLETION_SLOT);
418        b.emit(Op::LoadUndef, line);
419        b.emit(Op::CallBuiltin(ops::SETLOCAL, 2), line);
420        b.emit(Op::Pop, line);
421    }
422}
423
424/// The hidden binding a completion-valued Script accumulates into. Leading dot:
425/// no source text can name it, so nothing a script declares can collide.
426const COMPLETION_SLOT: &str = ".completion";
427
428/// Does this statement list open with a `"use strict"` directive prologue?
429///
430/// A directive prologue is the run of leading statements that are nothing but a
431/// string literal, so `"use strict"` counts only while every statement before it
432/// is also one.
433fn has_use_strict(stmts: &[Stmt]) -> bool {
434    for s in stmts {
435        match &s.kind {
436            StmtKind::Expr(e) => match e {
437                Expr::Str(v) if v == "use strict" => return true,
438                Expr::Str(_) => continue,
439                _ => return false,
440            },
441            _ => return false,
442        }
443    }
444    false
445}
446
447/// The callee's source text, re-printed from its AST the way V8's `CallPrinter`
448/// does for the `TypeError` a failed call raises: `o.a.b`, `o[k]`, `"s".x`,
449/// `3.x`, `o?.a?.zz`. A string-literal computed access normalizes to dot form
450/// (`o['a']` prints `o.a`), which is what node reports.
451///
452/// `None` for any shape this does not print faithfully — the caller then keeps
453/// the bare method name it already used, so an unprinted shape is never given
454/// invented text.
455fn callee_text(e: &Expr) -> Option<String> {
456    Some(match e {
457        Expr::Ident(n) => n.clone(),
458        Expr::This => "this".into(),
459        Expr::Number(n) => crate::host::fmt_number(*n),
460        Expr::Str(s) => format!("\"{s}\""),
461        Expr::True => "true".into(),
462        Expr::False => "false".into(),
463        Expr::Null => "null".into(),
464        Expr::Undefined => "undefined".into(),
465        Expr::Array(items) if items.is_empty() => "[]".into(),
466        Expr::Object(props) if props.is_empty() => "{}".into(),
467        // A non-empty OBJECT literal is the one shape V8 will not render from
468        // source: `({a: 1})()` is `{(intermediate value)} is not a function`,
469        // where an array literal or a template is printed as written. Without
470        // this the message fell back to the VALUE, which renders
471        // `[object Object]` — a spelling node never produces here.
472        Expr::Object(_) => "{(intermediate value)}".into(),
473        Expr::Member {
474            object,
475            property,
476            optional,
477        } => {
478            let dot = if *optional { "?." } else { "." };
479            format!("{}{dot}{property}", callee_text(object)?)
480        }
481        Expr::Index {
482            object,
483            index,
484            optional,
485        } => {
486            let obj = callee_text(object)?;
487            // A string-literal key that is a plain identifier prints as a dot
488            // access, exactly as node reports it.
489            if let Expr::Str(k) = &**index {
490                if is_identifier(k) {
491                    let dot = if *optional { "?." } else { "." };
492                    return Some(format!("{obj}{dot}{k}"));
493                }
494            }
495            let idx = callee_text(index)?;
496            let open = if *optional { "?.[" } else { "[" };
497            format!("{obj}{open}{idx}]")
498        }
499        // V8 prints a call in a callee position as `f(...)`, whatever its
500        // arguments were: `require('fs').nope()` reports `require(...).nope`.
501        Expr::Call { func, .. } => format!("{}(...)", callee_text(func)?),
502        Expr::Sequence(items) => {
503            let parts: Option<Vec<String>> = items.iter().map(callee_text).collect();
504            format!("({})", parts?.join(" , "))
505        }
506        _ => return None,
507    })
508}
509
510/// Whether `s` can be written after a `.` — the test that decides whether a
511/// string-literal computed access prints in dot form.
512fn is_identifier(s: &str) -> bool {
513    let mut chars = s.chars();
514    match chars.next() {
515        Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
516        _ => return false,
517    }
518    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
519}
520
521fn argc(n: usize) -> Result<u8, String> {
522    u8::try_from(n).map_err(|_| "too many arguments (>255) for one call".to_string())
523}
524
525/// Does this expression already leave a `Value::Bool` on the stack? A condition
526/// that does needs no `TRUTHY` call: `JumpIfFalse` reads the boolean directly.
527///
528/// The gain is one host round-trip per condition evaluation — `for (let i = 0;
529/// i < n; i++)` paid it on every iteration — and it also puts the comparison
530/// immediately before the jump that consumes it, which is what fusevm's block
531/// JIT requires of a bool-producing op (`bool_is_consumed_in_place`).
532///
533/// Every arm listed here is a lowering that ends in a `Bool`: the relational
534/// ops go to `Op::Num{Lt,Le,Gt,Ge}` (the numeric hook's `relational` returns a
535/// Rust `bool`), the equality ops to `STRICT_EQ`/`LOOSE_EQ`, `in` to
536/// `CONTAINS`, `instanceof` to `INSTANCEOF`, and `!`/`!=`/`!==` end in
537/// `Op::LogNot`. Anything else — including `&&`/`||`/`??`, which evaluate to an
538/// OPERAND and not to a boolean — keeps the call.
539fn yields_bool(e: &Expr) -> bool {
540    match e {
541        Expr::True | Expr::False => true,
542        Expr::Unary(UnOp::Not, _) | Expr::Unary(UnOp::Delete, _) => true,
543        Expr::Binary(op, _, _) => matches!(
544            op,
545            BinOp::Lt
546                | BinOp::Le
547                | BinOp::Gt
548                | BinOp::Ge
549                | BinOp::EqEq
550                | BinOp::NeEq
551                | BinOp::EqEqEq
552                | BinOp::NeEqEq
553                | BinOp::In
554                | BinOp::InstanceOf
555        ),
556        _ => false,
557    }
558}
559
560impl Compiler {
561    // ── emit helpers ─────────────────────────────────────────────────────
562    fn name_const(&self, b: &mut ChunkBuilder, s: &str) {
563        let k = b.add_constant(Value::str(s));
564        b.emit(Op::LoadConst(k), 0);
565    }
566    fn strlit(&self, b: &mut ChunkBuilder, s: &str) {
567        let k = b.add_constant(Value::str(s));
568        b.emit(Op::LoadConst(k), 0);
569        b.emit(Op::CallBuiltin(ops::MKSTR, 1), 0);
570    }
571    fn tmp_name(&mut self, tag: &str) -> String {
572        let n = format!(".{tag}{}", self.tmp);
573        self.tmp += 1;
574        n
575    }
576
577    /// Emit MKFUNC for a compiled function template and leave the closure on the
578    /// stack.
579    fn emit_mkfunc(&self, b: &mut ChunkBuilder, def_id: usize) {
580        b.emit(Op::LoadInt(def_id as i64), 0);
581        b.emit(Op::CallBuiltin(ops::MKFUNC, 1), 0);
582    }
583
584    /// Emit the `var` hoisting for one function (or program) scope.
585    ///
586    /// A `var` binding exists from the moment its scope is entered, so
587    /// `f(){ x; var x = 1 }` reads `undefined` where a `let` would throw. The
588    /// walk therefore descends through every block, loop, `switch`, `try` and
589    /// label — `var` ignores block scope — but stops at a nested function, which
590    /// begins a scope of its own. Only the binding is created here; the
591    /// initialiser still runs where it is written.
592    ///
593    /// Emitted BEFORE [`Self::hoist_funcs`] so a function declaration overwrites
594    /// the `undefined` rather than the other way round, which is the order the
595    /// spec instantiates them in.
596    fn hoist_vars(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) -> Result<(), String> {
597        let mut names = Vec::new();
598        for s in stmts {
599            collect_var_names(s, &mut names);
600        }
601        for n in names {
602            // A slotted local is its slot, which already reads `undefined`
603            // before its first write, so there is no binding to create.
604            if self.slot_of(&n).is_some() {
605                continue;
606            }
607            self.name_const(b, &n);
608            b.emit(Op::CallBuiltin(ops::HOIST_VAR, 1), 0);
609            b.emit(Op::Pop, 0);
610        }
611        Ok(())
612    }
613
614    /// Declare every `let`/`const`/`class` named DIRECTLY in `stmts` as
615    /// uninitialized, at the top of the scope those statements form.
616    ///
617    /// Without it a lexical binding simply did not exist until its declaration
618    /// ran, so a read above it either found an OUTER binding of the same name —
619    /// `let x = 1; { x; let x = 2 }` read `1` where node throws — or reported
620    /// the name as undefined, which is the message for a typo rather than for
621    /// the temporal dead zone. Nested blocks are NOT walked: each opens its own
622    /// scope and hoists its own.
623    fn hoist_lexical(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) {
624        for s in stmts {
625            match &s.kind {
626                StmtKind::Decl {
627                    kind: DeclKind::Let | DeclKind::Const,
628                    decls,
629                } => {
630                    for d in decls {
631                        for name in binding_names(&d.target) {
632                            self.name_const(b, &name);
633                            b.emit(Op::CallBuiltin(ops::HOIST_TDZ, 1), 0);
634                            b.emit(Op::Pop, 0);
635                        }
636                    }
637                }
638                StmtKind::ClassDecl(c) => {
639                    if let Some(name) = &c.name {
640                        self.name_const(b, name);
641                        b.emit(Op::CallBuiltin(ops::HOIST_TDZ, 1), 0);
642                        b.emit(Op::Pop, 0);
643                    }
644                }
645                _ => {}
646            }
647        }
648    }
649
650    fn hoist_funcs(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) -> Result<(), String> {
651        self.hoist_funcs_in(b, stmts, false)
652    }
653
654    /// `in_block` marks a declaration that is nested in a BLOCK rather than at
655    /// the top of a function body or script. Only that case is affected by
656    /// strictness.
657    fn hoist_funcs_in(
658        &mut self,
659        b: &mut ChunkBuilder,
660        stmts: &[Stmt],
661        in_block: bool,
662    ) -> Result<(), String> {
663        for s in stmts {
664            if let StmtKind::FuncDecl {
665                name,
666                params,
667                body,
668                is_generator,
669                is_async,
670                span,
671            } = &s.kind
672            {
673                let def_id = self.build_function(name, params, body, *is_generator, *is_async)?;
674                self.functions[def_id].1.span = *span;
675                self.emit_mkfunc(b, def_id);
676                // A function declaration in a BLOCK is block-scoped (14.2.x);
677                // only Annex B.3.3's sloppy-mode legacy hoists it to the
678                // enclosing FUNCTION scope as well. Hoisting unconditionally
679                // made `function o() { { function g() {} } return typeof g }`
680                // answer `"function"` under `'use strict'`, where node says
681                // `"undefined"`.
682                let mode = if in_block && self.strict {
683                    BindMode::Lexical
684                } else {
685                    BindMode::Var
686                };
687                self.declare_as(b, &Expr::Ident(name.clone()), mode);
688            }
689        }
690        Ok(())
691    }
692
693    fn compile_stmts(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) -> Result<(), String> {
694        for s in stmts {
695            self.compile_stmt(b, s)?;
696        }
697        Ok(())
698    }
699
700    fn compile_stmt(&mut self, b: &mut ChunkBuilder, s: &Stmt) -> Result<(), String> {
701        if self.debug && s.line != 0 {
702            b.emit(Op::LoadInt(s.line as i64), s.line);
703            b.emit(Op::CallBuiltin(ops::DBG_LINE, 1), s.line);
704            b.emit(Op::Pop, s.line);
705        }
706        let line = s.line;
707        match &s.kind {
708            StmtKind::Expr(e) => {
709                self.compile_expr(b, e)?;
710                if self.completion {
711                    self.name_const(b, COMPLETION_SLOT);
712                    b.emit(Op::Swap, line);
713                    b.emit(Op::CallBuiltin(ops::SETLOCAL, 2), line);
714                }
715                b.emit(Op::Pop, line);
716            }
717            StmtKind::Empty => {}
718            StmtKind::FuncDecl { .. } => {} // hoisted at block entry
719            StmtKind::ClassDecl(node) => {
720                self.compile_class(b, node)?;
721                // Bind the class to its name in the current scope.
722                if let Some(name) = &node.name {
723                    self.declare(b, &Expr::Ident(name.clone()));
724                } else {
725                    b.emit(Op::Pop, line);
726                }
727            }
728            StmtKind::Decl { kind, decls } => {
729                let mode = bind_mode(*kind);
730                for d in decls {
731                    // `var x;` with no initialiser names a binding that scope
732                    // entry already created, and must NOT reset it — in
733                    // `function f(a) { var a; }` the parameter stands.
734                    if d.init.is_none() && *kind == DeclKind::Var {
735                        continue;
736                    }
737                    match &d.init {
738                        Some(v) => {
739                            self.compile_expr(b, v)?;
740                            // Name inference: `const f = () => {}` / `= function(){}`
741                            // / `= class {}` gives the function/class the name `f`.
742                            if let Expr::Ident(name) = &d.target {
743                                self.infer_name(b, v, name);
744                            }
745                        }
746                        None => {
747                            b.emit(Op::LoadUndef, line);
748                        }
749                    }
750                    // 13.3.1.1: a `var`/`let`/`const` may not BIND `eval` or
751                    // `arguments` in strict code.
752                    if self.strict {
753                        for n in binding_names(&d.target) {
754                            if RESERVED_IN_STRICT.contains(&n.as_str()) {
755                                return Err(strict_reserved_error());
756                            }
757                        }
758                    }
759                    self.destructure_src = d.init.as_ref().and_then(destructure_source_text);
760                    self.destructure_is_decl = true;
761                    let r = self.compile_bind(b, &d.target, mode);
762                    self.destructure_src = None;
763                    self.destructure_is_decl = false;
764                    r?;
765                }
766            }
767            StmtKind::Block(body) => {
768                // A block that declares nothing lexical has nothing to put in a
769                // scope, and opening one costs an `EnvData` allocation and free
770                // every time control enters the block — once per iteration when
771                // the block is a loop body, which is where most of them are.
772                let scoped = crate::capture::block_needs_scope(body);
773                if scoped {
774                    self.emit_push_scope(b);
775                }
776                self.hoist_lexical(b, body);
777                self.hoist_funcs_in(b, body, true)?;
778                self.compile_stmts(b, body)?;
779                if scoped {
780                    self.emit_pop_scope(b);
781                }
782            }
783            StmtKind::If { test, cons, alt } => {
784                self.reset_completion(b, line);
785                self.compile_if(b, test, cons, alt)?
786            }
787            StmtKind::While { test, body } => {
788                self.reset_completion(b, line);
789                self.compile_while(b, test, body)?
790            }
791            StmtKind::DoWhile { body, test } => {
792                self.reset_completion(b, line);
793                self.compile_do_while(b, body, test)?
794            }
795            StmtKind::For {
796                init,
797                test,
798                update,
799                body,
800            } => {
801                self.reset_completion(b, line);
802                self.compile_for(b, init, test, update, body)?
803            }
804            StmtKind::ForOf {
805                decl_kind,
806                target,
807                iter,
808                body,
809                is_await,
810            } => {
811                self.reset_completion(b, line);
812                let mode = decl_kind.map(bind_mode).unwrap_or(BindMode::Assign);
813                if *is_await {
814                    self.compile_for_await(b, mode, target, iter, body)?
815                } else {
816                    self.compile_for_of(b, mode, target, iter, body)?
817                }
818            }
819            StmtKind::ForIn {
820                decl_kind,
821                target,
822                object,
823                body,
824            } => {
825                self.reset_completion(b, line);
826                let mode = decl_kind.map(bind_mode).unwrap_or(BindMode::Assign);
827                self.compile_for_in(b, mode, target, object, body)?
828            }
829            StmtKind::Switch { disc, cases } => {
830                self.reset_completion(b, line);
831                self.compile_switch(b, disc, cases)?
832            }
833            StmtKind::Return(e) => {
834                match e {
835                    Some(e) => self.compile_expr(b, e)?,
836                    None => {
837                        b.emit(Op::LoadUndef, line);
838                    }
839                }
840                // A `return` out of a `for…of` is an abrupt completion, and
841                // 7.4.9 `IteratorClose` runs the iterator's `return` for it —
842                // which is what makes a generator's `finally` run. `break` and
843                // `continue` already closed theirs; a `return` walked away and
844                // left the iterator suspended forever.
845                self.emit_close_iters_under_value(b);
846                b.emit(Op::CallBuiltin(ops::SIG_RETURN, 1), line);
847            }
848            StmtKind::Labeled { label, body } => self.compile_labeled(b, label, body)?,
849            StmtKind::Break(label) => {
850                let idx = match label {
851                    // `break outer`: the nearest enclosing context carrying that label.
852                    Some(name) => self
853                        .loops
854                        .iter()
855                        .rposition(|c| c.label.as_deref() == Some(name.as_str()))
856                        .ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?,
857                    None => self
858                        .loops
859                        .len()
860                        .checked_sub(1)
861                        .ok_or("SyntaxError: 'break' outside loop")?,
862                };
863                if idx >= self.chunk_loop_base {
864                    self.emit_unwind_scopes(b, self.loops[idx].break_depth);
865                    self.emit_close_iters(b, self.loops[idx].iter_depth);
866                    let j = b.emit(Op::Jump(0), line);
867                    self.loops[idx].breaks.push(j);
868                } else {
869                    self.emit_signal_jump(b, ops::SIG_BREAK, label.as_deref(), line);
870                }
871            }
872            StmtKind::Continue(label) => {
873                let idx = match label {
874                    // `continue outer`: the labeled loop (a label on a non-loop
875                    // cannot catch `continue`).
876                    Some(name) => self
877                        .loops
878                        .iter()
879                        .rposition(|c| {
880                            c.catches_continue && c.label.as_deref() == Some(name.as_str())
881                        })
882                        .ok_or_else(|| {
883                            format!("SyntaxError: Undefined label '{name}' for continue")
884                        })?,
885                    None => self
886                        .loops
887                        .iter()
888                        .rposition(|c| c.catches_continue)
889                        .ok_or("SyntaxError: 'continue' outside loop")?,
890                };
891                if idx >= self.chunk_loop_base {
892                    self.emit_unwind_scopes(b, self.loops[idx].continue_depth);
893                    self.emit_close_iters(b, self.loops[idx].iter_depth);
894                    let j = b.emit(Op::Jump(0), line);
895                    self.loops[idx].continues.push(j);
896                } else {
897                    self.emit_signal_jump(b, ops::SIG_CONTINUE, label.as_deref(), line);
898                }
899            }
900            StmtKind::Throw(e) => {
901                self.compile_expr(b, e)?;
902                b.emit(Op::CallBuiltin(ops::THROW, 1), line);
903            }
904            StmtKind::Try {
905                block,
906                handler,
907                finalizer,
908            } => {
909                self.reset_completion(b, line);
910                self.compile_try(b, block, handler, finalizer)?
911            }
912        }
913        Ok(())
914    }
915
916    // ── binding / assignment ─────────────────────────────────────────────
917    /// Store the value on top of the stack into `target`. `declare` chooses
918    /// `DECLARE` (new binding) vs `SETLOCAL` (existing binding / global).
919    fn compile_bind(
920        &mut self,
921        b: &mut ChunkBuilder,
922        target: &Expr,
923        declare: BindMode,
924    ) -> Result<(), String> {
925        // A DESTRUCTURING target named `eval` or `arguments` is refused in
926        // strict code too, with its own wording — `({ a: eval } = {})` slipped
927        // past the assignment check because it never builds an `Expr::Assign`.
928        if self.strict {
929            if let Expr::Ident(n) = target {
930                if declare == BindMode::Assign && RESERVED_IN_STRICT.contains(&n.as_str()) {
931                    return Err("SyntaxError: Invalid destructuring assignment target".to_string());
932                }
933            }
934        }
935        match target {
936            Expr::Ident(_) => {
937                if declare == BindMode::Assign {
938                    self.store_simple(b, target)?;
939                } else {
940                    self.declare_as(b, target, declare);
941                }
942            }
943            Expr::Member { .. } | Expr::Index { .. } => {
944                self.store_simple(b, target)?;
945            }
946            Expr::Array(items) => self.destructure_array(b, items, declare)?,
947            Expr::Object(props) => self.destructure_object(b, props, declare)?,
948            Expr::Assign { target, value, .. } => {
949                // Pattern element with a default: use it when TOS is undefined.
950                b.emit(Op::Dup, 0);
951                b.emit(Op::LoadUndef, 0);
952                b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
953                let jf = b.emit(Op::JumpIfFalse(0), 0);
954                b.emit(Op::Pop, 0); // drop the undefined
955                self.compile_expr(b, value)?;
956                // 8.6.3 / 14.3.3: a destructuring default whose target is a
957                // single binding identifier names an anonymous function after
958                // it — `const {a = function(){}} = {}` gives `a.name === "a"`.
959                if let Expr::Ident(n) = &**target {
960                    self.infer_name(b, value, n);
961                }
962                let end = b.current_pos();
963                b.patch_jump(jf, end);
964                self.compile_bind(b, target, declare)?;
965            }
966            _ => return Err("SyntaxError: invalid assignment target".into()),
967        }
968        Ok(())
969    }
970
971    /// Emit a `DECLARE` of a simple name binding, consuming TOS value.
972    fn declare(&self, b: &mut ChunkBuilder, target: &Expr) {
973        self.declare_as(b, target, BindMode::Lexical);
974    }
975
976    /// Emit the declaration op matching `mode`: block-scoped for `let`/`const`,
977    /// function-scoped for `var` and hoisted function declarations.
978    fn declare_as(&self, b: &mut ChunkBuilder, target: &Expr, mode: BindMode) {
979        if let Expr::Ident(n) = target {
980            // A slotted local has no scope entry to declare into: the binding IS
981            // the store.
982            if let Some(slot) = self.slot_of(n) {
983                b.emit(Op::SetSlot(slot), 0);
984                return;
985            }
986            let op = match mode {
987                BindMode::Var => ops::DECLARE_VAR,
988                BindMode::Const => ops::DECLARE_CONST,
989                _ => ops::DECLARE,
990            };
991            self.name_const(b, n);
992            b.emit(Op::Swap, 0);
993            b.emit(Op::CallBuiltin(op, 2), 0);
994            b.emit(Op::Pop, 0);
995        }
996    }
997
998    /// Emit `throw new TypeError("Assignment to constant variable.")`.
999    ///
1000    /// A store to a `const` is a RUNTIME error, not a parse error — the spec
1001    /// puts it in SetMutableBinding (8.5.2), so `try { const c=1; c=2 } catch {}`
1002    /// has to catch it. Emitting the throw in place of the store gives exactly
1003    /// that, and costs nothing for every store that is not to a const.
1004    fn throw_const_assignment(&mut self, b: &mut ChunkBuilder) {
1005        let e = Expr::New {
1006            callee: Box::new(Expr::Ident("TypeError".into())),
1007            args: vec![Expr::Str("Assignment to constant variable.".into())],
1008        };
1009        // `New` of a known builtin with a literal argument cannot fail to
1010        // compile, so the error path is unreachable rather than swallowed.
1011        if self.compile_expr(b, &e).is_ok() {
1012            b.emit(Op::CallBuiltin(ops::THROW, 1), 0);
1013        }
1014    }
1015
1016    /// Store TOS into an lvalue (Ident/Member/Index), leaving nothing.
1017    fn store_simple(&mut self, b: &mut ChunkBuilder, target: &Expr) -> Result<(), String> {
1018        match target {
1019            Expr::Ident(n) => {
1020                // A slotted binding never reaches the host's scope chain, so the
1021                // host's immutable-binding check cannot see it. The slot plan is
1022                // exact about which names are const (one declaration per name,
1023                // unreachable from another chunk, simple identifiers only), so
1024                // the store is rejected here instead — at run time, as the spec
1025                // requires, since `try { const c=1; c=2 } catch {}` must CATCH
1026                // this rather than fail to parse.
1027                if self.slots.consts.contains(n) {
1028                    b.emit(Op::Pop, 0); // drop the value that will never be stored
1029                    self.throw_const_assignment(b);
1030                    return Ok(());
1031                }
1032                if let Some(slot) = self.slot_of(n) {
1033                    b.emit(Op::SetSlot(slot), 0);
1034                    return Ok(());
1035                }
1036                self.name_const(b, n);
1037                b.emit(Op::Swap, 0);
1038                // `PutValue` (6.2.5.6) on an unresolvable reference: strict code
1039                // throws `ReferenceError`, sloppy code creates a global.
1040                let op = if self.strict {
1041                    ops::SETLOCAL_STRICT
1042                } else {
1043                    ops::SETLOCAL
1044                };
1045                b.emit(Op::CallBuiltin(op, 2), 0);
1046                b.emit(Op::Pop, 0);
1047            }
1048            Expr::Member {
1049                object, property, ..
1050            } => {
1051                self.compile_expr(b, object)?; // [value, recv]
1052                self.name_const(b, property); // [value, recv, name]
1053                b.emit(Op::Rot, 0); // [recv, name, value]
1054                b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0);
1055                b.emit(Op::Pop, 0);
1056            }
1057            Expr::Index { object, index, .. } => {
1058                self.compile_expr(b, object)?; // [value, recv]
1059                self.compile_expr(b, index)?; // [value, recv, idx]
1060                b.emit(Op::Rot, 0); // [recv, idx, value]
1061                b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0);
1062                b.emit(Op::Pop, 0);
1063            }
1064            _ => return Err("SyntaxError: invalid assignment target".into()),
1065        }
1066        Ok(())
1067    }
1068
1069    fn destructure_array(
1070        &mut self,
1071        b: &mut ChunkBuilder,
1072        items: &[Expr],
1073        declare: BindMode,
1074    ) -> Result<(), String> {
1075        let star_idx = items
1076            .iter()
1077            .position(|e| matches!(e, Expr::Spread(_)))
1078            .map(|i| i as i64)
1079            .unwrap_or(-1);
1080        b.emit(Op::LoadInt(items.len() as i64), 0);
1081        b.emit(Op::LoadInt(star_idx), 0);
1082        let at = b.current_pos();
1083        b.emit(Op::CallBuiltin(ops::UNPACK, 3), 0); // pushes items[0]..items[n-1], items[0] on top
1084                                                    // Destructuring a non-iterable names the SOURCE the same way `for-of`
1085                                                    // does: `const [x] = a` reports `a is not iterable`. The text is the one
1086                                                    // the object-pattern error already carries; a synthesized `.param<n>`
1087                                                    // slot has no source spelling and is skipped.
1088                                                    // …and only for a plain IDENTIFIER source. Node reports the TYPE for
1089                                                    // every other shape — a member, an index, a call, a nested pattern —
1090                                                    // even though the text exists, so recording one there would name an
1091                                                    // expression node never names.
1092        if let Some(src) = self
1093            .destructure_src
1094            .clone()
1095            .filter(|_| self.destructure_is_decl)
1096            .filter(|t| !t.starts_with('.'))
1097            .filter(|t| {
1098                t.starts_with('{')
1099                    || t.chars()
1100                        .all(|c| c.is_alphanumeric() || c == '_' || c == '$')
1101            })
1102        {
1103            self.call_sites.push((at, src));
1104        }
1105        for it in items {
1106            match it {
1107                // An elided target position (`const [a, , b] = xs`) still
1108                // consumes its unpacked value; nothing is bound to it.
1109                Expr::Hole | Expr::Undefined => {
1110                    b.emit(Op::Pop, 0);
1111                }
1112                Expr::Spread(inner) => self.compile_bind(b, inner, declare)?,
1113                _ => self.compile_bind(b, it, declare)?,
1114            }
1115        }
1116        Ok(())
1117    }
1118
1119    fn destructure_object(
1120        &mut self,
1121        b: &mut ChunkBuilder,
1122        props: &[Prop],
1123        declare: BindMode,
1124    ) -> Result<(), String> {
1125        // A NULLISH source: node names the pattern's first property and the
1126        // source expression rather than reporting the property read that failed.
1127        // Which of the two wordings it uses is decided by that first element —
1128        // a plain property names itself, a rest / computed key / empty pattern
1129        // does not, and one carrying a DEFAULT falls through to the ordinary
1130        // read error because the default is what reads it.
1131        // A leading `.` marks a compiler-generated name (a parameter slot), which
1132        // is not something the user wrote and must not be quoted back at them.
1133        if let Some(src) = self.destructure_src.take().filter(|s| !s.starts_with('.')) {
1134            let first = match props.first() {
1135                Some(Prop::KeyValue {
1136                    key: Expr::Str(k),
1137                    value,
1138                    computed: false,
1139                }) if !matches!(value, Expr::Assign { .. }) => Some(k.clone()),
1140                // A COMPUTED first key names nothing — evaluating it is what
1141                // would fail — and neither does a rest or an empty pattern.
1142                None | Some(Prop::Spread(_)) | Some(Prop::KeyValue { computed: true, .. }) => None,
1143                // A first property carrying a DEFAULT falls through: the default
1144                // is what performs the read, so node reports the read.
1145                _ => return self.destructure_object_body(b, props, declare),
1146            };
1147            self.emit_destructure_guard(b, &src, first.as_deref());
1148        }
1149        self.destructure_object_body(b, props, declare)
1150    }
1151
1152    /// `throw new TypeError(…)` when TOS is nullish, leaving TOS untouched
1153    /// otherwise.
1154    fn emit_destructure_guard(&mut self, b: &mut ChunkBuilder, src: &str, first: Option<&str>) {
1155        for (is_null, word) in [(true, "null"), (false, "undefined")] {
1156            b.emit(Op::Dup, 0);
1157            if is_null {
1158                b.emit(Op::CallBuiltin(ops::LOAD_NULL, 0), 0);
1159            } else {
1160                b.emit(Op::LoadUndef, 0);
1161            }
1162            b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
1163            let ok = b.emit(Op::JumpIfFalse(0), 0);
1164            let msg = match first {
1165                Some(k) => {
1166                    format!("Cannot destructure property '{k}' of '{src}' as it is {word}.")
1167                }
1168                None => format!("Cannot destructure '{src}' as it is {word}."),
1169            };
1170            // A real `TypeError` instance, not a bare string — the handler
1171            // reads `e.constructor.name` and `e.stack`.
1172            let e = Expr::New {
1173                callee: Box::new(Expr::Ident("TypeError".into())),
1174                args: vec![Expr::Str(msg)],
1175            };
1176            if self.compile_expr(b, &e).is_ok() {
1177                b.emit(Op::CallBuiltin(ops::THROW, 1), 0);
1178            }
1179            let end = b.current_pos();
1180            b.patch_jump(ok, end);
1181        }
1182    }
1183
1184    fn destructure_object_body(
1185        &mut self,
1186        b: &mut ChunkBuilder,
1187        props: &[Prop],
1188        declare: BindMode,
1189    ) -> Result<(), String> {
1190        // Object value on TOS; keep it, read each key, bind, then drop.
1191        let obj_tmp = self.tmp_name("destr");
1192        self.name_const(b, &obj_tmp);
1193        b.emit(Op::Swap, 0);
1194        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1195        b.emit(Op::Pop, 0);
1196        // The keys a `...rest` must EXCLUDE. A statically-spelled key is known
1197        // here; a computed one (`{ [k]: v, ...rest }`) is only a value at run
1198        // time, and only collecting the static ones left every computed key in
1199        // the rest object — `const { [k]: y, ...r } = { a: 1, b: 2 }` with
1200        // `k === 'b'` put `b` in BOTH `y` and `r`.
1201        //
1202        // A computed key must still be evaluated exactly once, so its value is
1203        // stashed in a temporary as it is computed and the rest reads that
1204        // temporary rather than re-running the expression.
1205        enum Excl {
1206            Static(String),
1207            Computed(String),
1208        }
1209        let has_rest = props.iter().any(|p| matches!(p, Prop::Spread(_)));
1210        let mut named: Vec<Excl> = Vec::new();
1211        for p in props {
1212            match p {
1213                Prop::KeyValue { key, value, .. } => {
1214                    // Load obj, read key.
1215                    self.load_local(b, &obj_tmp);
1216                    self.compile_expr(b, key)?; // [obj, key]
1217                    match key {
1218                        Expr::Str(s) => named.push(Excl::Static(s.clone())),
1219                        // Only worth a temporary when a rest will read it.
1220                        _ if has_rest => {
1221                            let t = self.tmp_name("destrkey");
1222                            b.emit(Op::Dup, 0); // [obj, key, key]
1223                            self.name_const(b, &t); // [obj, key, key, name]
1224                            b.emit(Op::Swap, 0); // [obj, key, name, key]
1225                            b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0); // [obj, key, _]
1226                            b.emit(Op::Pop, 0); // [obj, key]
1227                            named.push(Excl::Computed(t));
1228                        }
1229                        _ => {}
1230                    }
1231                    b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0); // [value]
1232                    self.compile_bind(b, value, declare)?;
1233                }
1234                Prop::Spread(target) => {
1235                    self.load_local(b, &obj_tmp);
1236                    for k in &named {
1237                        match k {
1238                            Excl::Static(s) => self.strlit(b, s),
1239                            Excl::Computed(t) => self.load_local(b, t),
1240                        }
1241                    }
1242                    b.emit(Op::CallBuiltin(ops::MKARR, argc(named.len())?), 0);
1243                    b.emit(Op::CallBuiltin(ops::OBJ_REST, 2), 0); // [rest_object]
1244                    self.compile_bind(b, target, declare)?;
1245                }
1246                // Accessors never appear in a destructuring pattern.
1247                Prop::Accessor { .. } => {}
1248            }
1249        }
1250        Ok(())
1251    }
1252
1253    fn load_local(&self, b: &mut ChunkBuilder, name: &str) {
1254        if let Some(slot) = self.slot_of(name) {
1255            b.emit(Op::GetSlot(slot), 0);
1256            return;
1257        }
1258        self.name_const(b, name);
1259        b.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0);
1260    }
1261
1262    /// The frame slot holding `name` in the chunk being emitted, if it has one.
1263    fn slot_of(&self, name: &str) -> Option<u16> {
1264        self.slots.table.get(name).copied()
1265    }
1266
1267    /// The slot for `name` if it also provably holds a Number, so `++`/`--` can
1268    /// be a native add rather than a `NUM_STEP` round-trip through the host.
1269    fn numeric_slot_of(&self, name: &str) -> Option<u16> {
1270        self.slots
1271            .numeric
1272            .contains(name)
1273            .then(|| self.slot_of(name))
1274            .flatten()
1275    }
1276
1277    // ── control flow ─────────────────────────────────────────────────────
1278    fn compile_condition(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
1279        self.compile_expr(b, e)?;
1280        if !yields_bool(e) {
1281            b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
1282        }
1283        Ok(())
1284    }
1285
1286    fn compile_if(
1287        &mut self,
1288        b: &mut ChunkBuilder,
1289        test: &Expr,
1290        cons: &Stmt,
1291        alt: &Option<Box<Stmt>>,
1292    ) -> Result<(), String> {
1293        self.compile_condition(b, test)?;
1294        let jfalse = b.emit(Op::JumpIfFalse(0), 0);
1295        self.compile_stmt(b, cons)?;
1296        if let Some(alt) = alt {
1297            let jend = b.emit(Op::Jump(0), 0);
1298            let else_start = b.current_pos();
1299            b.patch_jump(jfalse, else_start);
1300            self.compile_stmt(b, alt)?;
1301            let end = b.current_pos();
1302            b.patch_jump(jend, end);
1303        } else {
1304            let end = b.current_pos();
1305            b.patch_jump(jfalse, end);
1306        }
1307        Ok(())
1308    }
1309
1310    /// `label: stmt`. If the body is a loop, the label rides into that loop's
1311    /// `LoopCtx` (so labeled `break`/`continue` target it); otherwise a break-only
1312    /// context spans the body so `break label` can jump past it.
1313    fn compile_labeled(
1314        &mut self,
1315        b: &mut ChunkBuilder,
1316        label: &str,
1317        body: &Stmt,
1318    ) -> Result<(), String> {
1319        if matches!(
1320            body.kind,
1321            StmtKind::While { .. }
1322                | StmtKind::DoWhile { .. }
1323                | StmtKind::For { .. }
1324                | StmtKind::ForOf { .. }
1325                | StmtKind::ForIn { .. }
1326        ) {
1327            self.pending_label = Some(label.to_string());
1328            self.compile_stmt(b, body)?;
1329            // The loop claimed it; clear any residue defensively.
1330            self.pending_label = None;
1331        } else {
1332            self.loops.push(LoopCtx {
1333                breaks: Vec::new(),
1334                continues: Vec::new(),
1335                break_depth: self.scope_depth,
1336                continue_depth: self.scope_depth,
1337                iter_depth: self.iter_depth,
1338                catches_continue: false,
1339                label: Some(label.to_string()),
1340            });
1341            self.compile_stmt(b, body)?;
1342            let ctx = self.loops.pop().unwrap();
1343            let end = b.current_pos();
1344            for br in ctx.breaks {
1345                b.patch_jump(br, end);
1346            }
1347            self.redispatch_after_loop(b);
1348        }
1349        Ok(())
1350    }
1351
1352    /// After a loop/switch exits, a signal raised deeper in this chunk may still be
1353    /// pending (a LABELED `break`/`continue` for an OUTER loop). Re-dispatch it one
1354    /// level out. Emitted only when this chunk actually raises signals.
1355    fn redispatch_after_loop(&mut self, b: &mut ChunkBuilder) {
1356        if self.chunk_signals {
1357            self.emit_signal_dispatch(b);
1358        }
1359    }
1360
1361    /// `while (test) body`, lowered ROTATED: the test is emitted once as an entry
1362    /// guard and once at the bottom, so the loop closes with a CONDITIONAL
1363    /// backward branch rather than an unconditional `Jump` back to a test at the
1364    /// top.
1365    ///
1366    /// That shape is what fusevm's tracing JIT needs — it only closes a trace on
1367    /// a conditional backward branch. Emitted the other way, `--tiers` reported
1368    /// `trace-eligible=true traced=false` and `reaches native code false` for
1369    /// every `for` and `while` this frontend produced, while the same arithmetic
1370    /// written as `do { … } while (…)` — the one loop form that already ended in
1371    /// a conditional branch — reported `traced=true`. Measured on a debug build:
1372    /// `for (let i = 0; i < 3000000; i++) s += i` took 5.76s of user CPU
1373    /// unrotated and 0.02s rotated.
1374    ///
1375    /// Evaluation order and count are unchanged: a top-test loop runs the test
1376    /// `n + 1` times for `n` iterations, and so does this — one entry test, then
1377    /// one after each pass. Rotation costs one copy of the condition's code and
1378    /// saves one jump per iteration.
1379    fn compile_while(
1380        &mut self,
1381        b: &mut ChunkBuilder,
1382        test: &Expr,
1383        body: &Stmt,
1384    ) -> Result<(), String> {
1385        self.compile_condition(b, test)?;
1386        let jfalse = b.emit(Op::JumpIfFalse(0), 0);
1387        let top = b.current_pos();
1388        self.loops.push(LoopCtx {
1389            breaks: Vec::new(),
1390            continues: Vec::new(),
1391            break_depth: self.scope_depth,
1392            continue_depth: self.scope_depth,
1393            iter_depth: self.iter_depth,
1394            catches_continue: true,
1395            label: self.pending_label.take(),
1396        });
1397        self.compile_stmt(b, body)?;
1398        // `continue` re-tests the condition, which is now the BOTTOM copy of it.
1399        let cont_target = b.current_pos();
1400        self.compile_condition(b, test)?;
1401        b.emit(Op::JumpIfTrue(top), 0);
1402        let ctx = self.loops.pop().unwrap();
1403        for c in ctx.continues {
1404            b.patch_jump(c, cont_target);
1405        }
1406        let end = b.current_pos();
1407        b.patch_jump(jfalse, end);
1408        for br in ctx.breaks {
1409            b.patch_jump(br, end);
1410        }
1411        self.redispatch_after_loop(b);
1412        Ok(())
1413    }
1414
1415    fn compile_do_while(
1416        &mut self,
1417        b: &mut ChunkBuilder,
1418        body: &Stmt,
1419        test: &Expr,
1420    ) -> Result<(), String> {
1421        let start = b.current_pos();
1422        self.loops.push(LoopCtx {
1423            breaks: Vec::new(),
1424            continues: Vec::new(),
1425            break_depth: self.scope_depth,
1426            continue_depth: self.scope_depth,
1427            iter_depth: self.iter_depth,
1428            catches_continue: true,
1429            label: self.pending_label.take(),
1430        });
1431        self.compile_stmt(b, body)?;
1432        let cont_target = b.current_pos();
1433        self.compile_condition(b, test)?;
1434        b.emit(Op::JumpIfTrue(start), 0);
1435        let ctx = self.loops.pop().unwrap();
1436        for c in ctx.continues {
1437            b.patch_jump(c, cont_target);
1438        }
1439        let end = b.current_pos();
1440        for br in ctx.breaks {
1441            b.patch_jump(br, end);
1442        }
1443        self.redispatch_after_loop(b);
1444        Ok(())
1445    }
1446
1447    fn compile_for(
1448        &mut self,
1449        b: &mut ChunkBuilder,
1450        init: &Option<Box<Stmt>>,
1451        test: &Option<Expr>,
1452        update: &Option<Expr>,
1453        body: &Stmt,
1454    ) -> Result<(), String> {
1455        // A `let`/`const` head is scoped to the loop AND re-bound per iteration, so
1456        // a closure made in one pass keeps that pass's value (ForBodyEvaluation's
1457        // CreatePerIterationEnvironment). A `var` head belongs to the function.
1458        let lexical_head = matches!(
1459            init.as_deref(),
1460            Some(Stmt {
1461                kind: StmtKind::Decl {
1462                    kind: DeclKind::Let | DeclKind::Const,
1463                    ..
1464                },
1465                ..
1466            })
1467        );
1468        // The loop's own scope is not optional — it is what keeps `let i` from
1469        // leaking past the loop or clobbering an outer `i`. The per-iteration
1470        // COPY of that scope is: only code that can CAPTURE a binding can tell
1471        // one copy per pass from one binding mutated in place, and the copy is a
1472        // whole-scope clone every iteration. A 5M-iteration counting loop spent
1473        // 17% of its samples cloning scopes that nothing could observe.
1474        let per_iteration = lexical_head;
1475        let copy_per_iteration = lexical_head
1476            && (crate::capture::stmt_captures(body)
1477                || init.as_deref().is_some_and(crate::capture::stmt_captures)
1478                || test.as_ref().is_some_and(crate::capture::expr_captures)
1479                || update.as_ref().is_some_and(crate::capture::expr_captures));
1480        if per_iteration {
1481            self.emit_push_scope(b);
1482            // The head's own bindings are in scope — and in their dead zone —
1483            // for the head itself: `for (let i = i; …)` is a ReferenceError.
1484            if let Some(init) = init.as_deref() {
1485                self.hoist_lexical(b, std::slice::from_ref(init));
1486            }
1487        }
1488        if let Some(init) = init {
1489            self.compile_stmt(b, init)?;
1490        }
1491        if copy_per_iteration {
1492            self.emit_copy_scope(b);
1493        }
1494        // Rotated, for the reason `compile_while` documents: the test as an entry
1495        // guard plus a conditional backward branch at the bottom.
1496        let jfalse = match test {
1497            Some(t) => {
1498                self.compile_condition(b, t)?;
1499                Some(b.emit(Op::JumpIfFalse(0), 0))
1500            }
1501            None => None,
1502        };
1503        let top = b.current_pos();
1504        self.loops.push(LoopCtx {
1505            breaks: Vec::new(),
1506            continues: Vec::new(),
1507            break_depth: self.scope_depth,
1508            continue_depth: self.scope_depth,
1509            iter_depth: self.iter_depth,
1510            catches_continue: true,
1511            label: self.pending_label.take(),
1512        });
1513        self.compile_stmt(b, body)?;
1514        let cont_target = b.current_pos();
1515        if copy_per_iteration {
1516            // Fresh copy BEFORE the update, so the update advances the NEXT pass's
1517            // binding and the one just captured keeps this pass's value.
1518            self.emit_copy_scope(b);
1519        }
1520        if let Some(u) = update {
1521            self.compile_expr(b, u)?;
1522            b.emit(Op::Pop, 0);
1523        }
1524        match test {
1525            Some(t) => {
1526                self.compile_condition(b, t)?;
1527                b.emit(Op::JumpIfTrue(top), 0);
1528            }
1529            // `for (;;)` has no test to branch on, so the back edge is a
1530            // constant-true CONDITIONAL branch rather than an unconditional
1531            // `Jump`. The distinction is not cosmetic: fusevm's trace compiler
1532            // only ever installs a trace closed by `JumpIfTrue`/`JumpIfFalse`
1533            // and silently declines an `Op::Jump` close, so `for (;;)` stayed
1534            // interpreted while the identical `while (true)` — which already
1535            // emitted `LoadTrue; JumpIfTrue` — reached native code. Measured on
1536            // a debug build, 3M iterations of `s += i`: 4.26s against 0.02s.
1537            None => {
1538                b.emit(Op::LoadTrue, 0);
1539                b.emit(Op::JumpIfTrue(top), 0);
1540            }
1541        }
1542        let ctx = self.loops.pop().unwrap();
1543        for c in ctx.continues {
1544            b.patch_jump(c, cont_target);
1545        }
1546        let end = b.current_pos();
1547        if let Some(jf) = jfalse {
1548            b.patch_jump(jf, end);
1549        }
1550        for br in ctx.breaks {
1551            b.patch_jump(br, end);
1552        }
1553        if per_iteration {
1554            self.emit_pop_scope(b);
1555        }
1556        self.redispatch_after_loop(b);
1557        Ok(())
1558    }
1559
1560    fn compile_for_of(
1561        &mut self,
1562        b: &mut ChunkBuilder,
1563        declare: BindMode,
1564        target: &Expr,
1565        iter: &Expr,
1566        body: &Stmt,
1567    ) -> Result<(), String> {
1568        self.compile_expr(b, iter)?;
1569        let at = b.current_pos();
1570        b.emit(Op::CallBuiltin(ops::GETITER, 1), 0); // [iterator]
1571                                                     // A `for-of` over a non-iterable names the SOURCE expression:
1572                                                     // `for (const x of a)` reports `a is not iterable`, not the rendering
1573                                                     // of whatever `a` held. Same table the callee-naming uses.
1574                                                     //
1575                                                     // A CALL source gets V8's combined wording, since either half could be
1576                                                     // at fault: `for (const x of f())` is `f is not a function or its
1577                                                     // return value is not iterable`. Recording the whole subject rather
1578                                                     // than a marker keeps the runtime side one string substitution.
1579        match iter {
1580            Expr::Call { func, .. } => {
1581                let name = callee_text(func).unwrap_or_else(|| "(intermediate value)".into());
1582                self.call_sites
1583                    .push((at, format!("{name} is not a function or its return value")));
1584            }
1585            _ => self.note_call_site(at, iter),
1586        }
1587        self.iter_depth += 1;
1588        let r = self.loop_over(b, declare, target, body);
1589        self.iter_depth -= 1;
1590        r
1591    }
1592
1593    fn compile_for_in(
1594        &mut self,
1595        b: &mut ChunkBuilder,
1596        declare: BindMode,
1597        target: &Expr,
1598        object: &Expr,
1599        body: &Stmt,
1600    ) -> Result<(), String> {
1601        // The object is kept in a temp for the whole loop: each key is re-checked
1602        // against it just before it is visited, because the body can delete one
1603        // (14.7.5.10 enumerates lazily, so a key deleted before its turn is never
1604        // visited). Without that, `for (const k in d) delete d.z` still visited
1605        // `z`.
1606        let obj_tmp = self.tmp_name("forin");
1607        self.compile_expr(b, object)?;
1608        self.name_const(b, &obj_tmp);
1609        b.emit(Op::Swap, 0);
1610        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0); // [obj]
1611        b.emit(Op::CallBuiltin(ops::FORIN_KEYS, 1), 0); // [keys_array]
1612        b.emit(Op::CallBuiltin(ops::GETITER, 1), 0); // [iterator]
1613        self.iter_depth += 1;
1614        let r = self.loop_over_inner(b, declare, target, body, Some(obj_tmp));
1615        self.iter_depth -= 1;
1616        r
1617    }
1618
1619    /// `for await (target of iterable) body`. Obtains an async iterator, then each
1620    /// pass `await`s a `{value, done}` step (a native async iterator's promise, or
1621    /// the sync fallback's per-value await). The iterator lives in a temp local.
1622    fn compile_for_await(
1623        &mut self,
1624        b: &mut ChunkBuilder,
1625        declare: BindMode,
1626        target: &Expr,
1627        iter: &Expr,
1628        body: &Stmt,
1629    ) -> Result<(), String> {
1630        let iter_tmp = self.tmp_name("aiter");
1631        self.compile_expr(b, iter)?;
1632        let at = b.current_pos();
1633        b.emit(Op::CallBuiltin(ops::GET_ASYNC_ITER, 1), 0); // [iterator]
1634                                                            // `for await` over a non-iterable names the source AND says ASYNC:
1635                                                            // `for await (const x of o)` is `o is not async iterable`. Recording
1636                                                            // the whole subject keeps the runtime side one substitution, as the
1637                                                            // call-source wording above does.
1638        self.note_call_site(at, iter);
1639        self.name_const(b, &iter_tmp);
1640        b.emit(Op::Swap, 0);
1641        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1642        b.emit(Op::Pop, 0);
1643        let start = b.current_pos();
1644        // step = await ASYNC_STEP(iterator)  -> {value, done}
1645        self.load_local(b, &iter_tmp);
1646        b.emit(Op::CallBuiltin(ops::ASYNC_STEP, 1), 0); // [stepPromise]
1647        b.emit(Op::CallBuiltin(ops::AWAIT, 1), 0); // [step]
1648        let step_tmp = self.tmp_name("astep");
1649        self.name_const(b, &step_tmp);
1650        b.emit(Op::Swap, 0);
1651        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1652        b.emit(Op::Pop, 0);
1653        // if (step.done) break
1654        self.load_local(b, &step_tmp);
1655        self.name_const(b, "done");
1656        b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
1657        b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
1658        let jdone = b.emit(Op::JumpIfTrue(0), 0);
1659        // target = step.value
1660        self.load_local(b, &step_tmp);
1661        self.name_const(b, "value");
1662        b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [value]
1663        let per_iteration = matches!(declare, BindMode::Lexical | BindMode::Const);
1664        if per_iteration {
1665            self.emit_push_scope(b);
1666        }
1667        self.compile_bind(b, target, declare)?;
1668        self.loops.push(LoopCtx {
1669            breaks: Vec::new(),
1670            continues: Vec::new(),
1671            break_depth: self.scope_depth,
1672            continue_depth: self.scope_depth,
1673            iter_depth: self.iter_depth,
1674            catches_continue: true,
1675            label: self.pending_label.take(),
1676        });
1677        self.compile_stmt(b, body)?;
1678        let cont_target = b.current_pos();
1679        if per_iteration {
1680            self.emit_pop_scope(b);
1681        }
1682        b.emit(Op::Jump(start), 0);
1683        let ctx = self.loops.pop().unwrap();
1684        for c in ctx.continues {
1685            b.patch_jump(c, cont_target);
1686        }
1687        // `done` arrives before the iteration scope is open; `break` from inside it
1688        // still has one to close.
1689        let break_target = b.current_pos();
1690        if per_iteration {
1691            b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
1692            b.emit(Op::Pop, 0);
1693        }
1694        // Leaving early closes the async iterator, running an async generator's
1695        // pending `finally` / calling a user iterator's `.return()`.
1696        self.load_local(b, &iter_tmp);
1697        b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0);
1698        b.emit(Op::Pop, 0);
1699        let end = b.current_pos();
1700        b.patch_jump(jdone, end);
1701        for br in ctx.breaks {
1702            b.patch_jump(br, break_target);
1703        }
1704        self.redispatch_after_loop(b);
1705        Ok(())
1706    }
1707
1708    /// Shared loop tail for for-of / for-in: iterator on TOS.
1709    fn loop_over(
1710        &mut self,
1711        b: &mut ChunkBuilder,
1712        declare: BindMode,
1713        target: &Expr,
1714        body: &Stmt,
1715    ) -> Result<(), String> {
1716        self.loop_over_inner(b, declare, target, body, None)
1717    }
1718
1719    /// `alive_in` names the local holding a `for-in`'s object. When it is set,
1720    /// each key is re-checked against that object before it is bound, and a key
1721    /// the body already deleted is skipped rather than visited.
1722    fn loop_over_inner(
1723        &mut self,
1724        b: &mut ChunkBuilder,
1725        declare: BindMode,
1726        target: &Expr,
1727        body: &Stmt,
1728        alive_in: Option<String>,
1729    ) -> Result<(), String> {
1730        // `for (const v of …)` binds a FRESH `v` each pass, so a closure made in one
1731        // pass keeps that pass's element.
1732        let per_iteration = matches!(declare, BindMode::Lexical | BindMode::Const);
1733        let start = b.current_pos();
1734        b.emit(Op::CallBuiltin(ops::FORITER, 0), 0); // [iterator, value, has_next]
1735        let jdone = b.emit(Op::JumpIfFalse(0), 0); // pops has_next
1736        if let Some(obj_tmp) = &alive_in {
1737            b.emit(Op::Dup, 0); // [iterator, key, key]
1738            self.load_local(b, obj_tmp); // [iterator, key, key, obj]
1739            b.emit(Op::Swap, 0); // [iterator, key, obj, key]
1740            b.emit(Op::CallBuiltin(ops::FORIN_ALIVE, 2), 0); // [iterator, key, alive]
1741            let jalive = b.emit(Op::JumpIfTrue(0), 0);
1742            b.emit(Op::Pop, 0); // drop the dead key -> [iterator]
1743            b.emit(Op::Jump(start), 0);
1744            b.patch_jump(jalive, b.current_pos());
1745        }
1746        if per_iteration {
1747            self.emit_push_scope(b);
1748        }
1749        self.compile_bind(b, target, declare)?; // consumes value -> [iterator]
1750        self.loops.push(LoopCtx {
1751            breaks: Vec::new(),
1752            continues: Vec::new(),
1753            break_depth: self.scope_depth,
1754            continue_depth: self.scope_depth,
1755            iter_depth: self.iter_depth,
1756            catches_continue: true,
1757            label: self.pending_label.take(),
1758        });
1759        self.compile_stmt(b, body)?;
1760        let cont_target = b.current_pos();
1761        if per_iteration {
1762            self.emit_pop_scope(b);
1763        }
1764        b.emit(Op::Jump(start), 0);
1765        let ctx = self.loops.pop().unwrap();
1766        for c in ctx.continues {
1767            b.patch_jump(c, cont_target);
1768        }
1769        let done = b.current_pos();
1770        b.patch_jump(jdone, done);
1771        b.emit(Op::Pop, 0); // drop iterator
1772        let jafter = b.emit(Op::Jump(0), 0);
1773        let break_target = b.current_pos();
1774        // `break` out of a for-of closes the iterator (runs a generator's pending
1775        // `finally` / calls a user iterator's `.return()`), then drops it.
1776        if per_iteration {
1777            b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
1778            b.emit(Op::Pop, 0);
1779        }
1780        b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0);
1781        b.emit(Op::Pop, 0); // ITER_CLOSE leaves its result; the `done` path popped
1782        let end = b.current_pos();
1783        b.patch_jump(jafter, end);
1784        for br in ctx.breaks {
1785            b.patch_jump(br, break_target);
1786        }
1787        // Every exit path above has already closed and dropped THIS loop's
1788        // iterator, so a signal re-dispatched here must not count it as live.
1789        self.iter_depth -= 1;
1790        self.redispatch_after_loop(b);
1791        self.iter_depth += 1;
1792        Ok(())
1793    }
1794
1795    fn compile_switch(
1796        &mut self,
1797        b: &mut ChunkBuilder,
1798        disc: &Expr,
1799        cases: &[SwitchCase],
1800    ) -> Result<(), String> {
1801        let disc_tmp = self.tmp_name("switch");
1802        self.compile_expr(b, disc)?;
1803        self.name_const(b, &disc_tmp);
1804        b.emit(Op::Swap, 0);
1805        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1806        b.emit(Op::Pop, 0);
1807        // All cases share ONE block scope, so `case 1: let x = …` is visible to the
1808        // later cases but dies with the switch. It opens BEFORE the test chain
1809        // because each case test jumps straight into its body.
1810        self.emit_push_scope(b);
1811        // …and every case's lexical names are hoisted into that one scope, so a
1812        // read from an EARLIER case is a dead-zone error rather than a lookup
1813        // that escapes to an outer binding.
1814        let all: Vec<Stmt> = cases.iter().flat_map(|c| c.body.iter().cloned()).collect();
1815        self.hoist_lexical(b, &all);
1816        // Emit the test chain: `if (disc === caseTest) goto bodyN`.
1817        let mut body_jumps: Vec<Option<usize>> = Vec::new();
1818        let mut default_idx: Option<usize> = None;
1819        for (i, case) in cases.iter().enumerate() {
1820            match &case.test {
1821                Some(t) => {
1822                    self.load_local(b, &disc_tmp);
1823                    self.compile_expr(b, t)?;
1824                    b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
1825                    let j = b.emit(Op::JumpIfTrue(0), 0);
1826                    body_jumps.push(Some(j));
1827                }
1828                None => {
1829                    default_idx = Some(i);
1830                    body_jumps.push(None);
1831                }
1832            }
1833        }
1834        // No test matched: jump to default (if any) or end.
1835        let no_match_jump = b.emit(Op::Jump(0), 0);
1836        self.loops.push(LoopCtx {
1837            breaks: Vec::new(),
1838            continues: Vec::new(),
1839            break_depth: self.scope_depth,
1840            continue_depth: self.scope_depth,
1841            iter_depth: self.iter_depth,
1842            catches_continue: false,
1843            label: None,
1844        });
1845        let mut body_starts: Vec<usize> = Vec::new();
1846        for case in cases {
1847            body_starts.push(b.current_pos());
1848            self.compile_stmts(b, &case.body)?;
1849        }
1850        let end = b.current_pos();
1851        // Patch each case test-jump to its body start.
1852        for (i, j) in body_jumps.iter().enumerate() {
1853            if let Some(j) = j {
1854                b.patch_jump(*j, body_starts[i]);
1855            }
1856        }
1857        match default_idx {
1858            Some(i) => b.patch_jump(no_match_jump, body_starts[i]),
1859            None => b.patch_jump(no_match_jump, end),
1860        }
1861        let ctx = self.loops.pop().unwrap();
1862        for br in ctx.breaks {
1863            b.patch_jump(br, end);
1864        }
1865        self.emit_pop_scope(b);
1866        self.redispatch_after_loop(b);
1867        Ok(())
1868    }
1869
1870    fn compile_try(
1871        &mut self,
1872        b: &mut ChunkBuilder,
1873        block: &[Stmt],
1874        handler: &Option<(Option<Expr>, Vec<Stmt>)>,
1875        finalizer: &Option<Vec<Stmt>>,
1876    ) -> Result<(), String> {
1877        let block_chunk = self.compile_block_chunk(block)?;
1878        let handler_def = match handler {
1879            Some((param, body)) => match param {
1880                Some(Expr::Ident(n)) => {
1881                    let hbody = self.compile_block_chunk(body)?;
1882                    Some((Some(n.clone()), hbody))
1883                }
1884                // `catch ({ code })` / `catch ([a, b])`. The handler receives
1885                // ONE value under a name, so a pattern binds a temp and is
1886                // destructured out of it before the body runs. Only a bare
1887                // identifier was handled, so the pattern bound nothing at all
1888                // and the body saw a ReferenceError for every name in it.
1889                Some(pattern) => {
1890                    let tmp = self.tmp_name("catch");
1891                    let pattern = pattern.clone();
1892                    let name = tmp.clone();
1893                    let hbody = self.compile_chunk_with(body, move |s, cb| {
1894                        s.load_local(cb, &name);
1895                        s.compile_bind(cb, &pattern, BindMode::Lexical)
1896                    })?;
1897                    Some((Some(tmp), hbody))
1898                }
1899                None => {
1900                    let hbody = self.compile_block_chunk(body)?;
1901                    Some((None, hbody))
1902                }
1903            },
1904            None => None,
1905        };
1906        let final_chunk = match finalizer {
1907            Some(f) => {
1908                // 14.15.3: a `finally` that completes NORMALLY has its
1909                // completion DISCARDED — the try/catch value is what the
1910                // statement produces. `eval('try{5}finally{6}')` is 5, and was
1911                // 6 while the block updated the completion register like any
1912                // other.
1913                let saved = std::mem::take(&mut self.completion);
1914                let chunk = self.compile_block_chunk(f);
1915                self.completion = saved;
1916                Some(chunk?)
1917            }
1918            None => None,
1919        };
1920        let id = self.tries.len();
1921        self.tries.push(TryDef {
1922            block: block_chunk,
1923            handler: handler_def,
1924            finalizer: final_chunk,
1925        });
1926        b.emit(Op::LoadInt(id as i64), 0);
1927        b.emit(Op::CallBuiltin(ops::TRY, 1), 0);
1928        b.emit(Op::Pop, 0);
1929        // The try/catch/finally bodies ran as their own chunks, so a `return` or
1930        // a `break`/`continue` inside them left a signal instead of jumping.
1931        self.emit_signal_dispatch(b);
1932        Ok(())
1933    }
1934
1935    /// Compile statements into a SEPARATE chunk (a try/catch/finally body). Loops
1936    /// opened outside it are unreachable by a plain jump, so `chunk_loop_base`
1937    /// moves up for the duration.
1938    fn compile_block_chunk(&mut self, stmts: &[Stmt]) -> Result<Chunk, String> {
1939        self.compile_chunk_with(stmts, |_, _| Ok(()))
1940    }
1941
1942    /// A try/catch/finally body chunk, with `prelude` emitted ahead of the
1943    /// statements. Used to destructure a `catch ({ code })` parameter, which has
1944    /// to bind before the handler's first statement runs.
1945    fn compile_chunk_with(
1946        &mut self,
1947        stmts: &[Stmt],
1948        prelude: impl FnOnce(&mut Self, &mut ChunkBuilder) -> Result<(), String>,
1949    ) -> Result<Chunk, String> {
1950        let mut cb = ChunkBuilder::new();
1951        // A nested chunk runs on its OWN VM frame, so the enclosing chunk's
1952        // slots are not reachable from it — everything here goes by name. (The
1953        // slot analysis already refuses any chunk containing a `try`, which is
1954        // what builds these; this keeps that true if another one appears.)
1955        let saved_slot_table = std::mem::take(&mut self.slots);
1956        let base = std::mem::replace(&mut self.chunk_loop_base, self.loops.len());
1957        let signals = std::mem::take(&mut self.chunk_signals);
1958        let depth = std::mem::take(&mut self.scope_depth);
1959        let iters = std::mem::take(&mut self.iter_depth);
1960        let sites = std::mem::take(&mut self.call_sites);
1961        let yields = std::mem::take(&mut self.yield_sites);
1962        let r = (|| {
1963            prelude(self, &mut cb)?;
1964            self.hoist_lexical(&mut cb, stmts);
1965            self.hoist_funcs(&mut cb, stmts)?;
1966            self.compile_stmts(&mut cb, stmts)
1967        })();
1968        self.chunk_loop_base = base;
1969        self.scope_depth = depth;
1970        self.iter_depth = iters;
1971        self.slots = saved_slot_table;
1972        // A signal raised inside the nested chunk still has to be dispatched by a
1973        // loop in THIS chunk, so the flag propagates outward.
1974        self.chunk_signals |= signals;
1975        r?;
1976        let chunk = self.finish_chunk(cb);
1977        self.call_sites = sites;
1978        self.yield_sites = yields;
1979        Ok(chunk)
1980    }
1981
1982    // ── functions ────────────────────────────────────────────────────────
1983    fn build_function(
1984        &mut self,
1985        name: &str,
1986        params: &[Param],
1987        body: &[Stmt],
1988        is_generator: bool,
1989        is_async: bool,
1990    ) -> Result<usize, String> {
1991        let (param_slots, prologue) = self.lower_params(params)?;
1992        let mut fb = ChunkBuilder::new();
1993        // Each function body is its own frame, so it gets its own slot table.
1994        // The analysis sees the parameter prologue (defaults, destructuring)
1995        // ahead of the body, which is the order they are emitted in.
1996        let mut planned: Vec<Stmt> = prologue.clone();
1997        planned.extend_from_slice(body);
1998        let saved_slot_table = std::mem::replace(
1999            &mut self.slots,
2000            if self.debug || is_generator || is_async {
2001                Default::default()
2002            } else {
2003                crate::slots::plan(params, &planned, false)
2004            },
2005        );
2006        // Prologue: a parameter arrives in the call environment (`bind_params`
2007        // ran before this chunk), so copy each slotted one into its slot once,
2008        // and everything after it is a bare `GetSlot`.
2009        for name in crate::slots::param_names(params) {
2010            if let Some(slot) = self.slot_of(&name) {
2011                self.name_const(&mut fb, &name);
2012                fb.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0);
2013                fb.emit(Op::SetSlot(slot), 0);
2014            }
2015        }
2016        // A function body is its own control-flow universe: `break`/`continue` can
2017        // never target a loop in the enclosing function.
2018        let saved_loops = std::mem::take(&mut self.loops);
2019        let saved_base = std::mem::replace(&mut self.chunk_loop_base, 0);
2020        let saved_signals = std::mem::take(&mut self.chunk_signals);
2021        let saved_depth = std::mem::take(&mut self.scope_depth);
2022        let saved_iters = std::mem::take(&mut self.iter_depth);
2023        let saved_agen = std::mem::replace(&mut self.in_async_generator, is_generator && is_async);
2024        // Strictness is inherited by every nested function and can only be
2025        // ADDED by a body's own directive prologue — never dropped.
2026        let saved_strict = self.strict;
2027        self.strict = self.strict || has_use_strict(body);
2028        // A nested function's statements are not the SCRIPT's, so none of them
2029        // may touch the completion register.
2030        let saved_completion = std::mem::take(&mut self.completion);
2031        // Captured before the restore below, since the FuncDef is built after
2032        // `self.strict` has been put back to the enclosing value.
2033        let body_strict = self.strict;
2034        // The body is a chunk of its own, so its call sites are keyed to ITS
2035        // `op_hash`; the enclosing chunk's pending ones must not be swept in.
2036        let saved_sites = std::mem::take(&mut self.call_sites);
2037        let saved_yields = std::mem::take(&mut self.yield_sites);
2038        let r = (|| {
2039            // Function-body hoisting: `var` bindings first, so a same-named
2040            // function declaration below overwrites the `undefined` rather than
2041            // being overwritten by it. Parameters are already bound, and
2042            // `hoist_var_name` leaves an existing binding alone.
2043            self.hoist_vars(&mut fb, body)?;
2044            self.hoist_funcs(&mut fb, &prologue)?;
2045            self.hoist_lexical(&mut fb, body);
2046            self.hoist_funcs(&mut fb, body)?;
2047            self.compile_stmts(&mut fb, &prologue)?;
2048            self.compile_stmts(&mut fb, body)
2049        })();
2050        self.loops = saved_loops;
2051        self.chunk_loop_base = saved_base;
2052        self.chunk_signals = saved_signals;
2053        self.scope_depth = saved_depth;
2054        self.iter_depth = saved_iters;
2055        self.in_async_generator = saved_agen;
2056        self.strict = saved_strict;
2057        self.completion = saved_completion;
2058        self.slots = saved_slot_table;
2059        r?;
2060        let def = FuncDef {
2061            name: name.to_string(),
2062            params: param_slots,
2063            chunk: self.finish_chunk(fb),
2064            is_arrow: false,
2065            is_generator,
2066            is_async,
2067            is_method: false,
2068            self_name: false,
2069            strict: body_strict,
2070            span: (0, 0),
2071            script: None,
2072        };
2073        self.call_sites = saved_sites;
2074        self.yield_sites = saved_yields;
2075        self.functions.push((name.to_string(), def));
2076        Ok(self.functions.len() - 1)
2077    }
2078
2079    /// A FuncDef that only carries `span`: the source of a `class`, which has
2080    /// no function of its own when it declares no constructor.
2081    fn source_record(&mut self, name: &str, span: Span) -> usize {
2082        let def = FuncDef {
2083            name: name.to_string(),
2084            params: Vec::new(),
2085            chunk: ChunkBuilder::new().build(),
2086            is_arrow: false,
2087            is_generator: false,
2088            is_async: false,
2089            is_method: true,
2090            self_name: false,
2091            strict: true,
2092            span,
2093            script: None,
2094        };
2095        self.functions.push((name.to_string(), def));
2096        self.functions.len() - 1
2097    }
2098
2099    fn build_arrow(
2100        &mut self,
2101        params: &[Param],
2102        body: &FnBody,
2103        is_async: bool,
2104    ) -> Result<usize, String> {
2105        let stmts = match body {
2106            FnBody::Block(b) => b.clone(),
2107            FnBody::Expr(e) => vec![Stmt::from(StmtKind::Return(Some((**e).clone())))],
2108        };
2109        let id = self.build_function("", params, &stmts, false, is_async)?;
2110        // Mark the template as an arrow so `this` is captured lexically.
2111        self.functions[id].1.is_arrow = true;
2112        Ok(id)
2113    }
2114
2115    // ── classes ──────────────────────────────────────────────────────────
2116    /// Lower a `class` to runtime builder ops, leaving the class value on the
2117    /// stack: `MKCLASS` (name, parent, ctor) then `DEF_MEMBER`/`DEF_FIELD` for
2118    /// each member (each keeps the class on the stack).
2119    fn compile_class(&mut self, b: &mut ChunkBuilder, node: &ClassNode) -> Result<(), String> {
2120        // A class body is strict code unconditionally (10.2.4), directive or not.
2121        let saved_strict = std::mem::replace(&mut self.strict, true);
2122        let r = self.compile_class_body(b, node);
2123        self.strict = saved_strict;
2124        r
2125    }
2126
2127    /// `#name` when this member's key is a literal private name, else `None`. A
2128    /// private name is never computed, so a computed key is never one.
2129    fn private_key(m: &ClassMember) -> Option<String> {
2130        match &m.key {
2131            Expr::Str(s) if !m.computed && s.starts_with('#') => Some(s.clone()),
2132            Expr::Ident(s) if !m.computed && s.starts_with('#') => Some(s.clone()),
2133            _ => None,
2134        }
2135    }
2136
2137    fn compile_class_body(&mut self, b: &mut ChunkBuilder, node: &ClassNode) -> Result<(), String> {
2138        let cname = node.name.clone().unwrap_or_default();
2139        // Push name, parent (or undefined), constructor (or undefined).
2140        self.name_const(b, &cname);
2141        match &node.parent {
2142            Some(p) => self.compile_expr(b, p)?,
2143            None => {
2144                b.emit(Op::LoadUndef, 0);
2145            }
2146        }
2147        let ctor = node
2148            .members
2149            .iter()
2150            .find(|m| m.kind == MemberKind::Constructor);
2151        match ctor {
2152            Some(m) => {
2153                let def_id = self.build_function(&cname, &m.params, &m.body, false, false)?;
2154                self.emit_mkfunc(b, def_id);
2155            }
2156            None => {
2157                b.emit(Op::LoadUndef, 0);
2158            }
2159        }
2160        // The class's source text (`String(C)`) rides on a FuncDef that is
2161        // never called, so its span is script-relative like any function's
2162        // and its id is rebased with the rest.
2163        let record = self.source_record(&cname, node.span);
2164        b.emit(Op::LoadInt(record as i64), 0);
2165        b.emit(Op::CallBuiltin(ops::MKCLASS, 4), 0); // -> [class]
2166
2167        // 15.7.14 steps 8-17: the class body runs inside its OWN environment,
2168        // holding one immutable binding for the class name, initialized to the
2169        // class itself at step 17 — before the static-field initializers of step
2170        // 32. So `class C { static x = C.m(); static m(){return 5} }` is 5, and a
2171        // class EXPRESSION's name (`const K = class Inner { static s = Inner.name }`)
2172        // is reachable from inside the body even though it is never a binding
2173        // outside it. node-js had no such scope: both threw `ReferenceError: C is
2174        // not defined`, because the only binding was the outer one the class
2175        // DECLARATION installs afterwards. An instance method's body already
2176        // worked, but only by accident — it runs late enough for the outer
2177        // binding to exist, which a class expression never gets.
2178        let body_scope = node.name.is_some();
2179        if let Some(name) = &node.name {
2180            self.emit_push_scope(b);
2181            b.emit(Op::Dup, 0); // [class, class]
2182            self.declare_as(b, &Expr::Ident(name.clone()), BindMode::Const); // [class]
2183        }
2184
2185        // `ClassDefinitionEvaluation` (15.7.14) installs every method and
2186        // accessor while evaluating the class body, and only then runs the
2187        // static-field initializers (step 32). So a static field may call a
2188        // static method declared after it, and `getOwnPropertyNames(C)` lists
2189        // the methods before the fields regardless of source order.
2190        // A `static { … }` block is a static ELEMENT, not a method: it belongs in
2191        // the deferred group with the field initializers and runs interleaved
2192        // with them in source order (both filters are stable over `members`).
2193        let deferred = |k: &MemberKind| matches!(k, MemberKind::Field | MemberKind::StaticBlock);
2194        let ordered = node
2195            .members
2196            .iter()
2197            .filter(|m| !deferred(&m.kind))
2198            .chain(node.members.iter().filter(|m| deferred(&m.kind)));
2199        let mut static_block_n = 0usize;
2200        for m in ordered {
2201            match m.kind {
2202                MemberKind::Constructor => {}
2203                // A PRIVATE static field declares a private element, so it
2204                // cannot be an ordinary write: `C.#s = 5` through `SETATTR`
2205                // trips the brand check that exists to reject exactly that write
2206                // on an object that has not declared `#s`. `DEF_MEMBER` installs
2207                // it directly, which is what a declaration is.
2208                MemberKind::Field if m.is_static && Self::private_key(m).is_some() => {
2209                    let key = Self::private_key(m).expect("guarded above");
2210                    self.name_const(b, &key); // [class, name]
2211                    b.emit(Op::LoadInt(member::STATIC_FIELD), 0);
2212                    b.emit(Op::LoadTrue, 0); // is_static
2213                    match &m.field_init {
2214                        Some(e) => self.emit_keyed_value(b, &m.key, e, false, member::METHOD)?,
2215                        None => {
2216                            b.emit(Op::LoadUndef, 0);
2217                        }
2218                    }
2219                    b.emit(Op::CallBuiltin(ops::DEF_MEMBER, 5), 0); // -> [class]
2220                }
2221                MemberKind::Field if m.is_static => {
2222                    // A static field is evaluated once at class-definition time and
2223                    // set as an own property of the constructor: `[class]` stays on
2224                    // the stack, `Dup` it as the SETATTR receiver.
2225                    b.emit(Op::Dup, 0); // [class, class]
2226                    self.emit_member_key(b, m)?; // [class, class, name]
2227                    match &m.field_init {
2228                        // 15.7.10: a static field's initializer is named after
2229                        // the field (`static s = function(){}` → `s`).
2230                        Some(e) => {
2231                            self.emit_keyed_value(b, &m.key, e, m.computed, member::METHOD)?
2232                        }
2233                        None => {
2234                            b.emit(Op::LoadUndef, 0);
2235                        }
2236                    }
2237                    // [class, class, name, val] -> SETATTR sets on the class -> [class, val]
2238                    b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0);
2239                    b.emit(Op::Pop, 0); // drop the returned value -> [class]
2240                }
2241                MemberKind::Field => {
2242                    // [class] name thunk name_anon -> DEF_FIELD -> [class]
2243                    self.emit_member_key(b, m)?;
2244                    let init = m.field_init.clone().unwrap_or(Expr::Undefined);
2245                    // 15.7.10: `class C { f = function(){} }` names the function
2246                    // `f`. An instance field's initializer runs per-instance from
2247                    // a thunk, and under a computed key the key is only known at
2248                    // class-definition time, so the decision travels to the host
2249                    // as a flag rather than as an emitted rename.
2250                    let name_anon = Self::is_anon_fn_def(&init);
2251                    let stmts = vec![Stmt::from(StmtKind::Return(Some(init)))];
2252                    let def_id = self.build_function("", &[], &stmts, false, false)?;
2253                    self.emit_mkfunc(b, def_id);
2254                    b.emit(
2255                        if name_anon {
2256                            Op::LoadTrue
2257                        } else {
2258                            Op::LoadFalse
2259                        },
2260                        0,
2261                    );
2262                    b.emit(Op::CallBuiltin(ops::DEF_FIELD, 4), 0);
2263                }
2264                MemberKind::StaticBlock => {
2265                    // `static { … }` runs ONCE at class-definition time with
2266                    // `this` bound to the constructor — exactly what a static
2267                    // method called as `C.m()` gets. So it is compiled as a
2268                    // static method under a HIDDEN key, invoked, and removed
2269                    // again; the `@@` prefix keeps it out of every enumeration
2270                    // (`Object.getOwnPropertyNames(C)` and friends filter
2271                    // internal slots) for the window in which it exists, and the
2272                    // counter keeps sibling blocks from colliding.
2273                    static_block_n += 1;
2274                    let slot = format!("@@staticBlock:{static_block_n}");
2275                    // [class] name kind static fn -> DEF_MEMBER -> [class]
2276                    self.name_const(b, &slot);
2277                    b.emit(Op::LoadInt(member::METHOD), 0);
2278                    b.emit(Op::LoadTrue, 0);
2279                    let def_id = self.build_function("", &[], &m.body, false, false)?;
2280                    self.functions[def_id].1.is_method = true;
2281                    self.emit_mkfunc(b, def_id);
2282                    b.emit(Op::CallBuiltin(ops::DEF_MEMBER, 5), 0);
2283                    // [class] -> C[slot]() -> discard the result
2284                    b.emit(Op::Dup, 0);
2285                    self.name_const(b, &slot);
2286                    b.emit(Op::CallBuiltin(ops::CALL_METHOD, 2), 0);
2287                    b.emit(Op::Pop, 0);
2288                    // [class] -> delete C[slot] -> discard the Bool
2289                    b.emit(Op::Dup, 0);
2290                    self.name_const(b, &slot);
2291                    self.emit_bool(b, false);
2292                    b.emit(Op::CallBuiltin(ops::DELPROP_NAME, 3), 0);
2293                    b.emit(Op::Pop, 0);
2294                }
2295                MemberKind::Method | MemberKind::Get | MemberKind::Set => {
2296                    // [class] name kind static fn -> DEF_MEMBER -> [class]
2297                    self.emit_member_key(b, m)?;
2298                    let kind = match m.kind {
2299                        MemberKind::Get => member::GET,
2300                        MemberKind::Set => member::SET,
2301                        _ => member::METHOD,
2302                    };
2303                    b.emit(Op::LoadInt(kind), 0);
2304                    b.emit(
2305                        if m.is_static {
2306                            Op::LoadTrue
2307                        } else {
2308                            Op::LoadFalse
2309                        },
2310                        0,
2311                    );
2312                    // 10.2.9 step 4: an accessor's function name carries the
2313                    // `get `/`set ` prefix — `class C { get gg(){} }` gives
2314                    // `get gg`, not `gg`.
2315                    let mname = match &m.key {
2316                        Expr::Str(s) if !m.computed => match m.kind {
2317                            MemberKind::Get => format!("get {s}"),
2318                            MemberKind::Set => format!("set {s}"),
2319                            _ => s.clone(),
2320                        },
2321                        _ => String::new(),
2322                    };
2323                    let def_id = self.build_function(
2324                        &mname,
2325                        &m.params,
2326                        &m.body,
2327                        m.is_generator,
2328                        m.is_async,
2329                    )?;
2330                    // A class method/accessor is a MethodDefinition: not a
2331                    // constructor, so it owns no `prototype` property.
2332                    self.functions[def_id].1.is_method = true;
2333                    self.functions[def_id].1.span = m.span;
2334                    self.emit_mkfunc(b, def_id);
2335                    b.emit(Op::CallBuiltin(ops::DEF_MEMBER, 5), 0);
2336                }
2337            }
2338        }
2339        if body_scope {
2340            self.emit_pop_scope(b);
2341        }
2342        Ok(())
2343    }
2344
2345    /// `IsAnonymousFunctionDefinition(expr)` — the SYNTACTIC predicate that
2346    /// decides whether NamedEvaluation applies. It is deliberately not a runtime
2347    /// "does this function have an empty name" test: measured against node
2348    /// v26.7.0, `const anon = (0, function(){}); ({ m: anon }).m.name` is `""`,
2349    /// because the property definition's right-hand side is an
2350    /// IdentifierReference, not a function definition. Renaming by value would
2351    /// also mutate a function the program still holds under another binding.
2352    fn is_anon_fn_def(init: &Expr) -> bool {
2353        match init {
2354            Expr::Function { name: None, .. } => true,
2355            Expr::Class(node) => node.name.is_none(),
2356            _ => false,
2357        }
2358    }
2359
2360    /// If `init` is an anonymous function/arrow/class (value already on TOS), set
2361    /// its `.name` to `name` (JS binding name-inference). No-op otherwise.
2362    fn infer_name(&mut self, b: &mut ChunkBuilder, init: &Expr, name: &str) {
2363        if !Self::is_anon_fn_def(init) {
2364            return;
2365        }
2366        // [fn] Dup; .name = name; drop the SETATTR result.
2367        b.emit(Op::Dup, 0);
2368        self.name_const(b, "name");
2369        self.strlit(b, name);
2370        b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0);
2371        b.emit(Op::Pop, 0);
2372    }
2373
2374    /// Compile a member's VALUE with the key already on the stack, applying
2375    /// NamedEvaluation (10.2.9 SetFunctionName) when the value is an anonymous
2376    /// function definition — `{ m: function(){} }`, `{ m(){} }`, `{ [k]: () => {} }`,
2377    /// `class C { static [k] = function(){} }`.
2378    ///
2379    /// A literal key resolves at compile time; a computed one is only known at
2380    /// run time, so the key already on the stack is duplicated and handed to
2381    /// `NAMED_EVAL` along with `kind` (which supplies the `get `/`set ` prefix).
2382    /// Leaves exactly one value on the stack either way, so every caller's
2383    /// arity is unchanged.
2384    fn emit_keyed_value(
2385        &mut self,
2386        b: &mut ChunkBuilder,
2387        key: &Expr,
2388        value: &Expr,
2389        computed: bool,
2390        kind: i64,
2391    ) -> Result<(), String> {
2392        match (Self::is_anon_fn_def(value), computed, key) {
2393            (true, false, Expr::Str(s)) => {
2394                self.compile_expr(b, value)?;
2395                let name = match kind {
2396                    member::GET => format!("get {s}"),
2397                    member::SET => format!("set {s}"),
2398                    _ => s.clone(),
2399                };
2400                self.infer_name(b, value, &name);
2401            }
2402            // [.., key] -> [.., key, key, kind, fn] -> NAMED_EVAL -> [.., key, fn]
2403            (true, true, _) => {
2404                b.emit(Op::Dup, 0);
2405                b.emit(Op::LoadInt(kind), 0);
2406                self.compile_expr(b, value)?;
2407                b.emit(Op::CallBuiltin(ops::NAMED_EVAL, 3), 0);
2408            }
2409            _ => self.compile_expr(b, value)?,
2410        }
2411        Ok(())
2412    }
2413
2414    /// Push a class/object member's property key: a computed expression coerced
2415    /// via `PROPKEY` (Symbol-aware), or a static name constant.
2416    fn emit_member_key(&mut self, b: &mut ChunkBuilder, m: &ClassMember) -> Result<(), String> {
2417        if m.computed {
2418            self.compile_expr(b, &m.key)?;
2419            b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
2420        } else if let Expr::Str(s) = &m.key {
2421            self.name_const(b, s);
2422        } else {
2423            self.compile_expr(b, &m.key)?;
2424            b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
2425        }
2426        Ok(())
2427    }
2428
2429    // ── generators / yield ───────────────────────────────────────────────
2430    fn compile_yield(
2431        &mut self,
2432        b: &mut ChunkBuilder,
2433        arg: &Option<Box<Expr>>,
2434        delegate: bool,
2435    ) -> Result<(), String> {
2436        if delegate && self.in_async_generator {
2437            // `yield* x` inside an `async function*` delegates over the ASYNC
2438            // iterator: await each step, re-yield its value, and evaluate to the
2439            // delegate's return value.
2440            match arg {
2441                Some(e) => self.compile_expr(b, e)?,
2442                None => {
2443                    b.emit(Op::LoadUndef, 0);
2444                }
2445            }
2446            b.emit(Op::CallBuiltin(ops::GET_ASYNC_ITER, 1), 0); // [aiter]
2447            let start = b.current_pos();
2448            b.emit(Op::Dup, 0); // [aiter, aiter]
2449            b.emit(Op::CallBuiltin(ops::ASYNC_STEP, 1), 0); // [aiter, stepPromise]
2450            b.emit(Op::CallBuiltin(ops::AWAIT, 1), 0); // [aiter, step]
2451            b.emit(Op::Dup, 0); // [aiter, step, step]
2452            self.name_const(b, "done");
2453            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
2454            b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
2455            let jdone = b.emit(Op::JumpIfTrue(0), 0); // [aiter, step]
2456            self.name_const(b, "value");
2457            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [aiter, value]
2458            let at = b.emit(Op::CallBuiltin(ops::YIELD, 1), 0); // [aiter, sent]
2459            self.yield_sites.push((at, self.iter_depth));
2460            b.emit(Op::Pop, 0); // [aiter]
2461            b.emit(Op::Jump(start), 0);
2462            let done = b.current_pos();
2463            b.patch_jump(jdone, done);
2464            self.name_const(b, "value"); // [aiter, step, "value"]
2465            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [aiter, returnValue]
2466            b.emit(Op::Swap, 0); // [returnValue, aiter]
2467            b.emit(Op::Pop, 0); // [returnValue]
2468        } else if delegate {
2469            // `yield* iterable`: step the delegate through the iterator protocol,
2470            // re-yielding each value and FORWARDING whatever `.next(x)` sent in.
2471            // The expression's value is the delegate's RETURN value, which
2472            // `FORITER` discards — hence the explicit `.next()` calls.
2473            let sent_tmp = self.tmp_name("delegated");
2474            match arg {
2475                Some(e) => self.compile_expr(b, e)?,
2476                None => {
2477                    b.emit(Op::LoadUndef, 0);
2478                }
2479            }
2480            b.emit(Op::CallBuiltin(ops::GETITER, 1), 0); // [iterator]
2481                                                         // The delegate is parked on the stack for the whole delegation, so
2482                                                         // it counts as a live iterator: a `.return()`/`.throw()` injected
2483                                                         // into the OUTER generator has to close it (7.4.9 IteratorClose),
2484                                                         // which is what runs the delegate's pending `finally`.
2485            self.iter_depth += 1;
2486            self.name_const(b, &sent_tmp);
2487            b.emit(Op::LoadUndef, 0);
2488            b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
2489            b.emit(Op::Pop, 0);
2490            let start = b.current_pos();
2491            b.emit(Op::Dup, 0); // [iterator, iterator]
2492            self.name_const(b, "next");
2493            self.load_local(b, &sent_tmp);
2494            b.emit(Op::CallBuiltin(ops::CALL_METHOD, 3), 0); // [iterator, step]
2495            b.emit(Op::Dup, 0);
2496            self.name_const(b, "done");
2497            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
2498            b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
2499            let jdone = b.emit(Op::JumpIfTrue(0), 0); // [iterator, step]
2500            self.name_const(b, "value");
2501            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [iterator, value]
2502            let at = b.emit(Op::CallBuiltin(ops::YIELD, 1), 0); // [iterator, sent]
2503            self.yield_sites.push((at, self.iter_depth));
2504            self.name_const(b, &sent_tmp);
2505            b.emit(Op::Swap, 0);
2506            b.emit(Op::CallBuiltin(ops::SETLOCAL, 2), 0);
2507            b.emit(Op::Pop, 0);
2508            b.emit(Op::Jump(start), 0);
2509            let done = b.current_pos();
2510            b.patch_jump(jdone, done);
2511            self.name_const(b, "value"); // [iterator, step, "value"]
2512            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [iterator, returnValue]
2513            b.emit(Op::Swap, 0);
2514            b.emit(Op::Pop, 0); // [returnValue]
2515            self.iter_depth -= 1;
2516        } else {
2517            match arg {
2518                Some(e) => self.compile_expr(b, e)?,
2519                None => {
2520                    b.emit(Op::LoadUndef, 0);
2521                }
2522            }
2523            // YIELD suspends and leaves the value sent by `.next(x)` on the stack.
2524            let at = b.emit(Op::CallBuiltin(ops::YIELD, 1), 0);
2525            self.yield_sites.push((at, self.iter_depth));
2526        }
2527        Ok(())
2528    }
2529
2530    /// Lower a formal-parameter list into simple slots plus prologue statements
2531    /// (defaults + destructuring), executed at the top of the body.
2532    fn lower_params(&mut self, params: &[Param]) -> Result<(Vec<ParamSlot>, Vec<Stmt>), String> {
2533        // Strict code refuses a DUPLICATE parameter name, and refuses `eval` or
2534        // `arguments` as one. Both are early errors, so they fire before the
2535        // body runs — sloppy code still allows the duplicate, where the LAST
2536        // one wins.
2537        if self.strict {
2538            let mut seen: Vec<String> = Vec::new();
2539            for p in params {
2540                for n in binding_names(&p.pattern) {
2541                    if RESERVED_IN_STRICT.contains(&n.as_str()) {
2542                        return Err(strict_reserved_error());
2543                    }
2544                    if seen.contains(&n) {
2545                        return Err(
2546                            "SyntaxError: Duplicate parameter name not allowed in this context"
2547                                .to_string(),
2548                        );
2549                    }
2550                    seen.push(n);
2551                }
2552            }
2553        }
2554        let mut slots = Vec::new();
2555        let mut prologue: Vec<Stmt> = Vec::new();
2556        for (i, p) in params.iter().enumerate() {
2557            if p.rest {
2558                let name = match &p.pattern {
2559                    Expr::Ident(n) => n.clone(),
2560                    _ => return Err("SyntaxError: rest parameter must be an identifier".into()),
2561                };
2562                slots.push(ParamSlot {
2563                    name,
2564                    rest: true,
2565                    has_default: false,
2566                });
2567                continue;
2568            }
2569            match &p.pattern {
2570                Expr::Ident(name) => {
2571                    slots.push(ParamSlot {
2572                        name: name.clone(),
2573                        rest: false,
2574                        has_default: p.default.is_some(),
2575                    });
2576                    if let Some(d) = &p.default {
2577                        prologue.push(default_stmt(name, d));
2578                    }
2579                }
2580                pattern => {
2581                    let synth = format!(".param{i}");
2582                    slots.push(ParamSlot {
2583                        name: synth.clone(),
2584                        rest: false,
2585                        has_default: p.default.is_some(),
2586                    });
2587                    if let Some(d) = &p.default {
2588                        prologue.push(default_stmt(&synth, d));
2589                    }
2590                    prologue.push(Stmt::from(StmtKind::Decl {
2591                        kind: DeclKind::Let,
2592                        decls: vec![Declarator {
2593                            target: pattern.clone(),
2594                            init: Some(Expr::Ident(synth)),
2595                        }],
2596                    }));
2597                }
2598            }
2599        }
2600        Ok((slots, prologue))
2601    }
2602
2603    // ── expressions ──────────────────────────────────────────────────────
2604    fn compile_expr(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
2605        match e {
2606            Expr::Undefined => {
2607                b.emit(Op::LoadUndef, 0);
2608            }
2609            // A hole only carries its extra meaning INSIDE an array literal
2610            // (`compile_array` records it); evaluated anywhere else it is just
2611            // the `undefined` an elided read produces.
2612            Expr::Hole => {
2613                b.emit(Op::LoadUndef, 0);
2614            }
2615            Expr::Null => {
2616                b.emit(Op::CallBuiltin(ops::LOAD_NULL, 0), 0);
2617            }
2618            Expr::True => {
2619                b.emit(Op::LoadTrue, 0);
2620            }
2621            Expr::False => {
2622                b.emit(Op::LoadFalse, 0);
2623            }
2624            Expr::Number(n) => {
2625                b.emit(Op::LoadFloat(*n), 0);
2626            }
2627            Expr::BigInt(digits) => {
2628                // The canonical decimal digit string travels as a native constant;
2629                // MKBIGINT parses it into a heap BigInt at runtime.
2630                let k = b.add_constant(Value::str(digits));
2631                b.emit(Op::LoadConst(k), 0);
2632                b.emit(Op::CallBuiltin(ops::MKBIGINT, 1), 0);
2633            }
2634            Expr::Regex(pat, flags) => {
2635                let kp = b.add_constant(Value::str(pat));
2636                b.emit(Op::LoadConst(kp), 0);
2637                let kf = b.add_constant(Value::str(flags));
2638                b.emit(Op::LoadConst(kf), 0);
2639                b.emit(Op::CallBuiltin(ops::MKREGEX, 2), 0);
2640            }
2641            Expr::Str(s) => self.strlit(b, s),
2642            Expr::Template { quasis, exprs } => self.compile_template(b, quasis, exprs)?,
2643            Expr::TaggedTemplate {
2644                tag,
2645                quasis,
2646                raws,
2647                exprs,
2648            } => self.compile_tagged_template(b, tag, quasis, raws, exprs)?,
2649            Expr::Ident(n) => self.load_local(b, n),
2650            Expr::This => {
2651                b.emit(Op::CallBuiltin(ops::THIS, 0), 0);
2652            }
2653            Expr::Array(items) => self.compile_array(b, items)?,
2654            Expr::Object(props) => self.compile_object(b, props)?,
2655            Expr::Spread(inner) => self.compile_expr(b, inner)?,
2656            Expr::Logical(op, l, r) => self.compile_logical(b, *op, l, r)?,
2657            Expr::Unary(op, e) => self.compile_unary(b, *op, e)?,
2658            Expr::Binary(op, l, r) => self.compile_binary(b, *op, l, r)?,
2659            Expr::Conditional { test, cons, alt } => {
2660                self.compile_condition(b, test)?;
2661                let jf = b.emit(Op::JumpIfFalse(0), 0);
2662                self.compile_expr(b, cons)?;
2663                let je = b.emit(Op::Jump(0), 0);
2664                let els = b.current_pos();
2665                b.patch_jump(jf, els);
2666                self.compile_expr(b, alt)?;
2667                let end = b.current_pos();
2668                b.patch_jump(je, end);
2669            }
2670            // A COMPOUND assignment (`o[k()] += 1`) evaluates the target
2671            // reference once. Handled ahead of the plain-`=` arms below because
2672            // it must keep that reference on the stack across the read, the
2673            // computation and the write, which a plain assignment never does.
2674            Expr::Assign {
2675                target,
2676                op: Some(aop),
2677                value,
2678            } => self.compile_compound_assign(b, target, *aop, value)?,
2679            // 13.15.1 / 13.4.1: strict code may not assign to, or update,
2680            // `eval` or `arguments`. Both are early errors.
2681            Expr::Assign { target, .. } | Expr::Update { target, .. }
2682                if self.strict
2683                    && matches!(&**target, Expr::Ident(n)
2684                        if RESERVED_IN_STRICT.contains(&n.as_str())) =>
2685            {
2686                return Err(strict_reserved_error());
2687            }
2688            Expr::Assign { target, value, .. } => match &**target {
2689                // 13.15.2 steps 1.a-1.f: for a PROPERTY target the reference is
2690                // evaluated first — the object, then the key — and only then the
2691                // right-hand side. Routing these through `compile_bind` emitted
2692                // the value first and the reference after, so every side effect
2693                // in the target ran in the wrong order: `o[k()] = v()` called
2694                // `v` before `k`, and `a[i++] = f()` passed `f` the
2695                // already-incremented index. Both builtins return the value they
2696                // stored, which is also the value of the assignment expression,
2697                // so the `Dup`/`Rot`/`Pop` the generic path needed all fall away.
2698                Expr::Member {
2699                    object, property, ..
2700                } => {
2701                    self.compile_expr(b, object)?; // [recv]
2702                    self.name_const(b, property); // [recv, name]
2703                    self.compile_expr(b, value)?; // [recv, name, value]
2704                    b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0); // [value]
2705                }
2706                Expr::Index { object, index, .. } => {
2707                    self.compile_expr(b, object)?; // [recv]
2708                    self.compile_expr(b, index)?; // [recv, idx]
2709                    self.compile_expr(b, value)?; // [recv, idx, value]
2710                    b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0); // [value]
2711                }
2712                _ => {
2713                    self.compile_expr(b, value)?;
2714                    // 13.15.2 step 1.e: `h = function(){}` names the function `h`.
2715                    // Only an IdentifierReference target counts — `o.p = function(){}`
2716                    // leaves the name empty in node too.
2717                    if let Expr::Ident(n) = &**target {
2718                        self.infer_name(b, value, n);
2719                    }
2720                    b.emit(Op::Dup, 0); // assignment yields the value
2721                    self.destructure_src = destructure_source_text(value);
2722                    let r = self.compile_bind(b, target, BindMode::Assign);
2723                    self.destructure_src = None;
2724                    r?;
2725                }
2726            },
2727            Expr::Update { op, prefix, target } => self.compile_update(b, *op, *prefix, target)?,
2728            // A chain's ROOT opens the frame its `?.` links park their jumps
2729            // in; nested links see it already open and add to it.
2730            Expr::Call { .. } | Expr::Member { .. } | Expr::Index { .. }
2731                if self.opt_chain.is_empty() && Self::spine_has_optional(e) =>
2732            {
2733                self.compile_chain_root(b, e)?
2734            }
2735            Expr::Call {
2736                func,
2737                args,
2738                optional,
2739            } => self.compile_call(b, func, args, *optional)?,
2740            Expr::New { callee, args } => self.compile_new(b, callee, args)?,
2741            Expr::Member {
2742                object,
2743                property,
2744                optional,
2745            } => self.compile_member(b, object, property, *optional)?,
2746            Expr::Index {
2747                object,
2748                index,
2749                optional,
2750            } => self.compile_index(b, object, index, *optional)?,
2751            Expr::Function {
2752                params,
2753                body,
2754                is_arrow,
2755                name,
2756                is_generator,
2757                is_async,
2758                is_method,
2759                span,
2760            } => {
2761                let def_id = if *is_arrow {
2762                    self.build_arrow(params, body, *is_async)?
2763                } else {
2764                    let n = name.clone().unwrap_or_default();
2765                    let stmts = match body {
2766                        FnBody::Block(b) => b.clone(),
2767                        FnBody::Expr(e) => vec![Stmt::from(StmtKind::Return(Some((**e).clone())))],
2768                    };
2769                    let id = self.build_function(&n, params, &stmts, *is_generator, *is_async)?;
2770                    // A NAMED function expression binds its own name inside the body
2771                    // (object/class methods parse with `name: None`, so this only
2772                    // fires for `function name(…) {…}` in expression position).
2773                    if name.is_some() {
2774                        self.functions[id].1.self_name = true;
2775                    }
2776                    self.functions[id].1.is_method = *is_method;
2777                    id
2778                };
2779                self.functions[def_id].1.span = *span;
2780                self.emit_mkfunc(b, def_id);
2781            }
2782            Expr::Class(node) => self.compile_class(b, node)?,
2783            Expr::Super => {
2784                // Bare `super` only appears as a call/member callee, handled by
2785                // compile_call / compile_member; a stray `super` yields undefined.
2786                b.emit(Op::LoadUndef, 0);
2787            }
2788            Expr::NewTarget => {
2789                b.emit(Op::CallBuiltin(ops::NEW_TARGET, 0), 0);
2790            }
2791            Expr::Yield { arg, delegate } => self.compile_yield(b, arg, *delegate)?,
2792            Expr::Await(inner) => {
2793                self.compile_expr(b, inner)?;
2794                b.emit(Op::CallBuiltin(ops::AWAIT, 1), 0);
2795            }
2796            Expr::Sequence(items) => {
2797                for (i, it) in items.iter().enumerate() {
2798                    self.compile_expr(b, it)?;
2799                    if i + 1 < items.len() {
2800                        b.emit(Op::Pop, 0);
2801                    }
2802                }
2803            }
2804        }
2805        Ok(())
2806    }
2807
2808    fn compile_template(
2809        &mut self,
2810        b: &mut ChunkBuilder,
2811        quasis: &[String],
2812        exprs: &[Expr],
2813    ) -> Result<(), String> {
2814        let mut n = 0;
2815        for (i, q) in quasis.iter().enumerate() {
2816            let k = b.add_constant(Value::str(q));
2817            b.emit(Op::LoadConst(k), 0);
2818            n += 1;
2819            if i < exprs.len() {
2820                self.compile_expr(b, &exprs[i])?;
2821                b.emit(Op::CallBuiltin(ops::TOSTR, 1), 0);
2822                n += 1;
2823            }
2824        }
2825        b.emit(Op::CallBuiltin(ops::MKSTR, argc(n)?), 0);
2826        Ok(())
2827    }
2828
2829    /// Lower a tagged template to `TAG_TMPL`. Operand layout (matching
2830    /// `builtins::b_tag_tmpl`): `[this, tag, n, m, site, cooked×n, raw×n,
2831    /// values×m]`, where `n = quasis.len()` and `m = exprs.len()`
2832    /// (`n == m + 1`).
2833    ///
2834    /// `this` is the tag's receiver. A tagged template IS a call (13.3.11.1
2835    /// evaluates the tag as a MemberExpression and passes its reference's base
2836    /// as the `this` argument), so ``o.m`a` `` runs `m` with `this === o` — it
2837    /// ran with `this` undefined, which broke every tag written as a method.
2838    /// `undefined` for a tag that is not a property reference.
2839    ///
2840    /// `site` is this site's ordinal in the compilation; the runtime caches the
2841    /// template object under it so a site evaluated twice hands back the same
2842    /// object.
2843    fn compile_tagged_template(
2844        &mut self,
2845        b: &mut ChunkBuilder,
2846        tag: &Expr,
2847        quasis: &[String],
2848        raws: &[String],
2849        exprs: &[Expr],
2850    ) -> Result<(), String> {
2851        match tag {
2852            // `o.m`…`` / `o?.m`…`` — the receiver stays on the stack under the
2853            // method, so both are evaluated exactly once.
2854            Expr::Member {
2855                object,
2856                property,
2857                optional: false,
2858            } if !matches!(**object, Expr::Super) => {
2859                self.compile_expr(b, object)?; // [o]
2860                b.emit(Op::Dup, 0); // [o, o]
2861                self.name_const(b, property);
2862                b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [o, f]
2863            }
2864            _ => {
2865                b.emit(Op::LoadUndef, 0);
2866                self.compile_expr(b, tag)?;
2867            }
2868        }
2869        let n = quasis.len();
2870        let m = exprs.len();
2871        let site = self.tmpl_sites;
2872        self.tmpl_sites += 1;
2873        b.emit(Op::LoadInt(n as i64), 0);
2874        b.emit(Op::LoadInt(m as i64), 0);
2875        b.emit(Op::LoadInt(site as i64), 0);
2876        for q in quasis {
2877            self.strlit(b, q); // cooked strings (heap)
2878        }
2879        for r in raws {
2880            self.strlit(b, r); // raw strings (heap)
2881        }
2882        for e in exprs {
2883            self.compile_expr(b, e)?; // substitution values
2884        }
2885        b.emit(Op::CallBuiltin(ops::TAG_TMPL, argc(5 + 2 * n + m)?), 0);
2886        Ok(())
2887    }
2888
2889    fn compile_array(&mut self, b: &mut ChunkBuilder, items: &[Expr]) -> Result<(), String> {
2890        if items.iter().any(|e| matches!(e, Expr::Spread(_))) {
2891            // (tag, value) pairs; tag 1 = spread, tag 2 = elision. A spread
2892            // makes every later element's index a RUN-TIME quantity, so the
2893            // holes cannot be recorded from here — the tag carries the fact and
2894            // `BUILD_ARGS` marks them as it walks.
2895            for it in items {
2896                match it {
2897                    Expr::Spread(inner) => {
2898                        b.emit(Op::LoadInt(1), 0);
2899                        self.compile_expr(b, inner)?;
2900                    }
2901                    Expr::Hole => {
2902                        b.emit(Op::LoadInt(2), 0);
2903                        b.emit(Op::LoadUndef, 0);
2904                    }
2905                    _ => {
2906                        b.emit(Op::LoadInt(0), 0);
2907                        self.compile_expr(b, it)?;
2908                    }
2909                }
2910            }
2911            let at = b.current_pos();
2912            b.emit(Op::CallBuiltin(ops::BUILD_ARGS, argc(items.len() * 2)?), 0);
2913            // A spread over a non-iterable names the SOURCE: `[...o]` reports
2914            // `o is not iterable`. One op covers the whole literal, so the text
2915            // is recorded only when a SINGLE spread could have raised it —
2916            // with two, this cannot say which one did, and naming the wrong
2917            // expression is worse than rendering the value.
2918            let texts: Vec<Option<String>> = items
2919                .iter()
2920                .filter_map(|e| match e {
2921                    Expr::Spread(inner) => Some(callee_text(inner)),
2922                    _ => None,
2923                })
2924                .collect();
2925            // One op covers the whole literal, so a name can be recorded only
2926            // when every spread would produce the SAME one — with one spread
2927            // trivially, and with `[...o, ...o]` because either is the answer.
2928            // Two DIFFERENT sources cannot be told apart here, and naming the
2929            // wrong expression is worse than rendering the value.
2930            if let Some(first) = texts.first().cloned().flatten() {
2931                if texts.iter().all(|t| t.as_deref() == Some(first.as_str())) {
2932                    self.call_sites.push((at, first));
2933                }
2934            }
2935        } else if items.len() <= u8::MAX as usize {
2936            for it in items {
2937                self.compile_expr(b, it)?;
2938            }
2939            b.emit(Op::CallBuiltin(ops::MKARR, argc(items.len())?), 0);
2940            self.mark_literal_holes(b, items);
2941        } else {
2942            // A literal larger than one CallBuiltin's u8 arg count can hold (the
2943            // generated data tables in iconv-lite hit this): start from an empty
2944            // array and append each element with an indexed store, keeping the
2945            // array on the stack across iterations.
2946            b.emit(Op::CallBuiltin(ops::MKARR, 0), 0); // [arr]
2947            for (i, it) in items.iter().enumerate() {
2948                b.emit(Op::Dup, 0); // [arr, arr]
2949                b.emit(Op::LoadInt(i as i64), 0); // [arr, arr, i]
2950                self.compile_expr(b, it)?; // [arr, arr, i, val]
2951                b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0); // -> [arr, val]
2952                b.emit(Op::Pop, 0); // [arr]
2953            }
2954            // After the writes: a `SETITEM` CLEARS the hole at the index it
2955            // writes, so marking has to come last.
2956            self.mark_literal_holes(b, items);
2957        }
2958        Ok(())
2959    }
2960
2961    /// Emit a `MARK_HOLE` per elided position of a spread-free array literal,
2962    /// with the finished array on top of the stack. Emits nothing at all for the
2963    /// dense literals that are essentially every literal in real code.
2964    fn mark_literal_holes(&mut self, b: &mut ChunkBuilder, items: &[Expr]) {
2965        for (i, it) in items.iter().enumerate() {
2966            if !matches!(it, Expr::Hole) {
2967                continue;
2968            }
2969            b.emit(Op::Dup, 0); // [arr, arr]
2970            b.emit(Op::LoadInt(i as i64), 0); // [arr, arr, i]
2971            b.emit(Op::CallBuiltin(ops::MARK_HOLE, 2), 0); // [arr, undefined]
2972            b.emit(Op::Pop, 0); // [arr]
2973        }
2974    }
2975
2976    fn compile_object(&mut self, b: &mut ChunkBuilder, props: &[Prop]) -> Result<(), String> {
2977        // (tag, key, val) triples for the data/spread props; tag 1 = ...spread.
2978        // Accessors are installed afterward via DEF_ACCESSOR.
2979        // An ACCESSOR keeps its slot in this list — with a tag of its own — so
2980        // the object enumerates it where the source declared it. The pair
2981        // `get`/`set` for one key contributes ONE slot.
2982        let mut seen_accessor: Vec<String> = Vec::new();
2983        let data: Vec<&Prop> = props
2984            .iter()
2985            .filter(|p| match p {
2986                Prop::Accessor { key, computed, .. } => {
2987                    // Only a literal key can be de-duplicated at compile time; a
2988                    // computed one is settled by `b_mkobj`'s `or_insert`.
2989                    let literal = match (key, computed) {
2990                        (Expr::Str(s), false) => Some(s.clone()),
2991                        _ => None,
2992                    };
2993                    match literal {
2994                        Some(k) if seen_accessor.contains(&k) => false,
2995                        Some(k) => {
2996                            seen_accessor.push(k);
2997                            true
2998                        }
2999                        None => true,
3000                    }
3001                }
3002                _ => true,
3003            })
3004            .collect();
3005        let has_spread = data.iter().any(|p| matches!(p, Prop::Spread(_)));
3006        // A spread-free literal with more triples than one CallBuiltin's u8 arg
3007        // count can hold (iconv-lite's generated codepage tables are 150+ keys)
3008        // is built incrementally: start empty, store each key, keeping the object
3009        // on the stack. Spread merges need the single-shot MKOBJ tag path, so
3010        // large-with-spread stays on it (a rare, genuine limitation).
3011        if data.len() * 3 > u8::MAX as usize && !has_spread {
3012            b.emit(Op::CallBuiltin(ops::MKOBJ, 0), 0); // [obj]
3013            for p in &data {
3014                if let Prop::KeyValue {
3015                    key,
3016                    value,
3017                    computed,
3018                } = p
3019                {
3020                    b.emit(Op::Dup, 0); // [obj, obj]
3021                    self.compile_expr(b, key)?;
3022                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0); // [obj, obj, key]
3023                    self.emit_keyed_value(b, key, value, *computed, member::METHOD)?;
3024                    b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0); // -> [obj, val]
3025                    b.emit(Op::Pop, 0); // [obj]
3026                }
3027            }
3028            // The incremental path's accessors keep their trailing order: a
3029            // literal that large is a generated data table, and none carry one.
3030            return self.compile_object_accessors(b, props);
3031        }
3032        for p in &data {
3033            match p {
3034                Prop::KeyValue {
3035                    key,
3036                    value,
3037                    computed,
3038                } => {
3039                    // Tag 3 marks a METHOD DEFINITION, so `MKOBJ` can give it
3040                    // the literal as its `[[HomeObject]]`. It has to be decided
3041                    // HERE: a method assigned from elsewhere (`{ m: other.m }`)
3042                    // is an ordinary value whose home object was fixed where it
3043                    // was defined, and the runtime cannot tell the two apart
3044                    // from the value alone.
3045                    let defines_method = matches!(
3046                        value,
3047                        Expr::Function {
3048                            is_method: true,
3049                            ..
3050                        }
3051                    );
3052                    b.emit(Op::LoadInt(if defines_method { 3 } else { 0 }), 0);
3053                    // Key coerces to a property key (Symbol-aware: a Symbol maps to
3054                    // its internal `@@…` key rather than a `String()` coercion).
3055                    self.compile_expr(b, key)?;
3056                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
3057                    self.emit_keyed_value(b, key, value, *computed, member::METHOD)?;
3058                }
3059                Prop::Spread(src) => {
3060                    b.emit(Op::LoadInt(1), 0);
3061                    self.compile_expr(b, src)?;
3062                    b.emit(Op::LoadUndef, 0);
3063                }
3064                // Reserve the accessor's enumeration slot; `DEF_ACCESSOR` below
3065                // installs the functions themselves.
3066                Prop::Accessor { key, computed, .. } => {
3067                    let _ = computed; // the key expression covers both forms
3068                    b.emit(Op::LoadInt(2), 0);
3069                    self.compile_expr(b, key)?;
3070                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
3071                    b.emit(Op::LoadUndef, 0);
3072                }
3073            }
3074        }
3075        b.emit(Op::CallBuiltin(ops::MKOBJ, argc(data.len() * 3)?), 0); // [obj]
3076        self.compile_object_accessors(b, props)
3077    }
3078
3079    /// Install any getter/setter accessors of an object literal onto the object
3080    /// left on the stack (shared by the single-shot and incremental build paths).
3081    fn compile_object_accessors(
3082        &mut self,
3083        b: &mut ChunkBuilder,
3084        props: &[Prop],
3085    ) -> Result<(), String> {
3086        for p in props {
3087            if let Prop::Accessor {
3088                key,
3089                computed,
3090                is_getter,
3091                func,
3092            } = p
3093            {
3094                if *computed {
3095                    self.compile_expr(b, key)?;
3096                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
3097                } else if let Expr::Str(s) = key {
3098                    self.name_const(b, s);
3099                } else {
3100                    self.compile_expr(b, key)?;
3101                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
3102                }
3103                let kind = if *is_getter { member::GET } else { member::SET };
3104                b.emit(Op::LoadInt(kind), 0);
3105                // `{ get g(){} }` names the getter `get g` (10.2.9 step 4 via
3106                // 13.2.5.5). A COMPUTED accessor key is the one member position
3107                // whose key is not still reachable on the stack here — `kind`
3108                // sits between it and the function — so it keeps the empty name.
3109                if *computed {
3110                    self.compile_expr(b, func)?;
3111                } else if let Expr::Str(s) = key {
3112                    self.compile_expr(b, func)?;
3113                    let prefix = if *is_getter { "get" } else { "set" };
3114                    self.infer_name(b, func, &format!("{prefix} {s}"));
3115                } else {
3116                    self.compile_expr(b, func)?;
3117                }
3118                b.emit(Op::CallBuiltin(ops::DEF_ACCESSOR, 4), 0);
3119            }
3120        }
3121        Ok(())
3122    }
3123
3124    fn compile_logical(
3125        &mut self,
3126        b: &mut ChunkBuilder,
3127        op: LogicalOp,
3128        l: &Expr,
3129        r: &Expr,
3130    ) -> Result<(), String> {
3131        self.compile_expr(b, l)?;
3132        b.emit(Op::Dup, 0);
3133        let test_op = match op {
3134            LogicalOp::And | LogicalOp::Or => ops::TRUTHY,
3135            LogicalOp::Nullish => ops::NULLISH,
3136        };
3137        b.emit(Op::CallBuiltin(test_op, 1), 0);
3138        let jump = match op {
3139            LogicalOp::And => b.emit(Op::JumpIfFalse(0), 0), // false -> keep left
3140            LogicalOp::Or => b.emit(Op::JumpIfTrue(0), 0),   // true -> keep left
3141            LogicalOp::Nullish => b.emit(Op::JumpIfFalse(0), 0), // not-nullish -> keep left
3142        };
3143        b.emit(Op::Pop, 0); // drop left, evaluate right
3144        self.compile_expr(b, r)?;
3145        let end = b.current_pos();
3146        b.patch_jump(jump, end);
3147        Ok(())
3148    }
3149
3150    /// `target op= value` with the target reference evaluated exactly ONCE.
3151    ///
3152    /// The parser hands the operator over instead of rewriting `a op= b` into
3153    /// `a = a op b`; that rewrite duplicated the target subtree, so every side
3154    /// effect in it ran twice (`o[k()] += 1` called `k` twice, and the logical
3155    /// forms called it twice even when they short-circuited and never wrote).
3156    /// The duplication could not be repaired downstream: after the rewrite,
3157    /// `o[k()] += 1` and the genuinely-twice-calling `o[k()] = o[k()] + 1` are
3158    /// the same tree.
3159    ///
3160    /// For a property target the reference is the pair `[recv, key]`, which
3161    /// `Dup2` copies for the read while the originals serve for the write. An
3162    /// identifier target has no reference to preserve — reading a name twice
3163    /// has no observable effect — so it keeps the simple lowering.
3164    fn compile_compound_assign(
3165        &mut self,
3166        b: &mut ChunkBuilder,
3167        target: &Expr,
3168        aop: AssignOp,
3169        value: &Expr,
3170    ) -> Result<(), String> {
3171        // The reference: leave `[recv, key]` on the stack, and report which
3172        // builtin pair reads and writes through it.
3173        let (get, set) = match target {
3174            Expr::Member {
3175                object, property, ..
3176            } => {
3177                self.compile_expr(b, object)?; // [recv]
3178                self.name_const(b, property); // [recv, name]
3179                (ops::GETATTR, ops::SETATTR)
3180            }
3181            Expr::Index { object, index, .. } => {
3182                self.compile_expr(b, object)?; // [recv]
3183                self.compile_expr(b, index)?; // [recv, idx]
3184                (ops::GETITEM, ops::SETITEM)
3185            }
3186            // An identifier (or anything else `compile_bind` accepts): no
3187            // reference to preserve, so read it, combine, and bind the result.
3188            _ => return self.compile_compound_ident(b, target, aop, value),
3189        };
3190        b.emit(Op::Dup2, 0); // [recv, key, recv, key]
3191        b.emit(Op::CallBuiltin(get, 2), 0); // [recv, key, old]
3192        match aop {
3193            AssignOp::Binary(op) => {
3194                self.emit_compound_binop(b, op, value)?; // [recv, key, new]
3195                b.emit(Op::CallBuiltin(set, 3), 0); // [new]
3196            }
3197            AssignOp::Logical(lop) => {
3198                // Short-circuit: the write is skipped entirely when the old
3199                // value already decides the result. That is the case the
3200                // duplicating desugaring got most visibly wrong — it evaluated
3201                // the target a second time to perform a write that the spec
3202                // says never happens.
3203                b.emit(Op::Dup, 0); // [recv, key, old, old]
3204                let test_op = match lop {
3205                    LogicalOp::And | LogicalOp::Or => ops::TRUTHY,
3206                    LogicalOp::Nullish => ops::NULLISH,
3207                };
3208                b.emit(Op::CallBuiltin(test_op, 1), 0); // [recv, key, old, cond]
3209                let skip = match lop {
3210                    LogicalOp::And => b.emit(Op::JumpIfFalse(0), 0), // falsy -> keep old
3211                    LogicalOp::Or => b.emit(Op::JumpIfTrue(0), 0),   // truthy -> keep old
3212                    LogicalOp::Nullish => b.emit(Op::JumpIfFalse(0), 0), // non-nullish -> keep old
3213                };
3214                b.emit(Op::Pop, 0); // drop old: [recv, key]
3215                self.compile_expr(b, value)?; // [recv, key, rhs]
3216                b.emit(Op::CallBuiltin(set, 3), 0); // [rhs]
3217                let done = b.emit(Op::Jump(0), 0);
3218                // Short-circuit landing: `[recv, key, old]` has to become
3219                // `[old]` with no write. There is no "drop the two below the
3220                // top", so the old value is rotated under and the reference
3221                // popped out from beneath it.
3222                let short = b.current_pos();
3223                b.patch_jump(skip, short);
3224                b.emit(Op::Rot, 0); // [key, old, recv]
3225                b.emit(Op::Pop, 0); // [key, old]
3226                b.emit(Op::Swap, 0); // [old, key]
3227                b.emit(Op::Pop, 0); // [old]
3228                let end = b.current_pos();
3229                b.patch_jump(done, end);
3230            }
3231        }
3232        Ok(())
3233    }
3234
3235    /// `x op= value` for an identifier target: the name may be read twice with
3236    /// no observable difference, so this keeps the pre-existing desugaring.
3237    fn compile_compound_ident(
3238        &mut self,
3239        b: &mut ChunkBuilder,
3240        target: &Expr,
3241        aop: AssignOp,
3242        value: &Expr,
3243    ) -> Result<(), String> {
3244        let rebuilt = match aop {
3245            AssignOp::Binary(op) => {
3246                Expr::Binary(op, Box::new(target.clone()), Box::new(value.clone()))
3247            }
3248            AssignOp::Logical(lop) => {
3249                Expr::Logical(lop, Box::new(target.clone()), Box::new(value.clone()))
3250            }
3251        };
3252        self.compile_expr(
3253            b,
3254            &Expr::Assign {
3255                target: Box::new(target.clone()),
3256                op: None,
3257                value: Box::new(rebuilt),
3258            },
3259        )
3260    }
3261
3262    /// The old value is already on the stack; compile `rhs` and combine the two
3263    /// with `op`, leaving one value in their place.
3264    ///
3265    /// This mirrors [`Self::compile_binary`], which cannot be reused because it
3266    /// compiles both operands itself — and for the bitwise family it pushes an
3267    /// operator TAG *below* them, which is why those arms slide the tag under
3268    /// the already-present old value rather than simply emitting it.
3269    fn emit_compound_binop(
3270        &mut self,
3271        b: &mut ChunkBuilder,
3272        op: BinOp,
3273        rhs: &Expr,
3274    ) -> Result<(), String> {
3275        let bitwise = match op {
3276            BinOp::BitAnd => Some(bop::BITAND),
3277            BinOp::BitOr => Some(bop::BITOR),
3278            BinOp::BitXor => Some(bop::BITXOR),
3279            BinOp::Shl => Some(bop::SHL),
3280            BinOp::Shr => Some(bop::SHR),
3281            BinOp::UShr => Some(bop::USHR),
3282            _ => None,
3283        };
3284        if let Some(tag) = bitwise {
3285            b.emit(Op::LoadInt(tag), 0); // [old, tag]
3286            b.emit(Op::Swap, 0); // [tag, old]
3287            self.compile_expr(b, rhs)?; // [tag, old, rhs]
3288            b.emit(Op::CallBuiltin(ops::BINOP, 3), 0);
3289            return Ok(());
3290        }
3291        self.compile_expr(b, rhs)?; // [old, rhs]
3292        match op {
3293            BinOp::Add => b.emit(Op::Add, 0),
3294            BinOp::Sub => b.emit(Op::Sub, 0),
3295            BinOp::Mul => b.emit(Op::Mul, 0),
3296            BinOp::Mod => b.emit(Op::Mod, 0),
3297            // `/` and `**` are builtins rather than the native ops, for the same
3298            // reason `compile_binary` routes them that way: fusevm's division
3299            // answers `Undef` on a zero divisor and its `pow` is IEEE-754.
3300            BinOp::Div => b.emit(Op::CallBuiltin(ops::DIV, 2), 0),
3301            BinOp::Pow => b.emit(Op::CallBuiltin(ops::POW, 2), 0),
3302            // No other operator has an `op=` spelling.
3303            _ => return Err(format!("unsupported compound assignment operator {op:?}")),
3304        };
3305        Ok(())
3306    }
3307
3308    fn compile_unary(&mut self, b: &mut ChunkBuilder, op: UnOp, e: &Expr) -> Result<(), String> {
3309        match op {
3310            UnOp::Neg => {
3311                self.compile_expr(b, e)?;
3312                b.emit(Op::Negate, 0);
3313            }
3314            UnOp::Not => {
3315                self.compile_condition(b, e)?;
3316                b.emit(Op::LogNot, 0);
3317            }
3318            UnOp::Pos => {
3319                b.emit(Op::LoadInt(unop::POS), 0);
3320                self.compile_expr(b, e)?;
3321                b.emit(Op::CallBuiltin(ops::UNARY, 2), 0);
3322            }
3323            UnOp::BitNot => {
3324                b.emit(Op::LoadInt(unop::BITNOT), 0);
3325                self.compile_expr(b, e)?;
3326                b.emit(Op::CallBuiltin(ops::UNARY, 2), 0);
3327            }
3328            UnOp::TypeOf => {
3329                // `typeof <bare ident>` must NOT throw when the name is unbound —
3330                // JS returns "undefined". Route a plain identifier through a
3331                // non-throwing name read; any other operand evaluates normally.
3332                if let Expr::Ident(n) = e {
3333                    // A slotted local is always bound by the time it is read
3334                    // (that is rule 3 of the slot analysis), so there is no
3335                    // unbound case for `TYPEOF_NAME` to absorb.
3336                    if let Some(slot) = self.slot_of(n) {
3337                        b.emit(Op::GetSlot(slot), 0);
3338                        b.emit(Op::CallBuiltin(ops::TYPEOF, 1), 0);
3339                        return Ok(());
3340                    }
3341                    self.name_const(b, n);
3342                    b.emit(Op::CallBuiltin(ops::TYPEOF_NAME, 1), 0);
3343                } else {
3344                    self.compile_expr(b, e)?;
3345                    b.emit(Op::CallBuiltin(ops::TYPEOF, 1), 0);
3346                }
3347            }
3348            UnOp::Void => {
3349                self.compile_expr(b, e)?;
3350                b.emit(Op::Pop, 0);
3351                b.emit(Op::LoadUndef, 0);
3352            }
3353            // STRICT mode turns a refused delete into a TypeError (13.5.1.2
3354            // step 5.b). Strictness is static, so it rides along as a third
3355            // operand rather than being looked up at run time — and the error
3356            // is raised where the key and the receiver are both still in hand,
3357            // which a compiler-side check after the Bool could not manage.
3358            UnOp::Delete if self.strict && matches!(e, Expr::Ident(_)) => {
3359                // 13.5.1.1: `delete x` on a plain name is an early error in
3360                // strict code, whatever `x` is bound to.
3361                return Err(
3362                    "SyntaxError: Delete of an unqualified identifier in strict mode.".to_string(),
3363                );
3364            }
3365            UnOp::Delete => match e {
3366                Expr::Member {
3367                    object, property, ..
3368                } => {
3369                    self.compile_expr(b, object)?;
3370                    self.name_const(b, property);
3371                    self.emit_bool(b, self.strict);
3372                    b.emit(Op::CallBuiltin(ops::DELPROP_NAME, 3), 0);
3373                }
3374                Expr::Index { object, index, .. } => {
3375                    self.compile_expr(b, object)?;
3376                    self.compile_expr(b, index)?;
3377                    self.emit_bool(b, self.strict);
3378                    b.emit(Op::CallBuiltin(ops::DELITEM, 3), 0);
3379                }
3380                _ => {
3381                    b.emit(Op::LoadTrue, 0);
3382                }
3383            },
3384        }
3385        Ok(())
3386    }
3387
3388    fn compile_binary(
3389        &mut self,
3390        b: &mut ChunkBuilder,
3391        op: BinOp,
3392        l: &Expr,
3393        r: &Expr,
3394    ) -> Result<(), String> {
3395        // Native fast path (JIT-traceable); the numeric hook supplies JS
3396        // semantics for non-number operands.
3397        macro_rules! native {
3398            ($opc:expr) => {{
3399                self.compile_expr(b, l)?;
3400                self.compile_expr(b, r)?;
3401                b.emit($opc, 0);
3402                return Ok(());
3403            }};
3404        }
3405        match op {
3406            BinOp::Add => native!(Op::Add),
3407            BinOp::Sub => native!(Op::Sub),
3408            BinOp::Mul => native!(Op::Mul),
3409            BinOp::Div => {
3410                // NOT native `Op::Div`: fusevm returns `Undef` for a zero divisor,
3411                // but JS needs `x/0 === ±Infinity` / `0/0 === NaN`, so `/` is a
3412                // builtin (fusevm's own documented pattern for non-default `/`).
3413                self.compile_expr(b, l)?;
3414                self.compile_expr(b, r)?;
3415                b.emit(Op::CallBuiltin(ops::DIV, 2), 0);
3416                return Ok(());
3417            }
3418            BinOp::Mod => native!(Op::Mod),
3419            // NOT native `Op::Pow`, for the same reason `/` is a builtin above:
3420            // fusevm's is IEEE-754 `pow`, where `(-1) ** Infinity` and `1 ** NaN`
3421            // come back 1 rather than the spec's NaN.
3422            BinOp::Pow => {
3423                self.compile_expr(b, l)?;
3424                self.compile_expr(b, r)?;
3425                b.emit(Op::CallBuiltin(ops::POW, 2), 0);
3426                return Ok(());
3427            }
3428            BinOp::Lt => native!(Op::NumLt),
3429            BinOp::Le => native!(Op::NumLe),
3430            BinOp::Gt => native!(Op::NumGt),
3431            BinOp::Ge => native!(Op::NumGe),
3432            BinOp::EqEqEq => {
3433                self.compile_expr(b, l)?;
3434                self.compile_expr(b, r)?;
3435                b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
3436            }
3437            BinOp::NeEqEq => {
3438                self.compile_expr(b, l)?;
3439                self.compile_expr(b, r)?;
3440                b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
3441                b.emit(Op::LogNot, 0);
3442            }
3443            BinOp::EqEq => {
3444                self.compile_expr(b, l)?;
3445                self.compile_expr(b, r)?;
3446                b.emit(Op::CallBuiltin(ops::LOOSE_EQ, 2), 0);
3447            }
3448            BinOp::NeEq => {
3449                self.compile_expr(b, l)?;
3450                self.compile_expr(b, r)?;
3451                b.emit(Op::CallBuiltin(ops::LOOSE_EQ, 2), 0);
3452                b.emit(Op::LogNot, 0);
3453            }
3454            BinOp::In => {
3455                // `#field in obj` is the private-brand check: the left operand is a
3456                // private NAME, not a variable read, so it lowers to the key string
3457                // (private fields live as `#`-prefixed properties on the instance).
3458                match l {
3459                    Expr::Ident(n) if n.starts_with('#') => self.name_const(b, n),
3460                    _ => self.compile_expr(b, l)?,
3461                }
3462                self.compile_expr(b, r)?;
3463                b.emit(Op::CallBuiltin(ops::CONTAINS, 2), 0);
3464            }
3465            BinOp::InstanceOf => {
3466                self.compile_expr(b, l)?;
3467                self.compile_expr(b, r)?;
3468                b.emit(Op::CallBuiltin(ops::INSTANCEOF, 2), 0);
3469            }
3470            BinOp::BitAnd => self.emit_bitwise(b, bop::BITAND, l, r)?,
3471            BinOp::BitOr => self.emit_bitwise(b, bop::BITOR, l, r)?,
3472            BinOp::BitXor => self.emit_bitwise(b, bop::BITXOR, l, r)?,
3473            BinOp::Shl => self.emit_bitwise(b, bop::SHL, l, r)?,
3474            BinOp::Shr => self.emit_bitwise(b, bop::SHR, l, r)?,
3475            BinOp::UShr => self.emit_bitwise(b, bop::USHR, l, r)?,
3476        }
3477        Ok(())
3478    }
3479
3480    fn emit_bitwise(
3481        &mut self,
3482        b: &mut ChunkBuilder,
3483        tag: i64,
3484        l: &Expr,
3485        r: &Expr,
3486    ) -> Result<(), String> {
3487        b.emit(Op::LoadInt(tag), 0);
3488        self.compile_expr(b, l)?;
3489        self.compile_expr(b, r)?;
3490        b.emit(Op::CallBuiltin(ops::BINOP, 3), 0);
3491        Ok(())
3492    }
3493
3494    fn compile_update(
3495        &mut self,
3496        b: &mut ChunkBuilder,
3497        op: UpdateOp,
3498        prefix: bool,
3499        target: &Expr,
3500    ) -> Result<(), String> {
3501        // `NUM_STEP(tag, old)` computes `ToNumeric(old)` and `old ± 1` preserving
3502        // the operand's numeric type — so `x++` on a BigInt stays a BigInt
3503        // (`+old`/`old + 1` would throw the mix error). It pushes the coerced old
3504        // value and returns the new value: stack `[tag, old]` → `[oldN, new]`.
3505        let tag = if matches!(op, UpdateOp::Inc) { 1 } else { -1 };
3506        // A slot that provably holds a Number needs none of that: `ToNumeric`
3507        // is the identity on it and `Number ± 1` is a Number, so the whole
3508        // update is `GetSlot`, a native `Add`, and `SetSlot`. This is what takes
3509        // the last `CallBuiltin` out of a counting loop's body — and with it the
3510        // reason fusevm's tiers decline the loop.
3511        if let Expr::Ident(n) = target {
3512            // `c++` on a `const` is an assignment and throws like one. The
3513            // numeric fast path below writes the slot directly, and the general
3514            // path reaches the check through `compile_bind`, so this has to come
3515            // before both — otherwise `const c = 1; c++` silently incremented a
3516            // constant while `c = 2` correctly threw.
3517            if self.slots.consts.contains(n) {
3518                self.throw_const_assignment(b);
3519                return Ok(());
3520            }
3521            if let Some(slot) = self.numeric_slot_of(n) {
3522                b.emit(Op::GetSlot(slot), 0); // [old]
3523                if !prefix {
3524                    b.emit(Op::Dup, 0); // [old, old]
3525                }
3526                b.emit(Op::LoadFloat(tag as f64), 0);
3527                b.emit(Op::Add, 0); // [ (old,) new ]
3528                if prefix {
3529                    b.emit(Op::Dup, 0); // [new, new]
3530                }
3531                b.emit(Op::SetSlot(slot), 0); // stores, leaves the yielded value
3532                return Ok(());
3533            }
3534        }
3535        b.emit(Op::LoadInt(tag), 0);
3536        self.compile_expr(b, target)?; // [tag, old]
3537        b.emit(Op::CallBuiltin(ops::NUM_STEP, 2), 0); // [oldN, new]
3538        if prefix {
3539            // ++x: discard oldN, store new, yield new.
3540            b.emit(Op::Swap, 0); // [new, oldN]
3541            b.emit(Op::Pop, 0); // [new]
3542            b.emit(Op::Dup, 0); // [new, new]
3543            self.compile_bind(b, target, BindMode::Assign)?; // stores new -> [new]
3544        } else {
3545            // x++: store new, yield oldN.
3546            self.compile_bind(b, target, BindMode::Assign)?; // stores new -> [oldN]
3547        }
3548        Ok(())
3549    }
3550
3551    fn compile_member(
3552        &mut self,
3553        b: &mut ChunkBuilder,
3554        object: &Expr,
3555        property: &str,
3556        optional: bool,
3557    ) -> Result<(), String> {
3558        // `super.prop` — read a data/accessor property off the parent prototype.
3559        if matches!(object, Expr::Super) {
3560            self.name_const(b, property);
3561            b.emit(Op::CallBuiltin(ops::SUPER_GET, 1), 0);
3562            return Ok(());
3563        }
3564        self.compile_expr(b, object)?;
3565        if optional {
3566            let jshort = self.emit_optional_guard(b);
3567            self.name_const(b, property);
3568            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
3569            // Inside a chain the jump belongs to the chain's end, not to this
3570            // link's — otherwise the rest of the chain runs on the `undefined`
3571            // the short-circuit just produced.
3572            match self.opt_chain.last_mut() {
3573                Some(frame) => frame.push(jshort),
3574                None => {
3575                    let end = b.current_pos();
3576                    b.patch_jump(jshort, end);
3577                }
3578            }
3579        } else {
3580            self.name_const(b, property);
3581            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
3582        }
3583        Ok(())
3584    }
3585
3586    fn compile_index(
3587        &mut self,
3588        b: &mut ChunkBuilder,
3589        object: &Expr,
3590        index: &Expr,
3591        optional: bool,
3592    ) -> Result<(), String> {
3593        // `super[expr]` READ — the computed twin of `super.prop`, which
3594        // `compile_member` handles. Without it `super` compiled as a value and
3595        // the read went against `undefined`.
3596        if matches!(object, Expr::Super) {
3597            self.compile_expr(b, index)?;
3598            b.emit(Op::CallBuiltin(ops::SUPER_GET, 1), 0);
3599            return Ok(());
3600        }
3601        self.compile_expr(b, object)?;
3602        if optional {
3603            let jshort = self.emit_optional_guard(b);
3604            self.compile_off_spine(b, index)?;
3605            b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0);
3606            match self.opt_chain.last_mut() {
3607                Some(frame) => frame.push(jshort),
3608                None => {
3609                    let end = b.current_pos();
3610                    b.patch_jump(jshort, end);
3611                }
3612            }
3613        } else {
3614            self.compile_off_spine(b, index)?;
3615            b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0);
3616        }
3617        Ok(())
3618    }
3619
3620    /// For an optional access: object on TOS. If nullish, replace with undefined
3621    /// and jump over the access. Returns the jump index to patch to the end.
3622    // ── block scopes ─────────────────────────────────────────────────────
3623    /// Enter a block scope: `let`/`const` declared after this point die at the
3624    /// matching [`Self::emit_pop_scope`].
3625    /// Push a literal boolean.
3626    fn emit_bool(&self, b: &mut ChunkBuilder, v: bool) {
3627        b.emit(if v { Op::LoadTrue } else { Op::LoadFalse }, 0);
3628    }
3629
3630    fn emit_push_scope(&mut self, b: &mut ChunkBuilder) {
3631        b.emit(Op::CallBuiltin(ops::PUSH_SCOPE, 0), 0);
3632        b.emit(Op::Pop, 0);
3633        self.scope_depth += 1;
3634    }
3635
3636    fn emit_pop_scope(&mut self, b: &mut ChunkBuilder) {
3637        b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
3638        b.emit(Op::Pop, 0);
3639        self.scope_depth -= 1;
3640    }
3641
3642    /// Replace the innermost scope with a copy of its bindings — the per-iteration
3643    /// environment that makes each `for (let i …)` pass capture its own `i`.
3644    fn emit_copy_scope(&self, b: &mut ChunkBuilder) {
3645        b.emit(Op::CallBuiltin(ops::COPY_SCOPE, 0), 0);
3646        b.emit(Op::Pop, 0);
3647    }
3648
3649    /// Close and drop every for-of/for-in iterator between here and `target`
3650    /// depth. A jump to an OUTER loop abandons the inner loops, and their
3651    /// iterators are parked on the VM stack, so they must be popped (running a
3652    /// generator's `finally` / the iterator protocol's `.return()`) or the outer
3653    /// `FORITER` would read the wrong stack slot.
3654    /// Close every iterator this chunk has parked, with a value already on top
3655    /// of the stack that must survive: the iterators sit UNDER it, so each one
3656    /// is swapped up, closed, and its result dropped.
3657    /// Build the chunk being emitted and hand the host its call-site table. Every
3658    /// chunk goes through here so a site is registered exactly once, under the
3659    /// `op_hash` `build()` computes.
3660    fn finish_chunk(&mut self, b: ChunkBuilder) -> Chunk {
3661        let sites = std::mem::take(&mut self.call_sites);
3662        let yields = std::mem::take(&mut self.yield_sites);
3663        let chunk = b.build();
3664        crate::host::register_call_sites(chunk.op_hash, sites);
3665        crate::host::register_yield_sites(chunk.op_hash, yields);
3666        chunk
3667    }
3668
3669    /// Record the callee's source text for the call op just emitted at `at`, so
3670    /// a `TypeError` raised there can name the callee the way V8 does. Nothing
3671    /// is recorded for a shape `callee_text` declines to print.
3672    fn note_call_site(&mut self, at: usize, callee: &Expr) {
3673        if let Some(text) = callee_text(callee) {
3674            self.call_sites.push((at, text));
3675        }
3676    }
3677
3678    fn emit_close_iters_under_value(&self, b: &mut ChunkBuilder) {
3679        for _ in 0..self.iter_depth {
3680            b.emit(Op::Swap, 0); // [.., iter, val] -> [.., val, iter]
3681            b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0); // -> [.., val, result]
3682            b.emit(Op::Pop, 0); // -> [.., val]
3683        }
3684    }
3685
3686    fn emit_close_iters(&self, b: &mut ChunkBuilder, target: usize) {
3687        for _ in target..self.iter_depth {
3688            b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0);
3689            b.emit(Op::Pop, 0);
3690        }
3691    }
3692
3693    /// Close every block scope between here and `target` depth, without changing
3694    /// the compile-time depth (the jump that follows leaves this code path).
3695    fn emit_unwind_scopes(&self, b: &mut ChunkBuilder, target: usize) {
3696        for _ in target..self.scope_depth {
3697            b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
3698            b.emit(Op::Pop, 0);
3699        }
3700    }
3701
3702    /// Raise a `break`/`continue` whose target loop is outside this chunk.
3703    fn emit_signal_jump(&mut self, b: &mut ChunkBuilder, op: u16, label: Option<&str>, line: u32) {
3704        self.name_const(b, label.unwrap_or(""));
3705        b.emit(Op::CallBuiltin(op, 1), line);
3706        b.emit(Op::Pop, line);
3707        self.chunk_signals = true;
3708    }
3709
3710    /// Emit the `SIG_UNWIND` dispatch that runs right after a `TRY` (or after a
3711    /// loop that may still hold a signal for an outer labeled loop): route a
3712    /// pending `break`/`continue` to the enclosing loop's exit/continue target, or
3713    /// halt the chunk so a `return` (or a signal for a loop further out) keeps
3714    /// propagating.
3715    fn emit_signal_dispatch(&mut self, b: &mut ChunkBuilder) {
3716        // `break` lands on the innermost enclosing context, `continue` on the
3717        // innermost one that CATCHES it — a `switch` catches `break` but not
3718        // `continue`, so the two targets are resolved INDEPENDENTLY. Either may be
3719        // absent from this chunk, in which case a signal of that kind keeps
3720        // travelling outward. (`cont` implies `brk`: a continue-catching loop is
3721        // itself breakable, so it can never sit above the innermost context.)
3722        let brk = self
3723            .loops
3724            .len()
3725            .checked_sub(1)
3726            .filter(|i| *i >= self.chunk_loop_base);
3727        let cont = self
3728            .loops
3729            .iter()
3730            .rposition(|c| c.catches_continue)
3731            .filter(|i| *i >= self.chunk_loop_base);
3732        let tag_of = |i: Option<usize>, loops: &[LoopCtx]| match i {
3733            Some(i) => loops[i]
3734                .label
3735                .clone()
3736                .unwrap_or_else(|| unwind::PLAIN_LOOP.to_string()),
3737            None => unwind::NO_LOOP.to_string(),
3738        };
3739        let brk_tag = tag_of(brk, &self.loops);
3740        let cont_tag = tag_of(cont, &self.loops);
3741        self.name_const(b, &brk_tag);
3742        self.name_const(b, &cont_tag);
3743        b.emit(Op::CallBuiltin(ops::SIG_UNWIND, 2), 0); // [code]
3744        let Some(idx) = brk else {
3745            // Nothing in this chunk can catch the signal; `SIG_UNWIND` already
3746            // halted the chunk, so just drop its code.
3747            b.emit(Op::Pop, 0);
3748            return;
3749        };
3750        b.emit(Op::Dup, 0);
3751        b.emit(Op::LoadInt(unwind::BREAK), 0);
3752        b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
3753        let jb = b.emit(Op::JumpIfTrue(0), 0);
3754        let jc = cont.map(|_| {
3755            b.emit(Op::Dup, 0);
3756            b.emit(Op::LoadInt(unwind::CONTINUE), 0);
3757            b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
3758            b.emit(Op::JumpIfTrue(0), 0)
3759        });
3760        b.emit(Op::Pop, 0); // no signal: drop the code and fall through
3761        let jafter = b.emit(Op::Jump(0), 0);
3762        // The landing pads leave every block scope and iterator opened between
3763        // here and the target, exactly as the plain compiler-resolved `break` /
3764        // `continue` does. Skipping this leaked a scope onto the frame, so the
3765        // NEXT `let`/`const` at that level bound in a dead child env and became
3766        // invisible to any closure created afterwards.
3767        let (brk_scope, brk_iter) = (self.loops[idx].break_depth, self.loops[idx].iter_depth);
3768        let brk_land = b.current_pos();
3769        b.emit(Op::Pop, 0);
3770        self.emit_unwind_scopes(b, brk_scope);
3771        self.emit_close_iters(b, brk_iter);
3772        let brk_jump = b.emit(Op::Jump(0), 0);
3773        let cont_jump = jc.map(|_| {
3774            let (cs, ci) = cont
3775                .map(|i| (self.loops[i].continue_depth, self.loops[i].iter_depth))
3776                .unwrap_or((self.scope_depth, self.iter_depth));
3777            let cont_land = b.current_pos();
3778            b.emit(Op::Pop, 0);
3779            self.emit_unwind_scopes(b, cs);
3780            self.emit_close_iters(b, ci);
3781            (cont_land, b.emit(Op::Jump(0), 0))
3782        });
3783        let after = b.current_pos();
3784        b.patch_jump(jb, brk_land);
3785        if let (Some(jc), Some((cont_land, _))) = (jc, cont_jump) {
3786            b.patch_jump(jc, cont_land);
3787        }
3788        b.patch_jump(jafter, after);
3789        self.loops[idx].breaks.push(brk_jump);
3790        if let (Some(cont_idx), Some((_, cj))) = (cont, cont_jump) {
3791            self.loops[cont_idx].continues.push(cj);
3792        }
3793    }
3794
3795    /// Whether `e` is a link in an optional chain that short-circuits — i.e.
3796    /// walking the SPINE (a member's object, an index's object, a call's
3797    /// callee) reaches a `?.`. An argument or a computed index is not on the
3798    /// spine: `a?.b[c?.d]` is two chains, not one.
3799    fn spine_has_optional(e: &Expr) -> bool {
3800        match e {
3801            Expr::Member {
3802                object, optional, ..
3803            } => *optional || Self::spine_has_optional(object),
3804            Expr::Index {
3805                object, optional, ..
3806            } => *optional || Self::spine_has_optional(object),
3807            Expr::Call { func, optional, .. } => *optional || Self::spine_has_optional(func),
3808            _ => false,
3809        }
3810    }
3811
3812    /// Lower `e` as the ROOT of an optional chain: every `?.` inside its spine
3813    /// parks a jump, and all of them land here, past the whole chain.
3814    fn compile_chain_root(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
3815        self.opt_chain.push(Vec::new());
3816        let r = self.compile_expr(b, e);
3817        let pending = self.opt_chain.pop().unwrap_or_default();
3818        r?;
3819        let end = b.current_pos();
3820        for j in pending {
3821            b.patch_jump(j, end);
3822        }
3823        Ok(())
3824    }
3825
3826    /// Lower `e` with the enclosing chain SUSPENDED, so a `?.` inside it forms
3827    /// its own chain. Used for the parts that are not on the spine — call
3828    /// arguments and a computed index.
3829    fn compile_off_spine(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
3830        let saved = std::mem::take(&mut self.opt_chain);
3831        let r = self.compile_expr(b, e);
3832        self.opt_chain = saved;
3833        r
3834    }
3835
3836    fn emit_optional_guard(&mut self, b: &mut ChunkBuilder) -> usize {
3837        b.emit(Op::Dup, 0);
3838        b.emit(Op::CallBuiltin(ops::NULLISH, 1), 0);
3839        let jnull = b.emit(Op::JumpIfFalse(0), 0); // not nullish -> continue access
3840                                                   // nullish: drop object, push undefined, jump to end.
3841        b.emit(Op::Pop, 0);
3842        b.emit(Op::LoadUndef, 0);
3843        let jend = b.emit(Op::Jump(0), 0);
3844        let cont = b.current_pos();
3845        b.patch_jump(jnull, cont);
3846        jend
3847    }
3848
3849    /// `callee?.(args)` — the CALLEE itself may be nullish, in which case the whole
3850    /// call short-circuits to `undefined` without evaluating the arguments. A
3851    /// method callee (`obj.m?.()`) must still be invoked with `this === obj`, so it
3852    /// is dispatched through `m.call(obj, …)` / `m.apply(obj, …)`.
3853    fn compile_optional_call(
3854        &mut self,
3855        b: &mut ChunkBuilder,
3856        func: &Expr,
3857        args: &[Expr],
3858    ) -> Result<(), String> {
3859        let has_spread = args.iter().any(|a| matches!(a, Expr::Spread(_)));
3860        if let Expr::Member {
3861            object,
3862            property,
3863            optional: obj_optional,
3864        } = func
3865        {
3866            self.compile_expr(b, object)?; // [recv]
3867            let jobj = if *obj_optional {
3868                Some(self.emit_optional_guard(b))
3869            } else {
3870                None
3871            };
3872            b.emit(Op::Dup, 0); // [recv, recv]
3873            self.name_const(b, property); // [recv, recv, name]
3874            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [recv, fn]
3875                                                         // Nullish callee: drop both the method and the receiver.
3876            b.emit(Op::Dup, 0);
3877            b.emit(Op::CallBuiltin(ops::NULLISH, 1), 0);
3878            let jlive = b.emit(Op::JumpIfFalse(0), 0);
3879            b.emit(Op::Pop, 0);
3880            b.emit(Op::Pop, 0);
3881            b.emit(Op::LoadUndef, 0);
3882            let jend = b.emit(Op::Jump(0), 0);
3883            let live = b.current_pos();
3884            b.patch_jump(jlive, live);
3885            // [recv, fn] -> fn.call(recv, …) / fn.apply(recv, argsArray)
3886            let via = if has_spread { "apply" } else { "call" };
3887            self.name_const(b, via); // [recv, fn, via]
3888            b.emit(Op::Rot, 0); // [fn, via, recv]
3889            let extra = if has_spread {
3890                self.compile_spread_args(b, args)?; // [fn, via, recv, argsArray]
3891                1
3892            } else {
3893                for a in args {
3894                    self.compile_expr(b, a)?;
3895                }
3896                args.len()
3897            };
3898            b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + extra)?), 0);
3899            match self.opt_chain.last_mut() {
3900                Some(frame) => {
3901                    frame.push(jend);
3902                    if let Some(j) = jobj {
3903                        frame.push(j);
3904                    }
3905                }
3906                None => {
3907                    let end = b.current_pos();
3908                    b.patch_jump(jend, end);
3909                    if let Some(j) = jobj {
3910                        b.patch_jump(j, end);
3911                    }
3912                }
3913            }
3914            return Ok(());
3915        }
3916        // `recv[expr]?.(…)` — the optional-call form of a COMPUTED member. Same
3917        // receiver rule as `recv.name?.(…)` above; only the key differs, being
3918        // known at run time rather than compile time. This used to fall through
3919        // to the plain-callee path below and lose `this`, so `o['self']?.()`
3920        // threw where `o.self?.()` worked.
3921        if let Expr::Index {
3922            object,
3923            index,
3924            optional: obj_optional,
3925        } = func
3926        {
3927            self.compile_expr(b, object)?; // [recv]
3928            let jobj = if *obj_optional {
3929                Some(self.emit_optional_guard(b))
3930            } else {
3931                None
3932            };
3933            b.emit(Op::Dup, 0); // [recv, recv]
3934            self.compile_expr(b, index)?; // [recv, recv, key]
3935            b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0); // [recv, fn]
3936            b.emit(Op::Dup, 0);
3937            b.emit(Op::CallBuiltin(ops::NULLISH, 1), 0);
3938            let jlive = b.emit(Op::JumpIfFalse(0), 0);
3939            b.emit(Op::Pop, 0);
3940            b.emit(Op::Pop, 0);
3941            b.emit(Op::LoadUndef, 0);
3942            let jend = b.emit(Op::Jump(0), 0);
3943            let live = b.current_pos();
3944            b.patch_jump(jlive, live);
3945            let via = if has_spread { "apply" } else { "call" };
3946            self.name_const(b, via); // [recv, fn, via]
3947            b.emit(Op::Rot, 0); // [fn, via, recv]
3948            let extra = if has_spread {
3949                self.compile_spread_args(b, args)?;
3950                1
3951            } else {
3952                for a in args {
3953                    self.compile_expr(b, a)?;
3954                }
3955                args.len()
3956            };
3957            b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + extra)?), 0);
3958            match self.opt_chain.last_mut() {
3959                Some(frame) => {
3960                    frame.push(jend);
3961                    if let Some(j) = jobj {
3962                        frame.push(j);
3963                    }
3964                }
3965                None => {
3966                    let end = b.current_pos();
3967                    b.patch_jump(jend, end);
3968                    if let Some(j) = jobj {
3969                        b.patch_jump(j, end);
3970                    }
3971                }
3972            }
3973            return Ok(());
3974        }
3975        // Plain callee (`f?.()`): evaluate it, guard, then call with no
3976        // receiver — a bare expression has none to keep.
3977        self.compile_expr(b, func)?;
3978        let jend = self.emit_optional_guard(b);
3979        if has_spread {
3980            self.compile_spread_args(b, args)?;
3981            b.emit(Op::CallBuiltin(ops::APPLY, 2), 0);
3982        } else {
3983            for a in args {
3984                self.compile_expr(b, a)?;
3985            }
3986            b.emit(Op::CallBuiltin(ops::CALL_VALUE, argc(1 + args.len())?), 0);
3987        }
3988        let end = b.current_pos();
3989        b.patch_jump(jend, end);
3990        Ok(())
3991    }
3992
3993    fn compile_call(
3994        &mut self,
3995        b: &mut ChunkBuilder,
3996        func: &Expr,
3997        args: &[Expr],
3998        optional: bool,
3999    ) -> Result<(), String> {
4000        if optional {
4001            return self.compile_optional_call(b, func, args);
4002        }
4003        let has_spread = args.iter().any(|a| matches!(a, Expr::Spread(_)));
4004        match func {
4005            // `super(...args)` — invoke the parent constructor on the current
4006            // `this` (SUPER_CALL runs the parent ctor + this class's field inits).
4007            Expr::Super => {
4008                // A `...spread` has to be EXPANDED here as it is at every other
4009                // call site: compiling it as an ordinary expression passed the
4010                // spread OBJECT as one argument, so `super(...[1, 2])` gave the
4011                // parent the array and left its second parameter undefined.
4012                if has_spread {
4013                    self.compile_spread_args(b, args)?;
4014                    b.emit(Op::CallBuiltin(ops::SUPER_CALL_SPREAD, 1), 0);
4015                    return Ok(());
4016                }
4017                for a in args {
4018                    self.compile_expr(b, a)?;
4019                }
4020                b.emit(Op::CallBuiltin(ops::SUPER_CALL, argc(args.len())?), 0);
4021                return Ok(());
4022            }
4023            // `super.method(...args)` — resolve the parent method, call it bound to
4024            // the current `this` via `method.call(this, ...args)`.
4025            Expr::Member {
4026                object, property, ..
4027            } if matches!(**object, Expr::Super) => {
4028                self.name_const(b, property);
4029                b.emit(Op::CallBuiltin(ops::SUPER_GET, 1), 0); // [method]
4030                                                               // With a spread the argument count is not static, so the call
4031                                                               // goes through `apply` with a run-time array rather than `call`
4032                                                               // with a fixed run. Compiling the spread as an ordinary
4033                                                               // argument handed the parent method the array itself.
4034                if has_spread {
4035                    self.name_const(b, "apply"); // [method, "apply"]
4036                    b.emit(Op::CallBuiltin(ops::THIS, 0), 0); // [method, "apply", this]
4037                    self.compile_spread_args(b, args)?; // [..., argsArray]
4038                    b.emit(Op::CallBuiltin(ops::CALL_METHOD, 4), 0);
4039                    return Ok(());
4040                }
4041                self.name_const(b, "call"); // [method, "call"]
4042                b.emit(Op::CallBuiltin(ops::THIS, 0), 0); // [method, "call", this]
4043                                                          // `method.call(this, ...args)`: compile args and dispatch as a
4044                                                          // method call named "call".
4045                for a in args {
4046                    self.compile_expr(b, a)?;
4047                }
4048                b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + args.len())?), 0);
4049                return Ok(());
4050            }
4051            Expr::Member {
4052                object,
4053                property,
4054                optional,
4055            } => {
4056                self.compile_expr(b, object)?;
4057                // `obj?.method(...)`: if `obj` is nullish, short-circuit the whole
4058                // call to `undefined` (skip the method name, args, and dispatch).
4059                let jshort = if *optional {
4060                    Some(self.emit_optional_guard(b))
4061                } else {
4062                    None
4063                };
4064                self.name_const(b, property);
4065                if has_spread {
4066                    self.compile_spread_args(b, args)?; // [recv, name, argsArray]
4067                    b.emit(Op::CallBuiltin(ops::APPLY_METHOD, 3), 0);
4068                } else {
4069                    // Arguments are not on the chain's spine: a `?.` inside one
4070                    // is its own chain and must not jump past this call.
4071                    for a in args {
4072                        self.compile_off_spine(b, a)?;
4073                    }
4074                    let at = b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(2 + args.len())?), 0);
4075                    self.note_call_site(at, func);
4076                }
4077                if let Some(j) = jshort {
4078                    match self.opt_chain.last_mut() {
4079                        Some(frame) => frame.push(j),
4080                        None => {
4081                            let end = b.current_pos();
4082                            b.patch_jump(j, end);
4083                        }
4084                    }
4085                }
4086            }
4087            // `super[expr](args)` — the computed twin of `super.m(args)` above.
4088            // The dotted form was handled and this was not, so it fell through
4089            // to the ordinary computed-call path, which compiled `super` as a
4090            // value and dispatched on that.
4091            Expr::Index { object, index, .. } if matches!(**object, Expr::Super) => {
4092                self.compile_expr(b, index)?; // [name]
4093                b.emit(Op::CallBuiltin(ops::SUPER_GET, 1), 0); // [method]
4094                self.name_const(b, "call"); // [method, "call"]
4095                b.emit(Op::CallBuiltin(ops::THIS, 0), 0); // [method, "call", this]
4096                for a in args {
4097                    self.compile_expr(b, a)?;
4098                }
4099                b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + args.len())?), 0);
4100                return Ok(());
4101            }
4102            Expr::Index {
4103                object,
4104                index,
4105                optional,
4106            } => {
4107                // recv[expr](args) — evaluate as a method via computed name.
4108                self.compile_expr(b, object)?; // [recv]
4109                                               // `recv?.[expr](...)`: short-circuit to `undefined` when nullish.
4110                let jshort = if *optional {
4111                    Some(self.emit_optional_guard(b))
4112                } else {
4113                    None
4114                };
4115                // 13.3.6 EvaluateCall: the receiver of `recv[expr](...)` is
4116                // `recv`, exactly as for `recv.name(...)`. This used to read the
4117                // function with GETITEM, DROP the receiver, and call the value
4118                // with no `this` — the comment called it "approximated", and it
4119                // silently produced wrong answers rather than errors:
4120                //
4121                //     const o = {x: 42, f() { return this.x }};
4122                //     o.f()      // 42
4123                //     o['f']()   // undefined      <- was
4124                //     c['m']()   // TypeError      <- on a class instance
4125                //
4126                // CALL_METHOD/APPLY_METHOD take the name off the STACK, so a
4127                // computed key dispatches through the same path a static one
4128                // does and keeps the receiver.
4129                self.compile_expr(b, index)?; // [recv, name]
4130                if has_spread {
4131                    self.compile_spread_args(b, args)?; // [recv, name, argsArray]
4132                    b.emit(Op::CallBuiltin(ops::APPLY_METHOD, 3), 0);
4133                } else {
4134                    for a in args {
4135                        self.compile_off_spine(b, a)?;
4136                    }
4137                    let at = b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(2 + args.len())?), 0);
4138                    self.note_call_site(at, func);
4139                }
4140                if let Some(j) = jshort {
4141                    let end = b.current_pos();
4142                    b.patch_jump(j, end);
4143                }
4144            }
4145            // A slotted callee has no name to resolve at run time: it falls
4146            // through to the value path below, which reads the slot and calls
4147            // through `CALL_VALUE`.
4148            Expr::Ident(n) if self.slot_of(n).is_none() => {
4149                self.name_const(b, n);
4150                if has_spread {
4151                    self.compile_spread_args(b, args)?; // [name, argsArray]
4152                                                        // Resolve name to a value, then APPLY.
4153                    b.emit(Op::Swap, 0); // [argsArray, name]
4154                    b.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0); // [argsArray, fn]
4155                    b.emit(Op::Swap, 0); // [fn, argsArray]
4156                    b.emit(Op::CallBuiltin(ops::APPLY, 2), 0);
4157                } else {
4158                    for a in args {
4159                        self.compile_expr(b, a)?;
4160                    }
4161                    let at = b.emit(Op::CallBuiltin(ops::CALL, argc(1 + args.len())?), 0);
4162                    self.note_call_site(at, func);
4163                }
4164            }
4165            _ => {
4166                self.compile_expr(b, func)?;
4167                if has_spread {
4168                    self.compile_spread_args(b, args)?;
4169                    b.emit(Op::CallBuiltin(ops::APPLY, 2), 0);
4170                } else {
4171                    for a in args {
4172                        self.compile_expr(b, a)?;
4173                    }
4174                    let at = b.emit(Op::CallBuiltin(ops::CALL_VALUE, argc(1 + args.len())?), 0);
4175                    self.note_call_site(at, func);
4176                }
4177            }
4178        }
4179        Ok(())
4180    }
4181
4182    /// Build a flat args array from a mix of plain args and `...spread` args.
4183    fn compile_spread_args(&mut self, b: &mut ChunkBuilder, args: &[Expr]) -> Result<(), String> {
4184        for a in args {
4185            match a {
4186                // Tag 3, not 1: a spread in a CALL argument list reports a
4187                // non-iterable differently from one in an ARRAY LITERAL, and
4188                // `BUILD_ARGS` serves both. Node names the missing protocol
4189                // here (`Spread syntax requires ...`) and the VALUE there.
4190                Expr::Spread(inner) => {
4191                    b.emit(Op::LoadInt(3), 0);
4192                    self.compile_expr(b, inner)?;
4193                }
4194                _ => {
4195                    b.emit(Op::LoadInt(0), 0);
4196                    self.compile_expr(b, a)?;
4197                }
4198            }
4199        }
4200        b.emit(Op::CallBuiltin(ops::BUILD_ARGS, argc(args.len() * 2)?), 0);
4201        Ok(())
4202    }
4203
4204    fn compile_new(
4205        &mut self,
4206        b: &mut ChunkBuilder,
4207        callee: &Expr,
4208        args: &[Expr],
4209    ) -> Result<(), String> {
4210        self.compile_expr(b, callee)?;
4211        // A `...spread` argument has to be EXPANDED into the argument list. A
4212        // plain `compile_expr` of one yields the spread object itself, so
4213        // `new C(...[1, 2])` passed the array as a single argument.
4214        if args.iter().any(|a| matches!(a, Expr::Spread(_))) {
4215            // `compile_spread_args` emits its own `BUILD_ARGS`, leaving the flat
4216            // argument array on the stack above the constructor.
4217            self.compile_spread_args(b, args)?;
4218            let at = b.emit(Op::CallBuiltin(ops::NEW_SPREAD, 2), 0);
4219            self.note_call_site(at, callee);
4220            return Ok(());
4221        }
4222        for a in args {
4223            self.compile_expr(b, a)?;
4224        }
4225        let at = b.emit(Op::CallBuiltin(ops::NEW, argc(1 + args.len())?), 0);
4226        self.note_call_site(at, callee);
4227        Ok(())
4228    }
4229}
4230
4231/// A prologue statement applying a parameter default: `if (name === undefined)
4232/// name = default;`.
4233fn default_stmt(name: &str, default: &Expr) -> Stmt {
4234    Stmt::from(StmtKind::If {
4235        test: Expr::Binary(
4236            BinOp::EqEqEq,
4237            Box::new(Expr::Ident(name.to_string())),
4238            Box::new(Expr::Undefined),
4239        ),
4240        cons: Box::new(Stmt::from(StmtKind::Expr(Expr::Assign {
4241            target: Box::new(Expr::Ident(name.to_string())),
4242            op: None,
4243            value: Box::new(default.clone()),
4244        }))),
4245        alt: None,
4246    })
4247}
4248
4249/// Every name a `var` binds inside one function scope, in source order.
4250///
4251/// Descends through block-scoped constructs, because `var` is not block-scoped,
4252/// and stops at a nested `function` declaration, whose body is its own scope.
4253/// Function *expressions* and arrows are inside `Expr`, which is not walked at
4254/// all: a `var` can only be introduced by a statement.
4255fn collect_var_names(s: &Stmt, out: &mut Vec<String>) {
4256    match &s.kind {
4257        StmtKind::Decl {
4258            kind: DeclKind::Var,
4259            decls,
4260        } => {
4261            for d in decls {
4262                pattern_names(&d.target, out);
4263            }
4264        }
4265        StmtKind::Block(body) => body.iter().for_each(|s| collect_var_names(s, out)),
4266        StmtKind::If { cons, alt, .. } => {
4267            collect_var_names(cons, out);
4268            if let Some(a) = alt {
4269                collect_var_names(a, out);
4270            }
4271        }
4272        StmtKind::While { body, .. }
4273        | StmtKind::DoWhile { body, .. }
4274        | StmtKind::Labeled { body, .. } => collect_var_names(body, out),
4275        StmtKind::For { init, body, .. } => {
4276            if let Some(i) = init {
4277                collect_var_names(i, out);
4278            }
4279            collect_var_names(body, out);
4280        }
4281        StmtKind::ForOf {
4282            decl_kind,
4283            target,
4284            body,
4285            ..
4286        }
4287        | StmtKind::ForIn {
4288            decl_kind,
4289            target,
4290            body,
4291            ..
4292        } => {
4293            if *decl_kind == Some(DeclKind::Var) {
4294                pattern_names(target, out);
4295            }
4296            collect_var_names(body, out);
4297        }
4298        StmtKind::Switch { cases, .. } => {
4299            for c in cases {
4300                c.body.iter().for_each(|s| collect_var_names(s, out));
4301            }
4302        }
4303        StmtKind::Try {
4304            block,
4305            handler,
4306            finalizer,
4307        } => {
4308            block.iter().for_each(|s| collect_var_names(s, out));
4309            if let Some((_, body)) = handler {
4310                // The catch PARAMETER is block-scoped to the handler, so it is
4311                // not collected; a `var` in the handler body still hoists.
4312                body.iter().for_each(|s| collect_var_names(s, out));
4313            }
4314            if let Some(f) = finalizer {
4315                f.iter().for_each(|s| collect_var_names(s, out));
4316            }
4317        }
4318        _ => {}
4319    }
4320}
4321
4322/// The binding names a declaration target introduces, destructuring included.
4323fn pattern_names(target: &Expr, out: &mut Vec<String>) {
4324    match target {
4325        Expr::Ident(n) => {
4326            if !out.iter().any(|x| x == n) {
4327                out.push(n.clone());
4328            }
4329        }
4330        Expr::Array(items) => items.iter().for_each(|i| pattern_names(i, out)),
4331        Expr::Object(props) => {
4332            for p in props {
4333                match p {
4334                    Prop::KeyValue { value, .. } => pattern_names(value, out),
4335                    Prop::Spread(e) => pattern_names(e, out),
4336                    Prop::Accessor { .. } => {}
4337                }
4338            }
4339        }
4340        // `[a = 1]` / `{a: b = 1}` — the binding is the target, not the default.
4341        Expr::Assign { target, .. } => pattern_names(target, out),
4342        Expr::Spread(inner) => pattern_names(inner, out),
4343        // A member target (`[obj.x] = …`) assigns a property, binding nothing.
4344        _ => {}
4345    }
4346}
4347
4348/// How node renders the SOURCE of a failed object destructuring: `const {w} =
4349/// v` names `v`. Only the forms whose text can be reproduced exactly are
4350/// rendered; anything else answers `None` and the caller falls back to the
4351/// ordinary property-read error rather than inventing a rendering.
4352fn destructure_source_text(e: &Expr) -> Option<String> {
4353    Some(match e {
4354        Expr::Null => "null".into(),
4355        Expr::Undefined => "undefined".into(),
4356        Expr::Ident(n) => n.clone(),
4357        // A LITERAL names itself: `const [x] = 5` is `5 is not iterable`. An
4358        // OBJECT literal is named too, but by shape rather than by text — empty
4359        // renders `{}` and anything else `{(intermediate value)}`, which is
4360        // what V8 calls a value with no source name.
4361        Expr::Number(n) => crate::host::fmt_number(*n),
4362        Expr::True => "true".into(),
4363        Expr::False => "false".into(),
4364        Expr::Object(props) if props.is_empty() => "{}".into(),
4365        Expr::Object(_) => "{(intermediate value)}".into(),
4366        Expr::Member {
4367            object,
4368            property,
4369            optional: false,
4370        } => format!("{}.{property}", destructure_source_text(object)?),
4371        _ => return None,
4372    })
4373}
4374
4375/// Every name a binding pattern introduces, in source order — one for a plain
4376/// identifier, and the leaves of an object or array pattern otherwise.
4377fn binding_names(target: &Expr) -> Vec<String> {
4378    let mut out = Vec::new();
4379    fn walk(e: &Expr, out: &mut Vec<String>) {
4380        match e {
4381            Expr::Ident(n) => out.push(n.clone()),
4382            Expr::Assign { target, .. } => walk(target, out),
4383            Expr::Spread(inner) => walk(inner, out),
4384            Expr::Array(items) => items.iter().for_each(|i| walk(i, out)),
4385            Expr::Object(props) => {
4386                for p in props {
4387                    match p {
4388                        Prop::KeyValue { value, .. } => walk(value, out),
4389                        Prop::Spread(inner) => walk(inner, out),
4390                        Prop::Accessor { .. } => {}
4391                    }
4392                }
4393            }
4394            _ => {}
4395        }
4396    }
4397    walk(target, &mut out);
4398    out
4399}