we (web engine): Experimental web browser project to understand the limits of Claude
1//! AST → register-based bytecode compiler.
2//!
3//! Walks the AST produced by the parser and emits bytecode instructions.
4//! Uses a simple greedy register allocator: each new temporary gets the next
5//! available register, and registers are freed when no longer needed.
6
7use crate::ast::*;
8use crate::bytecode::*;
9use crate::JsError;
10use std::collections::HashSet;
11
12/// Compiler state for a single function scope.
13struct FunctionCompiler {
14 builder: BytecodeBuilder,
15 /// Maps local variable names to their register slots.
16 locals: Vec<Local>,
17 /// Next free register index.
18 next_reg: u8,
19 /// Stack of loop contexts for break/continue.
20 loop_stack: Vec<LoopCtx>,
21 /// Upvalues captured from the parent scope (used in inner functions).
22 upvalues: Vec<UpvalueEntry>,
23 /// Set of local variable names that are captured by inner functions.
24 /// Pre-populated before compilation by scanning inner function bodies.
25 captured_names: HashSet<String>,
26}
27
28#[derive(Debug, Clone)]
29struct Local {
30 name: String,
31 reg: Reg,
32 /// Whether this variable is captured by an inner function (stored in a cell).
33 is_captured: bool,
34 /// Whether this variable was declared with `const`.
35 is_const: bool,
36}
37
38/// An upvalue entry tracking how this function captures an outer variable.
39struct UpvalueEntry {
40 /// Name of the captured variable (for dedup during resolution).
41 name: String,
42 /// The resolved upvalue definition.
43 def: UpvalueDef,
44 /// Whether the original declaration was `const`.
45 is_const: bool,
46}
47
48struct LoopCtx {
49 /// Label, if this is a labeled loop.
50 label: Option<String>,
51 /// Patch positions for break jumps.
52 break_patches: Vec<usize>,
53 /// Patch positions for continue jumps (patched after body compilation).
54 continue_patches: Vec<usize>,
55}
56
57impl FunctionCompiler {
58 fn new(name: String, param_count: u8) -> Self {
59 Self {
60 builder: BytecodeBuilder::new(name, param_count),
61 locals: Vec::new(),
62 next_reg: 0,
63 loop_stack: Vec::new(),
64 upvalues: Vec::new(),
65 captured_names: HashSet::new(),
66 }
67 }
68
69 /// Allocate a register, updating the high-water mark.
70 fn alloc_reg(&mut self) -> Reg {
71 let r = self.next_reg;
72 self.next_reg = self.next_reg.checked_add(1).expect("register overflow");
73 if self.next_reg > self.builder.func.register_count {
74 self.builder.func.register_count = self.next_reg;
75 }
76 r
77 }
78
79 /// Free the last allocated register (must be called in reverse order).
80 fn free_reg(&mut self, r: Reg) {
81 debug_assert_eq!(
82 r,
83 self.next_reg - 1,
84 "registers must be freed in reverse order"
85 );
86 self.next_reg -= 1;
87 }
88
89 /// Look up a local variable by name, returning full info.
90 fn find_local_info(&self, name: &str) -> Option<&Local> {
91 self.locals.iter().rev().find(|l| l.name == name)
92 }
93
94 /// Look up an upvalue by name, returning its index.
95 fn find_upvalue(&self, name: &str) -> Option<u8> {
96 self.upvalues
97 .iter()
98 .position(|u| u.name == name)
99 .map(|i| i as u8)
100 }
101
102 /// Check if an upvalue is const.
103 fn is_upvalue_const(&self, idx: u8) -> bool {
104 self.upvalues
105 .get(idx as usize)
106 .map(|u| u.is_const)
107 .unwrap_or(false)
108 }
109
110 /// Define a local variable with capture and const flags.
111 fn define_local_ext(&mut self, name: &str, is_captured: bool, is_const: bool) -> Reg {
112 let reg = self.alloc_reg();
113 self.locals.push(Local {
114 name: name.to_string(),
115 reg,
116 is_captured,
117 is_const,
118 });
119 reg
120 }
121}
122
123// ── Free variable analysis ──────────────────────────────────
124
125/// Collect identifiers referenced inside `body` that are not declared locally
126/// (params or variable declarations within the body). This set represents the
127/// "free variables" of a function body — variables that must be captured.
128/// This includes transitive free variables from nested functions.
129fn collect_free_vars(params: &[Pattern], body: &[Stmt]) -> HashSet<String> {
130 let mut declared = HashSet::new();
131 let mut referenced = HashSet::new();
132
133 // Params are local declarations.
134 for p in params {
135 collect_pattern_names(p, &mut declared);
136 }
137
138 // Collect declarations and references from the body.
139 for stmt in body {
140 collect_stmt_decls(stmt, &mut declared);
141 }
142 for stmt in body {
143 collect_stmt_refs(stmt, &declared, &mut referenced);
144 }
145
146 // Also include transitive free variables from nested inner functions.
147 // If an inner function references `x` and `x` is not declared in THIS scope,
148 // then `x` is also a free variable of THIS function.
149 let inner_caps = collect_inner_captures(body);
150 for name in inner_caps {
151 if !declared.contains(&name) {
152 referenced.insert(name);
153 }
154 }
155
156 referenced
157}
158
159/// Collect free variables from an arrow function body.
160fn collect_free_vars_arrow(params: &[Pattern], body: &ArrowBody) -> HashSet<String> {
161 let mut declared = HashSet::new();
162 let mut referenced = HashSet::new();
163
164 for p in params {
165 collect_pattern_names(p, &mut declared);
166 }
167
168 match body {
169 ArrowBody::Expr(expr) => {
170 collect_expr_refs(expr, &declared, &mut referenced);
171 }
172 ArrowBody::Block(stmts) => {
173 for stmt in stmts {
174 collect_stmt_decls(stmt, &mut declared);
175 }
176 for stmt in stmts {
177 collect_stmt_refs(stmt, &declared, &mut referenced);
178 }
179 // Transitive free variables from nested functions.
180 let inner_caps = collect_inner_captures(stmts);
181 for name in inner_caps {
182 if !declared.contains(&name) {
183 referenced.insert(name);
184 }
185 }
186 }
187 }
188
189 referenced
190}
191
192fn collect_pattern_names(pat: &Pattern, names: &mut HashSet<String>) {
193 match &pat.kind {
194 PatternKind::Identifier(name) => {
195 names.insert(name.clone());
196 }
197 PatternKind::Array { elements, rest } => {
198 for elem in elements.iter().flatten() {
199 collect_pattern_names(elem, names);
200 }
201 if let Some(rest) = rest {
202 collect_pattern_names(rest, names);
203 }
204 }
205 PatternKind::Object { properties, rest } => {
206 for prop in properties {
207 collect_pattern_names(&prop.value, names);
208 }
209 if let Some(rest) = rest {
210 collect_pattern_names(rest, names);
211 }
212 }
213 PatternKind::Assign { left, .. } => {
214 collect_pattern_names(left, names);
215 }
216 }
217}
218
219/// Collect all variable/function declarations in a statement (not recursing into
220/// inner functions — those form their own scope).
221fn collect_stmt_decls(stmt: &Stmt, declared: &mut HashSet<String>) {
222 match &stmt.kind {
223 StmtKind::VarDecl { declarators, .. } => {
224 for d in declarators {
225 collect_pattern_names(&d.pattern, declared);
226 }
227 }
228 StmtKind::FunctionDecl(f) => {
229 if let Some(name) = &f.id {
230 declared.insert(name.clone());
231 }
232 }
233 StmtKind::ClassDecl(c) => {
234 if let Some(name) = &c.id {
235 declared.insert(name.clone());
236 }
237 }
238 StmtKind::Block(stmts) => {
239 for s in stmts {
240 collect_stmt_decls(s, declared);
241 }
242 }
243 StmtKind::If {
244 consequent,
245 alternate,
246 ..
247 } => {
248 collect_stmt_decls(consequent, declared);
249 if let Some(alt) = alternate {
250 collect_stmt_decls(alt, declared);
251 }
252 }
253 StmtKind::While { body, .. }
254 | StmtKind::DoWhile { body, .. }
255 | StmtKind::Labeled { body, .. } => {
256 collect_stmt_decls(body, declared);
257 }
258 StmtKind::For { init, body, .. } => {
259 if let Some(ForInit::VarDecl { declarators, .. }) = init {
260 for d in declarators {
261 collect_pattern_names(&d.pattern, declared);
262 }
263 }
264 collect_stmt_decls(body, declared);
265 }
266 StmtKind::ForIn { left, body, .. } | StmtKind::ForOf { left, body, .. } => {
267 if let ForInOfLeft::VarDecl { pattern, .. } = left {
268 collect_pattern_names(pattern, declared);
269 }
270 collect_stmt_decls(body, declared);
271 }
272 StmtKind::Try {
273 block,
274 handler,
275 finalizer,
276 } => {
277 for s in block {
278 collect_stmt_decls(s, declared);
279 }
280 if let Some(h) = handler {
281 if let Some(param) = &h.param {
282 collect_pattern_names(param, declared);
283 }
284 for s in &h.body {
285 collect_stmt_decls(s, declared);
286 }
287 }
288 if let Some(fin) = finalizer {
289 for s in fin {
290 collect_stmt_decls(s, declared);
291 }
292 }
293 }
294 StmtKind::Switch { cases, .. } => {
295 for case in cases {
296 for s in &case.consequent {
297 collect_stmt_decls(s, declared);
298 }
299 }
300 }
301 _ => {}
302 }
303}
304
305/// Collect all identifier references in a statement, excluding inner function scopes.
306/// Identifiers that are in `declared` are local and skipped.
307fn collect_stmt_refs(stmt: &Stmt, declared: &HashSet<String>, refs: &mut HashSet<String>) {
308 match &stmt.kind {
309 StmtKind::Expr(expr) => collect_expr_refs(expr, declared, refs),
310 StmtKind::Block(stmts) => {
311 for s in stmts {
312 collect_stmt_refs(s, declared, refs);
313 }
314 }
315 StmtKind::VarDecl { declarators, .. } => {
316 for d in declarators {
317 if let Some(init) = &d.init {
318 collect_expr_refs(init, declared, refs);
319 }
320 }
321 }
322 StmtKind::FunctionDecl(_) => {
323 // Don't recurse into inner functions — they have their own scope.
324 }
325 StmtKind::If {
326 test,
327 consequent,
328 alternate,
329 } => {
330 collect_expr_refs(test, declared, refs);
331 collect_stmt_refs(consequent, declared, refs);
332 if let Some(alt) = alternate {
333 collect_stmt_refs(alt, declared, refs);
334 }
335 }
336 StmtKind::While { test, body } => {
337 collect_expr_refs(test, declared, refs);
338 collect_stmt_refs(body, declared, refs);
339 }
340 StmtKind::DoWhile { body, test } => {
341 collect_stmt_refs(body, declared, refs);
342 collect_expr_refs(test, declared, refs);
343 }
344 StmtKind::For {
345 init,
346 test,
347 update,
348 body,
349 } => {
350 if let Some(init) = init {
351 match init {
352 ForInit::VarDecl { declarators, .. } => {
353 for d in declarators {
354 if let Some(init) = &d.init {
355 collect_expr_refs(init, declared, refs);
356 }
357 }
358 }
359 ForInit::Expr(e) => collect_expr_refs(e, declared, refs),
360 }
361 }
362 if let Some(t) = test {
363 collect_expr_refs(t, declared, refs);
364 }
365 if let Some(u) = update {
366 collect_expr_refs(u, declared, refs);
367 }
368 collect_stmt_refs(body, declared, refs);
369 }
370 StmtKind::ForIn { right, body, .. } | StmtKind::ForOf { right, body, .. } => {
371 collect_expr_refs(right, declared, refs);
372 collect_stmt_refs(body, declared, refs);
373 }
374 StmtKind::Return(Some(expr)) | StmtKind::Throw(expr) => {
375 collect_expr_refs(expr, declared, refs);
376 }
377 StmtKind::Try {
378 block,
379 handler,
380 finalizer,
381 } => {
382 for s in block {
383 collect_stmt_refs(s, declared, refs);
384 }
385 if let Some(h) = handler {
386 for s in &h.body {
387 collect_stmt_refs(s, declared, refs);
388 }
389 }
390 if let Some(fin) = finalizer {
391 for s in fin {
392 collect_stmt_refs(s, declared, refs);
393 }
394 }
395 }
396 StmtKind::Switch {
397 discriminant,
398 cases,
399 } => {
400 collect_expr_refs(discriminant, declared, refs);
401 for case in cases {
402 if let Some(test) = &case.test {
403 collect_expr_refs(test, declared, refs);
404 }
405 for s in &case.consequent {
406 collect_stmt_refs(s, declared, refs);
407 }
408 }
409 }
410 StmtKind::Labeled { body, .. } => {
411 collect_stmt_refs(body, declared, refs);
412 }
413 _ => {}
414 }
415}
416
417/// Collect identifier references in an expression. Does NOT recurse into
418/// inner function/arrow bodies (those form their own scope).
419fn collect_expr_refs(expr: &Expr, declared: &HashSet<String>, refs: &mut HashSet<String>) {
420 match &expr.kind {
421 ExprKind::Identifier(name) => {
422 if !declared.contains(name) {
423 refs.insert(name.clone());
424 }
425 }
426 ExprKind::Binary { left, right, .. }
427 | ExprKind::Logical { left, right, .. }
428 | ExprKind::Assignment { left, right, .. } => {
429 collect_expr_refs(left, declared, refs);
430 collect_expr_refs(right, declared, refs);
431 }
432 ExprKind::Unary { argument, .. } | ExprKind::Update { argument, .. } => {
433 collect_expr_refs(argument, declared, refs);
434 }
435 ExprKind::Conditional {
436 test,
437 consequent,
438 alternate,
439 } => {
440 collect_expr_refs(test, declared, refs);
441 collect_expr_refs(consequent, declared, refs);
442 collect_expr_refs(alternate, declared, refs);
443 }
444 ExprKind::Call { callee, arguments } | ExprKind::New { callee, arguments } => {
445 collect_expr_refs(callee, declared, refs);
446 for arg in arguments {
447 collect_expr_refs(arg, declared, refs);
448 }
449 }
450 ExprKind::Member {
451 object,
452 property,
453 computed,
454 ..
455 } => {
456 collect_expr_refs(object, declared, refs);
457 if *computed {
458 collect_expr_refs(property, declared, refs);
459 }
460 }
461 ExprKind::Array(elements) => {
462 for elem in elements.iter().flatten() {
463 match elem {
464 ArrayElement::Expr(e) | ArrayElement::Spread(e) => {
465 collect_expr_refs(e, declared, refs);
466 }
467 }
468 }
469 }
470 ExprKind::Object(props) => {
471 for prop in props {
472 if let PropertyKey::Computed(e) = &prop.key {
473 collect_expr_refs(e, declared, refs);
474 }
475 if let Some(val) = &prop.value {
476 collect_expr_refs(val, declared, refs);
477 }
478 }
479 }
480 ExprKind::Sequence(exprs) => {
481 for e in exprs {
482 collect_expr_refs(e, declared, refs);
483 }
484 }
485 ExprKind::Spread(inner) => {
486 collect_expr_refs(inner, declared, refs);
487 }
488 ExprKind::TemplateLiteral { expressions, .. } => {
489 for e in expressions {
490 collect_expr_refs(e, declared, refs);
491 }
492 }
493 // Function/Arrow/Class bodies are new scopes — don't recurse.
494 ExprKind::Function(_) | ExprKind::Arrow { .. } | ExprKind::Class(_) => {}
495 _ => {}
496 }
497}
498
499/// Collect the free variables of ALL inner functions/arrows within a list of
500/// statements. Returns the set of outer-scope names they reference.
501fn collect_inner_captures(stmts: &[Stmt]) -> HashSet<String> {
502 let mut captures = HashSet::new();
503 for stmt in stmts {
504 collect_inner_captures_stmt(stmt, &mut captures);
505 }
506 captures
507}
508
509fn collect_inner_captures_stmt(stmt: &Stmt, caps: &mut HashSet<String>) {
510 match &stmt.kind {
511 StmtKind::FunctionDecl(f) => {
512 let fv = collect_free_vars(&f.params, &f.body);
513 caps.extend(fv);
514 }
515 StmtKind::Expr(expr) => collect_inner_captures_expr(expr, caps),
516 StmtKind::VarDecl { declarators, .. } => {
517 for d in declarators {
518 if let Some(init) = &d.init {
519 collect_inner_captures_expr(init, caps);
520 }
521 }
522 }
523 StmtKind::Block(stmts) => {
524 for s in stmts {
525 collect_inner_captures_stmt(s, caps);
526 }
527 }
528 StmtKind::If {
529 test,
530 consequent,
531 alternate,
532 } => {
533 collect_inner_captures_expr(test, caps);
534 collect_inner_captures_stmt(consequent, caps);
535 if let Some(alt) = alternate {
536 collect_inner_captures_stmt(alt, caps);
537 }
538 }
539 StmtKind::While { test, body } => {
540 collect_inner_captures_expr(test, caps);
541 collect_inner_captures_stmt(body, caps);
542 }
543 StmtKind::DoWhile { body, test } => {
544 collect_inner_captures_stmt(body, caps);
545 collect_inner_captures_expr(test, caps);
546 }
547 StmtKind::For {
548 init,
549 test,
550 update,
551 body,
552 } => {
553 if let Some(ForInit::Expr(e)) = init {
554 collect_inner_captures_expr(e, caps);
555 }
556 if let Some(ForInit::VarDecl { declarators, .. }) = init {
557 for d in declarators {
558 if let Some(init) = &d.init {
559 collect_inner_captures_expr(init, caps);
560 }
561 }
562 }
563 if let Some(t) = test {
564 collect_inner_captures_expr(t, caps);
565 }
566 if let Some(u) = update {
567 collect_inner_captures_expr(u, caps);
568 }
569 collect_inner_captures_stmt(body, caps);
570 }
571 StmtKind::ForIn { right, body, .. } | StmtKind::ForOf { right, body, .. } => {
572 collect_inner_captures_expr(right, caps);
573 collect_inner_captures_stmt(body, caps);
574 }
575 StmtKind::Return(Some(expr)) | StmtKind::Throw(expr) => {
576 collect_inner_captures_expr(expr, caps);
577 }
578 StmtKind::Try {
579 block,
580 handler,
581 finalizer,
582 } => {
583 for s in block {
584 collect_inner_captures_stmt(s, caps);
585 }
586 if let Some(h) = handler {
587 for s in &h.body {
588 collect_inner_captures_stmt(s, caps);
589 }
590 }
591 if let Some(fin) = finalizer {
592 for s in fin {
593 collect_inner_captures_stmt(s, caps);
594 }
595 }
596 }
597 StmtKind::Switch {
598 discriminant,
599 cases,
600 } => {
601 collect_inner_captures_expr(discriminant, caps);
602 for case in cases {
603 if let Some(test) = &case.test {
604 collect_inner_captures_expr(test, caps);
605 }
606 for s in &case.consequent {
607 collect_inner_captures_stmt(s, caps);
608 }
609 }
610 }
611 StmtKind::Labeled { body, .. } => {
612 collect_inner_captures_stmt(body, caps);
613 }
614 _ => {}
615 }
616}
617
618fn collect_inner_captures_expr(expr: &Expr, caps: &mut HashSet<String>) {
619 match &expr.kind {
620 ExprKind::Function(f) => {
621 let fv = collect_free_vars(&f.params, &f.body);
622 caps.extend(fv);
623 }
624 ExprKind::Arrow { params, body, .. } => {
625 let fv = collect_free_vars_arrow(params, body);
626 caps.extend(fv);
627 }
628 ExprKind::Binary { left, right, .. }
629 | ExprKind::Logical { left, right, .. }
630 | ExprKind::Assignment { left, right, .. } => {
631 collect_inner_captures_expr(left, caps);
632 collect_inner_captures_expr(right, caps);
633 }
634 ExprKind::Unary { argument, .. } | ExprKind::Update { argument, .. } => {
635 collect_inner_captures_expr(argument, caps);
636 }
637 ExprKind::Conditional {
638 test,
639 consequent,
640 alternate,
641 } => {
642 collect_inner_captures_expr(test, caps);
643 collect_inner_captures_expr(consequent, caps);
644 collect_inner_captures_expr(alternate, caps);
645 }
646 ExprKind::Call { callee, arguments } | ExprKind::New { callee, arguments } => {
647 collect_inner_captures_expr(callee, caps);
648 for arg in arguments {
649 collect_inner_captures_expr(arg, caps);
650 }
651 }
652 ExprKind::Member {
653 object,
654 property,
655 computed,
656 ..
657 } => {
658 collect_inner_captures_expr(object, caps);
659 if *computed {
660 collect_inner_captures_expr(property, caps);
661 }
662 }
663 ExprKind::Array(elements) => {
664 for elem in elements.iter().flatten() {
665 match elem {
666 ArrayElement::Expr(e) | ArrayElement::Spread(e) => {
667 collect_inner_captures_expr(e, caps);
668 }
669 }
670 }
671 }
672 ExprKind::Object(props) => {
673 for prop in props {
674 if let PropertyKey::Computed(e) = &prop.key {
675 collect_inner_captures_expr(e, caps);
676 }
677 if let Some(val) = &prop.value {
678 collect_inner_captures_expr(val, caps);
679 }
680 }
681 }
682 ExprKind::Sequence(exprs) => {
683 for e in exprs {
684 collect_inner_captures_expr(e, caps);
685 }
686 }
687 ExprKind::Spread(inner) => {
688 collect_inner_captures_expr(inner, caps);
689 }
690 ExprKind::TemplateLiteral { expressions, .. } => {
691 for e in expressions {
692 collect_inner_captures_expr(e, caps);
693 }
694 }
695 ExprKind::Class(c) => {
696 for member in &c.body {
697 if let ClassMemberKind::Method { value, .. } = &member.kind {
698 let fv = collect_free_vars(&value.params, &value.body);
699 caps.extend(fv);
700 }
701 }
702 }
703 _ => {}
704 }
705}
706
707/// Compile a parsed program into a top-level bytecode function.
708pub fn compile(program: &Program) -> Result<Function, JsError> {
709 let mut fc = FunctionCompiler::new("<main>".into(), 0);
710
711 // Pre-scan to find which top-level locals are captured by inner functions.
712 fc.captured_names = collect_inner_captures(&program.body);
713
714 // Reserve r0 for the implicit return value.
715 let result_reg = fc.alloc_reg();
716 fc.builder.emit_reg(Op::LoadUndefined, result_reg);
717
718 compile_stmts(&mut fc, &program.body, result_reg)?;
719
720 fc.builder.emit_reg(Op::Return, result_reg);
721 Ok(fc.builder.finish())
722}
723
724fn compile_stmts(
725 fc: &mut FunctionCompiler,
726 stmts: &[Stmt],
727 result_reg: Reg,
728) -> Result<(), JsError> {
729 for stmt in stmts {
730 compile_stmt(fc, stmt, result_reg)?;
731 }
732 Ok(())
733}
734
735fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Result<(), JsError> {
736 match &stmt.kind {
737 StmtKind::Expr(expr) => {
738 // Expression statement: compile and store result in result_reg.
739 compile_expr(fc, expr, result_reg)?;
740 }
741
742 StmtKind::Block(stmts) => {
743 let saved_locals = fc.locals.len();
744 let saved_next = fc.next_reg;
745 compile_stmts(fc, stmts, result_reg)?;
746 // Pop locals from this block.
747 fc.locals.truncate(saved_locals);
748 fc.next_reg = saved_next;
749 }
750
751 StmtKind::VarDecl { kind, declarators } => {
752 for decl in declarators {
753 compile_var_declarator(fc, decl, *kind)?;
754 }
755 }
756
757 StmtKind::FunctionDecl(func_def) => {
758 compile_function_decl(fc, func_def)?;
759 }
760
761 StmtKind::If {
762 test,
763 consequent,
764 alternate,
765 } => {
766 compile_if(fc, test, consequent, alternate.as_deref(), result_reg)?;
767 }
768
769 StmtKind::While { test, body } => {
770 compile_while(fc, test, body, None, result_reg)?;
771 }
772
773 StmtKind::DoWhile { body, test } => {
774 compile_do_while(fc, body, test, None, result_reg)?;
775 }
776
777 StmtKind::For {
778 init,
779 test,
780 update,
781 body,
782 } => {
783 compile_for(
784 fc,
785 init.as_ref(),
786 test.as_ref(),
787 update.as_ref(),
788 body,
789 None,
790 result_reg,
791 )?;
792 }
793
794 StmtKind::ForIn { left, right, body } => {
795 let saved_locals = fc.locals.len();
796 let saved_next = fc.next_reg;
797
798 // Evaluate the RHS object.
799 let obj_r = fc.alloc_reg();
800 compile_expr(fc, right, obj_r)?;
801
802 // ForInInit: collect enumerable keys into an array.
803 let keys_r = fc.alloc_reg();
804 fc.builder.emit_reg_reg(Op::ForInInit, keys_r, obj_r);
805 // obj_r is no longer needed but we don't free it (LIFO constraint).
806
807 // Index register (starts at 0).
808 let idx_r = fc.alloc_reg();
809 fc.builder.emit_load_int8(idx_r, 0);
810
811 // Key and done registers (reused each iteration).
812 let key_r = fc.alloc_reg();
813 let done_r = fc.alloc_reg();
814
815 let loop_start = fc.builder.offset();
816
817 // ForInNext: get next key or done flag.
818 fc.builder
819 .emit_reg4(Op::ForInNext, key_r, done_r, keys_r, idx_r);
820
821 // Exit loop if done.
822 let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r);
823
824 // Bind the loop variable.
825 match left {
826 ForInOfLeft::VarDecl { kind, pattern } => {
827 if let PatternKind::Identifier(name) = &pattern.kind {
828 let is_captured = fc.captured_names.contains(name.as_str());
829 let is_const = *kind == VarKind::Const;
830 let var_r = fc.define_local_ext(name, is_captured, is_const);
831 if is_captured {
832 fc.builder.emit_reg(Op::NewCell, var_r);
833 fc.builder.emit_reg_reg(Op::CellStore, var_r, key_r);
834 } else {
835 fc.builder.emit_reg_reg(Op::Move, var_r, key_r);
836 }
837 }
838 }
839 ForInOfLeft::Pattern(pattern) => {
840 if let PatternKind::Identifier(name) = &pattern.kind {
841 if let Some(local) = fc.find_local_info(name) {
842 let reg = local.reg;
843 let captured = local.is_captured;
844 if captured {
845 fc.builder.emit_reg_reg(Op::CellStore, reg, key_r);
846 } else {
847 fc.builder.emit_reg_reg(Op::Move, reg, key_r);
848 }
849 } else if let Some(uv_idx) = fc.find_upvalue(name) {
850 fc.builder.emit_store_upvalue(uv_idx, key_r);
851 } else {
852 let ni = fc.builder.add_name(name);
853 fc.builder.emit_store_global(ni, key_r);
854 }
855 }
856 }
857 }
858
859 // Compile the body.
860 fc.loop_stack.push(LoopCtx {
861 label: None,
862 break_patches: Vec::new(),
863 continue_patches: Vec::new(),
864 });
865 compile_stmt(fc, body, result_reg)?;
866
867 // Increment index: idx = idx + 1.
868 // Use a temp register for the constant 1. Since we allocate it
869 // after the loop body, we can't free it with LIFO either — the
870 // saved_next restoration handles cleanup.
871 let one_r = fc.alloc_reg();
872 fc.builder.emit_load_int8(one_r, 1);
873 fc.builder.emit_reg3(Op::Add, idx_r, idx_r, one_r);
874
875 // Jump back to loop start.
876 fc.builder.emit_jump_to(loop_start);
877
878 // Patch exit and continue jumps.
879 fc.builder.patch_jump(exit_patch);
880 let ctx = fc.loop_stack.pop().unwrap();
881 for patch in ctx.break_patches {
882 fc.builder.patch_jump(patch);
883 }
884 for patch in ctx.continue_patches {
885 fc.builder.patch_jump_to(patch, loop_start);
886 }
887
888 // Restore locals/regs — frees all temporaries at once.
889 fc.locals.truncate(saved_locals);
890 fc.next_reg = saved_next;
891 }
892
893 StmtKind::ForOf {
894 left,
895 right,
896 body,
897 is_await: _,
898 } => {
899 let saved_locals = fc.locals.len();
900 let saved_next = fc.next_reg;
901
902 // Evaluate the iterable.
903 let iterable_r = fc.alloc_reg();
904 compile_expr(fc, right, iterable_r)?;
905
906 // Get the iterator: call iterable[@@iterator]().
907 let iter_method_r = fc.alloc_reg();
908 let sym_iter_ni = fc.builder.add_name("@@iterator");
909 fc.builder
910 .emit_get_prop_name(iter_method_r, iterable_r, sym_iter_ni);
911
912 // Set `this` = iterable for the call.
913 let this_ni = fc.builder.add_name("this");
914 fc.builder.emit_store_global(this_ni, iterable_r);
915
916 // Call [@@iterator]() with 0 args.
917 let iterator_r = fc.alloc_reg();
918 let args_start = fc.next_reg;
919 fc.builder
920 .emit_call(iterator_r, iter_method_r, args_start, 0);
921
922 // Temp registers for next method, result, done, value.
923 let next_method_r = fc.alloc_reg();
924 let next_ni = fc.builder.add_name("next");
925 fc.builder
926 .emit_get_prop_name(next_method_r, iterator_r, next_ni);
927
928 let result_obj_r = fc.alloc_reg();
929 let done_r = fc.alloc_reg();
930 let value_r = fc.alloc_reg();
931
932 // Loop start.
933 let loop_start = fc.builder.offset();
934
935 // Set `this` = iterator for the .next() call.
936 fc.builder.emit_store_global(this_ni, iterator_r);
937
938 // Call iterator.next().
939 fc.builder
940 .emit_call(result_obj_r, next_method_r, args_start, 0);
941
942 // Extract done and value.
943 let done_ni = fc.builder.add_name("done");
944 let value_ni = fc.builder.add_name("value");
945 fc.builder.emit_get_prop_name(done_r, result_obj_r, done_ni);
946
947 // Exit if done.
948 let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r);
949
950 // Extract value.
951 fc.builder
952 .emit_get_prop_name(value_r, result_obj_r, value_ni);
953
954 // Bind the loop variable.
955 match left {
956 ForInOfLeft::VarDecl { kind, pattern } => match &pattern.kind {
957 PatternKind::Identifier(name) => {
958 let is_captured = fc.captured_names.contains(name.as_str());
959 let is_const = *kind == VarKind::Const;
960 let var_r = fc.define_local_ext(name, is_captured, is_const);
961 if is_captured {
962 fc.builder.emit_reg(Op::NewCell, var_r);
963 fc.builder.emit_reg_reg(Op::CellStore, var_r, value_r);
964 } else {
965 fc.builder.emit_reg_reg(Op::Move, var_r, value_r);
966 }
967 }
968 _ => {
969 // Destructuring pattern in for...of.
970 compile_destructuring_pattern(fc, pattern, value_r)?;
971 }
972 },
973 ForInOfLeft::Pattern(pattern) => match &pattern.kind {
974 PatternKind::Identifier(name) => {
975 if let Some(local) = fc.find_local_info(name) {
976 let reg = local.reg;
977 let captured = local.is_captured;
978 if captured {
979 fc.builder.emit_reg_reg(Op::CellStore, reg, value_r);
980 } else {
981 fc.builder.emit_reg_reg(Op::Move, reg, value_r);
982 }
983 } else if let Some(uv_idx) = fc.find_upvalue(name) {
984 fc.builder.emit_store_upvalue(uv_idx, value_r);
985 } else {
986 let ni = fc.builder.add_name(name);
987 fc.builder.emit_store_global(ni, value_r);
988 }
989 }
990 _ => {
991 compile_destructuring_pattern(fc, pattern, value_r)?;
992 }
993 },
994 }
995
996 // Push loop context for break/continue.
997 fc.loop_stack.push(LoopCtx {
998 label: None,
999 break_patches: Vec::new(),
1000 continue_patches: Vec::new(),
1001 });
1002
1003 // Compile body.
1004 compile_stmt(fc, body, result_reg)?;
1005
1006 // Jump back to loop start.
1007 fc.builder.emit_jump_to(loop_start);
1008
1009 // Patch exit.
1010 fc.builder.patch_jump(exit_patch);
1011 let ctx = fc.loop_stack.pop().unwrap();
1012 for patch in ctx.break_patches {
1013 fc.builder.patch_jump(patch);
1014 }
1015 for patch in ctx.continue_patches {
1016 fc.builder.patch_jump_to(patch, loop_start);
1017 }
1018
1019 // Restore locals/regs.
1020 fc.locals.truncate(saved_locals);
1021 fc.next_reg = saved_next;
1022 }
1023
1024 StmtKind::Return(expr) => {
1025 let ret_reg = fc.alloc_reg();
1026 if let Some(e) = expr {
1027 compile_expr(fc, e, ret_reg)?;
1028 } else {
1029 fc.builder.emit_reg(Op::LoadUndefined, ret_reg);
1030 }
1031 fc.builder.emit_reg(Op::Return, ret_reg);
1032 fc.free_reg(ret_reg);
1033 }
1034
1035 StmtKind::Throw(expr) => {
1036 let tmp = fc.alloc_reg();
1037 compile_expr(fc, expr, tmp)?;
1038 fc.builder.emit_reg(Op::Throw, tmp);
1039 fc.free_reg(tmp);
1040 }
1041
1042 StmtKind::Break(label) => {
1043 // Find the matching loop context.
1044 let idx = find_loop_ctx(&fc.loop_stack, label.as_deref())
1045 .ok_or_else(|| JsError::SyntaxError("break outside of loop".into()))?;
1046 let patch = fc.builder.emit_jump(Op::Jump);
1047 fc.loop_stack[idx].break_patches.push(patch);
1048 }
1049
1050 StmtKind::Continue(label) => {
1051 let idx = find_loop_ctx(&fc.loop_stack, label.as_deref())
1052 .ok_or_else(|| JsError::SyntaxError("continue outside of loop".into()))?;
1053 let patch = fc.builder.emit_jump(Op::Jump);
1054 fc.loop_stack[idx].continue_patches.push(patch);
1055 }
1056
1057 StmtKind::Labeled { label, body } => {
1058 // If body is a loop, propagate the label.
1059 match &body.kind {
1060 StmtKind::While { test, body: inner } => {
1061 compile_while(fc, test, inner, Some(label.clone()), result_reg)?;
1062 }
1063 StmtKind::DoWhile { body: inner, test } => {
1064 compile_do_while(fc, inner, test, Some(label.clone()), result_reg)?;
1065 }
1066 StmtKind::For {
1067 init,
1068 test,
1069 update,
1070 body: inner,
1071 } => {
1072 compile_for(
1073 fc,
1074 init.as_ref(),
1075 test.as_ref(),
1076 update.as_ref(),
1077 inner,
1078 Some(label.clone()),
1079 result_reg,
1080 )?;
1081 }
1082 _ => {
1083 compile_stmt(fc, body, result_reg)?;
1084 }
1085 }
1086 }
1087
1088 StmtKind::Switch {
1089 discriminant,
1090 cases,
1091 } => {
1092 compile_switch(fc, discriminant, cases, result_reg)?;
1093 }
1094
1095 StmtKind::Try {
1096 block,
1097 handler,
1098 finalizer,
1099 } => {
1100 if let Some(catch) = handler {
1101 // The catch register will receive the exception value. Use the
1102 // current next_reg so it doesn't conflict with temporaries
1103 // allocated inside the try block.
1104 let saved_next = fc.next_reg;
1105 let catch_reg = fc.alloc_reg();
1106 // Immediately "release" it so the try block can reuse registers
1107 // from this point. We remember catch_reg for PushExceptionHandler.
1108 fc.next_reg = saved_next;
1109
1110 // Emit PushExceptionHandler with placeholder offset to catch block.
1111 let catch_patch = fc.builder.emit_push_exception_handler(catch_reg);
1112
1113 let locals_len = fc.locals.len();
1114
1115 // Compile the try block.
1116 compile_stmts(fc, block, result_reg)?;
1117
1118 // If we reach here, no exception was thrown. Pop handler and
1119 // jump past the catch block.
1120 fc.builder.emit_pop_exception_handler();
1121 let end_patch = fc.builder.emit_jump(Op::Jump);
1122
1123 // Reset register state for catch block — locals declared in
1124 // the try block are out of scope.
1125 fc.locals.truncate(locals_len);
1126 fc.next_reg = saved_next;
1127
1128 // Patch the exception handler to jump here (catch block start).
1129 fc.builder.patch_jump(catch_patch);
1130
1131 // Bind the catch parameter if present.
1132 if let Some(param) = &catch.param {
1133 if let PatternKind::Identifier(name) = ¶m.kind {
1134 let is_captured = fc.captured_names.contains(name.as_str());
1135 let local = fc.define_local_ext(name, is_captured, false);
1136 if is_captured {
1137 fc.builder.emit_reg(Op::NewCell, local);
1138 fc.builder.emit_reg_reg(Op::CellStore, local, catch_reg);
1139 } else {
1140 fc.builder.emit_reg_reg(Op::Move, local, catch_reg);
1141 }
1142 }
1143 }
1144
1145 // Compile the catch body.
1146 compile_stmts(fc, &catch.body, result_reg)?;
1147
1148 // End of catch — restore state.
1149 fc.locals.truncate(locals_len);
1150 fc.next_reg = saved_next;
1151
1152 // Jump target from the try block.
1153 fc.builder.patch_jump(end_patch);
1154 } else {
1155 // No catch handler: just compile the try block.
1156 compile_stmts(fc, block, result_reg)?;
1157 }
1158
1159 // Compile the finally block (always runs after try or catch).
1160 if let Some(fin) = finalizer {
1161 compile_stmts(fc, fin, result_reg)?;
1162 }
1163 }
1164
1165 StmtKind::Empty | StmtKind::Debugger => {
1166 // No-op.
1167 }
1168
1169 StmtKind::With { object, body } => {
1170 // Compile `with` as: evaluate object (discard), then run body.
1171 // Proper `with` scope requires VM support.
1172 let tmp = fc.alloc_reg();
1173 compile_expr(fc, object, tmp)?;
1174 fc.free_reg(tmp);
1175 compile_stmt(fc, body, result_reg)?;
1176 }
1177
1178 StmtKind::Import { .. } => {
1179 // Module imports are resolved before execution; no bytecode needed.
1180 }
1181
1182 StmtKind::Export(export) => {
1183 compile_export(fc, export, result_reg)?;
1184 }
1185
1186 StmtKind::ClassDecl(class_def) => {
1187 compile_class_decl(fc, class_def)?;
1188 }
1189 }
1190 Ok(())
1191}
1192
1193// ── Variable declarations ───────────────────────────────────
1194
1195fn compile_var_declarator(
1196 fc: &mut FunctionCompiler,
1197 decl: &VarDeclarator,
1198 kind: VarKind,
1199) -> Result<(), JsError> {
1200 match &decl.pattern.kind {
1201 PatternKind::Identifier(name) => {
1202 let is_const = kind == VarKind::Const;
1203 let is_captured = fc.captured_names.contains(name.as_str());
1204
1205 if is_const && decl.init.is_none() {
1206 return Err(JsError::SyntaxError(
1207 "Missing initializer in const declaration".into(),
1208 ));
1209 }
1210
1211 let reg = fc.define_local_ext(name, is_captured, is_const);
1212
1213 if is_captured {
1214 // Allocate a cell for this variable.
1215 fc.builder.emit_reg(Op::NewCell, reg);
1216 if let Some(init) = &decl.init {
1217 let tmp = fc.alloc_reg();
1218 compile_expr(fc, init, tmp)?;
1219 fc.builder.emit_reg_reg(Op::CellStore, reg, tmp);
1220 fc.free_reg(tmp);
1221 }
1222 // No init => cell stays undefined (already the default).
1223 } else if let Some(init) = &decl.init {
1224 compile_expr(fc, init, reg)?;
1225 } else {
1226 fc.builder.emit_reg(Op::LoadUndefined, reg);
1227 }
1228 }
1229 _ => {
1230 // Destructuring: evaluate init, then bind patterns.
1231 // Note: don't free tmp — destructuring pattern allocates permanent
1232 // local registers above it. The tmp register slot is reused via
1233 // next_reg restoration by the parent scope.
1234 let tmp = fc.alloc_reg();
1235 if let Some(init) = &decl.init {
1236 compile_expr(fc, init, tmp)?;
1237 } else {
1238 fc.builder.emit_reg(Op::LoadUndefined, tmp);
1239 }
1240 compile_destructuring_pattern(fc, &decl.pattern, tmp)?;
1241 }
1242 }
1243 Ok(())
1244}
1245
1246fn compile_destructuring_pattern(
1247 fc: &mut FunctionCompiler,
1248 pattern: &Pattern,
1249 src: Reg,
1250) -> Result<(), JsError> {
1251 match &pattern.kind {
1252 PatternKind::Identifier(name) => {
1253 let is_captured = fc.captured_names.contains(name.as_str());
1254 let reg = fc.define_local_ext(name, is_captured, false);
1255 if is_captured {
1256 fc.builder.emit_reg(Op::NewCell, reg);
1257 fc.builder.emit_reg_reg(Op::CellStore, reg, src);
1258 } else {
1259 fc.builder.emit_reg_reg(Op::Move, reg, src);
1260 }
1261 }
1262 PatternKind::Object { properties, rest } => {
1263 // For each property, extract the value and bind it.
1264 // We use a single temp register that we reuse for each property
1265 // by resetting next_reg after each binding.
1266 for prop in properties {
1267 let key_name = match &prop.key {
1268 PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(),
1269 PropertyKey::Computed(expr) => {
1270 let saved = fc.next_reg;
1271 let key_reg = fc.alloc_reg();
1272 compile_expr(fc, expr, key_reg)?;
1273 let val_reg = fc.alloc_reg();
1274 fc.builder.emit_reg3(Op::GetProperty, val_reg, src, key_reg);
1275 compile_destructuring_pattern(fc, &prop.value, val_reg)?;
1276 // Temp regs are buried; just let them be.
1277 let _ = saved;
1278 continue;
1279 }
1280 PropertyKey::Number(n) => {
1281 if n.fract() == 0.0 && n.abs() < 1e15 {
1282 format!("{}", *n as i64)
1283 } else {
1284 format!("{n}")
1285 }
1286 }
1287 };
1288 // For simple identifier patterns, load property directly into
1289 // the target register to avoid LIFO register allocation issues.
1290 if let PatternKind::Identifier(name) = &prop.value.kind {
1291 let is_captured = fc.captured_names.contains(name.as_str());
1292 let reg = fc.define_local_ext(name, is_captured, false);
1293 let name_idx = fc.builder.add_name(&key_name);
1294 if is_captured {
1295 let tmp = fc.alloc_reg();
1296 fc.builder.emit_get_prop_name(tmp, src, name_idx);
1297 fc.builder.emit_reg(Op::NewCell, reg);
1298 fc.builder.emit_reg_reg(Op::CellStore, reg, tmp);
1299 fc.free_reg(tmp);
1300 } else {
1301 fc.builder.emit_get_prop_name(reg, src, name_idx);
1302 }
1303 } else {
1304 // Complex inner pattern (nested, default, etc.)
1305 // Allocate temp, extract value, then recurse.
1306 // Temp register won't be freed (LIFO constraint with inner locals).
1307 let val_reg = fc.alloc_reg();
1308 let name_idx = fc.builder.add_name(&key_name);
1309 fc.builder.emit_get_prop_name(val_reg, src, name_idx);
1310 compile_destructuring_pattern(fc, &prop.value, val_reg)?;
1311 }
1312 }
1313
1314 // Handle rest: collect remaining own enumerable properties.
1315 if let Some(rest_pat) = rest {
1316 // Collect extracted key names for exclusion.
1317 let extracted_keys: Vec<String> = properties
1318 .iter()
1319 .filter_map(|prop| match &prop.key {
1320 PropertyKey::Identifier(s) | PropertyKey::String(s) => Some(s.clone()),
1321 _ => None,
1322 })
1323 .collect();
1324
1325 let rest_obj = fc.alloc_reg();
1326 fc.builder.emit_reg(Op::CreateObject, rest_obj);
1327
1328 let keys_r = fc.alloc_reg();
1329 fc.builder.emit_reg_reg(Op::ForInInit, keys_r, src);
1330 let idx_r = fc.alloc_reg();
1331 fc.builder.emit_load_int8(idx_r, 0);
1332 let key_r = fc.alloc_reg();
1333 let done_r = fc.alloc_reg();
1334
1335 let loop_start = fc.builder.offset();
1336 fc.builder
1337 .emit_reg4(Op::ForInNext, key_r, done_r, keys_r, idx_r);
1338 let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r);
1339
1340 let mut skip_patches = Vec::new();
1341 for excluded in &extracted_keys {
1342 let excluded_r = fc.alloc_reg();
1343 let ci = fc.builder.add_constant(Constant::String(excluded.clone()));
1344 fc.builder.emit_reg_u16(Op::LoadConst, excluded_r, ci);
1345 let cmp_r = fc.alloc_reg();
1346 fc.builder.emit_reg3(Op::StrictEq, cmp_r, key_r, excluded_r);
1347 let skip = fc.builder.emit_cond_jump(Op::JumpIfTrue, cmp_r);
1348 skip_patches.push(skip);
1349 fc.free_reg(cmp_r);
1350 fc.free_reg(excluded_r);
1351 }
1352
1353 let val_r = fc.alloc_reg();
1354 fc.builder.emit_reg3(Op::GetProperty, val_r, src, key_r);
1355 fc.builder
1356 .emit_reg3(Op::SetProperty, rest_obj, key_r, val_r);
1357 fc.free_reg(val_r);
1358
1359 for patch in skip_patches {
1360 fc.builder.patch_jump(patch);
1361 }
1362
1363 let one_r = fc.alloc_reg();
1364 fc.builder.emit_load_int8(one_r, 1);
1365 fc.builder.emit_reg3(Op::Add, idx_r, idx_r, one_r);
1366 fc.free_reg(one_r);
1367
1368 fc.builder.emit_jump_to(loop_start);
1369 fc.builder.patch_jump(exit_patch);
1370
1371 fc.free_reg(done_r);
1372 fc.free_reg(key_r);
1373 fc.free_reg(idx_r);
1374 fc.free_reg(keys_r);
1375
1376 compile_destructuring_pattern(fc, rest_pat, rest_obj)?;
1377 }
1378 }
1379 PatternKind::Array { elements, rest } => {
1380 for (i, elem) in elements.iter().enumerate() {
1381 if let Some(pat) = elem {
1382 // For simple identifier patterns, load directly into local.
1383 if let PatternKind::Identifier(name) = &pat.kind {
1384 let is_captured = fc.captured_names.contains(name.as_str());
1385 let reg = fc.define_local_ext(name, is_captured, false);
1386 let idx_reg = fc.alloc_reg();
1387 if i <= 127 {
1388 fc.builder.emit_load_int8(idx_reg, i as i8);
1389 } else {
1390 let ci = fc.builder.add_constant(Constant::Number(i as f64));
1391 fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci);
1392 }
1393 if is_captured {
1394 let tmp = fc.alloc_reg();
1395 fc.builder.emit_reg3(Op::GetProperty, tmp, src, idx_reg);
1396 fc.builder.emit_reg(Op::NewCell, reg);
1397 fc.builder.emit_reg_reg(Op::CellStore, reg, tmp);
1398 fc.free_reg(tmp);
1399 } else {
1400 fc.builder.emit_reg3(Op::GetProperty, reg, src, idx_reg);
1401 }
1402 fc.free_reg(idx_reg);
1403 } else {
1404 // Complex inner pattern (nested, default, etc.)
1405 let idx_reg = fc.alloc_reg();
1406 if i <= 127 {
1407 fc.builder.emit_load_int8(idx_reg, i as i8);
1408 } else {
1409 let ci = fc.builder.add_constant(Constant::Number(i as f64));
1410 fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci);
1411 }
1412 let val_reg = fc.alloc_reg();
1413 fc.builder.emit_reg3(Op::GetProperty, val_reg, src, idx_reg);
1414 compile_destructuring_pattern(fc, pat, val_reg)?;
1415 // Don't free val_reg/idx_reg — inner pattern may have
1416 // allocated locals above them.
1417 }
1418 }
1419 }
1420
1421 // Handle rest element: ...rest = src.slice(elements.len())
1422 if let Some(rest_pat) = rest {
1423 let slice_fn_r = fc.alloc_reg();
1424 let slice_ni = fc.builder.add_name("slice");
1425 fc.builder.emit_get_prop_name(slice_fn_r, src, slice_ni);
1426
1427 let this_ni = fc.builder.add_name("this");
1428 fc.builder.emit_store_global(this_ni, src);
1429
1430 let start_r = fc.alloc_reg();
1431 let elem_count = elements.len();
1432 if elem_count <= 127 {
1433 fc.builder.emit_load_int8(start_r, elem_count as i8);
1434 } else {
1435 let ci = fc.builder.add_constant(Constant::Number(elem_count as f64));
1436 fc.builder.emit_reg_u16(Op::LoadConst, start_r, ci);
1437 }
1438
1439 let rest_val = fc.alloc_reg();
1440 fc.builder.emit_call(rest_val, slice_fn_r, start_r, 1);
1441
1442 compile_destructuring_pattern(fc, rest_pat, rest_val)?;
1443 // Don't free temps — rest pattern allocates locals.
1444 }
1445 }
1446 PatternKind::Assign { left, right } => {
1447 // Default value: if src is undefined, use default.
1448 let val_reg = fc.alloc_reg();
1449 fc.builder.emit_reg_reg(Op::Move, val_reg, src);
1450 // Check if undefined, if so use default.
1451 let check_reg = fc.alloc_reg();
1452 let undef_reg = fc.alloc_reg();
1453 fc.builder.emit_reg(Op::LoadUndefined, undef_reg);
1454 fc.builder
1455 .emit_reg3(Op::StrictEq, check_reg, val_reg, undef_reg);
1456 fc.free_reg(undef_reg);
1457 let patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, check_reg);
1458 fc.free_reg(check_reg);
1459 // Is undefined → evaluate default.
1460 compile_expr(fc, right, val_reg)?;
1461 fc.builder.patch_jump(patch);
1462 compile_destructuring_pattern(fc, left, val_reg)?;
1463 // Don't free val_reg — inner pattern may have allocated locals.
1464 }
1465 }
1466 Ok(())
1467}
1468
1469// ── Function declarations ───────────────────────────────────
1470
1471fn compile_function_decl(fc: &mut FunctionCompiler, func_def: &FunctionDef) -> Result<(), JsError> {
1472 let name = func_def.id.clone().unwrap_or_default();
1473 let inner = compile_function_body_with_captures(fc, func_def)?;
1474 let func_idx = fc.builder.add_function(inner);
1475
1476 let is_captured = fc.captured_names.contains(name.as_str());
1477 let reg = fc.define_local_ext(&name, is_captured, false);
1478
1479 if is_captured {
1480 // Create a cell, then create the closure into a temp, then store into cell.
1481 fc.builder.emit_reg(Op::NewCell, reg);
1482 let tmp = fc.alloc_reg();
1483 fc.builder.emit_reg_u16(Op::CreateClosure, tmp, func_idx);
1484 fc.builder.emit_reg_reg(Op::CellStore, reg, tmp);
1485 // Also store as global.
1486 if !name.is_empty() {
1487 let name_idx = fc.builder.add_name(&name);
1488 fc.builder.emit_store_global(name_idx, tmp);
1489 }
1490 fc.free_reg(tmp);
1491 } else {
1492 fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx);
1493 // Also store as global so inner/recursive calls via LoadGlobal can find it.
1494 if !name.is_empty() {
1495 let name_idx = fc.builder.add_name(&name);
1496 fc.builder.emit_store_global(name_idx, reg);
1497 }
1498 }
1499 Ok(())
1500}
1501
1502/// Compile a function body, resolving upvalue captures from the parent scope.
1503fn compile_function_body_with_captures(
1504 parent: &mut FunctionCompiler,
1505 func_def: &FunctionDef,
1506) -> Result<Function, JsError> {
1507 // 1. Collect free variables of this inner function.
1508 let free_vars = collect_free_vars(&func_def.params, &func_def.body);
1509
1510 // 2. Build upvalue list by resolving free vars against the parent scope.
1511 let mut upvalue_entries = Vec::new();
1512 for name in &free_vars {
1513 if let Some(local) = parent.find_local_info(name) {
1514 let reg = local.reg;
1515 let is_const = local.is_const;
1516 // Mark the parent's local as captured (if not already).
1517 // We need to update the parent's local, so find the index and mutate.
1518 if let Some(l) = parent.locals.iter_mut().rev().find(|l| l.name == *name) {
1519 l.is_captured = true;
1520 }
1521 upvalue_entries.push(UpvalueEntry {
1522 name: name.clone(),
1523 def: UpvalueDef {
1524 is_local: true,
1525 index: reg,
1526 },
1527 is_const,
1528 });
1529 } else if let Some(parent_uv_idx) = parent.find_upvalue(name) {
1530 // Transitive capture: the parent captures it from its own parent.
1531 let is_const = parent.is_upvalue_const(parent_uv_idx);
1532 upvalue_entries.push(UpvalueEntry {
1533 name: name.clone(),
1534 def: UpvalueDef {
1535 is_local: false,
1536 index: parent_uv_idx,
1537 },
1538 is_const,
1539 });
1540 }
1541 // If not found in parent or parent's upvalues, it must be a global — no upvalue needed.
1542 }
1543
1544 // 3. Compile the inner function with its own scope.
1545 let mut inner = compile_function_body_inner(func_def, &upvalue_entries)?;
1546
1547 // 4. Attach upvalue definitions to the compiled function.
1548 inner.upvalue_defs = upvalue_entries.iter().map(|e| e.def.clone()).collect();
1549
1550 Ok(inner)
1551}
1552
1553/// Core function body compilation. The `upvalue_entries` tell this function which
1554/// outer variables it can access via LoadUpvalue/StoreUpvalue.
1555fn compile_function_body_inner(
1556 func_def: &FunctionDef,
1557 upvalue_entries: &[UpvalueEntry],
1558) -> Result<Function, JsError> {
1559 let name = func_def.id.clone().unwrap_or_default();
1560 let param_count = func_def.params.len().min(255) as u8;
1561 let mut inner = FunctionCompiler::new(name, param_count);
1562
1563 // Copy upvalue entries into the inner compiler so it can resolve references.
1564 for entry in upvalue_entries {
1565 inner.upvalues.push(UpvalueEntry {
1566 name: entry.name.clone(),
1567 def: entry.def.clone(),
1568 is_const: entry.is_const,
1569 });
1570 }
1571
1572 // Pre-scan to find which of this function's locals are captured by ITS inner functions.
1573 let inner_caps = collect_inner_captures(&func_def.body);
1574 inner.captured_names = inner_caps;
1575
1576 // Allocate registers for parameters.
1577 for p in &func_def.params {
1578 if let PatternKind::Identifier(pname) = &p.kind {
1579 let is_captured = inner.captured_names.contains(pname.as_str());
1580 inner.define_local_ext(pname, is_captured, false);
1581 } else {
1582 let _ = inner.alloc_reg();
1583 }
1584 }
1585
1586 // Box captured parameters into cells.
1587 for p in &func_def.params {
1588 if let PatternKind::Identifier(pname) = &p.kind {
1589 if let Some(local) = inner.find_local_info(pname) {
1590 if local.is_captured {
1591 let reg = local.reg;
1592 // Move param value to temp, allocate cell, store value into cell.
1593 let tmp = inner.alloc_reg();
1594 inner.builder.emit_reg_reg(Op::Move, tmp, reg);
1595 inner.builder.emit_reg(Op::NewCell, reg);
1596 inner.builder.emit_reg_reg(Op::CellStore, reg, tmp);
1597 inner.free_reg(tmp);
1598 }
1599 }
1600 }
1601 }
1602
1603 // Result register for the function body.
1604 let result_reg = inner.alloc_reg();
1605 inner.builder.emit_reg(Op::LoadUndefined, result_reg);
1606
1607 compile_stmts(&mut inner, &func_def.body, result_reg)?;
1608
1609 // Implicit return undefined.
1610 inner.builder.emit_reg(Op::Return, result_reg);
1611 let mut func = inner.builder.finish();
1612 func.is_generator = func_def.is_generator;
1613 Ok(func)
1614}
1615
1616// ── Class declarations ──────────────────────────────────────
1617
1618fn compile_class_decl(fc: &mut FunctionCompiler, class_def: &ClassDef) -> Result<(), JsError> {
1619 let name = class_def.id.clone().unwrap_or_default();
1620 let is_captured = fc.captured_names.contains(name.as_str());
1621 let reg = fc.define_local_ext(&name, is_captured, false);
1622
1623 // For captured classes, build the constructor into a temp register so we can
1624 // set prototype methods on it before wrapping it in a cell.
1625 let ctor_reg = if is_captured { fc.alloc_reg() } else { reg };
1626
1627 // Find constructor or create empty one.
1628 let ctor = class_def.body.iter().find(|m| {
1629 matches!(
1630 &m.kind,
1631 ClassMemberKind::Method {
1632 kind: MethodKind::Constructor,
1633 ..
1634 }
1635 )
1636 });
1637
1638 if let Some(member) = ctor {
1639 if let ClassMemberKind::Method { value, .. } = &member.kind {
1640 let inner = compile_function_body_with_captures(fc, value)?;
1641 let func_idx = fc.builder.add_function(inner);
1642 fc.builder
1643 .emit_reg_u16(Op::CreateClosure, ctor_reg, func_idx);
1644 }
1645 } else {
1646 // No constructor: create a minimal function that returns undefined.
1647 let mut empty = BytecodeBuilder::new(name.clone(), 0);
1648 let r = 0u8;
1649 empty.func.register_count = 1;
1650 empty.emit_reg(Op::LoadUndefined, r);
1651 empty.emit_reg(Op::Return, r);
1652 let func_idx = fc.builder.add_function(empty.finish());
1653 fc.builder
1654 .emit_reg_u16(Op::CreateClosure, ctor_reg, func_idx);
1655 }
1656
1657 // Compile methods: set them as properties on the constructor's prototype.
1658 // This is simplified — real class compilation needs prototype chain setup.
1659 for member in &class_def.body {
1660 match &member.kind {
1661 ClassMemberKind::Method {
1662 key,
1663 value,
1664 kind,
1665 is_static: _,
1666 computed: _,
1667 } => {
1668 if matches!(kind, MethodKind::Constructor) {
1669 continue;
1670 }
1671 let method_name = match key {
1672 PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(),
1673 _ => continue,
1674 };
1675 let inner = compile_function_body_with_captures(fc, value)?;
1676 let func_idx = fc.builder.add_function(inner);
1677 let method_reg = fc.alloc_reg();
1678 fc.builder
1679 .emit_reg_u16(Op::CreateClosure, method_reg, func_idx);
1680 let name_idx = fc.builder.add_name(&method_name);
1681 fc.builder
1682 .emit_set_prop_name(ctor_reg, name_idx, method_reg);
1683 fc.free_reg(method_reg);
1684 }
1685 ClassMemberKind::Property { .. } => {
1686 // Class fields are set in constructor; skip here.
1687 }
1688 }
1689 }
1690
1691 if is_captured {
1692 fc.builder.emit_reg(Op::NewCell, reg);
1693 fc.builder.emit_reg_reg(Op::CellStore, reg, ctor_reg);
1694 fc.free_reg(ctor_reg);
1695 }
1696
1697 Ok(())
1698}
1699
1700// ── Export ───────────────────────────────────────────────────
1701
1702fn compile_export(
1703 fc: &mut FunctionCompiler,
1704 export: &ExportDecl,
1705
1706 result_reg: Reg,
1707) -> Result<(), JsError> {
1708 match export {
1709 ExportDecl::Declaration(stmt) => {
1710 compile_stmt(fc, stmt, result_reg)?;
1711 }
1712 ExportDecl::Default(expr) => {
1713 compile_expr(fc, expr, result_reg)?;
1714 }
1715 ExportDecl::Named { .. } | ExportDecl::AllFrom(_) => {
1716 // Named re-exports are module-level; no bytecode needed.
1717 }
1718 }
1719 Ok(())
1720}
1721
1722// ── Control flow ────────────────────────────────────────────
1723
1724fn compile_if(
1725 fc: &mut FunctionCompiler,
1726 test: &Expr,
1727 consequent: &Stmt,
1728 alternate: Option<&Stmt>,
1729
1730 result_reg: Reg,
1731) -> Result<(), JsError> {
1732 let cond = fc.alloc_reg();
1733 compile_expr(fc, test, cond)?;
1734 let else_patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond);
1735 fc.free_reg(cond);
1736
1737 compile_stmt(fc, consequent, result_reg)?;
1738
1739 if let Some(alt) = alternate {
1740 let end_patch = fc.builder.emit_jump(Op::Jump);
1741 fc.builder.patch_jump(else_patch);
1742 compile_stmt(fc, alt, result_reg)?;
1743 fc.builder.patch_jump(end_patch);
1744 } else {
1745 fc.builder.patch_jump(else_patch);
1746 }
1747 Ok(())
1748}
1749
1750fn compile_while(
1751 fc: &mut FunctionCompiler,
1752 test: &Expr,
1753 body: &Stmt,
1754 label: Option<String>,
1755
1756 result_reg: Reg,
1757) -> Result<(), JsError> {
1758 let loop_start = fc.builder.offset();
1759
1760 let cond = fc.alloc_reg();
1761 compile_expr(fc, test, cond)?;
1762 let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond);
1763 fc.free_reg(cond);
1764
1765 fc.loop_stack.push(LoopCtx {
1766 label,
1767 break_patches: Vec::new(),
1768 continue_patches: Vec::new(),
1769 });
1770
1771 compile_stmt(fc, body, result_reg)?;
1772 fc.builder.emit_jump_to(loop_start);
1773 fc.builder.patch_jump(exit_patch);
1774
1775 let ctx = fc.loop_stack.pop().unwrap();
1776 for patch in ctx.break_patches {
1777 fc.builder.patch_jump(patch);
1778 }
1779 // In a while loop, continue jumps back to the condition check (loop_start).
1780 for patch in ctx.continue_patches {
1781 fc.builder.patch_jump_to(patch, loop_start);
1782 }
1783 Ok(())
1784}
1785
1786fn compile_do_while(
1787 fc: &mut FunctionCompiler,
1788 body: &Stmt,
1789 test: &Expr,
1790 label: Option<String>,
1791
1792 result_reg: Reg,
1793) -> Result<(), JsError> {
1794 let loop_start = fc.builder.offset();
1795
1796 fc.loop_stack.push(LoopCtx {
1797 label,
1798 break_patches: Vec::new(),
1799 continue_patches: Vec::new(),
1800 });
1801
1802 compile_stmt(fc, body, result_reg)?;
1803
1804 // continue in do-while should jump here (the condition check).
1805 let cond_start = fc.builder.offset();
1806
1807 let cond = fc.alloc_reg();
1808 compile_expr(fc, test, cond)?;
1809 fc.builder
1810 .emit_cond_jump_to(Op::JumpIfTrue, cond, loop_start);
1811 fc.free_reg(cond);
1812
1813 let ctx = fc.loop_stack.pop().unwrap();
1814 for patch in ctx.break_patches {
1815 fc.builder.patch_jump(patch);
1816 }
1817 for patch in ctx.continue_patches {
1818 fc.builder.patch_jump_to(patch, cond_start);
1819 }
1820 Ok(())
1821}
1822
1823fn compile_for(
1824 fc: &mut FunctionCompiler,
1825 init: Option<&ForInit>,
1826 test: Option<&Expr>,
1827 update: Option<&Expr>,
1828 body: &Stmt,
1829 label: Option<String>,
1830
1831 result_reg: Reg,
1832) -> Result<(), JsError> {
1833 let saved_locals = fc.locals.len();
1834 let saved_next = fc.next_reg;
1835
1836 // Init.
1837 if let Some(init) = init {
1838 match init {
1839 ForInit::VarDecl { kind, declarators } => {
1840 for decl in declarators {
1841 compile_var_declarator(fc, decl, *kind)?;
1842 }
1843 }
1844 ForInit::Expr(expr) => {
1845 let tmp = fc.alloc_reg();
1846 compile_expr(fc, expr, tmp)?;
1847 fc.free_reg(tmp);
1848 }
1849 }
1850 }
1851
1852 let loop_start = fc.builder.offset();
1853
1854 // Test.
1855 let exit_patch = if let Some(test) = test {
1856 let cond = fc.alloc_reg();
1857 compile_expr(fc, test, cond)?;
1858 let patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond);
1859 fc.free_reg(cond);
1860 Some(patch)
1861 } else {
1862 None
1863 };
1864
1865 fc.loop_stack.push(LoopCtx {
1866 label,
1867 break_patches: Vec::new(),
1868 continue_patches: Vec::new(),
1869 });
1870
1871 compile_stmt(fc, body, result_reg)?;
1872
1873 // continue in a for-loop should jump here (the update expression).
1874 let continue_target = fc.builder.offset();
1875
1876 // Update.
1877 if let Some(update) = update {
1878 let tmp = fc.alloc_reg();
1879 compile_expr(fc, update, tmp)?;
1880 fc.free_reg(tmp);
1881 }
1882
1883 fc.builder.emit_jump_to(loop_start);
1884
1885 if let Some(patch) = exit_patch {
1886 fc.builder.patch_jump(patch);
1887 }
1888
1889 let ctx = fc.loop_stack.pop().unwrap();
1890 for patch in ctx.break_patches {
1891 fc.builder.patch_jump(patch);
1892 }
1893 for patch in ctx.continue_patches {
1894 fc.builder.patch_jump_to(patch, continue_target);
1895 }
1896
1897 fc.locals.truncate(saved_locals);
1898 fc.next_reg = saved_next;
1899 Ok(())
1900}
1901
1902fn compile_switch(
1903 fc: &mut FunctionCompiler,
1904 discriminant: &Expr,
1905 cases: &[SwitchCase],
1906
1907 result_reg: Reg,
1908) -> Result<(), JsError> {
1909 let disc_reg = fc.alloc_reg();
1910 compile_expr(fc, discriminant, disc_reg)?;
1911
1912 // Use a loop context for break statements.
1913 fc.loop_stack.push(LoopCtx {
1914 label: None,
1915 break_patches: Vec::new(),
1916 continue_patches: Vec::new(),
1917 });
1918
1919 // Phase 1: emit comparison jumps for each non-default case.
1920 // Store (case_index, patch_position) for each case with a test.
1921 let mut case_jump_patches: Vec<(usize, usize)> = Vec::new();
1922 let mut default_index: Option<usize> = None;
1923
1924 for (i, case) in cases.iter().enumerate() {
1925 if let Some(test) = &case.test {
1926 let test_reg = fc.alloc_reg();
1927 compile_expr(fc, test, test_reg)?;
1928 let cmp_reg = fc.alloc_reg();
1929 fc.builder
1930 .emit_reg3(Op::StrictEq, cmp_reg, disc_reg, test_reg);
1931 let patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, cmp_reg);
1932 fc.free_reg(cmp_reg);
1933 fc.free_reg(test_reg);
1934 case_jump_patches.push((i, patch));
1935 } else {
1936 default_index = Some(i);
1937 }
1938 }
1939
1940 // After all comparisons: jump to default body or end.
1941 let fallthrough_patch = fc.builder.emit_jump(Op::Jump);
1942
1943 // Phase 2: emit case bodies in order (fall-through semantics).
1944 let mut body_offsets: Vec<(usize, usize)> = Vec::new();
1945 for (i, case) in cases.iter().enumerate() {
1946 body_offsets.push((i, fc.builder.offset()));
1947 compile_stmts(fc, &case.consequent, result_reg)?;
1948 }
1949
1950 let end_offset = fc.builder.offset();
1951
1952 // Patch case test jumps to their respective body offsets.
1953 for (case_idx, patch) in &case_jump_patches {
1954 let body_offset = body_offsets
1955 .iter()
1956 .find(|(i, _)| i == case_idx)
1957 .map(|(_, off)| *off)
1958 .unwrap();
1959 fc.builder.patch_jump_to(*patch, body_offset);
1960 }
1961
1962 // Patch fallthrough: jump to default body if present, otherwise to end.
1963 if let Some(def_idx) = default_index {
1964 let default_offset = body_offsets
1965 .iter()
1966 .find(|(i, _)| *i == def_idx)
1967 .map(|(_, off)| *off)
1968 .unwrap();
1969 fc.builder.patch_jump_to(fallthrough_patch, default_offset);
1970 } else {
1971 fc.builder.patch_jump_to(fallthrough_patch, end_offset);
1972 }
1973
1974 fc.free_reg(disc_reg);
1975
1976 let ctx = fc.loop_stack.pop().unwrap();
1977 for patch in ctx.break_patches {
1978 fc.builder.patch_jump(patch);
1979 }
1980 Ok(())
1981}
1982
1983fn find_loop_ctx(stack: &[LoopCtx], label: Option<&str>) -> Option<usize> {
1984 if let Some(label) = label {
1985 stack
1986 .iter()
1987 .rposition(|ctx| ctx.label.as_deref() == Some(label))
1988 } else {
1989 if stack.is_empty() {
1990 None
1991 } else {
1992 Some(stack.len() - 1)
1993 }
1994 }
1995}
1996
1997// ── Expressions ─────────────────────────────────────────────
1998
1999fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), JsError> {
2000 match &expr.kind {
2001 ExprKind::Number(n) => {
2002 // Optimize small integers.
2003 let int_val = *n as i64;
2004 if int_val as f64 == *n && (-128..=127).contains(&int_val) {
2005 fc.builder.emit_load_int8(dst, int_val as i8);
2006 } else {
2007 let ci = fc.builder.add_constant(Constant::Number(*n));
2008 fc.builder.emit_reg_u16(Op::LoadConst, dst, ci);
2009 }
2010 }
2011
2012 ExprKind::String(s) => {
2013 let ci = fc.builder.add_constant(Constant::String(s.clone()));
2014 fc.builder.emit_reg_u16(Op::LoadConst, dst, ci);
2015 }
2016
2017 ExprKind::Bool(true) => {
2018 fc.builder.emit_reg(Op::LoadTrue, dst);
2019 }
2020
2021 ExprKind::Bool(false) => {
2022 fc.builder.emit_reg(Op::LoadFalse, dst);
2023 }
2024
2025 ExprKind::Null => {
2026 fc.builder.emit_reg(Op::LoadNull, dst);
2027 }
2028
2029 ExprKind::Identifier(name) => {
2030 if let Some(local) = fc.find_local_info(name) {
2031 let reg = local.reg;
2032 let captured = local.is_captured;
2033 if captured {
2034 fc.builder.emit_reg_reg(Op::CellLoad, dst, reg);
2035 } else if reg != dst {
2036 fc.builder.emit_reg_reg(Op::Move, dst, reg);
2037 }
2038 } else if let Some(uv_idx) = fc.find_upvalue(name) {
2039 fc.builder.emit_load_upvalue(dst, uv_idx);
2040 } else {
2041 // Global lookup.
2042 let ni = fc.builder.add_name(name);
2043 fc.builder.emit_load_global(dst, ni);
2044 }
2045 }
2046
2047 ExprKind::This => {
2048 // `this` is loaded as a global named "this" (the VM binds it).
2049 let ni = fc.builder.add_name("this");
2050 fc.builder.emit_load_global(dst, ni);
2051 }
2052
2053 ExprKind::Binary { op, left, right } => {
2054 let lhs = fc.alloc_reg();
2055 compile_expr(fc, left, lhs)?;
2056 let rhs = fc.alloc_reg();
2057 compile_expr(fc, right, rhs)?;
2058 let bytecode_op = binary_op_to_opcode(*op);
2059 fc.builder.emit_reg3(bytecode_op, dst, lhs, rhs);
2060 fc.free_reg(rhs);
2061 fc.free_reg(lhs);
2062 }
2063
2064 ExprKind::Unary { op, argument } => {
2065 if *op == UnaryOp::Delete {
2066 // Handle delete specially: need object + key form for member expressions.
2067 match &argument.kind {
2068 ExprKind::Member {
2069 object,
2070 property,
2071 computed,
2072 } => {
2073 let obj_r = fc.alloc_reg();
2074 compile_expr(fc, object, obj_r)?;
2075 let key_r = fc.alloc_reg();
2076 if *computed {
2077 compile_expr(fc, property, key_r)?;
2078 } else if let ExprKind::Identifier(name) = &property.kind {
2079 let ci = fc.builder.add_constant(Constant::String(name.clone()));
2080 fc.builder.emit_reg_u16(Op::LoadConst, key_r, ci);
2081 } else {
2082 compile_expr(fc, property, key_r)?;
2083 }
2084 fc.builder.emit_reg3(Op::Delete, dst, obj_r, key_r);
2085 fc.free_reg(key_r);
2086 fc.free_reg(obj_r);
2087 }
2088 _ => {
2089 // `delete x` on a simple identifier: always true in non-strict mode.
2090 fc.builder.emit_reg(Op::LoadTrue, dst);
2091 }
2092 }
2093 } else {
2094 let src = fc.alloc_reg();
2095 compile_expr(fc, argument, src)?;
2096 match op {
2097 UnaryOp::Minus => fc.builder.emit_reg_reg(Op::Neg, dst, src),
2098 UnaryOp::Plus => {
2099 fc.builder.emit_reg_reg(Op::Move, dst, src);
2100 }
2101 UnaryOp::Not => fc.builder.emit_reg_reg(Op::LogicalNot, dst, src),
2102 UnaryOp::BitwiseNot => fc.builder.emit_reg_reg(Op::BitNot, dst, src),
2103 UnaryOp::Typeof => fc.builder.emit_reg_reg(Op::TypeOf, dst, src),
2104 UnaryOp::Void => fc.builder.emit_reg_reg(Op::Void, dst, src),
2105 UnaryOp::Delete => unreachable!(),
2106 }
2107 fc.free_reg(src);
2108 }
2109 }
2110
2111 ExprKind::Update {
2112 op,
2113 argument,
2114 prefix,
2115 } => {
2116 // Get current value.
2117 compile_expr(fc, argument, dst)?;
2118
2119 let one = fc.alloc_reg();
2120 fc.builder.emit_load_int8(one, 1);
2121
2122 if *prefix {
2123 // ++x / --x: modify first, return modified.
2124 match op {
2125 UpdateOp::Increment => fc.builder.emit_reg3(Op::Add, dst, dst, one),
2126 UpdateOp::Decrement => fc.builder.emit_reg3(Op::Sub, dst, dst, one),
2127 }
2128 // Store back.
2129 compile_store(fc, argument, dst)?;
2130 } else {
2131 // x++ / x--: return original, then modify.
2132 let tmp = fc.alloc_reg();
2133 fc.builder.emit_reg_reg(Op::Move, tmp, dst);
2134 match op {
2135 UpdateOp::Increment => fc.builder.emit_reg3(Op::Add, tmp, tmp, one),
2136 UpdateOp::Decrement => fc.builder.emit_reg3(Op::Sub, tmp, tmp, one),
2137 }
2138 compile_store(fc, argument, tmp)?;
2139 fc.free_reg(tmp);
2140 }
2141 fc.free_reg(one);
2142 }
2143
2144 ExprKind::Logical { op, left, right } => {
2145 compile_expr(fc, left, dst)?;
2146 match op {
2147 LogicalOp::And => {
2148 // Short-circuit: if falsy, skip right.
2149 let skip = fc.builder.emit_cond_jump(Op::JumpIfFalse, dst);
2150 compile_expr(fc, right, dst)?;
2151 fc.builder.patch_jump(skip);
2152 }
2153 LogicalOp::Or => {
2154 let skip = fc.builder.emit_cond_jump(Op::JumpIfTrue, dst);
2155 compile_expr(fc, right, dst)?;
2156 fc.builder.patch_jump(skip);
2157 }
2158 LogicalOp::Nullish => {
2159 let skip = fc.builder.emit_cond_jump(Op::JumpIfNullish, dst);
2160 // If NOT nullish, skip the right side. Wait — JumpIfNullish
2161 // should mean "jump if nullish" so we want: evaluate left,
2162 // if NOT nullish skip right.
2163 // Let's invert: evaluate left, check if nullish → evaluate right.
2164 // We need the jump to skip the "evaluate right" if NOT nullish.
2165 // Since JumpIfNullish jumps when nullish, we need the inverse.
2166 // Instead: use a two-step approach.
2167 //
2168 // Actually, rethink: for `a ?? b`:
2169 // 1. evaluate a → dst
2170 // 2. if dst is NOT null/undefined, jump to end
2171 // 3. evaluate b → dst
2172 // end:
2173 // JumpIfNullish jumps when IS nullish. So we want jump when NOT nullish.
2174 // Let's just use a "not nullish" check.
2175 // For now: negate and use JumpIfFalse.
2176 // Actually simpler: skip right when not nullish.
2177 // JumpIfNullish jumps WHEN nullish. We want to jump over right when NOT nullish.
2178 // So:
2179 // evaluate a → dst
2180 // JumpIfNullish dst → evaluate_right
2181 // Jump → end
2182 // evaluate_right: evaluate b → dst
2183 // end:
2184 // But we already emitted JumpIfNullish. Let's fix this.
2185 // The JumpIfNullish we emitted jumps to "after patch", which is where
2186 // we'll put the right-side code. We need another jump to skip right.
2187 let end_patch = fc.builder.emit_jump(Op::Jump);
2188 fc.builder.patch_jump(skip); // nullish → evaluate right
2189 compile_expr(fc, right, dst)?;
2190 fc.builder.patch_jump(end_patch);
2191 }
2192 }
2193 }
2194
2195 ExprKind::Assignment { op, left, right } => {
2196 if *op == AssignOp::Assign {
2197 compile_expr(fc, right, dst)?;
2198 compile_store(fc, left, dst)?;
2199 } else {
2200 // Compound assignment: load current, operate, store.
2201 compile_expr(fc, left, dst)?;
2202 let rhs = fc.alloc_reg();
2203 compile_expr(fc, right, rhs)?;
2204 let arith_op = compound_assign_op(*op);
2205 fc.builder.emit_reg3(arith_op, dst, dst, rhs);
2206 fc.free_reg(rhs);
2207 compile_store(fc, left, dst)?;
2208 }
2209 }
2210
2211 ExprKind::Conditional {
2212 test,
2213 consequent,
2214 alternate,
2215 } => {
2216 let cond = fc.alloc_reg();
2217 compile_expr(fc, test, cond)?;
2218 let else_patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond);
2219 fc.free_reg(cond);
2220 compile_expr(fc, consequent, dst)?;
2221 let end_patch = fc.builder.emit_jump(Op::Jump);
2222 fc.builder.patch_jump(else_patch);
2223 compile_expr(fc, alternate, dst)?;
2224 fc.builder.patch_jump(end_patch);
2225 }
2226
2227 ExprKind::Call { callee, arguments } => {
2228 // Detect method calls (obj.method()) to set `this`.
2229 if let ExprKind::Member {
2230 object,
2231 property,
2232 computed,
2233 } = &callee.kind
2234 {
2235 // Layout: [obj_reg] [func_reg] [arg0] [arg1] ...
2236 // We keep obj_reg alive so we can set `this` before the call.
2237 let obj_reg = fc.alloc_reg();
2238 compile_expr(fc, object, obj_reg)?;
2239 let func_reg = fc.alloc_reg();
2240 if !computed {
2241 if let ExprKind::Identifier(name) = &property.kind {
2242 let ni = fc.builder.add_name(name);
2243 fc.builder.emit_get_prop_name(func_reg, obj_reg, ni);
2244 } else {
2245 let key_reg = fc.alloc_reg();
2246 compile_expr(fc, property, key_reg)?;
2247 fc.builder
2248 .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg);
2249 fc.free_reg(key_reg);
2250 }
2251 } else {
2252 let key_reg = fc.alloc_reg();
2253 compile_expr(fc, property, key_reg)?;
2254 fc.builder
2255 .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg);
2256 fc.free_reg(key_reg);
2257 }
2258
2259 // Set `this` to the receiver object before calling.
2260 let this_ni = fc.builder.add_name("this");
2261 fc.builder.emit_store_global(this_ni, obj_reg);
2262
2263 let args_start = fc.next_reg;
2264 let arg_count = arguments.len().min(255) as u8;
2265 for arg in arguments {
2266 let arg_reg = fc.alloc_reg();
2267 compile_expr(fc, arg, arg_reg)?;
2268 }
2269
2270 fc.builder.emit_call(dst, func_reg, args_start, arg_count);
2271
2272 // Free in LIFO order: args, func_reg, obj_reg.
2273 for _ in 0..arg_count {
2274 fc.next_reg -= 1;
2275 }
2276 fc.free_reg(func_reg);
2277 fc.free_reg(obj_reg);
2278 } else {
2279 let func_reg = fc.alloc_reg();
2280 compile_expr(fc, callee, func_reg)?;
2281
2282 let args_start = fc.next_reg;
2283 let arg_count = arguments.len().min(255) as u8;
2284 for arg in arguments {
2285 let arg_reg = fc.alloc_reg();
2286 compile_expr(fc, arg, arg_reg)?;
2287 }
2288
2289 fc.builder.emit_call(dst, func_reg, args_start, arg_count);
2290
2291 for _ in 0..arg_count {
2292 fc.next_reg -= 1;
2293 }
2294 fc.free_reg(func_reg);
2295 }
2296 }
2297
2298 ExprKind::New { callee, arguments } => {
2299 // For now, compile like a regular call. The VM will differentiate
2300 // based on the `New` vs `Call` distinction (TODO: add NewCall opcode).
2301 let func_reg = fc.alloc_reg();
2302 compile_expr(fc, callee, func_reg)?;
2303
2304 let args_start = fc.next_reg;
2305 let arg_count = arguments.len().min(255) as u8;
2306 for arg in arguments {
2307 let arg_reg = fc.alloc_reg();
2308 compile_expr(fc, arg, arg_reg)?;
2309 }
2310
2311 fc.builder.emit_call(dst, func_reg, args_start, arg_count);
2312
2313 for _ in 0..arg_count {
2314 fc.next_reg -= 1;
2315 }
2316 fc.free_reg(func_reg);
2317 }
2318
2319 ExprKind::Member {
2320 object,
2321 property,
2322 computed,
2323 } => {
2324 let obj_reg = fc.alloc_reg();
2325 compile_expr(fc, object, obj_reg)?;
2326
2327 if !computed {
2328 // Static member: obj.prop → GetPropertyByName.
2329 if let ExprKind::Identifier(name) = &property.kind {
2330 let ni = fc.builder.add_name(name);
2331 fc.builder.emit_get_prop_name(dst, obj_reg, ni);
2332 } else {
2333 let key_reg = fc.alloc_reg();
2334 compile_expr(fc, property, key_reg)?;
2335 fc.builder.emit_reg3(Op::GetProperty, dst, obj_reg, key_reg);
2336 fc.free_reg(key_reg);
2337 }
2338 } else {
2339 // Computed member: obj[expr].
2340 let key_reg = fc.alloc_reg();
2341 compile_expr(fc, property, key_reg)?;
2342 fc.builder.emit_reg3(Op::GetProperty, dst, obj_reg, key_reg);
2343 fc.free_reg(key_reg);
2344 }
2345 fc.free_reg(obj_reg);
2346 }
2347
2348 ExprKind::Array(elements) => {
2349 let has_spread = elements
2350 .iter()
2351 .any(|e| matches!(e, Some(ArrayElement::Spread(_))));
2352
2353 fc.builder.emit_reg(Op::CreateArray, dst);
2354
2355 if has_spread {
2356 // When spreads are present, we track the index dynamically.
2357 // For each normal element, push at current length.
2358 // For spread elements, use the Spread opcode.
2359 for el in elements.iter().flatten() {
2360 match el {
2361 ArrayElement::Expr(e) => {
2362 let val_reg = fc.alloc_reg();
2363 compile_expr(fc, e, val_reg)?;
2364 // Get current length as index.
2365 let idx_reg = fc.alloc_reg();
2366 let len_ni = fc.builder.add_name("length");
2367 fc.builder.emit_get_prop_name(idx_reg, dst, len_ni);
2368 fc.builder.emit_reg3(Op::SetProperty, dst, idx_reg, val_reg);
2369 // Increment length.
2370 let one_r = fc.alloc_reg();
2371 fc.builder.emit_load_int8(one_r, 1);
2372 fc.builder.emit_reg3(Op::Add, idx_reg, idx_reg, one_r);
2373 fc.builder.emit_set_prop_name(dst, len_ni, idx_reg);
2374 fc.free_reg(one_r);
2375 fc.free_reg(idx_reg);
2376 fc.free_reg(val_reg);
2377 }
2378 ArrayElement::Spread(e) => {
2379 let spread_src = fc.alloc_reg();
2380 compile_expr(fc, e, spread_src)?;
2381 fc.builder.emit_spread(dst, spread_src);
2382 fc.free_reg(spread_src);
2383 }
2384 }
2385 }
2386 } else {
2387 // No spreads: use simple indexed assignment.
2388 for (i, elem) in elements.iter().enumerate() {
2389 if let Some(ArrayElement::Expr(e)) = elem {
2390 let val_reg = fc.alloc_reg();
2391 compile_expr(fc, e, val_reg)?;
2392 let idx_reg = fc.alloc_reg();
2393 if i <= 127 {
2394 fc.builder.emit_load_int8(idx_reg, i as i8);
2395 } else {
2396 let ci = fc.builder.add_constant(Constant::Number(i as f64));
2397 fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci);
2398 }
2399 fc.builder.emit_reg3(Op::SetProperty, dst, idx_reg, val_reg);
2400 fc.free_reg(idx_reg);
2401 fc.free_reg(val_reg);
2402 }
2403 }
2404 // Set length.
2405 if !elements.is_empty() {
2406 let len_name = fc.builder.add_name("length");
2407 let len_reg = fc.alloc_reg();
2408 if elements.len() <= 127 {
2409 fc.builder.emit_load_int8(len_reg, elements.len() as i8);
2410 } else {
2411 let ci = fc
2412 .builder
2413 .add_constant(Constant::Number(elements.len() as f64));
2414 fc.builder.emit_reg_u16(Op::LoadConst, len_reg, ci);
2415 }
2416 fc.builder.emit_set_prop_name(dst, len_name, len_reg);
2417 fc.free_reg(len_reg);
2418 }
2419 }
2420 }
2421
2422 ExprKind::Object(properties) => {
2423 fc.builder.emit_reg(Op::CreateObject, dst);
2424 for prop in properties {
2425 let val_reg = fc.alloc_reg();
2426 if let Some(value) = &prop.value {
2427 compile_expr(fc, value, val_reg)?;
2428 } else {
2429 // Shorthand: `{ x }` means `{ x: x }`.
2430 if let PropertyKey::Identifier(name) = &prop.key {
2431 if let Some(local) = fc.find_local_info(name) {
2432 let reg = local.reg;
2433 let captured = local.is_captured;
2434 if captured {
2435 fc.builder.emit_reg_reg(Op::CellLoad, val_reg, reg);
2436 } else {
2437 fc.builder.emit_reg_reg(Op::Move, val_reg, reg);
2438 }
2439 } else if let Some(uv_idx) = fc.find_upvalue(name) {
2440 fc.builder.emit_load_upvalue(val_reg, uv_idx);
2441 } else {
2442 let ni = fc.builder.add_name(name);
2443 fc.builder.emit_load_global(val_reg, ni);
2444 }
2445 } else {
2446 fc.builder.emit_reg(Op::LoadUndefined, val_reg);
2447 }
2448 }
2449
2450 match &prop.key {
2451 PropertyKey::Identifier(name) | PropertyKey::String(name) => {
2452 let ni = fc.builder.add_name(name);
2453 fc.builder.emit_set_prop_name(dst, ni, val_reg);
2454 }
2455 PropertyKey::Number(n) => {
2456 let key_reg = fc.alloc_reg();
2457 let ci = fc.builder.add_constant(Constant::Number(*n));
2458 fc.builder.emit_reg_u16(Op::LoadConst, key_reg, ci);
2459 fc.builder.emit_reg3(Op::SetProperty, dst, key_reg, val_reg);
2460 fc.free_reg(key_reg);
2461 }
2462 PropertyKey::Computed(expr) => {
2463 let key_reg = fc.alloc_reg();
2464 compile_expr(fc, expr, key_reg)?;
2465 fc.builder.emit_reg3(Op::SetProperty, dst, key_reg, val_reg);
2466 fc.free_reg(key_reg);
2467 }
2468 }
2469 fc.free_reg(val_reg);
2470 }
2471 }
2472
2473 ExprKind::Function(func_def) => {
2474 let inner = compile_function_body_with_captures(fc, func_def)?;
2475 let func_idx = fc.builder.add_function(inner);
2476 fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx);
2477 }
2478
2479 ExprKind::Arrow {
2480 params,
2481 body,
2482 is_async: _,
2483 } => {
2484 // Collect free variables from the arrow body.
2485 let free_vars = collect_free_vars_arrow(params, body);
2486
2487 // Resolve upvalues against the parent scope.
2488 let mut upvalue_entries = Vec::new();
2489 for name in &free_vars {
2490 if let Some(local) = fc.find_local_info(name) {
2491 let reg = local.reg;
2492 let is_const = local.is_const;
2493 if let Some(l) = fc.locals.iter_mut().rev().find(|l| l.name == *name) {
2494 l.is_captured = true;
2495 }
2496 upvalue_entries.push(UpvalueEntry {
2497 name: name.clone(),
2498 def: UpvalueDef {
2499 is_local: true,
2500 index: reg,
2501 },
2502 is_const,
2503 });
2504 } else if let Some(parent_uv_idx) = fc.find_upvalue(name) {
2505 let is_const = fc.is_upvalue_const(parent_uv_idx);
2506 upvalue_entries.push(UpvalueEntry {
2507 name: name.clone(),
2508 def: UpvalueDef {
2509 is_local: false,
2510 index: parent_uv_idx,
2511 },
2512 is_const,
2513 });
2514 }
2515 }
2516
2517 let param_count = params.len().min(255) as u8;
2518 let mut inner = FunctionCompiler::new("<arrow>".into(), param_count);
2519
2520 // Copy upvalue entries.
2521 for entry in &upvalue_entries {
2522 inner.upvalues.push(UpvalueEntry {
2523 name: entry.name.clone(),
2524 def: entry.def.clone(),
2525 is_const: entry.is_const,
2526 });
2527 }
2528
2529 // Pre-scan for inner captures within the arrow body.
2530 match body {
2531 ArrowBody::Expr(_) => {}
2532 ArrowBody::Block(stmts) => {
2533 inner.captured_names = collect_inner_captures(stmts);
2534 }
2535 }
2536
2537 for p in params {
2538 if let PatternKind::Identifier(pname) = &p.kind {
2539 let is_captured = inner.captured_names.contains(pname.as_str());
2540 inner.define_local_ext(pname, is_captured, false);
2541 } else {
2542 let _ = inner.alloc_reg();
2543 }
2544 }
2545
2546 // Box captured parameters.
2547 for p in params {
2548 if let PatternKind::Identifier(pname) = &p.kind {
2549 if let Some(local) = inner.find_local_info(pname) {
2550 if local.is_captured {
2551 let reg = local.reg;
2552 let tmp = inner.alloc_reg();
2553 inner.builder.emit_reg_reg(Op::Move, tmp, reg);
2554 inner.builder.emit_reg(Op::NewCell, reg);
2555 inner.builder.emit_reg_reg(Op::CellStore, reg, tmp);
2556 inner.free_reg(tmp);
2557 }
2558 }
2559 }
2560 }
2561
2562 let result = inner.alloc_reg();
2563 match body {
2564 ArrowBody::Expr(e) => {
2565 compile_expr(&mut inner, e, result)?;
2566 }
2567 ArrowBody::Block(stmts) => {
2568 inner.builder.emit_reg(Op::LoadUndefined, result);
2569 compile_stmts(&mut inner, stmts, result)?;
2570 }
2571 }
2572 inner.builder.emit_reg(Op::Return, result);
2573 let mut inner_func = inner.builder.finish();
2574 inner_func.upvalue_defs = upvalue_entries.iter().map(|e| e.def.clone()).collect();
2575 let func_idx = fc.builder.add_function(inner_func);
2576 fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx);
2577 }
2578
2579 ExprKind::Class(class_def) => {
2580 // Class expression: compile like class decl but into dst.
2581 let name = class_def.id.clone().unwrap_or_default();
2582 // Find constructor.
2583 let ctor = class_def.body.iter().find(|m| {
2584 matches!(
2585 &m.kind,
2586 ClassMemberKind::Method {
2587 kind: MethodKind::Constructor,
2588 ..
2589 }
2590 )
2591 });
2592 if let Some(member) = ctor {
2593 if let ClassMemberKind::Method { value, .. } = &member.kind {
2594 let inner = compile_function_body_with_captures(fc, value)?;
2595 let func_idx = fc.builder.add_function(inner);
2596 fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx);
2597 }
2598 } else {
2599 let mut empty = BytecodeBuilder::new(name, 0);
2600 let r = 0u8;
2601 empty.func.register_count = 1;
2602 empty.emit_reg(Op::LoadUndefined, r);
2603 empty.emit_reg(Op::Return, r);
2604 let func_idx = fc.builder.add_function(empty.finish());
2605 fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx);
2606 }
2607
2608 // Compile methods as properties on the constructor.
2609 for member in &class_def.body {
2610 match &member.kind {
2611 ClassMemberKind::Method {
2612 key,
2613 value,
2614 kind,
2615 is_static: _,
2616 computed: _,
2617 } => {
2618 if matches!(kind, MethodKind::Constructor) {
2619 continue;
2620 }
2621 let method_name = match key {
2622 PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(),
2623 _ => continue,
2624 };
2625 let inner = compile_function_body_with_captures(fc, value)?;
2626 let func_idx = fc.builder.add_function(inner);
2627 let method_reg = fc.alloc_reg();
2628 fc.builder
2629 .emit_reg_u16(Op::CreateClosure, method_reg, func_idx);
2630 let name_idx = fc.builder.add_name(&method_name);
2631 fc.builder.emit_set_prop_name(dst, name_idx, method_reg);
2632 fc.free_reg(method_reg);
2633 }
2634 ClassMemberKind::Property { .. } => {}
2635 }
2636 }
2637 }
2638
2639 ExprKind::Sequence(exprs) => {
2640 for e in exprs {
2641 compile_expr(fc, e, dst)?;
2642 }
2643 }
2644
2645 ExprKind::Spread(inner) => {
2646 compile_expr(fc, inner, dst)?;
2647 }
2648
2649 ExprKind::TemplateLiteral {
2650 quasis,
2651 expressions,
2652 } => {
2653 // Compile template literal as string concatenation.
2654 if quasis.len() == 1 && expressions.is_empty() {
2655 let ci = fc.builder.add_constant(Constant::String(quasis[0].clone()));
2656 fc.builder.emit_reg_u16(Op::LoadConst, dst, ci);
2657 } else {
2658 // Start with first quasi.
2659 let ci = fc.builder.add_constant(Constant::String(quasis[0].clone()));
2660 fc.builder.emit_reg_u16(Op::LoadConst, dst, ci);
2661 for (i, expr) in expressions.iter().enumerate() {
2662 let tmp = fc.alloc_reg();
2663 compile_expr(fc, expr, tmp)?;
2664 fc.builder.emit_reg3(Op::Add, dst, dst, tmp);
2665 fc.free_reg(tmp);
2666 if i + 1 < quasis.len() {
2667 let qi = fc
2668 .builder
2669 .add_constant(Constant::String(quasis[i + 1].clone()));
2670 let tmp2 = fc.alloc_reg();
2671 fc.builder.emit_reg_u16(Op::LoadConst, tmp2, qi);
2672 fc.builder.emit_reg3(Op::Add, dst, dst, tmp2);
2673 fc.free_reg(tmp2);
2674 }
2675 }
2676 }
2677 }
2678
2679 ExprKind::TaggedTemplate { tag, quasi } => {
2680 // Simplified: call tag with the template as argument.
2681 let func_reg = fc.alloc_reg();
2682 compile_expr(fc, tag, func_reg)?;
2683 let arg_reg = fc.alloc_reg();
2684 compile_expr(fc, quasi, arg_reg)?;
2685 fc.builder.emit_call(dst, func_reg, arg_reg, 1);
2686 fc.free_reg(arg_reg);
2687 fc.free_reg(func_reg);
2688 }
2689
2690 ExprKind::Yield { argument, delegate } => {
2691 if *delegate {
2692 // yield* expr: iterate the sub-iterator and yield each value.
2693 let iter_r = fc.alloc_reg();
2694 if let Some(arg) = argument {
2695 compile_expr(fc, arg, iter_r)?;
2696 } else {
2697 fc.builder.emit_reg(Op::LoadUndefined, iter_r);
2698 }
2699
2700 // Get iterator from the expression.
2701 let iter_method_r = fc.alloc_reg();
2702 let sym_iter_ni = fc.builder.add_name("@@iterator");
2703 fc.builder
2704 .emit_get_prop_name(iter_method_r, iter_r, sym_iter_ni);
2705 let this_ni = fc.builder.add_name("this");
2706 fc.builder.emit_store_global(this_ni, iter_r);
2707 let iterator_r = fc.alloc_reg();
2708 let args_start = fc.next_reg;
2709 fc.builder
2710 .emit_call(iterator_r, iter_method_r, args_start, 0);
2711
2712 // Get next method.
2713 let next_r = fc.alloc_reg();
2714 let next_ni = fc.builder.add_name("next");
2715 fc.builder.emit_get_prop_name(next_r, iterator_r, next_ni);
2716
2717 let result_r = fc.alloc_reg();
2718 let done_r = fc.alloc_reg();
2719 let val_r = fc.alloc_reg();
2720
2721 let loop_start = fc.builder.offset();
2722
2723 // Call next().
2724 fc.builder.emit_store_global(this_ni, iterator_r);
2725 fc.builder.emit_call(result_r, next_r, args_start, 0);
2726
2727 let done_ni = fc.builder.add_name("done");
2728 let value_ni = fc.builder.add_name("value");
2729 fc.builder.emit_get_prop_name(done_r, result_r, done_ni);
2730
2731 let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r);
2732
2733 fc.builder.emit_get_prop_name(val_r, result_r, value_ni);
2734
2735 // Yield the value.
2736 fc.builder.emit_yield(dst, val_r);
2737
2738 // Jump back.
2739 fc.builder.emit_jump_to(loop_start);
2740
2741 // Exit: the last result's value is the yield* expression value.
2742 fc.builder.patch_jump(exit_patch);
2743 fc.builder.emit_get_prop_name(dst, result_r, value_ni);
2744
2745 fc.free_reg(val_r);
2746 fc.free_reg(done_r);
2747 fc.free_reg(result_r);
2748 fc.free_reg(next_r);
2749 fc.free_reg(iterator_r);
2750 fc.free_reg(iter_method_r);
2751 fc.free_reg(iter_r);
2752 } else {
2753 // yield expr: emit Yield opcode.
2754 let src = fc.alloc_reg();
2755 if let Some(arg) = argument {
2756 compile_expr(fc, arg, src)?;
2757 } else {
2758 fc.builder.emit_reg(Op::LoadUndefined, src);
2759 }
2760 fc.builder.emit_yield(dst, src);
2761 fc.free_reg(src);
2762 }
2763 }
2764
2765 ExprKind::Await(inner) => {
2766 // Await is a VM-level operation; compile the argument.
2767 compile_expr(fc, inner, dst)?;
2768 }
2769
2770 ExprKind::RegExp { pattern, flags } => {
2771 // Compile as: RegExp(pattern, flags) — a call to the global constructor.
2772 let func_reg = fc.alloc_reg();
2773 let name_idx = fc.builder.add_name("RegExp");
2774 fc.builder.emit_reg_u16(Op::LoadGlobal, func_reg, name_idx);
2775
2776 let args_start = fc.next_reg;
2777 let pat_reg = fc.alloc_reg();
2778 let pat_idx = fc.builder.add_constant(Constant::String(pattern.clone()));
2779 fc.builder.emit_reg_u16(Op::LoadConst, pat_reg, pat_idx);
2780
2781 let flags_reg = fc.alloc_reg();
2782 let flags_idx = fc.builder.add_constant(Constant::String(flags.clone()));
2783 fc.builder.emit_reg_u16(Op::LoadConst, flags_reg, flags_idx);
2784
2785 fc.builder.emit_call(dst, func_reg, args_start, 2);
2786
2787 fc.next_reg -= 1; // flags_reg
2788 fc.next_reg -= 1; // pat_reg
2789 fc.free_reg(func_reg);
2790 }
2791
2792 ExprKind::OptionalChain { base } => {
2793 compile_expr(fc, base, dst)?;
2794 }
2795 }
2796 Ok(())
2797}
2798
2799/// Compile a store operation (assignment target).
2800fn compile_store(fc: &mut FunctionCompiler, target: &Expr, src: Reg) -> Result<(), JsError> {
2801 match &target.kind {
2802 ExprKind::Identifier(name) => {
2803 if let Some(local) = fc.find_local_info(name) {
2804 if local.is_const {
2805 return Err(JsError::SyntaxError(format!(
2806 "Assignment to constant variable '{name}'"
2807 )));
2808 }
2809 let reg = local.reg;
2810 let captured = local.is_captured;
2811 if captured {
2812 fc.builder.emit_reg_reg(Op::CellStore, reg, src);
2813 } else if reg != src {
2814 fc.builder.emit_reg_reg(Op::Move, reg, src);
2815 }
2816 } else if let Some(uv_idx) = fc.find_upvalue(name) {
2817 if fc.is_upvalue_const(uv_idx) {
2818 return Err(JsError::SyntaxError(format!(
2819 "Assignment to constant variable '{name}'"
2820 )));
2821 }
2822 fc.builder.emit_store_upvalue(uv_idx, src);
2823 } else {
2824 let ni = fc.builder.add_name(name);
2825 fc.builder.emit_store_global(ni, src);
2826 }
2827 }
2828 ExprKind::Member {
2829 object,
2830 property,
2831 computed,
2832 } => {
2833 let obj_reg = fc.alloc_reg();
2834 compile_expr(fc, object, obj_reg)?;
2835 if !computed {
2836 if let ExprKind::Identifier(name) = &property.kind {
2837 let ni = fc.builder.add_name(name);
2838 fc.builder.emit_set_prop_name(obj_reg, ni, src);
2839 } else {
2840 let key_reg = fc.alloc_reg();
2841 compile_expr(fc, property, key_reg)?;
2842 fc.builder.emit_reg3(Op::SetProperty, obj_reg, key_reg, src);
2843 fc.free_reg(key_reg);
2844 }
2845 } else {
2846 let key_reg = fc.alloc_reg();
2847 compile_expr(fc, property, key_reg)?;
2848 fc.builder.emit_reg3(Op::SetProperty, obj_reg, key_reg, src);
2849 fc.free_reg(key_reg);
2850 }
2851 fc.free_reg(obj_reg);
2852 }
2853 _ => {
2854 // Other assignment targets (destructuring) not handled here.
2855 }
2856 }
2857 Ok(())
2858}
2859
2860fn binary_op_to_opcode(op: BinaryOp) -> Op {
2861 match op {
2862 BinaryOp::Add => Op::Add,
2863 BinaryOp::Sub => Op::Sub,
2864 BinaryOp::Mul => Op::Mul,
2865 BinaryOp::Div => Op::Div,
2866 BinaryOp::Rem => Op::Rem,
2867 BinaryOp::Exp => Op::Exp,
2868 BinaryOp::Eq => Op::Eq,
2869 BinaryOp::Ne => Op::NotEq,
2870 BinaryOp::StrictEq => Op::StrictEq,
2871 BinaryOp::StrictNe => Op::StrictNotEq,
2872 BinaryOp::Lt => Op::LessThan,
2873 BinaryOp::Le => Op::LessEq,
2874 BinaryOp::Gt => Op::GreaterThan,
2875 BinaryOp::Ge => Op::GreaterEq,
2876 BinaryOp::Shl => Op::ShiftLeft,
2877 BinaryOp::Shr => Op::ShiftRight,
2878 BinaryOp::Ushr => Op::UShiftRight,
2879 BinaryOp::BitAnd => Op::BitAnd,
2880 BinaryOp::BitOr => Op::BitOr,
2881 BinaryOp::BitXor => Op::BitXor,
2882 BinaryOp::In => Op::In,
2883 BinaryOp::Instanceof => Op::InstanceOf,
2884 }
2885}
2886
2887fn compound_assign_op(op: AssignOp) -> Op {
2888 match op {
2889 AssignOp::AddAssign => Op::Add,
2890 AssignOp::SubAssign => Op::Sub,
2891 AssignOp::MulAssign => Op::Mul,
2892 AssignOp::DivAssign => Op::Div,
2893 AssignOp::RemAssign => Op::Rem,
2894 AssignOp::ExpAssign => Op::Exp,
2895 AssignOp::ShlAssign => Op::ShiftLeft,
2896 AssignOp::ShrAssign => Op::ShiftRight,
2897 AssignOp::UshrAssign => Op::UShiftRight,
2898 AssignOp::BitAndAssign => Op::BitAnd,
2899 AssignOp::BitOrAssign => Op::BitOr,
2900 AssignOp::BitXorAssign => Op::BitXor,
2901 AssignOp::AndAssign => Op::BitAnd, // logical AND assignment uses short-circuit; simplified here
2902 AssignOp::OrAssign => Op::BitOr, // likewise
2903 AssignOp::NullishAssign => Op::Move, // simplified
2904 AssignOp::Assign => unreachable!(),
2905 }
2906}
2907
2908#[cfg(test)]
2909mod tests {
2910 use super::*;
2911 use crate::parser::Parser;
2912
2913 /// Helper: parse and compile source, return the top-level function.
2914 fn compile_src(src: &str) -> Function {
2915 let program = Parser::parse(src).expect("parse failed");
2916 compile(&program).expect("compile failed")
2917 }
2918
2919 #[test]
2920 fn test_compile_number_literal() {
2921 let f = compile_src("42;");
2922 let dis = f.disassemble();
2923 assert!(dis.contains("LoadInt8 r0, 42"), "got:\n{dis}");
2924 assert!(dis.contains("Return r0"));
2925 }
2926
2927 #[test]
2928 fn test_compile_large_number() {
2929 let f = compile_src("3.14;");
2930 let dis = f.disassemble();
2931 assert!(dis.contains("LoadConst r0, #0"), "got:\n{dis}");
2932 assert!(
2933 f.constants.contains(&Constant::Number(3.14)),
2934 "constants: {:?}",
2935 f.constants
2936 );
2937 }
2938
2939 #[test]
2940 fn test_compile_string() {
2941 let f = compile_src("\"hello\";");
2942 let dis = f.disassemble();
2943 assert!(dis.contains("LoadConst r0, #0"));
2944 assert!(f.constants.contains(&Constant::String("hello".into())));
2945 }
2946
2947 #[test]
2948 fn test_compile_bool_null() {
2949 let f = compile_src("true; false; null;");
2950 let dis = f.disassemble();
2951 assert!(dis.contains("LoadTrue r0"));
2952 assert!(dis.contains("LoadFalse r0"));
2953 assert!(dis.contains("LoadNull r0"));
2954 }
2955
2956 #[test]
2957 fn test_compile_binary_arithmetic() {
2958 let f = compile_src("1 + 2;");
2959 let dis = f.disassemble();
2960 assert!(dis.contains("Add r0, r1, r2"), "got:\n{dis}");
2961 }
2962
2963 #[test]
2964 fn test_compile_nested_arithmetic() {
2965 let f = compile_src("(1 + 2) * 3;");
2966 let dis = f.disassemble();
2967 assert!(dis.contains("Add"), "got:\n{dis}");
2968 assert!(dis.contains("Mul"), "got:\n{dis}");
2969 }
2970
2971 #[test]
2972 fn test_compile_var_decl() {
2973 let f = compile_src("var x = 10; x;");
2974 let dis = f.disassemble();
2975 // x should get a register, then be loaded from that register.
2976 assert!(dis.contains("LoadInt8"), "got:\n{dis}");
2977 assert!(
2978 dis.contains("Move") || dis.contains("LoadInt8"),
2979 "got:\n{dis}"
2980 );
2981 }
2982
2983 #[test]
2984 fn test_compile_let_const() {
2985 let f = compile_src("let a = 1; const b = 2; a + b;");
2986 let dis = f.disassemble();
2987 assert!(dis.contains("Add"), "got:\n{dis}");
2988 }
2989
2990 #[test]
2991 fn test_compile_if_else() {
2992 let f = compile_src("if (true) { 1; } else { 2; }");
2993 let dis = f.disassemble();
2994 assert!(dis.contains("JumpIfFalse"), "got:\n{dis}");
2995 assert!(dis.contains("Jump"), "got:\n{dis}");
2996 }
2997
2998 #[test]
2999 fn test_compile_while() {
3000 let f = compile_src("var i = 0; while (i < 10) { i = i + 1; }");
3001 let dis = f.disassemble();
3002 assert!(dis.contains("LessThan"), "got:\n{dis}");
3003 assert!(dis.contains("JumpIfFalse"), "got:\n{dis}");
3004 assert!(
3005 dis.contains("Jump"),
3006 "backward jump should be present: {dis}"
3007 );
3008 }
3009
3010 #[test]
3011 fn test_compile_do_while() {
3012 let f = compile_src("var i = 0; do { i = i + 1; } while (i < 5);");
3013 let dis = f.disassemble();
3014 assert!(dis.contains("JumpIfTrue"), "got:\n{dis}");
3015 }
3016
3017 #[test]
3018 fn test_compile_for_loop() {
3019 let f = compile_src("for (var i = 0; i < 10; i = i + 1) { i; }");
3020 let dis = f.disassemble();
3021 assert!(dis.contains("LessThan"), "got:\n{dis}");
3022 assert!(dis.contains("JumpIfFalse"), "got:\n{dis}");
3023 }
3024
3025 #[test]
3026 fn test_compile_function_decl() {
3027 let f = compile_src("function add(a, b) { return a + b; }");
3028 let dis = f.disassemble();
3029 assert!(dis.contains("CreateClosure"), "got:\n{dis}");
3030 assert!(!f.functions.is_empty(), "should have nested function");
3031 let inner = &f.functions[0];
3032 assert_eq!(inner.name, "add");
3033 assert_eq!(inner.param_count, 2);
3034 let inner_dis = inner.disassemble();
3035 assert!(inner_dis.contains("Add"), "inner:\n{inner_dis}");
3036 assert!(inner_dis.contains("Return"), "inner:\n{inner_dis}");
3037 }
3038
3039 #[test]
3040 fn test_compile_function_call() {
3041 let f = compile_src("function f() { return 42; } f();");
3042 let dis = f.disassemble();
3043 assert!(dis.contains("Call"), "got:\n{dis}");
3044 }
3045
3046 #[test]
3047 fn test_compile_arrow_function() {
3048 let f = compile_src("var add = (a, b) => a + b;");
3049 let dis = f.disassemble();
3050 assert!(dis.contains("CreateClosure"), "got:\n{dis}");
3051 let inner = &f.functions[0];
3052 assert_eq!(inner.param_count, 2);
3053 }
3054
3055 #[test]
3056 fn test_compile_assignment() {
3057 let f = compile_src("var x = 1; x = x + 2;");
3058 let dis = f.disassemble();
3059 assert!(dis.contains("Add"), "got:\n{dis}");
3060 assert!(
3061 dis.contains("Move"),
3062 "assignment should produce Move:\n{dis}"
3063 );
3064 }
3065
3066 #[test]
3067 fn test_compile_compound_assignment() {
3068 let f = compile_src("var x = 10; x += 5;");
3069 let dis = f.disassemble();
3070 assert!(dis.contains("Add"), "got:\n{dis}");
3071 }
3072
3073 #[test]
3074 fn test_compile_member_access() {
3075 let f = compile_src("var obj = {}; obj.x;");
3076 let dis = f.disassemble();
3077 assert!(dis.contains("CreateObject"), "got:\n{dis}");
3078 assert!(dis.contains("GetPropertyByName"), "got:\n{dis}");
3079 }
3080
3081 #[test]
3082 fn test_compile_computed_member() {
3083 let f = compile_src("var arr = []; arr[0];");
3084 let dis = f.disassemble();
3085 assert!(dis.contains("GetProperty"), "got:\n{dis}");
3086 }
3087
3088 #[test]
3089 fn test_compile_object_literal() {
3090 let f = compile_src("var obj = { a: 1, b: 2 };");
3091 let dis = f.disassemble();
3092 assert!(dis.contains("CreateObject"), "got:\n{dis}");
3093 assert!(dis.contains("SetPropertyByName"), "got:\n{dis}");
3094 }
3095
3096 #[test]
3097 fn test_compile_array_literal() {
3098 let f = compile_src("[1, 2, 3];");
3099 let dis = f.disassemble();
3100 assert!(dis.contains("CreateArray"), "got:\n{dis}");
3101 assert!(dis.contains("SetProperty"), "got:\n{dis}");
3102 }
3103
3104 #[test]
3105 fn test_compile_conditional() {
3106 let f = compile_src("true ? 1 : 2;");
3107 let dis = f.disassemble();
3108 assert!(dis.contains("JumpIfFalse"), "got:\n{dis}");
3109 }
3110
3111 #[test]
3112 fn test_compile_logical_and() {
3113 let f = compile_src("true && false;");
3114 let dis = f.disassemble();
3115 assert!(dis.contains("JumpIfFalse"), "short-circuit:\n{dis}");
3116 }
3117
3118 #[test]
3119 fn test_compile_logical_or() {
3120 let f = compile_src("false || true;");
3121 let dis = f.disassemble();
3122 assert!(dis.contains("JumpIfTrue"), "short-circuit:\n{dis}");
3123 }
3124
3125 #[test]
3126 fn test_compile_typeof() {
3127 let f = compile_src("typeof 42;");
3128 let dis = f.disassemble();
3129 assert!(dis.contains("TypeOf"), "got:\n{dis}");
3130 }
3131
3132 #[test]
3133 fn test_compile_unary_minus() {
3134 let f = compile_src("-42;");
3135 let dis = f.disassemble();
3136 assert!(dis.contains("Neg"), "got:\n{dis}");
3137 }
3138
3139 #[test]
3140 fn test_compile_not() {
3141 let f = compile_src("!true;");
3142 let dis = f.disassemble();
3143 assert!(dis.contains("LogicalNot"), "got:\n{dis}");
3144 }
3145
3146 #[test]
3147 fn test_compile_return() {
3148 let f = compile_src("function f() { return 42; }");
3149 let inner = &f.functions[0];
3150 let dis = inner.disassemble();
3151 assert!(dis.contains("Return"), "got:\n{dis}");
3152 }
3153
3154 #[test]
3155 fn test_compile_empty_return() {
3156 let f = compile_src("function f() { return; }");
3157 let inner = &f.functions[0];
3158 let dis = inner.disassemble();
3159 assert!(dis.contains("LoadUndefined"), "got:\n{dis}");
3160 assert!(dis.contains("Return"), "got:\n{dis}");
3161 }
3162
3163 #[test]
3164 fn test_compile_throw() {
3165 let f = compile_src("function f() { throw 42; }");
3166 let inner = &f.functions[0];
3167 let dis = inner.disassemble();
3168 assert!(dis.contains("Throw"), "got:\n{dis}");
3169 }
3170
3171 #[test]
3172 fn test_compile_this() {
3173 let f = compile_src("this;");
3174 let dis = f.disassemble();
3175 assert!(dis.contains("LoadGlobal"), "got:\n{dis}");
3176 assert!(f.names.contains(&"this".to_string()));
3177 }
3178
3179 #[test]
3180 fn test_compile_global_var() {
3181 let f = compile_src("console;");
3182 let dis = f.disassemble();
3183 assert!(dis.contains("LoadGlobal"), "got:\n{dis}");
3184 assert!(f.names.contains(&"console".to_string()));
3185 }
3186
3187 #[test]
3188 fn test_compile_template_literal() {
3189 let f = compile_src("`hello`;");
3190 assert!(
3191 f.constants.contains(&Constant::String("hello".into())),
3192 "constants: {:?}",
3193 f.constants
3194 );
3195 }
3196
3197 #[test]
3198 fn test_compile_switch() {
3199 let f = compile_src("switch (1) { case 1: 42; break; case 2: 99; break; }");
3200 let dis = f.disassemble();
3201 assert!(dis.contains("StrictEq"), "got:\n{dis}");
3202 }
3203
3204 #[test]
3205 fn test_compile_class() {
3206 let f = compile_src("class Foo { constructor() {} greet() { return 1; } }");
3207 let dis = f.disassemble();
3208 assert!(dis.contains("CreateClosure"), "got:\n{dis}");
3209 }
3210
3211 #[test]
3212 fn test_compile_update_prefix() {
3213 let f = compile_src("var x = 0; ++x;");
3214 let dis = f.disassemble();
3215 assert!(dis.contains("Add"), "got:\n{dis}");
3216 }
3217
3218 #[test]
3219 fn test_compile_comparison() {
3220 let f = compile_src("1 === 2;");
3221 let dis = f.disassemble();
3222 assert!(dis.contains("StrictEq"), "got:\n{dis}");
3223 }
3224
3225 #[test]
3226 fn test_compile_bitwise() {
3227 let f = compile_src("1 & 2;");
3228 let dis = f.disassemble();
3229 assert!(dis.contains("BitAnd"), "got:\n{dis}");
3230 }
3231
3232 #[test]
3233 fn test_compile_void() {
3234 let f = compile_src("void 0;");
3235 let dis = f.disassemble();
3236 assert!(dis.contains("Void"), "got:\n{dis}");
3237 }
3238
3239 #[test]
3240 fn test_disassembler_output_format() {
3241 let f = compile_src("var x = 42; x + 1;");
3242 let dis = f.disassemble();
3243 // Should contain function header.
3244 assert!(dis.contains("function <main>"));
3245 // Should contain code section.
3246 assert!(dis.contains("code:"));
3247 // Should have hex offsets.
3248 assert!(dis.contains("0000"));
3249 }
3250
3251 #[test]
3252 fn test_register_allocation_is_minimal() {
3253 // `var a = 1; var b = 2; a + b;` should use few registers.
3254 let f = compile_src("var a = 1; var b = 2; a + b;");
3255 // r0 = result, r1 = a, r2 = b, r3/r4 = temps for addition
3256 assert!(
3257 f.register_count <= 6,
3258 "too many registers: {}",
3259 f.register_count
3260 );
3261 }
3262
3263 #[test]
3264 fn test_nested_function_closure() {
3265 let f = compile_src("function outer() { function inner() { return 1; } return inner; }");
3266 assert_eq!(f.functions.len(), 1);
3267 let outer = &f.functions[0];
3268 assert_eq!(outer.name, "outer");
3269 assert_eq!(outer.functions.len(), 1);
3270 let inner = &outer.functions[0];
3271 assert_eq!(inner.name, "inner");
3272 }
3273
3274 #[test]
3275 fn test_for_with_no_parts() {
3276 // `for (;;) { break; }` — infinite loop with immediate break.
3277 let f = compile_src("for (;;) { break; }");
3278 let dis = f.disassemble();
3279 assert!(dis.contains("Jump"), "got:\n{dis}");
3280 }
3281
3282 #[test]
3283 fn test_for_continue_targets_update() {
3284 // `continue` in a for-loop must jump to the update expression, not back
3285 // to the condition check. Verify the continue jump goes to the Add (i + 1)
3286 // rather than to the LessThan condition.
3287 let f = compile_src("for (var i = 0; i < 10; i = i + 1) { continue; }");
3288 let dis = f.disassemble();
3289 // The for-loop should contain: LessThan (test), JumpIfFalse (exit),
3290 // Jump (continue), Add (update), Jump (back to test).
3291 assert!(dis.contains("LessThan"), "missing test: {dis}");
3292 assert!(dis.contains("Add"), "missing update: {dis}");
3293 // There should be at least 2 Jump instructions (continue + back-edge).
3294 let jump_count = dis.matches("Jump ").count();
3295 assert!(
3296 jump_count >= 2,
3297 "expected >= 2 jumps for continue + back-edge, got {jump_count}: {dis}"
3298 );
3299 }
3300
3301 #[test]
3302 fn test_do_while_continue_targets_condition() {
3303 // `continue` in do-while must jump to the condition, not the body start.
3304 let f = compile_src("var i = 0; do { i = i + 1; continue; } while (i < 5);");
3305 let dis = f.disassemble();
3306 assert!(dis.contains("LessThan"), "missing condition: {dis}");
3307 assert!(dis.contains("JumpIfTrue"), "missing back-edge: {dis}");
3308 }
3309
3310 #[test]
3311 fn test_switch_default_case() {
3312 // Default case must not corrupt bytecode.
3313 let f = compile_src("switch (1) { case 1: 10; break; default: 20; break; }");
3314 let dis = f.disassemble();
3315 assert!(dis.contains("StrictEq"), "missing case test: {dis}");
3316 // The first instruction should NOT be corrupted.
3317 assert!(
3318 dis.contains("LoadUndefined r0"),
3319 "first instruction corrupted: {dis}"
3320 );
3321 }
3322
3323 #[test]
3324 fn test_switch_only_default() {
3325 // Switch with only a default case.
3326 let f = compile_src("switch (42) { default: 99; }");
3327 let dis = f.disassemble();
3328 // Should compile without panicking and contain the default body.
3329 assert!(dis.contains("LoadInt8"), "got:\n{dis}");
3330 }
3331
3332 #[test]
3333 fn test_class_empty_constructor_has_return() {
3334 // A class without an explicit constructor should produce a function with Return.
3335 let f = compile_src("class Foo {}");
3336 assert!(!f.functions.is_empty(), "should have constructor function");
3337 let ctor = &f.functions[0];
3338 let dis = ctor.disassemble();
3339 assert!(
3340 dis.contains("Return"),
3341 "empty constructor must have Return: {dis}"
3342 );
3343 }
3344
3345 #[test]
3346 fn test_class_expression_compiles_methods() {
3347 // Class expression should compile methods, not just the constructor.
3348 let f = compile_src("var C = class { greet() { return 1; } };");
3349 let dis = f.disassemble();
3350 assert!(
3351 dis.contains("SetPropertyByName"),
3352 "method should be set as property: {dis}"
3353 );
3354 }
3355}