1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::rc::Rc;
5
6use harn_parser::TypeExpr;
7use serde::{Deserialize, Serialize};
8
9use crate::runtime_guards::RuntimeParamGuard;
10
11pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
19
20pub use crate::vm::ops::Op;
27pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum Constant {
32 Int(i64),
33 Float(f64),
34 String(String),
35 Bool(bool),
36 Nil,
37 Duration(i64),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) enum InlineCacheEntry {
51 Empty,
52 Property {
53 name_idx: u16,
54 target: PropertyCacheTarget,
55 },
56 Method {
57 name_idx: u16,
58 argc: usize,
59 target: MethodCacheTarget,
60 },
61 AdaptiveBinary {
62 op: AdaptiveBinaryOp,
63 state: AdaptiveBinaryState,
64 },
65 DirectCall {
66 state: DirectCallState,
67 },
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) enum AdaptiveBinaryOp {
72 Add,
73 Sub,
74 Mul,
75 Div,
76 Mod,
77 Equal,
78 NotEqual,
79 Less,
80 Greater,
81 LessEqual,
82 GreaterEqual,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub(crate) enum AdaptiveBinaryState {
95 Warmup {
96 shape: BinaryShape,
97 hits: u8,
98 },
99 Specialized {
100 shape: BinaryShape,
101 hits: u64,
102 misses: u64,
103 },
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub(crate) enum BinaryShape {
108 Int,
109 Float,
110 Bool,
111 String,
112}
113
114#[derive(Debug, Clone)]
115pub(crate) enum DirectCallState {
116 Warmup {
117 argc: usize,
118 target: DirectCallTarget,
119 hits: u8,
120 },
121 Specialized {
122 argc: usize,
123 target: DirectCallTarget,
124 hits: u64,
125 misses: u64,
126 },
127}
128
129#[derive(Debug, Clone)]
130pub(crate) enum DirectCallTarget {
131 Closure(Rc<crate::value::VmClosure>),
132}
133
134impl PartialEq for DirectCallTarget {
135 fn eq(&self, other: &Self) -> bool {
136 match (self, other) {
137 (Self::Closure(left), Self::Closure(right)) => Rc::ptr_eq(left, right),
138 }
139 }
140}
141
142impl Eq for DirectCallTarget {}
143
144impl PartialEq for DirectCallState {
145 fn eq(&self, other: &Self) -> bool {
146 match (self, other) {
147 (
148 Self::Warmup {
149 argc: left_argc,
150 target: left_target,
151 hits: left_hits,
152 },
153 Self::Warmup {
154 argc: right_argc,
155 target: right_target,
156 hits: right_hits,
157 },
158 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
159 (
160 Self::Specialized {
161 argc: left_argc,
162 target: left_target,
163 hits: left_hits,
164 misses: left_misses,
165 },
166 Self::Specialized {
167 argc: right_argc,
168 target: right_target,
169 hits: right_hits,
170 misses: right_misses,
171 },
172 ) => {
173 left_argc == right_argc
174 && left_target == right_target
175 && left_hits == right_hits
176 && left_misses == right_misses
177 }
178 _ => false,
179 }
180 }
181}
182
183impl Eq for DirectCallState {}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub(crate) enum PropertyCacheTarget {
187 DictField(Rc<str>),
188 StructField { field_name: Rc<str>, index: usize },
189 ListCount,
190 ListEmpty,
191 ListFirst,
192 ListLast,
193 StringCount,
194 StringEmpty,
195 PairFirst,
196 PairSecond,
197 EnumVariant,
198 EnumFields,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub(crate) enum MethodCacheTarget {
203 ListCount,
204 ListEmpty,
205 ListContains,
206 StringCount,
207 StringEmpty,
208 StringContains,
209 DictCount,
210 DictHas,
211 RangeCount,
212 RangeLen,
213 RangeEmpty,
214 RangeFirst,
215 RangeLast,
216 SetCount,
217 SetLen,
218 SetEmpty,
219 SetContains,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct LocalSlotInfo {
225 pub name: String,
226 pub mutable: bool,
227 pub scope_depth: usize,
228}
229
230impl fmt::Display for Constant {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 Constant::Int(n) => write!(f, "{n}"),
234 Constant::Float(n) => write!(f, "{n}"),
235 Constant::String(s) => write!(f, "\"{s}\""),
236 Constant::Bool(b) => write!(f, "{b}"),
237 Constant::Nil => write!(f, "nil"),
238 Constant::Duration(ms) => write!(f, "{ms}ms"),
239 }
240 }
241}
242
243#[derive(Debug, Clone)]
245pub struct Chunk {
246 pub code: Vec<u8>,
248 pub constants: Vec<Constant>,
250 pub lines: Vec<u32>,
252 pub columns: Vec<u32>,
255 pub source_file: Option<String>,
260 current_col: u32,
262 pub functions: Vec<CompiledFunctionRef>,
264 inline_cache_slots: BTreeMap<usize, usize>,
270 inline_cache_index: Vec<u32>,
278 inline_caches: Rc<RefCell<Vec<InlineCacheEntry>>>,
281 constant_strings: Rc<RefCell<Vec<Option<Rc<str>>>>>,
289 pub(crate) local_slots: Vec<LocalSlotInfo>,
291 pub(crate) references_outer_names: bool,
307 #[cfg(debug_assertions)]
321 balance_depth: i32,
322 #[cfg(debug_assertions)]
323 balance_nonlinear: u32,
324}
325
326pub type ChunkRef = Rc<Chunk>;
327pub type CompiledFunctionRef = Rc<CompiledFunction>;
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct CachedChunk {
335 pub(crate) code: Vec<u8>,
336 pub(crate) constants: Vec<Constant>,
337 pub(crate) lines: Vec<u32>,
338 pub(crate) columns: Vec<u32>,
339 pub(crate) source_file: Option<String>,
340 pub(crate) current_col: u32,
341 pub(crate) functions: Vec<CachedCompiledFunction>,
342 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
343 pub(crate) local_slots: Vec<LocalSlotInfo>,
344 #[serde(default)]
345 pub(crate) references_outer_names: bool,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct CachedCompiledFunction {
350 pub(crate) name: String,
351 pub(crate) type_params: Vec<String>,
352 pub(crate) nominal_type_names: Vec<String>,
353 pub(crate) params: Vec<CachedParamSlot>,
354 pub(crate) default_start: Option<usize>,
355 pub(crate) chunk: CachedChunk,
356 pub(crate) is_generator: bool,
357 pub(crate) is_stream: bool,
358 pub(crate) has_rest_param: bool,
359 pub(crate) has_runtime_type_checks: bool,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub(crate) struct CachedParamSlot {
364 pub(crate) name: String,
365 pub(crate) type_expr: Option<TypeExpr>,
366 pub(crate) has_default: bool,
367}
368
369impl CachedParamSlot {
370 fn thaw(&self) -> ParamSlot {
371 ParamSlot {
372 name: self.name.clone(),
373 type_expr: self.type_expr.clone(),
374 runtime_guard: self
375 .type_expr
376 .as_ref()
377 .map(RuntimeParamGuard::from_type_expr),
378 has_default: self.has_default,
379 }
380 }
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct ParamSlot {
390 pub name: String,
391 pub type_expr: Option<TypeExpr>,
394 #[serde(skip)]
397 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
398 pub has_default: bool,
402}
403
404impl ParamSlot {
405 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
408 Self {
409 name: param.name.clone(),
410 type_expr: param.type_expr.clone(),
411 runtime_guard: param
412 .type_expr
413 .as_ref()
414 .map(RuntimeParamGuard::from_type_expr),
415 has_default: param.default_value.is_some(),
416 }
417 }
418
419 fn freeze_for_cache(&self) -> CachedParamSlot {
420 CachedParamSlot {
421 name: self.name.clone(),
422 type_expr: self.type_expr.clone(),
423 has_default: self.has_default,
424 }
425 }
426
427 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
432 params.iter().map(Self::from_typed_param).collect()
433 }
434}
435
436#[derive(Debug, Clone)]
438pub struct CompiledFunction {
439 pub name: String,
440 pub type_params: Vec<String>,
444 pub nominal_type_names: Vec<String>,
448 pub params: Vec<ParamSlot>,
449 pub default_start: Option<usize>,
451 pub chunk: ChunkRef,
452 pub is_generator: bool,
454 pub is_stream: bool,
456 pub has_rest_param: bool,
458 pub has_runtime_type_checks: bool,
463}
464
465impl CompiledFunction {
466 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
467 params.iter().any(|param| param.type_expr.is_some())
468 }
469
470 pub fn param_names(&self) -> impl Iterator<Item = &str> {
473 self.params.iter().map(|p| p.name.as_str())
474 }
475
476 pub fn required_param_count(&self) -> usize {
478 self.default_start.unwrap_or(self.params.len())
479 }
480
481 pub fn declares_type_param(&self, name: &str) -> bool {
482 self.type_params.iter().any(|param| param == name)
483 }
484
485 pub fn has_nominal_type(&self, name: &str) -> bool {
486 self.nominal_type_names.iter().any(|ty| ty == name)
487 }
488
489 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
490 CachedCompiledFunction {
491 name: self.name.clone(),
492 type_params: self.type_params.clone(),
493 nominal_type_names: self.nominal_type_names.clone(),
494 params: self
495 .params
496 .iter()
497 .map(ParamSlot::freeze_for_cache)
498 .collect(),
499 default_start: self.default_start,
500 chunk: self.chunk.freeze_for_cache(),
501 is_generator: self.is_generator,
502 is_stream: self.is_stream,
503 has_rest_param: self.has_rest_param,
504 has_runtime_type_checks: self.has_runtime_type_checks,
505 }
506 }
507
508 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
509 Self {
510 name: cached.name.clone(),
511 type_params: cached.type_params.clone(),
512 nominal_type_names: cached.nominal_type_names.clone(),
513 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
514 default_start: cached.default_start,
515 chunk: Rc::new(Chunk::from_cached(&cached.chunk)),
516 is_generator: cached.is_generator,
517 is_stream: cached.is_stream,
518 has_rest_param: cached.has_rest_param,
519 has_runtime_type_checks: cached.has_runtime_type_checks,
520 }
521 }
522}
523
524#[cfg(debug_assertions)]
527#[derive(Clone, Copy)]
528pub(crate) struct BalanceProbe {
529 depth: i32,
530 nonlinear: u32,
531}
532
533#[cfg(debug_assertions)]
550fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
551 use Op::*;
552 let count = count as i32;
553 Some(match op {
554 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
556 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty | Pop => -1,
560 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
564 | PushScope | PopScope | PopIterator | PopHandler => 0,
565 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
567 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
568 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
569 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
570 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
571 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
572 IterInit => -1,
575 Slice | SetSubscript => -2,
577 BuildList | Concat | CallBuiltin => 1 - count,
579 BuildDict => 1 - 2 * count,
580 Call | MethodCall | MethodCallOpt => -count,
582 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
585 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
586 | SyncMutexEnter | Import | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum
587 | MatchEnum | Yield | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
588 })
589}
590
591impl Chunk {
592 pub fn new() -> Self {
593 Self {
594 code: Vec::new(),
595 constants: Vec::new(),
596 lines: Vec::new(),
597 columns: Vec::new(),
598 source_file: None,
599 current_col: 0,
600 functions: Vec::new(),
601 inline_cache_slots: BTreeMap::new(),
602 inline_cache_index: Vec::new(),
603 inline_caches: Rc::new(RefCell::new(Vec::new())),
604 constant_strings: Rc::new(RefCell::new(Vec::new())),
605 local_slots: Vec::new(),
606 references_outer_names: false,
607 #[cfg(debug_assertions)]
608 balance_depth: 0,
609 #[cfg(debug_assertions)]
610 balance_nonlinear: 0,
611 }
612 }
613
614 pub fn set_column(&mut self, col: u32) {
616 self.current_col = col;
617 }
618
619 pub fn add_constant(&mut self, constant: Constant) -> u16 {
621 for (i, c) in self.constants.iter().enumerate() {
622 if c == &constant {
623 return i as u16;
624 }
625 }
626 let idx = self.constants.len();
627 self.constants.push(constant);
628 idx as u16
629 }
630
631 pub fn emit(&mut self, op: Op, line: u32) {
633 #[cfg(debug_assertions)]
634 self.note_balance(op, 0);
635 let col = self.current_col;
636 let op_offset = self.code.len();
637 self.code.push(op as u8);
638 self.lines.push(line);
639 self.columns.push(col);
640 if is_adaptive_binary_op(op) {
641 self.register_inline_cache(op_offset);
642 }
643 if op_reads_outer_name(op) {
644 self.references_outer_names = true;
645 }
646 }
647
648 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
650 #[cfg(debug_assertions)]
651 self.note_balance(op, arg);
652 let col = self.current_col;
653 let op_offset = self.code.len();
654 self.code.push(op as u8);
655 self.code.push((arg >> 8) as u8);
656 self.code.push((arg & 0xFF) as u8);
657 self.lines.push(line);
658 self.lines.push(line);
659 self.lines.push(line);
660 self.columns.push(col);
661 self.columns.push(col);
662 self.columns.push(col);
663 if matches!(
664 op,
665 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
666 ) {
667 self.register_inline_cache(op_offset);
668 }
669 if op_reads_outer_name(op) {
670 self.references_outer_names = true;
671 }
672 }
673
674 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
676 #[cfg(debug_assertions)]
677 self.note_balance(op, arg as u16);
678 let col = self.current_col;
679 let op_offset = self.code.len();
680 self.code.push(op as u8);
681 self.code.push(arg);
682 self.lines.push(line);
683 self.lines.push(line);
684 self.columns.push(col);
685 self.columns.push(col);
686 if matches!(op, Op::Call) {
687 self.register_inline_cache(op_offset);
688 }
689 if op_reads_outer_name(op) {
690 self.references_outer_names = true;
691 }
692 }
693
694 pub fn emit_call_builtin(
696 &mut self,
697 id: crate::BuiltinId,
698 name_idx: u16,
699 arg_count: u8,
700 line: u32,
701 ) {
702 #[cfg(debug_assertions)]
703 self.note_balance(Op::CallBuiltin, arg_count as u16);
704 let col = self.current_col;
705 let op_offset = self.code.len();
706 self.code.push(Op::CallBuiltin as u8);
707 self.code.extend_from_slice(&id.raw().to_be_bytes());
708 self.code.push((name_idx >> 8) as u8);
709 self.code.push((name_idx & 0xFF) as u8);
710 self.code.push(arg_count);
711 for _ in 0..12 {
712 self.lines.push(line);
713 self.columns.push(col);
714 }
715 self.register_inline_cache(op_offset);
716 self.references_outer_names = true;
717 }
718
719 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
721 #[cfg(debug_assertions)]
722 self.note_balance(Op::CallBuiltinSpread, 0);
723 let col = self.current_col;
724 self.code.push(Op::CallBuiltinSpread as u8);
725 self.code.extend_from_slice(&id.raw().to_be_bytes());
726 self.code.push((name_idx >> 8) as u8);
727 self.code.push((name_idx & 0xFF) as u8);
728 for _ in 0..11 {
729 self.lines.push(line);
730 self.columns.push(col);
731 }
732 self.references_outer_names = true;
733 }
734
735 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
737 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
738 }
739
740 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
742 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
743 }
744
745 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
746 #[cfg(debug_assertions)]
747 self.note_balance(op, arg_count as u16);
748 let col = self.current_col;
749 let op_offset = self.code.len();
750 self.code.push(op as u8);
751 self.code.push((name_idx >> 8) as u8);
752 self.code.push((name_idx & 0xFF) as u8);
753 self.code.push(arg_count);
754 self.lines.push(line);
755 self.lines.push(line);
756 self.lines.push(line);
757 self.lines.push(line);
758 self.columns.push(col);
759 self.columns.push(col);
760 self.columns.push(col);
761 self.columns.push(col);
762 self.register_inline_cache(op_offset);
763 }
764
765 pub fn current_offset(&self) -> usize {
767 self.code.len()
768 }
769
770 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
772 #[cfg(debug_assertions)]
773 self.note_balance(op, 0);
774 let col = self.current_col;
775 self.code.push(op as u8);
776 let patch_pos = self.code.len();
777 self.code.push(0xFF);
778 self.code.push(0xFF);
779 self.lines.push(line);
780 self.lines.push(line);
781 self.lines.push(line);
782 self.columns.push(col);
783 self.columns.push(col);
784 self.columns.push(col);
785 patch_pos
786 }
787
788 pub fn patch_jump(&mut self, patch_pos: usize) {
790 let target = self.code.len() as u16;
791 self.code[patch_pos] = (target >> 8) as u8;
792 self.code[patch_pos + 1] = (target & 0xFF) as u8;
793 }
794
795 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
797 let target = target as u16;
798 self.code[patch_pos] = (target >> 8) as u8;
799 self.code[patch_pos + 1] = (target & 0xFF) as u8;
800 }
801
802 pub fn read_u16(&self, pos: usize) -> u16 {
804 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
805 }
806
807 #[cfg(debug_assertions)]
811 fn note_balance(&mut self, op: Op, count: u16) {
812 match op_stack_delta(op, count) {
813 Some(delta) => self.balance_depth += delta,
814 None => self.balance_nonlinear += 1,
815 }
816 }
817
818 #[cfg(debug_assertions)]
821 pub(crate) fn balance_probe(&self) -> BalanceProbe {
822 BalanceProbe {
823 depth: self.balance_depth,
824 nonlinear: self.balance_nonlinear,
825 }
826 }
827
828 #[cfg(debug_assertions)]
834 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
835 if self.balance_nonlinear == probe.nonlinear {
836 Some(self.balance_depth - probe.depth)
837 } else {
838 None
839 }
840 }
841
842 fn register_inline_cache(&mut self, op_offset: usize) {
843 if self.inline_cache_slots.contains_key(&op_offset) {
844 return;
845 }
846 let mut entries = self.inline_caches.borrow_mut();
847 let slot = entries.len();
848 entries.push(InlineCacheEntry::Empty);
849 self.inline_cache_slots.insert(op_offset, slot);
850 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
851 }
852
853 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
858 if op_offset >= index.len() {
859 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
860 }
861 index[op_offset] = slot as u32;
862 }
863
864 #[inline]
873 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
874 match self.inline_cache_index.get(op_offset).copied() {
875 None | Some(NO_INLINE_CACHE_SLOT) => None,
876 Some(slot) => Some(slot as usize),
877 }
878 }
879
880 #[cfg(feature = "vm-bench-internals")]
887 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
888 self.inline_cache_slots.get(&op_offset).copied()
889 }
890
891 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Rc<str>> {
896 let mut entries = self.constant_strings.borrow_mut();
901 if entries.len() < self.constants.len() {
902 entries.resize(self.constants.len(), None);
903 }
904 if let Some(Some(existing)) = entries.get(idx) {
905 return Some(Rc::clone(existing));
906 }
907 let materialized = match self.constants.get(idx)? {
908 Constant::String(s) => Rc::<str>::from(s.as_str()),
909 _ => return None,
910 };
911 entries[idx] = Some(Rc::clone(&materialized));
912 Some(materialized)
913 }
914
915 #[cfg(feature = "vm-bench-internals")]
916 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
917 self.inline_caches
918 .borrow()
919 .get(slot)
920 .cloned()
921 .unwrap_or(InlineCacheEntry::Empty)
922 }
923
924 #[inline]
936 pub(crate) fn peek_adaptive_binary_cache(
937 &self,
938 slot: usize,
939 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
940 match self.inline_caches.borrow().get(slot)? {
941 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
942 _ => None,
943 }
944 }
945
946 #[inline]
958 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
959 match self.inline_caches.borrow().get(slot)? {
960 &InlineCacheEntry::Method {
961 name_idx,
962 argc,
963 target,
964 } => Some((name_idx, argc, target)),
965 _ => None,
966 }
967 }
968
969 #[inline]
979 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
980 match self.inline_caches.borrow().get(slot)? {
981 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
982 _ => None,
983 }
984 }
985
986 #[inline]
996 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
997 match self.inline_caches.borrow().get(slot)? {
998 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
999 _ => None,
1000 }
1001 }
1002
1003 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1004 if let Some(existing) = self.inline_caches.borrow_mut().get_mut(slot) {
1005 *existing = entry;
1006 }
1007 }
1008
1009 pub fn freeze_for_cache(&self) -> CachedChunk {
1010 CachedChunk {
1011 code: self.code.clone(),
1012 constants: self.constants.clone(),
1013 lines: self.lines.clone(),
1014 columns: self.columns.clone(),
1015 source_file: self.source_file.clone(),
1016 current_col: self.current_col,
1017 functions: self
1018 .functions
1019 .iter()
1020 .map(|function| function.freeze_for_cache())
1021 .collect(),
1022 inline_cache_slots: self.inline_cache_slots.clone(),
1023 local_slots: self.local_slots.clone(),
1024 references_outer_names: self.references_outer_names,
1025 }
1026 }
1027
1028 pub fn from_cached(cached: &CachedChunk) -> Self {
1029 let inline_cache_count = cached.inline_cache_slots.len();
1030 let constants_count = cached.constants.len();
1031 let mut inline_cache_index = Vec::new();
1038 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1039 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1040 if op_offset < inline_cache_index.len() {
1041 inline_cache_index[op_offset] = slot as u32;
1042 }
1043 }
1044 Self {
1045 code: cached.code.clone(),
1046 constants: cached.constants.clone(),
1047 lines: cached.lines.clone(),
1048 columns: cached.columns.clone(),
1049 source_file: cached.source_file.clone(),
1050 current_col: cached.current_col,
1051 functions: cached
1052 .functions
1053 .iter()
1054 .map(|function| Rc::new(CompiledFunction::from_cached(function)))
1055 .collect(),
1056 inline_cache_slots: cached.inline_cache_slots.clone(),
1057 inline_cache_index,
1058 inline_caches: Rc::new(RefCell::new(vec![
1059 InlineCacheEntry::Empty;
1060 inline_cache_count
1061 ])),
1062 constant_strings: Rc::new(RefCell::new(vec![None; constants_count])),
1063 local_slots: cached.local_slots.clone(),
1064 references_outer_names: cached.references_outer_names,
1065 #[cfg(debug_assertions)]
1066 balance_depth: 0,
1067 #[cfg(debug_assertions)]
1068 balance_nonlinear: 0,
1069 }
1070 }
1071
1072 pub(crate) fn add_local_slot(
1073 &mut self,
1074 name: String,
1075 mutable: bool,
1076 scope_depth: usize,
1077 ) -> u16 {
1078 let idx = self.local_slots.len();
1079 self.local_slots.push(LocalSlotInfo {
1080 name,
1081 mutable,
1082 scope_depth,
1083 });
1084 idx as u16
1085 }
1086
1087 #[cfg(test)]
1088 pub(crate) fn inline_cache_entries(&self) -> Vec<InlineCacheEntry> {
1089 self.inline_caches.borrow().clone()
1090 }
1091
1092 pub fn read_u64(&self, pos: usize) -> u64 {
1094 u64::from_be_bytes([
1095 self.code[pos],
1096 self.code[pos + 1],
1097 self.code[pos + 2],
1098 self.code[pos + 3],
1099 self.code[pos + 4],
1100 self.code[pos + 5],
1101 self.code[pos + 6],
1102 self.code[pos + 7],
1103 ])
1104 }
1105
1106 pub fn disassemble(&self, name: &str) -> String {
1110 let mut out = format!("== {name} ==\n");
1111 let mut ip = 0;
1112 while ip < self.code.len() {
1113 let op_byte = self.code[ip];
1114 let line = self.lines.get(ip).copied().unwrap_or(0);
1115 out.push_str(&format!("{ip:04} [{line:>4}] "));
1116 ip += 1;
1117
1118 if let Some(op) = Op::from_byte(op_byte) {
1119 self.disassemble_op(op, &mut ip, &mut out);
1120 } else {
1121 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1122 }
1123 }
1124 out
1125 }
1126}
1127
1128pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1139 label.to_string()
1140}
1141
1142pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1143 let arg = chunk.code[*ip];
1144 *ip += 1;
1145 format!("{label} {arg:>4}")
1146}
1147
1148pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1149 let arg = chunk.read_u16(*ip);
1150 *ip += 2;
1151 format!("{label} {arg:>4}")
1152}
1153
1154pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1155 let idx = chunk.read_u16(*ip);
1156 *ip += 2;
1157 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1158}
1159
1160pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1161 let slot = chunk.read_u16(*ip);
1162 *ip += 2;
1163 let mut out = format!("{label} {slot:>4}");
1164 if let Some(info) = chunk.local_slots.get(slot as usize) {
1165 out.push_str(&format!(" ({})", info.name));
1166 }
1167 out
1168}
1169
1170pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1171 let idx = chunk.read_u16(*ip);
1172 *ip += 2;
1173 let argc = chunk.code[*ip];
1174 *ip += 1;
1175 format!(
1176 "{label} {idx:>4} ({}) argc={argc}",
1177 chunk.constants[idx as usize]
1178 )
1179}
1180
1181pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1182 let enum_idx = chunk.read_u16(*ip);
1183 *ip += 2;
1184 let var_idx = chunk.read_u16(*ip);
1185 *ip += 2;
1186 format!(
1187 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1188 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1189 )
1190}
1191
1192pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1193 let enum_idx = chunk.read_u16(*ip);
1194 *ip += 2;
1195 let var_idx = chunk.read_u16(*ip);
1196 *ip += 2;
1197 let field_count = chunk.read_u16(*ip);
1198 *ip += 2;
1199 format!(
1200 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1201 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1202 )
1203}
1204
1205pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1206 let path_idx = chunk.read_u16(*ip);
1207 *ip += 2;
1208 let names_idx = chunk.read_u16(*ip);
1209 *ip += 2;
1210 format!(
1211 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1212 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1213 )
1214}
1215
1216pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1217 let var_idx = chunk.read_u16(*ip);
1218 *ip += 2;
1219 let type_idx = chunk.read_u16(*ip);
1220 *ip += 2;
1221 format!(
1222 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1223 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1224 )
1225}
1226
1227pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1228 let id = chunk.read_u64(*ip);
1229 *ip += 8;
1230 let idx = chunk.read_u16(*ip);
1231 *ip += 2;
1232 let argc = chunk.code[*ip];
1233 *ip += 1;
1234 format!(
1235 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1236 chunk.constants[idx as usize],
1237 )
1238}
1239
1240pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1241 let id = chunk.read_u64(*ip);
1242 *ip += 8;
1243 let idx = chunk.read_u16(*ip);
1244 *ip += 2;
1245 format!(
1246 "{label} {id:#018x} {idx:>4} ({})",
1247 chunk.constants[idx as usize],
1248 )
1249}
1250
1251pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1252 let idx = chunk.read_u16(*ip);
1258 *ip += 2;
1259 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1260}
1261
1262impl Default for Chunk {
1263 fn default() -> Self {
1264 Self::new()
1265 }
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use std::rc::Rc;
1271
1272 use super::{
1273 Chunk, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget, Op,
1274 PropertyCacheTarget,
1275 };
1276 use crate::BuiltinId;
1277
1278 #[test]
1279 fn op_from_byte_matches_repr_order() {
1280 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1281 assert_eq!(byte as u8, op as u8);
1282 assert_eq!(Op::from_byte(byte as u8), Some(op));
1283 }
1284 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1285 assert_eq!(Op::COUNT, Op::ALL.len());
1286 }
1287
1288 #[test]
1289 fn disassemble_covers_every_opcode_variant() {
1290 for op in Op::ALL.iter().copied() {
1300 let mut chunk = Chunk::new();
1301 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1302 for _ in 0..16 {
1306 chunk.code.push(0);
1307 }
1308 let mut ip: usize = 0;
1309 let mut out = String::new();
1310 chunk.disassemble_op(op, &mut ip, &mut out);
1311 assert!(
1312 !out.contains("UNKNOWN"),
1313 "disasm emitted UNKNOWN for {op:?}: {out}",
1314 );
1315 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1316 }
1317 }
1318
1319 #[test]
1328 fn empty_chunk_does_not_reference_outer_names() {
1329 let chunk = Chunk::new();
1330 assert!(!chunk.references_outer_names);
1331 }
1332
1333 #[test]
1334 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1335 let mut chunk = Chunk::new();
1340 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1341 chunk.emit_u16(Op::Constant, 0, 1);
1342 chunk.emit(Op::MulInt, 1);
1343 chunk.emit(Op::Pop, 1);
1344 chunk.emit(Op::Return, 1);
1345 assert!(!chunk.references_outer_names);
1346 }
1347
1348 #[test]
1349 fn slot_only_chunk_does_not_reference_outer_names() {
1350 let mut chunk = Chunk::new();
1352 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1353 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1354 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1355 assert!(!chunk.references_outer_names);
1356 }
1357
1358 #[test]
1359 fn get_var_flags_outer_name_reference() {
1360 let mut chunk = Chunk::new();
1361 chunk.emit_u16(Op::GetVar, 0, 1);
1362 assert!(chunk.references_outer_names);
1363 }
1364
1365 #[test]
1366 fn set_var_flags_outer_name_reference() {
1367 let mut chunk = Chunk::new();
1368 chunk.emit_u16(Op::SetVar, 0, 1);
1369 assert!(chunk.references_outer_names);
1370 }
1371
1372 #[test]
1373 fn check_type_flags_outer_name_reference() {
1374 let mut chunk = Chunk::new();
1375 chunk.emit_u16(Op::CheckType, 0, 1);
1376 assert!(chunk.references_outer_names);
1377 }
1378
1379 #[test]
1380 fn call_builtin_flags_outer_name_reference() {
1381 let mut chunk = Chunk::new();
1382 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1383 assert!(chunk.references_outer_names);
1384 }
1385
1386 #[test]
1387 fn call_builtin_spread_flags_outer_name_reference() {
1388 let mut chunk = Chunk::new();
1389 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1390 assert!(chunk.references_outer_names);
1391 }
1392
1393 #[test]
1394 fn tail_call_flags_outer_name_reference() {
1395 let mut chunk = Chunk::new();
1398 chunk.emit_u8(Op::TailCall, 1, 1);
1399 assert!(chunk.references_outer_names);
1400 }
1401
1402 #[test]
1403 fn call_flags_outer_name_reference() {
1404 let mut chunk = Chunk::new();
1407 chunk.emit_u8(Op::Call, 1, 1);
1408 assert!(chunk.references_outer_names);
1409 }
1410
1411 #[test]
1412 fn pipe_flags_outer_name_reference() {
1413 let mut chunk = Chunk::new();
1416 chunk.emit(Op::Pipe, 1);
1417 assert!(chunk.references_outer_names);
1418 }
1419
1420 #[test]
1421 fn method_call_does_not_flag_outer_name_reference() {
1422 let mut chunk = Chunk::new();
1425 chunk.emit_method_call(0, 1, 1);
1426 chunk.emit_method_call_opt(0, 1, 1);
1427 assert!(!chunk.references_outer_names);
1428 }
1429
1430 #[test]
1431 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1432 let mut chunk = Chunk::new();
1435 chunk.emit_u16(Op::Constant, 0, 1);
1436 chunk.emit(Op::JumpIfFalse, 1);
1437 chunk.emit(Op::Jump, 1);
1438 chunk.emit(Op::Return, 1);
1439 chunk.emit(Op::Pop, 1);
1440 assert!(!chunk.references_outer_names);
1441 }
1442
1443 #[test]
1444 fn references_outer_names_is_monotonic() {
1445 let mut chunk = Chunk::new();
1448 chunk.emit_u16(Op::GetVar, 0, 1);
1449 assert!(chunk.references_outer_names);
1450 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1451 chunk.emit(Op::MulInt, 1);
1452 assert!(chunk.references_outer_names);
1453 }
1454
1455 #[test]
1456 fn freeze_thaw_round_trips_references_outer_names() {
1457 let mut chunk = Chunk::new();
1461 chunk.emit_u16(Op::GetVar, 0, 1);
1462 assert!(chunk.references_outer_names);
1463 let frozen = chunk.freeze_for_cache();
1464 let thawed = Chunk::from_cached(&frozen);
1465 assert!(thawed.references_outer_names);
1466
1467 let plain = Chunk::new();
1468 assert!(!plain.references_outer_names);
1469 let frozen_plain = plain.freeze_for_cache();
1470 let thawed_plain = Chunk::from_cached(&frozen_plain);
1471 assert!(!thawed_plain.references_outer_names);
1472 }
1473
1474 #[test]
1486 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1487 let mut chunk = Chunk::new();
1490 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1491 chunk.emit(Op::Pop, 1);
1492 chunk.emit(Op::Return, 1);
1493 assert!(chunk.inline_cache_slot(0).is_none());
1494 assert!(chunk.inline_cache_slot(3).is_none());
1495 assert!(chunk.inline_cache_slot(4).is_none());
1496 }
1497
1498 #[test]
1499 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1500 let mut chunk = Chunk::new();
1504 chunk.emit(Op::Add, 1);
1505 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1506 }
1507
1508 #[test]
1509 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1510 let mut chunk = Chunk::new();
1515 chunk.emit(Op::Add, 1);
1516 chunk.emit(Op::Sub, 1);
1517 chunk.emit(Op::Mul, 1);
1518 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1519 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1520 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1521 assert_ne!(s0, s1);
1522 assert_ne!(s1, s2);
1523 assert_ne!(s0, s2);
1524 }
1525
1526 #[test]
1527 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1528 let mut chunk = Chunk::new();
1531 chunk.emit(Op::Add, 1);
1532 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1533 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1534 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1535 }
1536
1537 #[test]
1538 fn inline_cache_slot_for_get_property_and_method_call() {
1539 let mut chunk = Chunk::new();
1543 chunk.emit_u16(Op::GetProperty, 0, 1); chunk.emit_method_call(0, 1, 1); chunk.emit_method_call_opt(0, 1, 1); chunk.emit_u16(Op::GetPropertyOpt, 0, 1); assert!(chunk.inline_cache_slot(0).is_some(), "GetProperty");
1548 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1549 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1550 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1551 }
1552
1553 #[test]
1554 fn inline_cache_slot_for_call_and_call_builtin() {
1555 let mut chunk = Chunk::new();
1560 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1562 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1563 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1564 assert!(
1565 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1566 "Op::CallBuiltin IC slot"
1567 );
1568 }
1569
1570 #[test]
1571 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1572 let mut chunk = Chunk::new();
1578 chunk.emit(Op::Add, 1);
1579 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1580 chunk.register_inline_cache(0);
1582 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1583 assert_eq!(slot_before, slot_after);
1584 }
1585
1586 #[test]
1587 fn inline_cache_index_round_trips_through_cached_chunk() {
1588 let mut chunk = Chunk::new();
1595 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1596 chunk.emit_u16(Op::Constant, 0, 1);
1597 chunk.emit(Op::Add, 1);
1598 chunk.emit(Op::Sub, 1);
1599 chunk.emit_method_call(0, 1, 1);
1600 chunk.emit_u8(Op::Call, 1, 1);
1601 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1602 .map(|o| (o, chunk.inline_cache_slot(o)))
1603 .collect();
1604 let frozen = chunk.freeze_for_cache();
1605 let thawed = Chunk::from_cached(&frozen);
1606 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1607 .map(|o| (o, thawed.inline_cache_slot(o)))
1608 .collect();
1609 assert_eq!(live_slots, thawed_slots);
1610 }
1611
1612 #[test]
1613 fn inline_cache_index_agrees_with_btreemap_view() {
1614 let mut chunk = Chunk::new();
1620 chunk.emit(Op::Add, 1);
1621 chunk.emit_u16(Op::GetVar, 0, 1);
1622 chunk.emit(Op::LessInt, 1);
1623 chunk.emit_u8(Op::Call, 2, 1);
1624 chunk.emit(Op::Equal, 1);
1625 chunk.emit_u16(Op::GetProperty, 0, 1);
1626 chunk.emit_method_call_opt(0, 0, 1);
1627 for offset in 0..chunk.code.len() {
1628 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1629 let from_index = chunk.inline_cache_slot(offset);
1630 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1631 }
1632 }
1633
1634 #[test]
1645 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1646 let mut chunk = Chunk::new();
1647 chunk.emit(Op::Add, 1);
1648 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1649 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1651 }
1652
1653 #[test]
1654 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1655 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1656 let mut chunk = Chunk::new();
1657 chunk.emit(Op::Add, 1);
1658 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1659 chunk.set_inline_cache_entry(
1660 slot,
1661 InlineCacheEntry::AdaptiveBinary {
1662 op: AdaptiveBinaryOp::Add,
1663 state: AdaptiveBinaryState::Warmup {
1664 shape: BinaryShape::Int,
1665 hits: 2,
1666 },
1667 },
1668 );
1669 let (op, state) = chunk
1670 .peek_adaptive_binary_cache(slot)
1671 .expect("warmed slot peek");
1672 assert_eq!(op, AdaptiveBinaryOp::Add);
1673 assert!(matches!(
1674 state,
1675 AdaptiveBinaryState::Warmup {
1676 shape: BinaryShape::Int,
1677 hits: 2
1678 }
1679 ));
1680 }
1681
1682 #[test]
1683 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1684 use super::{InlineCacheEntry, PropertyCacheTarget};
1690 let mut chunk = Chunk::new();
1691 chunk.emit(Op::Add, 1);
1692 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1693 chunk.set_inline_cache_entry(
1694 slot,
1695 InlineCacheEntry::Property {
1696 name_idx: 0,
1697 target: PropertyCacheTarget::ListCount,
1698 },
1699 );
1700 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1701 }
1702
1703 #[test]
1704 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1705 let chunk = Chunk::new();
1710 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1711 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1712 }
1713
1714 #[test]
1715 fn peek_adaptive_binary_state_is_copy() {
1716 fn assert_copy<T: Copy>() {}
1722 assert_copy::<super::AdaptiveBinaryState>();
1723 assert_copy::<super::AdaptiveBinaryOp>();
1724 assert_copy::<super::BinaryShape>();
1725 }
1726
1727 #[test]
1738 fn peek_method_cache_returns_none_for_empty_slot() {
1739 let mut chunk = Chunk::new();
1740 chunk.emit_method_call(0, 0, 1);
1741 let slot = chunk
1742 .inline_cache_slot(0)
1743 .expect("MethodCall registers a slot");
1744 assert!(chunk.peek_method_cache(slot).is_none());
1745 }
1746
1747 #[test]
1748 fn peek_method_cache_returns_triple_after_warmup() {
1749 let mut chunk = Chunk::new();
1750 chunk.emit_method_call(7, 2, 1);
1751 let slot = chunk
1752 .inline_cache_slot(0)
1753 .expect("MethodCall registers a slot");
1754 chunk.set_inline_cache_entry(
1755 slot,
1756 InlineCacheEntry::Method {
1757 name_idx: 7,
1758 argc: 2,
1759 target: MethodCacheTarget::ListContains,
1760 },
1761 );
1762 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1763 assert_eq!(name_idx, 7);
1764 assert_eq!(argc, 2);
1765 assert_eq!(target, MethodCacheTarget::ListContains);
1766 }
1767
1768 #[test]
1769 fn peek_method_cache_returns_none_for_non_method_variants() {
1770 let mut chunk = Chunk::new();
1774 chunk.emit_method_call(0, 0, 1);
1775 let slot = chunk
1776 .inline_cache_slot(0)
1777 .expect("MethodCall registers a slot");
1778
1779 chunk.set_inline_cache_entry(
1780 slot,
1781 InlineCacheEntry::Property {
1782 name_idx: 0,
1783 target: PropertyCacheTarget::ListCount,
1784 },
1785 );
1786 assert!(chunk.peek_method_cache(slot).is_none());
1787 }
1788
1789 #[test]
1790 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1791 let chunk = Chunk::new();
1792 assert!(chunk.peek_method_cache(0).is_none());
1793 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1794 }
1795
1796 #[test]
1797 fn peek_method_cache_target_is_copy() {
1798 fn assert_copy<T: Copy>() {}
1804 assert_copy::<super::MethodCacheTarget>();
1805 }
1806
1807 #[test]
1817 fn peek_property_cache_returns_none_for_empty_slot() {
1818 let mut chunk = Chunk::new();
1819 chunk.emit_u16(Op::GetProperty, 0, 1);
1820 let slot = chunk
1821 .inline_cache_slot(0)
1822 .expect("GetProperty registers a slot");
1823 assert!(chunk.peek_property_cache(slot).is_none());
1824 }
1825
1826 #[test]
1827 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1828 let mut chunk = Chunk::new();
1829 chunk.emit_u16(Op::GetProperty, 0, 1);
1830 let slot = chunk
1831 .inline_cache_slot(0)
1832 .expect("GetProperty registers a slot");
1833 chunk.set_inline_cache_entry(
1834 slot,
1835 InlineCacheEntry::Property {
1836 name_idx: 11,
1837 target: PropertyCacheTarget::DictField(Rc::from("count")),
1838 },
1839 );
1840 let (name_idx, target) = chunk
1841 .peek_property_cache(slot)
1842 .expect("warmed property slot peek");
1843 assert_eq!(name_idx, 11);
1844 match target {
1845 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1846 other => panic!("expected DictField, got {other:?}"),
1847 }
1848 }
1849
1850 #[test]
1851 fn peek_property_cache_returns_pair_for_unit_target() {
1852 let mut chunk = Chunk::new();
1856 chunk.emit_u16(Op::GetProperty, 0, 1);
1857 let slot = chunk
1858 .inline_cache_slot(0)
1859 .expect("GetProperty registers a slot");
1860 chunk.set_inline_cache_entry(
1861 slot,
1862 InlineCacheEntry::Property {
1863 name_idx: 3,
1864 target: PropertyCacheTarget::ListCount,
1865 },
1866 );
1867 let (name_idx, target) = chunk
1868 .peek_property_cache(slot)
1869 .expect("warmed property slot peek");
1870 assert_eq!(name_idx, 3);
1871 assert_eq!(target, PropertyCacheTarget::ListCount);
1872 }
1873
1874 #[test]
1875 fn peek_property_cache_returns_none_for_non_property_variants() {
1876 let mut chunk = Chunk::new();
1877 chunk.emit_u16(Op::GetProperty, 0, 1);
1878 let slot = chunk
1879 .inline_cache_slot(0)
1880 .expect("GetProperty registers a slot");
1881 chunk.set_inline_cache_entry(
1882 slot,
1883 InlineCacheEntry::Method {
1884 name_idx: 0,
1885 argc: 0,
1886 target: MethodCacheTarget::ListCount,
1887 },
1888 );
1889 assert!(chunk.peek_property_cache(slot).is_none());
1890 }
1891
1892 #[test]
1893 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1894 let chunk = Chunk::new();
1895 assert!(chunk.peek_property_cache(0).is_none());
1896 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1897 }
1898
1899 #[test]
1909 fn peek_direct_call_state_returns_none_for_empty_slot() {
1910 let mut chunk = Chunk::new();
1911 chunk.emit_u8(Op::Call, 0, 1);
1912 let slot = chunk
1913 .inline_cache_slot(0)
1914 .expect("Op::Call registers a slot");
1915 assert!(chunk.peek_direct_call_state(slot).is_none());
1916 }
1917
1918 #[test]
1919 fn peek_direct_call_state_returns_warmup_state() {
1920 let mut chunk = Chunk::new();
1921 chunk.emit_u8(Op::Call, 0, 1);
1922 let slot = chunk
1923 .inline_cache_slot(0)
1924 .expect("Op::Call registers a slot");
1925 let target = synthetic_direct_call_target();
1926 chunk.set_inline_cache_entry(
1927 slot,
1928 InlineCacheEntry::DirectCall {
1929 state: DirectCallState::Warmup {
1930 argc: 2,
1931 target: target.clone(),
1932 hits: 1,
1933 },
1934 },
1935 );
1936 let state = chunk
1937 .peek_direct_call_state(slot)
1938 .expect("warmed direct-call slot peek");
1939 match state {
1940 DirectCallState::Warmup {
1941 argc,
1942 target: peeked_target,
1943 hits,
1944 } => {
1945 assert_eq!(argc, 2);
1946 assert_eq!(hits, 1);
1947 assert_eq!(peeked_target, target);
1948 }
1949 other => panic!("expected Warmup, got {other:?}"),
1950 }
1951 }
1952
1953 #[test]
1954 fn peek_direct_call_state_returns_specialized_state() {
1955 let mut chunk = Chunk::new();
1956 chunk.emit_u8(Op::Call, 0, 1);
1957 let slot = chunk
1958 .inline_cache_slot(0)
1959 .expect("Op::Call registers a slot");
1960 let target = synthetic_direct_call_target();
1961 chunk.set_inline_cache_entry(
1962 slot,
1963 InlineCacheEntry::DirectCall {
1964 state: DirectCallState::Specialized {
1965 argc: 3,
1966 target: target.clone(),
1967 hits: 100,
1968 misses: 0,
1969 },
1970 },
1971 );
1972 let state = chunk
1973 .peek_direct_call_state(slot)
1974 .expect("warmed direct-call slot peek");
1975 match state {
1976 DirectCallState::Specialized {
1977 argc,
1978 target: peeked_target,
1979 hits,
1980 misses,
1981 } => {
1982 assert_eq!(argc, 3);
1983 assert_eq!(hits, 100);
1984 assert_eq!(misses, 0);
1985 assert_eq!(peeked_target, target);
1986 }
1987 other => panic!("expected Specialized, got {other:?}"),
1988 }
1989 }
1990
1991 #[test]
1992 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
1993 let mut chunk = Chunk::new();
1994 chunk.emit_u8(Op::Call, 0, 1);
1995 let slot = chunk
1996 .inline_cache_slot(0)
1997 .expect("Op::Call registers a slot");
1998
1999 chunk.set_inline_cache_entry(
2000 slot,
2001 InlineCacheEntry::Property {
2002 name_idx: 0,
2003 target: PropertyCacheTarget::ListCount,
2004 },
2005 );
2006 assert!(chunk.peek_direct_call_state(slot).is_none());
2007 }
2008
2009 #[test]
2010 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2011 let chunk = Chunk::new();
2012 assert!(chunk.peek_direct_call_state(0).is_none());
2013 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2014 }
2015
2016 fn synthetic_direct_call_target() -> DirectCallTarget {
2020 use crate::value::VmClosure;
2021 use crate::{CompiledFunction, VmEnv};
2022 let func = CompiledFunction {
2023 name: "synthetic".to_string(),
2024 type_params: Vec::new(),
2025 nominal_type_names: Vec::new(),
2026 params: Vec::new(),
2027 default_start: None,
2028 chunk: Rc::new(Chunk::new()),
2029 is_generator: false,
2030 is_stream: false,
2031 has_rest_param: false,
2032 has_runtime_type_checks: false,
2033 };
2034 DirectCallTarget::Closure(Rc::new(VmClosure {
2035 func: Rc::new(func),
2036 env: VmEnv::new(),
2037 source_dir: None,
2038 module_functions: None,
2039 module_state: None,
2040 }))
2041 }
2042}