1use std::collections::{BTreeMap, HashMap};
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, Hash)]
68enum ConstantKey {
69 Int(i64),
70 Float(u64),
71 String(String),
72 Bool(bool),
73 Nil,
74 Duration(i64),
75}
76
77impl From<&Constant> for ConstantKey {
78 fn from(constant: &Constant) -> Self {
79 match constant {
80 Constant::Int(value) => Self::Int(*value),
81 Constant::Float(value) => Self::Float(value.to_bits()),
82 Constant::String(value) => Self::String(value.clone()),
83 Constant::Bool(value) => Self::Bool(*value),
84 Constant::Nil => Self::Nil,
85 Constant::Duration(value) => Self::Duration(*value),
86 }
87 }
88}
89
90fn build_constant_index(constants: &[Constant]) -> HashMap<ConstantKey, u16> {
91 let mut index = HashMap::with_capacity(constants.len());
92 for (slot, constant) in constants.iter().enumerate() {
93 if let Ok(slot) = u16::try_from(slot) {
94 index.entry(ConstantKey::from(constant)).or_insert(slot);
95 }
96 }
97 index
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
110pub(crate) enum InlineCacheEntry {
111 Empty,
112 Property {
113 name_idx: u16,
114 target: PropertyCacheTarget,
115 },
116 Method {
117 name_idx: u16,
118 argc: usize,
119 target: MethodCacheTarget,
120 },
121 AdaptiveBinary {
122 op: AdaptiveBinaryOp,
123 state: AdaptiveBinaryState,
124 },
125 DirectCall {
126 state: DirectCallState,
127 },
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub(crate) enum AdaptiveBinaryOp {
132 Add,
133 Sub,
134 Mul,
135 Div,
136 Mod,
137 Equal,
138 NotEqual,
139 Less,
140 Greater,
141 LessEqual,
142 GreaterEqual,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub(crate) enum AdaptiveBinaryState {
152 Warmup {
153 shape: BinaryShape,
154 hits: u8,
155 },
156 Specialized {
157 shape: BinaryShape,
158 hits: u64,
159 misses: u64,
160 },
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub(crate) enum BinaryShape {
165 Int,
166 Float,
167 Bool,
168 String,
169}
170
171#[derive(Debug, Clone)]
172pub(crate) enum DirectCallState {
173 Warmup {
174 argc: usize,
175 target: DirectCallTarget,
176 hits: u8,
177 },
178 Specialized {
179 argc: usize,
180 target: DirectCallTarget,
181 hits: u64,
182 misses: u64,
183 },
184}
185
186#[derive(Debug, Clone)]
187pub(crate) enum DirectCallTarget {
188 Closure(Arc<crate::value::VmClosure>),
189}
190
191impl PartialEq for DirectCallTarget {
192 fn eq(&self, other: &Self) -> bool {
193 match (self, other) {
194 (Self::Closure(left), Self::Closure(right)) => Arc::ptr_eq(left, right),
195 }
196 }
197}
198
199impl Eq for DirectCallTarget {}
200
201impl PartialEq for DirectCallState {
202 fn eq(&self, other: &Self) -> bool {
203 match (self, other) {
204 (
205 Self::Warmup {
206 argc: left_argc,
207 target: left_target,
208 hits: left_hits,
209 },
210 Self::Warmup {
211 argc: right_argc,
212 target: right_target,
213 hits: right_hits,
214 },
215 ) => left_argc == right_argc && left_target == right_target && left_hits == right_hits,
216 (
217 Self::Specialized {
218 argc: left_argc,
219 target: left_target,
220 hits: left_hits,
221 misses: left_misses,
222 },
223 Self::Specialized {
224 argc: right_argc,
225 target: right_target,
226 hits: right_hits,
227 misses: right_misses,
228 },
229 ) => {
230 left_argc == right_argc
231 && left_target == right_target
232 && left_hits == right_hits
233 && left_misses == right_misses
234 }
235 _ => false,
236 }
237 }
238}
239
240impl Eq for DirectCallState {}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub(crate) enum PropertyCacheTarget {
244 DictField(Arc<str>),
245 StructField { field_name: Arc<str>, index: usize },
246 HarnessSubHandle(HarnessKind),
247 ListCount,
248 ListEmpty,
249 ListFirst,
250 ListLast,
251 StringCount,
252 StringEmpty,
253 PairFirst,
254 PairSecond,
255 EnumVariant,
256 EnumFields,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub(crate) enum MethodCacheTarget {
261 Harness(HarnessKind),
262 ListCount,
263 ListEmpty,
264 ListContains,
265 StringCount,
266 StringEmpty,
267 StringContains,
268 DictCount,
269 DictHas,
270 RangeCount,
271 RangeLen,
272 RangeEmpty,
273 RangeFirst,
274 RangeLast,
275 SetCount,
276 SetLen,
277 SetEmpty,
278 SetContains,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub struct LocalSlotInfo {
284 pub name: String,
285 pub mutable: bool,
286 pub scope_depth: usize,
287}
288
289impl fmt::Display for Constant {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 Constant::Int(n) => write!(f, "{n}"),
293 Constant::Float(n) => write!(f, "{n}"),
294 Constant::String(s) => write!(f, "\"{s}\""),
295 Constant::Bool(b) => write!(f, "{b}"),
296 Constant::Nil => write!(f, "nil"),
297 Constant::Duration(ms) => write!(f, "{ms}ms"),
298 }
299 }
300}
301
302#[derive(Debug)]
304pub struct Chunk {
305 cache_id: u64,
309 pub code: Vec<u8>,
311 pub constants: Vec<Constant>,
313 constant_index: HashMap<ConstantKey, u16>,
317 pub lines: Vec<u32>,
319 pub columns: Vec<u32>,
322 pub source_file: Option<String>,
327 current_col: u32,
329 pub functions: Vec<CompiledFunctionRef>,
331 inline_cache_slots: BTreeMap<usize, usize>,
337 inline_cache_index: Vec<u32>,
345 inline_caches: Arc<Mutex<Vec<InlineCacheEntry>>>,
349 constant_strings: Arc<Mutex<Vec<Option<Arc<str>>>>>,
353 pub(crate) local_slots: Vec<LocalSlotInfo>,
355 pub(crate) references_outer_names: bool,
371 #[cfg(debug_assertions)]
385 balance_depth: i32,
386 #[cfg(debug_assertions)]
387 balance_nonlinear: u32,
388}
389
390pub type ChunkRef = Arc<Chunk>;
391pub type CompiledFunctionRef = Arc<CompiledFunction>;
392
393impl Clone for Chunk {
394 fn clone(&self) -> Self {
395 Self {
396 cache_id: self.cache_id,
397 code: self.code.clone(),
398 constants: self.constants.clone(),
399 constant_index: self.constant_index.clone(),
400 lines: self.lines.clone(),
401 columns: self.columns.clone(),
402 source_file: self.source_file.clone(),
403 current_col: self.current_col,
404 functions: self.functions.clone(),
405 inline_cache_slots: self.inline_cache_slots.clone(),
406 inline_cache_index: self.inline_cache_index.clone(),
407 inline_caches: Arc::new(Mutex::new(vec![
408 InlineCacheEntry::Empty;
409 self.inline_cache_slot_count()
410 ])),
411 constant_strings: Arc::new(Mutex::new(vec![None; self.constants.len()])),
412 local_slots: self.local_slots.clone(),
413 references_outer_names: self.references_outer_names,
414 #[cfg(debug_assertions)]
415 balance_depth: self.balance_depth,
416 #[cfg(debug_assertions)]
417 balance_nonlinear: self.balance_nonlinear,
418 }
419 }
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct CachedChunk {
428 pub(crate) code: Vec<u8>,
429 pub(crate) constants: Vec<Constant>,
430 pub(crate) lines: Vec<u32>,
431 pub(crate) columns: Vec<u32>,
432 pub(crate) source_file: Option<String>,
433 pub(crate) current_col: u32,
434 pub(crate) functions: Vec<CachedCompiledFunction>,
435 pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
436 pub(crate) local_slots: Vec<LocalSlotInfo>,
437 #[serde(default)]
438 pub(crate) references_outer_names: bool,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct CachedCompiledFunction {
443 pub(crate) name: String,
444 pub(crate) type_params: Vec<String>,
445 pub(crate) nominal_type_names: Vec<String>,
446 pub(crate) params: Vec<CachedParamSlot>,
447 pub(crate) default_start: Option<usize>,
448 pub(crate) chunk: CachedChunk,
449 pub(crate) is_generator: bool,
450 pub(crate) is_stream: bool,
451 pub(crate) has_rest_param: bool,
452 pub(crate) has_runtime_type_checks: bool,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub(crate) struct CachedParamSlot {
457 pub(crate) name: String,
458 pub(crate) type_expr: Option<TypeExpr>,
459 pub(crate) has_default: bool,
460}
461
462impl CachedParamSlot {
463 fn thaw(&self) -> ParamSlot {
464 ParamSlot {
465 name: self.name.clone(),
466 type_expr: self.type_expr.clone(),
467 runtime_guard: self
468 .type_expr
469 .as_ref()
470 .map(RuntimeParamGuard::from_type_expr),
471 has_default: self.has_default,
472 }
473 }
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct ParamSlot {
483 pub name: String,
484 pub type_expr: Option<TypeExpr>,
487 #[serde(skip)]
490 pub(crate) runtime_guard: Option<RuntimeParamGuard>,
491 pub has_default: bool,
495}
496
497impl ParamSlot {
498 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
501 Self {
502 name: param.name.clone(),
503 type_expr: param.type_expr.clone(),
504 runtime_guard: param
505 .type_expr
506 .as_ref()
507 .map(RuntimeParamGuard::from_type_expr),
508 has_default: param.default_value.is_some(),
509 }
510 }
511
512 fn freeze_for_cache(&self) -> CachedParamSlot {
513 CachedParamSlot {
514 name: self.name.clone(),
515 type_expr: self.type_expr.clone(),
516 has_default: self.has_default,
517 }
518 }
519
520 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
525 params.iter().map(Self::from_typed_param).collect()
526 }
527}
528
529#[derive(Debug, Clone)]
531pub struct CompiledFunction {
532 pub name: String,
533 pub type_params: Vec<String>,
537 pub nominal_type_names: Vec<String>,
541 pub params: Vec<ParamSlot>,
542 pub default_start: Option<usize>,
544 pub chunk: ChunkRef,
545 pub is_generator: bool,
547 pub is_stream: bool,
549 pub has_rest_param: bool,
551 pub has_runtime_type_checks: bool,
556}
557
558impl CompiledFunction {
559 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
560 params.iter().any(|param| param.type_expr.is_some())
561 }
562
563 pub fn param_names(&self) -> impl Iterator<Item = &str> {
566 self.params.iter().map(|p| p.name.as_str())
567 }
568
569 pub fn required_param_count(&self) -> usize {
571 self.default_start.unwrap_or(self.params.len())
572 }
573
574 pub fn declares_type_param(&self, name: &str) -> bool {
575 self.type_params.iter().any(|param| param == name)
576 }
577
578 pub fn has_nominal_type(&self, name: &str) -> bool {
579 self.nominal_type_names.iter().any(|ty| ty == name)
580 }
581
582 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
583 CachedCompiledFunction {
584 name: self.name.clone(),
585 type_params: self.type_params.clone(),
586 nominal_type_names: self.nominal_type_names.clone(),
587 params: self
588 .params
589 .iter()
590 .map(ParamSlot::freeze_for_cache)
591 .collect(),
592 default_start: self.default_start,
593 chunk: self.chunk.freeze_for_cache(),
594 is_generator: self.is_generator,
595 is_stream: self.is_stream,
596 has_rest_param: self.has_rest_param,
597 has_runtime_type_checks: self.has_runtime_type_checks,
598 }
599 }
600
601 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
602 Self {
603 name: cached.name.clone(),
604 type_params: cached.type_params.clone(),
605 nominal_type_names: cached.nominal_type_names.clone(),
606 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
607 default_start: cached.default_start,
608 chunk: Arc::new(Chunk::from_cached(&cached.chunk)),
609 is_generator: cached.is_generator,
610 is_stream: cached.is_stream,
611 has_rest_param: cached.has_rest_param,
612 has_runtime_type_checks: cached.has_runtime_type_checks,
613 }
614 }
615}
616
617#[cfg(debug_assertions)]
620#[derive(Clone, Copy)]
621pub(crate) struct BalanceProbe {
622 depth: i32,
623 nonlinear: u32,
624}
625
626#[cfg(debug_assertions)]
643fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
644 use Op::*;
645 let count = count as i32;
646 Some(match op {
647 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
649 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty | Pop => -1,
653 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
657 | PushScope | PopScope | PopIterator | PopHandler => 0,
658 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
660 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
661 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
662 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
663 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
664 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
665 IterInit => -1,
668 Slice | SetSubscript => -2,
670 BuildList | Concat | CallBuiltin => 1 - count,
672 BuildDict => 1 - 2 * count,
673 Call | MethodCall | MethodCallOpt => -count,
675 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
678 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
679 | SyncMutexEnter | SyncMutexEnterKeyed | TaskScopeEnter | TaskScopeExit | Import
680 | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum | MatchEnum | Yield
681 | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
682 })
683}
684
685impl Chunk {
686 pub fn new() -> Self {
687 Self {
688 cache_id: next_chunk_cache_id(),
689 code: Vec::new(),
690 constants: Vec::new(),
691 constant_index: HashMap::new(),
692 lines: Vec::new(),
693 columns: Vec::new(),
694 source_file: None,
695 current_col: 0,
696 functions: Vec::new(),
697 inline_cache_slots: BTreeMap::new(),
698 inline_cache_index: Vec::new(),
699 inline_caches: Arc::new(Mutex::new(Vec::new())),
700 constant_strings: Arc::new(Mutex::new(Vec::new())),
701 local_slots: Vec::new(),
702 references_outer_names: false,
703 #[cfg(debug_assertions)]
704 balance_depth: 0,
705 #[cfg(debug_assertions)]
706 balance_nonlinear: 0,
707 }
708 }
709
710 pub fn set_column(&mut self, col: u32) {
712 self.current_col = col;
713 }
714
715 pub fn add_constant(&mut self, constant: Constant) -> u16 {
717 debug_assert!(
718 self.constant_index.len() <= self.constants.len(),
719 "constant side index cannot outgrow the constant pool"
720 );
721 let key = ConstantKey::from(&constant);
722 if let Some(index) = self.constant_index.get(&key) {
723 debug_assert!(
724 self.constants
725 .get(*index as usize)
726 .is_some_and(|existing| constants_identical(existing, &constant)),
727 "constant side index drifted from the constant pool"
728 );
729 return *index;
730 }
731 let idx = self.constants.len();
732 let idx = u16::try_from(idx).expect("constant pool exceeded u16 operand space");
733 self.constants.push(constant);
734 self.constant_index.insert(key, idx);
735 idx
736 }
737
738 pub fn emit(&mut self, op: Op, line: u32) {
740 #[cfg(debug_assertions)]
741 self.note_balance(op, 0);
742 let col = self.current_col;
743 let op_offset = self.code.len();
744 self.code.push(op as u8);
745 self.lines.push(line);
746 self.columns.push(col);
747 if is_adaptive_binary_op(op) {
748 self.register_inline_cache(op_offset);
749 }
750 if op_reads_outer_name(op) {
751 self.references_outer_names = true;
752 }
753 }
754
755 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
757 #[cfg(debug_assertions)]
758 self.note_balance(op, arg);
759 let col = self.current_col;
760 let op_offset = self.code.len();
761 self.code.push(op as u8);
762 self.code.push((arg >> 8) as u8);
763 self.code.push((arg & 0xFF) as u8);
764 self.lines.push(line);
765 self.lines.push(line);
766 self.lines.push(line);
767 self.columns.push(col);
768 self.columns.push(col);
769 self.columns.push(col);
770 if matches!(
771 op,
772 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
773 ) {
774 self.register_inline_cache(op_offset);
775 }
776 if op_reads_outer_name(op) {
777 self.references_outer_names = true;
778 }
779 }
780
781 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
783 #[cfg(debug_assertions)]
784 self.note_balance(op, arg as u16);
785 let col = self.current_col;
786 let op_offset = self.code.len();
787 self.code.push(op as u8);
788 self.code.push(arg);
789 self.lines.push(line);
790 self.lines.push(line);
791 self.columns.push(col);
792 self.columns.push(col);
793 if matches!(op, Op::Call) {
794 self.register_inline_cache(op_offset);
795 }
796 if op_reads_outer_name(op) {
797 self.references_outer_names = true;
798 }
799 }
800
801 pub fn emit_call_builtin(
803 &mut self,
804 id: crate::BuiltinId,
805 name_idx: u16,
806 arg_count: u8,
807 line: u32,
808 ) {
809 #[cfg(debug_assertions)]
810 self.note_balance(Op::CallBuiltin, arg_count as u16);
811 let col = self.current_col;
812 let op_offset = self.code.len();
813 self.code.push(Op::CallBuiltin as u8);
814 self.code.extend_from_slice(&id.raw().to_be_bytes());
815 self.code.push((name_idx >> 8) as u8);
816 self.code.push((name_idx & 0xFF) as u8);
817 self.code.push(arg_count);
818 for _ in 0..12 {
819 self.lines.push(line);
820 self.columns.push(col);
821 }
822 self.register_inline_cache(op_offset);
823 self.references_outer_names = true;
824 }
825
826 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
828 #[cfg(debug_assertions)]
829 self.note_balance(Op::CallBuiltinSpread, 0);
830 let col = self.current_col;
831 self.code.push(Op::CallBuiltinSpread as u8);
832 self.code.extend_from_slice(&id.raw().to_be_bytes());
833 self.code.push((name_idx >> 8) as u8);
834 self.code.push((name_idx & 0xFF) as u8);
835 for _ in 0..11 {
836 self.lines.push(line);
837 self.columns.push(col);
838 }
839 self.references_outer_names = true;
840 }
841
842 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
844 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
845 }
846
847 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
849 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
850 }
851
852 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
853 #[cfg(debug_assertions)]
854 self.note_balance(op, arg_count as u16);
855 let col = self.current_col;
856 let op_offset = self.code.len();
857 self.code.push(op as u8);
858 self.code.push((name_idx >> 8) as u8);
859 self.code.push((name_idx & 0xFF) as u8);
860 self.code.push(arg_count);
861 self.lines.push(line);
862 self.lines.push(line);
863 self.lines.push(line);
864 self.lines.push(line);
865 self.columns.push(col);
866 self.columns.push(col);
867 self.columns.push(col);
868 self.columns.push(col);
869 self.register_inline_cache(op_offset);
870 }
871
872 pub fn current_offset(&self) -> usize {
874 self.code.len()
875 }
876
877 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
879 #[cfg(debug_assertions)]
880 self.note_balance(op, 0);
881 let col = self.current_col;
882 self.code.push(op as u8);
883 let patch_pos = self.code.len();
884 self.code.push(0xFF);
885 self.code.push(0xFF);
886 self.lines.push(line);
887 self.lines.push(line);
888 self.lines.push(line);
889 self.columns.push(col);
890 self.columns.push(col);
891 self.columns.push(col);
892 patch_pos
893 }
894
895 pub fn patch_jump(&mut self, patch_pos: usize) {
897 let target = self.code.len() as u16;
898 self.code[patch_pos] = (target >> 8) as u8;
899 self.code[patch_pos + 1] = (target & 0xFF) as u8;
900 }
901
902 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
904 let target = target as u16;
905 self.code[patch_pos] = (target >> 8) as u8;
906 self.code[patch_pos + 1] = (target & 0xFF) as u8;
907 }
908
909 pub fn read_u16(&self, pos: usize) -> u16 {
911 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
912 }
913
914 #[cfg(debug_assertions)]
918 fn note_balance(&mut self, op: Op, count: u16) {
919 match op_stack_delta(op, count) {
920 Some(delta) => self.balance_depth += delta,
921 None => self.balance_nonlinear += 1,
922 }
923 }
924
925 #[cfg(debug_assertions)]
928 pub(crate) fn balance_probe(&self) -> BalanceProbe {
929 BalanceProbe {
930 depth: self.balance_depth,
931 nonlinear: self.balance_nonlinear,
932 }
933 }
934
935 #[cfg(debug_assertions)]
941 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
942 if self.balance_nonlinear == probe.nonlinear {
943 Some(self.balance_depth - probe.depth)
944 } else {
945 None
946 }
947 }
948
949 fn register_inline_cache(&mut self, op_offset: usize) {
950 if self.inline_cache_slots.contains_key(&op_offset) {
951 return;
952 }
953 let mut entries = self.inline_caches.lock();
954 let slot = entries.len();
955 entries.push(InlineCacheEntry::Empty);
956 self.inline_cache_slots.insert(op_offset, slot);
957 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
958 }
959
960 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
965 if op_offset >= index.len() {
966 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
967 }
968 index[op_offset] = slot as u32;
969 }
970
971 #[inline]
980 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
981 match self.inline_cache_index.get(op_offset).copied() {
982 None | Some(NO_INLINE_CACHE_SLOT) => None,
983 Some(slot) => Some(slot as usize),
984 }
985 }
986
987 pub(crate) fn inline_cache_slot_count(&self) -> usize {
988 self.inline_cache_slots.len()
989 }
990
991 pub(crate) fn cache_id(&self) -> u64 {
992 self.cache_id
993 }
994
995 #[cfg(feature = "vm-bench-internals")]
1002 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
1003 self.inline_cache_slots.get(&op_offset).copied()
1004 }
1005
1006 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
1011 let mut entries = self.constant_strings.lock();
1016 if entries.len() < self.constants.len() {
1017 entries.resize(self.constants.len(), None);
1018 }
1019 if let Some(Some(existing)) = entries.get(idx) {
1020 return Some(Arc::clone(existing));
1021 }
1022 let materialized = match self.constants.get(idx)? {
1023 Constant::String(s) => Arc::<str>::from(s.as_str()),
1024 _ => return None,
1025 };
1026 entries[idx] = Some(Arc::clone(&materialized));
1027 Some(materialized)
1028 }
1029
1030 #[cfg(feature = "vm-bench-internals")]
1031 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
1032 self.inline_caches
1033 .lock()
1034 .get(slot)
1035 .cloned()
1036 .unwrap_or(InlineCacheEntry::Empty)
1037 }
1038
1039 #[inline]
1051 #[cfg(any(test, feature = "vm-bench-internals"))]
1052 pub(crate) fn peek_adaptive_binary_cache(
1053 &self,
1054 slot: usize,
1055 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
1056 match self.inline_caches.lock().get(slot)? {
1057 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
1058 _ => None,
1059 }
1060 }
1061
1062 #[inline]
1074 #[cfg(any(test, feature = "vm-bench-internals"))]
1075 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1076 match self.inline_caches.lock().get(slot)? {
1077 &InlineCacheEntry::Method {
1078 name_idx,
1079 argc,
1080 target,
1081 } => Some((name_idx, argc, target)),
1082 _ => None,
1083 }
1084 }
1085
1086 #[inline]
1096 #[cfg(any(test, feature = "vm-bench-internals"))]
1097 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1098 match self.inline_caches.lock().get(slot)? {
1099 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1100 _ => None,
1101 }
1102 }
1103
1104 #[inline]
1114 #[cfg(any(test, feature = "vm-bench-internals"))]
1115 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1116 match self.inline_caches.lock().get(slot)? {
1117 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1118 _ => None,
1119 }
1120 }
1121
1122 #[cfg(any(test, feature = "vm-bench-internals"))]
1123 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1124 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1125 *existing = entry;
1126 }
1127 }
1128
1129 pub fn freeze_for_cache(&self) -> CachedChunk {
1130 CachedChunk {
1131 code: self.code.clone(),
1132 constants: self.constants.clone(),
1133 lines: self.lines.clone(),
1134 columns: self.columns.clone(),
1135 source_file: self.source_file.clone(),
1136 current_col: self.current_col,
1137 functions: self
1138 .functions
1139 .iter()
1140 .map(|function| function.freeze_for_cache())
1141 .collect(),
1142 inline_cache_slots: self.inline_cache_slots.clone(),
1143 local_slots: self.local_slots.clone(),
1144 references_outer_names: self.references_outer_names,
1145 }
1146 }
1147
1148 pub fn from_cached(cached: &CachedChunk) -> Self {
1149 let inline_cache_count = cached.inline_cache_slots.len();
1150 let constants_count = cached.constants.len();
1151 let mut inline_cache_index = Vec::new();
1158 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1159 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1160 if op_offset < inline_cache_index.len() {
1161 inline_cache_index[op_offset] = slot as u32;
1162 }
1163 }
1164 Self {
1165 cache_id: next_chunk_cache_id(),
1166 code: cached.code.clone(),
1167 constants: cached.constants.clone(),
1168 constant_index: build_constant_index(&cached.constants),
1169 lines: cached.lines.clone(),
1170 columns: cached.columns.clone(),
1171 source_file: cached.source_file.clone(),
1172 current_col: cached.current_col,
1173 functions: cached
1174 .functions
1175 .iter()
1176 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1177 .collect(),
1178 inline_cache_slots: cached.inline_cache_slots.clone(),
1179 inline_cache_index,
1180 inline_caches: Arc::new(Mutex::new(vec![
1181 InlineCacheEntry::Empty;
1182 inline_cache_count
1183 ])),
1184 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1185 local_slots: cached.local_slots.clone(),
1186 references_outer_names: cached.references_outer_names,
1187 #[cfg(debug_assertions)]
1188 balance_depth: 0,
1189 #[cfg(debug_assertions)]
1190 balance_nonlinear: 0,
1191 }
1192 }
1193
1194 pub(crate) fn add_local_slot(
1195 &mut self,
1196 name: String,
1197 mutable: bool,
1198 scope_depth: usize,
1199 ) -> u16 {
1200 let idx = self.local_slots.len();
1201 self.local_slots.push(LocalSlotInfo {
1202 name,
1203 mutable,
1204 scope_depth,
1205 });
1206 idx as u16
1207 }
1208
1209 pub fn read_u64(&self, pos: usize) -> u64 {
1211 u64::from_be_bytes([
1212 self.code[pos],
1213 self.code[pos + 1],
1214 self.code[pos + 2],
1215 self.code[pos + 3],
1216 self.code[pos + 4],
1217 self.code[pos + 5],
1218 self.code[pos + 6],
1219 self.code[pos + 7],
1220 ])
1221 }
1222
1223 pub fn disassemble(&self, name: &str) -> String {
1227 let mut out = format!("== {name} ==\n");
1228 let mut ip = 0;
1229 while ip < self.code.len() {
1230 let op_byte = self.code[ip];
1231 let line = self.lines.get(ip).copied().unwrap_or(0);
1232 out.push_str(&format!("{ip:04} [{line:>4}] "));
1233 ip += 1;
1234
1235 if let Some(op) = Op::from_byte(op_byte) {
1236 self.disassemble_op(op, &mut ip, &mut out);
1237 } else {
1238 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1239 }
1240 }
1241 out
1242 }
1243}
1244
1245pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1256 label.to_string()
1257}
1258
1259pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1260 let arg = chunk.code[*ip];
1261 *ip += 1;
1262 format!("{label} {arg:>4}")
1263}
1264
1265pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1266 let arg = chunk.read_u16(*ip);
1267 *ip += 2;
1268 format!("{label} {arg:>4}")
1269}
1270
1271pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1272 let catch_offset = chunk.read_u16(*ip);
1273 *ip += 2;
1274 let type_idx = chunk.read_u16(*ip);
1275 *ip += 2;
1276 if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1277 format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1278 } else {
1279 format!("{label} {catch_offset:>4} type {type_idx:>4}")
1280 }
1281}
1282
1283pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1284 let idx = chunk.read_u16(*ip);
1285 *ip += 2;
1286 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1287}
1288
1289pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1290 let slot = chunk.read_u16(*ip);
1291 *ip += 2;
1292 let mut out = format!("{label} {slot:>4}");
1293 if let Some(info) = chunk.local_slots.get(slot as usize) {
1294 out.push_str(&format!(" ({})", info.name));
1295 }
1296 out
1297}
1298
1299pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1300 let idx = chunk.read_u16(*ip);
1301 *ip += 2;
1302 let argc = chunk.code[*ip];
1303 *ip += 1;
1304 format!(
1305 "{label} {idx:>4} ({}) argc={argc}",
1306 chunk.constants[idx as usize]
1307 )
1308}
1309
1310pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1311 let enum_idx = chunk.read_u16(*ip);
1312 *ip += 2;
1313 let var_idx = chunk.read_u16(*ip);
1314 *ip += 2;
1315 format!(
1316 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1317 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1318 )
1319}
1320
1321pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1322 let enum_idx = chunk.read_u16(*ip);
1323 *ip += 2;
1324 let var_idx = chunk.read_u16(*ip);
1325 *ip += 2;
1326 let field_count = chunk.read_u16(*ip);
1327 *ip += 2;
1328 format!(
1329 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1330 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1331 )
1332}
1333
1334pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1335 let path_idx = chunk.read_u16(*ip);
1336 *ip += 2;
1337 let names_idx = chunk.read_u16(*ip);
1338 *ip += 2;
1339 format!(
1340 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1341 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1342 )
1343}
1344
1345pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1346 let var_idx = chunk.read_u16(*ip);
1347 *ip += 2;
1348 let type_idx = chunk.read_u16(*ip);
1349 *ip += 2;
1350 format!(
1351 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1352 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1353 )
1354}
1355
1356pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1357 let id = chunk.read_u64(*ip);
1358 *ip += 8;
1359 let idx = chunk.read_u16(*ip);
1360 *ip += 2;
1361 let argc = chunk.code[*ip];
1362 *ip += 1;
1363 format!(
1364 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1365 chunk.constants[idx as usize],
1366 )
1367}
1368
1369pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1370 let id = chunk.read_u64(*ip);
1371 *ip += 8;
1372 let idx = chunk.read_u16(*ip);
1373 *ip += 2;
1374 format!(
1375 "{label} {id:#018x} {idx:>4} ({})",
1376 chunk.constants[idx as usize],
1377 )
1378}
1379
1380pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1381 let idx = chunk.read_u16(*ip);
1387 *ip += 2;
1388 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1389}
1390
1391impl Default for Chunk {
1392 fn default() -> Self {
1393 Self::new()
1394 }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399 use std::sync::Arc;
1400
1401 use super::{
1402 Chunk, Constant, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget,
1403 Op, PropertyCacheTarget,
1404 };
1405 use crate::BuiltinId;
1406
1407 #[test]
1408 fn op_from_byte_matches_repr_order() {
1409 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1410 assert_eq!(byte as u8, op as u8);
1411 assert_eq!(Op::from_byte(byte as u8), Some(op));
1412 }
1413 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1414 assert_eq!(Op::COUNT, Op::ALL.len());
1415 }
1416
1417 #[test]
1418 fn disassemble_covers_every_opcode_variant() {
1419 for op in Op::ALL.iter().copied() {
1429 let mut chunk = Chunk::new();
1430 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1431 for _ in 0..16 {
1435 chunk.code.push(0);
1436 }
1437 let mut ip: usize = 0;
1438 let mut out = String::new();
1439 chunk.disassemble_op(op, &mut ip, &mut out);
1440 assert!(
1441 !out.contains("UNKNOWN"),
1442 "disasm emitted UNKNOWN for {op:?}: {out}",
1443 );
1444 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1445 }
1446 }
1447
1448 #[test]
1457 fn empty_chunk_does_not_reference_outer_names() {
1458 let chunk = Chunk::new();
1459 assert!(!chunk.references_outer_names);
1460 }
1461
1462 #[test]
1463 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1464 let mut chunk = Chunk::new();
1469 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1470 chunk.emit_u16(Op::Constant, 0, 1);
1471 chunk.emit(Op::MulInt, 1);
1472 chunk.emit(Op::Pop, 1);
1473 chunk.emit(Op::Return, 1);
1474 assert!(!chunk.references_outer_names);
1475 }
1476
1477 #[test]
1478 fn slot_only_chunk_does_not_reference_outer_names() {
1479 let mut chunk = Chunk::new();
1481 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1482 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1483 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1484 assert!(!chunk.references_outer_names);
1485 }
1486
1487 #[test]
1488 fn get_var_flags_outer_name_reference() {
1489 let mut chunk = Chunk::new();
1490 chunk.emit_u16(Op::GetVar, 0, 1);
1491 assert!(chunk.references_outer_names);
1492 }
1493
1494 #[test]
1495 fn set_var_flags_outer_name_reference() {
1496 let mut chunk = Chunk::new();
1497 chunk.emit_u16(Op::SetVar, 0, 1);
1498 assert!(chunk.references_outer_names);
1499 }
1500
1501 #[test]
1502 fn check_type_flags_outer_name_reference() {
1503 let mut chunk = Chunk::new();
1504 chunk.emit_u16(Op::CheckType, 0, 1);
1505 assert!(chunk.references_outer_names);
1506 }
1507
1508 #[test]
1509 fn call_builtin_flags_outer_name_reference() {
1510 let mut chunk = Chunk::new();
1511 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1512 assert!(chunk.references_outer_names);
1513 }
1514
1515 #[test]
1516 fn call_builtin_spread_flags_outer_name_reference() {
1517 let mut chunk = Chunk::new();
1518 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1519 assert!(chunk.references_outer_names);
1520 }
1521
1522 #[test]
1523 fn tail_call_flags_outer_name_reference() {
1524 let mut chunk = Chunk::new();
1527 chunk.emit_u8(Op::TailCall, 1, 1);
1528 assert!(chunk.references_outer_names);
1529 }
1530
1531 #[test]
1532 fn call_flags_outer_name_reference() {
1533 let mut chunk = Chunk::new();
1536 chunk.emit_u8(Op::Call, 1, 1);
1537 assert!(chunk.references_outer_names);
1538 }
1539
1540 #[test]
1541 fn pipe_flags_outer_name_reference() {
1542 let mut chunk = Chunk::new();
1545 chunk.emit(Op::Pipe, 1);
1546 assert!(chunk.references_outer_names);
1547 }
1548
1549 #[test]
1550 fn method_call_does_not_flag_outer_name_reference() {
1551 let mut chunk = Chunk::new();
1554 chunk.emit_method_call(0, 1, 1);
1555 chunk.emit_method_call_opt(0, 1, 1);
1556 assert!(!chunk.references_outer_names);
1557 }
1558
1559 #[test]
1560 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1561 let mut chunk = Chunk::new();
1564 chunk.emit_u16(Op::Constant, 0, 1);
1565 chunk.emit(Op::JumpIfFalse, 1);
1566 chunk.emit(Op::Jump, 1);
1567 chunk.emit(Op::Return, 1);
1568 chunk.emit(Op::Pop, 1);
1569 assert!(!chunk.references_outer_names);
1570 }
1571
1572 #[test]
1573 fn references_outer_names_is_monotonic() {
1574 let mut chunk = Chunk::new();
1577 chunk.emit_u16(Op::GetVar, 0, 1);
1578 assert!(chunk.references_outer_names);
1579 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1580 chunk.emit(Op::MulInt, 1);
1581 assert!(chunk.references_outer_names);
1582 }
1583
1584 #[test]
1585 fn freeze_thaw_round_trips_references_outer_names() {
1586 let mut chunk = Chunk::new();
1590 chunk.emit_u16(Op::GetVar, 0, 1);
1591 assert!(chunk.references_outer_names);
1592 let frozen = chunk.freeze_for_cache();
1593 let thawed = Chunk::from_cached(&frozen);
1594 assert!(thawed.references_outer_names);
1595
1596 let plain = Chunk::new();
1597 assert!(!plain.references_outer_names);
1598 let frozen_plain = plain.freeze_for_cache();
1599 let thawed_plain = Chunk::from_cached(&frozen_plain);
1600 assert!(!thawed_plain.references_outer_names);
1601 }
1602
1603 #[test]
1615 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1616 let mut chunk = Chunk::new();
1619 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1620 chunk.emit(Op::Pop, 1);
1621 chunk.emit(Op::Return, 1);
1622 assert!(chunk.inline_cache_slot(0).is_none());
1623 assert!(chunk.inline_cache_slot(3).is_none());
1624 assert!(chunk.inline_cache_slot(4).is_none());
1625 }
1626
1627 #[test]
1628 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1629 let mut chunk = Chunk::new();
1633 chunk.emit(Op::Add, 1);
1634 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1635 }
1636
1637 #[test]
1638 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1639 let mut chunk = Chunk::new();
1644 chunk.emit(Op::Add, 1);
1645 chunk.emit(Op::Sub, 1);
1646 chunk.emit(Op::Mul, 1);
1647 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1648 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1649 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1650 assert_ne!(s0, s1);
1651 assert_ne!(s1, s2);
1652 assert_ne!(s0, s2);
1653 }
1654
1655 #[test]
1656 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1657 let mut chunk = Chunk::new();
1660 chunk.emit(Op::Add, 1);
1661 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1662 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1663 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1664 }
1665
1666 #[test]
1667 fn inline_cache_slot_for_get_property_and_method_call() {
1668 let mut chunk = Chunk::new();
1672 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");
1677 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1678 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1679 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1680 }
1681
1682 #[test]
1683 fn inline_cache_slot_for_call_and_call_builtin() {
1684 let mut chunk = Chunk::new();
1689 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1691 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1692 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1693 assert!(
1694 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1695 "Op::CallBuiltin IC slot"
1696 );
1697 }
1698
1699 #[test]
1700 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1701 let mut chunk = Chunk::new();
1707 chunk.emit(Op::Add, 1);
1708 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1709 chunk.register_inline_cache(0);
1711 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1712 assert_eq!(slot_before, slot_after);
1713 }
1714
1715 #[test]
1716 fn inline_cache_index_round_trips_through_cached_chunk() {
1717 let mut chunk = Chunk::new();
1724 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1725 chunk.emit_u16(Op::Constant, 0, 1);
1726 chunk.emit(Op::Add, 1);
1727 chunk.emit(Op::Sub, 1);
1728 chunk.emit_method_call(0, 1, 1);
1729 chunk.emit_u8(Op::Call, 1, 1);
1730 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1731 .map(|o| (o, chunk.inline_cache_slot(o)))
1732 .collect();
1733 let frozen = chunk.freeze_for_cache();
1734 let thawed = Chunk::from_cached(&frozen);
1735 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1736 .map(|o| (o, thawed.inline_cache_slot(o)))
1737 .collect();
1738 assert_eq!(live_slots, thawed_slots);
1739 }
1740
1741 #[test]
1742 fn inline_cache_index_agrees_with_btreemap_view() {
1743 let mut chunk = Chunk::new();
1749 chunk.emit(Op::Add, 1);
1750 chunk.emit_u16(Op::GetVar, 0, 1);
1751 chunk.emit(Op::LessInt, 1);
1752 chunk.emit_u8(Op::Call, 2, 1);
1753 chunk.emit(Op::Equal, 1);
1754 chunk.emit_u16(Op::GetProperty, 0, 1);
1755 chunk.emit_method_call_opt(0, 0, 1);
1756 for offset in 0..chunk.code.len() {
1757 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1758 let from_index = chunk.inline_cache_slot(offset);
1759 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1760 }
1761 }
1762
1763 #[test]
1774 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1775 let mut chunk = Chunk::new();
1776 chunk.emit(Op::Add, 1);
1777 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1778 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1780 }
1781
1782 #[test]
1783 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1784 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1785 let mut chunk = Chunk::new();
1786 chunk.emit(Op::Add, 1);
1787 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1788 chunk.set_inline_cache_entry(
1789 slot,
1790 InlineCacheEntry::AdaptiveBinary {
1791 op: AdaptiveBinaryOp::Add,
1792 state: AdaptiveBinaryState::Warmup {
1793 shape: BinaryShape::Int,
1794 hits: 2,
1795 },
1796 },
1797 );
1798 let (op, state) = chunk
1799 .peek_adaptive_binary_cache(slot)
1800 .expect("warmed slot peek");
1801 assert_eq!(op, AdaptiveBinaryOp::Add);
1802 assert!(matches!(
1803 state,
1804 AdaptiveBinaryState::Warmup {
1805 shape: BinaryShape::Int,
1806 hits: 2
1807 }
1808 ));
1809 }
1810
1811 #[test]
1812 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1813 use super::{InlineCacheEntry, PropertyCacheTarget};
1819 let mut chunk = Chunk::new();
1820 chunk.emit(Op::Add, 1);
1821 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1822 chunk.set_inline_cache_entry(
1823 slot,
1824 InlineCacheEntry::Property {
1825 name_idx: 0,
1826 target: PropertyCacheTarget::ListCount,
1827 },
1828 );
1829 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1830 }
1831
1832 #[test]
1833 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1834 let chunk = Chunk::new();
1839 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1840 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1841 }
1842
1843 #[test]
1844 fn peek_adaptive_binary_state_is_copy() {
1845 fn assert_copy<T: Copy>() {}
1851 assert_copy::<super::AdaptiveBinaryState>();
1852 assert_copy::<super::AdaptiveBinaryOp>();
1853 assert_copy::<super::BinaryShape>();
1854 }
1855
1856 #[test]
1867 fn peek_method_cache_returns_none_for_empty_slot() {
1868 let mut chunk = Chunk::new();
1869 chunk.emit_method_call(0, 0, 1);
1870 let slot = chunk
1871 .inline_cache_slot(0)
1872 .expect("MethodCall registers a slot");
1873 assert!(chunk.peek_method_cache(slot).is_none());
1874 }
1875
1876 #[test]
1877 fn peek_method_cache_returns_triple_after_warmup() {
1878 let mut chunk = Chunk::new();
1879 chunk.emit_method_call(7, 2, 1);
1880 let slot = chunk
1881 .inline_cache_slot(0)
1882 .expect("MethodCall registers a slot");
1883 chunk.set_inline_cache_entry(
1884 slot,
1885 InlineCacheEntry::Method {
1886 name_idx: 7,
1887 argc: 2,
1888 target: MethodCacheTarget::ListContains,
1889 },
1890 );
1891 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1892 assert_eq!(name_idx, 7);
1893 assert_eq!(argc, 2);
1894 assert_eq!(target, MethodCacheTarget::ListContains);
1895 }
1896
1897 #[test]
1898 fn peek_method_cache_returns_none_for_non_method_variants() {
1899 let mut chunk = Chunk::new();
1903 chunk.emit_method_call(0, 0, 1);
1904 let slot = chunk
1905 .inline_cache_slot(0)
1906 .expect("MethodCall registers a slot");
1907
1908 chunk.set_inline_cache_entry(
1909 slot,
1910 InlineCacheEntry::Property {
1911 name_idx: 0,
1912 target: PropertyCacheTarget::ListCount,
1913 },
1914 );
1915 assert!(chunk.peek_method_cache(slot).is_none());
1916 }
1917
1918 #[test]
1919 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1920 let chunk = Chunk::new();
1921 assert!(chunk.peek_method_cache(0).is_none());
1922 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1923 }
1924
1925 #[test]
1926 fn peek_method_cache_target_is_copy() {
1927 fn assert_copy<T: Copy>() {}
1933 assert_copy::<super::MethodCacheTarget>();
1934 }
1935
1936 #[test]
1946 fn peek_property_cache_returns_none_for_empty_slot() {
1947 let mut chunk = Chunk::new();
1948 chunk.emit_u16(Op::GetProperty, 0, 1);
1949 let slot = chunk
1950 .inline_cache_slot(0)
1951 .expect("GetProperty registers a slot");
1952 assert!(chunk.peek_property_cache(slot).is_none());
1953 }
1954
1955 #[test]
1956 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1957 let mut chunk = Chunk::new();
1958 chunk.emit_u16(Op::GetProperty, 0, 1);
1959 let slot = chunk
1960 .inline_cache_slot(0)
1961 .expect("GetProperty registers a slot");
1962 chunk.set_inline_cache_entry(
1963 slot,
1964 InlineCacheEntry::Property {
1965 name_idx: 11,
1966 target: PropertyCacheTarget::DictField(Arc::from("count")),
1967 },
1968 );
1969 let (name_idx, target) = chunk
1970 .peek_property_cache(slot)
1971 .expect("warmed property slot peek");
1972 assert_eq!(name_idx, 11);
1973 match target {
1974 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1975 other => panic!("expected DictField, got {other:?}"),
1976 }
1977 }
1978
1979 #[test]
1980 fn peek_property_cache_returns_pair_for_unit_target() {
1981 let mut chunk = Chunk::new();
1985 chunk.emit_u16(Op::GetProperty, 0, 1);
1986 let slot = chunk
1987 .inline_cache_slot(0)
1988 .expect("GetProperty registers a slot");
1989 chunk.set_inline_cache_entry(
1990 slot,
1991 InlineCacheEntry::Property {
1992 name_idx: 3,
1993 target: PropertyCacheTarget::ListCount,
1994 },
1995 );
1996 let (name_idx, target) = chunk
1997 .peek_property_cache(slot)
1998 .expect("warmed property slot peek");
1999 assert_eq!(name_idx, 3);
2000 assert_eq!(target, PropertyCacheTarget::ListCount);
2001 }
2002
2003 #[test]
2004 fn peek_property_cache_returns_none_for_non_property_variants() {
2005 let mut chunk = Chunk::new();
2006 chunk.emit_u16(Op::GetProperty, 0, 1);
2007 let slot = chunk
2008 .inline_cache_slot(0)
2009 .expect("GetProperty registers a slot");
2010 chunk.set_inline_cache_entry(
2011 slot,
2012 InlineCacheEntry::Method {
2013 name_idx: 0,
2014 argc: 0,
2015 target: MethodCacheTarget::ListCount,
2016 },
2017 );
2018 assert!(chunk.peek_property_cache(slot).is_none());
2019 }
2020
2021 #[test]
2022 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
2023 let chunk = Chunk::new();
2024 assert!(chunk.peek_property_cache(0).is_none());
2025 assert!(chunk.peek_property_cache(usize::MAX).is_none());
2026 }
2027
2028 #[test]
2038 fn add_constant_keeps_signed_zero_and_nan_distinct() {
2039 let mut chunk = Chunk::new();
2040 let pos = chunk.add_constant(Constant::Float(0.0));
2043 let neg = chunk.add_constant(Constant::Float(-0.0));
2044 assert_ne!(pos, neg, "+0.0 and -0.0 must get distinct constant slots");
2045 assert_eq!(pos, chunk.add_constant(Constant::Float(0.0)));
2047 assert_eq!(neg, chunk.add_constant(Constant::Float(-0.0)));
2048 let a = chunk.add_constant(Constant::Float(1.5));
2050 assert_eq!(a, chunk.add_constant(Constant::Float(1.5)));
2051 let nan_a = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)));
2052 let nan_b = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0002)));
2053 assert_ne!(
2054 nan_a, nan_b,
2055 "distinct NaN payloads must get distinct constant slots"
2056 );
2057 assert_eq!(
2058 nan_a,
2059 chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)))
2060 );
2061 let s = chunk.add_constant(Constant::Int(7));
2063 assert_eq!(s, chunk.add_constant(Constant::Int(7)));
2064 }
2065
2066 #[test]
2067 fn add_constant_uses_first_slot_after_many_unique_constants() {
2068 let mut chunk = Chunk::new();
2069 let first = chunk.add_constant(Constant::String("shared".to_string()));
2070 for index in 0..10_000 {
2071 let slot = chunk.add_constant(Constant::String(format!("unique_{index}")));
2072 assert_eq!(slot as usize, index + 1);
2073 }
2074 assert_eq!(
2075 first,
2076 chunk.add_constant(Constant::String("shared".to_string())),
2077 "duplicate lookup must return the original slot after index growth"
2078 );
2079 }
2080
2081 #[test]
2082 fn constant_index_round_trips_through_cached_chunk() {
2083 let mut chunk = Chunk::new();
2084 let shared = chunk.add_constant(Constant::String("shared".to_string()));
2085 for index in 0..128 {
2086 chunk.add_constant(Constant::Int(index));
2087 }
2088
2089 let frozen = chunk.freeze_for_cache();
2090 let mut thawed = Chunk::from_cached(&frozen);
2091 assert_eq!(
2092 shared,
2093 thawed.add_constant(Constant::String("shared".to_string())),
2094 "cache thaw must rebuild the constant side index"
2095 );
2096 let next = thawed.add_constant(Constant::String("new".to_string()));
2097 assert_eq!(next as usize, frozen.constants.len());
2098 }
2099
2100 #[test]
2101 fn peek_direct_call_state_returns_none_for_empty_slot() {
2102 let mut chunk = Chunk::new();
2103 chunk.emit_u8(Op::Call, 0, 1);
2104 let slot = chunk
2105 .inline_cache_slot(0)
2106 .expect("Op::Call registers a slot");
2107 assert!(chunk.peek_direct_call_state(slot).is_none());
2108 }
2109
2110 #[test]
2111 fn peek_direct_call_state_returns_warmup_state() {
2112 let mut chunk = Chunk::new();
2113 chunk.emit_u8(Op::Call, 0, 1);
2114 let slot = chunk
2115 .inline_cache_slot(0)
2116 .expect("Op::Call registers a slot");
2117 let target = synthetic_direct_call_target();
2118 chunk.set_inline_cache_entry(
2119 slot,
2120 InlineCacheEntry::DirectCall {
2121 state: DirectCallState::Warmup {
2122 argc: 2,
2123 target: target.clone(),
2124 hits: 1,
2125 },
2126 },
2127 );
2128 let state = chunk
2129 .peek_direct_call_state(slot)
2130 .expect("warmed direct-call slot peek");
2131 match state {
2132 DirectCallState::Warmup {
2133 argc,
2134 target: peeked_target,
2135 hits,
2136 } => {
2137 assert_eq!(argc, 2);
2138 assert_eq!(hits, 1);
2139 assert_eq!(peeked_target, target);
2140 }
2141 other => panic!("expected Warmup, got {other:?}"),
2142 }
2143 }
2144
2145 #[test]
2146 fn peek_direct_call_state_returns_specialized_state() {
2147 let mut chunk = Chunk::new();
2148 chunk.emit_u8(Op::Call, 0, 1);
2149 let slot = chunk
2150 .inline_cache_slot(0)
2151 .expect("Op::Call registers a slot");
2152 let target = synthetic_direct_call_target();
2153 chunk.set_inline_cache_entry(
2154 slot,
2155 InlineCacheEntry::DirectCall {
2156 state: DirectCallState::Specialized {
2157 argc: 3,
2158 target: target.clone(),
2159 hits: 100,
2160 misses: 0,
2161 },
2162 },
2163 );
2164 let state = chunk
2165 .peek_direct_call_state(slot)
2166 .expect("warmed direct-call slot peek");
2167 match state {
2168 DirectCallState::Specialized {
2169 argc,
2170 target: peeked_target,
2171 hits,
2172 misses,
2173 } => {
2174 assert_eq!(argc, 3);
2175 assert_eq!(hits, 100);
2176 assert_eq!(misses, 0);
2177 assert_eq!(peeked_target, target);
2178 }
2179 other => panic!("expected Specialized, got {other:?}"),
2180 }
2181 }
2182
2183 #[test]
2184 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2185 let mut chunk = Chunk::new();
2186 chunk.emit_u8(Op::Call, 0, 1);
2187 let slot = chunk
2188 .inline_cache_slot(0)
2189 .expect("Op::Call registers a slot");
2190
2191 chunk.set_inline_cache_entry(
2192 slot,
2193 InlineCacheEntry::Property {
2194 name_idx: 0,
2195 target: PropertyCacheTarget::ListCount,
2196 },
2197 );
2198 assert!(chunk.peek_direct_call_state(slot).is_none());
2199 }
2200
2201 #[test]
2202 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2203 let chunk = Chunk::new();
2204 assert!(chunk.peek_direct_call_state(0).is_none());
2205 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2206 }
2207
2208 fn synthetic_direct_call_target() -> DirectCallTarget {
2212 use crate::value::VmClosure;
2213 use crate::{CompiledFunction, VmEnv};
2214 let func = CompiledFunction {
2215 name: "synthetic".to_string(),
2216 type_params: Vec::new(),
2217 nominal_type_names: Vec::new(),
2218 params: Vec::new(),
2219 default_start: None,
2220 chunk: Arc::new(Chunk::new()),
2221 is_generator: false,
2222 is_stream: false,
2223 has_rest_param: false,
2224 has_runtime_type_checks: false,
2225 };
2226 DirectCallTarget::Closure(Arc::new(VmClosure {
2227 func: Arc::new(func),
2228 env: VmEnv::new(),
2229 source_dir: None,
2230 module_functions: None,
2231 module_state: None,
2232 }))
2233 }
2234}