1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4
5use harn_parser::TypeExpr;
6use parking_lot::Mutex;
7use serde::{Deserialize, Serialize};
8
9use crate::runtime_guards::RuntimeParamGuard;
10
11pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
19
20pub use crate::vm::ops::Op;
27pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum Constant {
32 Int(i64),
33 Float(f64),
34 String(String),
35 Bool(bool),
36 Nil,
37 Duration(i64),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) enum InlineCacheEntry {
51 Empty,
52 Property {
53 name_idx: u16,
54 target: PropertyCacheTarget,
55 },
56 Method {
57 name_idx: u16,
58 argc: usize,
59 target: MethodCacheTarget,
60 },
61 AdaptiveBinary {
62 op: AdaptiveBinaryOp,
63 state: AdaptiveBinaryState,
64 },
65 DirectCall {
66 state: DirectCallState,
67 },
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) enum AdaptiveBinaryOp {
72 Add,
73 Sub,
74 Mul,
75 Div,
76 Mod,
77 Equal,
78 NotEqual,
79 Less,
80 Greater,
81 LessEqual,
82 GreaterEqual,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub(crate) enum AdaptiveBinaryState {
92 Warmup {
93 shape: BinaryShape,
94 hits: u8,
95 },
96 Specialized {
97 shape: BinaryShape,
98 hits: u64,
99 misses: u64,
100 },
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub(crate) enum BinaryShape {
105 Int,
106 Float,
107 Bool,
108 String,
109}
110
111#[derive(Debug, Clone)]
112pub(crate) enum DirectCallState {
113 Warmup {
114 argc: usize,
115 target: DirectCallTarget,
116 hits: u8,
117 },
118 Specialized {
119 argc: usize,
120 target: DirectCallTarget,
121 hits: u64,
122 misses: u64,
123 },
124}
125
126#[derive(Debug, Clone)]
127pub(crate) enum DirectCallTarget {
128 Closure(Arc<crate::value::VmClosure>),
129}
130
131impl PartialEq for DirectCallTarget {
132 fn eq(&self, other: &Self) -> bool {
133 match (self, other) {
134 (Self::Closure(left), Self::Closure(right)) => Arc::ptr_eq(left, right),
135 }
136 }
137}
138
139impl Eq for DirectCallTarget {}
140
141impl PartialEq for DirectCallState {
142 fn eq(&self, other: &Self) -> bool {
143 match (self, other) {
144 (
145 Self::Warmup {
146 argc: left_argc,
147 target: left_target,
148 hits: left_hits,
149 },
150 Self::Warmup {
151 argc: right_argc,
152 target: right_target,
153 hits: right_hits,
154 },
155 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
156 (
157 Self::Specialized {
158 argc: left_argc,
159 target: left_target,
160 hits: left_hits,
161 misses: left_misses,
162 },
163 Self::Specialized {
164 argc: right_argc,
165 target: right_target,
166 hits: right_hits,
167 misses: right_misses,
168 },
169 ) => {
170 left_argc == right_argc
171 && left_target == right_target
172 && left_hits == right_hits
173 && left_misses == right_misses
174 }
175 _ => false,
176 }
177 }
178}
179
180impl Eq for DirectCallState {}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub(crate) enum PropertyCacheTarget {
184 DictField(Arc<str>),
185 StructField { field_name: Arc<str>, index: usize },
186 ListCount,
187 ListEmpty,
188 ListFirst,
189 ListLast,
190 StringCount,
191 StringEmpty,
192 PairFirst,
193 PairSecond,
194 EnumVariant,
195 EnumFields,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub(crate) enum MethodCacheTarget {
200 ListCount,
201 ListEmpty,
202 ListContains,
203 StringCount,
204 StringEmpty,
205 StringContains,
206 DictCount,
207 DictHas,
208 RangeCount,
209 RangeLen,
210 RangeEmpty,
211 RangeFirst,
212 RangeLast,
213 SetCount,
214 SetLen,
215 SetEmpty,
216 SetContains,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct LocalSlotInfo {
222 pub name: String,
223 pub mutable: bool,
224 pub scope_depth: usize,
225}
226
227impl fmt::Display for Constant {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match self {
230 Constant::Int(n) => write!(f, "{n}"),
231 Constant::Float(n) => write!(f, "{n}"),
232 Constant::String(s) => write!(f, "\"{s}\""),
233 Constant::Bool(b) => write!(f, "{b}"),
234 Constant::Nil => write!(f, "nil"),
235 Constant::Duration(ms) => write!(f, "{ms}ms"),
236 }
237 }
238}
239
240#[derive(Debug, Clone)]
242pub struct Chunk {
243 pub code: Vec<u8>,
245 pub constants: Vec<Constant>,
247 pub lines: Vec<u32>,
249 pub columns: Vec<u32>,
252 pub source_file: Option<String>,
257 current_col: u32,
259 pub functions: Vec<CompiledFunctionRef>,
261 inline_cache_slots: BTreeMap<usize, usize>,
267 inline_cache_index: Vec<u32>,
275 inline_caches: Arc<Mutex<Vec<InlineCacheEntry>>>,
278 constant_strings: Arc<Mutex<Vec<Option<Arc<str>>>>>,
282 pub(crate) local_slots: Vec<LocalSlotInfo>,
284 pub(crate) references_outer_names: bool,
300 #[cfg(debug_assertions)]
314 balance_depth: i32,
315 #[cfg(debug_assertions)]
316 balance_nonlinear: u32,
317}
318
319pub type ChunkRef = Arc<Chunk>;
320pub type CompiledFunctionRef = Arc<CompiledFunction>;
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct CachedChunk {
328 pub(crate) code: Vec<u8>,
329 pub(crate) constants: Vec<Constant>,
330 pub(crate) lines: Vec<u32>,
331 pub(crate) columns: Vec<u32>,
332 pub(crate) source_file: Option<String>,
333 pub(crate) current_col: u32,
334 pub(crate) functions: Vec<CachedCompiledFunction>,
335 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
336 pub(crate) local_slots: Vec<LocalSlotInfo>,
337 #[serde(default)]
338 pub(crate) references_outer_names: bool,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct CachedCompiledFunction {
343 pub(crate) name: String,
344 pub(crate) type_params: Vec<String>,
345 pub(crate) nominal_type_names: Vec<String>,
346 pub(crate) params: Vec<CachedParamSlot>,
347 pub(crate) default_start: Option<usize>,
348 pub(crate) chunk: CachedChunk,
349 pub(crate) is_generator: bool,
350 pub(crate) is_stream: bool,
351 pub(crate) has_rest_param: bool,
352 pub(crate) has_runtime_type_checks: bool,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub(crate) struct CachedParamSlot {
357 pub(crate) name: String,
358 pub(crate) type_expr: Option<TypeExpr>,
359 pub(crate) has_default: bool,
360}
361
362impl CachedParamSlot {
363 fn thaw(&self) -> ParamSlot {
364 ParamSlot {
365 name: self.name.clone(),
366 type_expr: self.type_expr.clone(),
367 runtime_guard: self
368 .type_expr
369 .as_ref()
370 .map(RuntimeParamGuard::from_type_expr),
371 has_default: self.has_default,
372 }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct ParamSlot {
383 pub name: String,
384 pub type_expr: Option<TypeExpr>,
387 #[serde(skip)]
390 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
391 pub has_default: bool,
395}
396
397impl ParamSlot {
398 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
401 Self {
402 name: param.name.clone(),
403 type_expr: param.type_expr.clone(),
404 runtime_guard: param
405 .type_expr
406 .as_ref()
407 .map(RuntimeParamGuard::from_type_expr),
408 has_default: param.default_value.is_some(),
409 }
410 }
411
412 fn freeze_for_cache(&self) -> CachedParamSlot {
413 CachedParamSlot {
414 name: self.name.clone(),
415 type_expr: self.type_expr.clone(),
416 has_default: self.has_default,
417 }
418 }
419
420 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
425 params.iter().map(Self::from_typed_param).collect()
426 }
427}
428
429#[derive(Debug, Clone)]
431pub struct CompiledFunction {
432 pub name: String,
433 pub type_params: Vec<String>,
437 pub nominal_type_names: Vec<String>,
441 pub params: Vec<ParamSlot>,
442 pub default_start: Option<usize>,
444 pub chunk: ChunkRef,
445 pub is_generator: bool,
447 pub is_stream: bool,
449 pub has_rest_param: bool,
451 pub has_runtime_type_checks: bool,
456}
457
458impl CompiledFunction {
459 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
460 params.iter().any(|param| param.type_expr.is_some())
461 }
462
463 pub fn param_names(&self) -> impl Iterator<Item = &str> {
466 self.params.iter().map(|p| p.name.as_str())
467 }
468
469 pub fn required_param_count(&self) -> usize {
471 self.default_start.unwrap_or(self.params.len())
472 }
473
474 pub fn declares_type_param(&self, name: &str) -> bool {
475 self.type_params.iter().any(|param| param == name)
476 }
477
478 pub fn has_nominal_type(&self, name: &str) -> bool {
479 self.nominal_type_names.iter().any(|ty| ty == name)
480 }
481
482 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
483 CachedCompiledFunction {
484 name: self.name.clone(),
485 type_params: self.type_params.clone(),
486 nominal_type_names: self.nominal_type_names.clone(),
487 params: self
488 .params
489 .iter()
490 .map(ParamSlot::freeze_for_cache)
491 .collect(),
492 default_start: self.default_start,
493 chunk: self.chunk.freeze_for_cache(),
494 is_generator: self.is_generator,
495 is_stream: self.is_stream,
496 has_rest_param: self.has_rest_param,
497 has_runtime_type_checks: self.has_runtime_type_checks,
498 }
499 }
500
501 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
502 Self {
503 name: cached.name.clone(),
504 type_params: cached.type_params.clone(),
505 nominal_type_names: cached.nominal_type_names.clone(),
506 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
507 default_start: cached.default_start,
508 chunk: Arc::new(Chunk::from_cached(&cached.chunk)),
509 is_generator: cached.is_generator,
510 is_stream: cached.is_stream,
511 has_rest_param: cached.has_rest_param,
512 has_runtime_type_checks: cached.has_runtime_type_checks,
513 }
514 }
515}
516
517#[cfg(debug_assertions)]
520#[derive(Clone, Copy)]
521pub(crate) struct BalanceProbe {
522 depth: i32,
523 nonlinear: u32,
524}
525
526#[cfg(debug_assertions)]
543fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
544 use Op::*;
545 let count = count as i32;
546 Some(match op {
547 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
549 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty | Pop => -1,
553 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
557 | PushScope | PopScope | PopIterator | PopHandler => 0,
558 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
560 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
561 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
562 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
563 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
564 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
565 IterInit => -1,
568 Slice | SetSubscript => -2,
570 BuildList | Concat | CallBuiltin => 1 - count,
572 BuildDict => 1 - 2 * count,
573 Call | MethodCall | MethodCallOpt => -count,
575 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
578 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
579 | SyncMutexEnter | Import | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum
580 | MatchEnum | Yield | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
581 })
582}
583
584impl Chunk {
585 pub fn new() -> Self {
586 Self {
587 code: Vec::new(),
588 constants: Vec::new(),
589 lines: Vec::new(),
590 columns: Vec::new(),
591 source_file: None,
592 current_col: 0,
593 functions: Vec::new(),
594 inline_cache_slots: BTreeMap::new(),
595 inline_cache_index: Vec::new(),
596 inline_caches: Arc::new(Mutex::new(Vec::new())),
597 constant_strings: Arc::new(Mutex::new(Vec::new())),
598 local_slots: Vec::new(),
599 references_outer_names: false,
600 #[cfg(debug_assertions)]
601 balance_depth: 0,
602 #[cfg(debug_assertions)]
603 balance_nonlinear: 0,
604 }
605 }
606
607 pub fn set_column(&mut self, col: u32) {
609 self.current_col = col;
610 }
611
612 pub fn add_constant(&mut self, constant: Constant) -> u16 {
614 for (i, c) in self.constants.iter().enumerate() {
615 if c == &constant {
616 return i as u16;
617 }
618 }
619 let idx = self.constants.len();
620 self.constants.push(constant);
621 idx as u16
622 }
623
624 pub fn emit(&mut self, op: Op, line: u32) {
626 #[cfg(debug_assertions)]
627 self.note_balance(op, 0);
628 let col = self.current_col;
629 let op_offset = self.code.len();
630 self.code.push(op as u8);
631 self.lines.push(line);
632 self.columns.push(col);
633 if is_adaptive_binary_op(op) {
634 self.register_inline_cache(op_offset);
635 }
636 if op_reads_outer_name(op) {
637 self.references_outer_names = true;
638 }
639 }
640
641 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
643 #[cfg(debug_assertions)]
644 self.note_balance(op, arg);
645 let col = self.current_col;
646 let op_offset = self.code.len();
647 self.code.push(op as u8);
648 self.code.push((arg >> 8) as u8);
649 self.code.push((arg & 0xFF) as u8);
650 self.lines.push(line);
651 self.lines.push(line);
652 self.lines.push(line);
653 self.columns.push(col);
654 self.columns.push(col);
655 self.columns.push(col);
656 if matches!(
657 op,
658 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
659 ) {
660 self.register_inline_cache(op_offset);
661 }
662 if op_reads_outer_name(op) {
663 self.references_outer_names = true;
664 }
665 }
666
667 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
669 #[cfg(debug_assertions)]
670 self.note_balance(op, arg as u16);
671 let col = self.current_col;
672 let op_offset = self.code.len();
673 self.code.push(op as u8);
674 self.code.push(arg);
675 self.lines.push(line);
676 self.lines.push(line);
677 self.columns.push(col);
678 self.columns.push(col);
679 if matches!(op, Op::Call) {
680 self.register_inline_cache(op_offset);
681 }
682 if op_reads_outer_name(op) {
683 self.references_outer_names = true;
684 }
685 }
686
687 pub fn emit_call_builtin(
689 &mut self,
690 id: crate::BuiltinId,
691 name_idx: u16,
692 arg_count: u8,
693 line: u32,
694 ) {
695 #[cfg(debug_assertions)]
696 self.note_balance(Op::CallBuiltin, arg_count as u16);
697 let col = self.current_col;
698 let op_offset = self.code.len();
699 self.code.push(Op::CallBuiltin as u8);
700 self.code.extend_from_slice(&id.raw().to_be_bytes());
701 self.code.push((name_idx >> 8) as u8);
702 self.code.push((name_idx & 0xFF) as u8);
703 self.code.push(arg_count);
704 for _ in 0..12 {
705 self.lines.push(line);
706 self.columns.push(col);
707 }
708 self.register_inline_cache(op_offset);
709 self.references_outer_names = true;
710 }
711
712 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
714 #[cfg(debug_assertions)]
715 self.note_balance(Op::CallBuiltinSpread, 0);
716 let col = self.current_col;
717 self.code.push(Op::CallBuiltinSpread as u8);
718 self.code.extend_from_slice(&id.raw().to_be_bytes());
719 self.code.push((name_idx >> 8) as u8);
720 self.code.push((name_idx & 0xFF) as u8);
721 for _ in 0..11 {
722 self.lines.push(line);
723 self.columns.push(col);
724 }
725 self.references_outer_names = true;
726 }
727
728 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
730 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
731 }
732
733 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
735 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
736 }
737
738 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
739 #[cfg(debug_assertions)]
740 self.note_balance(op, arg_count as u16);
741 let col = self.current_col;
742 let op_offset = self.code.len();
743 self.code.push(op as u8);
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 self.lines.push(line);
748 self.lines.push(line);
749 self.lines.push(line);
750 self.lines.push(line);
751 self.columns.push(col);
752 self.columns.push(col);
753 self.columns.push(col);
754 self.columns.push(col);
755 self.register_inline_cache(op_offset);
756 }
757
758 pub fn current_offset(&self) -> usize {
760 self.code.len()
761 }
762
763 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
765 #[cfg(debug_assertions)]
766 self.note_balance(op, 0);
767 let col = self.current_col;
768 self.code.push(op as u8);
769 let patch_pos = self.code.len();
770 self.code.push(0xFF);
771 self.code.push(0xFF);
772 self.lines.push(line);
773 self.lines.push(line);
774 self.lines.push(line);
775 self.columns.push(col);
776 self.columns.push(col);
777 self.columns.push(col);
778 patch_pos
779 }
780
781 pub fn patch_jump(&mut self, patch_pos: usize) {
783 let target = self.code.len() as u16;
784 self.code[patch_pos] = (target >> 8) as u8;
785 self.code[patch_pos + 1] = (target & 0xFF) as u8;
786 }
787
788 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
790 let target = target as u16;
791 self.code[patch_pos] = (target >> 8) as u8;
792 self.code[patch_pos + 1] = (target & 0xFF) as u8;
793 }
794
795 pub fn read_u16(&self, pos: usize) -> u16 {
797 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
798 }
799
800 #[cfg(debug_assertions)]
804 fn note_balance(&mut self, op: Op, count: u16) {
805 match op_stack_delta(op, count) {
806 Some(delta) => self.balance_depth += delta,
807 None => self.balance_nonlinear += 1,
808 }
809 }
810
811 #[cfg(debug_assertions)]
814 pub(crate) fn balance_probe(&self) -> BalanceProbe {
815 BalanceProbe {
816 depth: self.balance_depth,
817 nonlinear: self.balance_nonlinear,
818 }
819 }
820
821 #[cfg(debug_assertions)]
827 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
828 if self.balance_nonlinear == probe.nonlinear {
829 Some(self.balance_depth - probe.depth)
830 } else {
831 None
832 }
833 }
834
835 fn register_inline_cache(&mut self, op_offset: usize) {
836 if self.inline_cache_slots.contains_key(&op_offset) {
837 return;
838 }
839 let mut entries = self.inline_caches.lock();
840 let slot = entries.len();
841 entries.push(InlineCacheEntry::Empty);
842 self.inline_cache_slots.insert(op_offset, slot);
843 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
844 }
845
846 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
851 if op_offset >= index.len() {
852 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
853 }
854 index[op_offset] = slot as u32;
855 }
856
857 #[inline]
866 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
867 match self.inline_cache_index.get(op_offset).copied() {
868 None | Some(NO_INLINE_CACHE_SLOT) => None,
869 Some(slot) => Some(slot as usize),
870 }
871 }
872
873 #[cfg(feature = "vm-bench-internals")]
880 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
881 self.inline_cache_slots.get(&op_offset).copied()
882 }
883
884 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
889 let mut entries = self.constant_strings.lock();
894 if entries.len() < self.constants.len() {
895 entries.resize(self.constants.len(), None);
896 }
897 if let Some(Some(existing)) = entries.get(idx) {
898 return Some(Arc::clone(existing));
899 }
900 let materialized = match self.constants.get(idx)? {
901 Constant::String(s) => Arc::<str>::from(s.as_str()),
902 _ => return None,
903 };
904 entries[idx] = Some(Arc::clone(&materialized));
905 Some(materialized)
906 }
907
908 #[cfg(feature = "vm-bench-internals")]
909 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
910 self.inline_caches
911 .lock()
912 .get(slot)
913 .cloned()
914 .unwrap_or(InlineCacheEntry::Empty)
915 }
916
917 #[inline]
929 pub(crate) fn peek_adaptive_binary_cache(
930 &self,
931 slot: usize,
932 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
933 match self.inline_caches.lock().get(slot)? {
934 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
935 _ => None,
936 }
937 }
938
939 #[inline]
951 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
952 match self.inline_caches.lock().get(slot)? {
953 &InlineCacheEntry::Method {
954 name_idx,
955 argc,
956 target,
957 } => Some((name_idx, argc, target)),
958 _ => None,
959 }
960 }
961
962 #[inline]
972 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
973 match self.inline_caches.lock().get(slot)? {
974 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
975 _ => None,
976 }
977 }
978
979 #[inline]
989 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
990 match self.inline_caches.lock().get(slot)? {
991 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
992 _ => None,
993 }
994 }
995
996 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
997 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
998 *existing = entry;
999 }
1000 }
1001
1002 pub fn freeze_for_cache(&self) -> CachedChunk {
1003 CachedChunk {
1004 code: self.code.clone(),
1005 constants: self.constants.clone(),
1006 lines: self.lines.clone(),
1007 columns: self.columns.clone(),
1008 source_file: self.source_file.clone(),
1009 current_col: self.current_col,
1010 functions: self
1011 .functions
1012 .iter()
1013 .map(|function| function.freeze_for_cache())
1014 .collect(),
1015 inline_cache_slots: self.inline_cache_slots.clone(),
1016 local_slots: self.local_slots.clone(),
1017 references_outer_names: self.references_outer_names,
1018 }
1019 }
1020
1021 pub fn from_cached(cached: &CachedChunk) -> Self {
1022 let inline_cache_count = cached.inline_cache_slots.len();
1023 let constants_count = cached.constants.len();
1024 let mut inline_cache_index = Vec::new();
1031 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1032 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1033 if op_offset < inline_cache_index.len() {
1034 inline_cache_index[op_offset] = slot as u32;
1035 }
1036 }
1037 Self {
1038 code: cached.code.clone(),
1039 constants: cached.constants.clone(),
1040 lines: cached.lines.clone(),
1041 columns: cached.columns.clone(),
1042 source_file: cached.source_file.clone(),
1043 current_col: cached.current_col,
1044 functions: cached
1045 .functions
1046 .iter()
1047 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1048 .collect(),
1049 inline_cache_slots: cached.inline_cache_slots.clone(),
1050 inline_cache_index,
1051 inline_caches: Arc::new(Mutex::new(vec![
1052 InlineCacheEntry::Empty;
1053 inline_cache_count
1054 ])),
1055 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1056 local_slots: cached.local_slots.clone(),
1057 references_outer_names: cached.references_outer_names,
1058 #[cfg(debug_assertions)]
1059 balance_depth: 0,
1060 #[cfg(debug_assertions)]
1061 balance_nonlinear: 0,
1062 }
1063 }
1064
1065 pub(crate) fn add_local_slot(
1066 &mut self,
1067 name: String,
1068 mutable: bool,
1069 scope_depth: usize,
1070 ) -> u16 {
1071 let idx = self.local_slots.len();
1072 self.local_slots.push(LocalSlotInfo {
1073 name,
1074 mutable,
1075 scope_depth,
1076 });
1077 idx as u16
1078 }
1079
1080 #[cfg(test)]
1081 pub(crate) fn inline_cache_entries(&self) -> Vec<InlineCacheEntry> {
1082 self.inline_caches.lock().clone()
1083 }
1084
1085 pub fn read_u64(&self, pos: usize) -> u64 {
1087 u64::from_be_bytes([
1088 self.code[pos],
1089 self.code[pos + 1],
1090 self.code[pos + 2],
1091 self.code[pos + 3],
1092 self.code[pos + 4],
1093 self.code[pos + 5],
1094 self.code[pos + 6],
1095 self.code[pos + 7],
1096 ])
1097 }
1098
1099 pub fn disassemble(&self, name: &str) -> String {
1103 let mut out = format!("== {name} ==\n");
1104 let mut ip = 0;
1105 while ip < self.code.len() {
1106 let op_byte = self.code[ip];
1107 let line = self.lines.get(ip).copied().unwrap_or(0);
1108 out.push_str(&format!("{ip:04} [{line:>4}] "));
1109 ip += 1;
1110
1111 if let Some(op) = Op::from_byte(op_byte) {
1112 self.disassemble_op(op, &mut ip, &mut out);
1113 } else {
1114 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1115 }
1116 }
1117 out
1118 }
1119}
1120
1121pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1132 label.to_string()
1133}
1134
1135pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1136 let arg = chunk.code[*ip];
1137 *ip += 1;
1138 format!("{label} {arg:>4}")
1139}
1140
1141pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1142 let arg = chunk.read_u16(*ip);
1143 *ip += 2;
1144 format!("{label} {arg:>4}")
1145}
1146
1147pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1148 let idx = chunk.read_u16(*ip);
1149 *ip += 2;
1150 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1151}
1152
1153pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1154 let slot = chunk.read_u16(*ip);
1155 *ip += 2;
1156 let mut out = format!("{label} {slot:>4}");
1157 if let Some(info) = chunk.local_slots.get(slot as usize) {
1158 out.push_str(&format!(" ({})", info.name));
1159 }
1160 out
1161}
1162
1163pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1164 let idx = chunk.read_u16(*ip);
1165 *ip += 2;
1166 let argc = chunk.code[*ip];
1167 *ip += 1;
1168 format!(
1169 "{label} {idx:>4} ({}) argc={argc}",
1170 chunk.constants[idx as usize]
1171 )
1172}
1173
1174pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1175 let enum_idx = chunk.read_u16(*ip);
1176 *ip += 2;
1177 let var_idx = chunk.read_u16(*ip);
1178 *ip += 2;
1179 format!(
1180 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1181 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1182 )
1183}
1184
1185pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1186 let enum_idx = chunk.read_u16(*ip);
1187 *ip += 2;
1188 let var_idx = chunk.read_u16(*ip);
1189 *ip += 2;
1190 let field_count = chunk.read_u16(*ip);
1191 *ip += 2;
1192 format!(
1193 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1194 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1195 )
1196}
1197
1198pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1199 let path_idx = chunk.read_u16(*ip);
1200 *ip += 2;
1201 let names_idx = chunk.read_u16(*ip);
1202 *ip += 2;
1203 format!(
1204 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1205 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1206 )
1207}
1208
1209pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1210 let var_idx = chunk.read_u16(*ip);
1211 *ip += 2;
1212 let type_idx = chunk.read_u16(*ip);
1213 *ip += 2;
1214 format!(
1215 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1216 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1217 )
1218}
1219
1220pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1221 let id = chunk.read_u64(*ip);
1222 *ip += 8;
1223 let idx = chunk.read_u16(*ip);
1224 *ip += 2;
1225 let argc = chunk.code[*ip];
1226 *ip += 1;
1227 format!(
1228 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1229 chunk.constants[idx as usize],
1230 )
1231}
1232
1233pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1234 let id = chunk.read_u64(*ip);
1235 *ip += 8;
1236 let idx = chunk.read_u16(*ip);
1237 *ip += 2;
1238 format!(
1239 "{label} {id:#018x} {idx:>4} ({})",
1240 chunk.constants[idx as usize],
1241 )
1242}
1243
1244pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1245 let idx = chunk.read_u16(*ip);
1251 *ip += 2;
1252 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1253}
1254
1255impl Default for Chunk {
1256 fn default() -> Self {
1257 Self::new()
1258 }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263 use std::sync::Arc;
1264
1265 use super::{
1266 Chunk, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget, Op,
1267 PropertyCacheTarget,
1268 };
1269 use crate::BuiltinId;
1270
1271 #[test]
1272 fn op_from_byte_matches_repr_order() {
1273 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1274 assert_eq!(byte as u8, op as u8);
1275 assert_eq!(Op::from_byte(byte as u8), Some(op));
1276 }
1277 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1278 assert_eq!(Op::COUNT, Op::ALL.len());
1279 }
1280
1281 #[test]
1282 fn disassemble_covers_every_opcode_variant() {
1283 for op in Op::ALL.iter().copied() {
1293 let mut chunk = Chunk::new();
1294 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1295 for _ in 0..16 {
1299 chunk.code.push(0);
1300 }
1301 let mut ip: usize = 0;
1302 let mut out = String::new();
1303 chunk.disassemble_op(op, &mut ip, &mut out);
1304 assert!(
1305 !out.contains("UNKNOWN"),
1306 "disasm emitted UNKNOWN for {op:?}: {out}",
1307 );
1308 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1309 }
1310 }
1311
1312 #[test]
1321 fn empty_chunk_does_not_reference_outer_names() {
1322 let chunk = Chunk::new();
1323 assert!(!chunk.references_outer_names);
1324 }
1325
1326 #[test]
1327 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1328 let mut chunk = Chunk::new();
1333 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1334 chunk.emit_u16(Op::Constant, 0, 1);
1335 chunk.emit(Op::MulInt, 1);
1336 chunk.emit(Op::Pop, 1);
1337 chunk.emit(Op::Return, 1);
1338 assert!(!chunk.references_outer_names);
1339 }
1340
1341 #[test]
1342 fn slot_only_chunk_does_not_reference_outer_names() {
1343 let mut chunk = Chunk::new();
1345 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1346 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1347 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1348 assert!(!chunk.references_outer_names);
1349 }
1350
1351 #[test]
1352 fn get_var_flags_outer_name_reference() {
1353 let mut chunk = Chunk::new();
1354 chunk.emit_u16(Op::GetVar, 0, 1);
1355 assert!(chunk.references_outer_names);
1356 }
1357
1358 #[test]
1359 fn set_var_flags_outer_name_reference() {
1360 let mut chunk = Chunk::new();
1361 chunk.emit_u16(Op::SetVar, 0, 1);
1362 assert!(chunk.references_outer_names);
1363 }
1364
1365 #[test]
1366 fn check_type_flags_outer_name_reference() {
1367 let mut chunk = Chunk::new();
1368 chunk.emit_u16(Op::CheckType, 0, 1);
1369 assert!(chunk.references_outer_names);
1370 }
1371
1372 #[test]
1373 fn call_builtin_flags_outer_name_reference() {
1374 let mut chunk = Chunk::new();
1375 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1376 assert!(chunk.references_outer_names);
1377 }
1378
1379 #[test]
1380 fn call_builtin_spread_flags_outer_name_reference() {
1381 let mut chunk = Chunk::new();
1382 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1383 assert!(chunk.references_outer_names);
1384 }
1385
1386 #[test]
1387 fn tail_call_flags_outer_name_reference() {
1388 let mut chunk = Chunk::new();
1391 chunk.emit_u8(Op::TailCall, 1, 1);
1392 assert!(chunk.references_outer_names);
1393 }
1394
1395 #[test]
1396 fn call_flags_outer_name_reference() {
1397 let mut chunk = Chunk::new();
1400 chunk.emit_u8(Op::Call, 1, 1);
1401 assert!(chunk.references_outer_names);
1402 }
1403
1404 #[test]
1405 fn pipe_flags_outer_name_reference() {
1406 let mut chunk = Chunk::new();
1409 chunk.emit(Op::Pipe, 1);
1410 assert!(chunk.references_outer_names);
1411 }
1412
1413 #[test]
1414 fn method_call_does_not_flag_outer_name_reference() {
1415 let mut chunk = Chunk::new();
1418 chunk.emit_method_call(0, 1, 1);
1419 chunk.emit_method_call_opt(0, 1, 1);
1420 assert!(!chunk.references_outer_names);
1421 }
1422
1423 #[test]
1424 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1425 let mut chunk = Chunk::new();
1428 chunk.emit_u16(Op::Constant, 0, 1);
1429 chunk.emit(Op::JumpIfFalse, 1);
1430 chunk.emit(Op::Jump, 1);
1431 chunk.emit(Op::Return, 1);
1432 chunk.emit(Op::Pop, 1);
1433 assert!(!chunk.references_outer_names);
1434 }
1435
1436 #[test]
1437 fn references_outer_names_is_monotonic() {
1438 let mut chunk = Chunk::new();
1441 chunk.emit_u16(Op::GetVar, 0, 1);
1442 assert!(chunk.references_outer_names);
1443 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1444 chunk.emit(Op::MulInt, 1);
1445 assert!(chunk.references_outer_names);
1446 }
1447
1448 #[test]
1449 fn freeze_thaw_round_trips_references_outer_names() {
1450 let mut chunk = Chunk::new();
1454 chunk.emit_u16(Op::GetVar, 0, 1);
1455 assert!(chunk.references_outer_names);
1456 let frozen = chunk.freeze_for_cache();
1457 let thawed = Chunk::from_cached(&frozen);
1458 assert!(thawed.references_outer_names);
1459
1460 let plain = Chunk::new();
1461 assert!(!plain.references_outer_names);
1462 let frozen_plain = plain.freeze_for_cache();
1463 let thawed_plain = Chunk::from_cached(&frozen_plain);
1464 assert!(!thawed_plain.references_outer_names);
1465 }
1466
1467 #[test]
1479 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1480 let mut chunk = Chunk::new();
1483 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1484 chunk.emit(Op::Pop, 1);
1485 chunk.emit(Op::Return, 1);
1486 assert!(chunk.inline_cache_slot(0).is_none());
1487 assert!(chunk.inline_cache_slot(3).is_none());
1488 assert!(chunk.inline_cache_slot(4).is_none());
1489 }
1490
1491 #[test]
1492 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1493 let mut chunk = Chunk::new();
1497 chunk.emit(Op::Add, 1);
1498 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1499 }
1500
1501 #[test]
1502 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1503 let mut chunk = Chunk::new();
1508 chunk.emit(Op::Add, 1);
1509 chunk.emit(Op::Sub, 1);
1510 chunk.emit(Op::Mul, 1);
1511 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1512 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1513 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1514 assert_ne!(s0, s1);
1515 assert_ne!(s1, s2);
1516 assert_ne!(s0, s2);
1517 }
1518
1519 #[test]
1520 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1521 let mut chunk = Chunk::new();
1524 chunk.emit(Op::Add, 1);
1525 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1526 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1527 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1528 }
1529
1530 #[test]
1531 fn inline_cache_slot_for_get_property_and_method_call() {
1532 let mut chunk = Chunk::new();
1536 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");
1541 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1542 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1543 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1544 }
1545
1546 #[test]
1547 fn inline_cache_slot_for_call_and_call_builtin() {
1548 let mut chunk = Chunk::new();
1553 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1555 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1556 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1557 assert!(
1558 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1559 "Op::CallBuiltin IC slot"
1560 );
1561 }
1562
1563 #[test]
1564 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1565 let mut chunk = Chunk::new();
1571 chunk.emit(Op::Add, 1);
1572 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1573 chunk.register_inline_cache(0);
1575 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1576 assert_eq!(slot_before, slot_after);
1577 }
1578
1579 #[test]
1580 fn inline_cache_index_round_trips_through_cached_chunk() {
1581 let mut chunk = Chunk::new();
1588 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1589 chunk.emit_u16(Op::Constant, 0, 1);
1590 chunk.emit(Op::Add, 1);
1591 chunk.emit(Op::Sub, 1);
1592 chunk.emit_method_call(0, 1, 1);
1593 chunk.emit_u8(Op::Call, 1, 1);
1594 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1595 .map(|o| (o, chunk.inline_cache_slot(o)))
1596 .collect();
1597 let frozen = chunk.freeze_for_cache();
1598 let thawed = Chunk::from_cached(&frozen);
1599 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1600 .map(|o| (o, thawed.inline_cache_slot(o)))
1601 .collect();
1602 assert_eq!(live_slots, thawed_slots);
1603 }
1604
1605 #[test]
1606 fn inline_cache_index_agrees_with_btreemap_view() {
1607 let mut chunk = Chunk::new();
1613 chunk.emit(Op::Add, 1);
1614 chunk.emit_u16(Op::GetVar, 0, 1);
1615 chunk.emit(Op::LessInt, 1);
1616 chunk.emit_u8(Op::Call, 2, 1);
1617 chunk.emit(Op::Equal, 1);
1618 chunk.emit_u16(Op::GetProperty, 0, 1);
1619 chunk.emit_method_call_opt(0, 0, 1);
1620 for offset in 0..chunk.code.len() {
1621 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1622 let from_index = chunk.inline_cache_slot(offset);
1623 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1624 }
1625 }
1626
1627 #[test]
1638 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1639 let mut chunk = Chunk::new();
1640 chunk.emit(Op::Add, 1);
1641 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1642 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1644 }
1645
1646 #[test]
1647 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1648 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1649 let mut chunk = Chunk::new();
1650 chunk.emit(Op::Add, 1);
1651 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1652 chunk.set_inline_cache_entry(
1653 slot,
1654 InlineCacheEntry::AdaptiveBinary {
1655 op: AdaptiveBinaryOp::Add,
1656 state: AdaptiveBinaryState::Warmup {
1657 shape: BinaryShape::Int,
1658 hits: 2,
1659 },
1660 },
1661 );
1662 let (op, state) = chunk
1663 .peek_adaptive_binary_cache(slot)
1664 .expect("warmed slot peek");
1665 assert_eq!(op, AdaptiveBinaryOp::Add);
1666 assert!(matches!(
1667 state,
1668 AdaptiveBinaryState::Warmup {
1669 shape: BinaryShape::Int,
1670 hits: 2
1671 }
1672 ));
1673 }
1674
1675 #[test]
1676 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1677 use super::{InlineCacheEntry, PropertyCacheTarget};
1683 let mut chunk = Chunk::new();
1684 chunk.emit(Op::Add, 1);
1685 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1686 chunk.set_inline_cache_entry(
1687 slot,
1688 InlineCacheEntry::Property {
1689 name_idx: 0,
1690 target: PropertyCacheTarget::ListCount,
1691 },
1692 );
1693 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1694 }
1695
1696 #[test]
1697 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1698 let chunk = Chunk::new();
1703 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1704 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1705 }
1706
1707 #[test]
1708 fn peek_adaptive_binary_state_is_copy() {
1709 fn assert_copy<T: Copy>() {}
1715 assert_copy::<super::AdaptiveBinaryState>();
1716 assert_copy::<super::AdaptiveBinaryOp>();
1717 assert_copy::<super::BinaryShape>();
1718 }
1719
1720 #[test]
1731 fn peek_method_cache_returns_none_for_empty_slot() {
1732 let mut chunk = Chunk::new();
1733 chunk.emit_method_call(0, 0, 1);
1734 let slot = chunk
1735 .inline_cache_slot(0)
1736 .expect("MethodCall registers a slot");
1737 assert!(chunk.peek_method_cache(slot).is_none());
1738 }
1739
1740 #[test]
1741 fn peek_method_cache_returns_triple_after_warmup() {
1742 let mut chunk = Chunk::new();
1743 chunk.emit_method_call(7, 2, 1);
1744 let slot = chunk
1745 .inline_cache_slot(0)
1746 .expect("MethodCall registers a slot");
1747 chunk.set_inline_cache_entry(
1748 slot,
1749 InlineCacheEntry::Method {
1750 name_idx: 7,
1751 argc: 2,
1752 target: MethodCacheTarget::ListContains,
1753 },
1754 );
1755 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1756 assert_eq!(name_idx, 7);
1757 assert_eq!(argc, 2);
1758 assert_eq!(target, MethodCacheTarget::ListContains);
1759 }
1760
1761 #[test]
1762 fn peek_method_cache_returns_none_for_non_method_variants() {
1763 let mut chunk = Chunk::new();
1767 chunk.emit_method_call(0, 0, 1);
1768 let slot = chunk
1769 .inline_cache_slot(0)
1770 .expect("MethodCall registers a slot");
1771
1772 chunk.set_inline_cache_entry(
1773 slot,
1774 InlineCacheEntry::Property {
1775 name_idx: 0,
1776 target: PropertyCacheTarget::ListCount,
1777 },
1778 );
1779 assert!(chunk.peek_method_cache(slot).is_none());
1780 }
1781
1782 #[test]
1783 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1784 let chunk = Chunk::new();
1785 assert!(chunk.peek_method_cache(0).is_none());
1786 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1787 }
1788
1789 #[test]
1790 fn peek_method_cache_target_is_copy() {
1791 fn assert_copy<T: Copy>() {}
1797 assert_copy::<super::MethodCacheTarget>();
1798 }
1799
1800 #[test]
1810 fn peek_property_cache_returns_none_for_empty_slot() {
1811 let mut chunk = Chunk::new();
1812 chunk.emit_u16(Op::GetProperty, 0, 1);
1813 let slot = chunk
1814 .inline_cache_slot(0)
1815 .expect("GetProperty registers a slot");
1816 assert!(chunk.peek_property_cache(slot).is_none());
1817 }
1818
1819 #[test]
1820 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1821 let mut chunk = Chunk::new();
1822 chunk.emit_u16(Op::GetProperty, 0, 1);
1823 let slot = chunk
1824 .inline_cache_slot(0)
1825 .expect("GetProperty registers a slot");
1826 chunk.set_inline_cache_entry(
1827 slot,
1828 InlineCacheEntry::Property {
1829 name_idx: 11,
1830 target: PropertyCacheTarget::DictField(Arc::from("count")),
1831 },
1832 );
1833 let (name_idx, target) = chunk
1834 .peek_property_cache(slot)
1835 .expect("warmed property slot peek");
1836 assert_eq!(name_idx, 11);
1837 match target {
1838 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1839 other => panic!("expected DictField, got {other:?}"),
1840 }
1841 }
1842
1843 #[test]
1844 fn peek_property_cache_returns_pair_for_unit_target() {
1845 let mut chunk = Chunk::new();
1849 chunk.emit_u16(Op::GetProperty, 0, 1);
1850 let slot = chunk
1851 .inline_cache_slot(0)
1852 .expect("GetProperty registers a slot");
1853 chunk.set_inline_cache_entry(
1854 slot,
1855 InlineCacheEntry::Property {
1856 name_idx: 3,
1857 target: PropertyCacheTarget::ListCount,
1858 },
1859 );
1860 let (name_idx, target) = chunk
1861 .peek_property_cache(slot)
1862 .expect("warmed property slot peek");
1863 assert_eq!(name_idx, 3);
1864 assert_eq!(target, PropertyCacheTarget::ListCount);
1865 }
1866
1867 #[test]
1868 fn peek_property_cache_returns_none_for_non_property_variants() {
1869 let mut chunk = Chunk::new();
1870 chunk.emit_u16(Op::GetProperty, 0, 1);
1871 let slot = chunk
1872 .inline_cache_slot(0)
1873 .expect("GetProperty registers a slot");
1874 chunk.set_inline_cache_entry(
1875 slot,
1876 InlineCacheEntry::Method {
1877 name_idx: 0,
1878 argc: 0,
1879 target: MethodCacheTarget::ListCount,
1880 },
1881 );
1882 assert!(chunk.peek_property_cache(slot).is_none());
1883 }
1884
1885 #[test]
1886 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1887 let chunk = Chunk::new();
1888 assert!(chunk.peek_property_cache(0).is_none());
1889 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1890 }
1891
1892 #[test]
1902 fn peek_direct_call_state_returns_none_for_empty_slot() {
1903 let mut chunk = Chunk::new();
1904 chunk.emit_u8(Op::Call, 0, 1);
1905 let slot = chunk
1906 .inline_cache_slot(0)
1907 .expect("Op::Call registers a slot");
1908 assert!(chunk.peek_direct_call_state(slot).is_none());
1909 }
1910
1911 #[test]
1912 fn peek_direct_call_state_returns_warmup_state() {
1913 let mut chunk = Chunk::new();
1914 chunk.emit_u8(Op::Call, 0, 1);
1915 let slot = chunk
1916 .inline_cache_slot(0)
1917 .expect("Op::Call registers a slot");
1918 let target = synthetic_direct_call_target();
1919 chunk.set_inline_cache_entry(
1920 slot,
1921 InlineCacheEntry::DirectCall {
1922 state: DirectCallState::Warmup {
1923 argc: 2,
1924 target: target.clone(),
1925 hits: 1,
1926 },
1927 },
1928 );
1929 let state = chunk
1930 .peek_direct_call_state(slot)
1931 .expect("warmed direct-call slot peek");
1932 match state {
1933 DirectCallState::Warmup {
1934 argc,
1935 target: peeked_target,
1936 hits,
1937 } => {
1938 assert_eq!(argc, 2);
1939 assert_eq!(hits, 1);
1940 assert_eq!(peeked_target, target);
1941 }
1942 other => panic!("expected Warmup, got {other:?}"),
1943 }
1944 }
1945
1946 #[test]
1947 fn peek_direct_call_state_returns_specialized_state() {
1948 let mut chunk = Chunk::new();
1949 chunk.emit_u8(Op::Call, 0, 1);
1950 let slot = chunk
1951 .inline_cache_slot(0)
1952 .expect("Op::Call registers a slot");
1953 let target = synthetic_direct_call_target();
1954 chunk.set_inline_cache_entry(
1955 slot,
1956 InlineCacheEntry::DirectCall {
1957 state: DirectCallState::Specialized {
1958 argc: 3,
1959 target: target.clone(),
1960 hits: 100,
1961 misses: 0,
1962 },
1963 },
1964 );
1965 let state = chunk
1966 .peek_direct_call_state(slot)
1967 .expect("warmed direct-call slot peek");
1968 match state {
1969 DirectCallState::Specialized {
1970 argc,
1971 target: peeked_target,
1972 hits,
1973 misses,
1974 } => {
1975 assert_eq!(argc, 3);
1976 assert_eq!(hits, 100);
1977 assert_eq!(misses, 0);
1978 assert_eq!(peeked_target, target);
1979 }
1980 other => panic!("expected Specialized, got {other:?}"),
1981 }
1982 }
1983
1984 #[test]
1985 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
1986 let mut chunk = Chunk::new();
1987 chunk.emit_u8(Op::Call, 0, 1);
1988 let slot = chunk
1989 .inline_cache_slot(0)
1990 .expect("Op::Call registers a slot");
1991
1992 chunk.set_inline_cache_entry(
1993 slot,
1994 InlineCacheEntry::Property {
1995 name_idx: 0,
1996 target: PropertyCacheTarget::ListCount,
1997 },
1998 );
1999 assert!(chunk.peek_direct_call_state(slot).is_none());
2000 }
2001
2002 #[test]
2003 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2004 let chunk = Chunk::new();
2005 assert!(chunk.peek_direct_call_state(0).is_none());
2006 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2007 }
2008
2009 fn synthetic_direct_call_target() -> DirectCallTarget {
2013 use crate::value::VmClosure;
2014 use crate::{CompiledFunction, VmEnv};
2015 let func = CompiledFunction {
2016 name: "synthetic".to_string(),
2017 type_params: Vec::new(),
2018 nominal_type_names: Vec::new(),
2019 params: Vec::new(),
2020 default_start: None,
2021 chunk: Arc::new(Chunk::new()),
2022 is_generator: false,
2023 is_stream: false,
2024 has_rest_param: false,
2025 has_runtime_type_checks: false,
2026 };
2027 DirectCallTarget::Closure(Arc::new(VmClosure {
2028 func: Arc::new(func),
2029 env: VmEnv::new(),
2030 source_dir: None,
2031 module_functions: None,
2032 module_state: None,
2033 }))
2034 }
2035}