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