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}
27
28/// Rebase every func-id and try-id reference so its ids sit above those already
29/// loaded on the host (needed only for incremental loading; a no-op for a single
30/// run).
31pub fn rebase_program(prog: &mut Program, func_off: usize, try_off: usize) {
32    if func_off == 0 && try_off == 0 {
33        return;
34    }
35    rebase_chunk(&mut prog.main, func_off, try_off);
36    for (_, f) in &mut prog.functions {
37        rebase_chunk(&mut f.chunk, func_off, try_off);
38    }
39    for t in &mut prog.tries {
40        rebase_chunk(&mut t.block, func_off, try_off);
41        if let Some((_, hb)) = &mut t.handler {
42            rebase_chunk(hb, func_off, try_off);
43        }
44        if let Some(f) = &mut t.finalizer {
45            rebase_chunk(f, func_off, try_off);
46        }
47    }
48}
49
50fn rebase_chunk(chunk: &mut Chunk, func_off: usize, try_off: usize) {
51    for i in 1..chunk.ops.len() {
52        let off = match chunk.ops[i] {
53            Op::CallBuiltin(id, _) if id == ops::MKFUNC => func_off,
54            Op::CallBuiltin(id, 1) if id == ops::TRY => try_off,
55            _ => continue,
56        };
57        if off == 0 {
58            continue;
59        }
60        if let Op::LoadInt(v) = &mut chunk.ops[i - 1] {
61            *v += off as i64;
62        }
63    }
64    for sub in &mut chunk.sub_chunks {
65        rebase_chunk(sub, func_off, try_off);
66    }
67}
68
69/// The binding scope a declaration keyword introduces.
70fn bind_mode(kind: DeclKind) -> BindMode {
71    match kind {
72        DeclKind::Var => BindMode::Var,
73        DeclKind::Let => BindMode::Lexical,
74        DeclKind::Const => BindMode::Const,
75    }
76}
77
78/// How a binding site introduces its name.
79#[derive(Clone, Copy, PartialEq, Eq)]
80enum BindMode {
81    /// Plain assignment to an existing binding (`x = 1`, a for-of head without
82    /// `let`/`const`/`var`).
83    Assign,
84    /// `let`/`class`: bound in the innermost BLOCK scope.
85    Lexical,
86    /// `const`: block-scoped like `Lexical`, but IMMUTABLE — a later assignment
87    /// to the name throws `TypeError: Assignment to constant variable.`
88    Const,
89    /// `var` / a hoisted function declaration: bound at FUNCTION scope.
90    Var,
91}
92
93/// Break/continue jump fixups for a loop or switch.
94struct LoopCtx {
95    breaks: Vec<usize>,
96    continues: Vec<usize>,
97    /// Block-scope depth the `break` target expects; a `break` from inside nested
98    /// blocks pops back down to it first.
99    break_depth: usize,
100    /// Block-scope depth the `continue` target expects.
101    continue_depth: usize,
102    /// Number of iterators on the VM stack inside this loop's body.
103    iter_depth: usize,
104    /// Whether `continue` binds here (true for loops, false for `switch`).
105    catches_continue: bool,
106    /// The source label attached to this loop/block, if any (`outer: for …`),
107    /// so labeled `break outer` / `continue outer` can target it directly.
108    label: Option<String>,
109}
110
111#[derive(Default)]
112pub struct Compiler {
113    /// Pending short-circuit jumps for the optional chain being lowered, one
114    /// frame per chain.
115    ///
116    /// `?.` short-circuits the WHOLE chain to its right, not just its own link:
117    /// `o.a?.b.c` is `undefined` when `o.a` is nullish, and never reads `.c`
118    /// off it. Each `?.` therefore parks its jump here and the chain's ROOT
119    /// patches every one of them to the end. An empty stack means no chain is
120    /// open, so a `?.` outside one patches itself as before.
121    opt_chain: Vec<Vec<usize>>,
122    functions: Vec<(String, FuncDef)>,
123    tries: Vec<TryDef>,
124    loops: Vec<LoopCtx>,
125    tmp: usize,
126    /// A label seen immediately before a loop, consumed by that loop's `LoopCtx`
127    /// (`outer: for (…)`); `None` once claimed.
128    pending_label: Option<String>,
129    /// Emit per-statement `DBG_LINE` markers for the DAP debugger (`node --dap`).
130    debug: bool,
131    /// Index into `loops` of the first loop opened by the chunk being emitted.
132    /// A `break`/`continue` targeting a loop BELOW this index leaves the current
133    /// chunk (a `try` body is compiled as its own chunk), so it cannot be a plain
134    /// jump and is raised as a signal instead.
135    chunk_loop_base: usize,
136    /// Whether this chunk contains a signal-raising `break`/`continue`, so loops
137    /// in it must re-dispatch a still-pending signal when they exit.
138    chunk_signals: bool,
139    /// Number of block scopes open at the current emission point, so a jump out of
140    /// them can pop exactly the right number.
141    scope_depth: usize,
142    /// True while compiling an `async function*` body, where `yield*` must drive
143    /// the delegate through the ASYNC iteration protocol.
144    in_async_generator: bool,
145    /// Number of for-of/for-in iterators parked on the VM stack at this point. A
146    /// `break`/`continue` that leaves such a loop must close and drop its iterator,
147    /// otherwise the enclosing loop's `FORITER` would peek at the wrong one.
148    iter_depth: usize,
149    /// Whether the code being emitted is in STRICT mode — a `'use strict'`
150    /// directive prologue on the program or an enclosing function body, or a
151    /// class body (which is strict unconditionally). The only difference it
152    /// makes here is `PutValue` on an unresolvable reference: strict code throws
153    /// `ReferenceError` where sloppy code creates a global.
154    strict: bool,
155    /// Callee SOURCE TEXT per call op of the chunk being emitted, handed to the
156    /// host when the chunk is built so a failed call can name the callee the way
157    /// the source wrote it. Saved and restored around every nested chunk.
158    call_sites: Vec<(usize, String)>,
159    /// Parked-iterator depth per `yield` op of the chunk being emitted, so an
160    /// injected `.return()`/`.throw()` can close the `for…of` / `yield*`
161    /// iterators the halt would otherwise abandon.
162    yield_sites: Vec<(usize, usize)>,
163    /// Locals of the chunk being emitted that live in fusevm frame slots rather
164    /// than the host's scope chain — see [`crate::slots`]. Empty for a chunk the
165    /// analysis refused, so `slot_of` answering `None` is the old path.
166    slots: crate::slots::Plan,
167}
168
169/// Compile a parsed program. `debug` enables per-statement DAP line markers.
170pub fn compile(stmts: &[Stmt], debug: bool) -> Result<Program, String> {
171    let mut c = Compiler {
172        opt_chain: Vec::new(),
173        debug,
174        // Under `--dap` the debugger reads scopes by name out of the host, and a
175        // slot has no name, so a debug run keeps every local a binding.
176        slots: if debug {
177            Default::default()
178        } else {
179            crate::slots::plan(&[], stmts, true)
180        },
181        strict: has_use_strict(stmts),
182        ..Default::default()
183    };
184    let mut b = ChunkBuilder::new();
185    // Hoist function declarations to the top (JS function hoisting).
186    c.hoist_vars(&mut b, stmts)?;
187    c.hoist_funcs(&mut b, stmts)?;
188    c.compile_stmts(&mut b, stmts)?;
189    Ok(Program {
190        main: c.finish_chunk(b),
191        functions: c.functions,
192        tries: c.tries,
193    })
194}
195
196/// Compile leaving the value of the final top-level expression statement on the
197/// stack (the program's completion value), for `eval`/`vm.runInThisContext`. A
198/// non-expression final statement leaves nothing (→ `undefined`).
199pub fn compile_completion(stmts: &[Stmt], debug: bool) -> Result<Program, String> {
200    let mut c = Compiler {
201        opt_chain: Vec::new(),
202        debug,
203        strict: has_use_strict(stmts),
204        ..Default::default()
205    };
206    let mut b = ChunkBuilder::new();
207    c.hoist_vars(&mut b, stmts)?;
208    c.hoist_funcs(&mut b, stmts)?;
209    if let Some((last, rest)) = stmts.split_last() {
210        c.compile_stmts(&mut b, rest)?;
211        if let StmtKind::Expr(e) = &last.kind {
212            // The final expression's value is NOT popped — it is the completion.
213            c.compile_expr(&mut b, e)?;
214        } else {
215            c.compile_stmt(&mut b, last)?;
216        }
217    }
218    Ok(Program {
219        main: c.finish_chunk(b),
220        functions: c.functions,
221        tries: c.tries,
222    })
223}
224
225/// Does this statement list open with a `"use strict"` directive prologue?
226///
227/// A directive prologue is the run of leading statements that are nothing but a
228/// string literal, so `"use strict"` counts only while every statement before it
229/// is also one.
230fn has_use_strict(stmts: &[Stmt]) -> bool {
231    for s in stmts {
232        match &s.kind {
233            StmtKind::Expr(e) => match e {
234                Expr::Str(v) if v == "use strict" => return true,
235                Expr::Str(_) => continue,
236                _ => return false,
237            },
238            _ => return false,
239        }
240    }
241    false
242}
243
244/// The callee's source text, re-printed from its AST the way V8's `CallPrinter`
245/// does for the `TypeError` a failed call raises: `o.a.b`, `o[k]`, `"s".x`,
246/// `3.x`, `o?.a?.zz`. A string-literal computed access normalizes to dot form
247/// (`o['a']` prints `o.a`), which is what node reports.
248///
249/// `None` for any shape this does not print faithfully — the caller then keeps
250/// the bare method name it already used, so an unprinted shape is never given
251/// invented text.
252fn callee_text(e: &Expr) -> Option<String> {
253    Some(match e {
254        Expr::Ident(n) => n.clone(),
255        Expr::This => "this".into(),
256        Expr::Number(n) => crate::host::fmt_number(*n),
257        Expr::Str(s) => format!("\"{s}\""),
258        Expr::True => "true".into(),
259        Expr::False => "false".into(),
260        Expr::Null => "null".into(),
261        Expr::Undefined => "undefined".into(),
262        Expr::Array(items) if items.is_empty() => "[]".into(),
263        Expr::Object(props) if props.is_empty() => "{}".into(),
264        Expr::Member {
265            object,
266            property,
267            optional,
268        } => {
269            let dot = if *optional { "?." } else { "." };
270            format!("{}{dot}{property}", callee_text(object)?)
271        }
272        Expr::Index {
273            object,
274            index,
275            optional,
276        } => {
277            let obj = callee_text(object)?;
278            // A string-literal key that is a plain identifier prints as a dot
279            // access, exactly as node reports it.
280            if let Expr::Str(k) = &**index {
281                if is_identifier(k) {
282                    let dot = if *optional { "?." } else { "." };
283                    return Some(format!("{obj}{dot}{k}"));
284                }
285            }
286            let idx = callee_text(index)?;
287            let open = if *optional { "?.[" } else { "[" };
288            format!("{obj}{open}{idx}]")
289        }
290        // V8 prints a call in a callee position as `f(...)`, whatever its
291        // arguments were: `require('fs').nope()` reports `require(...).nope`.
292        Expr::Call { func, .. } => format!("{}(...)", callee_text(func)?),
293        Expr::Sequence(items) => {
294            let parts: Option<Vec<String>> = items.iter().map(callee_text).collect();
295            format!("({})", parts?.join(" , "))
296        }
297        _ => return None,
298    })
299}
300
301/// Whether `s` can be written after a `.` — the test that decides whether a
302/// string-literal computed access prints in dot form.
303fn is_identifier(s: &str) -> bool {
304    let mut chars = s.chars();
305    match chars.next() {
306        Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
307        _ => return false,
308    }
309    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
310}
311
312fn argc(n: usize) -> Result<u8, String> {
313    u8::try_from(n).map_err(|_| "too many arguments (>255) for one call".to_string())
314}
315
316/// Does this expression already leave a `Value::Bool` on the stack? A condition
317/// that does needs no `TRUTHY` call: `JumpIfFalse` reads the boolean directly.
318///
319/// The gain is one host round-trip per condition evaluation — `for (let i = 0;
320/// i < n; i++)` paid it on every iteration — and it also puts the comparison
321/// immediately before the jump that consumes it, which is what fusevm's block
322/// JIT requires of a bool-producing op (`bool_is_consumed_in_place`).
323///
324/// Every arm listed here is a lowering that ends in a `Bool`: the relational
325/// ops go to `Op::Num{Lt,Le,Gt,Ge}` (the numeric hook's `relational` returns a
326/// Rust `bool`), the equality ops to `STRICT_EQ`/`LOOSE_EQ`, `in` to
327/// `CONTAINS`, `instanceof` to `INSTANCEOF`, and `!`/`!=`/`!==` end in
328/// `Op::LogNot`. Anything else — including `&&`/`||`/`??`, which evaluate to an
329/// OPERAND and not to a boolean — keeps the call.
330fn yields_bool(e: &Expr) -> bool {
331    match e {
332        Expr::True | Expr::False => true,
333        Expr::Unary(UnOp::Not, _) | Expr::Unary(UnOp::Delete, _) => true,
334        Expr::Binary(op, _, _) => matches!(
335            op,
336            BinOp::Lt
337                | BinOp::Le
338                | BinOp::Gt
339                | BinOp::Ge
340                | BinOp::EqEq
341                | BinOp::NeEq
342                | BinOp::EqEqEq
343                | BinOp::NeEqEq
344                | BinOp::In
345                | BinOp::InstanceOf
346        ),
347        _ => false,
348    }
349}
350
351impl Compiler {
352    // ── emit helpers ─────────────────────────────────────────────────────
353    fn name_const(&self, b: &mut ChunkBuilder, s: &str) {
354        let k = b.add_constant(Value::str(s));
355        b.emit(Op::LoadConst(k), 0);
356    }
357    fn strlit(&self, b: &mut ChunkBuilder, s: &str) {
358        let k = b.add_constant(Value::str(s));
359        b.emit(Op::LoadConst(k), 0);
360        b.emit(Op::CallBuiltin(ops::MKSTR, 1), 0);
361    }
362    fn tmp_name(&mut self, tag: &str) -> String {
363        let n = format!(".{tag}{}", self.tmp);
364        self.tmp += 1;
365        n
366    }
367
368    /// Emit MKFUNC for a compiled function template and leave the closure on the
369    /// stack.
370    fn emit_mkfunc(&self, b: &mut ChunkBuilder, def_id: usize) {
371        b.emit(Op::LoadInt(def_id as i64), 0);
372        b.emit(Op::CallBuiltin(ops::MKFUNC, 1), 0);
373    }
374
375    /// Emit the `var` hoisting for one function (or program) scope.
376    ///
377    /// A `var` binding exists from the moment its scope is entered, so
378    /// `f(){ x; var x = 1 }` reads `undefined` where a `let` would throw. The
379    /// walk therefore descends through every block, loop, `switch`, `try` and
380    /// label — `var` ignores block scope — but stops at a nested function, which
381    /// begins a scope of its own. Only the binding is created here; the
382    /// initialiser still runs where it is written.
383    ///
384    /// Emitted BEFORE [`Self::hoist_funcs`] so a function declaration overwrites
385    /// the `undefined` rather than the other way round, which is the order the
386    /// spec instantiates them in.
387    fn hoist_vars(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) -> Result<(), String> {
388        let mut names = Vec::new();
389        for s in stmts {
390            collect_var_names(s, &mut names);
391        }
392        for n in names {
393            // A slotted local is its slot, which already reads `undefined`
394            // before its first write, so there is no binding to create.
395            if self.slot_of(&n).is_some() {
396                continue;
397            }
398            self.name_const(b, &n);
399            b.emit(Op::CallBuiltin(ops::HOIST_VAR, 1), 0);
400            b.emit(Op::Pop, 0);
401        }
402        Ok(())
403    }
404
405    fn hoist_funcs(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) -> Result<(), String> {
406        for s in stmts {
407            if let StmtKind::FuncDecl {
408                name,
409                params,
410                body,
411                is_generator,
412                is_async,
413            } = &s.kind
414            {
415                let def_id = self.build_function(name, params, body, *is_generator, *is_async)?;
416                self.emit_mkfunc(b, def_id);
417                // Function declarations hoist to the enclosing FUNCTION scope.
418                self.declare_as(b, &Expr::Ident(name.clone()), BindMode::Var);
419            }
420        }
421        Ok(())
422    }
423
424    fn compile_stmts(&mut self, b: &mut ChunkBuilder, stmts: &[Stmt]) -> Result<(), String> {
425        for s in stmts {
426            self.compile_stmt(b, s)?;
427        }
428        Ok(())
429    }
430
431    fn compile_stmt(&mut self, b: &mut ChunkBuilder, s: &Stmt) -> Result<(), String> {
432        if self.debug && s.line != 0 {
433            b.emit(Op::LoadInt(s.line as i64), s.line);
434            b.emit(Op::CallBuiltin(ops::DBG_LINE, 1), s.line);
435            b.emit(Op::Pop, s.line);
436        }
437        let line = s.line;
438        match &s.kind {
439            StmtKind::Expr(e) => {
440                self.compile_expr(b, e)?;
441                b.emit(Op::Pop, line);
442            }
443            StmtKind::Empty => {}
444            StmtKind::FuncDecl { .. } => {} // hoisted at block entry
445            StmtKind::ClassDecl(node) => {
446                self.compile_class(b, node)?;
447                // Bind the class to its name in the current scope.
448                if let Some(name) = &node.name {
449                    self.declare(b, &Expr::Ident(name.clone()));
450                } else {
451                    b.emit(Op::Pop, line);
452                }
453            }
454            StmtKind::Decl { kind, decls } => {
455                let mode = bind_mode(*kind);
456                for d in decls {
457                    // `var x;` with no initialiser names a binding that scope
458                    // entry already created, and must NOT reset it — in
459                    // `function f(a) { var a; }` the parameter stands.
460                    if d.init.is_none() && *kind == DeclKind::Var {
461                        continue;
462                    }
463                    match &d.init {
464                        Some(v) => {
465                            self.compile_expr(b, v)?;
466                            // Name inference: `const f = () => {}` / `= function(){}`
467                            // / `= class {}` gives the function/class the name `f`.
468                            if let Expr::Ident(name) = &d.target {
469                                self.infer_name(b, v, name);
470                            }
471                        }
472                        None => {
473                            b.emit(Op::LoadUndef, line);
474                        }
475                    }
476                    self.compile_bind(b, &d.target, mode)?;
477                }
478            }
479            StmtKind::Block(body) => {
480                // A block that declares nothing lexical has nothing to put in a
481                // scope, and opening one costs an `EnvData` allocation and free
482                // every time control enters the block — once per iteration when
483                // the block is a loop body, which is where most of them are.
484                let scoped = crate::capture::block_needs_scope(body);
485                if scoped {
486                    self.emit_push_scope(b);
487                }
488                self.hoist_funcs(b, body)?;
489                self.compile_stmts(b, body)?;
490                if scoped {
491                    self.emit_pop_scope(b);
492                }
493            }
494            StmtKind::If { test, cons, alt } => self.compile_if(b, test, cons, alt)?,
495            StmtKind::While { test, body } => self.compile_while(b, test, body)?,
496            StmtKind::DoWhile { body, test } => self.compile_do_while(b, body, test)?,
497            StmtKind::For {
498                init,
499                test,
500                update,
501                body,
502            } => self.compile_for(b, init, test, update, body)?,
503            StmtKind::ForOf {
504                decl_kind,
505                target,
506                iter,
507                body,
508                is_await,
509            } => {
510                let mode = decl_kind.map(bind_mode).unwrap_or(BindMode::Assign);
511                if *is_await {
512                    self.compile_for_await(b, mode, target, iter, body)?
513                } else {
514                    self.compile_for_of(b, mode, target, iter, body)?
515                }
516            }
517            StmtKind::ForIn {
518                decl_kind,
519                target,
520                object,
521                body,
522            } => {
523                let mode = decl_kind.map(bind_mode).unwrap_or(BindMode::Assign);
524                self.compile_for_in(b, mode, target, object, body)?
525            }
526            StmtKind::Switch { disc, cases } => self.compile_switch(b, disc, cases)?,
527            StmtKind::Return(e) => {
528                match e {
529                    Some(e) => self.compile_expr(b, e)?,
530                    None => {
531                        b.emit(Op::LoadUndef, line);
532                    }
533                }
534                // A `return` out of a `for…of` is an abrupt completion, and
535                // 7.4.9 `IteratorClose` runs the iterator's `return` for it —
536                // which is what makes a generator's `finally` run. `break` and
537                // `continue` already closed theirs; a `return` walked away and
538                // left the iterator suspended forever.
539                self.emit_close_iters_under_value(b);
540                b.emit(Op::CallBuiltin(ops::SIG_RETURN, 1), line);
541            }
542            StmtKind::Labeled { label, body } => self.compile_labeled(b, label, body)?,
543            StmtKind::Break(label) => {
544                let idx = match label {
545                    // `break outer`: the nearest enclosing context carrying that label.
546                    Some(name) => self
547                        .loops
548                        .iter()
549                        .rposition(|c| c.label.as_deref() == Some(name.as_str()))
550                        .ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?,
551                    None => self
552                        .loops
553                        .len()
554                        .checked_sub(1)
555                        .ok_or("SyntaxError: 'break' outside loop")?,
556                };
557                if idx >= self.chunk_loop_base {
558                    self.emit_unwind_scopes(b, self.loops[idx].break_depth);
559                    self.emit_close_iters(b, self.loops[idx].iter_depth);
560                    let j = b.emit(Op::Jump(0), line);
561                    self.loops[idx].breaks.push(j);
562                } else {
563                    self.emit_signal_jump(b, ops::SIG_BREAK, label.as_deref(), line);
564                }
565            }
566            StmtKind::Continue(label) => {
567                let idx = match label {
568                    // `continue outer`: the labeled loop (a label on a non-loop
569                    // cannot catch `continue`).
570                    Some(name) => self
571                        .loops
572                        .iter()
573                        .rposition(|c| {
574                            c.catches_continue && c.label.as_deref() == Some(name.as_str())
575                        })
576                        .ok_or_else(|| {
577                            format!("SyntaxError: Undefined label '{name}' for continue")
578                        })?,
579                    None => self
580                        .loops
581                        .iter()
582                        .rposition(|c| c.catches_continue)
583                        .ok_or("SyntaxError: 'continue' outside loop")?,
584                };
585                if idx >= self.chunk_loop_base {
586                    self.emit_unwind_scopes(b, self.loops[idx].continue_depth);
587                    self.emit_close_iters(b, self.loops[idx].iter_depth);
588                    let j = b.emit(Op::Jump(0), line);
589                    self.loops[idx].continues.push(j);
590                } else {
591                    self.emit_signal_jump(b, ops::SIG_CONTINUE, label.as_deref(), line);
592                }
593            }
594            StmtKind::Throw(e) => {
595                self.compile_expr(b, e)?;
596                b.emit(Op::CallBuiltin(ops::THROW, 1), line);
597            }
598            StmtKind::Try {
599                block,
600                handler,
601                finalizer,
602            } => self.compile_try(b, block, handler, finalizer)?,
603        }
604        Ok(())
605    }
606
607    // ── binding / assignment ─────────────────────────────────────────────
608    /// Store the value on top of the stack into `target`. `declare` chooses
609    /// `DECLARE` (new binding) vs `SETLOCAL` (existing binding / global).
610    fn compile_bind(
611        &mut self,
612        b: &mut ChunkBuilder,
613        target: &Expr,
614        declare: BindMode,
615    ) -> Result<(), String> {
616        match target {
617            Expr::Ident(_) => {
618                if declare == BindMode::Assign {
619                    self.store_simple(b, target)?;
620                } else {
621                    self.declare_as(b, target, declare);
622                }
623            }
624            Expr::Member { .. } | Expr::Index { .. } => {
625                self.store_simple(b, target)?;
626            }
627            Expr::Array(items) => self.destructure_array(b, items, declare)?,
628            Expr::Object(props) => self.destructure_object(b, props, declare)?,
629            Expr::Assign { target, value } => {
630                // Pattern element with a default: use it when TOS is undefined.
631                b.emit(Op::Dup, 0);
632                b.emit(Op::LoadUndef, 0);
633                b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
634                let jf = b.emit(Op::JumpIfFalse(0), 0);
635                b.emit(Op::Pop, 0); // drop the undefined
636                self.compile_expr(b, value)?;
637                // 8.6.3 / 14.3.3: a destructuring default whose target is a
638                // single binding identifier names an anonymous function after
639                // it — `const {a = function(){}} = {}` gives `a.name === "a"`.
640                if let Expr::Ident(n) = &**target {
641                    self.infer_name(b, value, n);
642                }
643                let end = b.current_pos();
644                b.patch_jump(jf, end);
645                self.compile_bind(b, target, declare)?;
646            }
647            _ => return Err("SyntaxError: invalid assignment target".into()),
648        }
649        Ok(())
650    }
651
652    /// Emit a `DECLARE` of a simple name binding, consuming TOS value.
653    fn declare(&self, b: &mut ChunkBuilder, target: &Expr) {
654        self.declare_as(b, target, BindMode::Lexical);
655    }
656
657    /// Emit the declaration op matching `mode`: block-scoped for `let`/`const`,
658    /// function-scoped for `var` and hoisted function declarations.
659    fn declare_as(&self, b: &mut ChunkBuilder, target: &Expr, mode: BindMode) {
660        if let Expr::Ident(n) = target {
661            // A slotted local has no scope entry to declare into: the binding IS
662            // the store.
663            if let Some(slot) = self.slot_of(n) {
664                b.emit(Op::SetSlot(slot), 0);
665                return;
666            }
667            let op = match mode {
668                BindMode::Var => ops::DECLARE_VAR,
669                BindMode::Const => ops::DECLARE_CONST,
670                _ => ops::DECLARE,
671            };
672            self.name_const(b, n);
673            b.emit(Op::Swap, 0);
674            b.emit(Op::CallBuiltin(op, 2), 0);
675            b.emit(Op::Pop, 0);
676        }
677    }
678
679    /// Emit `throw new TypeError("Assignment to constant variable.")`.
680    ///
681    /// A store to a `const` is a RUNTIME error, not a parse error — the spec
682    /// puts it in SetMutableBinding (8.5.2), so `try { const c=1; c=2 } catch {}`
683    /// has to catch it. Emitting the throw in place of the store gives exactly
684    /// that, and costs nothing for every store that is not to a const.
685    fn throw_const_assignment(&mut self, b: &mut ChunkBuilder) {
686        let e = Expr::New {
687            callee: Box::new(Expr::Ident("TypeError".into())),
688            args: vec![Expr::Str("Assignment to constant variable.".into())],
689        };
690        // `New` of a known builtin with a literal argument cannot fail to
691        // compile, so the error path is unreachable rather than swallowed.
692        if self.compile_expr(b, &e).is_ok() {
693            b.emit(Op::CallBuiltin(ops::THROW, 1), 0);
694        }
695    }
696
697    /// Store TOS into an lvalue (Ident/Member/Index), leaving nothing.
698    fn store_simple(&mut self, b: &mut ChunkBuilder, target: &Expr) -> Result<(), String> {
699        match target {
700            Expr::Ident(n) => {
701                // A slotted binding never reaches the host's scope chain, so the
702                // host's immutable-binding check cannot see it. The slot plan is
703                // exact about which names are const (one declaration per name,
704                // unreachable from another chunk, simple identifiers only), so
705                // the store is rejected here instead — at run time, as the spec
706                // requires, since `try { const c=1; c=2 } catch {}` must CATCH
707                // this rather than fail to parse.
708                if self.slots.consts.contains(n) {
709                    b.emit(Op::Pop, 0); // drop the value that will never be stored
710                    self.throw_const_assignment(b);
711                    return Ok(());
712                }
713                if let Some(slot) = self.slot_of(n) {
714                    b.emit(Op::SetSlot(slot), 0);
715                    return Ok(());
716                }
717                self.name_const(b, n);
718                b.emit(Op::Swap, 0);
719                // `PutValue` (6.2.5.6) on an unresolvable reference: strict code
720                // throws `ReferenceError`, sloppy code creates a global.
721                let op = if self.strict {
722                    ops::SETLOCAL_STRICT
723                } else {
724                    ops::SETLOCAL
725                };
726                b.emit(Op::CallBuiltin(op, 2), 0);
727                b.emit(Op::Pop, 0);
728            }
729            Expr::Member {
730                object, property, ..
731            } => {
732                self.compile_expr(b, object)?; // [value, recv]
733                self.name_const(b, property); // [value, recv, name]
734                b.emit(Op::Rot, 0); // [recv, name, value]
735                b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0);
736                b.emit(Op::Pop, 0);
737            }
738            Expr::Index { object, index, .. } => {
739                self.compile_expr(b, object)?; // [value, recv]
740                self.compile_expr(b, index)?; // [value, recv, idx]
741                b.emit(Op::Rot, 0); // [recv, idx, value]
742                b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0);
743                b.emit(Op::Pop, 0);
744            }
745            _ => return Err("SyntaxError: invalid assignment target".into()),
746        }
747        Ok(())
748    }
749
750    fn destructure_array(
751        &mut self,
752        b: &mut ChunkBuilder,
753        items: &[Expr],
754        declare: BindMode,
755    ) -> Result<(), String> {
756        let star_idx = items
757            .iter()
758            .position(|e| matches!(e, Expr::Spread(_)))
759            .map(|i| i as i64)
760            .unwrap_or(-1);
761        b.emit(Op::LoadInt(items.len() as i64), 0);
762        b.emit(Op::LoadInt(star_idx), 0);
763        b.emit(Op::CallBuiltin(ops::UNPACK, 3), 0); // pushes items[0]..items[n-1], items[0] on top
764        for it in items {
765            match it {
766                // An elided target position (`const [a, , b] = xs`) still
767                // consumes its unpacked value; nothing is bound to it.
768                Expr::Hole | Expr::Undefined => {
769                    b.emit(Op::Pop, 0);
770                }
771                Expr::Spread(inner) => self.compile_bind(b, inner, declare)?,
772                _ => self.compile_bind(b, it, declare)?,
773            }
774        }
775        Ok(())
776    }
777
778    fn destructure_object(
779        &mut self,
780        b: &mut ChunkBuilder,
781        props: &[Prop],
782        declare: BindMode,
783    ) -> Result<(), String> {
784        // Object value on TOS; keep it, read each key, bind, then drop.
785        let obj_tmp = self.tmp_name("destr");
786        self.name_const(b, &obj_tmp);
787        b.emit(Op::Swap, 0);
788        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
789        b.emit(Op::Pop, 0);
790        // Collect statically-known destructured key names, for a `...rest`.
791        let mut named: Vec<String> = Vec::new();
792        for p in props {
793            match p {
794                Prop::KeyValue { key, value, .. } => {
795                    if let Expr::Str(s) = key {
796                        named.push(s.clone());
797                    }
798                    // Load obj, read key.
799                    self.load_local(b, &obj_tmp);
800                    self.compile_expr(b, key)?;
801                    b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0); // [value]
802                    self.compile_bind(b, value, declare)?;
803                }
804                Prop::Spread(target) => {
805                    self.load_local(b, &obj_tmp);
806                    for k in &named {
807                        self.strlit(b, k);
808                    }
809                    b.emit(Op::CallBuiltin(ops::MKARR, argc(named.len())?), 0);
810                    b.emit(Op::CallBuiltin(ops::OBJ_REST, 2), 0); // [rest_object]
811                    self.compile_bind(b, target, declare)?;
812                }
813                // Accessors never appear in a destructuring pattern.
814                Prop::Accessor { .. } => {}
815            }
816        }
817        Ok(())
818    }
819
820    fn load_local(&self, b: &mut ChunkBuilder, name: &str) {
821        if let Some(slot) = self.slot_of(name) {
822            b.emit(Op::GetSlot(slot), 0);
823            return;
824        }
825        self.name_const(b, name);
826        b.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0);
827    }
828
829    /// The frame slot holding `name` in the chunk being emitted, if it has one.
830    fn slot_of(&self, name: &str) -> Option<u16> {
831        self.slots.table.get(name).copied()
832    }
833
834    /// The slot for `name` if it also provably holds a Number, so `++`/`--` can
835    /// be a native add rather than a `NUM_STEP` round-trip through the host.
836    fn numeric_slot_of(&self, name: &str) -> Option<u16> {
837        self.slots
838            .numeric
839            .contains(name)
840            .then(|| self.slot_of(name))
841            .flatten()
842    }
843
844    // ── control flow ─────────────────────────────────────────────────────
845    fn compile_condition(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
846        self.compile_expr(b, e)?;
847        if !yields_bool(e) {
848            b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
849        }
850        Ok(())
851    }
852
853    fn compile_if(
854        &mut self,
855        b: &mut ChunkBuilder,
856        test: &Expr,
857        cons: &Stmt,
858        alt: &Option<Box<Stmt>>,
859    ) -> Result<(), String> {
860        self.compile_condition(b, test)?;
861        let jfalse = b.emit(Op::JumpIfFalse(0), 0);
862        self.compile_stmt(b, cons)?;
863        if let Some(alt) = alt {
864            let jend = b.emit(Op::Jump(0), 0);
865            let else_start = b.current_pos();
866            b.patch_jump(jfalse, else_start);
867            self.compile_stmt(b, alt)?;
868            let end = b.current_pos();
869            b.patch_jump(jend, end);
870        } else {
871            let end = b.current_pos();
872            b.patch_jump(jfalse, end);
873        }
874        Ok(())
875    }
876
877    /// `label: stmt`. If the body is a loop, the label rides into that loop's
878    /// `LoopCtx` (so labeled `break`/`continue` target it); otherwise a break-only
879    /// context spans the body so `break label` can jump past it.
880    fn compile_labeled(
881        &mut self,
882        b: &mut ChunkBuilder,
883        label: &str,
884        body: &Stmt,
885    ) -> Result<(), String> {
886        if matches!(
887            body.kind,
888            StmtKind::While { .. }
889                | StmtKind::DoWhile { .. }
890                | StmtKind::For { .. }
891                | StmtKind::ForOf { .. }
892                | StmtKind::ForIn { .. }
893        ) {
894            self.pending_label = Some(label.to_string());
895            self.compile_stmt(b, body)?;
896            // The loop claimed it; clear any residue defensively.
897            self.pending_label = None;
898        } else {
899            self.loops.push(LoopCtx {
900                breaks: Vec::new(),
901                continues: Vec::new(),
902                break_depth: self.scope_depth,
903                continue_depth: self.scope_depth,
904                iter_depth: self.iter_depth,
905                catches_continue: false,
906                label: Some(label.to_string()),
907            });
908            self.compile_stmt(b, body)?;
909            let ctx = self.loops.pop().unwrap();
910            let end = b.current_pos();
911            for br in ctx.breaks {
912                b.patch_jump(br, end);
913            }
914            self.redispatch_after_loop(b);
915        }
916        Ok(())
917    }
918
919    /// After a loop/switch exits, a signal raised deeper in this chunk may still be
920    /// pending (a LABELED `break`/`continue` for an OUTER loop). Re-dispatch it one
921    /// level out. Emitted only when this chunk actually raises signals.
922    fn redispatch_after_loop(&mut self, b: &mut ChunkBuilder) {
923        if self.chunk_signals {
924            self.emit_signal_dispatch(b);
925        }
926    }
927
928    /// `while (test) body`, lowered ROTATED: the test is emitted once as an entry
929    /// guard and once at the bottom, so the loop closes with a CONDITIONAL
930    /// backward branch rather than an unconditional `Jump` back to a test at the
931    /// top.
932    ///
933    /// That shape is what fusevm's tracing JIT needs — it only closes a trace on
934    /// a conditional backward branch. Emitted the other way, `--tiers` reported
935    /// `trace-eligible=true traced=false` and `reaches native code false` for
936    /// every `for` and `while` this frontend produced, while the same arithmetic
937    /// written as `do { … } while (…)` — the one loop form that already ended in
938    /// a conditional branch — reported `traced=true`. Measured on a debug build:
939    /// `for (let i = 0; i < 3000000; i++) s += i` took 5.76s of user CPU
940    /// unrotated and 0.02s rotated.
941    ///
942    /// Evaluation order and count are unchanged: a top-test loop runs the test
943    /// `n + 1` times for `n` iterations, and so does this — one entry test, then
944    /// one after each pass. Rotation costs one copy of the condition's code and
945    /// saves one jump per iteration.
946    fn compile_while(
947        &mut self,
948        b: &mut ChunkBuilder,
949        test: &Expr,
950        body: &Stmt,
951    ) -> Result<(), String> {
952        self.compile_condition(b, test)?;
953        let jfalse = b.emit(Op::JumpIfFalse(0), 0);
954        let top = b.current_pos();
955        self.loops.push(LoopCtx {
956            breaks: Vec::new(),
957            continues: Vec::new(),
958            break_depth: self.scope_depth,
959            continue_depth: self.scope_depth,
960            iter_depth: self.iter_depth,
961            catches_continue: true,
962            label: self.pending_label.take(),
963        });
964        self.compile_stmt(b, body)?;
965        // `continue` re-tests the condition, which is now the BOTTOM copy of it.
966        let cont_target = b.current_pos();
967        self.compile_condition(b, test)?;
968        b.emit(Op::JumpIfTrue(top), 0);
969        let ctx = self.loops.pop().unwrap();
970        for c in ctx.continues {
971            b.patch_jump(c, cont_target);
972        }
973        let end = b.current_pos();
974        b.patch_jump(jfalse, end);
975        for br in ctx.breaks {
976            b.patch_jump(br, end);
977        }
978        self.redispatch_after_loop(b);
979        Ok(())
980    }
981
982    fn compile_do_while(
983        &mut self,
984        b: &mut ChunkBuilder,
985        body: &Stmt,
986        test: &Expr,
987    ) -> Result<(), String> {
988        let start = b.current_pos();
989        self.loops.push(LoopCtx {
990            breaks: Vec::new(),
991            continues: Vec::new(),
992            break_depth: self.scope_depth,
993            continue_depth: self.scope_depth,
994            iter_depth: self.iter_depth,
995            catches_continue: true,
996            label: self.pending_label.take(),
997        });
998        self.compile_stmt(b, body)?;
999        let cont_target = b.current_pos();
1000        self.compile_condition(b, test)?;
1001        b.emit(Op::JumpIfTrue(start), 0);
1002        let ctx = self.loops.pop().unwrap();
1003        for c in ctx.continues {
1004            b.patch_jump(c, cont_target);
1005        }
1006        let end = b.current_pos();
1007        for br in ctx.breaks {
1008            b.patch_jump(br, end);
1009        }
1010        self.redispatch_after_loop(b);
1011        Ok(())
1012    }
1013
1014    fn compile_for(
1015        &mut self,
1016        b: &mut ChunkBuilder,
1017        init: &Option<Box<Stmt>>,
1018        test: &Option<Expr>,
1019        update: &Option<Expr>,
1020        body: &Stmt,
1021    ) -> Result<(), String> {
1022        // A `let`/`const` head is scoped to the loop AND re-bound per iteration, so
1023        // a closure made in one pass keeps that pass's value (ForBodyEvaluation's
1024        // CreatePerIterationEnvironment). A `var` head belongs to the function.
1025        let lexical_head = matches!(
1026            init.as_deref(),
1027            Some(Stmt {
1028                kind: StmtKind::Decl {
1029                    kind: DeclKind::Let | DeclKind::Const,
1030                    ..
1031                },
1032                ..
1033            })
1034        );
1035        // The loop's own scope is not optional — it is what keeps `let i` from
1036        // leaking past the loop or clobbering an outer `i`. The per-iteration
1037        // COPY of that scope is: only code that can CAPTURE a binding can tell
1038        // one copy per pass from one binding mutated in place, and the copy is a
1039        // whole-scope clone every iteration. A 5M-iteration counting loop spent
1040        // 17% of its samples cloning scopes that nothing could observe.
1041        let per_iteration = lexical_head;
1042        let copy_per_iteration = lexical_head
1043            && (crate::capture::stmt_captures(body)
1044                || init.as_deref().is_some_and(crate::capture::stmt_captures)
1045                || test.as_ref().is_some_and(crate::capture::expr_captures)
1046                || update.as_ref().is_some_and(crate::capture::expr_captures));
1047        if per_iteration {
1048            self.emit_push_scope(b);
1049        }
1050        if let Some(init) = init {
1051            self.compile_stmt(b, init)?;
1052        }
1053        if copy_per_iteration {
1054            self.emit_copy_scope(b);
1055        }
1056        // Rotated, for the reason `compile_while` documents: the test as an entry
1057        // guard plus a conditional backward branch at the bottom.
1058        let jfalse = match test {
1059            Some(t) => {
1060                self.compile_condition(b, t)?;
1061                Some(b.emit(Op::JumpIfFalse(0), 0))
1062            }
1063            None => None,
1064        };
1065        let top = b.current_pos();
1066        self.loops.push(LoopCtx {
1067            breaks: Vec::new(),
1068            continues: Vec::new(),
1069            break_depth: self.scope_depth,
1070            continue_depth: self.scope_depth,
1071            iter_depth: self.iter_depth,
1072            catches_continue: true,
1073            label: self.pending_label.take(),
1074        });
1075        self.compile_stmt(b, body)?;
1076        let cont_target = b.current_pos();
1077        if copy_per_iteration {
1078            // Fresh copy BEFORE the update, so the update advances the NEXT pass's
1079            // binding and the one just captured keeps this pass's value.
1080            self.emit_copy_scope(b);
1081        }
1082        if let Some(u) = update {
1083            self.compile_expr(b, u)?;
1084            b.emit(Op::Pop, 0);
1085        }
1086        match test {
1087            Some(t) => {
1088                self.compile_condition(b, t)?;
1089                b.emit(Op::JumpIfTrue(top), 0);
1090            }
1091            // `for (;;)` has no test to branch on, so the back edge is a
1092            // constant-true CONDITIONAL branch rather than an unconditional
1093            // `Jump`. The distinction is not cosmetic: fusevm's trace compiler
1094            // only ever installs a trace closed by `JumpIfTrue`/`JumpIfFalse`
1095            // and silently declines an `Op::Jump` close, so `for (;;)` stayed
1096            // interpreted while the identical `while (true)` — which already
1097            // emitted `LoadTrue; JumpIfTrue` — reached native code. Measured on
1098            // a debug build, 3M iterations of `s += i`: 4.26s against 0.02s.
1099            None => {
1100                b.emit(Op::LoadTrue, 0);
1101                b.emit(Op::JumpIfTrue(top), 0);
1102            }
1103        }
1104        let ctx = self.loops.pop().unwrap();
1105        for c in ctx.continues {
1106            b.patch_jump(c, cont_target);
1107        }
1108        let end = b.current_pos();
1109        if let Some(jf) = jfalse {
1110            b.patch_jump(jf, end);
1111        }
1112        for br in ctx.breaks {
1113            b.patch_jump(br, end);
1114        }
1115        if per_iteration {
1116            self.emit_pop_scope(b);
1117        }
1118        self.redispatch_after_loop(b);
1119        Ok(())
1120    }
1121
1122    fn compile_for_of(
1123        &mut self,
1124        b: &mut ChunkBuilder,
1125        declare: BindMode,
1126        target: &Expr,
1127        iter: &Expr,
1128        body: &Stmt,
1129    ) -> Result<(), String> {
1130        self.compile_expr(b, iter)?;
1131        b.emit(Op::CallBuiltin(ops::GETITER, 1), 0); // [iterator]
1132        self.iter_depth += 1;
1133        let r = self.loop_over(b, declare, target, body);
1134        self.iter_depth -= 1;
1135        r
1136    }
1137
1138    fn compile_for_in(
1139        &mut self,
1140        b: &mut ChunkBuilder,
1141        declare: BindMode,
1142        target: &Expr,
1143        object: &Expr,
1144        body: &Stmt,
1145    ) -> Result<(), String> {
1146        self.compile_expr(b, object)?;
1147        b.emit(Op::CallBuiltin(ops::FORIN_KEYS, 1), 0); // [keys_array]
1148        b.emit(Op::CallBuiltin(ops::GETITER, 1), 0); // [iterator]
1149        self.iter_depth += 1;
1150        let r = self.loop_over(b, declare, target, body);
1151        self.iter_depth -= 1;
1152        r
1153    }
1154
1155    /// `for await (target of iterable) body`. Obtains an async iterator, then each
1156    /// pass `await`s a `{value, done}` step (a native async iterator's promise, or
1157    /// the sync fallback's per-value await). The iterator lives in a temp local.
1158    fn compile_for_await(
1159        &mut self,
1160        b: &mut ChunkBuilder,
1161        declare: BindMode,
1162        target: &Expr,
1163        iter: &Expr,
1164        body: &Stmt,
1165    ) -> Result<(), String> {
1166        let iter_tmp = self.tmp_name("aiter");
1167        self.compile_expr(b, iter)?;
1168        b.emit(Op::CallBuiltin(ops::GET_ASYNC_ITER, 1), 0); // [iterator]
1169        self.name_const(b, &iter_tmp);
1170        b.emit(Op::Swap, 0);
1171        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1172        b.emit(Op::Pop, 0);
1173        let start = b.current_pos();
1174        // step = await ASYNC_STEP(iterator)  -> {value, done}
1175        self.load_local(b, &iter_tmp);
1176        b.emit(Op::CallBuiltin(ops::ASYNC_STEP, 1), 0); // [stepPromise]
1177        b.emit(Op::CallBuiltin(ops::AWAIT, 1), 0); // [step]
1178        let step_tmp = self.tmp_name("astep");
1179        self.name_const(b, &step_tmp);
1180        b.emit(Op::Swap, 0);
1181        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1182        b.emit(Op::Pop, 0);
1183        // if (step.done) break
1184        self.load_local(b, &step_tmp);
1185        self.name_const(b, "done");
1186        b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
1187        b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
1188        let jdone = b.emit(Op::JumpIfTrue(0), 0);
1189        // target = step.value
1190        self.load_local(b, &step_tmp);
1191        self.name_const(b, "value");
1192        b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [value]
1193        let per_iteration = matches!(declare, BindMode::Lexical | BindMode::Const);
1194        if per_iteration {
1195            self.emit_push_scope(b);
1196        }
1197        self.compile_bind(b, target, declare)?;
1198        self.loops.push(LoopCtx {
1199            breaks: Vec::new(),
1200            continues: Vec::new(),
1201            break_depth: self.scope_depth,
1202            continue_depth: self.scope_depth,
1203            iter_depth: self.iter_depth,
1204            catches_continue: true,
1205            label: self.pending_label.take(),
1206        });
1207        self.compile_stmt(b, body)?;
1208        let cont_target = b.current_pos();
1209        if per_iteration {
1210            self.emit_pop_scope(b);
1211        }
1212        b.emit(Op::Jump(start), 0);
1213        let ctx = self.loops.pop().unwrap();
1214        for c in ctx.continues {
1215            b.patch_jump(c, cont_target);
1216        }
1217        // `done` arrives before the iteration scope is open; `break` from inside it
1218        // still has one to close.
1219        let break_target = b.current_pos();
1220        if per_iteration {
1221            b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
1222            b.emit(Op::Pop, 0);
1223        }
1224        // Leaving early closes the async iterator, running an async generator's
1225        // pending `finally` / calling a user iterator's `.return()`.
1226        self.load_local(b, &iter_tmp);
1227        b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0);
1228        b.emit(Op::Pop, 0);
1229        let end = b.current_pos();
1230        b.patch_jump(jdone, end);
1231        for br in ctx.breaks {
1232            b.patch_jump(br, break_target);
1233        }
1234        self.redispatch_after_loop(b);
1235        Ok(())
1236    }
1237
1238    /// Shared loop tail for for-of / for-in: iterator on TOS.
1239    fn loop_over(
1240        &mut self,
1241        b: &mut ChunkBuilder,
1242        declare: BindMode,
1243        target: &Expr,
1244        body: &Stmt,
1245    ) -> Result<(), String> {
1246        // `for (const v of …)` binds a FRESH `v` each pass, so a closure made in one
1247        // pass keeps that pass's element.
1248        let per_iteration = matches!(declare, BindMode::Lexical | BindMode::Const);
1249        let start = b.current_pos();
1250        b.emit(Op::CallBuiltin(ops::FORITER, 0), 0); // [iterator, value, has_next]
1251        let jdone = b.emit(Op::JumpIfFalse(0), 0); // pops has_next
1252        if per_iteration {
1253            self.emit_push_scope(b);
1254        }
1255        self.compile_bind(b, target, declare)?; // consumes value -> [iterator]
1256        self.loops.push(LoopCtx {
1257            breaks: Vec::new(),
1258            continues: Vec::new(),
1259            break_depth: self.scope_depth,
1260            continue_depth: self.scope_depth,
1261            iter_depth: self.iter_depth,
1262            catches_continue: true,
1263            label: self.pending_label.take(),
1264        });
1265        self.compile_stmt(b, body)?;
1266        let cont_target = b.current_pos();
1267        if per_iteration {
1268            self.emit_pop_scope(b);
1269        }
1270        b.emit(Op::Jump(start), 0);
1271        let ctx = self.loops.pop().unwrap();
1272        for c in ctx.continues {
1273            b.patch_jump(c, cont_target);
1274        }
1275        let done = b.current_pos();
1276        b.patch_jump(jdone, done);
1277        b.emit(Op::Pop, 0); // drop iterator
1278        let jafter = b.emit(Op::Jump(0), 0);
1279        let break_target = b.current_pos();
1280        // `break` out of a for-of closes the iterator (runs a generator's pending
1281        // `finally` / calls a user iterator's `.return()`), then drops it.
1282        if per_iteration {
1283            b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
1284            b.emit(Op::Pop, 0);
1285        }
1286        b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0);
1287        b.emit(Op::Pop, 0); // ITER_CLOSE leaves its result; the `done` path popped
1288        let end = b.current_pos();
1289        b.patch_jump(jafter, end);
1290        for br in ctx.breaks {
1291            b.patch_jump(br, break_target);
1292        }
1293        // Every exit path above has already closed and dropped THIS loop's
1294        // iterator, so a signal re-dispatched here must not count it as live.
1295        self.iter_depth -= 1;
1296        self.redispatch_after_loop(b);
1297        self.iter_depth += 1;
1298        Ok(())
1299    }
1300
1301    fn compile_switch(
1302        &mut self,
1303        b: &mut ChunkBuilder,
1304        disc: &Expr,
1305        cases: &[SwitchCase],
1306    ) -> Result<(), String> {
1307        let disc_tmp = self.tmp_name("switch");
1308        self.compile_expr(b, disc)?;
1309        self.name_const(b, &disc_tmp);
1310        b.emit(Op::Swap, 0);
1311        b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1312        b.emit(Op::Pop, 0);
1313        // All cases share ONE block scope, so `case 1: let x = …` is visible to the
1314        // later cases but dies with the switch. It opens BEFORE the test chain
1315        // because each case test jumps straight into its body.
1316        self.emit_push_scope(b);
1317        // Emit the test chain: `if (disc === caseTest) goto bodyN`.
1318        let mut body_jumps: Vec<Option<usize>> = Vec::new();
1319        let mut default_idx: Option<usize> = None;
1320        for (i, case) in cases.iter().enumerate() {
1321            match &case.test {
1322                Some(t) => {
1323                    self.load_local(b, &disc_tmp);
1324                    self.compile_expr(b, t)?;
1325                    b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
1326                    let j = b.emit(Op::JumpIfTrue(0), 0);
1327                    body_jumps.push(Some(j));
1328                }
1329                None => {
1330                    default_idx = Some(i);
1331                    body_jumps.push(None);
1332                }
1333            }
1334        }
1335        // No test matched: jump to default (if any) or end.
1336        let no_match_jump = b.emit(Op::Jump(0), 0);
1337        self.loops.push(LoopCtx {
1338            breaks: Vec::new(),
1339            continues: Vec::new(),
1340            break_depth: self.scope_depth,
1341            continue_depth: self.scope_depth,
1342            iter_depth: self.iter_depth,
1343            catches_continue: false,
1344            label: None,
1345        });
1346        let mut body_starts: Vec<usize> = Vec::new();
1347        for case in cases {
1348            body_starts.push(b.current_pos());
1349            self.compile_stmts(b, &case.body)?;
1350        }
1351        let end = b.current_pos();
1352        // Patch each case test-jump to its body start.
1353        for (i, j) in body_jumps.iter().enumerate() {
1354            if let Some(j) = j {
1355                b.patch_jump(*j, body_starts[i]);
1356            }
1357        }
1358        match default_idx {
1359            Some(i) => b.patch_jump(no_match_jump, body_starts[i]),
1360            None => b.patch_jump(no_match_jump, end),
1361        }
1362        let ctx = self.loops.pop().unwrap();
1363        for br in ctx.breaks {
1364            b.patch_jump(br, end);
1365        }
1366        self.emit_pop_scope(b);
1367        self.redispatch_after_loop(b);
1368        Ok(())
1369    }
1370
1371    fn compile_try(
1372        &mut self,
1373        b: &mut ChunkBuilder,
1374        block: &[Stmt],
1375        handler: &Option<(Option<Expr>, Vec<Stmt>)>,
1376        finalizer: &Option<Vec<Stmt>>,
1377    ) -> Result<(), String> {
1378        let block_chunk = self.compile_block_chunk(block)?;
1379        let handler_def = match handler {
1380            Some((param, body)) => {
1381                let param_name = match param {
1382                    Some(Expr::Ident(n)) => Some(n.clone()),
1383                    _ => None,
1384                };
1385                let hbody = self.compile_block_chunk(body)?;
1386                Some((param_name, hbody))
1387            }
1388            None => None,
1389        };
1390        let final_chunk = match finalizer {
1391            Some(f) => Some(self.compile_block_chunk(f)?),
1392            None => None,
1393        };
1394        let id = self.tries.len();
1395        self.tries.push(TryDef {
1396            block: block_chunk,
1397            handler: handler_def,
1398            finalizer: final_chunk,
1399        });
1400        b.emit(Op::LoadInt(id as i64), 0);
1401        b.emit(Op::CallBuiltin(ops::TRY, 1), 0);
1402        b.emit(Op::Pop, 0);
1403        // The try/catch/finally bodies ran as their own chunks, so a `return` or
1404        // a `break`/`continue` inside them left a signal instead of jumping.
1405        self.emit_signal_dispatch(b);
1406        Ok(())
1407    }
1408
1409    /// Compile statements into a SEPARATE chunk (a try/catch/finally body). Loops
1410    /// opened outside it are unreachable by a plain jump, so `chunk_loop_base`
1411    /// moves up for the duration.
1412    fn compile_block_chunk(&mut self, stmts: &[Stmt]) -> Result<Chunk, String> {
1413        let mut cb = ChunkBuilder::new();
1414        // A nested chunk runs on its OWN VM frame, so the enclosing chunk's
1415        // slots are not reachable from it — everything here goes by name. (The
1416        // slot analysis already refuses any chunk containing a `try`, which is
1417        // what builds these; this keeps that true if another one appears.)
1418        let saved_slot_table = std::mem::take(&mut self.slots);
1419        let base = std::mem::replace(&mut self.chunk_loop_base, self.loops.len());
1420        let signals = std::mem::take(&mut self.chunk_signals);
1421        let depth = std::mem::take(&mut self.scope_depth);
1422        let iters = std::mem::take(&mut self.iter_depth);
1423        let sites = std::mem::take(&mut self.call_sites);
1424        let yields = std::mem::take(&mut self.yield_sites);
1425        let r = (|| {
1426            self.hoist_funcs(&mut cb, stmts)?;
1427            self.compile_stmts(&mut cb, stmts)
1428        })();
1429        self.chunk_loop_base = base;
1430        self.scope_depth = depth;
1431        self.iter_depth = iters;
1432        self.slots = saved_slot_table;
1433        // A signal raised inside the nested chunk still has to be dispatched by a
1434        // loop in THIS chunk, so the flag propagates outward.
1435        self.chunk_signals |= signals;
1436        r?;
1437        let chunk = self.finish_chunk(cb);
1438        self.call_sites = sites;
1439        self.yield_sites = yields;
1440        Ok(chunk)
1441    }
1442
1443    // ── functions ────────────────────────────────────────────────────────
1444    fn build_function(
1445        &mut self,
1446        name: &str,
1447        params: &[Param],
1448        body: &[Stmt],
1449        is_generator: bool,
1450        is_async: bool,
1451    ) -> Result<usize, String> {
1452        let (param_slots, prologue) = self.lower_params(params)?;
1453        let mut fb = ChunkBuilder::new();
1454        // Each function body is its own frame, so it gets its own slot table.
1455        // The analysis sees the parameter prologue (defaults, destructuring)
1456        // ahead of the body, which is the order they are emitted in.
1457        let mut planned: Vec<Stmt> = prologue.clone();
1458        planned.extend_from_slice(body);
1459        let saved_slot_table = std::mem::replace(
1460            &mut self.slots,
1461            if self.debug || is_generator || is_async {
1462                Default::default()
1463            } else {
1464                crate::slots::plan(params, &planned, false)
1465            },
1466        );
1467        // Prologue: a parameter arrives in the call environment (`bind_params`
1468        // ran before this chunk), so copy each slotted one into its slot once,
1469        // and everything after it is a bare `GetSlot`.
1470        for name in crate::slots::param_names(params) {
1471            if let Some(slot) = self.slot_of(&name) {
1472                self.name_const(&mut fb, &name);
1473                fb.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0);
1474                fb.emit(Op::SetSlot(slot), 0);
1475            }
1476        }
1477        // A function body is its own control-flow universe: `break`/`continue` can
1478        // never target a loop in the enclosing function.
1479        let saved_loops = std::mem::take(&mut self.loops);
1480        let saved_base = std::mem::replace(&mut self.chunk_loop_base, 0);
1481        let saved_signals = std::mem::take(&mut self.chunk_signals);
1482        let saved_depth = std::mem::take(&mut self.scope_depth);
1483        let saved_iters = std::mem::take(&mut self.iter_depth);
1484        let saved_agen = std::mem::replace(&mut self.in_async_generator, is_generator && is_async);
1485        // Strictness is inherited by every nested function and can only be
1486        // ADDED by a body's own directive prologue — never dropped.
1487        let saved_strict = self.strict;
1488        self.strict = self.strict || has_use_strict(body);
1489        // The body is a chunk of its own, so its call sites are keyed to ITS
1490        // `op_hash`; the enclosing chunk's pending ones must not be swept in.
1491        let saved_sites = std::mem::take(&mut self.call_sites);
1492        let saved_yields = std::mem::take(&mut self.yield_sites);
1493        let r = (|| {
1494            // Function-body hoisting: `var` bindings first, so a same-named
1495            // function declaration below overwrites the `undefined` rather than
1496            // being overwritten by it. Parameters are already bound, and
1497            // `hoist_var_name` leaves an existing binding alone.
1498            self.hoist_vars(&mut fb, body)?;
1499            self.hoist_funcs(&mut fb, &prologue)?;
1500            self.hoist_funcs(&mut fb, body)?;
1501            self.compile_stmts(&mut fb, &prologue)?;
1502            self.compile_stmts(&mut fb, body)
1503        })();
1504        self.loops = saved_loops;
1505        self.chunk_loop_base = saved_base;
1506        self.chunk_signals = saved_signals;
1507        self.scope_depth = saved_depth;
1508        self.iter_depth = saved_iters;
1509        self.in_async_generator = saved_agen;
1510        self.strict = saved_strict;
1511        self.slots = saved_slot_table;
1512        r?;
1513        let def = FuncDef {
1514            name: name.to_string(),
1515            params: param_slots,
1516            chunk: self.finish_chunk(fb),
1517            is_arrow: false,
1518            is_generator,
1519            is_async,
1520            is_method: false,
1521            self_name: false,
1522        };
1523        self.call_sites = saved_sites;
1524        self.yield_sites = saved_yields;
1525        self.functions.push((name.to_string(), def));
1526        Ok(self.functions.len() - 1)
1527    }
1528
1529    fn build_arrow(
1530        &mut self,
1531        params: &[Param],
1532        body: &FnBody,
1533        is_async: bool,
1534    ) -> Result<usize, String> {
1535        let stmts = match body {
1536            FnBody::Block(b) => b.clone(),
1537            FnBody::Expr(e) => vec![Stmt::from(StmtKind::Return(Some((**e).clone())))],
1538        };
1539        let id = self.build_function("", params, &stmts, false, is_async)?;
1540        // Mark the template as an arrow so `this` is captured lexically.
1541        self.functions[id].1.is_arrow = true;
1542        Ok(id)
1543    }
1544
1545    // ── classes ──────────────────────────────────────────────────────────
1546    /// Lower a `class` to runtime builder ops, leaving the class value on the
1547    /// stack: `MKCLASS` (name, parent, ctor) then `DEF_MEMBER`/`DEF_FIELD` for
1548    /// each member (each keeps the class on the stack).
1549    fn compile_class(&mut self, b: &mut ChunkBuilder, node: &ClassNode) -> Result<(), String> {
1550        // A class body is strict code unconditionally (10.2.4), directive or not.
1551        let saved_strict = std::mem::replace(&mut self.strict, true);
1552        let r = self.compile_class_body(b, node);
1553        self.strict = saved_strict;
1554        r
1555    }
1556
1557    /// `#name` when this member's key is a literal private name, else `None`. A
1558    /// private name is never computed, so a computed key is never one.
1559    fn private_key(m: &ClassMember) -> Option<String> {
1560        match &m.key {
1561            Expr::Str(s) if !m.computed && s.starts_with('#') => Some(s.clone()),
1562            Expr::Ident(s) if !m.computed && s.starts_with('#') => Some(s.clone()),
1563            _ => None,
1564        }
1565    }
1566
1567    fn compile_class_body(&mut self, b: &mut ChunkBuilder, node: &ClassNode) -> Result<(), String> {
1568        let cname = node.name.clone().unwrap_or_default();
1569        // Push name, parent (or undefined), constructor (or undefined).
1570        self.name_const(b, &cname);
1571        match &node.parent {
1572            Some(p) => self.compile_expr(b, p)?,
1573            None => {
1574                b.emit(Op::LoadUndef, 0);
1575            }
1576        }
1577        let ctor = node
1578            .members
1579            .iter()
1580            .find(|m| m.kind == MemberKind::Constructor);
1581        match ctor {
1582            Some(m) => {
1583                let def_id = self.build_function(&cname, &m.params, &m.body, false, false)?;
1584                self.emit_mkfunc(b, def_id);
1585            }
1586            None => {
1587                b.emit(Op::LoadUndef, 0);
1588            }
1589        }
1590        b.emit(Op::CallBuiltin(ops::MKCLASS, 3), 0); // -> [class]
1591
1592        // 15.7.14 steps 8-17: the class body runs inside its OWN environment,
1593        // holding one immutable binding for the class name, initialized to the
1594        // class itself at step 17 — before the static-field initializers of step
1595        // 32. So `class C { static x = C.m(); static m(){return 5} }` is 5, and a
1596        // class EXPRESSION's name (`const K = class Inner { static s = Inner.name }`)
1597        // is reachable from inside the body even though it is never a binding
1598        // outside it. node-js had no such scope: both threw `ReferenceError: C is
1599        // not defined`, because the only binding was the outer one the class
1600        // DECLARATION installs afterwards. An instance method's body already
1601        // worked, but only by accident — it runs late enough for the outer
1602        // binding to exist, which a class expression never gets.
1603        let body_scope = node.name.is_some();
1604        if let Some(name) = &node.name {
1605            self.emit_push_scope(b);
1606            b.emit(Op::Dup, 0); // [class, class]
1607            self.declare_as(b, &Expr::Ident(name.clone()), BindMode::Lexical); // [class]
1608        }
1609
1610        // `ClassDefinitionEvaluation` (15.7.14) installs every method and
1611        // accessor while evaluating the class body, and only then runs the
1612        // static-field initializers (step 32). So a static field may call a
1613        // static method declared after it, and `getOwnPropertyNames(C)` lists
1614        // the methods before the fields regardless of source order.
1615        // A `static { … }` block is a static ELEMENT, not a method: it belongs in
1616        // the deferred group with the field initializers and runs interleaved
1617        // with them in source order (both filters are stable over `members`).
1618        let deferred = |k: &MemberKind| matches!(k, MemberKind::Field | MemberKind::StaticBlock);
1619        let ordered = node
1620            .members
1621            .iter()
1622            .filter(|m| !deferred(&m.kind))
1623            .chain(node.members.iter().filter(|m| deferred(&m.kind)));
1624        let mut static_block_n = 0usize;
1625        for m in ordered {
1626            match m.kind {
1627                MemberKind::Constructor => {}
1628                // A PRIVATE static field declares a private element, so it
1629                // cannot be an ordinary write: `C.#s = 5` through `SETATTR`
1630                // trips the brand check that exists to reject exactly that write
1631                // on an object that has not declared `#s`. `DEF_MEMBER` installs
1632                // it directly, which is what a declaration is.
1633                MemberKind::Field if m.is_static && Self::private_key(m).is_some() => {
1634                    let key = Self::private_key(m).expect("guarded above");
1635                    self.name_const(b, &key); // [class, name]
1636                    b.emit(Op::LoadInt(member::STATIC_FIELD), 0);
1637                    b.emit(Op::LoadTrue, 0); // is_static
1638                    match &m.field_init {
1639                        Some(e) => self.emit_keyed_value(b, &m.key, e, false, member::METHOD)?,
1640                        None => {
1641                            b.emit(Op::LoadUndef, 0);
1642                        }
1643                    }
1644                    b.emit(Op::CallBuiltin(ops::DEF_MEMBER, 5), 0); // -> [class]
1645                }
1646                MemberKind::Field if m.is_static => {
1647                    // A static field is evaluated once at class-definition time and
1648                    // set as an own property of the constructor: `[class]` stays on
1649                    // the stack, `Dup` it as the SETATTR receiver.
1650                    b.emit(Op::Dup, 0); // [class, class]
1651                    self.emit_member_key(b, m)?; // [class, class, name]
1652                    match &m.field_init {
1653                        // 15.7.10: a static field's initializer is named after
1654                        // the field (`static s = function(){}` → `s`).
1655                        Some(e) => {
1656                            self.emit_keyed_value(b, &m.key, e, m.computed, member::METHOD)?
1657                        }
1658                        None => {
1659                            b.emit(Op::LoadUndef, 0);
1660                        }
1661                    }
1662                    // [class, class, name, val] -> SETATTR sets on the class -> [class, val]
1663                    b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0);
1664                    b.emit(Op::Pop, 0); // drop the returned value -> [class]
1665                }
1666                MemberKind::Field => {
1667                    // [class] name thunk name_anon -> DEF_FIELD -> [class]
1668                    self.emit_member_key(b, m)?;
1669                    let init = m.field_init.clone().unwrap_or(Expr::Undefined);
1670                    // 15.7.10: `class C { f = function(){} }` names the function
1671                    // `f`. An instance field's initializer runs per-instance from
1672                    // a thunk, and under a computed key the key is only known at
1673                    // class-definition time, so the decision travels to the host
1674                    // as a flag rather than as an emitted rename.
1675                    let name_anon = Self::is_anon_fn_def(&init);
1676                    let stmts = vec![Stmt::from(StmtKind::Return(Some(init)))];
1677                    let def_id = self.build_function("", &[], &stmts, false, false)?;
1678                    self.emit_mkfunc(b, def_id);
1679                    b.emit(
1680                        if name_anon {
1681                            Op::LoadTrue
1682                        } else {
1683                            Op::LoadFalse
1684                        },
1685                        0,
1686                    );
1687                    b.emit(Op::CallBuiltin(ops::DEF_FIELD, 4), 0);
1688                }
1689                MemberKind::StaticBlock => {
1690                    // `static { … }` runs ONCE at class-definition time with
1691                    // `this` bound to the constructor — exactly what a static
1692                    // method called as `C.m()` gets. So it is compiled as a
1693                    // static method under a HIDDEN key, invoked, and removed
1694                    // again; the `@@` prefix keeps it out of every enumeration
1695                    // (`Object.getOwnPropertyNames(C)` and friends filter
1696                    // internal slots) for the window in which it exists, and the
1697                    // counter keeps sibling blocks from colliding.
1698                    static_block_n += 1;
1699                    let slot = format!("@@staticBlock:{static_block_n}");
1700                    // [class] name kind static fn -> DEF_MEMBER -> [class]
1701                    self.name_const(b, &slot);
1702                    b.emit(Op::LoadInt(member::METHOD), 0);
1703                    b.emit(Op::LoadTrue, 0);
1704                    let def_id = self.build_function("", &[], &m.body, false, false)?;
1705                    self.functions[def_id].1.is_method = true;
1706                    self.emit_mkfunc(b, def_id);
1707                    b.emit(Op::CallBuiltin(ops::DEF_MEMBER, 5), 0);
1708                    // [class] -> C[slot]() -> discard the result
1709                    b.emit(Op::Dup, 0);
1710                    self.name_const(b, &slot);
1711                    b.emit(Op::CallBuiltin(ops::CALL_METHOD, 2), 0);
1712                    b.emit(Op::Pop, 0);
1713                    // [class] -> delete C[slot] -> discard the Bool
1714                    b.emit(Op::Dup, 0);
1715                    self.name_const(b, &slot);
1716                    b.emit(Op::CallBuiltin(ops::DELPROP_NAME, 2), 0);
1717                    b.emit(Op::Pop, 0);
1718                }
1719                MemberKind::Method | MemberKind::Get | MemberKind::Set => {
1720                    // [class] name kind static fn -> DEF_MEMBER -> [class]
1721                    self.emit_member_key(b, m)?;
1722                    let kind = match m.kind {
1723                        MemberKind::Get => member::GET,
1724                        MemberKind::Set => member::SET,
1725                        _ => member::METHOD,
1726                    };
1727                    b.emit(Op::LoadInt(kind), 0);
1728                    b.emit(
1729                        if m.is_static {
1730                            Op::LoadTrue
1731                        } else {
1732                            Op::LoadFalse
1733                        },
1734                        0,
1735                    );
1736                    // 10.2.9 step 4: an accessor's function name carries the
1737                    // `get `/`set ` prefix — `class C { get gg(){} }` gives
1738                    // `get gg`, not `gg`.
1739                    let mname = match &m.key {
1740                        Expr::Str(s) if !m.computed => match m.kind {
1741                            MemberKind::Get => format!("get {s}"),
1742                            MemberKind::Set => format!("set {s}"),
1743                            _ => s.clone(),
1744                        },
1745                        _ => String::new(),
1746                    };
1747                    let def_id = self.build_function(
1748                        &mname,
1749                        &m.params,
1750                        &m.body,
1751                        m.is_generator,
1752                        m.is_async,
1753                    )?;
1754                    // A class method/accessor is a MethodDefinition: not a
1755                    // constructor, so it owns no `prototype` property.
1756                    self.functions[def_id].1.is_method = true;
1757                    self.emit_mkfunc(b, def_id);
1758                    b.emit(Op::CallBuiltin(ops::DEF_MEMBER, 5), 0);
1759                }
1760            }
1761        }
1762        if body_scope {
1763            self.emit_pop_scope(b);
1764        }
1765        Ok(())
1766    }
1767
1768    /// `IsAnonymousFunctionDefinition(expr)` — the SYNTACTIC predicate that
1769    /// decides whether NamedEvaluation applies. It is deliberately not a runtime
1770    /// "does this function have an empty name" test: measured against node
1771    /// v26.7.0, `const anon = (0, function(){}); ({ m: anon }).m.name` is `""`,
1772    /// because the property definition's right-hand side is an
1773    /// IdentifierReference, not a function definition. Renaming by value would
1774    /// also mutate a function the program still holds under another binding.
1775    fn is_anon_fn_def(init: &Expr) -> bool {
1776        match init {
1777            Expr::Function { name: None, .. } => true,
1778            Expr::Class(node) => node.name.is_none(),
1779            _ => false,
1780        }
1781    }
1782
1783    /// If `init` is an anonymous function/arrow/class (value already on TOS), set
1784    /// its `.name` to `name` (JS binding name-inference). No-op otherwise.
1785    fn infer_name(&mut self, b: &mut ChunkBuilder, init: &Expr, name: &str) {
1786        if !Self::is_anon_fn_def(init) {
1787            return;
1788        }
1789        // [fn] Dup; .name = name; drop the SETATTR result.
1790        b.emit(Op::Dup, 0);
1791        self.name_const(b, "name");
1792        self.strlit(b, name);
1793        b.emit(Op::CallBuiltin(ops::SETATTR, 3), 0);
1794        b.emit(Op::Pop, 0);
1795    }
1796
1797    /// Compile a member's VALUE with the key already on the stack, applying
1798    /// NamedEvaluation (10.2.9 SetFunctionName) when the value is an anonymous
1799    /// function definition — `{ m: function(){} }`, `{ m(){} }`, `{ [k]: () => {} }`,
1800    /// `class C { static [k] = function(){} }`.
1801    ///
1802    /// A literal key resolves at compile time; a computed one is only known at
1803    /// run time, so the key already on the stack is duplicated and handed to
1804    /// `NAMED_EVAL` along with `kind` (which supplies the `get `/`set ` prefix).
1805    /// Leaves exactly one value on the stack either way, so every caller's
1806    /// arity is unchanged.
1807    fn emit_keyed_value(
1808        &mut self,
1809        b: &mut ChunkBuilder,
1810        key: &Expr,
1811        value: &Expr,
1812        computed: bool,
1813        kind: i64,
1814    ) -> Result<(), String> {
1815        match (Self::is_anon_fn_def(value), computed, key) {
1816            (true, false, Expr::Str(s)) => {
1817                self.compile_expr(b, value)?;
1818                let name = match kind {
1819                    member::GET => format!("get {s}"),
1820                    member::SET => format!("set {s}"),
1821                    _ => s.clone(),
1822                };
1823                self.infer_name(b, value, &name);
1824            }
1825            // [.., key] -> [.., key, key, kind, fn] -> NAMED_EVAL -> [.., key, fn]
1826            (true, true, _) => {
1827                b.emit(Op::Dup, 0);
1828                b.emit(Op::LoadInt(kind), 0);
1829                self.compile_expr(b, value)?;
1830                b.emit(Op::CallBuiltin(ops::NAMED_EVAL, 3), 0);
1831            }
1832            _ => self.compile_expr(b, value)?,
1833        }
1834        Ok(())
1835    }
1836
1837    /// Push a class/object member's property key: a computed expression coerced
1838    /// via `PROPKEY` (Symbol-aware), or a static name constant.
1839    fn emit_member_key(&mut self, b: &mut ChunkBuilder, m: &ClassMember) -> Result<(), String> {
1840        if m.computed {
1841            self.compile_expr(b, &m.key)?;
1842            b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
1843        } else if let Expr::Str(s) = &m.key {
1844            self.name_const(b, s);
1845        } else {
1846            self.compile_expr(b, &m.key)?;
1847            b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
1848        }
1849        Ok(())
1850    }
1851
1852    // ── generators / yield ───────────────────────────────────────────────
1853    fn compile_yield(
1854        &mut self,
1855        b: &mut ChunkBuilder,
1856        arg: &Option<Box<Expr>>,
1857        delegate: bool,
1858    ) -> Result<(), String> {
1859        if delegate && self.in_async_generator {
1860            // `yield* x` inside an `async function*` delegates over the ASYNC
1861            // iterator: await each step, re-yield its value, and evaluate to the
1862            // delegate's return value.
1863            match arg {
1864                Some(e) => self.compile_expr(b, e)?,
1865                None => {
1866                    b.emit(Op::LoadUndef, 0);
1867                }
1868            }
1869            b.emit(Op::CallBuiltin(ops::GET_ASYNC_ITER, 1), 0); // [aiter]
1870            let start = b.current_pos();
1871            b.emit(Op::Dup, 0); // [aiter, aiter]
1872            b.emit(Op::CallBuiltin(ops::ASYNC_STEP, 1), 0); // [aiter, stepPromise]
1873            b.emit(Op::CallBuiltin(ops::AWAIT, 1), 0); // [aiter, step]
1874            b.emit(Op::Dup, 0); // [aiter, step, step]
1875            self.name_const(b, "done");
1876            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
1877            b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
1878            let jdone = b.emit(Op::JumpIfTrue(0), 0); // [aiter, step]
1879            self.name_const(b, "value");
1880            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [aiter, value]
1881            let at = b.emit(Op::CallBuiltin(ops::YIELD, 1), 0); // [aiter, sent]
1882            self.yield_sites.push((at, self.iter_depth));
1883            b.emit(Op::Pop, 0); // [aiter]
1884            b.emit(Op::Jump(start), 0);
1885            let done = b.current_pos();
1886            b.patch_jump(jdone, done);
1887            self.name_const(b, "value"); // [aiter, step, "value"]
1888            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [aiter, returnValue]
1889            b.emit(Op::Swap, 0); // [returnValue, aiter]
1890            b.emit(Op::Pop, 0); // [returnValue]
1891        } else if delegate {
1892            // `yield* iterable`: step the delegate through the iterator protocol,
1893            // re-yielding each value and FORWARDING whatever `.next(x)` sent in.
1894            // The expression's value is the delegate's RETURN value, which
1895            // `FORITER` discards — hence the explicit `.next()` calls.
1896            let sent_tmp = self.tmp_name("delegated");
1897            match arg {
1898                Some(e) => self.compile_expr(b, e)?,
1899                None => {
1900                    b.emit(Op::LoadUndef, 0);
1901                }
1902            }
1903            b.emit(Op::CallBuiltin(ops::GETITER, 1), 0); // [iterator]
1904                                                         // The delegate is parked on the stack for the whole delegation, so
1905                                                         // it counts as a live iterator: a `.return()`/`.throw()` injected
1906                                                         // into the OUTER generator has to close it (7.4.9 IteratorClose),
1907                                                         // which is what runs the delegate's pending `finally`.
1908            self.iter_depth += 1;
1909            self.name_const(b, &sent_tmp);
1910            b.emit(Op::LoadUndef, 0);
1911            b.emit(Op::CallBuiltin(ops::DECLARE, 2), 0);
1912            b.emit(Op::Pop, 0);
1913            let start = b.current_pos();
1914            b.emit(Op::Dup, 0); // [iterator, iterator]
1915            self.name_const(b, "next");
1916            self.load_local(b, &sent_tmp);
1917            b.emit(Op::CallBuiltin(ops::CALL_METHOD, 3), 0); // [iterator, step]
1918            b.emit(Op::Dup, 0);
1919            self.name_const(b, "done");
1920            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
1921            b.emit(Op::CallBuiltin(ops::TRUTHY, 1), 0);
1922            let jdone = b.emit(Op::JumpIfTrue(0), 0); // [iterator, step]
1923            self.name_const(b, "value");
1924            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [iterator, value]
1925            let at = b.emit(Op::CallBuiltin(ops::YIELD, 1), 0); // [iterator, sent]
1926            self.yield_sites.push((at, self.iter_depth));
1927            self.name_const(b, &sent_tmp);
1928            b.emit(Op::Swap, 0);
1929            b.emit(Op::CallBuiltin(ops::SETLOCAL, 2), 0);
1930            b.emit(Op::Pop, 0);
1931            b.emit(Op::Jump(start), 0);
1932            let done = b.current_pos();
1933            b.patch_jump(jdone, done);
1934            self.name_const(b, "value"); // [iterator, step, "value"]
1935            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [iterator, returnValue]
1936            b.emit(Op::Swap, 0);
1937            b.emit(Op::Pop, 0); // [returnValue]
1938            self.iter_depth -= 1;
1939        } else {
1940            match arg {
1941                Some(e) => self.compile_expr(b, e)?,
1942                None => {
1943                    b.emit(Op::LoadUndef, 0);
1944                }
1945            }
1946            // YIELD suspends and leaves the value sent by `.next(x)` on the stack.
1947            let at = b.emit(Op::CallBuiltin(ops::YIELD, 1), 0);
1948            self.yield_sites.push((at, self.iter_depth));
1949        }
1950        Ok(())
1951    }
1952
1953    /// Lower a formal-parameter list into simple slots plus prologue statements
1954    /// (defaults + destructuring), executed at the top of the body.
1955    fn lower_params(&mut self, params: &[Param]) -> Result<(Vec<ParamSlot>, Vec<Stmt>), String> {
1956        let mut slots = Vec::new();
1957        let mut prologue: Vec<Stmt> = Vec::new();
1958        for (i, p) in params.iter().enumerate() {
1959            if p.rest {
1960                let name = match &p.pattern {
1961                    Expr::Ident(n) => n.clone(),
1962                    _ => return Err("SyntaxError: rest parameter must be an identifier".into()),
1963                };
1964                slots.push(ParamSlot {
1965                    name,
1966                    rest: true,
1967                    has_default: false,
1968                });
1969                continue;
1970            }
1971            match &p.pattern {
1972                Expr::Ident(name) => {
1973                    slots.push(ParamSlot {
1974                        name: name.clone(),
1975                        rest: false,
1976                        has_default: p.default.is_some(),
1977                    });
1978                    if let Some(d) = &p.default {
1979                        prologue.push(default_stmt(name, d));
1980                    }
1981                }
1982                pattern => {
1983                    let synth = format!(".param{i}");
1984                    slots.push(ParamSlot {
1985                        name: synth.clone(),
1986                        rest: false,
1987                        has_default: p.default.is_some(),
1988                    });
1989                    if let Some(d) = &p.default {
1990                        prologue.push(default_stmt(&synth, d));
1991                    }
1992                    prologue.push(Stmt::from(StmtKind::Decl {
1993                        kind: DeclKind::Let,
1994                        decls: vec![Declarator {
1995                            target: pattern.clone(),
1996                            init: Some(Expr::Ident(synth)),
1997                        }],
1998                    }));
1999                }
2000            }
2001        }
2002        Ok((slots, prologue))
2003    }
2004
2005    // ── expressions ──────────────────────────────────────────────────────
2006    fn compile_expr(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
2007        match e {
2008            Expr::Undefined => {
2009                b.emit(Op::LoadUndef, 0);
2010            }
2011            // A hole only carries its extra meaning INSIDE an array literal
2012            // (`compile_array` records it); evaluated anywhere else it is just
2013            // the `undefined` an elided read produces.
2014            Expr::Hole => {
2015                b.emit(Op::LoadUndef, 0);
2016            }
2017            Expr::Null => {
2018                b.emit(Op::CallBuiltin(ops::LOAD_NULL, 0), 0);
2019            }
2020            Expr::True => {
2021                b.emit(Op::LoadTrue, 0);
2022            }
2023            Expr::False => {
2024                b.emit(Op::LoadFalse, 0);
2025            }
2026            Expr::Number(n) => {
2027                b.emit(Op::LoadFloat(*n), 0);
2028            }
2029            Expr::BigInt(digits) => {
2030                // The canonical decimal digit string travels as a native constant;
2031                // MKBIGINT parses it into a heap BigInt at runtime.
2032                let k = b.add_constant(Value::str(digits));
2033                b.emit(Op::LoadConst(k), 0);
2034                b.emit(Op::CallBuiltin(ops::MKBIGINT, 1), 0);
2035            }
2036            Expr::Regex(pat, flags) => {
2037                let kp = b.add_constant(Value::str(pat));
2038                b.emit(Op::LoadConst(kp), 0);
2039                let kf = b.add_constant(Value::str(flags));
2040                b.emit(Op::LoadConst(kf), 0);
2041                b.emit(Op::CallBuiltin(ops::MKREGEX, 2), 0);
2042            }
2043            Expr::Str(s) => self.strlit(b, s),
2044            Expr::Template { quasis, exprs } => self.compile_template(b, quasis, exprs)?,
2045            Expr::TaggedTemplate {
2046                tag,
2047                quasis,
2048                raws,
2049                exprs,
2050            } => self.compile_tagged_template(b, tag, quasis, raws, exprs)?,
2051            Expr::Ident(n) => self.load_local(b, n),
2052            Expr::This => {
2053                b.emit(Op::CallBuiltin(ops::THIS, 0), 0);
2054            }
2055            Expr::Array(items) => self.compile_array(b, items)?,
2056            Expr::Object(props) => self.compile_object(b, props)?,
2057            Expr::Spread(inner) => self.compile_expr(b, inner)?,
2058            Expr::Logical(op, l, r) => self.compile_logical(b, *op, l, r)?,
2059            Expr::Unary(op, e) => self.compile_unary(b, *op, e)?,
2060            Expr::Binary(op, l, r) => self.compile_binary(b, *op, l, r)?,
2061            Expr::Conditional { test, cons, alt } => {
2062                self.compile_condition(b, test)?;
2063                let jf = b.emit(Op::JumpIfFalse(0), 0);
2064                self.compile_expr(b, cons)?;
2065                let je = b.emit(Op::Jump(0), 0);
2066                let els = b.current_pos();
2067                b.patch_jump(jf, els);
2068                self.compile_expr(b, alt)?;
2069                let end = b.current_pos();
2070                b.patch_jump(je, end);
2071            }
2072            Expr::Assign { target, value } => {
2073                self.compile_expr(b, value)?;
2074                // 13.15.2 step 1.e: `h = function(){}` names the function `h`.
2075                // Only an IdentifierReference target counts — `o.p = function(){}`
2076                // leaves the name empty in node too.
2077                if let Expr::Ident(n) = &**target {
2078                    self.infer_name(b, value, n);
2079                }
2080                b.emit(Op::Dup, 0); // assignment yields the value
2081                self.compile_bind(b, target, BindMode::Assign)?;
2082            }
2083            Expr::Update { op, prefix, target } => self.compile_update(b, *op, *prefix, target)?,
2084            // A chain's ROOT opens the frame its `?.` links park their jumps
2085            // in; nested links see it already open and add to it.
2086            Expr::Call { .. } | Expr::Member { .. } | Expr::Index { .. }
2087                if self.opt_chain.is_empty() && Self::spine_has_optional(e) =>
2088            {
2089                self.compile_chain_root(b, e)?
2090            }
2091            Expr::Call {
2092                func,
2093                args,
2094                optional,
2095            } => self.compile_call(b, func, args, *optional)?,
2096            Expr::New { callee, args } => self.compile_new(b, callee, args)?,
2097            Expr::Member {
2098                object,
2099                property,
2100                optional,
2101            } => self.compile_member(b, object, property, *optional)?,
2102            Expr::Index {
2103                object,
2104                index,
2105                optional,
2106            } => self.compile_index(b, object, index, *optional)?,
2107            Expr::Function {
2108                params,
2109                body,
2110                is_arrow,
2111                name,
2112                is_generator,
2113                is_async,
2114                is_method,
2115            } => {
2116                let def_id = if *is_arrow {
2117                    self.build_arrow(params, body, *is_async)?
2118                } else {
2119                    let n = name.clone().unwrap_or_default();
2120                    let stmts = match body {
2121                        FnBody::Block(b) => b.clone(),
2122                        FnBody::Expr(e) => vec![Stmt::from(StmtKind::Return(Some((**e).clone())))],
2123                    };
2124                    let id = self.build_function(&n, params, &stmts, *is_generator, *is_async)?;
2125                    // A NAMED function expression binds its own name inside the body
2126                    // (object/class methods parse with `name: None`, so this only
2127                    // fires for `function name(…) {…}` in expression position).
2128                    if name.is_some() {
2129                        self.functions[id].1.self_name = true;
2130                    }
2131                    self.functions[id].1.is_method = *is_method;
2132                    id
2133                };
2134                self.emit_mkfunc(b, def_id);
2135            }
2136            Expr::Class(node) => self.compile_class(b, node)?,
2137            Expr::Super => {
2138                // Bare `super` only appears as a call/member callee, handled by
2139                // compile_call / compile_member; a stray `super` yields undefined.
2140                b.emit(Op::LoadUndef, 0);
2141            }
2142            Expr::NewTarget => {
2143                b.emit(Op::CallBuiltin(ops::NEW_TARGET, 0), 0);
2144            }
2145            Expr::Yield { arg, delegate } => self.compile_yield(b, arg, *delegate)?,
2146            Expr::Await(inner) => {
2147                self.compile_expr(b, inner)?;
2148                b.emit(Op::CallBuiltin(ops::AWAIT, 1), 0);
2149            }
2150            Expr::Sequence(items) => {
2151                for (i, it) in items.iter().enumerate() {
2152                    self.compile_expr(b, it)?;
2153                    if i + 1 < items.len() {
2154                        b.emit(Op::Pop, 0);
2155                    }
2156                }
2157            }
2158        }
2159        Ok(())
2160    }
2161
2162    fn compile_template(
2163        &mut self,
2164        b: &mut ChunkBuilder,
2165        quasis: &[String],
2166        exprs: &[Expr],
2167    ) -> Result<(), String> {
2168        let mut n = 0;
2169        for (i, q) in quasis.iter().enumerate() {
2170            let k = b.add_constant(Value::str(q));
2171            b.emit(Op::LoadConst(k), 0);
2172            n += 1;
2173            if i < exprs.len() {
2174                self.compile_expr(b, &exprs[i])?;
2175                b.emit(Op::CallBuiltin(ops::TOSTR, 1), 0);
2176                n += 1;
2177            }
2178        }
2179        b.emit(Op::CallBuiltin(ops::MKSTR, argc(n)?), 0);
2180        Ok(())
2181    }
2182
2183    /// Lower a tagged template to `TAG_TMPL`. Operand layout (matching
2184    /// `builtins::b_tag_tmpl`): `[tag, n, m, cooked×n, raw×n, values×m]`, where
2185    /// `n = quasis.len()` and `m = exprs.len()` (`n == m + 1`).
2186    fn compile_tagged_template(
2187        &mut self,
2188        b: &mut ChunkBuilder,
2189        tag: &Expr,
2190        quasis: &[String],
2191        raws: &[String],
2192        exprs: &[Expr],
2193    ) -> Result<(), String> {
2194        self.compile_expr(b, tag)?;
2195        let n = quasis.len();
2196        let m = exprs.len();
2197        b.emit(Op::LoadInt(n as i64), 0);
2198        b.emit(Op::LoadInt(m as i64), 0);
2199        for q in quasis {
2200            self.strlit(b, q); // cooked strings (heap)
2201        }
2202        for r in raws {
2203            self.strlit(b, r); // raw strings (heap)
2204        }
2205        for e in exprs {
2206            self.compile_expr(b, e)?; // substitution values
2207        }
2208        b.emit(Op::CallBuiltin(ops::TAG_TMPL, argc(3 + 2 * n + m)?), 0);
2209        Ok(())
2210    }
2211
2212    fn compile_array(&mut self, b: &mut ChunkBuilder, items: &[Expr]) -> Result<(), String> {
2213        if items.iter().any(|e| matches!(e, Expr::Spread(_))) {
2214            // (tag, value) pairs; tag 1 = spread, tag 2 = elision. A spread
2215            // makes every later element's index a RUN-TIME quantity, so the
2216            // holes cannot be recorded from here — the tag carries the fact and
2217            // `BUILD_ARGS` marks them as it walks.
2218            for it in items {
2219                match it {
2220                    Expr::Spread(inner) => {
2221                        b.emit(Op::LoadInt(1), 0);
2222                        self.compile_expr(b, inner)?;
2223                    }
2224                    Expr::Hole => {
2225                        b.emit(Op::LoadInt(2), 0);
2226                        b.emit(Op::LoadUndef, 0);
2227                    }
2228                    _ => {
2229                        b.emit(Op::LoadInt(0), 0);
2230                        self.compile_expr(b, it)?;
2231                    }
2232                }
2233            }
2234            b.emit(Op::CallBuiltin(ops::BUILD_ARGS, argc(items.len() * 2)?), 0);
2235        } else if items.len() <= u8::MAX as usize {
2236            for it in items {
2237                self.compile_expr(b, it)?;
2238            }
2239            b.emit(Op::CallBuiltin(ops::MKARR, argc(items.len())?), 0);
2240            self.mark_literal_holes(b, items);
2241        } else {
2242            // A literal larger than one CallBuiltin's u8 arg count can hold (the
2243            // generated data tables in iconv-lite hit this): start from an empty
2244            // array and append each element with an indexed store, keeping the
2245            // array on the stack across iterations.
2246            b.emit(Op::CallBuiltin(ops::MKARR, 0), 0); // [arr]
2247            for (i, it) in items.iter().enumerate() {
2248                b.emit(Op::Dup, 0); // [arr, arr]
2249                b.emit(Op::LoadInt(i as i64), 0); // [arr, arr, i]
2250                self.compile_expr(b, it)?; // [arr, arr, i, val]
2251                b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0); // -> [arr, val]
2252                b.emit(Op::Pop, 0); // [arr]
2253            }
2254            // After the writes: a `SETITEM` CLEARS the hole at the index it
2255            // writes, so marking has to come last.
2256            self.mark_literal_holes(b, items);
2257        }
2258        Ok(())
2259    }
2260
2261    /// Emit a `MARK_HOLE` per elided position of a spread-free array literal,
2262    /// with the finished array on top of the stack. Emits nothing at all for the
2263    /// dense literals that are essentially every literal in real code.
2264    fn mark_literal_holes(&mut self, b: &mut ChunkBuilder, items: &[Expr]) {
2265        for (i, it) in items.iter().enumerate() {
2266            if !matches!(it, Expr::Hole) {
2267                continue;
2268            }
2269            b.emit(Op::Dup, 0); // [arr, arr]
2270            b.emit(Op::LoadInt(i as i64), 0); // [arr, arr, i]
2271            b.emit(Op::CallBuiltin(ops::MARK_HOLE, 2), 0); // [arr, undefined]
2272            b.emit(Op::Pop, 0); // [arr]
2273        }
2274    }
2275
2276    fn compile_object(&mut self, b: &mut ChunkBuilder, props: &[Prop]) -> Result<(), String> {
2277        // (tag, key, val) triples for the data/spread props; tag 1 = ...spread.
2278        // Accessors are installed afterward via DEF_ACCESSOR.
2279        // An ACCESSOR keeps its slot in this list — with a tag of its own — so
2280        // the object enumerates it where the source declared it. The pair
2281        // `get`/`set` for one key contributes ONE slot.
2282        let mut seen_accessor: Vec<String> = Vec::new();
2283        let data: Vec<&Prop> = props
2284            .iter()
2285            .filter(|p| match p {
2286                Prop::Accessor { key, computed, .. } => {
2287                    // Only a literal key can be de-duplicated at compile time; a
2288                    // computed one is settled by `b_mkobj`'s `or_insert`.
2289                    let literal = match (key, computed) {
2290                        (Expr::Str(s), false) => Some(s.clone()),
2291                        _ => None,
2292                    };
2293                    match literal {
2294                        Some(k) if seen_accessor.contains(&k) => false,
2295                        Some(k) => {
2296                            seen_accessor.push(k);
2297                            true
2298                        }
2299                        None => true,
2300                    }
2301                }
2302                _ => true,
2303            })
2304            .collect();
2305        let has_spread = data.iter().any(|p| matches!(p, Prop::Spread(_)));
2306        // A spread-free literal with more triples than one CallBuiltin's u8 arg
2307        // count can hold (iconv-lite's generated codepage tables are 150+ keys)
2308        // is built incrementally: start empty, store each key, keeping the object
2309        // on the stack. Spread merges need the single-shot MKOBJ tag path, so
2310        // large-with-spread stays on it (a rare, genuine limitation).
2311        if data.len() * 3 > u8::MAX as usize && !has_spread {
2312            b.emit(Op::CallBuiltin(ops::MKOBJ, 0), 0); // [obj]
2313            for p in &data {
2314                if let Prop::KeyValue {
2315                    key,
2316                    value,
2317                    computed,
2318                } = p
2319                {
2320                    b.emit(Op::Dup, 0); // [obj, obj]
2321                    self.compile_expr(b, key)?;
2322                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0); // [obj, obj, key]
2323                    self.emit_keyed_value(b, key, value, *computed, member::METHOD)?;
2324                    b.emit(Op::CallBuiltin(ops::SETITEM, 3), 0); // -> [obj, val]
2325                    b.emit(Op::Pop, 0); // [obj]
2326                }
2327            }
2328            // The incremental path's accessors keep their trailing order: a
2329            // literal that large is a generated data table, and none carry one.
2330            return self.compile_object_accessors(b, props);
2331        }
2332        for p in &data {
2333            match p {
2334                Prop::KeyValue {
2335                    key,
2336                    value,
2337                    computed,
2338                } => {
2339                    b.emit(Op::LoadInt(0), 0);
2340                    // Key coerces to a property key (Symbol-aware: a Symbol maps to
2341                    // its internal `@@…` key rather than a `String()` coercion).
2342                    self.compile_expr(b, key)?;
2343                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
2344                    self.emit_keyed_value(b, key, value, *computed, member::METHOD)?;
2345                }
2346                Prop::Spread(src) => {
2347                    b.emit(Op::LoadInt(1), 0);
2348                    self.compile_expr(b, src)?;
2349                    b.emit(Op::LoadUndef, 0);
2350                }
2351                // Reserve the accessor's enumeration slot; `DEF_ACCESSOR` below
2352                // installs the functions themselves.
2353                Prop::Accessor { key, computed, .. } => {
2354                    let _ = computed; // the key expression covers both forms
2355                    b.emit(Op::LoadInt(2), 0);
2356                    self.compile_expr(b, key)?;
2357                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
2358                    b.emit(Op::LoadUndef, 0);
2359                }
2360            }
2361        }
2362        b.emit(Op::CallBuiltin(ops::MKOBJ, argc(data.len() * 3)?), 0); // [obj]
2363        self.compile_object_accessors(b, props)
2364    }
2365
2366    /// Install any getter/setter accessors of an object literal onto the object
2367    /// left on the stack (shared by the single-shot and incremental build paths).
2368    fn compile_object_accessors(
2369        &mut self,
2370        b: &mut ChunkBuilder,
2371        props: &[Prop],
2372    ) -> Result<(), String> {
2373        for p in props {
2374            if let Prop::Accessor {
2375                key,
2376                computed,
2377                is_getter,
2378                func,
2379            } = p
2380            {
2381                if *computed {
2382                    self.compile_expr(b, key)?;
2383                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
2384                } else if let Expr::Str(s) = key {
2385                    self.name_const(b, s);
2386                } else {
2387                    self.compile_expr(b, key)?;
2388                    b.emit(Op::CallBuiltin(ops::PROPKEY, 1), 0);
2389                }
2390                let kind = if *is_getter { member::GET } else { member::SET };
2391                b.emit(Op::LoadInt(kind), 0);
2392                // `{ get g(){} }` names the getter `get g` (10.2.9 step 4 via
2393                // 13.2.5.5). A COMPUTED accessor key is the one member position
2394                // whose key is not still reachable on the stack here — `kind`
2395                // sits between it and the function — so it keeps the empty name.
2396                if *computed {
2397                    self.compile_expr(b, func)?;
2398                } else if let Expr::Str(s) = key {
2399                    self.compile_expr(b, func)?;
2400                    let prefix = if *is_getter { "get" } else { "set" };
2401                    self.infer_name(b, func, &format!("{prefix} {s}"));
2402                } else {
2403                    self.compile_expr(b, func)?;
2404                }
2405                b.emit(Op::CallBuiltin(ops::DEF_ACCESSOR, 4), 0);
2406            }
2407        }
2408        Ok(())
2409    }
2410
2411    fn compile_logical(
2412        &mut self,
2413        b: &mut ChunkBuilder,
2414        op: LogicalOp,
2415        l: &Expr,
2416        r: &Expr,
2417    ) -> Result<(), String> {
2418        self.compile_expr(b, l)?;
2419        b.emit(Op::Dup, 0);
2420        let test_op = match op {
2421            LogicalOp::And | LogicalOp::Or => ops::TRUTHY,
2422            LogicalOp::Nullish => ops::NULLISH,
2423        };
2424        b.emit(Op::CallBuiltin(test_op, 1), 0);
2425        let jump = match op {
2426            LogicalOp::And => b.emit(Op::JumpIfFalse(0), 0), // false -> keep left
2427            LogicalOp::Or => b.emit(Op::JumpIfTrue(0), 0),   // true -> keep left
2428            LogicalOp::Nullish => b.emit(Op::JumpIfFalse(0), 0), // not-nullish -> keep left
2429        };
2430        b.emit(Op::Pop, 0); // drop left, evaluate right
2431        self.compile_expr(b, r)?;
2432        let end = b.current_pos();
2433        b.patch_jump(jump, end);
2434        Ok(())
2435    }
2436
2437    fn compile_unary(&mut self, b: &mut ChunkBuilder, op: UnOp, e: &Expr) -> Result<(), String> {
2438        match op {
2439            UnOp::Neg => {
2440                self.compile_expr(b, e)?;
2441                b.emit(Op::Negate, 0);
2442            }
2443            UnOp::Not => {
2444                self.compile_condition(b, e)?;
2445                b.emit(Op::LogNot, 0);
2446            }
2447            UnOp::Pos => {
2448                b.emit(Op::LoadInt(unop::POS), 0);
2449                self.compile_expr(b, e)?;
2450                b.emit(Op::CallBuiltin(ops::UNARY, 2), 0);
2451            }
2452            UnOp::BitNot => {
2453                b.emit(Op::LoadInt(unop::BITNOT), 0);
2454                self.compile_expr(b, e)?;
2455                b.emit(Op::CallBuiltin(ops::UNARY, 2), 0);
2456            }
2457            UnOp::TypeOf => {
2458                // `typeof <bare ident>` must NOT throw when the name is unbound —
2459                // JS returns "undefined". Route a plain identifier through a
2460                // non-throwing name read; any other operand evaluates normally.
2461                if let Expr::Ident(n) = e {
2462                    // A slotted local is always bound by the time it is read
2463                    // (that is rule 3 of the slot analysis), so there is no
2464                    // unbound case for `TYPEOF_NAME` to absorb.
2465                    if let Some(slot) = self.slot_of(n) {
2466                        b.emit(Op::GetSlot(slot), 0);
2467                        b.emit(Op::CallBuiltin(ops::TYPEOF, 1), 0);
2468                        return Ok(());
2469                    }
2470                    self.name_const(b, n);
2471                    b.emit(Op::CallBuiltin(ops::TYPEOF_NAME, 1), 0);
2472                } else {
2473                    self.compile_expr(b, e)?;
2474                    b.emit(Op::CallBuiltin(ops::TYPEOF, 1), 0);
2475                }
2476            }
2477            UnOp::Void => {
2478                self.compile_expr(b, e)?;
2479                b.emit(Op::Pop, 0);
2480                b.emit(Op::LoadUndef, 0);
2481            }
2482            UnOp::Delete => match e {
2483                Expr::Member {
2484                    object, property, ..
2485                } => {
2486                    self.compile_expr(b, object)?;
2487                    self.name_const(b, property);
2488                    b.emit(Op::CallBuiltin(ops::DELPROP_NAME, 2), 0);
2489                }
2490                Expr::Index { object, index, .. } => {
2491                    self.compile_expr(b, object)?;
2492                    self.compile_expr(b, index)?;
2493                    b.emit(Op::CallBuiltin(ops::DELITEM, 2), 0);
2494                }
2495                _ => {
2496                    b.emit(Op::LoadTrue, 0);
2497                }
2498            },
2499        }
2500        Ok(())
2501    }
2502
2503    fn compile_binary(
2504        &mut self,
2505        b: &mut ChunkBuilder,
2506        op: BinOp,
2507        l: &Expr,
2508        r: &Expr,
2509    ) -> Result<(), String> {
2510        // Native fast path (JIT-traceable); the numeric hook supplies JS
2511        // semantics for non-number operands.
2512        macro_rules! native {
2513            ($opc:expr) => {{
2514                self.compile_expr(b, l)?;
2515                self.compile_expr(b, r)?;
2516                b.emit($opc, 0);
2517                return Ok(());
2518            }};
2519        }
2520        match op {
2521            BinOp::Add => native!(Op::Add),
2522            BinOp::Sub => native!(Op::Sub),
2523            BinOp::Mul => native!(Op::Mul),
2524            BinOp::Div => {
2525                // NOT native `Op::Div`: fusevm returns `Undef` for a zero divisor,
2526                // but JS needs `x/0 === ±Infinity` / `0/0 === NaN`, so `/` is a
2527                // builtin (fusevm's own documented pattern for non-default `/`).
2528                self.compile_expr(b, l)?;
2529                self.compile_expr(b, r)?;
2530                b.emit(Op::CallBuiltin(ops::DIV, 2), 0);
2531                return Ok(());
2532            }
2533            BinOp::Mod => native!(Op::Mod),
2534            // NOT native `Op::Pow`, for the same reason `/` is a builtin above:
2535            // fusevm's is IEEE-754 `pow`, where `(-1) ** Infinity` and `1 ** NaN`
2536            // come back 1 rather than the spec's NaN.
2537            BinOp::Pow => {
2538                self.compile_expr(b, l)?;
2539                self.compile_expr(b, r)?;
2540                b.emit(Op::CallBuiltin(ops::POW, 2), 0);
2541                return Ok(());
2542            }
2543            BinOp::Lt => native!(Op::NumLt),
2544            BinOp::Le => native!(Op::NumLe),
2545            BinOp::Gt => native!(Op::NumGt),
2546            BinOp::Ge => native!(Op::NumGe),
2547            BinOp::EqEqEq => {
2548                self.compile_expr(b, l)?;
2549                self.compile_expr(b, r)?;
2550                b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
2551            }
2552            BinOp::NeEqEq => {
2553                self.compile_expr(b, l)?;
2554                self.compile_expr(b, r)?;
2555                b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
2556                b.emit(Op::LogNot, 0);
2557            }
2558            BinOp::EqEq => {
2559                self.compile_expr(b, l)?;
2560                self.compile_expr(b, r)?;
2561                b.emit(Op::CallBuiltin(ops::LOOSE_EQ, 2), 0);
2562            }
2563            BinOp::NeEq => {
2564                self.compile_expr(b, l)?;
2565                self.compile_expr(b, r)?;
2566                b.emit(Op::CallBuiltin(ops::LOOSE_EQ, 2), 0);
2567                b.emit(Op::LogNot, 0);
2568            }
2569            BinOp::In => {
2570                // `#field in obj` is the private-brand check: the left operand is a
2571                // private NAME, not a variable read, so it lowers to the key string
2572                // (private fields live as `#`-prefixed properties on the instance).
2573                match l {
2574                    Expr::Ident(n) if n.starts_with('#') => self.name_const(b, n),
2575                    _ => self.compile_expr(b, l)?,
2576                }
2577                self.compile_expr(b, r)?;
2578                b.emit(Op::CallBuiltin(ops::CONTAINS, 2), 0);
2579            }
2580            BinOp::InstanceOf => {
2581                self.compile_expr(b, l)?;
2582                self.compile_expr(b, r)?;
2583                b.emit(Op::CallBuiltin(ops::INSTANCEOF, 2), 0);
2584            }
2585            BinOp::BitAnd => self.emit_bitwise(b, bop::BITAND, l, r)?,
2586            BinOp::BitOr => self.emit_bitwise(b, bop::BITOR, l, r)?,
2587            BinOp::BitXor => self.emit_bitwise(b, bop::BITXOR, l, r)?,
2588            BinOp::Shl => self.emit_bitwise(b, bop::SHL, l, r)?,
2589            BinOp::Shr => self.emit_bitwise(b, bop::SHR, l, r)?,
2590            BinOp::UShr => self.emit_bitwise(b, bop::USHR, l, r)?,
2591        }
2592        Ok(())
2593    }
2594
2595    fn emit_bitwise(
2596        &mut self,
2597        b: &mut ChunkBuilder,
2598        tag: i64,
2599        l: &Expr,
2600        r: &Expr,
2601    ) -> Result<(), String> {
2602        b.emit(Op::LoadInt(tag), 0);
2603        self.compile_expr(b, l)?;
2604        self.compile_expr(b, r)?;
2605        b.emit(Op::CallBuiltin(ops::BINOP, 3), 0);
2606        Ok(())
2607    }
2608
2609    fn compile_update(
2610        &mut self,
2611        b: &mut ChunkBuilder,
2612        op: UpdateOp,
2613        prefix: bool,
2614        target: &Expr,
2615    ) -> Result<(), String> {
2616        // `NUM_STEP(tag, old)` computes `ToNumeric(old)` and `old ± 1` preserving
2617        // the operand's numeric type — so `x++` on a BigInt stays a BigInt
2618        // (`+old`/`old + 1` would throw the mix error). It pushes the coerced old
2619        // value and returns the new value: stack `[tag, old]` → `[oldN, new]`.
2620        let tag = if matches!(op, UpdateOp::Inc) { 1 } else { -1 };
2621        // A slot that provably holds a Number needs none of that: `ToNumeric`
2622        // is the identity on it and `Number ± 1` is a Number, so the whole
2623        // update is `GetSlot`, a native `Add`, and `SetSlot`. This is what takes
2624        // the last `CallBuiltin` out of a counting loop's body — and with it the
2625        // reason fusevm's tiers decline the loop.
2626        if let Expr::Ident(n) = target {
2627            // `c++` on a `const` is an assignment and throws like one. The
2628            // numeric fast path below writes the slot directly, and the general
2629            // path reaches the check through `compile_bind`, so this has to come
2630            // before both — otherwise `const c = 1; c++` silently incremented a
2631            // constant while `c = 2` correctly threw.
2632            if self.slots.consts.contains(n) {
2633                self.throw_const_assignment(b);
2634                return Ok(());
2635            }
2636            if let Some(slot) = self.numeric_slot_of(n) {
2637                b.emit(Op::GetSlot(slot), 0); // [old]
2638                if !prefix {
2639                    b.emit(Op::Dup, 0); // [old, old]
2640                }
2641                b.emit(Op::LoadFloat(tag as f64), 0);
2642                b.emit(Op::Add, 0); // [ (old,) new ]
2643                if prefix {
2644                    b.emit(Op::Dup, 0); // [new, new]
2645                }
2646                b.emit(Op::SetSlot(slot), 0); // stores, leaves the yielded value
2647                return Ok(());
2648            }
2649        }
2650        b.emit(Op::LoadInt(tag), 0);
2651        self.compile_expr(b, target)?; // [tag, old]
2652        b.emit(Op::CallBuiltin(ops::NUM_STEP, 2), 0); // [oldN, new]
2653        if prefix {
2654            // ++x: discard oldN, store new, yield new.
2655            b.emit(Op::Swap, 0); // [new, oldN]
2656            b.emit(Op::Pop, 0); // [new]
2657            b.emit(Op::Dup, 0); // [new, new]
2658            self.compile_bind(b, target, BindMode::Assign)?; // stores new -> [new]
2659        } else {
2660            // x++: store new, yield oldN.
2661            self.compile_bind(b, target, BindMode::Assign)?; // stores new -> [oldN]
2662        }
2663        Ok(())
2664    }
2665
2666    fn compile_member(
2667        &mut self,
2668        b: &mut ChunkBuilder,
2669        object: &Expr,
2670        property: &str,
2671        optional: bool,
2672    ) -> Result<(), String> {
2673        // `super.prop` — read a data/accessor property off the parent prototype.
2674        if matches!(object, Expr::Super) {
2675            self.name_const(b, property);
2676            b.emit(Op::CallBuiltin(ops::SUPER_GET, 1), 0);
2677            return Ok(());
2678        }
2679        self.compile_expr(b, object)?;
2680        if optional {
2681            let jshort = self.emit_optional_guard(b);
2682            self.name_const(b, property);
2683            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
2684            // Inside a chain the jump belongs to the chain's end, not to this
2685            // link's — otherwise the rest of the chain runs on the `undefined`
2686            // the short-circuit just produced.
2687            match self.opt_chain.last_mut() {
2688                Some(frame) => frame.push(jshort),
2689                None => {
2690                    let end = b.current_pos();
2691                    b.patch_jump(jshort, end);
2692                }
2693            }
2694        } else {
2695            self.name_const(b, property);
2696            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0);
2697        }
2698        Ok(())
2699    }
2700
2701    fn compile_index(
2702        &mut self,
2703        b: &mut ChunkBuilder,
2704        object: &Expr,
2705        index: &Expr,
2706        optional: bool,
2707    ) -> Result<(), String> {
2708        self.compile_expr(b, object)?;
2709        if optional {
2710            let jshort = self.emit_optional_guard(b);
2711            self.compile_off_spine(b, index)?;
2712            b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0);
2713            match self.opt_chain.last_mut() {
2714                Some(frame) => frame.push(jshort),
2715                None => {
2716                    let end = b.current_pos();
2717                    b.patch_jump(jshort, end);
2718                }
2719            }
2720        } else {
2721            self.compile_off_spine(b, index)?;
2722            b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0);
2723        }
2724        Ok(())
2725    }
2726
2727    /// For an optional access: object on TOS. If nullish, replace with undefined
2728    /// and jump over the access. Returns the jump index to patch to the end.
2729    // ── block scopes ─────────────────────────────────────────────────────
2730    /// Enter a block scope: `let`/`const` declared after this point die at the
2731    /// matching [`Self::emit_pop_scope`].
2732    fn emit_push_scope(&mut self, b: &mut ChunkBuilder) {
2733        b.emit(Op::CallBuiltin(ops::PUSH_SCOPE, 0), 0);
2734        b.emit(Op::Pop, 0);
2735        self.scope_depth += 1;
2736    }
2737
2738    fn emit_pop_scope(&mut self, b: &mut ChunkBuilder) {
2739        b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
2740        b.emit(Op::Pop, 0);
2741        self.scope_depth -= 1;
2742    }
2743
2744    /// Replace the innermost scope with a copy of its bindings — the per-iteration
2745    /// environment that makes each `for (let i …)` pass capture its own `i`.
2746    fn emit_copy_scope(&self, b: &mut ChunkBuilder) {
2747        b.emit(Op::CallBuiltin(ops::COPY_SCOPE, 0), 0);
2748        b.emit(Op::Pop, 0);
2749    }
2750
2751    /// Close and drop every for-of/for-in iterator between here and `target`
2752    /// depth. A jump to an OUTER loop abandons the inner loops, and their
2753    /// iterators are parked on the VM stack, so they must be popped (running a
2754    /// generator's `finally` / the iterator protocol's `.return()`) or the outer
2755    /// `FORITER` would read the wrong stack slot.
2756    /// Close every iterator this chunk has parked, with a value already on top
2757    /// of the stack that must survive: the iterators sit UNDER it, so each one
2758    /// is swapped up, closed, and its result dropped.
2759    /// Build the chunk being emitted and hand the host its call-site table. Every
2760    /// chunk goes through here so a site is registered exactly once, under the
2761    /// `op_hash` `build()` computes.
2762    fn finish_chunk(&mut self, b: ChunkBuilder) -> Chunk {
2763        let sites = std::mem::take(&mut self.call_sites);
2764        let yields = std::mem::take(&mut self.yield_sites);
2765        let chunk = b.build();
2766        crate::host::register_call_sites(chunk.op_hash, sites);
2767        crate::host::register_yield_sites(chunk.op_hash, yields);
2768        chunk
2769    }
2770
2771    /// Record the callee's source text for the call op just emitted at `at`, so
2772    /// a `TypeError` raised there can name the callee the way V8 does. Nothing
2773    /// is recorded for a shape `callee_text` declines to print.
2774    fn note_call_site(&mut self, at: usize, callee: &Expr) {
2775        if let Some(text) = callee_text(callee) {
2776            self.call_sites.push((at, text));
2777        }
2778    }
2779
2780    fn emit_close_iters_under_value(&self, b: &mut ChunkBuilder) {
2781        for _ in 0..self.iter_depth {
2782            b.emit(Op::Swap, 0); // [.., iter, val] -> [.., val, iter]
2783            b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0); // -> [.., val, result]
2784            b.emit(Op::Pop, 0); // -> [.., val]
2785        }
2786    }
2787
2788    fn emit_close_iters(&self, b: &mut ChunkBuilder, target: usize) {
2789        for _ in target..self.iter_depth {
2790            b.emit(Op::CallBuiltin(ops::ITER_CLOSE, 1), 0);
2791            b.emit(Op::Pop, 0);
2792        }
2793    }
2794
2795    /// Close every block scope between here and `target` depth, without changing
2796    /// the compile-time depth (the jump that follows leaves this code path).
2797    fn emit_unwind_scopes(&self, b: &mut ChunkBuilder, target: usize) {
2798        for _ in target..self.scope_depth {
2799            b.emit(Op::CallBuiltin(ops::POP_SCOPE, 0), 0);
2800            b.emit(Op::Pop, 0);
2801        }
2802    }
2803
2804    /// Raise a `break`/`continue` whose target loop is outside this chunk.
2805    fn emit_signal_jump(&mut self, b: &mut ChunkBuilder, op: u16, label: Option<&str>, line: u32) {
2806        self.name_const(b, label.unwrap_or(""));
2807        b.emit(Op::CallBuiltin(op, 1), line);
2808        b.emit(Op::Pop, line);
2809        self.chunk_signals = true;
2810    }
2811
2812    /// Emit the `SIG_UNWIND` dispatch that runs right after a `TRY` (or after a
2813    /// loop that may still hold a signal for an outer labeled loop): route a
2814    /// pending `break`/`continue` to the enclosing loop's exit/continue target, or
2815    /// halt the chunk so a `return` (or a signal for a loop further out) keeps
2816    /// propagating.
2817    fn emit_signal_dispatch(&mut self, b: &mut ChunkBuilder) {
2818        // `break` lands on the innermost enclosing context, `continue` on the
2819        // innermost one that CATCHES it — a `switch` catches `break` but not
2820        // `continue`, so the two targets are resolved INDEPENDENTLY. Either may be
2821        // absent from this chunk, in which case a signal of that kind keeps
2822        // travelling outward. (`cont` implies `brk`: a continue-catching loop is
2823        // itself breakable, so it can never sit above the innermost context.)
2824        let brk = self
2825            .loops
2826            .len()
2827            .checked_sub(1)
2828            .filter(|i| *i >= self.chunk_loop_base);
2829        let cont = self
2830            .loops
2831            .iter()
2832            .rposition(|c| c.catches_continue)
2833            .filter(|i| *i >= self.chunk_loop_base);
2834        let tag_of = |i: Option<usize>, loops: &[LoopCtx]| match i {
2835            Some(i) => loops[i]
2836                .label
2837                .clone()
2838                .unwrap_or_else(|| unwind::PLAIN_LOOP.to_string()),
2839            None => unwind::NO_LOOP.to_string(),
2840        };
2841        let brk_tag = tag_of(brk, &self.loops);
2842        let cont_tag = tag_of(cont, &self.loops);
2843        self.name_const(b, &brk_tag);
2844        self.name_const(b, &cont_tag);
2845        b.emit(Op::CallBuiltin(ops::SIG_UNWIND, 2), 0); // [code]
2846        let Some(idx) = brk else {
2847            // Nothing in this chunk can catch the signal; `SIG_UNWIND` already
2848            // halted the chunk, so just drop its code.
2849            b.emit(Op::Pop, 0);
2850            return;
2851        };
2852        b.emit(Op::Dup, 0);
2853        b.emit(Op::LoadInt(unwind::BREAK), 0);
2854        b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
2855        let jb = b.emit(Op::JumpIfTrue(0), 0);
2856        let jc = cont.map(|_| {
2857            b.emit(Op::Dup, 0);
2858            b.emit(Op::LoadInt(unwind::CONTINUE), 0);
2859            b.emit(Op::CallBuiltin(ops::STRICT_EQ, 2), 0);
2860            b.emit(Op::JumpIfTrue(0), 0)
2861        });
2862        b.emit(Op::Pop, 0); // no signal: drop the code and fall through
2863        let jafter = b.emit(Op::Jump(0), 0);
2864        // The landing pads leave every block scope and iterator opened between
2865        // here and the target, exactly as the plain compiler-resolved `break` /
2866        // `continue` does. Skipping this leaked a scope onto the frame, so the
2867        // NEXT `let`/`const` at that level bound in a dead child env and became
2868        // invisible to any closure created afterwards.
2869        let (brk_scope, brk_iter) = (self.loops[idx].break_depth, self.loops[idx].iter_depth);
2870        let brk_land = b.current_pos();
2871        b.emit(Op::Pop, 0);
2872        self.emit_unwind_scopes(b, brk_scope);
2873        self.emit_close_iters(b, brk_iter);
2874        let brk_jump = b.emit(Op::Jump(0), 0);
2875        let cont_jump = jc.map(|_| {
2876            let (cs, ci) = cont
2877                .map(|i| (self.loops[i].continue_depth, self.loops[i].iter_depth))
2878                .unwrap_or((self.scope_depth, self.iter_depth));
2879            let cont_land = b.current_pos();
2880            b.emit(Op::Pop, 0);
2881            self.emit_unwind_scopes(b, cs);
2882            self.emit_close_iters(b, ci);
2883            (cont_land, b.emit(Op::Jump(0), 0))
2884        });
2885        let after = b.current_pos();
2886        b.patch_jump(jb, brk_land);
2887        if let (Some(jc), Some((cont_land, _))) = (jc, cont_jump) {
2888            b.patch_jump(jc, cont_land);
2889        }
2890        b.patch_jump(jafter, after);
2891        self.loops[idx].breaks.push(brk_jump);
2892        if let (Some(cont_idx), Some((_, cj))) = (cont, cont_jump) {
2893            self.loops[cont_idx].continues.push(cj);
2894        }
2895    }
2896
2897    /// Whether `e` is a link in an optional chain that short-circuits — i.e.
2898    /// walking the SPINE (a member's object, an index's object, a call's
2899    /// callee) reaches a `?.`. An argument or a computed index is not on the
2900    /// spine: `a?.b[c?.d]` is two chains, not one.
2901    fn spine_has_optional(e: &Expr) -> bool {
2902        match e {
2903            Expr::Member {
2904                object, optional, ..
2905            } => *optional || Self::spine_has_optional(object),
2906            Expr::Index {
2907                object, optional, ..
2908            } => *optional || Self::spine_has_optional(object),
2909            Expr::Call { func, optional, .. } => *optional || Self::spine_has_optional(func),
2910            _ => false,
2911        }
2912    }
2913
2914    /// Lower `e` as the ROOT of an optional chain: every `?.` inside its spine
2915    /// parks a jump, and all of them land here, past the whole chain.
2916    fn compile_chain_root(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
2917        self.opt_chain.push(Vec::new());
2918        let r = self.compile_expr(b, e);
2919        let pending = self.opt_chain.pop().unwrap_or_default();
2920        r?;
2921        let end = b.current_pos();
2922        for j in pending {
2923            b.patch_jump(j, end);
2924        }
2925        Ok(())
2926    }
2927
2928    /// Lower `e` with the enclosing chain SUSPENDED, so a `?.` inside it forms
2929    /// its own chain. Used for the parts that are not on the spine — call
2930    /// arguments and a computed index.
2931    fn compile_off_spine(&mut self, b: &mut ChunkBuilder, e: &Expr) -> Result<(), String> {
2932        let saved = std::mem::take(&mut self.opt_chain);
2933        let r = self.compile_expr(b, e);
2934        self.opt_chain = saved;
2935        r
2936    }
2937
2938    fn emit_optional_guard(&mut self, b: &mut ChunkBuilder) -> usize {
2939        b.emit(Op::Dup, 0);
2940        b.emit(Op::CallBuiltin(ops::NULLISH, 1), 0);
2941        let jnull = b.emit(Op::JumpIfFalse(0), 0); // not nullish -> continue access
2942                                                   // nullish: drop object, push undefined, jump to end.
2943        b.emit(Op::Pop, 0);
2944        b.emit(Op::LoadUndef, 0);
2945        let jend = b.emit(Op::Jump(0), 0);
2946        let cont = b.current_pos();
2947        b.patch_jump(jnull, cont);
2948        jend
2949    }
2950
2951    /// `callee?.(args)` — the CALLEE itself may be nullish, in which case the whole
2952    /// call short-circuits to `undefined` without evaluating the arguments. A
2953    /// method callee (`obj.m?.()`) must still be invoked with `this === obj`, so it
2954    /// is dispatched through `m.call(obj, …)` / `m.apply(obj, …)`.
2955    fn compile_optional_call(
2956        &mut self,
2957        b: &mut ChunkBuilder,
2958        func: &Expr,
2959        args: &[Expr],
2960    ) -> Result<(), String> {
2961        let has_spread = args.iter().any(|a| matches!(a, Expr::Spread(_)));
2962        if let Expr::Member {
2963            object,
2964            property,
2965            optional: obj_optional,
2966        } = func
2967        {
2968            self.compile_expr(b, object)?; // [recv]
2969            let jobj = if *obj_optional {
2970                Some(self.emit_optional_guard(b))
2971            } else {
2972                None
2973            };
2974            b.emit(Op::Dup, 0); // [recv, recv]
2975            self.name_const(b, property); // [recv, recv, name]
2976            b.emit(Op::CallBuiltin(ops::GETATTR, 2), 0); // [recv, fn]
2977                                                         // Nullish callee: drop both the method and the receiver.
2978            b.emit(Op::Dup, 0);
2979            b.emit(Op::CallBuiltin(ops::NULLISH, 1), 0);
2980            let jlive = b.emit(Op::JumpIfFalse(0), 0);
2981            b.emit(Op::Pop, 0);
2982            b.emit(Op::Pop, 0);
2983            b.emit(Op::LoadUndef, 0);
2984            let jend = b.emit(Op::Jump(0), 0);
2985            let live = b.current_pos();
2986            b.patch_jump(jlive, live);
2987            // [recv, fn] -> fn.call(recv, …) / fn.apply(recv, argsArray)
2988            let via = if has_spread { "apply" } else { "call" };
2989            self.name_const(b, via); // [recv, fn, via]
2990            b.emit(Op::Rot, 0); // [fn, via, recv]
2991            let extra = if has_spread {
2992                self.compile_spread_args(b, args)?; // [fn, via, recv, argsArray]
2993                1
2994            } else {
2995                for a in args {
2996                    self.compile_expr(b, a)?;
2997                }
2998                args.len()
2999            };
3000            b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + extra)?), 0);
3001            match self.opt_chain.last_mut() {
3002                Some(frame) => {
3003                    frame.push(jend);
3004                    if let Some(j) = jobj {
3005                        frame.push(j);
3006                    }
3007                }
3008                None => {
3009                    let end = b.current_pos();
3010                    b.patch_jump(jend, end);
3011                    if let Some(j) = jobj {
3012                        b.patch_jump(j, end);
3013                    }
3014                }
3015            }
3016            return Ok(());
3017        }
3018        // `recv[expr]?.(…)` — the optional-call form of a COMPUTED member. Same
3019        // receiver rule as `recv.name?.(…)` above; only the key differs, being
3020        // known at run time rather than compile time. This used to fall through
3021        // to the plain-callee path below and lose `this`, so `o['self']?.()`
3022        // threw where `o.self?.()` worked.
3023        if let Expr::Index {
3024            object,
3025            index,
3026            optional: obj_optional,
3027        } = func
3028        {
3029            self.compile_expr(b, object)?; // [recv]
3030            let jobj = if *obj_optional {
3031                Some(self.emit_optional_guard(b))
3032            } else {
3033                None
3034            };
3035            b.emit(Op::Dup, 0); // [recv, recv]
3036            self.compile_expr(b, index)?; // [recv, recv, key]
3037            b.emit(Op::CallBuiltin(ops::GETITEM, 2), 0); // [recv, fn]
3038            b.emit(Op::Dup, 0);
3039            b.emit(Op::CallBuiltin(ops::NULLISH, 1), 0);
3040            let jlive = b.emit(Op::JumpIfFalse(0), 0);
3041            b.emit(Op::Pop, 0);
3042            b.emit(Op::Pop, 0);
3043            b.emit(Op::LoadUndef, 0);
3044            let jend = b.emit(Op::Jump(0), 0);
3045            let live = b.current_pos();
3046            b.patch_jump(jlive, live);
3047            let via = if has_spread { "apply" } else { "call" };
3048            self.name_const(b, via); // [recv, fn, via]
3049            b.emit(Op::Rot, 0); // [fn, via, recv]
3050            let extra = if has_spread {
3051                self.compile_spread_args(b, args)?;
3052                1
3053            } else {
3054                for a in args {
3055                    self.compile_expr(b, a)?;
3056                }
3057                args.len()
3058            };
3059            b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + extra)?), 0);
3060            match self.opt_chain.last_mut() {
3061                Some(frame) => {
3062                    frame.push(jend);
3063                    if let Some(j) = jobj {
3064                        frame.push(j);
3065                    }
3066                }
3067                None => {
3068                    let end = b.current_pos();
3069                    b.patch_jump(jend, end);
3070                    if let Some(j) = jobj {
3071                        b.patch_jump(j, end);
3072                    }
3073                }
3074            }
3075            return Ok(());
3076        }
3077        // Plain callee (`f?.()`): evaluate it, guard, then call with no
3078        // receiver — a bare expression has none to keep.
3079        self.compile_expr(b, func)?;
3080        let jend = self.emit_optional_guard(b);
3081        if has_spread {
3082            self.compile_spread_args(b, args)?;
3083            b.emit(Op::CallBuiltin(ops::APPLY, 2), 0);
3084        } else {
3085            for a in args {
3086                self.compile_expr(b, a)?;
3087            }
3088            b.emit(Op::CallBuiltin(ops::CALL_VALUE, argc(1 + args.len())?), 0);
3089        }
3090        let end = b.current_pos();
3091        b.patch_jump(jend, end);
3092        Ok(())
3093    }
3094
3095    fn compile_call(
3096        &mut self,
3097        b: &mut ChunkBuilder,
3098        func: &Expr,
3099        args: &[Expr],
3100        optional: bool,
3101    ) -> Result<(), String> {
3102        if optional {
3103            return self.compile_optional_call(b, func, args);
3104        }
3105        let has_spread = args.iter().any(|a| matches!(a, Expr::Spread(_)));
3106        match func {
3107            // `super(...args)` — invoke the parent constructor on the current
3108            // `this` (SUPER_CALL runs the parent ctor + this class's field inits).
3109            Expr::Super => {
3110                for a in args {
3111                    self.compile_expr(b, a)?;
3112                }
3113                b.emit(Op::CallBuiltin(ops::SUPER_CALL, argc(args.len())?), 0);
3114                return Ok(());
3115            }
3116            // `super.method(...args)` — resolve the parent method, call it bound to
3117            // the current `this` via `method.call(this, ...args)`.
3118            Expr::Member {
3119                object, property, ..
3120            } if matches!(**object, Expr::Super) => {
3121                self.name_const(b, property);
3122                b.emit(Op::CallBuiltin(ops::SUPER_GET, 1), 0); // [method]
3123                self.name_const(b, "call"); // [method, "call"]
3124                b.emit(Op::CallBuiltin(ops::THIS, 0), 0); // [method, "call", this]
3125                                                          // `method.call(this, ...args)`: compile args (spread expands into
3126                                                          // the flat run) and dispatch as a method call named "call".
3127                for a in args {
3128                    self.compile_expr(b, a)?;
3129                }
3130                b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(3 + args.len())?), 0);
3131                return Ok(());
3132            }
3133            Expr::Member {
3134                object,
3135                property,
3136                optional,
3137            } => {
3138                self.compile_expr(b, object)?;
3139                // `obj?.method(...)`: if `obj` is nullish, short-circuit the whole
3140                // call to `undefined` (skip the method name, args, and dispatch).
3141                let jshort = if *optional {
3142                    Some(self.emit_optional_guard(b))
3143                } else {
3144                    None
3145                };
3146                self.name_const(b, property);
3147                if has_spread {
3148                    self.compile_spread_args(b, args)?; // [recv, name, argsArray]
3149                    b.emit(Op::CallBuiltin(ops::APPLY_METHOD, 3), 0);
3150                } else {
3151                    // Arguments are not on the chain's spine: a `?.` inside one
3152                    // is its own chain and must not jump past this call.
3153                    for a in args {
3154                        self.compile_off_spine(b, a)?;
3155                    }
3156                    let at = b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(2 + args.len())?), 0);
3157                    self.note_call_site(at, func);
3158                }
3159                if let Some(j) = jshort {
3160                    match self.opt_chain.last_mut() {
3161                        Some(frame) => frame.push(j),
3162                        None => {
3163                            let end = b.current_pos();
3164                            b.patch_jump(j, end);
3165                        }
3166                    }
3167                }
3168            }
3169            Expr::Index {
3170                object,
3171                index,
3172                optional,
3173            } => {
3174                // recv[expr](args) — evaluate as a method via computed name.
3175                self.compile_expr(b, object)?; // [recv]
3176                                               // `recv?.[expr](...)`: short-circuit to `undefined` when nullish.
3177                let jshort = if *optional {
3178                    Some(self.emit_optional_guard(b))
3179                } else {
3180                    None
3181                };
3182                // 13.3.6 EvaluateCall: the receiver of `recv[expr](...)` is
3183                // `recv`, exactly as for `recv.name(...)`. This used to read the
3184                // function with GETITEM, DROP the receiver, and call the value
3185                // with no `this` — the comment called it "approximated", and it
3186                // silently produced wrong answers rather than errors:
3187                //
3188                //     const o = {x: 42, f() { return this.x }};
3189                //     o.f()      // 42
3190                //     o['f']()   // undefined      <- was
3191                //     c['m']()   // TypeError      <- on a class instance
3192                //
3193                // CALL_METHOD/APPLY_METHOD take the name off the STACK, so a
3194                // computed key dispatches through the same path a static one
3195                // does and keeps the receiver.
3196                self.compile_expr(b, index)?; // [recv, name]
3197                if has_spread {
3198                    self.compile_spread_args(b, args)?; // [recv, name, argsArray]
3199                    b.emit(Op::CallBuiltin(ops::APPLY_METHOD, 3), 0);
3200                } else {
3201                    for a in args {
3202                        self.compile_off_spine(b, a)?;
3203                    }
3204                    let at = b.emit(Op::CallBuiltin(ops::CALL_METHOD, argc(2 + args.len())?), 0);
3205                    self.note_call_site(at, func);
3206                }
3207                if let Some(j) = jshort {
3208                    let end = b.current_pos();
3209                    b.patch_jump(j, end);
3210                }
3211            }
3212            // A slotted callee has no name to resolve at run time: it falls
3213            // through to the value path below, which reads the slot and calls
3214            // through `CALL_VALUE`.
3215            Expr::Ident(n) if self.slot_of(n).is_none() => {
3216                self.name_const(b, n);
3217                if has_spread {
3218                    self.compile_spread_args(b, args)?; // [name, argsArray]
3219                                                        // Resolve name to a value, then APPLY.
3220                    b.emit(Op::Swap, 0); // [argsArray, name]
3221                    b.emit(Op::CallBuiltin(ops::GETLOCAL, 1), 0); // [argsArray, fn]
3222                    b.emit(Op::Swap, 0); // [fn, argsArray]
3223                    b.emit(Op::CallBuiltin(ops::APPLY, 2), 0);
3224                } else {
3225                    for a in args {
3226                        self.compile_expr(b, a)?;
3227                    }
3228                    let at = b.emit(Op::CallBuiltin(ops::CALL, argc(1 + args.len())?), 0);
3229                    self.note_call_site(at, func);
3230                }
3231            }
3232            _ => {
3233                self.compile_expr(b, func)?;
3234                if has_spread {
3235                    self.compile_spread_args(b, args)?;
3236                    b.emit(Op::CallBuiltin(ops::APPLY, 2), 0);
3237                } else {
3238                    for a in args {
3239                        self.compile_expr(b, a)?;
3240                    }
3241                    let at = b.emit(Op::CallBuiltin(ops::CALL_VALUE, argc(1 + args.len())?), 0);
3242                    self.note_call_site(at, func);
3243                }
3244            }
3245        }
3246        Ok(())
3247    }
3248
3249    /// Build a flat args array from a mix of plain args and `...spread` args.
3250    fn compile_spread_args(&mut self, b: &mut ChunkBuilder, args: &[Expr]) -> Result<(), String> {
3251        for a in args {
3252            match a {
3253                Expr::Spread(inner) => {
3254                    b.emit(Op::LoadInt(1), 0);
3255                    self.compile_expr(b, inner)?;
3256                }
3257                _ => {
3258                    b.emit(Op::LoadInt(0), 0);
3259                    self.compile_expr(b, a)?;
3260                }
3261            }
3262        }
3263        b.emit(Op::CallBuiltin(ops::BUILD_ARGS, argc(args.len() * 2)?), 0);
3264        Ok(())
3265    }
3266
3267    fn compile_new(
3268        &mut self,
3269        b: &mut ChunkBuilder,
3270        callee: &Expr,
3271        args: &[Expr],
3272    ) -> Result<(), String> {
3273        self.compile_expr(b, callee)?;
3274        for a in args {
3275            self.compile_expr(b, a)?;
3276        }
3277        let at = b.emit(Op::CallBuiltin(ops::NEW, argc(1 + args.len())?), 0);
3278        self.note_call_site(at, callee);
3279        Ok(())
3280    }
3281}
3282
3283/// A prologue statement applying a parameter default: `if (name === undefined)
3284/// name = default;`.
3285fn default_stmt(name: &str, default: &Expr) -> Stmt {
3286    Stmt::from(StmtKind::If {
3287        test: Expr::Binary(
3288            BinOp::EqEqEq,
3289            Box::new(Expr::Ident(name.to_string())),
3290            Box::new(Expr::Undefined),
3291        ),
3292        cons: Box::new(Stmt::from(StmtKind::Expr(Expr::Assign {
3293            target: Box::new(Expr::Ident(name.to_string())),
3294            value: Box::new(default.clone()),
3295        }))),
3296        alt: None,
3297    })
3298}
3299
3300/// Every name a `var` binds inside one function scope, in source order.
3301///
3302/// Descends through block-scoped constructs, because `var` is not block-scoped,
3303/// and stops at a nested `function` declaration, whose body is its own scope.
3304/// Function *expressions* and arrows are inside `Expr`, which is not walked at
3305/// all: a `var` can only be introduced by a statement.
3306fn collect_var_names(s: &Stmt, out: &mut Vec<String>) {
3307    match &s.kind {
3308        StmtKind::Decl {
3309            kind: DeclKind::Var,
3310            decls,
3311        } => {
3312            for d in decls {
3313                pattern_names(&d.target, out);
3314            }
3315        }
3316        StmtKind::Block(body) => body.iter().for_each(|s| collect_var_names(s, out)),
3317        StmtKind::If { cons, alt, .. } => {
3318            collect_var_names(cons, out);
3319            if let Some(a) = alt {
3320                collect_var_names(a, out);
3321            }
3322        }
3323        StmtKind::While { body, .. }
3324        | StmtKind::DoWhile { body, .. }
3325        | StmtKind::Labeled { body, .. } => collect_var_names(body, out),
3326        StmtKind::For { init, body, .. } => {
3327            if let Some(i) = init {
3328                collect_var_names(i, out);
3329            }
3330            collect_var_names(body, out);
3331        }
3332        StmtKind::ForOf {
3333            decl_kind,
3334            target,
3335            body,
3336            ..
3337        }
3338        | StmtKind::ForIn {
3339            decl_kind,
3340            target,
3341            body,
3342            ..
3343        } => {
3344            if *decl_kind == Some(DeclKind::Var) {
3345                pattern_names(target, out);
3346            }
3347            collect_var_names(body, out);
3348        }
3349        StmtKind::Switch { cases, .. } => {
3350            for c in cases {
3351                c.body.iter().for_each(|s| collect_var_names(s, out));
3352            }
3353        }
3354        StmtKind::Try {
3355            block,
3356            handler,
3357            finalizer,
3358        } => {
3359            block.iter().for_each(|s| collect_var_names(s, out));
3360            if let Some((_, body)) = handler {
3361                // The catch PARAMETER is block-scoped to the handler, so it is
3362                // not collected; a `var` in the handler body still hoists.
3363                body.iter().for_each(|s| collect_var_names(s, out));
3364            }
3365            if let Some(f) = finalizer {
3366                f.iter().for_each(|s| collect_var_names(s, out));
3367            }
3368        }
3369        _ => {}
3370    }
3371}
3372
3373/// The binding names a declaration target introduces, destructuring included.
3374fn pattern_names(target: &Expr, out: &mut Vec<String>) {
3375    match target {
3376        Expr::Ident(n) => {
3377            if !out.iter().any(|x| x == n) {
3378                out.push(n.clone());
3379            }
3380        }
3381        Expr::Array(items) => items.iter().for_each(|i| pattern_names(i, out)),
3382        Expr::Object(props) => {
3383            for p in props {
3384                match p {
3385                    Prop::KeyValue { value, .. } => pattern_names(value, out),
3386                    Prop::Spread(e) => pattern_names(e, out),
3387                    Prop::Accessor { .. } => {}
3388                }
3389            }
3390        }
3391        // `[a = 1]` / `{a: b = 1}` — the binding is the target, not the default.
3392        Expr::Assign { target, .. } => pattern_names(target, out),
3393        Expr::Spread(inner) => pattern_names(inner, out),
3394        // A member target (`[obj.x] = …`) assigns a property, binding nothing.
3395        _ => {}
3396    }
3397}