@tishlang/tish 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,8 @@ use std::sync::Arc;
5
5
 
6
6
  use tishlang_ast::{
7
7
  ArrayElement, ArrowBody, BinOp, CallArg, DestructElement, DestructPattern, Expr,
8
- FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, MemberProp, ObjectProp, Program, Span,
9
- Statement,
8
+ ExportDeclaration, FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp,
9
+ MemberProp, ObjectProp, Program, Span, Statement,
10
10
  };
11
11
 
12
12
  use crate::chunk::{Chunk, Constant};
@@ -47,7 +47,13 @@ impl std::error::Error for CompileError {}
47
47
  /// Loop boundary for break/continue.
48
48
  struct LoopInfo {
49
49
  break_patches: Vec<usize>,
50
+ /// Operand positions for `continue`: either `JumpBack` (while / do-while / for-of) or `Jump`
51
+ /// (C-style `for`, where the update clause is emitted after the body).
50
52
  continue_patches: Vec<usize>,
53
+ /// When true, [`Opcode::Jump`] placeholders in `continue_patches` are patched forward with
54
+ /// [`Self::patch_jump`]. When false, they are [`Opcode::JumpBack`] patched with
55
+ /// [`Self::patch_jump_back`].
56
+ continue_is_forward_jump: bool,
51
57
  }
52
58
 
53
59
  /// Switch boundary: break exits the switch.
@@ -55,6 +61,15 @@ struct SwitchInfo {
55
61
  break_patches: Vec<usize>,
56
62
  }
57
63
 
64
+ /// Innermost break/continue target for unwinding `EnterBlock` before a jump.
65
+ #[derive(Clone, Copy)]
66
+ enum Breakable {
67
+ /// `usize` = `block_depth` before the loop body (same as Continue unwind target).
68
+ Loop { unwind_depth: usize },
69
+ /// `usize` = `block_depth` before the switch statement.
70
+ Switch { unwind_depth: usize },
71
+ }
72
+
58
73
  struct Compiler<'a> {
59
74
  chunk: &'a mut Chunk,
60
75
  /// Current scope: variable name -> (depth, is_captured). Depth 0 = local.
@@ -62,6 +77,10 @@ struct Compiler<'a> {
62
77
  /// Stack of loop info for break/continue.
63
78
  loop_stack: Vec<LoopInfo>,
64
79
  switch_stack: Vec<SwitchInfo>,
80
+ /// Parallel to nested loops/switches: innermost target for break/continue block unwind.
81
+ breakable_stack: Vec<Breakable>,
82
+ /// Nesting depth of emitted `EnterBlock` (lexical blocks) not yet closed on the compile path.
83
+ block_depth: usize,
65
84
  /// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
66
85
  retain_last_expr: bool,
67
86
  }
@@ -73,10 +92,101 @@ impl<'a> Compiler<'a> {
73
92
  scope: vec![HashMap::new()],
74
93
  loop_stack: Vec::new(),
75
94
  switch_stack: Vec::new(),
95
+ breakable_stack: Vec::new(),
96
+ block_depth: 0,
76
97
  retain_last_expr,
77
98
  }
78
99
  }
79
100
 
