1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5
6use harn_parser::TypeExpr;
7use parking_lot::Mutex;
8use serde::{Deserialize, Serialize};
9
10use crate::harness::HarnessKind;
11use crate::runtime_guards::RuntimeParamGuard;
12
13pub(crate) const NO_INLINE_CACHE_SLOT: u32 = u32::MAX;
21static NEXT_CHUNK_CACHE_ID: AtomicU64 = AtomicU64::new(1);
22
23fn next_chunk_cache_id() -> u64 {
24 NEXT_CHUNK_CACHE_ID.fetch_add(1, Ordering::Relaxed)
25}
26
27pub use crate::vm::ops::Op;
34pub(crate) use crate::vm::ops::{is_adaptive_binary_op, op_reads_outer_name};
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum Constant {
39 Int(i64),
40 Float(f64),
41 String(String),
42 Bool(bool),
43 Nil,
44 Duration(i64),
45}
46
47fn constants_identical(a: &Constant, b: &Constant) -> bool {
56 match (a, b) {
57 (Constant::Float(x), Constant::Float(y)) => x.to_bits() == y.to_bits(),
58 _ => a == b,
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
72pub(crate) enum InlineCacheEntry {
73 Empty,
74 Property {
75 name_idx: u16,
76 target: PropertyCacheTarget,
77 },
78 Method {
79 name_idx: u16,
80 argc: usize,
81 target: MethodCacheTarget,
82 },
83 AdaptiveBinary {
84 op: AdaptiveBinaryOp,
85 state: AdaptiveBinaryState,
86 },
87 DirectCall {
88 state: DirectCallState,
89 },
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub(crate) enum AdaptiveBinaryOp {
94 Add,
95 Sub,
96 Mul,
97 Div,
98 Mod,
99 Equal,
100 NotEqual,
101 Less,
102 Greater,
103 LessEqual,
104 GreaterEqual,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub(crate) enum AdaptiveBinaryState {
114 Warmup {
115 shape: BinaryShape,
116 hits: u8,
117 },
118 Specialized {
119 shape: BinaryShape,
120 hits: u64,
121 misses: u64,
122 },
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub(crate) enum BinaryShape {
127 Int,
128 Float,
129 Bool,
130 String,
131}
132
133#[derive(Debug, Clone)]
134pub(crate) enum DirectCallState {
135 Warmup {
136 argc: usize,
137 target: DirectCallTarget,
138 hits: u8,
139 },
140 Specialized {
141 argc: usize,
142 target: DirectCallTarget,
143 hits: u64,
144 misses: u64,
145 },
146}
147
148#[derive(Debug, Clone)]
149pub(crate) enum DirectCallTarget {
150 Closure(Arc<crate::value::VmClosure>),
151}
152
153impl PartialEq for DirectCallTarget {
154 fn eq(&self, other: &Self) -> bool {
155 match (self, other) {
156 (Self::Closure(left), Self::Closure(right)) => Arc::ptr_eq(left, right),
157 }
158 }
159}
160
161impl Eq for DirectCallTarget {}
162
163impl PartialEq for DirectCallState {
164 fn eq(&self, other: &Self) -> bool {
165 match (self, other) {
166 (
167 Self::Warmup {
168 argc: left_argc,
169 target: left_target,
170 hits: left_hits,
171 },
172 Self::Warmup {
173 argc: right_argc,
174 target: right_target,
175 hits: right_hits,
176 },
177 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
178 (
179 Self::Specialized {
180 argc: left_argc,
181 target: left_target,
182 hits: left_hits,
183 misses: left_misses,
184 },
185 Self::Specialized {
186 argc: right_argc,
187 target: right_target,
188 hits: right_hits,
189 misses: right_misses,
190 },
191 ) => {
192 left_argc == right_argc
193 && left_target == right_target
194 && left_hits == right_hits
195 && left_misses == right_misses
196 }
197 _ => false,
198 }
199 }
200}
201
202impl Eq for DirectCallState {}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub(crate) enum PropertyCacheTarget {
206 DictField(Arc<str>),
207 StructField { field_name: Arc<str>, index: usize },
208 HarnessSubHandle(HarnessKind),
209 ListCount,
210 ListEmpty,
211 ListFirst,
212 ListLast,
213 StringCount,
214 StringEmpty,
215 PairFirst,
216 PairSecond,
217 EnumVariant,
218 EnumFields,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub(crate) enum MethodCacheTarget {
223 Harness(HarnessKind),
224 ListCount,
225 ListEmpty,
226 ListContains,
227 StringCount,
228 StringEmpty,
229 StringContains,
230 DictCount,
231 DictHas,
232 RangeCount,
233 RangeLen,
234 RangeEmpty,
235 RangeFirst,
236 RangeLast,
237 SetCount,
238 SetLen,
239 SetEmpty,
240 SetContains,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct LocalSlotInfo {
246 pub name: String,
247 pub mutable: bool,
248 pub scope_depth: usize,
249}
250
251impl fmt::Display for Constant {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 match self {
254 Constant::Int(n) => write!(f, "{n}"),
255 Constant::Float(n) => write!(f, "{n}"),
256 Constant::String(s) => write!(f, "\"{s}\""),
257 Constant::Bool(b) => write!(f, "{b}"),
258 Constant::Nil => write!(f, "nil"),
259 Constant::Duration(ms) => write!(f, "{ms}ms"),
260 }
261 }
262}
263
264#[derive(Debug)]
266pub struct Chunk {
267 cache_id: u64,
271 pub code: Vec<u8>,
273 pub constants: Vec<Constant>,
275 pub lines: Vec<u32>,
277 pub columns: Vec<u32>,
280 pub source_file: Option<String>,
285 current_col: u32,
287 pub functions: Vec<CompiledFunctionRef>,
289 inline_cache_slots: BTreeMap<usize, usize>,
295 inline_cache_index: Vec<u32>,
303 inline_caches: Arc<Mutex<Vec<InlineCacheEntry>>>,
307 constant_strings: Arc<Mutex<Vec<Option<Arc<str>>>>>,
311 pub(crate) local_slots: Vec<LocalSlotInfo>,
313 pub(crate) references_outer_names: bool,
329 #[cfg(debug_assertions)]
343 balance_depth: i32,
344 #[cfg(debug_assertions)]
345 balance_nonlinear: u32,
346}
347
348pub type ChunkRef = Arc<Chunk>;
349pub type CompiledFunctionRef = Arc<CompiledFunction>;
350
351impl Clone for Chunk {
352 fn clone(&self) -> Self {
353 Self {
354 cache_id: self.cache_id,
355 code: self.code.clone(),
356 constants: self.constants.clone(),
357 lines: self.lines.clone(),
358 columns: self.columns.clone(),
359 source_file: self.source_file.clone(),
360 current_col: self.current_col,
361 functions: self.functions.clone(),
362 inline_cache_slots: self.inline_cache_slots.clone(),
363 inline_cache_index: self.inline_cache_index.clone(),
364 inline_caches: Arc::new(Mutex::new(vec![
365 InlineCacheEntry::Empty;
366 self.inline_cache_slot_count()
367 ])),
368 constant_strings: Arc::new(Mutex::new(vec![None; self.constants.len()])),
369 local_slots: self.local_slots.clone(),
370 references_outer_names: self.references_outer_names,
371 #[cfg(debug_assertions)]
372 balance_depth: self.balance_depth,
373 #[cfg(debug_assertions)]
374 balance_nonlinear: self.balance_nonlinear,
375 }
376 }
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct CachedChunk {
385 pub(crate) code: Vec<u8>,
386 pub(crate) constants: Vec<Constant>,
387 pub(crate) lines: Vec<u32>,
388 pub(crate) columns: Vec<u32>,
389 pub(crate) source_file: Option<String>,
390 pub(crate) current_col: u32,
391 pub(crate) functions: Vec<CachedCompiledFunction>,
392 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
393 pub(crate) local_slots: Vec<LocalSlotInfo>,
394 #[serde(default)]
395 pub(crate) references_outer_names: bool,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct CachedCompiledFunction {
400 pub(crate) name: String,
401 pub(crate) type_params: Vec<String>,
402 pub(crate) nominal_type_names: Vec<String>,
403 pub(crate) params: Vec<CachedParamSlot>,
404 pub(crate) default_start: Option<usize>,
405 pub(crate) chunk: CachedChunk,
406 pub(crate) is_generator: bool,
407 pub(crate) is_stream: bool,
408 pub(crate) has_rest_param: bool,
409 pub(crate) has_runtime_type_checks: bool,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub(crate) struct CachedParamSlot {
414 pub(crate) name: String,
415 pub(crate) type_expr: Option<TypeExpr>,
416 pub(crate) has_default: bool,
417}
418
419impl CachedParamSlot {
420 fn thaw(&self) -> ParamSlot {
421 ParamSlot {
422 name: self.name.clone(),
423 type_expr: self.type_expr.clone(),
424 runtime_guard: self
425 .type_expr
426 .as_ref()
427 .map(RuntimeParamGuard::from_type_expr),
428 has_default: self.has_default,
429 }
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct ParamSlot {
440 pub name: String,
441 pub type_expr: Option<TypeExpr>,
444 #[serde(skip)]
447 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
448 pub has_default: bool,
452}
453
454impl ParamSlot {
455 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
458 Self {
459 name: param.name.clone(),
460 type_expr: param.type_expr.clone(),
461 runtime_guard: param
462 .type_expr
463 .as_ref()
464 .map(RuntimeParamGuard::from_type_expr),
465 has_default: param.default_value.is_some(),
466 }
467 }
468
469 fn freeze_for_cache(&self) -> CachedParamSlot {
470 CachedParamSlot {
471 name: self.name.clone(),
472 type_expr: self.type_expr.clone(),
473 has_default: self.has_default,
474 }
475 }
476
477 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
482 params.iter().map(Self::from_typed_param).collect()
483 }
484}
485
486#[derive(Debug, Clone)]
488pub struct CompiledFunction {
489 pub name: String,
490 pub type_params: Vec<String>,
494 pub nominal_type_names: Vec<String>,
498 pub params: Vec<ParamSlot>,
499 pub default_start: Option<usize>,
501 pub chunk: ChunkRef,
502 pub is_generator: bool,
504 pub is_stream: bool,
506 pub has_rest_param: bool,
508 pub has_runtime_type_checks: bool,
513}
514
515impl CompiledFunction {
516 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
517 params.iter().any(|param| param.type_expr.is_some())
518 }
519
520 pub fn param_names(&self) -> impl Iterator<Item = &str> {
523 self.params.iter().map(|p| p.name.as_str())
524 }
525
526 pub fn required_param_count(&self) -> usize {
528 self.default_start.unwrap_or(self.params.len())
529 }
530
531 pub fn declares_type_param(&self, name: &str) -> bool {
532 self.type_params.iter().any(|param| param == name)
533 }
534
535 pub fn has_nominal_type(&self, name: &str) -> bool {
536 self.nominal_type_names.iter().any(|ty| ty == name)
537 }
538
539 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
540 CachedCompiledFunction {
541 name: self.name.clone(),
542 type_params: self.type_params.clone(),
543 nominal_type_names: self.nominal_type_names.clone(),
544 params: self
545 .params
546 .iter()
547 .map(ParamSlot::freeze_for_cache)
548 .collect(),
549 default_start: self.default_start,
550 chunk: self.chunk.freeze_for_cache(),
551 is_generator: self.is_generator,
552 is_stream: self.is_stream,
553 has_rest_param: self.has_rest_param,
554 has_runtime_type_checks: self.has_runtime_type_checks,
555 }
556 }
557
558 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
559 Self {
560 name: cached.name.clone(),
561 type_params: cached.type_params.clone(),
562 nominal_type_names: cached.nominal_type_names.clone(),
563 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
564 default_start: cached.default_start,
565 chunk: Arc::new(Chunk::from_cached(&cached.chunk)),
566 is_generator: cached.is_generator,
567 is_stream: cached.is_stream,
568 has_rest_param: cached.has_rest_param,
569 has_runtime_type_checks: cached.has_runtime_type_checks,
570 }
571 }
572}
573
574#[cfg(debug_assertions)]
577#[derive(Clone, Copy)]
578pub(crate) struct BalanceProbe {
579 depth: i32,
580 nonlinear: u32,
581}
582
583#[cfg(debug_assertions)]
600fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
601 use Op::*;
602 let count = count as i32;
603 Some(match op {
604 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
606 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty | Pop => -1,
610 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
614 | PushScope | PopScope | PopIterator | PopHandler => 0,
615 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
617 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
618 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
619 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
620 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
621 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
622 IterInit => -1,
625 Slice | SetSubscript => -2,
627 BuildList | Concat | CallBuiltin => 1 - count,
629 BuildDict => 1 - 2 * count,
630 Call | MethodCall | MethodCallOpt => -count,
632 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
635 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
636 | SyncMutexEnter | SyncMutexEnterKeyed | TaskScopeEnter | TaskScopeExit | Import
637 | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum | MatchEnum | Yield
638 | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
639 })
640}
641
642impl Chunk {
643 pub fn new() -> Self {
644 Self {
645 cache_id: next_chunk_cache_id(),
646 code: Vec::new(),
647 constants: Vec::new(),
648 lines: Vec::new(),
649 columns: Vec::new(),
650 source_file: None,
651 current_col: 0,
652 functions: Vec::new(),
653 inline_cache_slots: BTreeMap::new(),
654 inline_cache_index: Vec::new(),
655 inline_caches: Arc::new(Mutex::new(Vec::new())),
656 constant_strings: Arc::new(Mutex::new(Vec::new())),
657 local_slots: Vec::new(),
658 references_outer_names: false,
659 #[cfg(debug_assertions)]
660 balance_depth: 0,
661 #[cfg(debug_assertions)]
662 balance_nonlinear: 0,
663 }
664 }
665
666 pub fn set_column(&mut self, col: u32) {
668 self.current_col = col;
669 }
670
671 pub fn add_constant(&mut self, constant: Constant) -> u16 {
673 for (i, c) in self.constants.iter().enumerate() {
674 if constants_identical(c, &constant) {
675 return i as u16;
676 }
677 }
678 let idx = self.constants.len();
679 self.constants.push(constant);
680 idx as u16
681 }
682
683 pub fn emit(&mut self, op: Op, line: u32) {
685 #[cfg(debug_assertions)]
686 self.note_balance(op, 0);
687 let col = self.current_col;
688 let op_offset = self.code.len();
689 self.code.push(op as u8);
690 self.lines.push(line);
691 self.columns.push(col);
692 if is_adaptive_binary_op(op) {
693 self.register_inline_cache(op_offset);
694 }
695 if op_reads_outer_name(op) {
696 self.references_outer_names = true;
697 }
698 }
699
700 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
702 #[cfg(debug_assertions)]
703 self.note_balance(op, arg);
704 let col = self.current_col;
705 let op_offset = self.code.len();
706 self.code.push(op as u8);
707 self.code.push((arg >> 8) as u8);
708 self.code.push((arg & 0xFF) as u8);
709 self.lines.push(line);
710 self.lines.push(line);
711 self.lines.push(line);
712 self.columns.push(col);
713 self.columns.push(col);
714 self.columns.push(col);
715 if matches!(
716 op,
717 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
718 ) {
719 self.register_inline_cache(op_offset);
720 }
721 if op_reads_outer_name(op) {
722 self.references_outer_names = true;
723 }
724 }
725
726 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
728 #[cfg(debug_assertions)]
729 self.note_balance(op, arg as u16);
730 let col = self.current_col;
731 let op_offset = self.code.len();
732 self.code.push(op as u8);
733 self.code.push(arg);
734 self.lines.push(line);
735 self.lines.push(line);
736 self.columns.push(col);
737 self.columns.push(col);
738 if matches!(op, Op::Call) {
739 self.register_inline_cache(op_offset);
740 }
741 if op_reads_outer_name(op) {
742 self.references_outer_names = true;
743 }
744 }
745
746 pub fn emit_call_builtin(
748 &mut self,
749 id: crate::BuiltinId,
750 name_idx: u16,
751 arg_count: u8,
752 line: u32,
753 ) {
754 #[cfg(debug_assertions)]
755 self.note_balance(Op::CallBuiltin, arg_count as u16);
756 let col = self.current_col;
757 let op_offset = self.code.len();
758 self.code.push(Op::CallBuiltin as u8);
759 self.code.extend_from_slice(&id.raw().to_be_bytes());
760 self.code.push((name_idx >> 8) as u8);
761 self.code.push((name_idx & 0xFF) as u8);
762 self.code.push(arg_count);
763 for _ in 0..12 {
764 self.lines.push(line);
765 self.columns.push(col);
766 }
767 self.register_inline_cache(op_offset);
768 self.references_outer_names = true;
769 }
770
771 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
773 #[cfg(debug_assertions)]
774 self.note_balance(Op::CallBuiltinSpread, 0);
775 let col = self.current_col;
776 self.code.push(Op::CallBuiltinSpread as u8);
777 self.code.extend_from_slice(&id.raw().to_be_bytes());
778 self.code.push((name_idx >> 8) as u8);
779 self.code.push((name_idx & 0xFF) as u8);
780 for _ in 0..11 {
781 self.lines.push(line);
782 self.columns.push(col);
783 }
784 self.references_outer_names = true;
785 }
786
787 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
789 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
790 }
791
792 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
794 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
795 }
796
797 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
798 #[cfg(debug_assertions)]
799 self.note_balance(op, arg_count as u16);
800 let col = self.current_col;
801 let op_offset = self.code.len();
802 self.code.push(op as u8);
803 self.code.push((name_idx >> 8) as u8);
804 self.code.push((name_idx & 0xFF) as u8);
805 self.code.push(arg_count);
806 self.lines.push(line);
807 self.lines.push(line);
808 self.lines.push(line);
809 self.lines.push(line);
810 self.columns.push(col);
811 self.columns.push(col);
812 self.columns.push(col);
813 self.columns.push(col);
814 self.register_inline_cache(op_offset);
815 }
816
817 pub fn current_offset(&self) -> usize {
819 self.code.len()
820 }
821
822 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
824 #[cfg(debug_assertions)]
825 self.note_balance(op, 0);
826 let col = self.current_col;
827 self.code.push(op as u8);
828 let patch_pos = self.code.len();
829 self.code.push(0xFF);
830 self.code.push(0xFF);
831 self.lines.push(line);
832 self.lines.push(line);
833 self.lines.push(line);
834 self.columns.push(col);
835 self.columns.push(col);
836 self.columns.push(col);
837 patch_pos
838 }
839
840 pub fn patch_jump(&mut self, patch_pos: usize) {
842 let target = self.code.len() as u16;
843 self.code[patch_pos] = (target >> 8) as u8;
844 self.code[patch_pos + 1] = (target & 0xFF) as u8;
845 }
846
847 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
849 let target = target as u16;
850 self.code[patch_pos] = (target >> 8) as u8;
851 self.code[patch_pos + 1] = (target & 0xFF) as u8;
852 }
853
854 pub fn read_u16(&self, pos: usize) -> u16 {
856 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
857 }
858
859 #[cfg(debug_assertions)]
863 fn note_balance(&mut self, op: Op, count: u16) {
864 match op_stack_delta(op, count) {
865 Some(delta) => self.balance_depth += delta,
866 None => self.balance_nonlinear += 1,
867 }
868 }
869
870 #[cfg(debug_assertions)]
873 pub(crate) fn balance_probe(&self) -> BalanceProbe {
874 BalanceProbe {
875 depth: self.balance_depth,
876 nonlinear: self.balance_nonlinear,
877 }
878 }
879
880 #[cfg(debug_assertions)]
886 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
887 if self.balance_nonlinear == probe.nonlinear {
888 Some(self.balance_depth - probe.depth)
889 } else {
890 None
891 }
892 }
893
894 fn register_inline_cache(&mut self, op_offset: usize) {
895 if self.inline_cache_slots.contains_key(&op_offset) {
896 return;
897 }
898 let mut entries = self.inline_caches.lock();
899 let slot = entries.len();
900 entries.push(InlineCacheEntry::Empty);
901 self.inline_cache_slots.insert(op_offset, slot);
902 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
903 }
904
905 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
910 if op_offset >= index.len() {
911 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
912 }
913 index[op_offset] = slot as u32;
914 }
915
916 #[inline]
925 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
926 match self.inline_cache_index.get(op_offset).copied() {
927 None | Some(NO_INLINE_CACHE_SLOT) => None,
928 Some(slot) => Some(slot as usize),
929 }
930 }
931
932 pub(crate) fn inline_cache_slot_count(&self) -> usize {
933 self.inline_cache_slots.len()
934 }
935
936 pub(crate) fn cache_id(&self) -> u64 {
937 self.cache_id
938 }
939
940 #[cfg(feature = "vm-bench-internals")]
947 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
948 self.inline_cache_slots.get(&op_offset).copied()
949 }
950
951 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
956 let mut entries = self.constant_strings.lock();
961 if entries.len() < self.constants.len() {
962 entries.resize(self.constants.len(), None);
963 }
964 if let Some(Some(existing)) = entries.get(idx) {
965 return Some(Arc::clone(existing));
966 }
967 let materialized = match self.constants.get(idx)? {
968 Constant::String(s) => Arc::<str>::from(s.as_str()),
969 _ => return None,
970 };
971 entries[idx] = Some(Arc::clone(&materialized));
972 Some(materialized)
973 }
974
975 #[cfg(feature = "vm-bench-internals")]
976 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
977 self.inline_caches
978 .lock()
979 .get(slot)
980 .cloned()
981 .unwrap_or(InlineCacheEntry::Empty)
982 }
983
984 #[inline]
996 #[cfg(any(test, feature = "vm-bench-internals"))]
997 pub(crate) fn peek_adaptive_binary_cache(
998 &self,
999 slot: usize,
1000 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
1001 match self.inline_caches.lock().get(slot)? {
1002 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
1003 _ => None,
1004 }
1005 }
1006
1007 #[inline]
1019 #[cfg(any(test, feature = "vm-bench-internals"))]
1020 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1021 match self.inline_caches.lock().get(slot)? {
1022 &InlineCacheEntry::Method {
1023 name_idx,
1024 argc,
1025 target,
1026 } => Some((name_idx, argc, target)),
1027 _ => None,
1028 }
1029 }
1030
1031 #[inline]
1041 #[cfg(any(test, feature = "vm-bench-internals"))]
1042 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1043 match self.inline_caches.lock().get(slot)? {
1044 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1045 _ => None,
1046 }
1047 }
1048
1049 #[inline]
1059 #[cfg(any(test, feature = "vm-bench-internals"))]
1060 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1061 match self.inline_caches.lock().get(slot)? {
1062 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1063 _ => None,
1064 }
1065 }
1066
1067 #[cfg(any(test, feature = "vm-bench-internals"))]
1068 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1069 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1070 *existing = entry;
1071 }
1072 }
1073
1074 pub fn freeze_for_cache(&self) -> CachedChunk {
1075 CachedChunk {
1076 code: self.code.clone(),
1077 constants: self.constants.clone(),
1078 lines: self.lines.clone(),
1079 columns: self.columns.clone(),
1080 source_file: self.source_file.clone(),
1081 current_col: self.current_col,
1082 functions: self
1083 .functions
1084 .iter()
1085 .map(|function| function.freeze_for_cache())
1086 .collect(),
1087 inline_cache_slots: self.inline_cache_slots.clone(),
1088 local_slots: self.local_slots.clone(),
1089 references_outer_names: self.references_outer_names,
1090 }
1091 }
1092
1093 pub fn from_cached(cached: &CachedChunk) -> Self {
1094 let inline_cache_count = cached.inline_cache_slots.len();
1095 let constants_count = cached.constants.len();
1096 let mut inline_cache_index = Vec::new();
1103 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1104 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1105 if op_offset < inline_cache_index.len() {
1106 inline_cache_index[op_offset] = slot as u32;
1107 }
1108 }
1109 Self {
1110 cache_id: next_chunk_cache_id(),
1111 code: cached.code.clone(),
1112 constants: cached.constants.clone(),
1113 lines: cached.lines.clone(),
1114 columns: cached.columns.clone(),
1115 source_file: cached.source_file.clone(),
1116 current_col: cached.current_col,
1117 functions: cached
1118 .functions
1119 .iter()
1120 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1121 .collect(),
1122 inline_cache_slots: cached.inline_cache_slots.clone(),
1123 inline_cache_index,
1124 inline_caches: Arc::new(Mutex::new(vec![
1125 InlineCacheEntry::Empty;
1126 inline_cache_count
1127 ])),
1128 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1129 local_slots: cached.local_slots.clone(),
1130 references_outer_names: cached.references_outer_names,
1131 #[cfg(debug_assertions)]
1132 balance_depth: 0,
1133 #[cfg(debug_assertions)]
1134 balance_nonlinear: 0,
1135 }
1136 }
1137
1138 pub(crate) fn add_local_slot(
1139 &mut self,
1140 name: String,
1141 mutable: bool,
1142 scope_depth: usize,
1143 ) -> u16 {
1144 let idx = self.local_slots.len();
1145 self.local_slots.push(LocalSlotInfo {
1146 name,
1147 mutable,
1148 scope_depth,
1149 });
1150 idx as u16
1151 }
1152
1153 pub fn read_u64(&self, pos: usize) -> u64 {
1155 u64::from_be_bytes([
1156 self.code[pos],
1157 self.code[pos + 1],
1158 self.code[pos + 2],
1159 self.code[pos + 3],
1160 self.code[pos + 4],
1161 self.code[pos + 5],
1162 self.code[pos + 6],
1163 self.code[pos + 7],
1164 ])
1165 }
1166
1167 pub fn disassemble(&self, name: &str) -> String {
1171 let mut out = format!("== {name} ==\n");
1172 let mut ip = 0;
1173 while ip < self.code.len() {
1174 let op_byte = self.code[ip];
1175 let line = self.lines.get(ip).copied().unwrap_or(0);
1176 out.push_str(&format!("{ip:04} [{line:>4}] "));
1177 ip += 1;
1178
1179 if let Some(op) = Op::from_byte(op_byte) {
1180 self.disassemble_op(op, &mut ip, &mut out);
1181 } else {
1182 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1183 }
1184 }
1185 out
1186 }
1187}
1188
1189pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1200 label.to_string()
1201}
1202
1203pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1204 let arg = chunk.code[*ip];
1205 *ip += 1;
1206 format!("{label} {arg:>4}")
1207}
1208
1209pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1210 let arg = chunk.read_u16(*ip);
1211 *ip += 2;
1212 format!("{label} {arg:>4}")
1213}
1214
1215pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1216 let catch_offset = chunk.read_u16(*ip);
1217 *ip += 2;
1218 let type_idx = chunk.read_u16(*ip);
1219 *ip += 2;
1220 if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1221 format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1222 } else {
1223 format!("{label} {catch_offset:>4} type {type_idx:>4}")
1224 }
1225}
1226
1227pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1228 let idx = chunk.read_u16(*ip);
1229 *ip += 2;
1230 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1231}
1232
1233pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1234 let slot = chunk.read_u16(*ip);
1235 *ip += 2;
1236 let mut out = format!("{label} {slot:>4}");
1237 if let Some(info) = chunk.local_slots.get(slot as usize) {
1238 out.push_str(&format!(" ({})", info.name));
1239 }
1240 out
1241}
1242
1243pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1244 let idx = chunk.read_u16(*ip);
1245 *ip += 2;
1246 let argc = chunk.code[*ip];
1247 *ip += 1;
1248 format!(
1249 "{label} {idx:>4} ({}) argc={argc}",
1250 chunk.constants[idx as usize]
1251 )
1252}
1253
1254pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1255 let enum_idx = chunk.read_u16(*ip);
1256 *ip += 2;
1257 let var_idx = chunk.read_u16(*ip);
1258 *ip += 2;
1259 format!(
1260 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1261 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1262 )
1263}
1264
1265pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1266 let enum_idx = chunk.read_u16(*ip);
1267 *ip += 2;
1268 let var_idx = chunk.read_u16(*ip);
1269 *ip += 2;
1270 let field_count = chunk.read_u16(*ip);
1271 *ip += 2;
1272 format!(
1273 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1274 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1275 )
1276}
1277
1278pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1279 let path_idx = chunk.read_u16(*ip);
1280 *ip += 2;
1281 let names_idx = chunk.read_u16(*ip);
1282 *ip += 2;
1283 format!(
1284 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1285 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1286 )
1287}
1288
1289pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1290 let var_idx = chunk.read_u16(*ip);
1291 *ip += 2;
1292 let type_idx = chunk.read_u16(*ip);
1293 *ip += 2;
1294 format!(
1295 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1296 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1297 )
1298}
1299
1300pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1301 let id = chunk.read_u64(*ip);
1302 *ip += 8;
1303 let idx = chunk.read_u16(*ip);
1304 *ip += 2;
1305 let argc = chunk.code[*ip];
1306 *ip += 1;
1307 format!(
1308 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1309 chunk.constants[idx as usize],
1310 )
1311}
1312
1313pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1314 let id = chunk.read_u64(*ip);
1315 *ip += 8;
1316 let idx = chunk.read_u16(*ip);
1317 *ip += 2;
1318 format!(
1319 "{label} {id:#018x} {idx:>4} ({})",
1320 chunk.constants[idx as usize],
1321 )
1322}
1323
1324pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1325 let idx = chunk.read_u16(*ip);
1331 *ip += 2;
1332 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1333}
1334
1335impl Default for Chunk {
1336 fn default() -> Self {
1337 Self::new()
1338 }
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343 use std::sync::Arc;
1344
1345 use super::{
1346 Chunk, Constant, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget,
1347 Op, PropertyCacheTarget,
1348 };
1349 use crate::BuiltinId;
1350
1351 #[test]
1352 fn op_from_byte_matches_repr_order() {
1353 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1354 assert_eq!(byte as u8, op as u8);
1355 assert_eq!(Op::from_byte(byte as u8), Some(op));
1356 }
1357 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1358 assert_eq!(Op::COUNT, Op::ALL.len());
1359 }
1360
1361 #[test]
1362 fn disassemble_covers_every_opcode_variant() {
1363 for op in Op::ALL.iter().copied() {
1373 let mut chunk = Chunk::new();
1374 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1375 for _ in 0..16 {
1379 chunk.code.push(0);
1380 }
1381 let mut ip: usize = 0;
1382 let mut out = String::new();
1383 chunk.disassemble_op(op, &mut ip, &mut out);
1384 assert!(
1385 !out.contains("UNKNOWN"),
1386 "disasm emitted UNKNOWN for {op:?}: {out}",
1387 );
1388 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1389 }
1390 }
1391
1392 #[test]
1401 fn empty_chunk_does_not_reference_outer_names() {
1402 let chunk = Chunk::new();
1403 assert!(!chunk.references_outer_names);
1404 }
1405
1406 #[test]
1407 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1408 let mut chunk = Chunk::new();
1413 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1414 chunk.emit_u16(Op::Constant, 0, 1);
1415 chunk.emit(Op::MulInt, 1);
1416 chunk.emit(Op::Pop, 1);
1417 chunk.emit(Op::Return, 1);
1418 assert!(!chunk.references_outer_names);
1419 }
1420
1421 #[test]
1422 fn slot_only_chunk_does_not_reference_outer_names() {
1423 let mut chunk = Chunk::new();
1425 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1426 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1427 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1428 assert!(!chunk.references_outer_names);
1429 }
1430
1431 #[test]
1432 fn get_var_flags_outer_name_reference() {
1433 let mut chunk = Chunk::new();
1434 chunk.emit_u16(Op::GetVar, 0, 1);
1435 assert!(chunk.references_outer_names);
1436 }
1437
1438 #[test]
1439 fn set_var_flags_outer_name_reference() {
1440 let mut chunk = Chunk::new();
1441 chunk.emit_u16(Op::SetVar, 0, 1);
1442 assert!(chunk.references_outer_names);
1443 }
1444
1445 #[test]
1446 fn check_type_flags_outer_name_reference() {
1447 let mut chunk = Chunk::new();
1448 chunk.emit_u16(Op::CheckType, 0, 1);
1449 assert!(chunk.references_outer_names);
1450 }
1451
1452 #[test]
1453 fn call_builtin_flags_outer_name_reference() {
1454 let mut chunk = Chunk::new();
1455 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1456 assert!(chunk.references_outer_names);
1457 }
1458
1459 #[test]
1460 fn call_builtin_spread_flags_outer_name_reference() {
1461 let mut chunk = Chunk::new();
1462 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1463 assert!(chunk.references_outer_names);
1464 }
1465
1466 #[test]
1467 fn tail_call_flags_outer_name_reference() {
1468 let mut chunk = Chunk::new();
1471 chunk.emit_u8(Op::TailCall, 1, 1);
1472 assert!(chunk.references_outer_names);
1473 }
1474
1475 #[test]
1476 fn call_flags_outer_name_reference() {
1477 let mut chunk = Chunk::new();
1480 chunk.emit_u8(Op::Call, 1, 1);
1481 assert!(chunk.references_outer_names);
1482 }
1483
1484 #[test]
1485 fn pipe_flags_outer_name_reference() {
1486 let mut chunk = Chunk::new();
1489 chunk.emit(Op::Pipe, 1);
1490 assert!(chunk.references_outer_names);
1491 }
1492
1493 #[test]
1494 fn method_call_does_not_flag_outer_name_reference() {
1495 let mut chunk = Chunk::new();
1498 chunk.emit_method_call(0, 1, 1);
1499 chunk.emit_method_call_opt(0, 1, 1);
1500 assert!(!chunk.references_outer_names);
1501 }
1502
1503 #[test]
1504 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1505 let mut chunk = Chunk::new();
1508 chunk.emit_u16(Op::Constant, 0, 1);
1509 chunk.emit(Op::JumpIfFalse, 1);
1510 chunk.emit(Op::Jump, 1);
1511 chunk.emit(Op::Return, 1);
1512 chunk.emit(Op::Pop, 1);
1513 assert!(!chunk.references_outer_names);
1514 }
1515
1516 #[test]
1517 fn references_outer_names_is_monotonic() {
1518 let mut chunk = Chunk::new();
1521 chunk.emit_u16(Op::GetVar, 0, 1);
1522 assert!(chunk.references_outer_names);
1523 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1524 chunk.emit(Op::MulInt, 1);
1525 assert!(chunk.references_outer_names);
1526 }
1527
1528 #[test]
1529 fn freeze_thaw_round_trips_references_outer_names() {
1530 let mut chunk = Chunk::new();
1534 chunk.emit_u16(Op::GetVar, 0, 1);
1535 assert!(chunk.references_outer_names);
1536 let frozen = chunk.freeze_for_cache();
1537 let thawed = Chunk::from_cached(&frozen);
1538 assert!(thawed.references_outer_names);
1539
1540 let plain = Chunk::new();
1541 assert!(!plain.references_outer_names);
1542 let frozen_plain = plain.freeze_for_cache();
1543 let thawed_plain = Chunk::from_cached(&frozen_plain);
1544 assert!(!thawed_plain.references_outer_names);
1545 }
1546
1547 #[test]
1559 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1560 let mut chunk = Chunk::new();
1563 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1564 chunk.emit(Op::Pop, 1);
1565 chunk.emit(Op::Return, 1);
1566 assert!(chunk.inline_cache_slot(0).is_none());
1567 assert!(chunk.inline_cache_slot(3).is_none());
1568 assert!(chunk.inline_cache_slot(4).is_none());
1569 }
1570
1571 #[test]
1572 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1573 let mut chunk = Chunk::new();
1577 chunk.emit(Op::Add, 1);
1578 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1579 }
1580
1581 #[test]
1582 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1583 let mut chunk = Chunk::new();
1588 chunk.emit(Op::Add, 1);
1589 chunk.emit(Op::Sub, 1);
1590 chunk.emit(Op::Mul, 1);
1591 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1592 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1593 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1594 assert_ne!(s0, s1);
1595 assert_ne!(s1, s2);
1596 assert_ne!(s0, s2);
1597 }
1598
1599 #[test]
1600 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1601 let mut chunk = Chunk::new();
1604 chunk.emit(Op::Add, 1);
1605 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1606 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1607 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1608 }
1609
1610 #[test]
1611 fn inline_cache_slot_for_get_property_and_method_call() {
1612 let mut chunk = Chunk::new();
1616 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");
1621 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1622 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1623 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1624 }
1625
1626 #[test]
1627 fn inline_cache_slot_for_call_and_call_builtin() {
1628 let mut chunk = Chunk::new();
1633 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1635 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1636 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1637 assert!(
1638 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1639 "Op::CallBuiltin IC slot"
1640 );
1641 }
1642
1643 #[test]
1644 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1645 let mut chunk = Chunk::new();
1651 chunk.emit(Op::Add, 1);
1652 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1653 chunk.register_inline_cache(0);
1655 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1656 assert_eq!(slot_before, slot_after);
1657 }
1658
1659 #[test]
1660 fn inline_cache_index_round_trips_through_cached_chunk() {
1661 let mut chunk = Chunk::new();
1668 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1669 chunk.emit_u16(Op::Constant, 0, 1);
1670 chunk.emit(Op::Add, 1);
1671 chunk.emit(Op::Sub, 1);
1672 chunk.emit_method_call(0, 1, 1);
1673 chunk.emit_u8(Op::Call, 1, 1);
1674 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1675 .map(|o| (o, chunk.inline_cache_slot(o)))
1676 .collect();
1677 let frozen = chunk.freeze_for_cache();
1678 let thawed = Chunk::from_cached(&frozen);
1679 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1680 .map(|o| (o, thawed.inline_cache_slot(o)))
1681 .collect();
1682 assert_eq!(live_slots, thawed_slots);
1683 }
1684
1685 #[test]
1686 fn inline_cache_index_agrees_with_btreemap_view() {
1687 let mut chunk = Chunk::new();
1693 chunk.emit(Op::Add, 1);
1694 chunk.emit_u16(Op::GetVar, 0, 1);
1695 chunk.emit(Op::LessInt, 1);
1696 chunk.emit_u8(Op::Call, 2, 1);
1697 chunk.emit(Op::Equal, 1);
1698 chunk.emit_u16(Op::GetProperty, 0, 1);
1699 chunk.emit_method_call_opt(0, 0, 1);
1700 for offset in 0..chunk.code.len() {
1701 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1702 let from_index = chunk.inline_cache_slot(offset);
1703 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1704 }
1705 }
1706
1707 #[test]
1718 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1719 let mut chunk = Chunk::new();
1720 chunk.emit(Op::Add, 1);
1721 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1722 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1724 }
1725
1726 #[test]
1727 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1728 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1729 let mut chunk = Chunk::new();
1730 chunk.emit(Op::Add, 1);
1731 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1732 chunk.set_inline_cache_entry(
1733 slot,
1734 InlineCacheEntry::AdaptiveBinary {
1735 op: AdaptiveBinaryOp::Add,
1736 state: AdaptiveBinaryState::Warmup {
1737 shape: BinaryShape::Int,
1738 hits: 2,
1739 },
1740 },
1741 );
1742 let (op, state) = chunk
1743 .peek_adaptive_binary_cache(slot)
1744 .expect("warmed slot peek");
1745 assert_eq!(op, AdaptiveBinaryOp::Add);
1746 assert!(matches!(
1747 state,
1748 AdaptiveBinaryState::Warmup {
1749 shape: BinaryShape::Int,
1750 hits: 2
1751 }
1752 ));
1753 }
1754
1755 #[test]
1756 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1757 use super::{InlineCacheEntry, PropertyCacheTarget};
1763 let mut chunk = Chunk::new();
1764 chunk.emit(Op::Add, 1);
1765 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1766 chunk.set_inline_cache_entry(
1767 slot,
1768 InlineCacheEntry::Property {
1769 name_idx: 0,
1770 target: PropertyCacheTarget::ListCount,
1771 },
1772 );
1773 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1774 }
1775
1776 #[test]
1777 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1778 let chunk = Chunk::new();
1783 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1784 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1785 }
1786
1787 #[test]
1788 fn peek_adaptive_binary_state_is_copy() {
1789 fn assert_copy<T: Copy>() {}
1795 assert_copy::<super::AdaptiveBinaryState>();
1796 assert_copy::<super::AdaptiveBinaryOp>();
1797 assert_copy::<super::BinaryShape>();
1798 }
1799
1800 #[test]
1811 fn peek_method_cache_returns_none_for_empty_slot() {
1812 let mut chunk = Chunk::new();
1813 chunk.emit_method_call(0, 0, 1);
1814 let slot = chunk
1815 .inline_cache_slot(0)
1816 .expect("MethodCall registers a slot");
1817 assert!(chunk.peek_method_cache(slot).is_none());
1818 }
1819
1820 #[test]
1821 fn peek_method_cache_returns_triple_after_warmup() {
1822 let mut chunk = Chunk::new();
1823 chunk.emit_method_call(7, 2, 1);
1824 let slot = chunk
1825 .inline_cache_slot(0)
1826 .expect("MethodCall registers a slot");
1827 chunk.set_inline_cache_entry(
1828 slot,
1829 InlineCacheEntry::Method {
1830 name_idx: 7,
1831 argc: 2,
1832 target: MethodCacheTarget::ListContains,
1833 },
1834 );
1835 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1836 assert_eq!(name_idx, 7);
1837 assert_eq!(argc, 2);
1838 assert_eq!(target, MethodCacheTarget::ListContains);
1839 }
1840
1841 #[test]
1842 fn peek_method_cache_returns_none_for_non_method_variants() {
1843 let mut chunk = Chunk::new();
1847 chunk.emit_method_call(0, 0, 1);
1848 let slot = chunk
1849 .inline_cache_slot(0)
1850 .expect("MethodCall registers a slot");
1851
1852 chunk.set_inline_cache_entry(
1853 slot,
1854 InlineCacheEntry::Property {
1855 name_idx: 0,
1856 target: PropertyCacheTarget::ListCount,
1857 },
1858 );
1859 assert!(chunk.peek_method_cache(slot).is_none());
1860 }
1861
1862 #[test]
1863 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1864 let chunk = Chunk::new();
1865 assert!(chunk.peek_method_cache(0).is_none());
1866 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1867 }
1868
1869 #[test]
1870 fn peek_method_cache_target_is_copy() {
1871 fn assert_copy<T: Copy>() {}
1877 assert_copy::<super::MethodCacheTarget>();
1878 }
1879
1880 #[test]
1890 fn peek_property_cache_returns_none_for_empty_slot() {
1891 let mut chunk = Chunk::new();
1892 chunk.emit_u16(Op::GetProperty, 0, 1);
1893 let slot = chunk
1894 .inline_cache_slot(0)
1895 .expect("GetProperty registers a slot");
1896 assert!(chunk.peek_property_cache(slot).is_none());
1897 }
1898
1899 #[test]
1900 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1901 let mut chunk = Chunk::new();
1902 chunk.emit_u16(Op::GetProperty, 0, 1);
1903 let slot = chunk
1904 .inline_cache_slot(0)
1905 .expect("GetProperty registers a slot");
1906 chunk.set_inline_cache_entry(
1907 slot,
1908 InlineCacheEntry::Property {
1909 name_idx: 11,
1910 target: PropertyCacheTarget::DictField(Arc::from("count")),
1911 },
1912 );
1913 let (name_idx, target) = chunk
1914 .peek_property_cache(slot)
1915 .expect("warmed property slot peek");
1916 assert_eq!(name_idx, 11);
1917 match target {
1918 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1919 other => panic!("expected DictField, got {other:?}"),
1920 }
1921 }
1922
1923 #[test]
1924 fn peek_property_cache_returns_pair_for_unit_target() {
1925 let mut chunk = Chunk::new();
1929 chunk.emit_u16(Op::GetProperty, 0, 1);
1930 let slot = chunk
1931 .inline_cache_slot(0)
1932 .expect("GetProperty registers a slot");
1933 chunk.set_inline_cache_entry(
1934 slot,
1935 InlineCacheEntry::Property {
1936 name_idx: 3,
1937 target: PropertyCacheTarget::ListCount,
1938 },
1939 );
1940 let (name_idx, target) = chunk
1941 .peek_property_cache(slot)
1942 .expect("warmed property slot peek");
1943 assert_eq!(name_idx, 3);
1944 assert_eq!(target, PropertyCacheTarget::ListCount);
1945 }
1946
1947 #[test]
1948 fn peek_property_cache_returns_none_for_non_property_variants() {
1949 let mut chunk = Chunk::new();
1950 chunk.emit_u16(Op::GetProperty, 0, 1);
1951 let slot = chunk
1952 .inline_cache_slot(0)
1953 .expect("GetProperty registers a slot");
1954 chunk.set_inline_cache_entry(
1955 slot,
1956 InlineCacheEntry::Method {
1957 name_idx: 0,
1958 argc: 0,
1959 target: MethodCacheTarget::ListCount,
1960 },
1961 );
1962 assert!(chunk.peek_property_cache(slot).is_none());
1963 }
1964
1965 #[test]
1966 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
1967 let chunk = Chunk::new();
1968 assert!(chunk.peek_property_cache(0).is_none());
1969 assert!(chunk.peek_property_cache(usize::MAX).is_none());
1970 }
1971
1972 #[test]
1982 fn add_constant_keeps_signed_zero_and_nan_distinct() {
1983 let mut chunk = Chunk::new();
1984 let pos = chunk.add_constant(Constant::Float(0.0));
1987 let neg = chunk.add_constant(Constant::Float(-0.0));
1988 assert_ne!(pos, neg, "+0.0 and -0.0 must get distinct constant slots");
1989 assert_eq!(pos, chunk.add_constant(Constant::Float(0.0)));
1991 assert_eq!(neg, chunk.add_constant(Constant::Float(-0.0)));
1992 let a = chunk.add_constant(Constant::Float(1.5));
1994 assert_eq!(a, chunk.add_constant(Constant::Float(1.5)));
1995 let s = chunk.add_constant(Constant::Int(7));
1997 assert_eq!(s, chunk.add_constant(Constant::Int(7)));
1998 }
1999
2000 #[test]
2001 fn peek_direct_call_state_returns_none_for_empty_slot() {
2002 let mut chunk = Chunk::new();
2003 chunk.emit_u8(Op::Call, 0, 1);
2004 let slot = chunk
2005 .inline_cache_slot(0)
2006 .expect("Op::Call registers a slot");
2007 assert!(chunk.peek_direct_call_state(slot).is_none());
2008 }
2009
2010 #[test]
2011 fn peek_direct_call_state_returns_warmup_state() {
2012 let mut chunk = Chunk::new();
2013 chunk.emit_u8(Op::Call, 0, 1);
2014 let slot = chunk
2015 .inline_cache_slot(0)
2016 .expect("Op::Call registers a slot");
2017 let target = synthetic_direct_call_target();
2018 chunk.set_inline_cache_entry(
2019 slot,
2020 InlineCacheEntry::DirectCall {
2021 state: DirectCallState::Warmup {
2022 argc: 2,
2023 target: target.clone(),
2024 hits: 1,
2025 },
2026 },
2027 );
2028 let state = chunk
2029 .peek_direct_call_state(slot)
2030 .expect("warmed direct-call slot peek");
2031 match state {
2032 DirectCallState::Warmup {
2033 argc,
2034 target: peeked_target,
2035 hits,
2036 } => {
2037 assert_eq!(argc, 2);
2038 assert_eq!(hits, 1);
2039 assert_eq!(peeked_target, target);
2040 }
2041 other => panic!("expected Warmup, got {other:?}"),
2042 }
2043 }
2044
2045 #[test]
2046 fn peek_direct_call_state_returns_specialized_state() {
2047 let mut chunk = Chunk::new();
2048 chunk.emit_u8(Op::Call, 0, 1);
2049 let slot = chunk
2050 .inline_cache_slot(0)
2051 .expect("Op::Call registers a slot");
2052 let target = synthetic_direct_call_target();
2053 chunk.set_inline_cache_entry(
2054 slot,
2055 InlineCacheEntry::DirectCall {
2056 state: DirectCallState::Specialized {
2057 argc: 3,
2058 target: target.clone(),
2059 hits: 100,
2060 misses: 0,
2061 },
2062 },
2063 );
2064 let state = chunk
2065 .peek_direct_call_state(slot)
2066 .expect("warmed direct-call slot peek");
2067 match state {
2068 DirectCallState::Specialized {
2069 argc,
2070 target: peeked_target,
2071 hits,
2072 misses,
2073 } => {
2074 assert_eq!(argc, 3);
2075 assert_eq!(hits, 100);
2076 assert_eq!(misses, 0);
2077 assert_eq!(peeked_target, target);
2078 }
2079 other => panic!("expected Specialized, got {other:?}"),
2080 }
2081 }
2082
2083 #[test]
2084 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2085 let mut chunk = Chunk::new();
2086 chunk.emit_u8(Op::Call, 0, 1);
2087 let slot = chunk
2088 .inline_cache_slot(0)
2089 .expect("Op::Call registers a slot");
2090
2091 chunk.set_inline_cache_entry(
2092 slot,
2093 InlineCacheEntry::Property {
2094 name_idx: 0,
2095 target: PropertyCacheTarget::ListCount,
2096 },
2097 );
2098 assert!(chunk.peek_direct_call_state(slot).is_none());
2099 }
2100
2101 #[test]
2102 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2103 let chunk = Chunk::new();
2104 assert!(chunk.peek_direct_call_state(0).is_none());
2105 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2106 }
2107
2108 fn synthetic_direct_call_target() -> DirectCallTarget {
2112 use crate::value::VmClosure;
2113 use crate::{CompiledFunction, VmEnv};
2114 let func = CompiledFunction {
2115 name: "synthetic".to_string(),
2116 type_params: Vec::new(),
2117 nominal_type_names: Vec::new(),
2118 params: Vec::new(),
2119 default_start: None,
2120 chunk: Arc::new(Chunk::new()),
2121 is_generator: false,
2122 is_stream: false,
2123 has_rest_param: false,
2124 has_runtime_type_checks: false,
2125 };
2126 DirectCallTarget::Closure(Arc::new(VmClosure {
2127 func: Arc::new(func),
2128 env: VmEnv::new(),
2129 source_dir: None,
2130 module_functions: None,
2131 module_state: None,
2132 }))
2133 }
2134}