1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5
6use harn_parser::TypeExpr;
7use parking_lot::Mutex;
8use serde::{Deserialize, Serialize};
9
10use crate::harness::HarnessKind;
11use crate::runtime_guards::RuntimeParamGuard;
12
13pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
21static NEXT_CHUNK_CACHE_ID: AtomicU64 = AtomicU64::new(1);
22
23fn next_chunk_cache_id() -> u64 {
24 NEXT_CHUNK_CACHE_ID.fetch_add(1, Ordering::Relaxed)
25}
26
27pub use crate::vm::ops::Op;
34pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum Constant {
39 Int(i64),
40 Float(f64),
41 String(String),
42 Bool(bool),
43 Nil,
44 Duration(i64),
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
57pub(crate) enum InlineCacheEntry {
58 Empty,
59 Property {
60 name_idx: u16,
61 target: PropertyCacheTarget,
62 },
63 Method {
64 name_idx: u16,
65 argc: usize,
66 target: MethodCacheTarget,
67 },
68 AdaptiveBinary {
69 op: AdaptiveBinaryOp,
70 state: AdaptiveBinaryState,
71 },
72 DirectCall {
73 state: DirectCallState,
74 },
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) enum AdaptiveBinaryOp {
79 Add,
80 Sub,
81 Mul,
82 Div,
83 Mod,
84 Equal,
85 NotEqual,
86 Less,
87 Greater,
88 LessEqual,
89 GreaterEqual,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub(crate) enum AdaptiveBinaryState {
99 Warmup {
100 shape: BinaryShape,
101 hits: u8,
102 },
103 Specialized {
104 shape: BinaryShape,
105 hits: u64,
106 misses: u64,
107 },
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub(crate) enum BinaryShape {
112 Int,
113 Float,
114 Bool,
115 String,
116}
117
118#[derive(Debug, Clone)]
119pub(crate) enum DirectCallState {
120 Warmup {
121 argc: usize,
122 target: DirectCallTarget,
123 hits: u8,
124 },
125 Specialized {
126 argc: usize,
127 target: DirectCallTarget,
128 hits: u64,
129 misses: u64,
130 },
131}
132
133#[derive(Debug, Clone)]
134pub(crate) enum DirectCallTarget {
135 Closure(Arc<crate::value::VmClosure>),
136}
137
138impl PartialEq for DirectCallTarget {
139 fn eq(&self, other: &Self) -> bool {
140 match (self, other) {
141 (Self::Closure(left), Self::Closure(right)) => Arc::ptr_eq(left, right),
142 }
143 }
144}
145
146impl Eq for DirectCallTarget {}
147
148impl PartialEq for DirectCallState {
149 fn eq(&self, other: &Self) -> bool {
150 match (self, other) {
151 (
152 Self::Warmup {
153 argc: left_argc,
154 target: left_target,
155 hits: left_hits,
156 },
157 Self::Warmup {
158 argc: right_argc,
159 target: right_target,
160 hits: right_hits,
161 },
162 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
163 (
164 Self::Specialized {
165 argc: left_argc,
166 target: left_target,
167 hits: left_hits,
168 misses: left_misses,
169 },
170 Self::Specialized {
171 argc: right_argc,
172 target: right_target,
173 hits: right_hits,
174 misses: right_misses,
175 },
176 ) => {
177 left_argc == right_argc
178 && left_target == right_target
179 && left_hits == right_hits
180 && left_misses == right_misses
181 }
182 _ => false,
183 }
184 }
185}
186
187impl Eq for DirectCallState {}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub(crate) enum PropertyCacheTarget {
191 DictField(Arc<str>),
192 StructField { field_name: Arc<str>, index: usize },
193 HarnessSubHandle(HarnessKind),
194 ListCount,
195 ListEmpty,
196 ListFirst,
197 ListLast,
198 StringCount,
199 StringEmpty,
200 PairFirst,
201 PairSecond,
202 EnumVariant,
203 EnumFields,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub(crate) enum MethodCacheTarget {
208 Harness(HarnessKind),
209 ListCount,
210 ListEmpty,
211 ListContains,
212 StringCount,
213 StringEmpty,
214 StringContains,
215 DictCount,
216 DictHas,
217 RangeCount,
218 RangeLen,
219 RangeEmpty,
220 RangeFirst,
221 RangeLast,
222 SetCount,
223 SetLen,
224 SetEmpty,
225 SetContains,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230pub struct LocalSlotInfo {
231 pub name: String,
232 pub mutable: bool,
233 pub scope_depth: usize,
234}
235
236impl fmt::Display for Constant {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 match self {
239 Constant::Int(n) => write!(f, "{n}"),
240 Constant::Float(n) => write!(f, "{n}"),
241 Constant::String(s) => write!(f, "\"{s}\""),
242 Constant::Bool(b) => write!(f, "{b}"),
243 Constant::Nil => write!(f, "nil"),
244 Constant::Duration(ms) => write!(f, "{ms}ms"),
245 }
246 }
247}
248
249#[derive(Debug)]
251pub struct Chunk {
252 cache_id: u64,
256 pub code: Vec<u8>,
258 pub constants: Vec<Constant>,
260 pub lines: Vec<u32>,
262 pub columns: Vec<u32>,
265 pub source_file: Option<String>,
270 current_col: u32,
272 pub functions: Vec<CompiledFunctionRef>,
274 inline_cache_slots: BTreeMap<usize, usize>,
280 inline_cache_index: Vec<u32>,
288 inline_caches: Arc<Mutex<Vec<InlineCacheEntry>>>,
292 constant_strings: Arc<Mutex<Vec<Option<Arc<str>>>>>,
296 pub(crate) local_slots: Vec<LocalSlotInfo>,
298 pub(crate) references_outer_names: bool,
314 #[cfg(debug_assertions)]
328 balance_depth: i32,
329 #[cfg(debug_assertions)]
330 balance_nonlinear: u32,
331}
332
333pub type ChunkRef = Arc<Chunk>;
334pub type CompiledFunctionRef = Arc<CompiledFunction>;
335
336impl Clone for Chunk {
337 fn clone(&self) -> Self {
338 Self {
339 cache_id: self.cache_id,
340 code: self.code.clone(),
341 constants: self.constants.clone(),
342 lines: self.lines.clone(),
343 columns: self.columns.clone(),
344 source_file: self.source_file.clone(),
345 current_col: self.current_col,
346 functions: self.functions.clone(),
347 inline_cache_slots: self.inline_cache_slots.clone(),
348 inline_cache_index: self.inline_cache_index.clone(),
349 inline_caches: Arc::new(Mutex::new(vec![
350 InlineCacheEntry::Empty;
351 self.inline_cache_slot_count()
352 ])),
353 constant_strings: Arc::new(Mutex::new(vec![None; self.constants.len()])),
354 local_slots: self.local_slots.clone(),
355 references_outer_names: self.references_outer_names,
356 #[cfg(debug_assertions)]
357 balance_depth: self.balance_depth,
358 #[cfg(debug_assertions)]
359 balance_nonlinear: self.balance_nonlinear,
360 }
361 }
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct CachedChunk {
370 pub(crate) code: Vec<u8>,
371 pub(crate) constants: Vec<Constant>,
372 pub(crate) lines: Vec<u32>,
373 pub(crate) columns: Vec<u32>,
374 pub(crate) source_file: Option<String>,
375 pub(crate) current_col: u32,
376 pub(crate) functions: Vec<CachedCompiledFunction>,
377 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
378 pub(crate) local_slots: Vec<LocalSlotInfo>,
379 #[serde(default)]
380 pub(crate) references_outer_names: bool,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct CachedCompiledFunction {
385 pub(crate) name: String,
386 pub(crate) type_params: Vec<String>,
387 pub(crate) nominal_type_names: Vec<String>,
388 pub(crate) params: Vec<CachedParamSlot>,
389 pub(crate) default_start: Option<usize>,
390 pub(crate) chunk: CachedChunk,
391 pub(crate) is_generator: bool,
392 pub(crate) is_stream: bool,
393 pub(crate) has_rest_param: bool,
394 pub(crate) has_runtime_type_checks: bool,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub(crate) struct CachedParamSlot {
399 pub(crate) name: String,
400 pub(crate) type_expr: Option<TypeExpr>,
401 pub(crate) has_default: bool,
402}
403
404impl CachedParamSlot {
405 fn thaw(&self) -> ParamSlot {
406 ParamSlot {
407 name: self.name.clone(),
408 type_expr: self.type_expr.clone(),
409 runtime_guard: self
410 .type_expr
411 .as_ref()
412 .map(RuntimeParamGuard::from_type_expr),
413 has_default: self.has_default,
414 }
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ParamSlot {
425 pub name: String,
426 pub type_expr: Option<TypeExpr>,
429 #[serde(skip)]
432 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
433 pub has_default: bool,
437}
438
439impl ParamSlot {
440 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
443 Self {
444 name: param.name.clone(),
445 type_expr: param.type_expr.clone(),
446 runtime_guard: param
447 .type_expr
448 .as_ref()
449 .map(RuntimeParamGuard::from_type_expr),
450 has_default: param.default_value.is_some(),
451 }
452 }
453
454 fn freeze_for_cache(&self) -> CachedParamSlot {
455 CachedParamSlot {
456 name: self.name.clone(),
457 type_expr: self.type_expr.clone(),
458 has_default: self.has_default,
459 }
460 }
461
462 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
467 params.iter().map(Self::from_typed_param).collect()
468 }
469}
470
471#[derive(Debug, Clone)]
473pub struct CompiledFunction {
474 pub name: String,
475 pub type_params: Vec<String>,
479 pub nominal_type_names: Vec<String>,
483 pub params: Vec<ParamSlot>,
484 pub default_start: Option<usize>,
486 pub chunk: ChunkRef,
487 pub is_generator: bool,
489 pub is_stream: bool,
491 pub has_rest_param: bool,
493 pub has_runtime_type_checks: bool,
498}
499
500impl CompiledFunction {
501 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
502 params.iter().any(|param| param.type_expr.is_some())
503 }
504
505 pub fn param_names(&self) -> impl Iterator<Item = &str> {
508 self.params.iter().map(|p| p.name.as_str())
509 }
510
511 pub fn required_param_count(&self) -> usize {
513 self.default_start.unwrap_or(self.params.len())
514 }
515
516 pub fn declares_type_param(&self, name: &str) -> bool {
517 self.type_params.iter().any(|param| param == name)
518 }
519
520 pub fn has_nominal_type(&self, name: &str) -> bool {
521 self.nominal_type_names.iter().any(|ty| ty == name)
522 }
523
524 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
525 CachedCompiledFunction {
526 name: self.name.clone(),
527 type_params: self.type_params.clone(),
528 nominal_type_names: self.nominal_type_names.clone(),
529 params: self
530 .params
531 .iter()
532 .map(ParamSlot::freeze_for_cache)
533 .collect(),
534 default_start: self.default_start,
535 chunk: self.chunk.freeze_for_cache(),
536 is_generator: self.is_generator,
537 is_stream: self.is_stream,
538 has_rest_param: self.has_rest_param,
539 has_runtime_type_checks: self.has_runtime_type_checks,
540 }
541 }
542
543 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
544 Self {
545 name: cached.name.clone(),
546 type_params: cached.type_params.clone(),
547 nominal_type_names: cached.nominal_type_names.clone(),
548 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
549 default_start: cached.default_start,
550 chunk: Arc::new(Chunk::from_cached(&cached.chunk)),
551 is_generator: cached.is_generator,
552 is_stream: cached.is_stream,
553 has_rest_param: cached.has_rest_param,
554 has_runtime_type_checks: cached.has_runtime_type_checks,
555 }
556 }
557}
558
559#[cfg(debug_assertions)]
562#[derive(Clone, Copy)]
563pub(crate) struct BalanceProbe {
564 depth: i32,
565 nonlinear: u32,
566}
567
568#[cfg(debug_assertions)]
585fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
586 use Op::*;
587 let count = count as i32;
588 Some(match op {
589 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
591 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty | Pop => -1,
595 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
599 | PushScope | PopScope | PopIterator | PopHandler => 0,
600 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
602 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
603 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
604 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
605 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
606 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
607 IterInit => -1,
610 Slice | SetSubscript => -2,
612 BuildList | Concat | CallBuiltin => 1 - count,
614 BuildDict => 1 - 2 * count,
615 Call | MethodCall | MethodCallOpt => -count,
617 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
620 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
621 | SyncMutexEnter | SyncMutexEnterKeyed | TaskScopeEnter | TaskScopeExit | Import
622 | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum | MatchEnum | Yield
623 | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
624 })
625}
626
627impl Chunk {
628 pub fn new() -> Self {
629 Self {
630 cache_id: next_chunk_cache_id(),
631 code: Vec::new(),
632 constants: Vec::new(),
633 lines: Vec::new(),
634 columns: Vec::new(),
635 source_file: None,
636 current_col: 0,
637 functions: Vec::new(),
638 inline_cache_slots: BTreeMap::new(),
639 inline_cache_index: Vec::new(),
640 inline_caches: Arc::new(Mutex::new(Vec::new())),
641 constant_strings: Arc::new(Mutex::new(Vec::new())),
642 local_slots: Vec::new(),
643 references_outer_names: false,
644 #[cfg(debug_assertions)]
645 balance_depth: 0,
646 #[cfg(debug_assertions)]
647 balance_nonlinear: 0,
648 }
649 }
650
651 pub fn set_column(&mut self, col: u32) {
653 self.current_col = col;
654 }
655
656 pub fn add_constant(&mut self, constant: Constant) -> u16 {
658 for (i, c) in self.constants.iter().enumerate() {
659 if c == &constant {
660 return i as u16;
661 }
662 }
663 let idx = self.constants.len();
664 self.constants.push(constant);
665 idx as u16
666 }
667
668 pub fn emit(&mut self, op: Op, line: u32) {
670 #[cfg(debug_assertions)]
671 self.note_balance(op, 0);
672 let col = self.current_col;
673 let op_offset = self.code.len();
674 self.code.push(op as u8);
675 self.lines.push(line);
676 self.columns.push(col);
677 if is_adaptive_binary_op(op) {
678 self.register_inline_cache(op_offset);
679 }
680 if op_reads_outer_name(op) {
681 self.references_outer_names = true;
682 }
683 }
684
685 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
687 #[cfg(debug_assertions)]
688 self.note_balance(op, arg);
689 let col = self.current_col;
690 let op_offset = self.code.len();
691 self.code.push(op as u8);
692 self.code.push((arg >> 8) as u8);
693 self.code.push((arg & 0xFF) as u8);
694 self.lines.push(line);
695 self.lines.push(line);
696 self.lines.push(line);
697 self.columns.push(col);
698 self.columns.push(col);
699 self.columns.push(col);
700 if matches!(
701 op,
702 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
703 ) {
704 self.register_inline_cache(op_offset);
705 }
706 if op_reads_outer_name(op) {
707 self.references_outer_names = true;
708 }
709 }
710
711 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
713 #[cfg(debug_assertions)]
714 self.note_balance(op, arg as u16);
715 let col = self.current_col;
716 let op_offset = self.code.len();
717 self.code.push(op as u8);
718 self.code.push(arg);
719 self.lines.push(line);
720 self.lines.push(line);
721 self.columns.push(col);
722 self.columns.push(col);
723 if matches!(op, Op::Call) {
724 self.register_inline_cache(op_offset);
725 }
726 if op_reads_outer_name(op) {
727 self.references_outer_names = true;
728 }
729 }
730
731 pub fn emit_call_builtin(
733 &mut self,
734 id: crate::BuiltinId,
735 name_idx: u16,
736 arg_count: u8,
737 line: u32,
738 ) {
739 #[cfg(debug_assertions)]
740 self.note_balance(Op::CallBuiltin, arg_count as u16);
741 let col = self.current_col;
742 let op_offset = self.code.len();
743 self.code.push(Op::CallBuiltin as u8);
744 self.code.extend_from_slice(&id.raw().to_be_bytes());
745 self.code.push((name_idx >> 8) as u8);
746 self.code.push((name_idx & 0xFF) as u8);
747 self.code.push(arg_count);
748 for _ in 0..12 {
749 self.lines.push(line);
750 self.columns.push(col);
751 }
752 self.register_inline_cache(op_offset);
753 self.references_outer_names = true;
754 }
755
756 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
758 #[cfg(debug_assertions)]
759 self.note_balance(Op::CallBuiltinSpread, 0);
760 let col = self.current_col;
761 self.code.push(Op::CallBuiltinSpread as u8);
762 self.code.extend_from_slice(&id.raw().to_be_bytes());
763 self.code.push((name_idx >> 8) as u8);
764 self.code.push((name_idx & 0xFF) as u8);
765 for _ in 0..11 {
766 self.lines.push(line);
767 self.columns.push(col);
768 }
769 self.references_outer_names = true;
770 }
771
772 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
774 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
775 }
776
777 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
779 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
780 }
781
782 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
783 #[cfg(debug_assertions)]
784 self.note_balance(op, arg_count as u16);
785 let col = self.current_col;
786 let op_offset = self.code.len();
787 self.code.push(op as u8);
788 self.code.push((name_idx >> 8) as u8);
789 self.code.push((name_idx & 0xFF) as u8);
790 self.code.push(arg_count);
791 self.lines.push(line);
792 self.lines.push(line);
793 self.lines.push(line);
794 self.lines.push(line);
795 self.columns.push(col);
796 self.columns.push(col);
797 self.columns.push(col);
798 self.columns.push(col);
799 self.register_inline_cache(op_offset);
800 }
801
802 pub fn current_offset(&self) -> usize {
804 self.code.len()
805 }
806
807 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
809 #[cfg(debug_assertions)]
810 self.note_balance(op, 0);
811 let col = self.current_col;
812 self.code.push(op as u8);
813 let patch_pos = self.code.len();
814 self.code.push(0xFF);
815 self.code.push(0xFF);
816 self.lines.push(line);
817 self.lines.push(line);
818 self.lines.push(line);
819 self.columns.push(col);
820 self.columns.push(col);
821 self.columns.push(col);
822 patch_pos
823 }
824
825 pub fn patch_jump(&mut self, patch_pos: usize) {
827 let target = self.code.len() as u16;
828 self.code[patch_pos] = (target >> 8) as u8;
829 self.code[patch_pos + 1] = (target & 0xFF) as u8;
830 }
831
832 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
834 let target = target as u16;
835 self.code[patch_pos] = (target >> 8) as u8;
836 self.code[patch_pos + 1] = (target & 0xFF) as u8;
837 }
838
839 pub fn read_u16(&self, pos: usize) -> u16 {
841 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
842 }
843
844 #[cfg(debug_assertions)]
848 fn note_balance(&mut self, op: Op, count: u16) {
849 match op_stack_delta(op, count) {
850 Some(delta) => self.balance_depth += delta,
851 None => self.balance_nonlinear += 1,
852 }
853 }
854
855 #[cfg(debug_assertions)]
858 pub(crate) fn balance_probe(&self) -> BalanceProbe {
859 BalanceProbe {
860 depth: self.balance_depth,
861 nonlinear: self.balance_nonlinear,
862 }
863 }
864
865 #[cfg(debug_assertions)]
871 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
872 if self.balance_nonlinear == probe.nonlinear {
873 Some(self.balance_depth - probe.depth)
874 } else {
875 None
876 }
877 }
878
879 fn register_inline_cache(&mut self, op_offset: usize) {
880 if self.inline_cache_slots.contains_key(&op_offset) {
881 return;
882 }
883 let mut entries = self.inline_caches.lock();
884 let slot = entries.len();
885 entries.push(InlineCacheEntry::Empty);
886 self.inline_cache_slots.insert(op_offset, slot);
887 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
888 }
889
890 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
895 if op_offset >= index.len() {
896 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
897 }
898 index[op_offset] = slot as u32;
899 }
900
901 #[inline]
910 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
911 match self.inline_cache_index.get(op_offset).copied() {
912 None | Some(NO_INLINE_CACHE_SLOT) => None,
913 Some(slot) => Some(slot as usize),
914 }
915 }
916
917 pub(crate) fn inline_cache_slot_count(&self) -> usize {
918 self.inline_cache_slots.len()
919 }
920
921 pub(crate) fn cache_id(&self) -> u64 {
922 self.cache_id
923 }
924
925 #[cfg(feature = "vm-bench-internals")]
932 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
933 self.inline_cache_slots.get(&op_offset).copied()
934 }
935
936 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
941 let mut entries = self.constant_strings.lock();
946 if entries.len() < self.constants.len() {
947 entries.resize(self.constants.len(), None);
948 }
949 if let Some(Some(existing)) = entries.get(idx) {
950 return Some(Arc::clone(existing));
951 }
952 let materialized = match self.constants.get(idx)? {
953 Constant::String(s) => Arc::<str>::from(s.as_str()),
954 _ => return None,
955 };
956 entries[idx] = Some(Arc::clone(&materialized));
957 Some(materialized)
958 }
959
960 #[cfg(feature = "vm-bench-internals")]
961 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
962 self.inline_caches
963 .lock()
964 .get(slot)
965 .cloned()
966 .unwrap_or(InlineCacheEntry::Empty)
967 }
968
969 #[inline]
981 #[cfg(any(test, feature = "vm-bench-internals"))]
982 pub(crate) fn peek_adaptive_binary_cache(
983 &self,
984 slot: usize,
985 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
986 match self.inline_caches.lock().get(slot)? {
987 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
988 _ => None,
989 }
990 }
991
992 #[inline]
1004 #[cfg(any(test, feature = "vm-bench-internals"))]
1005 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1006 match self.inline_caches.lock().get(slot)? {
1007 &InlineCacheEntry::Method {
1008 name_idx,
1009 argc,
1010 target,
1011 } => Some((name_idx, argc, target)),
1012 _ => None,
1013 }
1014 }
1015
1016 #[inline]
1026 #[cfg(any(test, feature = "vm-bench-internals"))]
1027 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1028 match self.inline_caches.lock().get(slot)? {
1029 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1030 _ => None,
1031 }
1032 }
1033
1034 #[inline]
1044 #[cfg(any(test, feature = "vm-bench-internals"))]
1045 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1046 match self.inline_caches.lock().get(slot)? {
1047 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1048 _ => None,
1049 }
1050 }
1051
1052 #[cfg(any(test, feature = "vm-bench-internals"))]
1053 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1054 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1055 *existing = entry;
1056 }
1057 }
1058
1059 pub fn freeze_for_cache(&self) -> CachedChunk {
1060 CachedChunk {
1061 code: self.code.clone(),
1062 constants: self.constants.clone(),
1063 lines: self.lines.clone(),
1064 columns: self.columns.clone(),
1065 source_file: self.source_file.clone(),
1066 current_col: self.current_col,
1067 functions: self
1068 .functions
1069 .iter()
1070 .map(|function| function.freeze_for_cache())
1071 .collect(),
1072 inline_cache_slots: self.inline_cache_slots.clone(),
1073 local_slots: self.local_slots.clone(),
1074 references_outer_names: self.references_outer_names,
1075 }
1076 }
1077
1078 pub fn from_cached(cached: &CachedChunk) -> Self {
1079 let inline_cache_count = cached.inline_cache_slots.len();
1080 let constants_count = cached.constants.len();
1081 let mut inline_cache_index = Vec::new();
1088 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1089 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1090 if op_offset < inline_cache_index.len() {
1091 inline_cache_index[op_offset] = slot as u32;
1092 }
1093 }
1094 Self {
1095 cache_id: next_chunk_cache_id(),
1096 code: cached.code.clone(),
1097 constants: cached.constants.clone(),
1098 lines: cached.lines.clone(),
1099 columns: cached.columns.clone(),
1100 source_file: cached.source_file.clone(),
1101 current_col: cached.current_col,
1102 functions: cached
1103 .functions
1104 .iter()
1105 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1106 .collect(),
1107 inline_cache_slots: cached.inline_cache_slots.clone(),
1108 inline_cache_index,
1109 inline_caches: Arc::new(Mutex::new(vec![
1110 InlineCacheEntry::Empty;
1111 inline_cache_count
1112 ])),
1113 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1114 local_slots: cached.local_slots.clone(),
1115 references_outer_names: cached.references_outer_names,
1116 #[cfg(debug_assertions)]
1117 balance_depth: 0,
1118 #[cfg(debug_assertions)]
1119 balance_nonlinear: 0,
1120 }
1121 }
1122
1123 pub(crate) fn add_local_slot(
1124 &mut self,
1125 name: String,
1126 mutable: bool,
1127 scope_depth: usize,
1128 ) -> u16 {
1129 let idx = self.local_slots.len();
1130 self.local_slots.push(LocalSlotInfo {
1131 name,
1132 mutable,
1133 scope_depth,
1134 });
1135 idx as u16
1136 }
1137
1138 pub fn read_u64(&self, pos: usize) -> u64 {
1140 u64::from_be_bytes([
1141 self.code[pos],
1142 self.code[pos + 1],
1143 self.code[pos + 2],
1144 self.code[pos + 3],
1145 self.code[pos + 4],
1146 self.code[pos + 5],
1147 self.code[pos + 6],
1148 self.code[pos + 7],
1149 ])
1150 }
1151
1152 pub fn disassemble(&self, name: &str) -> String {
1156 let mut out = format!("== {name} ==\n");
1157 let mut ip = 0;
1158 while ip < self.code.len() {
1159 let op_byte = self.code[ip];
1160 let line = self.lines.get(ip).copied().unwrap_or(0);
1161 out.push_str(&format!("{ip:04} [{line:>4}] "));
1162 ip += 1;
1163
1164 if let Some(op) = Op::from_byte(op_byte) {
1165 self.disassemble_op(op, &mut ip, &mut out);
1166 } else {
1167 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1168 }
1169 }
1170 out
1171 }
1172}
1173
1174pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1185 label.to_string()
1186}
1187
1188pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1189 let arg = chunk.code[*ip];
1190 *ip += 1;
1191 format!("{label} {arg:>4}")
1192}
1193
1194pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1195 let arg = chunk.read_u16(*ip);
1196 *ip += 2;
1197 format!("{label} {arg:>4}")
1198}
1199
1200pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1201 let catch_offset = chunk.read_u16(*ip);
1202 *ip += 2;
1203 let type_idx = chunk.read_u16(*ip);
1204 *ip += 2;
1205 if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1206 format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1207 } else {
1208 format!("{label} {catch_offset:>4} type {type_idx:>4}")
1209 }
1210}
1211
1212pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1213 let idx = chunk.read_u16(*ip);
1214 *ip += 2;
1215 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1216}
1217
1218pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1219 let slot = chunk.read_u16(*ip);
1220 *ip += 2;
1221 let mut out = format!("{label} {slot:>4}");
1222 if let Some(info) = chunk.local_slots.get(slot as usize) {
1223 out.push_str(&format!(" ({})", info.name));
1224 }
1225 out
1226}
1227
1228pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1229 let idx = chunk.read_u16(*ip);
1230 *ip += 2;
1231 let argc = chunk.code[*ip];
1232 *ip += 1;
1233 format!(
1234 "{label} {idx:>4} ({}) argc={argc}",
1235 chunk.constants[idx as usize]
1236 )
1237}
1238
1239pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1240 let enum_idx = chunk.read_u16(*ip);
1241 *ip += 2;
1242 let var_idx = chunk.read_u16(*ip);
1243 *ip += 2;
1244 format!(
1245 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1246 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1247 )
1248}
1249
1250pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1251 let enum_idx = chunk.read_u16(*ip);
1252 *ip += 2;
1253 let var_idx = chunk.read_u16(*ip);
1254 *ip += 2;
1255 let field_count = chunk.read_u16(*ip);
1256 *ip += 2;
1257 format!(
1258 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1259 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1260 )
1261}
1262
1263pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1264 let path_idx = chunk.read_u16(*ip);
1265 *ip += 2;
1266 let names_idx = chunk.read_u16(*ip);
1267 *ip += 2;
1268 format!(
1269 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1270 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1271 )
1272}
1273
1274pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1275 let var_idx = chunk.read_u16(*ip);
1276 *ip += 2;
1277 let type_idx = chunk.read_u16(*ip);
1278 *ip += 2;
1279 format!(
1280 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1281 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1282 )
1283}
1284
1285pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1286 let id = chunk.read_u64(*ip);
1287 *ip += 8;
1288 let idx = chunk.read_u16(*ip);
1289 *ip += 2;
1290 let argc = chunk.code[*ip];
1291 *ip += 1;
1292 format!(
1293 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1294 chunk.constants[idx as usize],
1295 )
1296}
1297
1298pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1299 let id = chunk.read_u64(*ip);
1300 *ip += 8;
1301 let idx = chunk.read_u16(*ip);
1302 *ip += 2;
1303 format!(
1304 "{label} {id:#018x} {idx:>4} ({})",
1305 chunk.constants[idx as usize],
1306 )
1307}
1308
1309pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1310 let idx = chunk.read_u16(*ip);
1316 *ip += 2;
1317 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1318}
1319
1320impl Default for Chunk {
1321 fn default() -> Self {
1322 Self::new()
1323 }
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use std::sync::Arc;
1329
1330 use super::{
1331 Chunk, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget, Op,
1332 PropertyCacheTarget,
1333 };
1334 use crate::BuiltinId;
1335
1336 #[test]
1337 fn op_from_byte_matches_repr_order() {
1338 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1339 assert_eq!(byte as u8, op as u8);
1340 assert_eq!(Op::from_byte(byte as u8), Some(op));
1341 }
1342 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1343 assert_eq!(Op::COUNT, Op::ALL.len());
1344 }
1345
1346 #[test]
1347 fn disassemble_covers_every_opcode_variant() {
1348 for op in Op::ALL.iter().copied() {
1358 let mut chunk = Chunk::new();
1359 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1360 for _ in 0..16 {
1364 chunk.code.push(0);
1365 }
1366 let mut ip: usize = 0;
1367 let mut out = String::new();
1368 chunk.disassemble_op(op, &mut ip, &mut out);
1369 assert!(
1370 !out.contains("UNKNOWN"),
1371 "disasm emitted UNKNOWN for {op:?}: {out}",
1372 );
1373 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1374 }
1375 }
1376
1377 #[test]
1386 fn empty_chunk_does_not_reference_outer_names() {
1387 let chunk = Chunk::new();
1388 assert!(!chunk.references_outer_names);
1389 }
1390
1391 #[test]
1392 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1393 let mut chunk = Chunk::new();
1398 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1399 chunk.emit_u16(Op::Constant, 0, 1);
1400 chunk.emit(Op::MulInt, 1);
1401 chunk.emit(Op::Pop, 1);
1402 chunk.emit(Op::Return, 1);
1403 assert!(!chunk.references_outer_names);
1404 }
1405
1406 #[test]
1407 fn slot_only_chunk_does_not_reference_outer_names() {
1408 let mut chunk = Chunk::new();
1410 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1411 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1412 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1413 assert!(!chunk.references_outer_names);
1414 }
1415
1416 #[test]
1417 fn get_var_flags_outer_name_reference() {
1418 let mut chunk = Chunk::new();
1419 chunk.emit_u16(Op::GetVar, 0, 1);
1420 assert!(chunk.references_outer_names);
1421 }
1422
1423 #[test]
1424 fn set_var_flags_outer_name_reference() {
1425 let mut chunk = Chunk::new();
1426 chunk.emit_u16(Op::SetVar, 0, 1);
1427 assert!(chunk.references_outer_names);
1428 }
1429
1430 #[test]
1431 fn check_type_flags_outer_name_reference() {
1432 let mut chunk = Chunk::new();
1433 chunk.emit_u16(Op::CheckType, 0, 1);
1434 assert!(chunk.references_outer_names);
1435 }
1436
1437 #[test]
1438 fn call_builtin_flags_outer_name_reference() {
1439 let mut chunk = Chunk::new();
1440 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1441 assert!(chunk.references_outer_names);
1442 }
1443
1444 #[test]
1445 fn call_builtin_spread_flags_outer_name_reference() {
1446 let mut chunk = Chunk::new();
1447 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1448 assert!(chunk.references_outer_names);
1449 }
1450
1451 #[test]
1452 fn tail_call_flags_outer_name_reference() {
1453 let mut chunk = Chunk::new();
1456 chunk.emit_u8(Op::TailCall, 1, 1);
1457 assert!(chunk.references_outer_names);
1458 }
1459
1460 #[test]
1461 fn call_flags_outer_name_reference() {
1462 let mut chunk = Chunk::new();
1465 chunk.emit_u8(Op::Call, 1, 1);
1466 assert!(chunk.references_outer_names);
1467 }
1468
1469 #[test]
1470 fn pipe_flags_outer_name_reference() {
1471 let mut chunk = Chunk::new();
1474 chunk.emit(Op::Pipe, 1);
1475 assert!(chunk.references_outer_names);
1476 }
1477
1478 #[test]
1479 fn method_call_does_not_flag_outer_name_reference() {
1480 let mut chunk = Chunk::new();
1483 chunk.emit_method_call(0, 1, 1);
1484 chunk.emit_method_call_opt(0, 1, 1);
1485 assert!(!chunk.references_outer_names);
1486 }
1487
1488 #[test]
1489 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1490 let mut chunk = Chunk::new();
1493 chunk.emit_u16(Op::Constant, 0, 1);
1494 chunk.emit(Op::JumpIfFalse, 1);
1495 chunk.emit(Op::Jump, 1);
1496 chunk.emit(Op::Return, 1);
1497 chunk.emit(Op::Pop, 1);
1498 assert!(!chunk.references_outer_names);
1499 }
1500
1501 #[test]
1502 fn references_outer_names_is_monotonic() {
1503 let mut chunk = Chunk::new();
1506 chunk.emit_u16(Op::GetVar, 0, 1);
1507 assert!(chunk.references_outer_names);
1508 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1509 chunk.emit(Op::MulInt, 1);
1510 assert!(chunk.references_outer_names);
1511 }
1512
1513 #[test]
1514 fn freeze_thaw_round_trips_references_outer_names() {
1515 let mut chunk = Chunk::new();
1519 chunk.emit_u16(Op::GetVar, 0, 1);
1520 assert!(chunk.references_outer_names);
1521 let frozen = chunk.freeze_for_cache();
1522 let thawed = Chunk::from_cached(&frozen);
1523 assert!(thawed.references_outer_names);
1524
1525 let plain = Chunk::new();
1526 assert!(!plain.references_outer_names);
1527 let frozen_plain = plain.freeze_for_cache();
1528 let thawed_plain = Chunk::from_cached(&frozen_plain);
1529 assert!(!thawed_plain.references_outer_names);
1530 }
1531
1532 #[test]
1544 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1545 let mut chunk = Chunk::new();
1548 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1549 chunk.emit(Op::Pop, 1);
1550 chunk.emit(Op::Return, 1);
1551 assert!(chunk.inline_cache_slot(0).is_none());
1552 assert!(chunk.inline_cache_slot(3).is_none());
1553 assert!(chunk.inline_cache_slot(4).is_none());
1554 }
1555
1556 #[test]
1557 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1558 let mut chunk = Chunk::new();
1562 chunk.emit(Op::Add, 1);
1563 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1564 }
1565
1566 #[test]
1567 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1568 let mut chunk = Chunk::new();
1573 chunk.emit(Op::Add, 1);
1574 chunk.emit(Op::Sub, 1);
1575 chunk.emit(Op::Mul, 1);
1576 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1577 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1578 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1579 assert_ne!(s0, s1);
1580 assert_ne!(s1, s2);
1581 assert_ne!(s0, s2);
1582 }
1583
1584 #[test]
1585 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1586 let mut chunk = Chunk::new();
1589 chunk.emit(Op::Add, 1);
1590 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1591 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1592 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1593 }
1594
1595 #[test]
1596 fn inline_cache_slot_for_get_property_and_method_call() {
1597 let mut chunk = Chunk::new();
1601 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");
1606 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1607 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1608 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1609 }
1610
1611 #[test]
1612 fn inline_cache_slot_for_call_and_call_builtin() {
1613 let mut chunk = Chunk::new();
1618 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1620 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1621 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1622 assert!(
1623 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1624 "Op::CallBuiltin IC slot"
1625 );
1626 }
1627
1628 #[test]
1629 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1630 let mut chunk = Chunk::new();
1636 chunk.emit(Op::Add, 1);
1637 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1638 chunk.register_inline_cache(0);
1640 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1641 assert_eq!(slot_before, slot_after);
1642 }
1643
1644 #[test]
1645 fn inline_cache_index_round_trips_through_cached_chunk() {
1646 let mut chunk = Chunk::new();
1653 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1654 chunk.emit_u16(Op::Constant, 0, 1);
1655 chunk.emit(Op::Add, 1);
1656 chunk.emit(Op::Sub, 1);
1657 chunk.emit_method_call(0, 1, 1);
1658 chunk.emit_u8(Op::Call, 1, 1);
1659 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1660 .map(|o| (o, chunk.inline_cache_slot(o)))
1661 .collect();
1662 let frozen = chunk.freeze_for_cache();
1663 let thawed = Chunk::from_cached(&frozen);
1664 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1665 .map(|o| (o, thawed.inline_cache_slot(o)))
1666 .collect();
1667 assert_eq!(live_slots, thawed_slots);
1668 }
1669
1670 #[test]
1671 fn inline_cache_index_agrees_with_btreemap_view() {
1672 let mut chunk = Chunk::new();
1678 chunk.emit(Op::Add, 1);
1679 chunk.emit_u16(Op::GetVar, 0, 1);
1680 chunk.emit(Op::LessInt, 1);
1681 chunk.emit_u8(Op::Call, 2, 1);
1682 chunk.emit(Op::Equal, 1);
1683 chunk.emit_u16(Op::GetProperty, 0, 1);
1684 chunk.emit_method_call_opt(0, 0, 1);
1685 for offset in 0..chunk.code.len() {
1686 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1687 let from_index = chunk.inline_cache_slot(offset);
1688 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1689 }
1690 }
1691
1692 #[test]
1703 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1704 let mut chunk = Chunk::new();
1705 chunk.emit(Op::Add, 1);
1706 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1707 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1709 }
1710
1711 #[test]
1712 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1713 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1714 let mut chunk = Chunk::new();
1715 chunk.emit(Op::Add, 1);
1716 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1717 chunk.set_inline_cache_entry(
1718 slot,
1719 InlineCacheEntry::AdaptiveBinary {
1720 op: AdaptiveBinaryOp::Add,
1721 state: AdaptiveBinaryState::Warmup {
1722 shape: BinaryShape::Int,
1723 hits: 2,
1724 },
1725 },
1726 );
1727 let (op, state) = chunk
1728 .peek_adaptive_binary_cache(slot)
1729 .expect("warmed slot peek");
1730 assert_eq!(op, AdaptiveBinaryOp::Add);
1731 assert!(matches!(
1732 state,
1733 AdaptiveBinaryState::Warmup {
1734 shape: BinaryShape::Int,
1735 hits: 2
1736 }
1737 ));
1738 }
1739
1740 #[test]
1741 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1742 use super::{InlineCacheEntry, PropertyCacheTarget};
1748 let mut chunk = Chunk::new();
1749 chunk.emit(Op::Add, 1);
1750 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1751 chunk.set_inline_cache_entry(
1752 slot,
1753 InlineCacheEntry::Property {
1754 name_idx: 0,
1755 target: PropertyCacheTarget::ListCount,
1756 },
1757 );
1758 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1759 }
1760
1761 #[test]
1762 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1763 let chunk = Chunk::new();
1768 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1769 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1770 }
1771
1772 #[test]
1773 fn peek_adaptive_binary_state_is_copy() {
1774 fn assert_copy<T: Copy>() {}
1780 assert_copy::<super::AdaptiveBinaryState>();
1781 assert_copy::<super::AdaptiveBinaryOp>();
1782 assert_copy::<super::BinaryShape>();
1783 }
1784
1785 #[test]
1796 fn peek_method_cache_returns_none_for_empty_slot() {
1797 let mut chunk = Chunk::new();
1798 chunk.emit_method_call(0, 0, 1);
1799 let slot = chunk
1800 .inline_cache_slot(0)
1801 .expect("MethodCall registers a slot");
1802 assert!(chunk.peek_method_cache(slot).is_none());
1803 }
1804
1805 #[test]
1806 fn peek_method_cache_returns_triple_after_warmup() {
1807 let mut chunk = Chunk::new();
1808 chunk.emit_method_call(7, 2, 1);
1809 let slot = chunk
1810 .inline_cache_slot(0)
1811 .expect("MethodCall registers a slot");
1812 chunk.set_inline_cache_entry(
1813 slot,
1814 InlineCacheEntry::Method {
1815 name_idx: 7,
1816 argc: 2,
1817 target: MethodCacheTarget::ListContains,
1818 },
1819 );
1820 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1821 assert_eq!(name_idx, 7);
1822 assert_eq!(argc, 2);
1823 assert_eq!(target, MethodCacheTarget::ListContains);
1824 }
1825
1826 #[test]
1827 fn peek_method_cache_returns_none_for_non_method_variants() {
1828 let mut chunk = Chunk::new();
1832 chunk.emit_method_call(0, 0, 1);
1833 let slot = chunk
1834 .inline_cache_slot(0)
1835 .expect("MethodCall registers a slot");
1836
1837 chunk.set_inline_cache_entry(
1838 slot,
1839 InlineCacheEntry::Property {
1840 name_idx: 0,
1841 target: PropertyCacheTarget::ListCount,
1842 },
1843 );
1844 assert!(chunk.peek_method_cache(slot).is_none());
1845 }
1846
1847 #[test]
1848 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1849 let chunk = Chunk::new();
1850 assert!(chunk.peek_method_cache(0).is_none());
1851 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1852 }
1853
1854 #[test]
1855 fn peek_method_cache_target_is_copy() {
1856 fn assert_copy<T: Copy>() {}
1862 assert_copy::<super::MethodCacheTarget>();
1863 }
1864
1865 #[test]
1875 fn peek_property_cache_returns_none_for_empty_slot() {
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 assert!(chunk.peek_property_cache(slot).is_none());
1882 }
1883
1884 #[test]
1885 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1886 let mut chunk = Chunk::new();
1887 chunk.emit_u16(Op::GetProperty, 0, 1);
1888 let slot = chunk
1889 .inline_cache_slot(0)
1890 .expect("GetProperty registers a slot");
1891 chunk.set_inline_cache_entry(
1892 slot,
1893 InlineCacheEntry::Property {
1894 name_idx: 11,
1895 target: PropertyCacheTarget::DictField(Arc::from("count")),
1896 },
1897 );
1898 let (name_idx, target) = chunk
1899 .peek_property_cache(slot)
1900 .expect("warmed property slot peek");
1901 assert_eq!(name_idx, 11);
1902 match target {
1903 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1904 other => panic!("expected DictField, got {other:?}"),
1905 }
1906 }
1907
1908 #[test]
1909 fn peek_property_cache_returns_pair_for_unit_target() {
1910 let mut chunk = Chunk::new();
1914 chunk.emit_u16(Op::GetProperty, 0, 1);
1915 let slot = chunk
1916 .inline_cache_slot(0)
1917 .expect("GetProperty registers a slot");
1918 chunk.set_inline_cache_entry(
1919 slot,
1920 InlineCacheEntry::Property {
1921 name_idx: 3,
1922 target: PropertyCacheTarget::ListCount,
1923 },
1924 );
1925 let (name_idx, target) = chunk
1926 .peek_property_cache(slot)
1927 .expect("warmed property slot peek");
1928 assert_eq!(name_idx, 3);
1929 assert_eq!(target, PropertyCacheTarget::ListCount);
1930 }
1931
1932 #[test]
1933 fn peek_property_cache_returns_none_for_non_property_variants() {
1934 let mut chunk = Chunk::new();
1935 chunk.emit_u16(Op::GetProperty, 0, 1);
1936 let slot = chunk
1937 .inline_cache_slot(0)
1938 .expect("GetProperty registers a slot");
1939 chunk.set_inline_cache_entry(
1940 slot,
1941 InlineCacheEntry::Method {
1942 name_idx: 0,
1943 argc: 0,
1944 target: MethodCacheTarget::ListCount,
1945 },
1946 );
1947 assert!(chunk.peek_property_cache(slot).is_none());
1948 }
1949
1950 #[test]
1951 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1952 let chunk = Chunk::new();
1953 assert!(chunk.peek_property_cache(0).is_none());
1954 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1955 }
1956
1957 #[test]
1967 fn peek_direct_call_state_returns_none_for_empty_slot() {
1968 let mut chunk = Chunk::new();
1969 chunk.emit_u8(Op::Call, 0, 1);
1970 let slot = chunk
1971 .inline_cache_slot(0)
1972 .expect("Op::Call registers a slot");
1973 assert!(chunk.peek_direct_call_state(slot).is_none());
1974 }
1975
1976 #[test]
1977 fn peek_direct_call_state_returns_warmup_state() {
1978 let mut chunk = Chunk::new();
1979 chunk.emit_u8(Op::Call, 0, 1);
1980 let slot = chunk
1981 .inline_cache_slot(0)
1982 .expect("Op::Call registers a slot");
1983 let target = synthetic_direct_call_target();
1984 chunk.set_inline_cache_entry(
1985 slot,
1986 InlineCacheEntry::DirectCall {
1987 state: DirectCallState::Warmup {
1988 argc: 2,
1989 target: target.clone(),
1990 hits: 1,
1991 },
1992 },
1993 );
1994 let state = chunk
1995 .peek_direct_call_state(slot)
1996 .expect("warmed direct-call slot peek");
1997 match state {
1998 DirectCallState::Warmup {
1999 argc,
2000 target: peeked_target,
2001 hits,
2002 } => {
2003 assert_eq!(argc, 2);
2004 assert_eq!(hits, 1);
2005 assert_eq!(peeked_target, target);
2006 }
2007 other => panic!("expected Warmup, got {other:?}"),
2008 }
2009 }
2010
2011 #[test]
2012 fn peek_direct_call_state_returns_specialized_state() {
2013 let mut chunk = Chunk::new();
2014 chunk.emit_u8(Op::Call, 0, 1);
2015 let slot = chunk
2016 .inline_cache_slot(0)
2017 .expect("Op::Call registers a slot");
2018 let target = synthetic_direct_call_target();
2019 chunk.set_inline_cache_entry(
2020 slot,
2021 InlineCacheEntry::DirectCall {
2022 state: DirectCallState::Specialized {
2023 argc: 3,
2024 target: target.clone(),
2025 hits: 100,
2026 misses: 0,
2027 },
2028 },
2029 );
2030 let state = chunk
2031 .peek_direct_call_state(slot)
2032 .expect("warmed direct-call slot peek");
2033 match state {
2034 DirectCallState::Specialized {
2035 argc,
2036 target: peeked_target,
2037 hits,
2038 misses,
2039 } => {
2040 assert_eq!(argc, 3);
2041 assert_eq!(hits, 100);
2042 assert_eq!(misses, 0);
2043 assert_eq!(peeked_target, target);
2044 }
2045 other => panic!("expected Specialized, got {other:?}"),
2046 }
2047 }
2048
2049 #[test]
2050 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2051 let mut chunk = Chunk::new();
2052 chunk.emit_u8(Op::Call, 0, 1);
2053 let slot = chunk
2054 .inline_cache_slot(0)
2055 .expect("Op::Call registers a slot");
2056
2057 chunk.set_inline_cache_entry(
2058 slot,
2059 InlineCacheEntry::Property {
2060 name_idx: 0,
2061 target: PropertyCacheTarget::ListCount,
2062 },
2063 );
2064 assert!(chunk.peek_direct_call_state(slot).is_none());
2065 }
2066
2067 #[test]
2068 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2069 let chunk = Chunk::new();
2070 assert!(chunk.peek_direct_call_state(0).is_none());
2071 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2072 }
2073
2074 fn synthetic_direct_call_target() -> DirectCallTarget {
2078 use crate::value::VmClosure;
2079 use crate::{CompiledFunction, VmEnv};
2080 let func = CompiledFunction {
2081 name: "synthetic".to_string(),
2082 type_params: Vec::new(),
2083 nominal_type_names: Vec::new(),
2084 params: Vec::new(),
2085 default_start: None,
2086 chunk: Arc::new(Chunk::new()),
2087 is_generator: false,
2088 is_stream: false,
2089 has_rest_param: false,
2090 has_runtime_type_checks: false,
2091 };
2092 DirectCallTarget::Closure(Arc::new(VmClosure {
2093 func: Arc::new(func),
2094 env: VmEnv::new(),
2095 source_dir: None,
2096 module_functions: None,
2097 module_state: None,
2098 }))
2099 }
2100}