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