101
+ fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
102
+ let n = self.block_depth.saturating_sub(target_depth);
103
+ for _ in 0..n {
104
+ self.emit(Opcode::ExitBlock);
105
+ }
106
+ }
107
+
108
+ /// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
109
+ /// Formal parameters as VM slot names plus optional destructure patterns (one per formal).
110
+ fn plan_function_params(
111
+ params: &[FunParam],
112
+ ) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
113
+ let mut names = Vec::with_capacity(params.len());
114
+ let mut slots: Vec<Option<DestructPattern>> = Vec::with_capacity(params.len());
115
+ let mut syn_counter = 0u32;
116
+ for p in params {
117
+ match p {
118
+ FunParam::Simple(tp) => {
119
+ names.push(Arc::clone(&tp.name));
120
+ slots.push(None);
121
+ }
122
+ FunParam::Destructure {
123
+ pattern,
124
+ default,
125
+ ..
126
+ } => {
127
+ if default.is_some() {
128
+ return Err(CompileError {
129
+ message: "Default values on destructuring parameters are not supported in bytecode"
130
+ .to_string(),
131
+ });
132
+ }
133
+ names.push(Arc::from(format!("__param_{}", syn_counter)));
134
+ syn_counter += 1;
135
+ slots.push(Some(pattern.clone()));
136
+ }
137
+ }
138
+ }
139
+ Ok((names, slots))
140
+ }
141
+
142
+ /// After VM binds positional args to `param_names`, load each destructure slot and bind pattern locals.
143
+ fn emit_param_destructure_prologue(
144
+ &mut self,
145
+ param_names: &[Arc<str>],
146
+ slots: &[Option<DestructPattern>],
147
+ ) -> Result<(), CompileError> {
148
+ debug_assert_eq!(param_names.len(), slots.len());
149
+ for (name, slot) in param_names.iter().zip(slots.iter()) {
150
+ if let Some(pattern) = slot {
151
+ let idx = self.name_idx(name);
152
+ self.emit_u16(Opcode::LoadVar, idx);
153
+ self.compile_destructure(pattern, false, false)?;
154
+ }
155
+ }
156
+ Ok(())
157
+ }
158
+
159
+ fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
160
+ match stmt {
161
+ Statement::VarDecl {
162
+ name,
163
+ init,
164
+ mutable: _,
165
+ ..
166
+ } => {
167
+ if let Some(expr) = init {
168
+ self.compile_expr(expr)?;
169
+ } else {
170
+ let idx = self.constant_idx(Constant::Null);
171
+ self.emit(Opcode::LoadConst);
172
+ self.chunk.write_u16(idx);
173
+ }
174
+ let idx = self.name_idx(name);
175
+ self.emit_u16(Opcode::DeclareVarPlain, idx);
176
+ self.scope
177
+ .last_mut()
178
+ .unwrap()
179
+ .insert(Arc::clone(name), false);
180
+ }
181
+ Statement::VarDeclDestructure { pattern, init, .. } => {
182
+ self.compile_expr(init)?;
183
+ self.compile_destructure(pattern, false, true)?;
184
+ }
185
+ _ => self.compile_statement(stmt)?,
186
+ }
187
+ Ok(())
188
+ }
189
+
80
190
  fn name_idx(&mut self, name: &Arc<str>) -> u16 {
81
191
  self.chunk.add_name(Arc::clone(name))
82
192
  }
@@ -122,9 +232,10 @@ impl<'a> Compiler<'a> {
122
232
  self.chunk.code[patch_pos + 1] = bytes[1];
123
233
  }
124
234
 
125
- /// Patch a JumpBack operand: distance from (patch_pos+3) back to target.
235
+ /// Patch a JumpBack operand: distance from the IP after this insn back to `target`.
236
+ /// `patch_pos` is the first byte of the u16 operand (same as [`Self::emit_jump_back`]'s return value).
126
237
  fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
127
- let after_insn = patch_pos + 3;
238
+ let after_insn = patch_pos + 2;
128
239
  let dist = after_insn.saturating_sub(target);
129
240
  let bytes = (dist as u16).to_be_bytes();
130
241
  self.chunk.code[patch_pos] = bytes[0];
