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 | Import | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum
622 | MatchEnum | Yield | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
623 })
624}
625
626impl Chunk {
627 pub fn new() -> Self {
628 Self {
629 cache_id: next_chunk_cache_id(),
630 code: Vec::new(),
631 constants: Vec::new(),
632 lines: Vec::new(),
633 columns: Vec::new(),
634 source_file: None,
635 current_col: 0,
636 functions: Vec::new(),
637 inline_cache_slots: BTreeMap::new(),
638 inline_cache_index: Vec::new(),
639 inline_caches: Arc::new(Mutex::new(Vec::new())),
640 constant_strings: Arc::new(Mutex::new(Vec::new())),
641 local_slots: Vec::new(),
642 references_outer_names: false,
643 #[cfg(debug_assertions)]
644 balance_depth: 0,
645 #[cfg(debug_assertions)]
646 balance_nonlinear: 0,
647 }
648 }
649
650 pub fn set_column(&mut self, col: u32) {
652 self.current_col = col;
653 }
654
655 pub fn add_constant(&mut self, constant: Constant) -> u16 {
657 for (i, c) in self.constants.iter().enumerate() {
658 if c == &constant {
659 return i as u16;
660 }
661 }
662 let idx = self.constants.len();
663 self.constants.push(constant);
664 idx as u16
665 }
666
667 pub fn emit(&mut self, op: Op, line: u32) {
669 #[cfg(debug_assertions)]
670 self.note_balance(op, 0);
671 let col = self.current_col;
672 let op_offset = self.code.len();
673 self.code.push(op as u8);
674 self.lines.push(line);
675 self.columns.push(col);
676 if is_adaptive_binary_op(op) {
677 self.register_inline_cache(op_offset);
678 }
679 if op_reads_outer_name(op) {
680 self.references_outer_names = true;
681 }
682 }
683
684 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
686 #[cfg(debug_assertions)]
687 self.note_balance(op, arg);
688 let col = self.current_col;
689 let op_offset = self.code.len();
690 self.code.push(op as u8);
691 self.code.push((arg >> 8) as u8);
692 self.code.push((arg & 0xFF) as u8);
693 self.lines.push(line);
694 self.lines.push(line);
695 self.lines.push(line);
696 self.columns.push(col);
697 self.columns.push(col);
698 self.columns.push(col);
699 if matches!(
700 op,
701 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
702 ) {
703 self.register_inline_cache(op_offset);
704 }
705 if op_reads_outer_name(op) {
706 self.references_outer_names = true;
707 }
708 }
709
710 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
712 #[cfg(debug_assertions)]
713 self.note_balance(op, arg as u16);
714 let col = self.current_col;
715 let op_offset = self.code.len();
716 self.code.push(op as u8);
717 self.code.push(arg);
718 self.lines.push(line);
719 self.lines.push(line);
720 self.columns.push(col);
721 self.columns.push(col);
722 if matches!(op, Op::Call) {
723 self.register_inline_cache(op_offset);
724 }
725 if op_reads_outer_name(op) {
726 self.references_outer_names = true;
727 }
728 }
729
730 pub fn emit_call_builtin(
732 &mut self,
733 id: crate::BuiltinId,
734 name_idx: u16,
735 arg_count: u8,
736 line: u32,
737 ) {
738 #[cfg(debug_assertions)]
739 self.note_balance(Op::CallBuiltin, arg_count as u16);
740 let col = self.current_col;
741 let op_offset = self.code.len();
742 self.code.push(Op::CallBuiltin as u8);
743 self.code.extend_from_slice(&id.raw().to_be_bytes());
744 self.code.push((name_idx >> 8) as u8);
745 self.code.push((name_idx & 0xFF) as u8);
746 self.code.push(arg_count);
747 for _ in 0..12 {
748 self.lines.push(line);
749 self.columns.push(col);
750 }
751 self.register_inline_cache(op_offset);
752 self.references_outer_names = true;
753 }
754
755 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
757 #[cfg(debug_assertions)]
758 self.note_balance(Op::CallBuiltinSpread, 0);
759 let col = self.current_col;
760 self.code.push(Op::CallBuiltinSpread as u8);
761 self.code.extend_from_slice(&id.raw().to_be_bytes());
762 self.code.push((name_idx >> 8) as u8);
763 self.code.push((name_idx & 0xFF) as u8);
764 for _ in 0..11 {
765 self.lines.push(line);
766 self.columns.push(col);
767 }
768 self.references_outer_names = true;
769 }
770
771 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
773 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
774 }
775
776 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
778 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
779 }
780
781 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
782 #[cfg(debug_assertions)]
783 self.note_balance(op, arg_count as u16);
784 let col = self.current_col;
785 let op_offset = self.code.len();
786 self.code.push(op as u8);
787 self.code.push((name_idx >> 8) as u8);
788 self.code.push((name_idx & 0xFF) as u8);
789 self.code.push(arg_count);
790 self.lines.push(line);
791 self.lines.push(line);
792 self.lines.push(line);
793 self.lines.push(line);
794 self.columns.push(col);
795 self.columns.push(col);
796 self.columns.push(col);
797 self.columns.push(col);
798 self.register_inline_cache(op_offset);
799 }
800
801 pub fn current_offset(&self) -> usize {
803 self.code.len()
804 }
805
806 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
808 #[cfg(debug_assertions)]
809 self.note_balance(op, 0);
810 let col = self.current_col;
811 self.code.push(op as u8);
812 let patch_pos = self.code.len();
813 self.code.push(0xFF);
814 self.code.push(0xFF);
815 self.lines.push(line);
816 self.lines.push(line);
817 self.lines.push(line);
818 self.columns.push(col);
819 self.columns.push(col);
820 self.columns.push(col);
821 patch_pos
822 }
823
824 pub fn patch_jump(&mut self, patch_pos: usize) {
826 let target = self.code.len() as u16;
827 self.code[patch_pos] = (target >> 8) as u8;
828 self.code[patch_pos + 1] = (target & 0xFF) as u8;
829 }
830
831 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
833 let target = target as u16;
834 self.code[patch_pos] = (target >> 8) as u8;
835 self.code[patch_pos + 1] = (target & 0xFF) as u8;
836 }
837
838 pub fn read_u16(&self, pos: usize) -> u16 {
840 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
841 }
842
843 #[cfg(debug_assertions)]
847 fn note_balance(&mut self, op: Op, count: u16) {
848 match op_stack_delta(op, count) {
849 Some(delta) => self.balance_depth += delta,
850 None => self.balance_nonlinear += 1,
851 }
852 }
853
854 #[cfg(debug_assertions)]
857 pub(crate) fn balance_probe(&self) -> BalanceProbe {
858 BalanceProbe {
859 depth: self.balance_depth,
860 nonlinear: self.balance_nonlinear,
861 }
862 }
863
864 #[cfg(debug_assertions)]
870 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
871 if self.balance_nonlinear == probe.nonlinear {
872 Some(self.balance_depth - probe.depth)
873 } else {
874 None
875 }
876 }
877
878 fn register_inline_cache(&mut self, op_offset: usize) {
879 if self.inline_cache_slots.contains_key(&op_offset) {
880 return;
881 }
882 let mut entries = self.inline_caches.lock();
883 let slot = entries.len();
884 entries.push(InlineCacheEntry::Empty);
885 self.inline_cache_slots.insert(op_offset, slot);
886 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
887 }
888
889 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
894 if op_offset >= index.len() {
895 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
896 }
897 index[op_offset] = slot as u32;
898 }
899
900 #[inline]
909 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
910 match self.inline_cache_index.get(op_offset).copied() {
911 None | Some(NO_INLINE_CACHE_SLOT) => None,
912 Some(slot) => Some(slot as usize),
913 }
914 }
915
916 pub(crate) fn inline_cache_slot_count(&self) -> usize {
917 self.inline_cache_slots.len()
918 }
919
920 pub(crate) fn cache_id(&self) -> u64 {
921 self.cache_id
922 }
923
924 #[cfg(feature = "vm-bench-internals")]
931 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
932 self.inline_cache_slots.get(&op_offset).copied()
933 }
934
935 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
940 let mut entries = self.constant_strings.lock();
945 if entries.len() < self.constants.len() {
946 entries.resize(self.constants.len(), None);
947 }
948 if let Some(Some(existing)) = entries.get(idx) {
949 return Some(Arc::clone(existing));
950 }
951 let materialized = match self.constants.get(idx)? {
952 Constant::String(s) => Arc::<str>::from(s.as_str()),
953 _ => return None,
954 };
955 entries[idx] = Some(Arc::clone(&materialized));
956 Some(materialized)
957 }
958
959 #[cfg(feature = "vm-bench-internals")]
960 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
961 self.inline_caches
962 .lock()
963 .get(slot)
964 .cloned()
965 .unwrap_or(InlineCacheEntry::Empty)
966 }
967
968 #[inline]
980 #[cfg(any(test, feature = "vm-bench-internals"))]
981 pub(crate) fn peek_adaptive_binary_cache(
982 &self,
983 slot: usize,
984 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
985 match self.inline_caches.lock().get(slot)? {
986 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
987 _ => None,
988 }
989 }
990
991 #[inline]
1003 #[cfg(any(test, feature = "vm-bench-internals"))]
1004 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1005 match self.inline_caches.lock().get(slot)? {
1006 &InlineCacheEntry::Method {
1007 name_idx,
1008 argc,
1009 target,
1010 } => Some((name_idx, argc, target)),
1011 _ => None,
1012 }
1013 }
1014
1015 #[inline]
1025 #[cfg(any(test, feature = "vm-bench-internals"))]
1026 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1027 match self.inline_caches.lock().get(slot)? {
1028 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1029 _ => None,
1030 }
1031 }
1032
1033 #[inline]
1043 #[cfg(any(test, feature = "vm-bench-internals"))]
1044 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1045 match self.inline_caches.lock().get(slot)? {
1046 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1047 _ => None,
1048 }
1049 }
1050
1051 #[cfg(any(test, feature = "vm-bench-internals"))]
1052 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1053 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1054 *existing = entry;
1055 }
1056 }
1057
1058 pub fn freeze_for_cache(&self) -> CachedChunk {
1059 CachedChunk {
1060 code: self.code.clone(),
1061 constants: self.constants.clone(),
1062 lines: self.lines.clone(),
1063 columns: self.columns.clone(),
1064 source_file: self.source_file.clone(),
1065 current_col: self.current_col,
1066 functions: self
1067 .functions
1068 .iter()
1069 .map(|function| function.freeze_for_cache())
1070 .collect(),
1071 inline_cache_slots: self.inline_cache_slots.clone(),
1072 local_slots: self.local_slots.clone(),
1073 references_outer_names: self.references_outer_names,
1074 }
1075 }
1076
1077 pub fn from_cached(cached: &CachedChunk) -> Self {
1078 let inline_cache_count = cached.inline_cache_slots.len();
1079 let constants_count = cached.constants.len();
1080 let mut inline_cache_index = Vec::new();
1087 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1088 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1089 if op_offset < inline_cache_index.len() {
1090 inline_cache_index[op_offset] = slot as u32;
1091 }
1092 }
1093 Self {
1094 cache_id: next_chunk_cache_id(),
1095 code: cached.code.clone(),
1096 constants: cached.constants.clone(),
1097 lines: cached.lines.clone(),
1098 columns: cached.columns.clone(),
1099 source_file: cached.source_file.clone(),
1100 current_col: cached.current_col,
1101 functions: cached
1102 .functions
1103 .iter()
1104 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1105 .collect(),
1106 inline_cache_slots: cached.inline_cache_slots.clone(),
1107 inline_cache_index,
1108 inline_caches: Arc::new(Mutex::new(vec![
1109 InlineCacheEntry::Empty;
1110 inline_cache_count
1111 ])),
1112 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1113 local_slots: cached.local_slots.clone(),
1114 references_outer_names: cached.references_outer_names,
1115 #[cfg(debug_assertions)]
1116 balance_depth: 0,
1117 #[cfg(debug_assertions)]
1118 balance_nonlinear: 0,
1119 }
1120 }
1121
1122 pub(crate) fn add_local_slot(
1123 &mut self,
1124 name: String,
1125 mutable: bool,
1126 scope_depth: usize,
1127 ) -> u16 {
1128 let idx = self.local_slots.len();
1129 self.local_slots.push(LocalSlotInfo {
1130 name,
1131 mutable,
1132 scope_depth,
1133 });
1134 idx as u16
1135 }
1136
1137 pub fn read_u64(&self, pos: usize) -> u64 {
1139 u64::from_be_bytes([
1140 self.code[pos],
1141 self.code[pos + 1],
1142 self.code[pos + 2],
1143 self.code[pos + 3],
1144 self.code[pos + 4],
1145 self.code[pos + 5],
1146 self.code[pos + 6],
1147 self.code[pos + 7],
1148 ])
1149 }
1150
1151 pub fn disassemble(&self, name: &str) -> String {
1155 let mut out = format!("== {name} ==\n");
1156 let mut ip = 0;
1157 while ip < self.code.len() {
1158 let op_byte = self.code[ip];
1159 let line = self.lines.get(ip).copied().unwrap_or(0);
1160 out.push_str(&format!("{ip:04} [{line:>4}] "));
1161 ip += 1;
1162
1163 if let Some(op) = Op::from_byte(op_byte) {
1164 self.disassemble_op(op, &mut ip, &mut out);
1165 } else {
1166 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1167 }
1168 }
1169 out
1170 }
1171}
1172
1173pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1184 label.to_string()
1185}
1186
1187pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1188 let arg = chunk.code[*ip];
1189 *ip += 1;
1190 format!("{label} {arg:>4}")
1191}
1192
1193pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1194 let arg = chunk.read_u16(*ip);
1195 *ip += 2;
1196 format!("{label} {arg:>4}")
1197}
1198
1199pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1200 let idx = chunk.read_u16(*ip);
1201 *ip += 2;
1202 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1203}
1204
1205pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1206 let slot = chunk.read_u16(*ip);
1207 *ip += 2;
1208 let mut out = format!("{label} {slot:>4}");
1209 if let Some(info) = chunk.local_slots.get(slot as usize) {
1210 out.push_str(&format!(" ({})", info.name));
1211 }
1212 out
1213}
1214
1215pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1216 let idx = chunk.read_u16(*ip);
1217 *ip += 2;
1218 let argc = chunk.code[*ip];
1219 *ip += 1;
1220 format!(
1221 "{label} {idx:>4} ({}) argc={argc}",
1222 chunk.constants[idx as usize]
1223 )
1224}
1225
1226pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1227 let enum_idx = chunk.read_u16(*ip);
1228 *ip += 2;
1229 let var_idx = chunk.read_u16(*ip);
1230 *ip += 2;
1231 format!(
1232 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1233 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1234 )
1235}
1236
1237pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1238 let enum_idx = chunk.read_u16(*ip);
1239 *ip += 2;
1240 let var_idx = chunk.read_u16(*ip);
1241 *ip += 2;
1242 let field_count = chunk.read_u16(*ip);
1243 *ip += 2;
1244 format!(
1245 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1246 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1247 )
1248}
1249
1250pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1251 let path_idx = chunk.read_u16(*ip);
1252 *ip += 2;
1253 let names_idx = chunk.read_u16(*ip);
1254 *ip += 2;
1255 format!(
1256 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1257 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1258 )
1259}
1260
1261pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1262 let var_idx = chunk.read_u16(*ip);
1263 *ip += 2;
1264 let type_idx = chunk.read_u16(*ip);
1265 *ip += 2;
1266 format!(
1267 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1268 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1269 )
1270}
1271
1272pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1273 let id = chunk.read_u64(*ip);
1274 *ip += 8;
1275 let idx = chunk.read_u16(*ip);
1276 *ip += 2;
1277 let argc = chunk.code[*ip];
1278 *ip += 1;
1279 format!(
1280 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1281 chunk.constants[idx as usize],
1282 )
1283}
1284
1285pub(crate) fn disasm_call_builtin_spread(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 format!(
1291 "{label} {id:#018x} {idx:>4} ({})",
1292 chunk.constants[idx as usize],
1293 )
1294}
1295
1296pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1297 let idx = chunk.read_u16(*ip);
1303 *ip += 2;
1304 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1305}
1306
1307impl Default for Chunk {
1308 fn default() -> Self {
1309 Self::new()
1310 }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315 use std::sync::Arc;
1316
1317 use super::{
1318 Chunk, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget, Op,
1319 PropertyCacheTarget,
1320 };
1321 use crate::BuiltinId;
1322
1323 #[test]
1324 fn op_from_byte_matches_repr_order() {
1325 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1326 assert_eq!(byte as u8, op as u8);
1327 assert_eq!(Op::from_byte(byte as u8), Some(op));
1328 }
1329 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1330 assert_eq!(Op::COUNT, Op::ALL.len());
1331 }
1332
1333 #[test]
1334 fn disassemble_covers_every_opcode_variant() {
1335 for op in Op::ALL.iter().copied() {
1345 let mut chunk = Chunk::new();
1346 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1347 for _ in 0..16 {
1351 chunk.code.push(0);
1352 }
1353 let mut ip: usize = 0;
1354 let mut out = String::new();
1355 chunk.disassemble_op(op, &mut ip, &mut out);
1356 assert!(
1357 !out.contains("UNKNOWN"),
1358 "disasm emitted UNKNOWN for {op:?}: {out}",
1359 );
1360 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1361 }
1362 }
1363
1364 #[test]
1373 fn empty_chunk_does_not_reference_outer_names() {
1374 let chunk = Chunk::new();
1375 assert!(!chunk.references_outer_names);
1376 }
1377
1378 #[test]
1379 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1380 let mut chunk = Chunk::new();
1385 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1386 chunk.emit_u16(Op::Constant, 0, 1);
1387 chunk.emit(Op::MulInt, 1);
1388 chunk.emit(Op::Pop, 1);
1389 chunk.emit(Op::Return, 1);
1390 assert!(!chunk.references_outer_names);
1391 }
1392
1393 #[test]
1394 fn slot_only_chunk_does_not_reference_outer_names() {
1395 let mut chunk = Chunk::new();
1397 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1398 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1399 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1400 assert!(!chunk.references_outer_names);
1401 }
1402
1403 #[test]
1404 fn get_var_flags_outer_name_reference() {
1405 let mut chunk = Chunk::new();
1406 chunk.emit_u16(Op::GetVar, 0, 1);
1407 assert!(chunk.references_outer_names);
1408 }
1409
1410 #[test]
1411 fn set_var_flags_outer_name_reference() {
1412 let mut chunk = Chunk::new();
1413 chunk.emit_u16(Op::SetVar, 0, 1);
1414 assert!(chunk.references_outer_names);
1415 }
1416
1417 #[test]
1418 fn check_type_flags_outer_name_reference() {
1419 let mut chunk = Chunk::new();
1420 chunk.emit_u16(Op::CheckType, 0, 1);
1421 assert!(chunk.references_outer_names);
1422 }
1423
1424 #[test]
1425 fn call_builtin_flags_outer_name_reference() {
1426 let mut chunk = Chunk::new();
1427 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1428 assert!(chunk.references_outer_names);
1429 }
1430
1431 #[test]
1432 fn call_builtin_spread_flags_outer_name_reference() {
1433 let mut chunk = Chunk::new();
1434 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1435 assert!(chunk.references_outer_names);
1436 }
1437
1438 #[test]
1439 fn tail_call_flags_outer_name_reference() {
1440 let mut chunk = Chunk::new();
1443 chunk.emit_u8(Op::TailCall, 1, 1);
1444 assert!(chunk.references_outer_names);
1445 }
1446
1447 #[test]
1448 fn call_flags_outer_name_reference() {
1449 let mut chunk = Chunk::new();
1452 chunk.emit_u8(Op::Call, 1, 1);
1453 assert!(chunk.references_outer_names);
1454 }
1455
1456 #[test]
1457 fn pipe_flags_outer_name_reference() {
1458 let mut chunk = Chunk::new();
1461 chunk.emit(Op::Pipe, 1);
1462 assert!(chunk.references_outer_names);
1463 }
1464
1465 #[test]
1466 fn method_call_does_not_flag_outer_name_reference() {
1467 let mut chunk = Chunk::new();
1470 chunk.emit_method_call(0, 1, 1);
1471 chunk.emit_method_call_opt(0, 1, 1);
1472 assert!(!chunk.references_outer_names);
1473 }
1474
1475 #[test]
1476 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1477 let mut chunk = Chunk::new();
1480 chunk.emit_u16(Op::Constant, 0, 1);
1481 chunk.emit(Op::JumpIfFalse, 1);
1482 chunk.emit(Op::Jump, 1);
1483 chunk.emit(Op::Return, 1);
1484 chunk.emit(Op::Pop, 1);
1485 assert!(!chunk.references_outer_names);
1486 }
1487
1488 #[test]
1489 fn references_outer_names_is_monotonic() {
1490 let mut chunk = Chunk::new();
1493 chunk.emit_u16(Op::GetVar, 0, 1);
1494 assert!(chunk.references_outer_names);
1495 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1496 chunk.emit(Op::MulInt, 1);
1497 assert!(chunk.references_outer_names);
1498 }
1499
1500 #[test]
1501 fn freeze_thaw_round_trips_references_outer_names() {
1502 let mut chunk = Chunk::new();
1506 chunk.emit_u16(Op::GetVar, 0, 1);
1507 assert!(chunk.references_outer_names);
1508 let frozen = chunk.freeze_for_cache();
1509 let thawed = Chunk::from_cached(&frozen);
1510 assert!(thawed.references_outer_names);
1511
1512 let plain = Chunk::new();
1513 assert!(!plain.references_outer_names);
1514 let frozen_plain = plain.freeze_for_cache();
1515 let thawed_plain = Chunk::from_cached(&frozen_plain);
1516 assert!(!thawed_plain.references_outer_names);
1517 }
1518
1519 #[test]
1531 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1532 let mut chunk = Chunk::new();
1535 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1536 chunk.emit(Op::Pop, 1);
1537 chunk.emit(Op::Return, 1);
1538 assert!(chunk.inline_cache_slot(0).is_none());
1539 assert!(chunk.inline_cache_slot(3).is_none());
1540 assert!(chunk.inline_cache_slot(4).is_none());
1541 }
1542
1543 #[test]
1544 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1545 let mut chunk = Chunk::new();
1549 chunk.emit(Op::Add, 1);
1550 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1551 }
1552
1553 #[test]
1554 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1555 let mut chunk = Chunk::new();
1560 chunk.emit(Op::Add, 1);
1561 chunk.emit(Op::Sub, 1);
1562 chunk.emit(Op::Mul, 1);
1563 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1564 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1565 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1566 assert_ne!(s0, s1);
1567 assert_ne!(s1, s2);
1568 assert_ne!(s0, s2);
1569 }
1570
1571 #[test]
1572 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1573 let mut chunk = Chunk::new();
1576 chunk.emit(Op::Add, 1);
1577 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1578 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1579 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1580 }
1581
1582 #[test]
1583 fn inline_cache_slot_for_get_property_and_method_call() {
1584 let mut chunk = Chunk::new();
1588 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");
1593 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1594 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1595 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1596 }
1597
1598 #[test]
1599 fn inline_cache_slot_for_call_and_call_builtin() {
1600 let mut chunk = Chunk::new();
1605 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1607 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1608 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1609 assert!(
1610 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1611 "Op::CallBuiltin IC slot"
1612 );
1613 }
1614
1615 #[test]
1616 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1617 let mut chunk = Chunk::new();
1623 chunk.emit(Op::Add, 1);
1624 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1625 chunk.register_inline_cache(0);
1627 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1628 assert_eq!(slot_before, slot_after);
1629 }
1630
1631 #[test]
1632 fn inline_cache_index_round_trips_through_cached_chunk() {
1633 let mut chunk = Chunk::new();
1640 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1641 chunk.emit_u16(Op::Constant, 0, 1);
1642 chunk.emit(Op::Add, 1);
1643 chunk.emit(Op::Sub, 1);
1644 chunk.emit_method_call(0, 1, 1);
1645 chunk.emit_u8(Op::Call, 1, 1);
1646 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1647 .map(|o| (o, chunk.inline_cache_slot(o)))
1648 .collect();
1649 let frozen = chunk.freeze_for_cache();
1650 let thawed = Chunk::from_cached(&frozen);
1651 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1652 .map(|o| (o, thawed.inline_cache_slot(o)))
1653 .collect();
1654 assert_eq!(live_slots, thawed_slots);
1655 }
1656
1657 #[test]
1658 fn inline_cache_index_agrees_with_btreemap_view() {
1659 let mut chunk = Chunk::new();
1665 chunk.emit(Op::Add, 1);
1666 chunk.emit_u16(Op::GetVar, 0, 1);
1667 chunk.emit(Op::LessInt, 1);
1668 chunk.emit_u8(Op::Call, 2, 1);
1669 chunk.emit(Op::Equal, 1);
1670 chunk.emit_u16(Op::GetProperty, 0, 1);
1671 chunk.emit_method_call_opt(0, 0, 1);
1672 for offset in 0..chunk.code.len() {
1673 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1674 let from_index = chunk.inline_cache_slot(offset);
1675 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1676 }
1677 }
1678
1679 #[test]
1690 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1691 let mut chunk = Chunk::new();
1692 chunk.emit(Op::Add, 1);
1693 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1694 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1696 }
1697
1698 #[test]
1699 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1700 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1701 let mut chunk = Chunk::new();
1702 chunk.emit(Op::Add, 1);
1703 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1704 chunk.set_inline_cache_entry(
1705 slot,
1706 InlineCacheEntry::AdaptiveBinary {
1707 op: AdaptiveBinaryOp::Add,
1708 state: AdaptiveBinaryState::Warmup {
1709 shape: BinaryShape::Int,
1710 hits: 2,
1711 },
1712 },
1713 );
1714 let (op, state) = chunk
1715 .peek_adaptive_binary_cache(slot)
1716 .expect("warmed slot peek");
1717 assert_eq!(op, AdaptiveBinaryOp::Add);
1718 assert!(matches!(
1719 state,
1720 AdaptiveBinaryState::Warmup {
1721 shape: BinaryShape::Int,
1722 hits: 2
1723 }
1724 ));
1725 }
1726
1727 #[test]
1728 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1729 use super::{InlineCacheEntry, PropertyCacheTarget};
1735 let mut chunk = Chunk::new();
1736 chunk.emit(Op::Add, 1);
1737 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1738 chunk.set_inline_cache_entry(
1739 slot,
1740 InlineCacheEntry::Property {
1741 name_idx: 0,
1742 target: PropertyCacheTarget::ListCount,
1743 },
1744 );
1745 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1746 }
1747
1748 #[test]
1749 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1750 let chunk = Chunk::new();
1755 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1756 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1757 }
1758
1759 #[test]
1760 fn peek_adaptive_binary_state_is_copy() {
1761 fn assert_copy<T: Copy>() {}
1767 assert_copy::<super::AdaptiveBinaryState>();
1768 assert_copy::<super::AdaptiveBinaryOp>();
1769 assert_copy::<super::BinaryShape>();
1770 }
1771
1772 #[test]
1783 fn peek_method_cache_returns_none_for_empty_slot() {
1784 let mut chunk = Chunk::new();
1785 chunk.emit_method_call(0, 0, 1);
1786 let slot = chunk
1787 .inline_cache_slot(0)
1788 .expect("MethodCall registers a slot");
1789 assert!(chunk.peek_method_cache(slot).is_none());
1790 }
1791
1792 #[test]
1793 fn peek_method_cache_returns_triple_after_warmup() {
1794 let mut chunk = Chunk::new();
1795 chunk.emit_method_call(7, 2, 1);
1796 let slot = chunk
1797 .inline_cache_slot(0)
1798 .expect("MethodCall registers a slot");
1799 chunk.set_inline_cache_entry(
1800 slot,
1801 InlineCacheEntry::Method {
1802 name_idx: 7,
1803 argc: 2,
1804 target: MethodCacheTarget::ListContains,
1805 },
1806 );
1807 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1808 assert_eq!(name_idx, 7);
1809 assert_eq!(argc, 2);
1810 assert_eq!(target, MethodCacheTarget::ListContains);
1811 }
1812
1813 #[test]
1814 fn peek_method_cache_returns_none_for_non_method_variants() {
1815 let mut chunk = Chunk::new();
1819 chunk.emit_method_call(0, 0, 1);
1820 let slot = chunk
1821 .inline_cache_slot(0)
1822 .expect("MethodCall registers a slot");
1823
1824 chunk.set_inline_cache_entry(
1825 slot,
1826 InlineCacheEntry::Property {
1827 name_idx: 0,
1828 target: PropertyCacheTarget::ListCount,
1829 },
1830 );
1831 assert!(chunk.peek_method_cache(slot).is_none());
1832 }
1833
1834 #[test]
1835 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1836 let chunk = Chunk::new();
1837 assert!(chunk.peek_method_cache(0).is_none());
1838 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1839 }
1840
1841 #[test]
1842 fn peek_method_cache_target_is_copy() {
1843 fn assert_copy<T: Copy>() {}
1849 assert_copy::<super::MethodCacheTarget>();
1850 }
1851
1852 #[test]
1862 fn peek_property_cache_returns_none_for_empty_slot() {
1863 let mut chunk = Chunk::new();
1864 chunk.emit_u16(Op::GetProperty, 0, 1);
1865 let slot = chunk
1866 .inline_cache_slot(0)
1867 .expect("GetProperty registers a slot");
1868 assert!(chunk.peek_property_cache(slot).is_none());
1869 }
1870
1871 #[test]
1872 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1873 let mut chunk = Chunk::new();
1874 chunk.emit_u16(Op::GetProperty, 0, 1);
1875 let slot = chunk
1876 .inline_cache_slot(0)
1877 .expect("GetProperty registers a slot");
1878 chunk.set_inline_cache_entry(
1879 slot,
1880 InlineCacheEntry::Property {
1881 name_idx: 11,
1882 target: PropertyCacheTarget::DictField(Arc::from("count")),
1883 },
1884 );
1885 let (name_idx, target) = chunk
1886 .peek_property_cache(slot)
1887 .expect("warmed property slot peek");
1888 assert_eq!(name_idx, 11);
1889 match target {
1890 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1891 other => panic!("expected DictField, got {other:?}"),
1892 }
1893 }
1894
1895 #[test]
1896 fn peek_property_cache_returns_pair_for_unit_target() {
1897 let mut chunk = Chunk::new();
1901 chunk.emit_u16(Op::GetProperty, 0, 1);
1902 let slot = chunk
1903 .inline_cache_slot(0)
1904 .expect("GetProperty registers a slot");
1905 chunk.set_inline_cache_entry(
1906 slot,
1907 InlineCacheEntry::Property {
1908 name_idx: 3,
1909 target: PropertyCacheTarget::ListCount,
1910 },
1911 );
1912 let (name_idx, target) = chunk
1913 .peek_property_cache(slot)
1914 .expect("warmed property slot peek");
1915 assert_eq!(name_idx, 3);
1916 assert_eq!(target, PropertyCacheTarget::ListCount);
1917 }
1918
1919 #[test]
1920 fn peek_property_cache_returns_none_for_non_property_variants() {
1921 let mut chunk = Chunk::new();
1922 chunk.emit_u16(Op::GetProperty, 0, 1);
1923 let slot = chunk
1924 .inline_cache_slot(0)
1925 .expect("GetProperty registers a slot");
1926 chunk.set_inline_cache_entry(
1927 slot,
1928 InlineCacheEntry::Method {
1929 name_idx: 0,
1930 argc: 0,
1931 target: MethodCacheTarget::ListCount,
1932 },
1933 );
1934 assert!(chunk.peek_property_cache(slot).is_none());
1935 }
1936
1937 #[test]
1938 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1939 let chunk = Chunk::new();
1940 assert!(chunk.peek_property_cache(0).is_none());
1941 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1942 }
1943
1944 #[test]
1954 fn peek_direct_call_state_returns_none_for_empty_slot() {
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 assert!(chunk.peek_direct_call_state(slot).is_none());
1961 }
1962
1963 #[test]
1964 fn peek_direct_call_state_returns_warmup_state() {
1965 let mut chunk = Chunk::new();
1966 chunk.emit_u8(Op::Call, 0, 1);
1967 let slot = chunk
1968 .inline_cache_slot(0)
1969 .expect("Op::Call registers a slot");
1970 let target = synthetic_direct_call_target();
1971 chunk.set_inline_cache_entry(
1972 slot,
1973 InlineCacheEntry::DirectCall {
1974 state: DirectCallState::Warmup {
1975 argc: 2,
1976 target: target.clone(),
1977 hits: 1,
1978 },
1979 },
1980 );
1981 let state = chunk
1982 .peek_direct_call_state(slot)
1983 .expect("warmed direct-call slot peek");
1984 match state {
1985 DirectCallState::Warmup {
1986 argc,
1987 target: peeked_target,
1988 hits,
1989 } => {
1990 assert_eq!(argc, 2);
1991 assert_eq!(hits, 1);
1992 assert_eq!(peeked_target, target);
1993 }
1994 other => panic!("expected Warmup, got {other:?}"),
1995 }
1996 }
1997
1998 #[test]
1999 fn peek_direct_call_state_returns_specialized_state() {
2000 let mut chunk = Chunk::new();
2001 chunk.emit_u8(Op::Call, 0, 1);
2002 let slot = chunk
2003 .inline_cache_slot(0)
2004 .expect("Op::Call registers a slot");
2005 let target = synthetic_direct_call_target();
2006 chunk.set_inline_cache_entry(
2007 slot,
2008 InlineCacheEntry::DirectCall {
2009 state: DirectCallState::Specialized {
2010 argc: 3,
2011 target: target.clone(),
2012 hits: 100,
2013 misses: 0,
2014 },
2015 },
2016 );
2017 let state = chunk
2018 .peek_direct_call_state(slot)
2019 .expect("warmed direct-call slot peek");
2020 match state {
2021 DirectCallState::Specialized {
2022 argc,
2023 target: peeked_target,
2024 hits,
2025 misses,
2026 } => {
2027 assert_eq!(argc, 3);
2028 assert_eq!(hits, 100);
2029 assert_eq!(misses, 0);
2030 assert_eq!(peeked_target, target);
2031 }
2032 other => panic!("expected Specialized, got {other:?}"),
2033 }
2034 }
2035
2036 #[test]
2037 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2038 let mut chunk = Chunk::new();
2039 chunk.emit_u8(Op::Call, 0, 1);
2040 let slot = chunk
2041 .inline_cache_slot(0)
2042 .expect("Op::Call registers a slot");
2043
2044 chunk.set_inline_cache_entry(
2045 slot,
2046 InlineCacheEntry::Property {
2047 name_idx: 0,
2048 target: PropertyCacheTarget::ListCount,
2049 },
2050 );
2051 assert!(chunk.peek_direct_call_state(slot).is_none());
2052 }
2053
2054 #[test]
2055 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2056 let chunk = Chunk::new();
2057 assert!(chunk.peek_direct_call_state(0).is_none());
2058 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2059 }
2060
2061 fn synthetic_direct_call_target() -> DirectCallTarget {
2065 use crate::value::VmClosure;
2066 use crate::{CompiledFunction, VmEnv};
2067 let func = CompiledFunction {
2068 name: "synthetic".to_string(),
2069 type_params: Vec::new(),
2070 nominal_type_names: Vec::new(),
2071 params: Vec::new(),
2072 default_start: None,
2073 chunk: Arc::new(Chunk::new()),
2074 is_generator: false,
2075 is_stream: false,
2076 has_rest_param: false,
2077 has_runtime_type_checks: false,
2078 };
2079 DirectCallTarget::Closure(Arc::new(VmClosure {
2080 func: Arc::new(func),
2081 env: VmEnv::new(),
2082 source_dir: None,
2083 module_functions: None,
2084 module_state: None,
2085 }))
2086 }
2087}