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