1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::rc::Rc;
5
6use harn_parser::TypeExpr;
7use serde::{Deserialize, Serialize};
8
9use crate::runtime_guards::RuntimeParamGuard;
10
11pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
19
20pub use crate::vm::ops::Op;
27pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum Constant {
32 Int(i64),
33 Float(f64),
34 String(String),
35 Bool(bool),
36 Nil,
37 Duration(i64),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) enum InlineCacheEntry {
51 Empty,
52 Property {
53 name_idx: u16,
54 target: PropertyCacheTarget,
55 },
56 Method {
57 name_idx: u16,
58 argc: usize,
59 target: MethodCacheTarget,
60 },
61 AdaptiveBinary {
62 op: AdaptiveBinaryOp,
63 state: AdaptiveBinaryState,
64 },
65 DirectCall {
66 state: DirectCallState,
67 },
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) enum AdaptiveBinaryOp {
72 Add,
73 Sub,
74 Mul,
75 Div,
76 Mod,
77 Equal,
78 NotEqual,
79 Less,
80 Greater,
81 LessEqual,
82 GreaterEqual,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub(crate) enum AdaptiveBinaryState {
95 Warmup {
96 shape: BinaryShape,
97 hits: u8,
98 },
99 Specialized {
100 shape: BinaryShape,
101 hits: u64,
102 misses: u64,
103 },
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub(crate) enum BinaryShape {
108 Int,
109 Float,
110 Bool,
111 String,
112}
113
114#[derive(Debug, Clone)]
115pub(crate) enum DirectCallState {
116 Warmup {
117 argc: usize,
118 target: DirectCallTarget,
119 hits: u8,
120 },
121 Specialized {
122 argc: usize,
123 target: DirectCallTarget,
124 hits: u64,
125 misses: u64,
126 },
127}
128
129#[derive(Debug, Clone)]
130pub(crate) enum DirectCallTarget {
131 Closure(Rc<crate::value::VmClosure>),
132}
133
134impl PartialEq for DirectCallTarget {
135 fn eq(&self, other: &Self) -> bool {
136 match (self, other) {
137 (Self::Closure(left), Self::Closure(right)) => Rc::ptr_eq(left, right),
138 }
139 }
140}
141
142impl Eq for DirectCallTarget {}
143
144impl PartialEq for DirectCallState {
145 fn eq(&self, other: &Self) -> bool {
146 match (self, other) {
147 (
148 Self::Warmup {
149 argc: left_argc,
150 target: left_target,
151 hits: left_hits,
152 },
153 Self::Warmup {
154 argc: right_argc,
155 target: right_target,
156 hits: right_hits,
157 },
158 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
159 (
160 Self::Specialized {
161 argc: left_argc,
162 target: left_target,
163 hits: left_hits,
164 misses: left_misses,
165 },
166 Self::Specialized {
167 argc: right_argc,
168 target: right_target,
169 hits: right_hits,
170 misses: right_misses,
171 },
172 ) => {
173 left_argc == right_argc
174 && left_target == right_target
175 && left_hits == right_hits
176 && left_misses == right_misses
177 }
178 _ => false,
179 }
180 }
181}
182
183impl Eq for DirectCallState {}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub(crate) enum PropertyCacheTarget {
187 DictField(Rc<str>),
188 StructField { field_name: Rc<str>, index: usize },
189 ListCount,
190 ListEmpty,
191 ListFirst,
192 ListLast,
193 StringCount,
194 StringEmpty,
195 PairFirst,
196 PairSecond,
197 EnumVariant,
198 EnumFields,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub(crate) enum MethodCacheTarget {
203 ListCount,
204 ListEmpty,
205 ListContains,
206 StringCount,
207 StringEmpty,
208 StringContains,
209 DictCount,
210 DictHas,
211 RangeCount,
212 RangeLen,
213 RangeEmpty,
214 RangeFirst,
215 RangeLast,
216 SetCount,
217 SetLen,
218 SetEmpty,
219 SetContains,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct LocalSlotInfo {
225 pub name: String,
226 pub mutable: bool,
227 pub scope_depth: usize,
228}
229
230impl fmt::Display for Constant {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 Constant::Int(n) => write!(f, "{n}"),
234 Constant::Float(n) => write!(f, "{n}"),
235 Constant::String(s) => write!(f, "\"{s}\""),
236 Constant::Bool(b) => write!(f, "{b}"),
237 Constant::Nil => write!(f, "nil"),
238 Constant::Duration(ms) => write!(f, "{ms}ms"),
239 }
240 }
241}
242
243#[derive(Debug, Clone)]
245pub struct Chunk {
246 pub code: Vec<u8>,
248 pub constants: Vec<Constant>,
250 pub lines: Vec<u32>,
252 pub columns: Vec<u32>,
255 pub source_file: Option<String>,
260 current_col: u32,
262 pub functions: Vec<CompiledFunctionRef>,
264 inline_cache_slots: BTreeMap<usize, usize>,
270 inline_cache_index: Vec<u32>,
278 inline_caches: Rc<RefCell<Vec<InlineCacheEntry>>>,
281 constant_strings: Rc<RefCell<Vec<Option<Rc<str>>>>>,
289 pub(crate) local_slots: Vec<LocalSlotInfo>,
291 pub(crate) references_outer_names: bool,
307}
308
309pub type ChunkRef = Rc<Chunk>;
310pub type CompiledFunctionRef = Rc<CompiledFunction>;
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct CachedChunk {
318 pub(crate) code: Vec<u8>,
319 pub(crate) constants: Vec<Constant>,
320 pub(crate) lines: Vec<u32>,
321 pub(crate) columns: Vec<u32>,
322 pub(crate) source_file: Option<String>,
323 pub(crate) current_col: u32,
324 pub(crate) functions: Vec<CachedCompiledFunction>,
325 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
326 pub(crate) local_slots: Vec<LocalSlotInfo>,
327 #[serde(default)]
328 pub(crate) references_outer_names: bool,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct CachedCompiledFunction {
333 pub(crate) name: String,
334 pub(crate) type_params: Vec<String>,
335 pub(crate) nominal_type_names: Vec<String>,
336 pub(crate) params: Vec<CachedParamSlot>,
337 pub(crate) default_start: Option<usize>,
338 pub(crate) chunk: CachedChunk,
339 pub(crate) is_generator: bool,
340 pub(crate) is_stream: bool,
341 pub(crate) has_rest_param: bool,
342 pub(crate) has_runtime_type_checks: bool,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub(crate) struct CachedParamSlot {
347 pub(crate) name: String,
348 pub(crate) type_expr: Option<TypeExpr>,
349 pub(crate) has_default: bool,
350}
351
352impl CachedParamSlot {
353 fn thaw(&self) -> ParamSlot {
354 ParamSlot {
355 name: self.name.clone(),
356 type_expr: self.type_expr.clone(),
357 runtime_guard: self
358 .type_expr
359 .as_ref()
360 .map(RuntimeParamGuard::from_type_expr),
361 has_default: self.has_default,
362 }
363 }
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct ParamSlot {
373 pub name: String,
374 pub type_expr: Option<TypeExpr>,
377 #[serde(skip)]
380 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
381 pub has_default: bool,
385}
386
387impl ParamSlot {
388 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
391 Self {
392 name: param.name.clone(),
393 type_expr: param.type_expr.clone(),
394 runtime_guard: param
395 .type_expr
396 .as_ref()
397 .map(RuntimeParamGuard::from_type_expr),
398 has_default: param.default_value.is_some(),
399 }
400 }
401
402 fn freeze_for_cache(&self) -> CachedParamSlot {
403 CachedParamSlot {
404 name: self.name.clone(),
405 type_expr: self.type_expr.clone(),
406 has_default: self.has_default,
407 }
408 }
409
410 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
415 params.iter().map(Self::from_typed_param).collect()
416 }
417}
418
419#[derive(Debug, Clone)]
421pub struct CompiledFunction {
422 pub name: String,
423 pub type_params: Vec<String>,
427 pub nominal_type_names: Vec<String>,
431 pub params: Vec<ParamSlot>,
432 pub default_start: Option<usize>,
434 pub chunk: ChunkRef,
435 pub is_generator: bool,
437 pub is_stream: bool,
439 pub has_rest_param: bool,
441 pub has_runtime_type_checks: bool,
446}
447
448impl CompiledFunction {
449 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
450 params.iter().any(|param| param.type_expr.is_some())
451 }
452
453 pub fn param_names(&self) -> impl Iterator<Item = &str> {
456 self.params.iter().map(|p| p.name.as_str())
457 }
458
459 pub fn required_param_count(&self) -> usize {
461 self.default_start.unwrap_or(self.params.len())
462 }
463
464 pub fn declares_type_param(&self, name: &str) -> bool {
465 self.type_params.iter().any(|param| param == name)
466 }
467
468 pub fn has_nominal_type(&self, name: &str) -> bool {
469 self.nominal_type_names.iter().any(|ty| ty == name)
470 }
471
472 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
473 CachedCompiledFunction {
474 name: self.name.clone(),
475 type_params: self.type_params.clone(),
476 nominal_type_names: self.nominal_type_names.clone(),
477 params: self
478 .params
479 .iter()
480 .map(ParamSlot::freeze_for_cache)
481 .collect(),
482 default_start: self.default_start,
483 chunk: self.chunk.freeze_for_cache(),
484 is_generator: self.is_generator,
485 is_stream: self.is_stream,
486 has_rest_param: self.has_rest_param,
487 has_runtime_type_checks: self.has_runtime_type_checks,
488 }
489 }
490
491 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
492 Self {
493 name: cached.name.clone(),
494 type_params: cached.type_params.clone(),
495 nominal_type_names: cached.nominal_type_names.clone(),
496 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
497 default_start: cached.default_start,
498 chunk: Rc::new(Chunk::from_cached(&cached.chunk)),
499 is_generator: cached.is_generator,
500 is_stream: cached.is_stream,
501 has_rest_param: cached.has_rest_param,
502 has_runtime_type_checks: cached.has_runtime_type_checks,
503 }
504 }
505}
506
507impl Chunk {
508 pub fn new() -> Self {
509 Self {
510 code: Vec::new(),
511 constants: Vec::new(),
512 lines: Vec::new(),
513 columns: Vec::new(),
514 source_file: None,
515 current_col: 0,
516 functions: Vec::new(),
517 inline_cache_slots: BTreeMap::new(),
518 inline_cache_index: Vec::new(),
519 inline_caches: Rc::new(RefCell::new(Vec::new())),
520 constant_strings: Rc::new(RefCell::new(Vec::new())),
521 local_slots: Vec::new(),
522 references_outer_names: false,
523 }
524 }
525
526 pub fn set_column(&mut self, col: u32) {
528 self.current_col = col;
529 }
530
531 pub fn add_constant(&mut self, constant: Constant) -> u16 {
533 for (i, c) in self.constants.iter().enumerate() {
534 if c == &constant {
535 return i as u16;
536 }
537 }
538 let idx = self.constants.len();
539 self.constants.push(constant);
540 idx as u16
541 }
542
543 pub fn emit(&mut self, op: Op, line: u32) {
545 let col = self.current_col;
546 let op_offset = self.code.len();
547 self.code.push(op as u8);
548 self.lines.push(line);
549 self.columns.push(col);
550 if is_adaptive_binary_op(op) {
551 self.register_inline_cache(op_offset);
552 }
553 if op_reads_outer_name(op) {
554 self.references_outer_names = true;
555 }
556 }
557
558 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
560 let col = self.current_col;
561 let op_offset = self.code.len();
562 self.code.push(op as u8);
563 self.code.push((arg >> 8) as u8);
564 self.code.push((arg & 0xFF) as u8);
565 self.lines.push(line);
566 self.lines.push(line);
567 self.lines.push(line);
568 self.columns.push(col);
569 self.columns.push(col);
570 self.columns.push(col);
571 if matches!(
572 op,
573 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
574 ) {
575 self.register_inline_cache(op_offset);
576 }
577 if op_reads_outer_name(op) {
578 self.references_outer_names = true;
579 }
580 }
581
582 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
584 let col = self.current_col;
585 let op_offset = self.code.len();
586 self.code.push(op as u8);
587 self.code.push(arg);
588 self.lines.push(line);
589 self.lines.push(line);
590 self.columns.push(col);
591 self.columns.push(col);
592 if matches!(op, Op::Call) {
593 self.register_inline_cache(op_offset);
594 }
595 if op_reads_outer_name(op) {
596 self.references_outer_names = true;
597 }
598 }
599
600 pub fn emit_call_builtin(
602 &mut self,
603 id: crate::BuiltinId,
604 name_idx: u16,
605 arg_count: u8,
606 line: u32,
607 ) {
608 let col = self.current_col;
609 let op_offset = self.code.len();
610 self.code.push(Op::CallBuiltin as u8);
611 self.code.extend_from_slice(&id.raw().to_be_bytes());
612 self.code.push((name_idx >> 8) as u8);
613 self.code.push((name_idx & 0xFF) as u8);
614 self.code.push(arg_count);
615 for _ in 0..12 {
616 self.lines.push(line);
617 self.columns.push(col);
618 }
619 self.register_inline_cache(op_offset);
620 self.references_outer_names = true;
621 }
622
623 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
625 let col = self.current_col;
626 self.code.push(Op::CallBuiltinSpread as u8);
627 self.code.extend_from_slice(&id.raw().to_be_bytes());
628 self.code.push((name_idx >> 8) as u8);
629 self.code.push((name_idx & 0xFF) as u8);
630 for _ in 0..11 {
631 self.lines.push(line);
632 self.columns.push(col);
633 }
634 self.references_outer_names = true;
635 }
636
637 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
639 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
640 }
641
642 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
644 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
645 }
646
647 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
648 let col = self.current_col;
649 let op_offset = self.code.len();
650 self.code.push(op as u8);
651 self.code.push((name_idx >> 8) as u8);
652 self.code.push((name_idx & 0xFF) as u8);
653 self.code.push(arg_count);
654 self.lines.push(line);
655 self.lines.push(line);
656 self.lines.push(line);
657 self.lines.push(line);
658 self.columns.push(col);
659 self.columns.push(col);
660 self.columns.push(col);
661 self.columns.push(col);
662 self.register_inline_cache(op_offset);
663 }
664
665 pub fn current_offset(&self) -> usize {
667 self.code.len()
668 }
669
670 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
672 let col = self.current_col;
673 self.code.push(op as u8);
674 let patch_pos = self.code.len();
675 self.code.push(0xFF);
676 self.code.push(0xFF);
677 self.lines.push(line);
678 self.lines.push(line);
679 self.lines.push(line);
680 self.columns.push(col);
681 self.columns.push(col);
682 self.columns.push(col);
683 patch_pos
684 }
685
686 pub fn patch_jump(&mut self, patch_pos: usize) {
688 let target = self.code.len() as u16;
689 self.code[patch_pos] = (target >> 8) as u8;
690 self.code[patch_pos + 1] = (target & 0xFF) as u8;
691 }
692
693 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
695 let target = target as u16;
696 self.code[patch_pos] = (target >> 8) as u8;
697 self.code[patch_pos + 1] = (target & 0xFF) as u8;
698 }
699
700 pub fn read_u16(&self, pos: usize) -> u16 {
702 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
703 }
704
705 fn register_inline_cache(&mut self, op_offset: usize) {
706 if self.inline_cache_slots.contains_key(&op_offset) {
707 return;
708 }
709 let mut entries = self.inline_caches.borrow_mut();
710 let slot = entries.len();
711 entries.push(InlineCacheEntry::Empty);
712 self.inline_cache_slots.insert(op_offset, slot);
713 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
714 }
715
716 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
721 if op_offset >= index.len() {
722 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
723 }
724 index[op_offset] = slot as u32;
725 }
726
727 #[inline]
736 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
737 match self.inline_cache_index.get(op_offset).copied() {
738 None | Some(NO_INLINE_CACHE_SLOT) => None,
739 Some(slot) => Some(slot as usize),
740 }
741 }
742
743 #[cfg(feature = "vm-bench-internals")]
750 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
751 self.inline_cache_slots.get(&op_offset).copied()
752 }
753
754 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Rc<str>> {
759 let mut entries = self.constant_strings.borrow_mut();
764 if entries.len() < self.constants.len() {
765 entries.resize(self.constants.len(), None);
766 }
767 if let Some(Some(existing)) = entries.get(idx) {
768 return Some(Rc::clone(existing));
769 }
770 let materialized = match self.constants.get(idx)? {
771 Constant::String(s) => Rc::<str>::from(s.as_str()),
772 _ => return None,
773 };
774 entries[idx] = Some(Rc::clone(&materialized));
775 Some(materialized)
776 }
777
778 #[cfg(feature = "vm-bench-internals")]
779 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
780 self.inline_caches
781 .borrow()
782 .get(slot)
783 .cloned()
784 .unwrap_or(InlineCacheEntry::Empty)
785 }
786
787 #[inline]
799 pub(crate) fn peek_adaptive_binary_cache(
800 &self,
801 slot: usize,
802 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
803 match self.inline_caches.borrow().get(slot)? {
804 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
805 _ => None,
806 }
807 }
808
809 #[inline]
821 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
822 match self.inline_caches.borrow().get(slot)? {
823 &InlineCacheEntry::Method {
824 name_idx,
825 argc,
826 target,
827 } => Some((name_idx, argc, target)),
828 _ => None,
829 }
830 }
831
832 #[inline]
842 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
843 match self.inline_caches.borrow().get(slot)? {
844 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
845 _ => None,
846 }
847 }
848
849 #[inline]
859 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
860 match self.inline_caches.borrow().get(slot)? {
861 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
862 _ => None,
863 }
864 }
865
866 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
867 if let Some(existing) = self.inline_caches.borrow_mut().get_mut(slot) {
868 *existing = entry;
869 }
870 }
871
872 pub fn freeze_for_cache(&self) -> CachedChunk {
873 CachedChunk {
874 code: self.code.clone(),
875 constants: self.constants.clone(),
876 lines: self.lines.clone(),
877 columns: self.columns.clone(),
878 source_file: self.source_file.clone(),
879 current_col: self.current_col,
880 functions: self
881 .functions
882 .iter()
883 .map(|function| function.freeze_for_cache())
884 .collect(),
885 inline_cache_slots: self.inline_cache_slots.clone(),
886 local_slots: self.local_slots.clone(),
887 references_outer_names: self.references_outer_names,
888 }
889 }
890
891 pub fn from_cached(cached: &CachedChunk) -> Self {
892 let inline_cache_count = cached.inline_cache_slots.len();
893 let constants_count = cached.constants.len();
894 let mut inline_cache_index = Vec::new();
901 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
902 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
903 if op_offset < inline_cache_index.len() {
904 inline_cache_index[op_offset] = slot as u32;
905 }
906 }
907 Self {
908 code: cached.code.clone(),
909 constants: cached.constants.clone(),
910 lines: cached.lines.clone(),
911 columns: cached.columns.clone(),
912 source_file: cached.source_file.clone(),
913 current_col: cached.current_col,
914 functions: cached
915 .functions
916 .iter()
917 .map(|function| Rc::new(CompiledFunction::from_cached(function)))
918 .collect(),
919 inline_cache_slots: cached.inline_cache_slots.clone(),
920 inline_cache_index,
921 inline_caches: Rc::new(RefCell::new(vec![
922 InlineCacheEntry::Empty;
923 inline_cache_count
924 ])),
925 constant_strings: Rc::new(RefCell::new(vec![None; constants_count])),
926 local_slots: cached.local_slots.clone(),
927 references_outer_names: cached.references_outer_names,
928 }
929 }
930
931 pub(crate) fn add_local_slot(
932 &mut self,
933 name: String,
934 mutable: bool,
935 scope_depth: usize,
936 ) -> u16 {
937 let idx = self.local_slots.len();
938 self.local_slots.push(LocalSlotInfo {
939 name,
940 mutable,
941 scope_depth,
942 });
943 idx as u16
944 }
945
946 #[cfg(test)]
947 pub(crate) fn inline_cache_entries(&self) -> Vec<InlineCacheEntry> {
948 self.inline_caches.borrow().clone()
949 }
950
951 pub fn read_u64(&self, pos: usize) -> u64 {
953 u64::from_be_bytes([
954 self.code[pos],
955 self.code[pos + 1],
956 self.code[pos + 2],
957 self.code[pos + 3],
958 self.code[pos + 4],
959 self.code[pos + 5],
960 self.code[pos + 6],
961 self.code[pos + 7],
962 ])
963 }
964
965 pub fn disassemble(&self, name: &str) -> String {
969 let mut out = format!("== {name} ==\n");
970 let mut ip = 0;
971 while ip < self.code.len() {
972 let op_byte = self.code[ip];
973 let line = self.lines.get(ip).copied().unwrap_or(0);
974 out.push_str(&format!("{ip:04} [{line:>4}] "));
975 ip += 1;
976
977 if let Some(op) = Op::from_byte(op_byte) {
978 self.disassemble_op(op, &mut ip, &mut out);
979 } else {
980 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
981 }
982 }
983 out
984 }
985}
986
987pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
998 label.to_string()
999}
1000
1001pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1002 let arg = chunk.code[*ip];
1003 *ip += 1;
1004 format!("{label} {arg:>4}")
1005}
1006
1007pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1008 let arg = chunk.read_u16(*ip);
1009 *ip += 2;
1010 format!("{label} {arg:>4}")
1011}
1012
1013pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1014 let idx = chunk.read_u16(*ip);
1015 *ip += 2;
1016 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1017}
1018
1019pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1020 let slot = chunk.read_u16(*ip);
1021 *ip += 2;
1022 let mut out = format!("{label} {slot:>4}");
1023 if let Some(info) = chunk.local_slots.get(slot as usize) {
1024 out.push_str(&format!(" ({})", info.name));
1025 }
1026 out
1027}
1028
1029pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1030 let idx = chunk.read_u16(*ip);
1031 *ip += 2;
1032 let argc = chunk.code[*ip];
1033 *ip += 1;
1034 format!(
1035 "{label} {idx:>4} ({}) argc={argc}",
1036 chunk.constants[idx as usize]
1037 )
1038}
1039
1040pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1041 let enum_idx = chunk.read_u16(*ip);
1042 *ip += 2;
1043 let var_idx = chunk.read_u16(*ip);
1044 *ip += 2;
1045 format!(
1046 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1047 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1048 )
1049}
1050
1051pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1052 let enum_idx = chunk.read_u16(*ip);
1053 *ip += 2;
1054 let var_idx = chunk.read_u16(*ip);
1055 *ip += 2;
1056 let field_count = chunk.read_u16(*ip);
1057 *ip += 2;
1058 format!(
1059 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1060 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1061 )
1062}
1063
1064pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1065 let path_idx = chunk.read_u16(*ip);
1066 *ip += 2;
1067 let names_idx = chunk.read_u16(*ip);
1068 *ip += 2;
1069 format!(
1070 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1071 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1072 )
1073}
1074
1075pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1076 let var_idx = chunk.read_u16(*ip);
1077 *ip += 2;
1078 let type_idx = chunk.read_u16(*ip);
1079 *ip += 2;
1080 format!(
1081 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1082 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1083 )
1084}
1085
1086pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1087 let id = chunk.read_u64(*ip);
1088 *ip += 8;
1089 let idx = chunk.read_u16(*ip);
1090 *ip += 2;
1091 let argc = chunk.code[*ip];
1092 *ip += 1;
1093 format!(
1094 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1095 chunk.constants[idx as usize],
1096 )
1097}
1098
1099pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1100 let id = chunk.read_u64(*ip);
1101 *ip += 8;
1102 let idx = chunk.read_u16(*ip);
1103 *ip += 2;
1104 format!(
1105 "{label} {id:#018x} {idx:>4} ({})",
1106 chunk.constants[idx as usize],
1107 )
1108}
1109
1110pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1111 let idx = chunk.read_u16(*ip);
1117 *ip += 2;
1118 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1119}
1120
1121impl Default for Chunk {
1122 fn default() -> Self {
1123 Self::new()
1124 }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use std::rc::Rc;
1130
1131 use super::{
1132 Chunk, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget, Op,
1133 PropertyCacheTarget,
1134 };
1135 use crate::BuiltinId;
1136
1137 #[test]
1138 fn op_from_byte_matches_repr_order() {
1139 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1140 assert_eq!(byte as u8, op as u8);
1141 assert_eq!(Op::from_byte(byte as u8), Some(op));
1142 }
1143 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1144 assert_eq!(Op::COUNT, Op::ALL.len());
1145 }
1146
1147 #[test]
1148 fn disassemble_covers_every_opcode_variant() {
1149 for op in Op::ALL.iter().copied() {
1159 let mut chunk = Chunk::new();
1160 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1161 for _ in 0..16 {
1165 chunk.code.push(0);
1166 }
1167 let mut ip: usize = 0;
1168 let mut out = String::new();
1169 chunk.disassemble_op(op, &mut ip, &mut out);
1170 assert!(
1171 !out.contains("UNKNOWN"),
1172 "disasm emitted UNKNOWN for {op:?}: {out}",
1173 );
1174 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1175 }
1176 }
1177
1178 #[test]
1187 fn empty_chunk_does_not_reference_outer_names() {
1188 let chunk = Chunk::new();
1189 assert!(!chunk.references_outer_names);
1190 }
1191
1192 #[test]
1193 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1194 let mut chunk = Chunk::new();
1199 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1200 chunk.emit_u16(Op::Constant, 0, 1);
1201 chunk.emit(Op::MulInt, 1);
1202 chunk.emit(Op::Pop, 1);
1203 chunk.emit(Op::Return, 1);
1204 assert!(!chunk.references_outer_names);
1205 }
1206
1207 #[test]
1208 fn slot_only_chunk_does_not_reference_outer_names() {
1209 let mut chunk = Chunk::new();
1211 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1212 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1213 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1214 assert!(!chunk.references_outer_names);
1215 }
1216
1217 #[test]
1218 fn get_var_flags_outer_name_reference() {
1219 let mut chunk = Chunk::new();
1220 chunk.emit_u16(Op::GetVar, 0, 1);
1221 assert!(chunk.references_outer_names);
1222 }
1223
1224 #[test]
1225 fn set_var_flags_outer_name_reference() {
1226 let mut chunk = Chunk::new();
1227 chunk.emit_u16(Op::SetVar, 0, 1);
1228 assert!(chunk.references_outer_names);
1229 }
1230
1231 #[test]
1232 fn check_type_flags_outer_name_reference() {
1233 let mut chunk = Chunk::new();
1234 chunk.emit_u16(Op::CheckType, 0, 1);
1235 assert!(chunk.references_outer_names);
1236 }
1237
1238 #[test]
1239 fn call_builtin_flags_outer_name_reference() {
1240 let mut chunk = Chunk::new();
1241 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1242 assert!(chunk.references_outer_names);
1243 }
1244
1245 #[test]
1246 fn call_builtin_spread_flags_outer_name_reference() {
1247 let mut chunk = Chunk::new();
1248 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1249 assert!(chunk.references_outer_names);
1250 }
1251
1252 #[test]
1253 fn tail_call_flags_outer_name_reference() {
1254 let mut chunk = Chunk::new();
1257 chunk.emit_u8(Op::TailCall, 1, 1);
1258 assert!(chunk.references_outer_names);
1259 }
1260
1261 #[test]
1262 fn call_flags_outer_name_reference() {
1263 let mut chunk = Chunk::new();
1266 chunk.emit_u8(Op::Call, 1, 1);
1267 assert!(chunk.references_outer_names);
1268 }
1269
1270 #[test]
1271 fn pipe_flags_outer_name_reference() {
1272 let mut chunk = Chunk::new();
1275 chunk.emit(Op::Pipe, 1);
1276 assert!(chunk.references_outer_names);
1277 }
1278
1279 #[test]
1280 fn method_call_does_not_flag_outer_name_reference() {
1281 let mut chunk = Chunk::new();
1284 chunk.emit_method_call(0, 1, 1);
1285 chunk.emit_method_call_opt(0, 1, 1);
1286 assert!(!chunk.references_outer_names);
1287 }
1288
1289 #[test]
1290 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1291 let mut chunk = Chunk::new();
1294 chunk.emit_u16(Op::Constant, 0, 1);
1295 chunk.emit(Op::JumpIfFalse, 1);
1296 chunk.emit(Op::Jump, 1);
1297 chunk.emit(Op::Return, 1);
1298 chunk.emit(Op::Pop, 1);
1299 assert!(!chunk.references_outer_names);
1300 }
1301
1302 #[test]
1303 fn references_outer_names_is_monotonic() {
1304 let mut chunk = Chunk::new();
1307 chunk.emit_u16(Op::GetVar, 0, 1);
1308 assert!(chunk.references_outer_names);
1309 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1310 chunk.emit(Op::MulInt, 1);
1311 assert!(chunk.references_outer_names);
1312 }
1313
1314 #[test]
1315 fn freeze_thaw_round_trips_references_outer_names() {
1316 let mut chunk = Chunk::new();
1320 chunk.emit_u16(Op::GetVar, 0, 1);
1321 assert!(chunk.references_outer_names);
1322 let frozen = chunk.freeze_for_cache();
1323 let thawed = Chunk::from_cached(&frozen);
1324 assert!(thawed.references_outer_names);
1325
1326 let plain = Chunk::new();
1327 assert!(!plain.references_outer_names);
1328 let frozen_plain = plain.freeze_for_cache();
1329 let thawed_plain = Chunk::from_cached(&frozen_plain);
1330 assert!(!thawed_plain.references_outer_names);
1331 }
1332
1333 #[test]
1345 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1346 let mut chunk = Chunk::new();
1349 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1350 chunk.emit(Op::Pop, 1);
1351 chunk.emit(Op::Return, 1);
1352 assert!(chunk.inline_cache_slot(0).is_none());
1353 assert!(chunk.inline_cache_slot(3).is_none());
1354 assert!(chunk.inline_cache_slot(4).is_none());
1355 }
1356
1357 #[test]
1358 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1359 let mut chunk = Chunk::new();
1363 chunk.emit(Op::Add, 1);
1364 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1365 }
1366
1367 #[test]
1368 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1369 let mut chunk = Chunk::new();
1374 chunk.emit(Op::Add, 1);
1375 chunk.emit(Op::Sub, 1);
1376 chunk.emit(Op::Mul, 1);
1377 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1378 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1379 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1380 assert_ne!(s0, s1);
1381 assert_ne!(s1, s2);
1382 assert_ne!(s0, s2);
1383 }
1384
1385 #[test]
1386 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1387 let mut chunk = Chunk::new();
1390 chunk.emit(Op::Add, 1);
1391 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1392 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1393 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1394 }
1395
1396 #[test]
1397 fn inline_cache_slot_for_get_property_and_method_call() {
1398 let mut chunk = Chunk::new();
1402 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");
1407 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1408 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1409 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1410 }
1411
1412 #[test]
1413 fn inline_cache_slot_for_call_and_call_builtin() {
1414 let mut chunk = Chunk::new();
1419 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1421 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1422 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1423 assert!(
1424 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1425 "Op::CallBuiltin IC slot"
1426 );
1427 }
1428
1429 #[test]
1430 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1431 let mut chunk = Chunk::new();
1437 chunk.emit(Op::Add, 1);
1438 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1439 chunk.register_inline_cache(0);
1441 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1442 assert_eq!(slot_before, slot_after);
1443 }
1444
1445 #[test]
1446 fn inline_cache_index_round_trips_through_cached_chunk() {
1447 let mut chunk = Chunk::new();
1454 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1455 chunk.emit_u16(Op::Constant, 0, 1);
1456 chunk.emit(Op::Add, 1);
1457 chunk.emit(Op::Sub, 1);
1458 chunk.emit_method_call(0, 1, 1);
1459 chunk.emit_u8(Op::Call, 1, 1);
1460 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1461 .map(|o| (o, chunk.inline_cache_slot(o)))
1462 .collect();
1463 let frozen = chunk.freeze_for_cache();
1464 let thawed = Chunk::from_cached(&frozen);
1465 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1466 .map(|o| (o, thawed.inline_cache_slot(o)))
1467 .collect();
1468 assert_eq!(live_slots, thawed_slots);
1469 }
1470
1471 #[test]
1472 fn inline_cache_index_agrees_with_btreemap_view() {
1473 let mut chunk = Chunk::new();
1479 chunk.emit(Op::Add, 1);
1480 chunk.emit_u16(Op::GetVar, 0, 1);
1481 chunk.emit(Op::LessInt, 1);
1482 chunk.emit_u8(Op::Call, 2, 1);
1483 chunk.emit(Op::Equal, 1);
1484 chunk.emit_u16(Op::GetProperty, 0, 1);
1485 chunk.emit_method_call_opt(0, 0, 1);
1486 for offset in 0..chunk.code.len() {
1487 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1488 let from_index = chunk.inline_cache_slot(offset);
1489 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1490 }
1491 }
1492
1493 #[test]
1504 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1505 let mut chunk = Chunk::new();
1506 chunk.emit(Op::Add, 1);
1507 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1508 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1510 }
1511
1512 #[test]
1513 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1514 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1515 let mut chunk = Chunk::new();
1516 chunk.emit(Op::Add, 1);
1517 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1518 chunk.set_inline_cache_entry(
1519 slot,
1520 InlineCacheEntry::AdaptiveBinary {
1521 op: AdaptiveBinaryOp::Add,
1522 state: AdaptiveBinaryState::Warmup {
1523 shape: BinaryShape::Int,
1524 hits: 2,
1525 },
1526 },
1527 );
1528 let (op, state) = chunk
1529 .peek_adaptive_binary_cache(slot)
1530 .expect("warmed slot peek");
1531 assert_eq!(op, AdaptiveBinaryOp::Add);
1532 assert!(matches!(
1533 state,
1534 AdaptiveBinaryState::Warmup {
1535 shape: BinaryShape::Int,
1536 hits: 2
1537 }
1538 ));
1539 }
1540
1541 #[test]
1542 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1543 use super::{InlineCacheEntry, PropertyCacheTarget};
1549 let mut chunk = Chunk::new();
1550 chunk.emit(Op::Add, 1);
1551 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1552 chunk.set_inline_cache_entry(
1553 slot,
1554 InlineCacheEntry::Property {
1555 name_idx: 0,
1556 target: PropertyCacheTarget::ListCount,
1557 },
1558 );
1559 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1560 }
1561
1562 #[test]
1563 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1564 let chunk = Chunk::new();
1569 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1570 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1571 }
1572
1573 #[test]
1574 fn peek_adaptive_binary_state_is_copy() {
1575 fn assert_copy<T: Copy>() {}
1581 assert_copy::<super::AdaptiveBinaryState>();
1582 assert_copy::<super::AdaptiveBinaryOp>();
1583 assert_copy::<super::BinaryShape>();
1584 }
1585
1586 #[test]
1597 fn peek_method_cache_returns_none_for_empty_slot() {
1598 let mut chunk = Chunk::new();
1599 chunk.emit_method_call(0, 0, 1);
1600 let slot = chunk
1601 .inline_cache_slot(0)
1602 .expect("MethodCall registers a slot");
1603 assert!(chunk.peek_method_cache(slot).is_none());
1604 }
1605
1606 #[test]
1607 fn peek_method_cache_returns_triple_after_warmup() {
1608 let mut chunk = Chunk::new();
1609 chunk.emit_method_call(7, 2, 1);
1610 let slot = chunk
1611 .inline_cache_slot(0)
1612 .expect("MethodCall registers a slot");
1613 chunk.set_inline_cache_entry(
1614 slot,
1615 InlineCacheEntry::Method {
1616 name_idx: 7,
1617 argc: 2,
1618 target: MethodCacheTarget::ListContains,
1619 },
1620 );
1621 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1622 assert_eq!(name_idx, 7);
1623 assert_eq!(argc, 2);
1624 assert_eq!(target, MethodCacheTarget::ListContains);
1625 }
1626
1627 #[test]
1628 fn peek_method_cache_returns_none_for_non_method_variants() {
1629 let mut chunk = Chunk::new();
1633 chunk.emit_method_call(0, 0, 1);
1634 let slot = chunk
1635 .inline_cache_slot(0)
1636 .expect("MethodCall registers a slot");
1637
1638 chunk.set_inline_cache_entry(
1639 slot,
1640 InlineCacheEntry::Property {
1641 name_idx: 0,
1642 target: PropertyCacheTarget::ListCount,
1643 },
1644 );
1645 assert!(chunk.peek_method_cache(slot).is_none());
1646 }
1647
1648 #[test]
1649 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1650 let chunk = Chunk::new();
1651 assert!(chunk.peek_method_cache(0).is_none());
1652 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1653 }
1654
1655 #[test]
1656 fn peek_method_cache_target_is_copy() {
1657 fn assert_copy<T: Copy>() {}
1663 assert_copy::<super::MethodCacheTarget>();
1664 }
1665
1666 #[test]
1676 fn peek_property_cache_returns_none_for_empty_slot() {
1677 let mut chunk = Chunk::new();
1678 chunk.emit_u16(Op::GetProperty, 0, 1);
1679 let slot = chunk
1680 .inline_cache_slot(0)
1681 .expect("GetProperty registers a slot");
1682 assert!(chunk.peek_property_cache(slot).is_none());
1683 }
1684
1685 #[test]
1686 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1687 let mut chunk = Chunk::new();
1688 chunk.emit_u16(Op::GetProperty, 0, 1);
1689 let slot = chunk
1690 .inline_cache_slot(0)
1691 .expect("GetProperty registers a slot");
1692 chunk.set_inline_cache_entry(
1693 slot,
1694 InlineCacheEntry::Property {
1695 name_idx: 11,
1696 target: PropertyCacheTarget::DictField(Rc::from("count")),
1697 },
1698 );
1699 let (name_idx, target) = chunk
1700 .peek_property_cache(slot)
1701 .expect("warmed property slot peek");
1702 assert_eq!(name_idx, 11);
1703 match target {
1704 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1705 other => panic!("expected DictField, got {other:?}"),
1706 }
1707 }
1708
1709 #[test]
1710 fn peek_property_cache_returns_pair_for_unit_target() {
1711 let mut chunk = Chunk::new();
1715 chunk.emit_u16(Op::GetProperty, 0, 1);
1716 let slot = chunk
1717 .inline_cache_slot(0)
1718 .expect("GetProperty registers a slot");
1719 chunk.set_inline_cache_entry(
1720 slot,
1721 InlineCacheEntry::Property {
1722 name_idx: 3,
1723 target: PropertyCacheTarget::ListCount,
1724 },
1725 );
1726 let (name_idx, target) = chunk
1727 .peek_property_cache(slot)
1728 .expect("warmed property slot peek");
1729 assert_eq!(name_idx, 3);
1730 assert_eq!(target, PropertyCacheTarget::ListCount);
1731 }
1732
1733 #[test]
1734 fn peek_property_cache_returns_none_for_non_property_variants() {
1735 let mut chunk = Chunk::new();
1736 chunk.emit_u16(Op::GetProperty, 0, 1);
1737 let slot = chunk
1738 .inline_cache_slot(0)
1739 .expect("GetProperty registers a slot");
1740 chunk.set_inline_cache_entry(
1741 slot,
1742 InlineCacheEntry::Method {
1743 name_idx: 0,
1744 argc: 0,
1745 target: MethodCacheTarget::ListCount,
1746 },
1747 );
1748 assert!(chunk.peek_property_cache(slot).is_none());
1749 }
1750
1751 #[test]
1752 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1753 let chunk = Chunk::new();
1754 assert!(chunk.peek_property_cache(0).is_none());
1755 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1756 }
1757
1758 #[test]
1768 fn peek_direct_call_state_returns_none_for_empty_slot() {
1769 let mut chunk = Chunk::new();
1770 chunk.emit_u8(Op::Call, 0, 1);
1771 let slot = chunk
1772 .inline_cache_slot(0)
1773 .expect("Op::Call registers a slot");
1774 assert!(chunk.peek_direct_call_state(slot).is_none());
1775 }
1776
1777 #[test]
1778 fn peek_direct_call_state_returns_warmup_state() {
1779 let mut chunk = Chunk::new();
1780 chunk.emit_u8(Op::Call, 0, 1);
1781 let slot = chunk
1782 .inline_cache_slot(0)
1783 .expect("Op::Call registers a slot");
1784 let target = synthetic_direct_call_target();
1785 chunk.set_inline_cache_entry(
1786 slot,
1787 InlineCacheEntry::DirectCall {
1788 state: DirectCallState::Warmup {
1789 argc: 2,
1790 target: target.clone(),
1791 hits: 1,
1792 },
1793 },
1794 );
1795 let state = chunk
1796 .peek_direct_call_state(slot)
1797 .expect("warmed direct-call slot peek");
1798 match state {
1799 DirectCallState::Warmup {
1800 argc,
1801 target: peeked_target,
1802 hits,
1803 } => {
1804 assert_eq!(argc, 2);
1805 assert_eq!(hits, 1);
1806 assert_eq!(peeked_target, target);
1807 }
1808 other => panic!("expected Warmup, got {other:?}"),
1809 }
1810 }
1811
1812 #[test]
1813 fn peek_direct_call_state_returns_specialized_state() {
1814 let mut chunk = Chunk::new();
1815 chunk.emit_u8(Op::Call, 0, 1);
1816 let slot = chunk
1817 .inline_cache_slot(0)
1818 .expect("Op::Call registers a slot");
1819 let target = synthetic_direct_call_target();
1820 chunk.set_inline_cache_entry(
1821 slot,
1822 InlineCacheEntry::DirectCall {
1823 state: DirectCallState::Specialized {
1824 argc: 3,
1825 target: target.clone(),
1826 hits: 100,
1827 misses: 0,
1828 },
1829 },
1830 );
1831 let state = chunk
1832 .peek_direct_call_state(slot)
1833 .expect("warmed direct-call slot peek");
1834 match state {
1835 DirectCallState::Specialized {
1836 argc,
1837 target: peeked_target,
1838 hits,
1839 misses,
1840 } => {
1841 assert_eq!(argc, 3);
1842 assert_eq!(hits, 100);
1843 assert_eq!(misses, 0);
1844 assert_eq!(peeked_target, target);
1845 }
1846 other => panic!("expected Specialized, got {other:?}"),
1847 }
1848 }
1849
1850 #[test]
1851 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
1852 let mut chunk = Chunk::new();
1853 chunk.emit_u8(Op::Call, 0, 1);
1854 let slot = chunk
1855 .inline_cache_slot(0)
1856 .expect("Op::Call registers a slot");
1857
1858 chunk.set_inline_cache_entry(
1859 slot,
1860 InlineCacheEntry::Property {
1861 name_idx: 0,
1862 target: PropertyCacheTarget::ListCount,
1863 },
1864 );
1865 assert!(chunk.peek_direct_call_state(slot).is_none());
1866 }
1867
1868 #[test]
1869 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
1870 let chunk = Chunk::new();
1871 assert!(chunk.peek_direct_call_state(0).is_none());
1872 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
1873 }
1874
1875 fn synthetic_direct_call_target() -> DirectCallTarget {
1879 use crate::value::VmClosure;
1880 use crate::{CompiledFunction, VmEnv};
1881 let func = CompiledFunction {
1882 name: "synthetic".to_string(),
1883 type_params: Vec::new(),
1884 nominal_type_names: Vec::new(),
1885 params: Vec::new(),
1886 default_start: None,
1887 chunk: Rc::new(Chunk::new()),
1888 is_generator: false,
1889 is_stream: false,
1890 has_rest_param: false,
1891 has_runtime_type_checks: false,
1892 };
1893 DirectCallTarget::Closure(Rc::new(VmClosure {
1894 func: Rc::new(func),
1895 env: VmEnv::new(),
1896 source_dir: None,
1897 module_functions: None,
1898 module_state: None,
1899 }))
1900 }
1901}