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::runtime_guards::RuntimeParamGuard;
11
12pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
20static NEXT_CHUNK_CACHE_ID: AtomicU64 = AtomicU64::new(1);
21
22fn next_chunk_cache_id() -> u64 {
23 NEXT_CHUNK_CACHE_ID.fetch_add(1, Ordering::Relaxed)
24}
25
26pub use crate::vm::ops::Op;
33pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub enum Constant {
38 Int(i64),
39 Float(f64),
40 String(String),
41 Bool(bool),
42 Nil,
43 Duration(i64),
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
56pub(crate) enum InlineCacheEntry {
57 Empty,
58 Property {
59 name_idx: u16,
60 target: PropertyCacheTarget,
61 },
62 Method {
63 name_idx: u16,
64 argc: usize,
65 target: MethodCacheTarget,
66 },
67 AdaptiveBinary {
68 op: AdaptiveBinaryOp,
69 state: AdaptiveBinaryState,
70 },
71 DirectCall {
72 state: DirectCallState,
73 },
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(crate) enum AdaptiveBinaryOp {
78 Add,
79 Sub,
80 Mul,
81 Div,
82 Mod,
83 Equal,
84 NotEqual,
85 Less,
86 Greater,
87 LessEqual,
88 GreaterEqual,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub(crate) enum AdaptiveBinaryState {
98 Warmup {
99 shape: BinaryShape,
100 hits: u8,
101 },
102 Specialized {
103 shape: BinaryShape,
104 hits: u64,
105 misses: u64,
106 },
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub(crate) enum BinaryShape {
111 Int,
112 Float,
113 Bool,
114 String,
115}
116
117#[derive(Debug, Clone)]
118pub(crate) enum DirectCallState {
119 Warmup {
120 argc: usize,
121 target: DirectCallTarget,
122 hits: u8,
123 },
124 Specialized {
125 argc: usize,
126 target: DirectCallTarget,
127 hits: u64,
128 misses: u64,
129 },
130}
131
132#[derive(Debug, Clone)]
133pub(crate) enum DirectCallTarget {
134 Closure(Arc<crate::value::VmClosure>),
135}
136
137impl PartialEq for DirectCallTarget {
138 fn eq(&self, other: &Self) -> bool {
139 match (self, other) {
140 (Self::Closure(left), Self::Closure(right)) => Arc::ptr_eq(left, right),
141 }
142 }
143}
144
145impl Eq for DirectCallTarget {}
146
147impl PartialEq for DirectCallState {
148 fn eq(&self, other: &Self) -> bool {
149 match (self, other) {
150 (
151 Self::Warmup {
152 argc: left_argc,
153 target: left_target,
154 hits: left_hits,
155 },
156 Self::Warmup {
157 argc: right_argc,
158 target: right_target,
159 hits: right_hits,
160 },
161 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
162 (
163 Self::Specialized {
164 argc: left_argc,
165 target: left_target,
166 hits: left_hits,
167 misses: left_misses,
168 },
169 Self::Specialized {
170 argc: right_argc,
171 target: right_target,
172 hits: right_hits,
173 misses: right_misses,
174 },
175 ) => {
176 left_argc == right_argc
177 && left_target == right_target
178 && left_hits == right_hits
179 && left_misses == right_misses
180 }
181 _ => false,
182 }
183 }
184}
185
186impl Eq for DirectCallState {}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub(crate) enum PropertyCacheTarget {
190 DictField(Arc<str>),
191 StructField { field_name: Arc<str>, index: usize },
192 ListCount,
193 ListEmpty,
194 ListFirst,
195 ListLast,
196 StringCount,
197 StringEmpty,
198 PairFirst,
199 PairSecond,
200 EnumVariant,
201 EnumFields,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub(crate) enum MethodCacheTarget {
206 ListCount,
207 ListEmpty,
208 ListContains,
209 StringCount,
210 StringEmpty,
211 StringContains,
212 DictCount,
213 DictHas,
214 RangeCount,
215 RangeLen,
216 RangeEmpty,
217 RangeFirst,
218 RangeLast,
219 SetCount,
220 SetLen,
221 SetEmpty,
222 SetContains,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct LocalSlotInfo {
228 pub name: String,
229 pub mutable: bool,
230 pub scope_depth: usize,
231}
232
233impl fmt::Display for Constant {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match self {
236 Constant::Int(n) => write!(f, "{n}"),
237 Constant::Float(n) => write!(f, "{n}"),
238 Constant::String(s) => write!(f, "\"{s}\""),
239 Constant::Bool(b) => write!(f, "{b}"),
240 Constant::Nil => write!(f, "nil"),
241 Constant::Duration(ms) => write!(f, "{ms}ms"),
242 }
243 }
244}
245
246#[derive(Debug)]
248pub struct Chunk {
249 cache_id: u64,
253 pub code: Vec<u8>,
255 pub constants: Vec<Constant>,
257 pub lines: Vec<u32>,
259 pub columns: Vec<u32>,
262 pub source_file: Option<String>,
267 current_col: u32,
269 pub functions: Vec<CompiledFunctionRef>,
271 inline_cache_slots: BTreeMap<usize, usize>,
277 inline_cache_index: Vec<u32>,
285 inline_caches: Arc<Mutex<Vec<InlineCacheEntry>>>,
289 constant_strings: Arc<Mutex<Vec<Option<Arc<str>>>>>,
293 pub(crate) local_slots: Vec<LocalSlotInfo>,
295 pub(crate) references_outer_names: bool,
311 #[cfg(debug_assertions)]
325 balance_depth: i32,
326 #[cfg(debug_assertions)]
327 balance_nonlinear: u32,
328}
329
330pub type ChunkRef = Arc<Chunk>;
331pub type CompiledFunctionRef = Arc<CompiledFunction>;
332
333impl Clone for Chunk {
334 fn clone(&self) -> Self {
335 Self {
336 cache_id: self.cache_id,
337 code: self.code.clone(),
338 constants: self.constants.clone(),
339 lines: self.lines.clone(),
340 columns: self.columns.clone(),
341 source_file: self.source_file.clone(),
342 current_col: self.current_col,
343 functions: self.functions.clone(),
344 inline_cache_slots: self.inline_cache_slots.clone(),
345 inline_cache_index: self.inline_cache_index.clone(),
346 inline_caches: Arc::new(Mutex::new(vec![
347 InlineCacheEntry::Empty;
348 self.inline_cache_slot_count()
349 ])),
350 constant_strings: Arc::new(Mutex::new(vec![None; self.constants.len()])),
351 local_slots: self.local_slots.clone(),
352 references_outer_names: self.references_outer_names,
353 #[cfg(debug_assertions)]
354 balance_depth: self.balance_depth,
355 #[cfg(debug_assertions)]
356 balance_nonlinear: self.balance_nonlinear,
357 }
358 }
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct CachedChunk {
367 pub(crate) code: Vec<u8>,
368 pub(crate) constants: Vec<Constant>,
369 pub(crate) lines: Vec<u32>,
370 pub(crate) columns: Vec<u32>,
371 pub(crate) source_file: Option<String>,
372 pub(crate) current_col: u32,
373 pub(crate) functions: Vec<CachedCompiledFunction>,
374 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
375 pub(crate) local_slots: Vec<LocalSlotInfo>,
376 #[serde(default)]
377 pub(crate) references_outer_names: bool,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct CachedCompiledFunction {
382 pub(crate) name: String,
383 pub(crate) type_params: Vec<String>,
384 pub(crate) nominal_type_names: Vec<String>,
385 pub(crate) params: Vec<CachedParamSlot>,
386 pub(crate) default_start: Option<usize>,
387 pub(crate) chunk: CachedChunk,
388 pub(crate) is_generator: bool,
389 pub(crate) is_stream: bool,
390 pub(crate) has_rest_param: bool,
391 pub(crate) has_runtime_type_checks: bool,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub(crate) struct CachedParamSlot {
396 pub(crate) name: String,
397 pub(crate) type_expr: Option<TypeExpr>,
398 pub(crate) has_default: bool,
399}
400
401impl CachedParamSlot {
402 fn thaw(&self) -> ParamSlot {
403 ParamSlot {
404 name: self.name.clone(),
405 type_expr: self.type_expr.clone(),
406 runtime_guard: self
407 .type_expr
408 .as_ref()
409 .map(RuntimeParamGuard::from_type_expr),
410 has_default: self.has_default,
411 }
412 }
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct ParamSlot {
422 pub name: String,
423 pub type_expr: Option<TypeExpr>,
426 #[serde(skip)]
429 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
430 pub has_default: bool,
434}
435
436impl ParamSlot {
437 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
440 Self {
441 name: param.name.clone(),
442 type_expr: param.type_expr.clone(),
443 runtime_guard: param
444 .type_expr
445 .as_ref()
446 .map(RuntimeParamGuard::from_type_expr),
447 has_default: param.default_value.is_some(),
448 }
449 }
450
451 fn freeze_for_cache(&self) -> CachedParamSlot {
452 CachedParamSlot {
453 name: self.name.clone(),
454 type_expr: self.type_expr.clone(),
455 has_default: self.has_default,
456 }
457 }
458
459 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
464 params.iter().map(Self::from_typed_param).collect()
465 }
466}
467
468#[derive(Debug, Clone)]
470pub struct CompiledFunction {
471 pub name: String,
472 pub type_params: Vec<String>,
476 pub nominal_type_names: Vec<String>,
480 pub params: Vec<ParamSlot>,
481 pub default_start: Option<usize>,
483 pub chunk: ChunkRef,
484 pub is_generator: bool,
486 pub is_stream: bool,
488 pub has_rest_param: bool,
490 pub has_runtime_type_checks: bool,
495}
496
497impl CompiledFunction {
498 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
499 params.iter().any(|param| param.type_expr.is_some())
500 }
501
502 pub fn param_names(&self) -> impl Iterator<Item = &str> {
505 self.params.iter().map(|p| p.name.as_str())
506 }
507
508 pub fn required_param_count(&self) -> usize {
510 self.default_start.unwrap_or(self.params.len())
511 }
512
513 pub fn declares_type_param(&self, name: &str) -> bool {
514 self.type_params.iter().any(|param| param == name)
515 }
516
517 pub fn has_nominal_type(&self, name: &str) -> bool {
518 self.nominal_type_names.iter().any(|ty| ty == name)
519 }
520
521 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
522 CachedCompiledFunction {
523 name: self.name.clone(),
524 type_params: self.type_params.clone(),
525 nominal_type_names: self.nominal_type_names.clone(),
526 params: self
527 .params
528 .iter()
529 .map(ParamSlot::freeze_for_cache)
530 .collect(),
531 default_start: self.default_start,
532 chunk: self.chunk.freeze_for_cache(),
533 is_generator: self.is_generator,
534 is_stream: self.is_stream,
535 has_rest_param: self.has_rest_param,
536 has_runtime_type_checks: self.has_runtime_type_checks,
537 }
538 }
539
540 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
541 Self {
542 name: cached.name.clone(),
543 type_params: cached.type_params.clone(),
544 nominal_type_names: cached.nominal_type_names.clone(),
545 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
546 default_start: cached.default_start,
547 chunk: Arc::new(Chunk::from_cached(&cached.chunk)),
548 is_generator: cached.is_generator,
549 is_stream: cached.is_stream,
550 has_rest_param: cached.has_rest_param,
551 has_runtime_type_checks: cached.has_runtime_type_checks,
552 }
553 }
554}
555
556#[cfg(debug_assertions)]
559#[derive(Clone, Copy)]
560pub(crate) struct BalanceProbe {
561 depth: i32,
562 nonlinear: u32,
563}
564
565#[cfg(debug_assertions)]
582fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
583 use Op::*;
584 let count = count as i32;
585 Some(match op {
586 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
588 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty | Pop => -1,
592 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
596 | PushScope | PopScope | PopIterator | PopHandler => 0,
597 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
599 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
600 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
601 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
602 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
603 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
604 IterInit => -1,
607 Slice | SetSubscript => -2,
609 BuildList | Concat | CallBuiltin => 1 - count,
611 BuildDict => 1 - 2 * count,
612 Call | MethodCall | MethodCallOpt => -count,
614 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
617 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
618 | SyncMutexEnter | Import | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum
619 | MatchEnum | Yield | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
620 })
621}
622
623impl Chunk {
624 pub fn new() -> Self {
625 Self {
626 cache_id: next_chunk_cache_id(),
627 code: Vec::new(),
628 constants: Vec::new(),
629 lines: Vec::new(),
630 columns: Vec::new(),
631 source_file: None,
632 current_col: 0,
633 functions: Vec::new(),
634 inline_cache_slots: BTreeMap::new(),
635 inline_cache_index: Vec::new(),
636 inline_caches: Arc::new(Mutex::new(Vec::new())),
637 constant_strings: Arc::new(Mutex::new(Vec::new())),
638 local_slots: Vec::new(),
639 references_outer_names: false,
640 #[cfg(debug_assertions)]
641 balance_depth: 0,
642 #[cfg(debug_assertions)]
643 balance_nonlinear: 0,
644 }
645 }
646
647 pub fn set_column(&mut self, col: u32) {
649 self.current_col = col;
650 }
651
652 pub fn add_constant(&mut self, constant: Constant) -> u16 {
654 for (i, c) in self.constants.iter().enumerate() {
655 if c == &constant {
656 return i as u16;
657 }
658 }
659 let idx = self.constants.len();
660 self.constants.push(constant);
661 idx as u16
662 }
663
664 pub fn emit(&mut self, op: Op, line: u32) {
666 #[cfg(debug_assertions)]
667 self.note_balance(op, 0);
668 let col = self.current_col;
669 let op_offset = self.code.len();
670 self.code.push(op as u8);
671 self.lines.push(line);
672 self.columns.push(col);
673 if is_adaptive_binary_op(op) {
674 self.register_inline_cache(op_offset);
675 }
676 if op_reads_outer_name(op) {
677 self.references_outer_names = true;
678 }
679 }
680
681 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
683 #[cfg(debug_assertions)]
684 self.note_balance(op, arg);
685 let col = self.current_col;
686 let op_offset = self.code.len();
687 self.code.push(op as u8);
688 self.code.push((arg >> 8) as u8);
689 self.code.push((arg & 0xFF) as u8);
690 self.lines.push(line);
691 self.lines.push(line);
692 self.lines.push(line);
693 self.columns.push(col);
694 self.columns.push(col);
695 self.columns.push(col);
696 if matches!(
697 op,
698 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
699 ) {
700 self.register_inline_cache(op_offset);
701 }
702 if op_reads_outer_name(op) {
703 self.references_outer_names = true;
704 }
705 }
706
707 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
709 #[cfg(debug_assertions)]
710 self.note_balance(op, arg as u16);
711 let col = self.current_col;
712 let op_offset = self.code.len();
713 self.code.push(op as u8);
714 self.code.push(arg);
715 self.lines.push(line);
716 self.lines.push(line);
717 self.columns.push(col);
718 self.columns.push(col);
719 if matches!(op, Op::Call) {
720 self.register_inline_cache(op_offset);
721 }
722 if op_reads_outer_name(op) {
723 self.references_outer_names = true;
724 }
725 }
726
727 pub fn emit_call_builtin(
729 &mut self,
730 id: crate::BuiltinId,
731 name_idx: u16,
732 arg_count: u8,
733 line: u32,
734 ) {
735 #[cfg(debug_assertions)]
736 self.note_balance(Op::CallBuiltin, arg_count as u16);
737 let col = self.current_col;
738 let op_offset = self.code.len();
739 self.code.push(Op::CallBuiltin as u8);
740 self.code.extend_from_slice(&id.raw().to_be_bytes());
741 self.code.push((name_idx >> 8) as u8);
742 self.code.push((name_idx & 0xFF) as u8);
743 self.code.push(arg_count);
744 for _ in 0..12 {
745 self.lines.push(line);
746 self.columns.push(col);
747 }
748 self.register_inline_cache(op_offset);
749 self.references_outer_names = true;
750 }
751
752 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
754 #[cfg(debug_assertions)]
755 self.note_balance(Op::CallBuiltinSpread, 0);
756 let col = self.current_col;
757 self.code.push(Op::CallBuiltinSpread as u8);
758 self.code.extend_from_slice(&id.raw().to_be_bytes());
759 self.code.push((name_idx >> 8) as u8);
760 self.code.push((name_idx & 0xFF) as u8);
761 for _ in 0..11 {
762 self.lines.push(line);
763 self.columns.push(col);
764 }
765 self.references_outer_names = true;
766 }
767
768 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
770 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
771 }
772
773 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
775 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
776 }
777
778 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
779 #[cfg(debug_assertions)]
780 self.note_balance(op, arg_count as u16);
781 let col = self.current_col;
782 let op_offset = self.code.len();
783 self.code.push(op as u8);
784 self.code.push((name_idx >> 8) as u8);
785 self.code.push((name_idx & 0xFF) as u8);
786 self.code.push(arg_count);
787 self.lines.push(line);
788 self.lines.push(line);
789 self.lines.push(line);
790 self.lines.push(line);
791 self.columns.push(col);
792 self.columns.push(col);
793 self.columns.push(col);
794 self.columns.push(col);
795 self.register_inline_cache(op_offset);
796 }
797
798 pub fn current_offset(&self) -> usize {
800 self.code.len()
801 }
802
803 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
805 #[cfg(debug_assertions)]
806 self.note_balance(op, 0);
807 let col = self.current_col;
808 self.code.push(op as u8);
809 let patch_pos = self.code.len();
810 self.code.push(0xFF);
811 self.code.push(0xFF);
812 self.lines.push(line);
813 self.lines.push(line);
814 self.lines.push(line);
815 self.columns.push(col);
816 self.columns.push(col);
817 self.columns.push(col);
818 patch_pos
819 }
820
821 pub fn patch_jump(&mut self, patch_pos: usize) {
823 let target = self.code.len() as u16;
824 self.code[patch_pos] = (target >> 8) as u8;
825 self.code[patch_pos + 1] = (target & 0xFF) as u8;
826 }
827
828 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
830 let target = target as u16;
831 self.code[patch_pos] = (target >> 8) as u8;
832 self.code[patch_pos + 1] = (target & 0xFF) as u8;
833 }
834
835 pub fn read_u16(&self, pos: usize) -> u16 {
837 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
838 }
839
840 #[cfg(debug_assertions)]
844 fn note_balance(&mut self, op: Op, count: u16) {
845 match op_stack_delta(op, count) {
846 Some(delta) => self.balance_depth += delta,
847 None => self.balance_nonlinear += 1,
848 }
849 }
850
851 #[cfg(debug_assertions)]
854 pub(crate) fn balance_probe(&self) -> BalanceProbe {
855 BalanceProbe {
856 depth: self.balance_depth,
857 nonlinear: self.balance_nonlinear,
858 }
859 }
860
861 #[cfg(debug_assertions)]
867 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
868 if self.balance_nonlinear == probe.nonlinear {
869 Some(self.balance_depth - probe.depth)
870 } else {
871 None
872 }
873 }
874
875 fn register_inline_cache(&mut self, op_offset: usize) {
876 if self.inline_cache_slots.contains_key(&op_offset) {
877 return;
878 }
879 let mut entries = self.inline_caches.lock();
880 let slot = entries.len();
881 entries.push(InlineCacheEntry::Empty);
882 self.inline_cache_slots.insert(op_offset, slot);
883 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
884 }
885
886 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
891 if op_offset >= index.len() {
892 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
893 }
894 index[op_offset] = slot as u32;
895 }
896
897 #[inline]
906 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
907 match self.inline_cache_index.get(op_offset).copied() {
908 None | Some(NO_INLINE_CACHE_SLOT) => None,
909 Some(slot) => Some(slot as usize),
910 }
911 }
912
913 pub(crate) fn inline_cache_slot_count(&self) -> usize {
914 self.inline_cache_slots.len()
915 }
916
917 pub(crate) fn cache_id(&self) -> u64 {
918 self.cache_id
919 }
920
921 #[cfg(feature = "vm-bench-internals")]
928 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
929 self.inline_cache_slots.get(&op_offset).copied()
930 }
931
932 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
937 let mut entries = self.constant_strings.lock();
942 if entries.len() < self.constants.len() {
943 entries.resize(self.constants.len(), None);
944 }
945 if let Some(Some(existing)) = entries.get(idx) {
946 return Some(Arc::clone(existing));
947 }
948 let materialized = match self.constants.get(idx)? {
949 Constant::String(s) => Arc::<str>::from(s.as_str()),
950 _ => return None,
951 };
952 entries[idx] = Some(Arc::clone(&materialized));
953 Some(materialized)
954 }
955
956 #[cfg(feature = "vm-bench-internals")]
957 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
958 self.inline_caches
959 .lock()
960 .get(slot)
961 .cloned()
962 .unwrap_or(InlineCacheEntry::Empty)
963 }
964
965 #[inline]
977 #[cfg(any(test, feature = "vm-bench-internals"))]
978 pub(crate) fn peek_adaptive_binary_cache(
979 &self,
980 slot: usize,
981 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
982 match self.inline_caches.lock().get(slot)? {
983 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
984 _ => None,
985 }
986 }
987
988 #[inline]
1000 #[cfg(any(test, feature = "vm-bench-internals"))]
1001 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1002 match self.inline_caches.lock().get(slot)? {
1003 &InlineCacheEntry::Method {
1004 name_idx,
1005 argc,
1006 target,
1007 } => Some((name_idx, argc, target)),
1008 _ => None,
1009 }
1010 }
1011
1012 #[inline]
1022 #[cfg(any(test, feature = "vm-bench-internals"))]
1023 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1024 match self.inline_caches.lock().get(slot)? {
1025 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1026 _ => None,
1027 }
1028 }
1029
1030 #[inline]
1040 #[cfg(any(test, feature = "vm-bench-internals"))]
1041 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1042 match self.inline_caches.lock().get(slot)? {
1043 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1044 _ => None,
1045 }
1046 }
1047
1048 #[cfg(any(test, feature = "vm-bench-internals"))]
1049 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1050 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1051 *existing = entry;
1052 }
1053 }
1054
1055 pub fn freeze_for_cache(&self) -> CachedChunk {
1056 CachedChunk {
1057 code: self.code.clone(),
1058 constants: self.constants.clone(),
1059 lines: self.lines.clone(),
1060 columns: self.columns.clone(),
1061 source_file: self.source_file.clone(),
1062 current_col: self.current_col,
1063 functions: self
1064 .functions
1065 .iter()
1066 .map(|function| function.freeze_for_cache())
1067 .collect(),
1068 inline_cache_slots: self.inline_cache_slots.clone(),
1069 local_slots: self.local_slots.clone(),
1070 references_outer_names: self.references_outer_names,
1071 }
1072 }
1073
1074 pub fn from_cached(cached: &CachedChunk) -> Self {
1075 let inline_cache_count = cached.inline_cache_slots.len();
1076 let constants_count = cached.constants.len();
1077 let mut inline_cache_index = Vec::new();
1084 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1085 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1086 if op_offset < inline_cache_index.len() {
1087 inline_cache_index[op_offset] = slot as u32;
1088 }
1089 }
1090 Self {
1091 cache_id: next_chunk_cache_id(),
1092 code: cached.code.clone(),
1093 constants: cached.constants.clone(),
1094 lines: cached.lines.clone(),
1095 columns: cached.columns.clone(),
1096 source_file: cached.source_file.clone(),
1097 current_col: cached.current_col,
1098 functions: cached
1099 .functions
1100 .iter()
1101 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1102 .collect(),
1103 inline_cache_slots: cached.inline_cache_slots.clone(),
1104 inline_cache_index,
1105 inline_caches: Arc::new(Mutex::new(vec![
1106 InlineCacheEntry::Empty;
1107 inline_cache_count
1108 ])),
1109 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1110 local_slots: cached.local_slots.clone(),
1111 references_outer_names: cached.references_outer_names,
1112 #[cfg(debug_assertions)]
1113 balance_depth: 0,
1114 #[cfg(debug_assertions)]
1115 balance_nonlinear: 0,
1116 }
1117 }
1118
1119 pub(crate) fn add_local_slot(
1120 &mut self,
1121 name: String,
1122 mutable: bool,
1123 scope_depth: usize,
1124 ) -> u16 {
1125 let idx = self.local_slots.len();
1126 self.local_slots.push(LocalSlotInfo {
1127 name,
1128 mutable,
1129 scope_depth,
1130 });
1131 idx as u16
1132 }
1133
1134 pub fn read_u64(&self, pos: usize) -> u64 {
1136 u64::from_be_bytes([
1137 self.code[pos],
1138 self.code[pos + 1],
1139 self.code[pos + 2],
1140 self.code[pos + 3],
1141 self.code[pos + 4],
1142 self.code[pos + 5],
1143 self.code[pos + 6],
1144 self.code[pos + 7],
1145 ])
1146 }
1147
1148 pub fn disassemble(&self, name: &str) -> String {
1152 let mut out = format!("== {name} ==\n");
1153 let mut ip = 0;
1154 while ip < self.code.len() {
1155 let op_byte = self.code[ip];
1156 let line = self.lines.get(ip).copied().unwrap_or(0);
1157 out.push_str(&format!("{ip:04} [{line:>4}] "));
1158 ip += 1;
1159
1160 if let Some(op) = Op::from_byte(op_byte) {
1161 self.disassemble_op(op, &mut ip, &mut out);
1162 } else {
1163 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1164 }
1165 }
1166 out
1167 }
1168}
1169
1170pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1181 label.to_string()
1182}
1183
1184pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1185 let arg = chunk.code[*ip];
1186 *ip += 1;
1187 format!("{label} {arg:>4}")
1188}
1189
1190pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1191 let arg = chunk.read_u16(*ip);
1192 *ip += 2;
1193 format!("{label} {arg:>4}")
1194}
1195
1196pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1197 let idx = chunk.read_u16(*ip);
1198 *ip += 2;
1199 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1200}
1201
1202pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1203 let slot = chunk.read_u16(*ip);
1204 *ip += 2;
1205 let mut out = format!("{label} {slot:>4}");
1206 if let Some(info) = chunk.local_slots.get(slot as usize) {
1207 out.push_str(&format!(" ({})", info.name));
1208 }
1209 out
1210}
1211
1212pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1213 let idx = chunk.read_u16(*ip);
1214 *ip += 2;
1215 let argc = chunk.code[*ip];
1216 *ip += 1;
1217 format!(
1218 "{label} {idx:>4} ({}) argc={argc}",
1219 chunk.constants[idx as usize]
1220 )
1221}
1222
1223pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1224 let enum_idx = chunk.read_u16(*ip);
1225 *ip += 2;
1226 let var_idx = chunk.read_u16(*ip);
1227 *ip += 2;
1228 format!(
1229 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1230 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1231 )
1232}
1233
1234pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1235 let enum_idx = chunk.read_u16(*ip);
1236 *ip += 2;
1237 let var_idx = chunk.read_u16(*ip);
1238 *ip += 2;
1239 let field_count = chunk.read_u16(*ip);
1240 *ip += 2;
1241 format!(
1242 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1243 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1244 )
1245}
1246
1247pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1248 let path_idx = chunk.read_u16(*ip);
1249 *ip += 2;
1250 let names_idx = chunk.read_u16(*ip);
1251 *ip += 2;
1252 format!(
1253 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1254 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1255 )
1256}
1257
1258pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1259 let var_idx = chunk.read_u16(*ip);
1260 *ip += 2;
1261 let type_idx = chunk.read_u16(*ip);
1262 *ip += 2;
1263 format!(
1264 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1265 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1266 )
1267}
1268
1269pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1270 let id = chunk.read_u64(*ip);
1271 *ip += 8;
1272 let idx = chunk.read_u16(*ip);
1273 *ip += 2;
1274 let argc = chunk.code[*ip];
1275 *ip += 1;
1276 format!(
1277 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1278 chunk.constants[idx as usize],
1279 )
1280}
1281
1282pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1283 let id = chunk.read_u64(*ip);
1284 *ip += 8;
1285 let idx = chunk.read_u16(*ip);
1286 *ip += 2;
1287 format!(
1288 "{label} {id:#018x} {idx:>4} ({})",
1289 chunk.constants[idx as usize],
1290 )
1291}
1292
1293pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1294 let idx = chunk.read_u16(*ip);
1300 *ip += 2;
1301 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1302}
1303
1304impl Default for Chunk {
1305 fn default() -> Self {
1306 Self::new()
1307 }
1308}
1309
1310#[cfg(test)]
1311mod tests {
1312 use std::sync::Arc;
1313
1314 use super::{
1315 Chunk, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget, Op,
1316 PropertyCacheTarget,
1317 };
1318 use crate::BuiltinId;
1319
1320 #[test]
1321 fn op_from_byte_matches_repr_order() {
1322 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1323 assert_eq!(byte as u8, op as u8);
1324 assert_eq!(Op::from_byte(byte as u8), Some(op));
1325 }
1326 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1327 assert_eq!(Op::COUNT, Op::ALL.len());
1328 }
1329
1330 #[test]
1331 fn disassemble_covers_every_opcode_variant() {
1332 for op in Op::ALL.iter().copied() {
1342 let mut chunk = Chunk::new();
1343 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1344 for _ in 0..16 {
1348 chunk.code.push(0);
1349 }
1350 let mut ip: usize = 0;
1351 let mut out = String::new();
1352 chunk.disassemble_op(op, &mut ip, &mut out);
1353 assert!(
1354 !out.contains("UNKNOWN"),
1355 "disasm emitted UNKNOWN for {op:?}: {out}",
1356 );
1357 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1358 }
1359 }
1360
1361 #[test]
1370 fn empty_chunk_does_not_reference_outer_names() {
1371 let chunk = Chunk::new();
1372 assert!(!chunk.references_outer_names);
1373 }
1374
1375 #[test]
1376 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1377 let mut chunk = Chunk::new();
1382 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1383 chunk.emit_u16(Op::Constant, 0, 1);
1384 chunk.emit(Op::MulInt, 1);
1385 chunk.emit(Op::Pop, 1);
1386 chunk.emit(Op::Return, 1);
1387 assert!(!chunk.references_outer_names);
1388 }
1389
1390 #[test]
1391 fn slot_only_chunk_does_not_reference_outer_names() {
1392 let mut chunk = Chunk::new();
1394 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1395 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1396 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1397 assert!(!chunk.references_outer_names);
1398 }
1399
1400 #[test]
1401 fn get_var_flags_outer_name_reference() {
1402 let mut chunk = Chunk::new();
1403 chunk.emit_u16(Op::GetVar, 0, 1);
1404 assert!(chunk.references_outer_names);
1405 }
1406
1407 #[test]
1408 fn set_var_flags_outer_name_reference() {
1409 let mut chunk = Chunk::new();
1410 chunk.emit_u16(Op::SetVar, 0, 1);
1411 assert!(chunk.references_outer_names);
1412 }
1413
1414 #[test]
1415 fn check_type_flags_outer_name_reference() {
1416 let mut chunk = Chunk::new();
1417 chunk.emit_u16(Op::CheckType, 0, 1);
1418 assert!(chunk.references_outer_names);
1419 }
1420
1421 #[test]
1422 fn call_builtin_flags_outer_name_reference() {
1423 let mut chunk = Chunk::new();
1424 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1425 assert!(chunk.references_outer_names);
1426 }
1427
1428 #[test]
1429 fn call_builtin_spread_flags_outer_name_reference() {
1430 let mut chunk = Chunk::new();
1431 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1432 assert!(chunk.references_outer_names);
1433 }
1434
1435 #[test]
1436 fn tail_call_flags_outer_name_reference() {
1437 let mut chunk = Chunk::new();
1440 chunk.emit_u8(Op::TailCall, 1, 1);
1441 assert!(chunk.references_outer_names);
1442 }
1443
1444 #[test]
1445 fn call_flags_outer_name_reference() {
1446 let mut chunk = Chunk::new();
1449 chunk.emit_u8(Op::Call, 1, 1);
1450 assert!(chunk.references_outer_names);
1451 }
1452
1453 #[test]
1454 fn pipe_flags_outer_name_reference() {
1455 let mut chunk = Chunk::new();
1458 chunk.emit(Op::Pipe, 1);
1459 assert!(chunk.references_outer_names);
1460 }
1461
1462 #[test]
1463 fn method_call_does_not_flag_outer_name_reference() {
1464 let mut chunk = Chunk::new();
1467 chunk.emit_method_call(0, 1, 1);
1468 chunk.emit_method_call_opt(0, 1, 1);
1469 assert!(!chunk.references_outer_names);
1470 }
1471
1472 #[test]
1473 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1474 let mut chunk = Chunk::new();
1477 chunk.emit_u16(Op::Constant, 0, 1);
1478 chunk.emit(Op::JumpIfFalse, 1);
1479 chunk.emit(Op::Jump, 1);
1480 chunk.emit(Op::Return, 1);
1481 chunk.emit(Op::Pop, 1);
1482 assert!(!chunk.references_outer_names);
1483 }
1484
1485 #[test]
1486 fn references_outer_names_is_monotonic() {
1487 let mut chunk = Chunk::new();
1490 chunk.emit_u16(Op::GetVar, 0, 1);
1491 assert!(chunk.references_outer_names);
1492 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1493 chunk.emit(Op::MulInt, 1);
1494 assert!(chunk.references_outer_names);
1495 }
1496
1497 #[test]
1498 fn freeze_thaw_round_trips_references_outer_names() {
1499 let mut chunk = Chunk::new();
1503 chunk.emit_u16(Op::GetVar, 0, 1);
1504 assert!(chunk.references_outer_names);
1505 let frozen = chunk.freeze_for_cache();
1506 let thawed = Chunk::from_cached(&frozen);
1507 assert!(thawed.references_outer_names);
1508
1509 let plain = Chunk::new();
1510 assert!(!plain.references_outer_names);
1511 let frozen_plain = plain.freeze_for_cache();
1512 let thawed_plain = Chunk::from_cached(&frozen_plain);
1513 assert!(!thawed_plain.references_outer_names);
1514 }
1515
1516 #[test]
1528 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1529 let mut chunk = Chunk::new();
1532 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1533 chunk.emit(Op::Pop, 1);
1534 chunk.emit(Op::Return, 1);
1535 assert!(chunk.inline_cache_slot(0).is_none());
1536 assert!(chunk.inline_cache_slot(3).is_none());
1537 assert!(chunk.inline_cache_slot(4).is_none());
1538 }
1539
1540 #[test]
1541 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1542 let mut chunk = Chunk::new();
1546 chunk.emit(Op::Add, 1);
1547 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1548 }
1549
1550 #[test]
1551 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1552 let mut chunk = Chunk::new();
1557 chunk.emit(Op::Add, 1);
1558 chunk.emit(Op::Sub, 1);
1559 chunk.emit(Op::Mul, 1);
1560 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1561 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1562 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1563 assert_ne!(s0, s1);
1564 assert_ne!(s1, s2);
1565 assert_ne!(s0, s2);
1566 }
1567
1568 #[test]
1569 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1570 let mut chunk = Chunk::new();
1573 chunk.emit(Op::Add, 1);
1574 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1575 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1576 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1577 }
1578
1579 #[test]
1580 fn inline_cache_slot_for_get_property_and_method_call() {
1581 let mut chunk = Chunk::new();
1585 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");
1590 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1591 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1592 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1593 }
1594
1595 #[test]
1596 fn inline_cache_slot_for_call_and_call_builtin() {
1597 let mut chunk = Chunk::new();
1602 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1604 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1605 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1606 assert!(
1607 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1608 "Op::CallBuiltin IC slot"
1609 );
1610 }
1611
1612 #[test]
1613 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1614 let mut chunk = Chunk::new();
1620 chunk.emit(Op::Add, 1);
1621 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1622 chunk.register_inline_cache(0);
1624 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1625 assert_eq!(slot_before, slot_after);
1626 }
1627
1628 #[test]
1629 fn inline_cache_index_round_trips_through_cached_chunk() {
1630 let mut chunk = Chunk::new();
1637 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1638 chunk.emit_u16(Op::Constant, 0, 1);
1639 chunk.emit(Op::Add, 1);
1640 chunk.emit(Op::Sub, 1);
1641 chunk.emit_method_call(0, 1, 1);
1642 chunk.emit_u8(Op::Call, 1, 1);
1643 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1644 .map(|o| (o, chunk.inline_cache_slot(o)))
1645 .collect();
1646 let frozen = chunk.freeze_for_cache();
1647 let thawed = Chunk::from_cached(&frozen);
1648 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1649 .map(|o| (o, thawed.inline_cache_slot(o)))
1650 .collect();
1651 assert_eq!(live_slots, thawed_slots);
1652 }
1653
1654 #[test]
1655 fn inline_cache_index_agrees_with_btreemap_view() {
1656 let mut chunk = Chunk::new();
1662 chunk.emit(Op::Add, 1);
1663 chunk.emit_u16(Op::GetVar, 0, 1);
1664 chunk.emit(Op::LessInt, 1);
1665 chunk.emit_u8(Op::Call, 2, 1);
1666 chunk.emit(Op::Equal, 1);
1667 chunk.emit_u16(Op::GetProperty, 0, 1);
1668 chunk.emit_method_call_opt(0, 0, 1);
1669 for offset in 0..chunk.code.len() {
1670 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1671 let from_index = chunk.inline_cache_slot(offset);
1672 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1673 }
1674 }
1675
1676 #[test]
1687 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1688 let mut chunk = Chunk::new();
1689 chunk.emit(Op::Add, 1);
1690 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1691 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1693 }
1694
1695 #[test]
1696 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1697 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1698 let mut chunk = Chunk::new();
1699 chunk.emit(Op::Add, 1);
1700 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1701 chunk.set_inline_cache_entry(
1702 slot,
1703 InlineCacheEntry::AdaptiveBinary {
1704 op: AdaptiveBinaryOp::Add,
1705 state: AdaptiveBinaryState::Warmup {
1706 shape: BinaryShape::Int,
1707 hits: 2,
1708 },
1709 },
1710 );
1711 let (op, state) = chunk
1712 .peek_adaptive_binary_cache(slot)
1713 .expect("warmed slot peek");
1714 assert_eq!(op, AdaptiveBinaryOp::Add);
1715 assert!(matches!(
1716 state,
1717 AdaptiveBinaryState::Warmup {
1718 shape: BinaryShape::Int,
1719 hits: 2
1720 }
1721 ));
1722 }
1723
1724 #[test]
1725 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1726 use super::{InlineCacheEntry, PropertyCacheTarget};
1732 let mut chunk = Chunk::new();
1733 chunk.emit(Op::Add, 1);
1734 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1735 chunk.set_inline_cache_entry(
1736 slot,
1737 InlineCacheEntry::Property {
1738 name_idx: 0,
1739 target: PropertyCacheTarget::ListCount,
1740 },
1741 );
1742 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1743 }
1744
1745 #[test]
1746 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1747 let chunk = Chunk::new();
1752 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1753 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1754 }
1755
1756 #[test]
1757 fn peek_adaptive_binary_state_is_copy() {
1758 fn assert_copy<T: Copy>() {}
1764 assert_copy::<super::AdaptiveBinaryState>();
1765 assert_copy::<super::AdaptiveBinaryOp>();
1766 assert_copy::<super::BinaryShape>();
1767 }
1768
1769 #[test]
1780 fn peek_method_cache_returns_none_for_empty_slot() {
1781 let mut chunk = Chunk::new();
1782 chunk.emit_method_call(0, 0, 1);
1783 let slot = chunk
1784 .inline_cache_slot(0)
1785 .expect("MethodCall registers a slot");
1786 assert!(chunk.peek_method_cache(slot).is_none());
1787 }
1788
1789 #[test]
1790 fn peek_method_cache_returns_triple_after_warmup() {
1791 let mut chunk = Chunk::new();
1792 chunk.emit_method_call(7, 2, 1);
1793 let slot = chunk
1794 .inline_cache_slot(0)
1795 .expect("MethodCall registers a slot");
1796 chunk.set_inline_cache_entry(
1797 slot,
1798 InlineCacheEntry::Method {
1799 name_idx: 7,
1800 argc: 2,
1801 target: MethodCacheTarget::ListContains,
1802 },
1803 );
1804 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1805 assert_eq!(name_idx, 7);
1806 assert_eq!(argc, 2);
1807 assert_eq!(target, MethodCacheTarget::ListContains);
1808 }
1809
1810 #[test]
1811 fn peek_method_cache_returns_none_for_non_method_variants() {
1812 let mut chunk = Chunk::new();
1816 chunk.emit_method_call(0, 0, 1);
1817 let slot = chunk
1818 .inline_cache_slot(0)
1819 .expect("MethodCall registers a slot");
1820
1821 chunk.set_inline_cache_entry(
1822 slot,
1823 InlineCacheEntry::Property {
1824 name_idx: 0,
1825 target: PropertyCacheTarget::ListCount,
1826 },
1827 );
1828 assert!(chunk.peek_method_cache(slot).is_none());
1829 }
1830
1831 #[test]
1832 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1833 let chunk = Chunk::new();
1834 assert!(chunk.peek_method_cache(0).is_none());
1835 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1836 }
1837
1838 #[test]
1839 fn peek_method_cache_target_is_copy() {
1840 fn assert_copy<T: Copy>() {}
1846 assert_copy::<super::MethodCacheTarget>();
1847 }
1848
1849 #[test]
1859 fn peek_property_cache_returns_none_for_empty_slot() {
1860 let mut chunk = Chunk::new();
1861 chunk.emit_u16(Op::GetProperty, 0, 1);
1862 let slot = chunk
1863 .inline_cache_slot(0)
1864 .expect("GetProperty registers a slot");
1865 assert!(chunk.peek_property_cache(slot).is_none());
1866 }
1867
1868 #[test]
1869 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1870 let mut chunk = Chunk::new();
1871 chunk.emit_u16(Op::GetProperty, 0, 1);
1872 let slot = chunk
1873 .inline_cache_slot(0)
1874 .expect("GetProperty registers a slot");
1875 chunk.set_inline_cache_entry(
1876 slot,
1877 InlineCacheEntry::Property {
1878 name_idx: 11,
1879 target: PropertyCacheTarget::DictField(Arc::from("count")),
1880 },
1881 );
1882 let (name_idx, target) = chunk
1883 .peek_property_cache(slot)
1884 .expect("warmed property slot peek");
1885 assert_eq!(name_idx, 11);
1886 match target {
1887 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1888 other => panic!("expected DictField, got {other:?}"),
1889 }
1890 }
1891
1892 #[test]
1893 fn peek_property_cache_returns_pair_for_unit_target() {
1894 let mut chunk = Chunk::new();
1898 chunk.emit_u16(Op::GetProperty, 0, 1);
1899 let slot = chunk
1900 .inline_cache_slot(0)
1901 .expect("GetProperty registers a slot");
1902 chunk.set_inline_cache_entry(
1903 slot,
1904 InlineCacheEntry::Property {
1905 name_idx: 3,
1906 target: PropertyCacheTarget::ListCount,
1907 },
1908 );
1909 let (name_idx, target) = chunk
1910 .peek_property_cache(slot)
1911 .expect("warmed property slot peek");
1912 assert_eq!(name_idx, 3);
1913 assert_eq!(target, PropertyCacheTarget::ListCount);
1914 }
1915
1916 #[test]
1917 fn peek_property_cache_returns_none_for_non_property_variants() {
1918 let mut chunk = Chunk::new();
1919 chunk.emit_u16(Op::GetProperty, 0, 1);
1920 let slot = chunk
1921 .inline_cache_slot(0)
1922 .expect("GetProperty registers a slot");
1923 chunk.set_inline_cache_entry(
1924 slot,
1925 InlineCacheEntry::Method {
1926 name_idx: 0,
1927 argc: 0,
1928 target: MethodCacheTarget::ListCount,
1929 },
1930 );
1931 assert!(chunk.peek_property_cache(slot).is_none());
1932 }
1933
1934 #[test]
1935 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1936 let chunk = Chunk::new();
1937 assert!(chunk.peek_property_cache(0).is_none());
1938 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1939 }
1940
1941 #[test]
1951 fn peek_direct_call_state_returns_none_for_empty_slot() {
1952 let mut chunk = Chunk::new();
1953 chunk.emit_u8(Op::Call, 0, 1);
1954 let slot = chunk
1955 .inline_cache_slot(0)
1956 .expect("Op::Call registers a slot");
1957 assert!(chunk.peek_direct_call_state(slot).is_none());
1958 }
1959
1960 #[test]
1961 fn peek_direct_call_state_returns_warmup_state() {
1962 let mut chunk = Chunk::new();
1963 chunk.emit_u8(Op::Call, 0, 1);
1964 let slot = chunk
1965 .inline_cache_slot(0)
1966 .expect("Op::Call registers a slot");
1967 let target = synthetic_direct_call_target();
1968 chunk.set_inline_cache_entry(
1969 slot,
1970 InlineCacheEntry::DirectCall {
1971 state: DirectCallState::Warmup {
1972 argc: 2,
1973 target: target.clone(),
1974 hits: 1,
1975 },
1976 },
1977 );
1978 let state = chunk
1979 .peek_direct_call_state(slot)
1980 .expect("warmed direct-call slot peek");
1981 match state {
1982 DirectCallState::Warmup {
1983 argc,
1984 target: peeked_target,
1985 hits,
1986 } => {
1987 assert_eq!(argc, 2);
1988 assert_eq!(hits, 1);
1989 assert_eq!(peeked_target, target);
1990 }
1991 other => panic!("expected Warmup, got {other:?}"),
1992 }
1993 }
1994
1995 #[test]
1996 fn peek_direct_call_state_returns_specialized_state() {
1997 let mut chunk = Chunk::new();
1998 chunk.emit_u8(Op::Call, 0, 1);
1999 let slot = chunk
2000 .inline_cache_slot(0)
2001 .expect("Op::Call registers a slot");
2002 let target = synthetic_direct_call_target();
2003 chunk.set_inline_cache_entry(
2004 slot,
2005 InlineCacheEntry::DirectCall {
2006 state: DirectCallState::Specialized {
2007 argc: 3,
2008 target: target.clone(),
2009 hits: 100,
2010 misses: 0,
2011 },
2012 },
2013 );
2014 let state = chunk
2015 .peek_direct_call_state(slot)
2016 .expect("warmed direct-call slot peek");
2017 match state {
2018 DirectCallState::Specialized {
2019 argc,
2020 target: peeked_target,
2021 hits,
2022 misses,
2023 } => {
2024 assert_eq!(argc, 3);
2025 assert_eq!(hits, 100);
2026 assert_eq!(misses, 0);
2027 assert_eq!(peeked_target, target);
2028 }
2029 other => panic!("expected Specialized, got {other:?}"),
2030 }
2031 }
2032
2033 #[test]
2034 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2035 let mut chunk = Chunk::new();
2036 chunk.emit_u8(Op::Call, 0, 1);
2037 let slot = chunk
2038 .inline_cache_slot(0)
2039 .expect("Op::Call registers a slot");
2040
2041 chunk.set_inline_cache_entry(
2042 slot,
2043 InlineCacheEntry::Property {
2044 name_idx: 0,
2045 target: PropertyCacheTarget::ListCount,
2046 },
2047 );
2048 assert!(chunk.peek_direct_call_state(slot).is_none());
2049 }
2050
2051 #[test]
2052 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2053 let chunk = Chunk::new();
2054 assert!(chunk.peek_direct_call_state(0).is_none());
2055 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2056 }
2057
2058 fn synthetic_direct_call_target() -> DirectCallTarget {
2062 use crate::value::VmClosure;
2063 use crate::{CompiledFunction, VmEnv};
2064 let func = CompiledFunction {
2065 name: "synthetic".to_string(),
2066 type_params: Vec::new(),
2067 nominal_type_names: Vec::new(),
2068 params: Vec::new(),
2069 default_start: None,
2070 chunk: Arc::new(Chunk::new()),
2071 is_generator: false,
2072 is_stream: false,
2073 has_rest_param: false,
2074 has_runtime_type_checks: false,
2075 };
2076 DirectCallTarget::Closure(Arc::new(VmClosure {
2077 func: Arc::new(func),
2078 env: VmEnv::new(),
2079 source_dir: None,
2080 module_functions: None,
2081 module_state: None,
2082 }))
2083 }
2084}