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