@@ -362,11 +473,15 @@ impl<'a> Compiler<'a> {
362
473
  fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
363
474
  match stmt {
364
475
  Statement::Block { statements, .. } => {
476
+ self.emit(Opcode::EnterBlock);
477
+ self.block_depth += 1;
365
478
  self.scope.push(HashMap::new());
366
479
  for s in statements {
367
480
  self.compile_statement(s)?;
368
481
  }
369
482
  self.scope.pop();
483
+ self.emit(Opcode::ExitBlock);
484
+ self.block_depth -= 1;
370
485
  }
371
486
  Statement::VarDecl {
372
487
  name,
@@ -382,7 +497,7 @@ impl<'a> Compiler<'a> {
382
497
  self.chunk.write_u16(idx);
383
498
  }
384
499
  let idx = self.name_idx(name);
385
- self.emit_u16(Opcode::StoreVar, idx);
500
+ self.emit_u16(Opcode::DeclareVar, idx);
386
501
  self.scope
387
502
  .last_mut()
388
503
  .unwrap()
@@ -390,7 +505,7 @@ impl<'a> Compiler<'a> {
390
505
  }
391
506
  Statement::VarDeclDestructure { pattern, init, .. } => {
392
507
  self.compile_expr(init)?;
393
- self.compile_destructure(pattern, false)?;
508
+ self.compile_destructure(pattern, false, false)?;
394
509
  }
395
510
  Statement::ExprStmt { expr, .. } => {
396
511
  self.compile_expr(expr)?;
@@ -417,7 +532,12 @@ impl<'a> Compiler<'a> {
417
532
  self.loop_stack.push(LoopInfo {
418
533
  break_patches: Vec::new(),
419
534
  continue_patches: Vec::new(),
535
+ continue_is_forward_jump: false,
420
536
  });
537
+ self.breakable_stack
538
+ .push(Breakable::Loop {
539
+ unwind_depth: self.block_depth,
540
+ });
421
541
  self.compile_expr(cond)?;
422
542
  let jump_out = self.emit_jump(Opcode::JumpIfFalse);
423
543
  // JumpIfFalse already pops condition when taking body
@@ -427,6 +547,7 @@ impl<'a> Compiler<'a> {
427
547
  let end = self.chunk.code.len();
428
548
  self.patch_jump(jump_out, end);
429
549
  let info = self.loop_stack.pop().unwrap();
550
+ self.breakable_stack.pop();
430
551
  for p in info.continue_patches {
431
552
  self.patch_jump_back(p, start);
432
553
  }
@@ -443,7 +564,7 @@ impl<'a> Compiler<'a> {
443
564
  } => {
444
565
  self.scope.push(HashMap::new());
445
566
  if let Some(i) = init {
446
- self.compile_statement(i)?;
567
+ self.compile_for_init_statement(i.as_ref())?;
447
568
  }
448
569
  let cond_start = self.chunk.code.len();
449
570
  if let Some(c) = cond {
@@ -457,7 +578,12 @@ impl<'a> Compiler<'a> {
457
578
  self.loop_stack.push(LoopInfo {
458
579
  break_patches: Vec::new(),
459
580
  continue_patches: Vec::new(),
581
+ continue_is_forward_jump: true,
460
582
  });
583
+ self.breakable_stack
584
+ .push(Breakable::Loop {
585
+ unwind_depth: self.block_depth,
586
+ });
461
587
  self.compile_statement(body)?;
462
588
  let update_start = self.chunk.code.len();
463
589
  if let Some(u) = update {
@@ -466,7 +592,7 @@ impl<'a> Compiler<'a> {
466
592
  }
467
593
  let info = self.loop_stack.pop().unwrap();
468
594
  for p in info.continue_patches {
469
- self.patch_jump_back(p, update_start);
595
+ self.patch_jump(p, update_start);
470
596
  }
471
597
  let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
472
598
  self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
@@ -475,6 +601,7 @@ impl<'a> Compiler<'a> {
475
601
  for p in info.break_patches {
476
602
  self.patch_jump(p, end);
477
603
  }
604
+ self.breakable_stack.pop();
478
605
  self.scope.pop();
479
606
  }
480
607
  Statement::ForOf { name, iterable, body, .. } => {
@@ -487,27 +614,32 @@ impl<'a> Compiler<'a> {
487
614
  let i_idx = self.name_idx(&i_name);
488
615
  let len_idx = self.name_idx(&len_name);
489
616
  let name_idx = self.name_idx(name);
490
- self.emit_u16(Opcode::StoreVar, arr_idx);
617
+ self.emit_u16(Opcode::DeclareVar, arr_idx);
491
618
  self.scope.last_mut().unwrap().insert(arr_name.clone(), false);
492
619
  self.emit_u16(Opcode::LoadVar, arr_idx);
493
620
  let len_name_idx = self.name_idx(&Arc::from("length"));
494
621
  self.emit_u16(Opcode::GetMember, len_name_idx);
495
- self.emit_u16(Opcode::StoreVar, len_idx);
622
+ self.emit_u16(Opcode::DeclareVar, len_idx);
496
623
  self.scope.last_mut().unwrap().insert(len_name.clone(), false);
497
624
  let zero_idx = self.constant_idx(Constant::Number(0.0));
498
625
  self.emit(Opcode::LoadConst);
499
626
  self.chunk.write_u16(zero_idx);
500
- self.emit_u16(Opcode::StoreVar, i_idx);
627
+ self.emit_u16(Opcode::DeclareVar, i_idx);
501
628
  self.scope.last_mut().unwrap().insert(i_name.clone(), false);
502
629
  let loop_start = self.chunk.code.len();
503
630
  self.loop_stack.push(LoopInfo {
504
631
  break_patches: Vec::new(),
505
632
  continue_patches: Vec::new(),
633
+ continue_is_forward_jump: false,
506
634
  });
635
+ self.breakable_stack
636
+ .push(Breakable::Loop {
637
+ unwind_depth: self.block_depth,
638
+ });
507
639
  self.emit_u16(Opcode::LoadVar, arr_idx);
508
640
  self.emit_u16(Opcode::LoadVar, i_idx);
509
641
  self.emit(Opcode::GetIndex);
510
- self.emit_u16(Opcode::StoreVar, name_idx);
642
+ self.emit_u16(Opcode::DeclareVar, name_idx);
511
643
  self.scope.last_mut().unwrap().insert(Arc::clone(name), false);
512
644
  self.compile_statement(body)?;
513
645
  self.emit_u16(Opcode::LoadVar, i_idx);
@@ -525,6 +657,7 @@ impl<'a> Compiler<'a> {
525
657
  let end = self.chunk.code.len();
526
658
  self.patch_jump(jump_out, end);
527
659
  let info = self.loop_stack.pop().unwrap();
660
+ self.breakable_stack.pop();
528
661
  for p in info.continue_patches {
529
662
  self.patch_jump_back(p, loop_start);
530
663
  }
@@ -544,22 +677,56 @@ impl<'a> Compiler<'a> {
544
677
  self.emit(Opcode::Return);
545
678
  }
546
679
  Statement::Break { .. } => {
680
+ let unwind_depth = match self.breakable_stack.last() {
681
+ Some(Breakable::Loop { unwind_depth }) | Some(Breakable::Switch { unwind_depth }) => {
682
+ *unwind_depth
683
+ }
684
+ None => {
685
+ return Err(CompileError {
686
+ message: "break not inside a loop or switch".to_string(),
687
+ });
688
+ }
689
+ };
690
+ self.emit_exit_blocks_until_depth(unwind_depth);
547
691
  let pos = self.emit_jump(Opcode::Jump);
548
- if let Some(sw) = self.switch_stack.last_mut() {
549
- sw.break_patches.push(pos);
550
- } else if let Some(lo) = self.loop_stack.last_mut() {
551
- lo.break_patches.push(pos);
552
- } else {
553
- return Err(CompileError {
554
- message: "break not inside a loop or switch".to_string(),
555
- });
692
+ match self.breakable_stack.last() {
693
+ Some(Breakable::Loop { .. }) => {
694
+ self.loop_stack.last_mut().unwrap().break_patches.push(pos);
695
+ }
696
+ Some(Breakable::Switch { .. }) => {
697
+ self.switch_stack.last_mut().unwrap().break_patches.push(pos);
698
+ }
699
+ None => {}
556
700
  }
557
701
  }
558
702
  Statement::Continue { .. } => {
559
- let pos = self.emit_jump_back();
560
- self.loop_stack.last_mut().ok_or_else(|| CompileError {
561
- message: "continue not inside a loop".to_string(),
562
- })?.continue_patches.push(pos);
703
+ let unwind_depth = self
704
+ .breakable_stack
705
+ .iter()
706
+ .rev()
707
+ .find_map(|b| match b {
708
+ Breakable::Loop { unwind_depth } => Some(*unwind_depth),
709
+ Breakable::Switch { .. } => None,
710
+ })
711
+ .ok_or_else(|| CompileError {
712
+ message: "continue not inside a loop".to_string(),
713
+ })?;
714
+ self.emit_exit_blocks_until_depth(unwind_depth);
715
+ let forward = self
716
+ .loop_stack
717
+ .last()
718
+ .expect("continue not inside a loop")
719
+ .continue_is_forward_jump;
720
+ let pos = if forward {
721
+ self.emit_jump(Opcode::Jump)
722
+ } else {
723
+ self.emit_jump_back()
724
+ };
725
+ self.loop_stack
726
+ .last_mut()
727
+ .expect("continue not inside a loop")
728
+ .continue_patches
729
+ .push(pos);
563
730
  }
564
731
  Statement::FunDecl {
565
732
  name,
@@ -569,24 +736,11 @@ impl<'a> Compiler<'a> {
569
736
  async_: _,
570
737
  ..
571
738
  } => {
572
- for p in params {
573
- if matches!(p, FunParam::Destructure { .. }) {
574
- return Err(CompileError {
575
- message: "Destructuring parameters are not supported in bytecode"
576
- .to_string(),
577
- });
578
- }
579
- }
739
+ let formal_len = params.len();
740
+ let (mut param_names, slots) = Self::plan_function_params(params)?;
580
741
  let mut inner = Chunk::new();
581
- let mut param_names: Vec<Arc<str>> = params
582
- .iter()
583
- .map(|p| match p {
584
- FunParam::Simple(tp) => Arc::clone(&tp.name),
585
- _ => unreachable!(),
586
- })
587
- .collect();
588
742
  if let Some(rp) = rest_param {
589
- param_names.push(rp.name.clone());
743
+ param_names.push(Arc::clone(&rp.name));
590
744
  inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
591
745
  }
592
746
  for p in &param_names {
@@ -598,6 +752,7 @@ impl<'a> Compiler<'a> {
598
752
  .iter()
599
753
  .map(|n| (Arc::clone(n), false))
600
754
  .collect::<HashMap<_, _>>()];
755
+ inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
601
756
  inner_comp.compile_statement(body)?;
602
757
  inner_comp.emit(Opcode::LoadConst);
603
758
  let idx = inner_comp.constant_idx(Constant::Null);
@@ -608,7 +763,7 @@ impl<'a> Compiler<'a> {
608
763
  let idx = self.constant_idx(Constant::Closure(nested_idx));
609
764
  self.chunk.write_u16(idx);
610
765
  let idx = self.name_idx(name);
611
- self.emit_u16(Opcode::StoreVar, idx);
766
+ self.emit_u16(Opcode::DeclareVar, idx);
612
767
  self.scope.last_mut().unwrap().insert(Arc::clone(name), false);
613
768
  }
614
769
  Statement::DoWhile { body, cond, .. } => {
@@ -616,7 +771,12 @@ impl<'a> Compiler<'a> {
616
771
  self.loop_stack.push(LoopInfo {
617
772
  break_patches: Vec::new(),
618
773
  continue_patches: Vec::new(),
774
+ continue_is_forward_jump: false,
619
775
  });
776
+ self.breakable_stack
777
+ .push(Breakable::Loop {
778
+ unwind_depth: self.block_depth,
779
+ });
620
780
  self.compile_statement(body)?;
621
781
  let cond_start = self.chunk.code.len();
622
782
  self.compile_expr(cond)?;
@@ -626,6 +786,7 @@ impl<'a> Compiler<'a> {
626
786
  let end = self.chunk.code.len();
627
787
  self.patch_jump(jump_back, end);
628
788
  let info = self.loop_stack.pop().unwrap();
789
+ self.breakable_stack.pop();
629
790
  for p in info.continue_patches {
630
791
  self.patch_jump_back(p, cond_start);
631
792
  }
@@ -634,9 +795,13 @@ impl<'a> Compiler<'a> {
634
795
  }
635
796
  }
636
797
  Statement::Switch { expr, cases, default_body, .. } => {
798
+ let switch_unwind_depth = self.block_depth;
637
799
  self.switch_stack.push(SwitchInfo {
638
800
  break_patches: Vec::new(),
639
801
  });
802
+ self.breakable_stack.push(Breakable::Switch {
803
+ unwind_depth: switch_unwind_depth,
804
+ });
640
805
  self.compile_expr(expr)?;
641
806
  self.emit(Opcode::Dup);
642
807
  let mut end_patches = Vec::new();
@@ -684,6 +849,7 @@ impl<'a> Compiler<'a> {
684
849
  self.patch_jump(p, self.chunk.code.len());
685
850
  }
686
851
  let sw = self.switch_stack.pop().unwrap();
852
+ self.breakable_stack.pop();
687
853
  for p in sw.break_patches {
688
854
  self.patch_jump(p, self.chunk.code.len());
689
855
  }
@@ -708,16 +874,23 @@ impl<'a> Compiler<'a> {
708
874
  let catch_start = self.chunk.code.len();
709
875
  if let Some(catch_stmt) = catch_body {
710
876
  if let Some(param) = catch_param {
877
+ self.emit(Opcode::EnterBlock);
878
+ self.block_depth += 1;
879
+ self.scope.push(HashMap::new());
711
880
  let param_idx = self.name_idx(param);
712
- self.emit_u16(Opcode::StoreVar, param_idx);
881
+ self.emit_u16(Opcode::DeclareVar, param_idx);
713
882
  self.scope
714
883
  .last_mut()
715
884
  .unwrap()
716
885
  .insert(Arc::clone(param), false);
886
+ self.compile_statement(catch_stmt)?;
887
+ self.scope.pop();
888
+ self.emit(Opcode::ExitBlock);
889
+ self.block_depth -= 1;
717
890
  } else {
718
891
  self.emit(Opcode::Pop);
892
+ self.compile_statement(catch_stmt)?;
719
893
  }
720
- self.compile_statement(catch_stmt)?;
721
894
  } else {
722
895
  self.emit(Opcode::Throw);
723
896
  }
@@ -730,11 +903,21 @@ impl<'a> Compiler<'a> {
730
903
  self.chunk.code[catch_offset_pos + 1] = (catch_offset >> 8) as u8;
731
904
  self.chunk.code[catch_offset_pos + 2] = (catch_offset & 0xff) as u8;
732
905
  }
733
- Statement::Import { .. } | Statement::Export { .. } => {
906
+ Statement::Import { .. } => {
734
907
  return Err(CompileError {
735
- message: "Import/Export not supported in bytecode".to_string(),
908
+ message: "Import not supported in bytecode".to_string(),
736
909
  });
737
910
  }
911
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
912
+ ExportDeclaration::Named(inner_stmt) => {
913
+ self.compile_statement(inner_stmt.as_ref())?;
914
+ }
915
+ ExportDeclaration::Default(_) => {
916
+ return Err(CompileError {
917
+ message: "export default is not supported in bytecode".to_string(),
918
+ });
919
+ }
920
+ },
738
921
  }
739
922
  Ok(())
740
923
  }
@@ -743,7 +926,13 @@ impl<'a> Compiler<'a> {
743
926
  &mut self,
744
927
  pattern: &DestructPattern,
745
928
  mutable: bool,
929
+ for_header_binding: bool,
746
930
  ) -> Result<(), CompileError> {
931
+ let decl_op = if for_header_binding {
932
+ Opcode::DeclareVarPlain
933
+ } else {
934
+ Opcode::DeclareVar
935
+ };
747
936
  match pattern {
748
937
  DestructPattern::Array(elements) => {
749
938
  for (i, elem) in elements.iter().enumerate() {
@@ -755,7 +944,7 @@ impl<'a> Compiler<'a> {
755
944
  self.chunk.write_u16(idx);
756
945
  self.emit(Opcode::GetIndex);
757
946
  let idx = self.name_idx(name);
758
- self.emit_u16(Opcode::StoreVar, idx);
947
+ self.emit_u16(decl_op, idx);
759
948
  self.scope
760
949
  .last_mut()
761
950
  .unwrap()
@@ -780,7 +969,7 @@ impl<'a> Compiler<'a> {
780
969
  match &prop.value {
781
970
  DestructElement::Ident(name) => {
782
971
  let idx = self.name_idx(name);
783
- self.emit_u16(Opcode::StoreVar, idx);
972
+ self.emit_u16(decl_op, idx);
784
973
  if mutable {
785
974
  self.scope
786
975
  .last_mut()
@@ -1093,22 +1282,9 @@ impl<'a> Compiler<'a> {
1093
1282
  self.emit_u16(Opcode::Call, 1);
1094
1283
  }
1095
1284
  Expr::ArrowFunction { params, body, .. } => {
1096
- for p in params {
1097
- if matches!(p, FunParam::Destructure { .. }) {
1098
- return Err(CompileError {
1099
- message: "Destructuring parameters are not supported in bytecode"
1100
- .to_string(),
1101
- });
1102
- }
1103
- }
1285
+ let formal_len = params.len();
1286
+ let (param_names, slots) = Self::plan_function_params(params)?;
1104
1287
  let mut inner = Chunk::new();
1105
- let param_names: Vec<Arc<str>> = params
1106
- .iter()
1107
- .map(|p| match p {
1108
- FunParam::Simple(tp) => Arc::clone(&tp.name),
1109
- _ => unreachable!(),
1110
- })
1111
- .collect();
1112
1288
  for p in &param_names {
1113
1289
  inner.add_name(Arc::clone(p));
1114
1290
  }
@@ -1118,6 +1294,7 @@ impl<'a> Compiler<'a> {
1118
1294
  .iter()
1119
1295
  .map(|n| (Arc::clone(n), false))
1120
1296
  .collect::<HashMap<_, _>>()];
1297
+ inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
1121
1298
  match body {
1122
1299
  ArrowBody::Expr(e) => {
1123
1300
  inner_comp.compile_expr(e)?;
@@ -1245,10 +1422,56 @@ impl<'a> Compiler<'a> {
1245
1422
  self.compile_expr(operand)?;
1246
1423
  self.emit_u16(Opcode::Call, 1);
1247
1424
  }
1248
- Expr::LogicalAssign { .. } => {
1249
- return Err(CompileError {
1250
- message: "Logical assignment (&&=, ||=, ??=) not yet supported in bytecode".to_string(),
1251
- });
1425
+ Expr::LogicalAssign { name, op, value, .. } => {
1426
+ let idx = self.name_idx(name);
1427
+ match op {
1428
+ LogicalAssignOp::OrOr => {
1429
+ // ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
1430
+ self.emit_u16(Opcode::LoadVar, idx);
1431
+ self.emit(Opcode::Dup);
1432
+ let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
1433
+ let j_end = self.emit_jump(Opcode::Jump);
1434
+ self.patch_jump(j_rhs, self.chunk.code.len());
1435
+ self.emit(Opcode::Pop);
1436
+ self.compile_expr(value)?;
1437
+ self.emit_u16(Opcode::StoreVar, idx);
1438
+ self.emit_u16(Opcode::LoadVar, idx);
1439
+ let end = self.chunk.code.len();
1440
+ self.patch_jump(j_end, end);
1441
+ }
1442
+ LogicalAssignOp::AndAnd => {
1443
+ // &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
1444
+ self.emit_u16(Opcode::LoadVar, idx);
1445
+ self.emit(Opcode::Dup);
1446
+ let j_short = self.emit_jump(Opcode::JumpIfFalse);
1447
+ self.emit(Opcode::Pop);
1448
+ self.compile_expr(value)?;
1449
+ self.emit_u16(Opcode::StoreVar, idx);
1450
+ self.emit_u16(Opcode::LoadVar, idx);
1451
+ let j_end = self.emit_jump(Opcode::Jump);
1452
+ let end = self.chunk.code.len();
1453
+ self.patch_jump(j_short, end);
1454
+ self.patch_jump(j_end, end);
1455
+ }
1456
+ LogicalAssignOp::Nullish => {
1457
+ // ??= : assign only when current === null (matches interpreter)
1458
+ let null_c = self.constant_idx(Constant::Null);
1459
+ self.emit_u16(Opcode::LoadVar, idx);
1460
+ self.emit(Opcode::Dup);
1461
+ self.emit(Opcode::LoadConst);
1462
+ self.chunk.write_u16(null_c);
1463
+ self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictEq));
1464
+ let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
1465
+ self.emit(Opcode::Pop);
1466
+ self.compile_expr(value)?;
1467
+ self.emit_u16(Opcode::StoreVar, idx);
1468
+ self.emit_u16(Opcode::LoadVar, idx);
1469
+ let j_end = self.emit_jump(Opcode::Jump);
1470
+ let end = self.chunk.code.len();
1471
+ self.patch_jump(j_not_null, end);
1472
+ self.patch_jump(j_end, end);
1473
+ }
1474
+ }
1252
1475
  }
1253
1476
  Expr::New { callee, args, .. } => {
1254
1477
  let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
@@ -84,13 +84,22 @@ pub enum Opcode {
84
84
  Construct = 37,
85
85
  /// `new callee(...spread)` — stack: args array, then callee (same order as CallSpread).
86
86
  ConstructSpread = 38,
87
+ /// Declare `let`/`const` in the current lexical frame (operand: u16 name index). Pops value.
88
+ /// Does not walk enclosing scopes or globals (unlike [`StoreVar`]).
89
+ DeclareVar = 39,
90
+ /// Enter a block scope; pairs with [`ExitBlock`].
91
+ EnterBlock = 40,
92
+ /// Exit innermost block scope and restore shadowed bindings.
93
+ ExitBlock = 41,
94
+ /// Like [`DeclareVar`] but does not record block-scope undo (for `for`/`for-of` header bindings).
95
+ DeclareVarPlain = 42,
87
96
  }
88
97
 
89
98
  impl Opcode {
90
- /// Decode byte to opcode. Safe for b in 0..=38 (matches #[repr(u8)] discriminants).
99
+ /// Decode byte to opcode. Safe for b in 0..=42 (matches #[repr(u8)] discriminants).
91
100
  #[inline]
92
101
  pub fn from_u8(b: u8) -> Option<Opcode> {
93
- if b <= 38 {
102
+ if b <= 42 {
94
103
  Some(unsafe { std::mem::transmute(b) })
95
104
  } else {
96
105
  None
@@ -101,7 +110,8 @@ impl Opcode {
101
110
  pub fn instruction_size(self, code: &[u8], ip: usize) -> Option<usize> {
102
111
  let size = match self {
103
112
  Opcode::Nop | Opcode::Pop | Opcode::Dup | Opcode::Return | Opcode::ExitTry
104
- | Opcode::ArrayMapIdentity | Opcode::CallSpread | Opcode::ConstructSpread => 1,
113
+ | Opcode::ArrayMapIdentity | Opcode::CallSpread | Opcode::ConstructSpread
114
+ | Opcode::EnterBlock | Opcode::ExitBlock => 1,
105
115
  Opcode::ArraySortByProperty | Opcode::ArrayMapBinOp | Opcode::ArrayFilterBinOp
106
116
  | Opcode::LoadNativeExport => 5,
107
117
  _ => 3,