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