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
653 | SetLocalSlotProperty | Pop => -1,
654 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
658 | PushScope | PopScope | PopIterator | PopHandler => 0,
659 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
661 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
662 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
663 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
664 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
665 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
666 IterInit => -1,
669 Slice | SetSubscript | SetLocalSlotSubscript => -2,
672 BuildList | Concat | CallBuiltin => 1 - count,
674 BuildDict => 1 - 2 * count,
675 Call | MethodCall | MethodCallOpt => -count,
677 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
680 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
681 | SyncMutexEnter | SyncMutexEnterKeyed | TaskScopeEnter | TaskScopeExit | Import
682 | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum | MatchEnum | Yield
683 | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
684 })
685}
686
687impl Chunk {
688 pub fn new() -> Self {
689 Self {
690 cache_id: next_chunk_cache_id(),
691 code: Vec::new(),
692 constants: Vec::new(),
693 constant_index: HashMap::new(),
694 lines: Vec::new(),
695 columns: Vec::new(),
696 source_file: None,
697 current_col: 0,
698 functions: Vec::new(),
699 inline_cache_slots: BTreeMap::new(),
700 inline_cache_index: Vec::new(),
701 inline_caches: Arc::new(Mutex::new(Vec::new())),
702 constant_strings: Arc::new(Mutex::new(Vec::new())),
703 local_slots: Vec::new(),
704 references_outer_names: false,
705 #[cfg(debug_assertions)]
706 balance_depth: 0,
707 #[cfg(debug_assertions)]
708 balance_nonlinear: 0,
709 }
710 }
711
712 pub fn set_column(&mut self, col: u32) {
714 self.current_col = col;
715 }
716
717 pub fn add_constant(&mut self, constant: Constant) -> u16 {
719 debug_assert!(
720 self.constant_index.len() <= self.constants.len(),
721 "constant side index cannot outgrow the constant pool"
722 );
723 let key = ConstantKey::from(&constant);
724 if let Some(index) = self.constant_index.get(&key) {
725 debug_assert!(
726 self.constants
727 .get(*index as usize)
728 .is_some_and(|existing| constants_identical(existing, &constant)),
729 "constant side index drifted from the constant pool"
730 );
731 return *index;
732 }
733 let idx = self.constants.len();
734 let idx = u16::try_from(idx).expect("constant pool exceeded u16 operand space");
735 self.constants.push(constant);
736 self.constant_index.insert(key, idx);
737 idx
738 }
739
740 pub fn emit(&mut self, op: Op, line: u32) {
742 #[cfg(debug_assertions)]
743 self.note_balance(op, 0);
744 let col = self.current_col;
745 let op_offset = self.code.len();
746 self.code.push(op as u8);
747 self.lines.push(line);
748 self.columns.push(col);
749 if is_adaptive_binary_op(op) {
750 self.register_inline_cache(op_offset);
751 }
752 if op_reads_outer_name(op) {
753 self.references_outer_names = true;
754 }
755 }
756
757 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
759 #[cfg(debug_assertions)]
760 self.note_balance(op, arg);
761 let col = self.current_col;
762 let op_offset = self.code.len();
763 self.code.push(op as u8);
764 self.code.push((arg >> 8) as u8);
765 self.code.push((arg & 0xFF) as u8);
766 self.lines.push(line);
767 self.lines.push(line);
768 self.lines.push(line);
769 self.columns.push(col);
770 self.columns.push(col);
771 self.columns.push(col);
772 if matches!(
773 op,
774 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
775 ) {
776 self.register_inline_cache(op_offset);
777 }
778 if op_reads_outer_name(op) {
779 self.references_outer_names = true;
780 }
781 }
782
783 pub fn emit_set_local_slot_property(&mut self, prop_idx: u16, slot: u16, line: u32) {
786 #[cfg(debug_assertions)]
787 self.note_balance(Op::SetLocalSlotProperty, 0);
788 let col = self.current_col;
789 self.code.push(Op::SetLocalSlotProperty as u8);
790 self.code.push((prop_idx >> 8) as u8);
791 self.code.push((prop_idx & 0xFF) as u8);
792 self.code.push((slot >> 8) as u8);
793 self.code.push((slot & 0xFF) as u8);
794 for _ in 0..5 {
795 self.lines.push(line);
796 self.columns.push(col);
797 }
798 }
799
800 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
802 #[cfg(debug_assertions)]
803 self.note_balance(op, arg as u16);
804 let col = self.current_col;
805 let op_offset = self.code.len();
806 self.code.push(op as u8);
807 self.code.push(arg);
808 self.lines.push(line);
809 self.lines.push(line);
810 self.columns.push(col);
811 self.columns.push(col);
812 if matches!(op, Op::Call) {
813 self.register_inline_cache(op_offset);
814 }
815 if op_reads_outer_name(op) {
816 self.references_outer_names = true;
817 }
818 }
819
820 pub fn emit_call_builtin(
822 &mut self,
823 id: crate::BuiltinId,
824 name_idx: u16,
825 arg_count: u8,
826 line: u32,
827 ) {
828 #[cfg(debug_assertions)]
829 self.note_balance(Op::CallBuiltin, arg_count as u16);
830 let col = self.current_col;
831 let op_offset = self.code.len();
832 self.code.push(Op::CallBuiltin as u8);
833 self.code.extend_from_slice(&id.raw().to_be_bytes());
834 self.code.push((name_idx >> 8) as u8);
835 self.code.push((name_idx & 0xFF) as u8);
836 self.code.push(arg_count);
837 for _ in 0..12 {
838 self.lines.push(line);
839 self.columns.push(col);
840 }
841 self.register_inline_cache(op_offset);
842 self.references_outer_names = true;
843 }
844
845 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
847 #[cfg(debug_assertions)]
848 self.note_balance(Op::CallBuiltinSpread, 0);
849 let col = self.current_col;
850 self.code.push(Op::CallBuiltinSpread as u8);
851 self.code.extend_from_slice(&id.raw().to_be_bytes());
852 self.code.push((name_idx >> 8) as u8);
853 self.code.push((name_idx & 0xFF) as u8);
854 for _ in 0..11 {
855 self.lines.push(line);
856 self.columns.push(col);
857 }
858 self.references_outer_names = true;
859 }
860
861 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
863 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
864 }
865
866 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
868 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
869 }
870
871 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
872 #[cfg(debug_assertions)]
873 self.note_balance(op, arg_count as u16);
874 let col = self.current_col;
875 let op_offset = self.code.len();
876 self.code.push(op as u8);
877 self.code.push((name_idx >> 8) as u8);
878 self.code.push((name_idx & 0xFF) as u8);
879 self.code.push(arg_count);
880 self.lines.push(line);
881 self.lines.push(line);
882 self.lines.push(line);
883 self.lines.push(line);
884 self.columns.push(col);
885 self.columns.push(col);
886 self.columns.push(col);
887 self.columns.push(col);
888 self.register_inline_cache(op_offset);
889 }
890
891 pub fn current_offset(&self) -> usize {
893 self.code.len()
894 }
895
896 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
898 #[cfg(debug_assertions)]
899 self.note_balance(op, 0);
900 let col = self.current_col;
901 self.code.push(op as u8);
902 let patch_pos = self.code.len();
903 self.code.push(0xFF);
904 self.code.push(0xFF);
905 self.lines.push(line);
906 self.lines.push(line);
907 self.lines.push(line);
908 self.columns.push(col);
909 self.columns.push(col);
910 self.columns.push(col);
911 patch_pos
912 }
913
914 pub fn patch_jump(&mut self, patch_pos: usize) {
916 let target = self.code.len() as u16;
917 self.code[patch_pos] = (target >> 8) as u8;
918 self.code[patch_pos + 1] = (target & 0xFF) as u8;
919 }
920
921 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
923 let target = target as u16;
924 self.code[patch_pos] = (target >> 8) as u8;
925 self.code[patch_pos + 1] = (target & 0xFF) as u8;
926 }
927
928 pub fn read_u16(&self, pos: usize) -> u16 {
930 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
931 }
932
933 #[cfg(debug_assertions)]
937 fn note_balance(&mut self, op: Op, count: u16) {
938 match op_stack_delta(op, count) {
939 Some(delta) => self.balance_depth += delta,
940 None => self.balance_nonlinear += 1,
941 }
942 }
943
944 #[cfg(debug_assertions)]
947 pub(crate) fn balance_probe(&self) -> BalanceProbe {
948 BalanceProbe {
949 depth: self.balance_depth,
950 nonlinear: self.balance_nonlinear,
951 }
952 }
953
954 #[cfg(debug_assertions)]
960 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
961 if self.balance_nonlinear == probe.nonlinear {
962 Some(self.balance_depth - probe.depth)
963 } else {
964 None
965 }
966 }
967
968 fn register_inline_cache(&mut self, op_offset: usize) {
969 if self.inline_cache_slots.contains_key(&op_offset) {
970 return;
971 }
972 let mut entries = self.inline_caches.lock();
973 let slot = entries.len();
974 entries.push(InlineCacheEntry::Empty);
975 self.inline_cache_slots.insert(op_offset, slot);
976 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
977 }
978
979 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
984 if op_offset >= index.len() {
985 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
986 }
987 index[op_offset] = slot as u32;
988 }
989
990 #[inline]
999 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
1000 match self.inline_cache_index.get(op_offset).copied() {
1001 None | Some(NO_INLINE_CACHE_SLOT) => None,
1002 Some(slot) => Some(slot as usize),
1003 }
1004 }
1005
1006 pub(crate) fn inline_cache_slot_count(&self) -> usize {
1007 self.inline_cache_slots.len()
1008 }
1009
1010 pub(crate) fn cache_id(&self) -> u64 {
1011 self.cache_id
1012 }
1013
1014 #[cfg(feature = "vm-bench-internals")]
1021 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
1022 self.inline_cache_slots.get(&op_offset).copied()
1023 }
1024
1025 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<Arc<str>> {
1030 let mut entries = self.constant_strings.lock();
1035 if entries.len() < self.constants.len() {
1036 entries.resize(self.constants.len(), None);
1037 }
1038 if let Some(Some(existing)) = entries.get(idx) {
1039 return Some(Arc::clone(existing));
1040 }
1041 let materialized = match self.constants.get(idx)? {
1042 Constant::String(s) => Arc::<str>::from(s.as_str()),
1043 _ => return None,
1044 };
1045 entries[idx] = Some(Arc::clone(&materialized));
1046 Some(materialized)
1047 }
1048
1049 #[cfg(feature = "vm-bench-internals")]
1050 pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
1051 self.inline_caches
1052 .lock()
1053 .get(slot)
1054 .cloned()
1055 .unwrap_or(InlineCacheEntry::Empty)
1056 }
1057
1058 #[inline]
1070 #[cfg(any(test, feature = "vm-bench-internals"))]
1071 pub(crate) fn peek_adaptive_binary_cache(
1072 &self,
1073 slot: usize,
1074 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
1075 match self.inline_caches.lock().get(slot)? {
1076 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
1077 _ => None,
1078 }
1079 }
1080
1081 #[inline]
1093 #[cfg(any(test, feature = "vm-bench-internals"))]
1094 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1095 match self.inline_caches.lock().get(slot)? {
1096 &InlineCacheEntry::Method {
1097 name_idx,
1098 argc,
1099 target,
1100 } => Some((name_idx, argc, target)),
1101 _ => None,
1102 }
1103 }
1104
1105 #[inline]
1115 #[cfg(any(test, feature = "vm-bench-internals"))]
1116 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1117 match self.inline_caches.lock().get(slot)? {
1118 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1119 _ => None,
1120 }
1121 }
1122
1123 #[inline]
1133 #[cfg(any(test, feature = "vm-bench-internals"))]
1134 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1135 match self.inline_caches.lock().get(slot)? {
1136 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1137 _ => None,
1138 }
1139 }
1140
1141 #[cfg(any(test, feature = "vm-bench-internals"))]
1142 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1143 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1144 *existing = entry;
1145 }
1146 }
1147
1148 pub fn freeze_for_cache(&self) -> CachedChunk {
1149 CachedChunk {
1150 code: self.code.clone(),
1151 constants: self.constants.clone(),
1152 lines: self.lines.clone(),
1153 columns: self.columns.clone(),
1154 source_file: self.source_file.clone(),
1155 current_col: self.current_col,
1156 functions: self
1157 .functions
1158 .iter()
1159 .map(|function| function.freeze_for_cache())
1160 .collect(),
1161 inline_cache_slots: self.inline_cache_slots.clone(),
1162 local_slots: self.local_slots.clone(),
1163 references_outer_names: self.references_outer_names,
1164 }
1165 }
1166
1167 pub fn from_cached(cached: &CachedChunk) -> Self {
1168 let inline_cache_count = cached.inline_cache_slots.len();
1169 let constants_count = cached.constants.len();
1170 let mut inline_cache_index = Vec::new();
1177 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1178 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1179 if op_offset < inline_cache_index.len() {
1180 inline_cache_index[op_offset] = slot as u32;
1181 }
1182 }
1183 Self {
1184 cache_id: next_chunk_cache_id(),
1185 code: cached.code.clone(),
1186 constants: cached.constants.clone(),
1187 constant_index: build_constant_index(&cached.constants),
1188 lines: cached.lines.clone(),
1189 columns: cached.columns.clone(),
1190 source_file: cached.source_file.clone(),
1191 current_col: cached.current_col,
1192 functions: cached
1193 .functions
1194 .iter()
1195 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1196 .collect(),
1197 inline_cache_slots: cached.inline_cache_slots.clone(),
1198 inline_cache_index,
1199 inline_caches: Arc::new(Mutex::new(vec![
1200 InlineCacheEntry::Empty;
1201 inline_cache_count
1202 ])),
1203 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1204 local_slots: cached.local_slots.clone(),
1205 references_outer_names: cached.references_outer_names,
1206 #[cfg(debug_assertions)]
1207 balance_depth: 0,
1208 #[cfg(debug_assertions)]
1209 balance_nonlinear: 0,
1210 }
1211 }
1212
1213 pub(crate) fn add_local_slot(
1214 &mut self,
1215 name: String,
1216 mutable: bool,
1217 scope_depth: usize,
1218 ) -> u16 {
1219 let idx = self.local_slots.len();
1220 self.local_slots.push(LocalSlotInfo {
1221 name,
1222 mutable,
1223 scope_depth,
1224 });
1225 idx as u16
1226 }
1227
1228 pub fn read_u64(&self, pos: usize) -> u64 {
1230 u64::from_be_bytes([
1231 self.code[pos],
1232 self.code[pos + 1],
1233 self.code[pos + 2],
1234 self.code[pos + 3],
1235 self.code[pos + 4],
1236 self.code[pos + 5],
1237 self.code[pos + 6],
1238 self.code[pos + 7],
1239 ])
1240 }
1241
1242 pub fn disassemble(&self, name: &str) -> String {
1246 let mut out = format!("== {name} ==\n");
1247 let mut ip = 0;
1248 while ip < self.code.len() {
1249 let op_byte = self.code[ip];
1250 let line = self.lines.get(ip).copied().unwrap_or(0);
1251 out.push_str(&format!("{ip:04} [{line:>4}] "));
1252 ip += 1;
1253
1254 if let Some(op) = Op::from_byte(op_byte) {
1255 self.disassemble_op(op, &mut ip, &mut out);
1256 } else {
1257 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1258 }
1259 }
1260 out
1261 }
1262}
1263
1264pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1275 label.to_string()
1276}
1277
1278pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1279 let arg = chunk.code[*ip];
1280 *ip += 1;
1281 format!("{label} {arg:>4}")
1282}
1283
1284pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1285 let arg = chunk.read_u16(*ip);
1286 *ip += 2;
1287 format!("{label} {arg:>4}")
1288}
1289
1290pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1291 let catch_offset = chunk.read_u16(*ip);
1292 *ip += 2;
1293 let type_idx = chunk.read_u16(*ip);
1294 *ip += 2;
1295 if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1296 format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1297 } else {
1298 format!("{label} {catch_offset:>4} type {type_idx:>4}")
1299 }
1300}
1301
1302pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1303 let idx = chunk.read_u16(*ip);
1304 *ip += 2;
1305 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1306}
1307
1308pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1309 let slot = chunk.read_u16(*ip);
1310 *ip += 2;
1311 let mut out = format!("{label} {slot:>4}");
1312 if let Some(info) = chunk.local_slots.get(slot as usize) {
1313 out.push_str(&format!(" ({})", info.name));
1314 }
1315 out
1316}
1317
1318pub(crate) fn disasm_const_pool_local_slot(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1319 let prop = chunk.read_u16(*ip);
1320 *ip += 2;
1321 let slot = chunk.read_u16(*ip);
1322 *ip += 2;
1323 let mut out = format!(
1324 "{label} prop {prop:>4} ({}) slot {slot:>4}",
1325 chunk.constants[prop as usize]
1326 );
1327 if let Some(info) = chunk.local_slots.get(slot as usize) {
1328 out.push_str(&format!(" ({})", info.name));
1329 }
1330 out
1331}
1332
1333pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1334 let idx = chunk.read_u16(*ip);
1335 *ip += 2;
1336 let argc = chunk.code[*ip];
1337 *ip += 1;
1338 format!(
1339 "{label} {idx:>4} ({}) argc={argc}",
1340 chunk.constants[idx as usize]
1341 )
1342}
1343
1344pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1345 let enum_idx = chunk.read_u16(*ip);
1346 *ip += 2;
1347 let var_idx = chunk.read_u16(*ip);
1348 *ip += 2;
1349 format!(
1350 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1351 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1352 )
1353}
1354
1355pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1356 let enum_idx = chunk.read_u16(*ip);
1357 *ip += 2;
1358 let var_idx = chunk.read_u16(*ip);
1359 *ip += 2;
1360 let field_count = chunk.read_u16(*ip);
1361 *ip += 2;
1362 format!(
1363 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1364 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1365 )
1366}
1367
1368pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1369 let path_idx = chunk.read_u16(*ip);
1370 *ip += 2;
1371 let names_idx = chunk.read_u16(*ip);
1372 *ip += 2;
1373 format!(
1374 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1375 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1376 )
1377}
1378
1379pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1380 let var_idx = chunk.read_u16(*ip);
1381 *ip += 2;
1382 let type_idx = chunk.read_u16(*ip);
1383 *ip += 2;
1384 format!(
1385 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1386 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1387 )
1388}
1389
1390pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1391 let id = chunk.read_u64(*ip);
1392 *ip += 8;
1393 let idx = chunk.read_u16(*ip);
1394 *ip += 2;
1395 let argc = chunk.code[*ip];
1396 *ip += 1;
1397 format!(
1398 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1399 chunk.constants[idx as usize],
1400 )
1401}
1402
1403pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1404 let id = chunk.read_u64(*ip);
1405 *ip += 8;
1406 let idx = chunk.read_u16(*ip);
1407 *ip += 2;
1408 format!(
1409 "{label} {id:#018x} {idx:>4} ({})",
1410 chunk.constants[idx as usize],
1411 )
1412}
1413
1414pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1415 let idx = chunk.read_u16(*ip);
1421 *ip += 2;
1422 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1423}
1424
1425impl Default for Chunk {
1426 fn default() -> Self {
1427 Self::new()
1428 }
1429}
1430
1431#[cfg(test)]
1432mod tests {
1433 use std::sync::Arc;
1434
1435 use super::{
1436 Chunk, Constant, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget,
1437 Op, PropertyCacheTarget,
1438 };
1439 use crate::BuiltinId;
1440
1441 #[test]
1442 fn op_from_byte_matches_repr_order() {
1443 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1444 assert_eq!(byte as u8, op as u8);
1445 assert_eq!(Op::from_byte(byte as u8), Some(op));
1446 }
1447 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1448 assert_eq!(Op::COUNT, Op::ALL.len());
1449 }
1450
1451 #[test]
1452 fn disassemble_covers_every_opcode_variant() {
1453 for op in Op::ALL.iter().copied() {
1463 let mut chunk = Chunk::new();
1464 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1465 for _ in 0..16 {
1469 chunk.code.push(0);
1470 }
1471 let mut ip: usize = 0;
1472 let mut out = String::new();
1473 chunk.disassemble_op(op, &mut ip, &mut out);
1474 assert!(
1475 !out.contains("UNKNOWN"),
1476 "disasm emitted UNKNOWN for {op:?}: {out}",
1477 );
1478 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1479 }
1480 }
1481
1482 #[test]
1491 fn empty_chunk_does_not_reference_outer_names() {
1492 let chunk = Chunk::new();
1493 assert!(!chunk.references_outer_names);
1494 }
1495
1496 #[test]
1497 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1498 let mut chunk = Chunk::new();
1503 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1504 chunk.emit_u16(Op::Constant, 0, 1);
1505 chunk.emit(Op::MulInt, 1);
1506 chunk.emit(Op::Pop, 1);
1507 chunk.emit(Op::Return, 1);
1508 assert!(!chunk.references_outer_names);
1509 }
1510
1511 #[test]
1512 fn slot_only_chunk_does_not_reference_outer_names() {
1513 let mut chunk = Chunk::new();
1515 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1516 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1517 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1518 assert!(!chunk.references_outer_names);
1519 }
1520
1521 #[test]
1522 fn get_var_flags_outer_name_reference() {
1523 let mut chunk = Chunk::new();
1524 chunk.emit_u16(Op::GetVar, 0, 1);
1525 assert!(chunk.references_outer_names);
1526 }
1527
1528 #[test]
1529 fn set_var_flags_outer_name_reference() {
1530 let mut chunk = Chunk::new();
1531 chunk.emit_u16(Op::SetVar, 0, 1);
1532 assert!(chunk.references_outer_names);
1533 }
1534
1535 #[test]
1536 fn check_type_flags_outer_name_reference() {
1537 let mut chunk = Chunk::new();
1538 chunk.emit_u16(Op::CheckType, 0, 1);
1539 assert!(chunk.references_outer_names);
1540 }
1541
1542 #[test]
1543 fn call_builtin_flags_outer_name_reference() {
1544 let mut chunk = Chunk::new();
1545 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1546 assert!(chunk.references_outer_names);
1547 }
1548
1549 #[test]
1550 fn call_builtin_spread_flags_outer_name_reference() {
1551 let mut chunk = Chunk::new();
1552 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1553 assert!(chunk.references_outer_names);
1554 }
1555
1556 #[test]
1557 fn tail_call_flags_outer_name_reference() {
1558 let mut chunk = Chunk::new();
1561 chunk.emit_u8(Op::TailCall, 1, 1);
1562 assert!(chunk.references_outer_names);
1563 }
1564
1565 #[test]
1566 fn call_flags_outer_name_reference() {
1567 let mut chunk = Chunk::new();
1570 chunk.emit_u8(Op::Call, 1, 1);
1571 assert!(chunk.references_outer_names);
1572 }
1573
1574 #[test]
1575 fn pipe_flags_outer_name_reference() {
1576 let mut chunk = Chunk::new();
1579 chunk.emit(Op::Pipe, 1);
1580 assert!(chunk.references_outer_names);
1581 }
1582
1583 #[test]
1584 fn method_call_does_not_flag_outer_name_reference() {
1585 let mut chunk = Chunk::new();
1588 chunk.emit_method_call(0, 1, 1);
1589 chunk.emit_method_call_opt(0, 1, 1);
1590 assert!(!chunk.references_outer_names);
1591 }
1592
1593 #[test]
1594 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1595 let mut chunk = Chunk::new();
1598 chunk.emit_u16(Op::Constant, 0, 1);
1599 chunk.emit(Op::JumpIfFalse, 1);
1600 chunk.emit(Op::Jump, 1);
1601 chunk.emit(Op::Return, 1);
1602 chunk.emit(Op::Pop, 1);
1603 assert!(!chunk.references_outer_names);
1604 }
1605
1606 #[test]
1607 fn references_outer_names_is_monotonic() {
1608 let mut chunk = Chunk::new();
1611 chunk.emit_u16(Op::GetVar, 0, 1);
1612 assert!(chunk.references_outer_names);
1613 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1614 chunk.emit(Op::MulInt, 1);
1615 assert!(chunk.references_outer_names);
1616 }
1617
1618 #[test]
1619 fn freeze_thaw_round_trips_references_outer_names() {
1620 let mut chunk = Chunk::new();
1624 chunk.emit_u16(Op::GetVar, 0, 1);
1625 assert!(chunk.references_outer_names);
1626 let frozen = chunk.freeze_for_cache();
1627 let thawed = Chunk::from_cached(&frozen);
1628 assert!(thawed.references_outer_names);
1629
1630 let plain = Chunk::new();
1631 assert!(!plain.references_outer_names);
1632 let frozen_plain = plain.freeze_for_cache();
1633 let thawed_plain = Chunk::from_cached(&frozen_plain);
1634 assert!(!thawed_plain.references_outer_names);
1635 }
1636
1637 #[test]
1649 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1650 let mut chunk = Chunk::new();
1653 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1654 chunk.emit(Op::Pop, 1);
1655 chunk.emit(Op::Return, 1);
1656 assert!(chunk.inline_cache_slot(0).is_none());
1657 assert!(chunk.inline_cache_slot(3).is_none());
1658 assert!(chunk.inline_cache_slot(4).is_none());
1659 }
1660
1661 #[test]
1662 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1663 let mut chunk = Chunk::new();
1667 chunk.emit(Op::Add, 1);
1668 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1669 }
1670
1671 #[test]
1672 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1673 let mut chunk = Chunk::new();
1678 chunk.emit(Op::Add, 1);
1679 chunk.emit(Op::Sub, 1);
1680 chunk.emit(Op::Mul, 1);
1681 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1682 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1683 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1684 assert_ne!(s0, s1);
1685 assert_ne!(s1, s2);
1686 assert_ne!(s0, s2);
1687 }
1688
1689 #[test]
1690 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1691 let mut chunk = Chunk::new();
1694 chunk.emit(Op::Add, 1);
1695 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1696 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1697 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1698 }
1699
1700 #[test]
1701 fn inline_cache_slot_for_get_property_and_method_call() {
1702 let mut chunk = Chunk::new();
1706 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");
1711 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1712 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1713 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1714 }
1715
1716 #[test]
1717 fn inline_cache_slot_for_call_and_call_builtin() {
1718 let mut chunk = Chunk::new();
1723 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1725 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1726 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1727 assert!(
1728 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1729 "Op::CallBuiltin IC slot"
1730 );
1731 }
1732
1733 #[test]
1734 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1735 let mut chunk = Chunk::new();
1741 chunk.emit(Op::Add, 1);
1742 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1743 chunk.register_inline_cache(0);
1745 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1746 assert_eq!(slot_before, slot_after);
1747 }
1748
1749 #[test]
1750 fn inline_cache_index_round_trips_through_cached_chunk() {
1751 let mut chunk = Chunk::new();
1758 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1759 chunk.emit_u16(Op::Constant, 0, 1);
1760 chunk.emit(Op::Add, 1);
1761 chunk.emit(Op::Sub, 1);
1762 chunk.emit_method_call(0, 1, 1);
1763 chunk.emit_u8(Op::Call, 1, 1);
1764 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1765 .map(|o| (o, chunk.inline_cache_slot(o)))
1766 .collect();
1767 let frozen = chunk.freeze_for_cache();
1768 let thawed = Chunk::from_cached(&frozen);
1769 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1770 .map(|o| (o, thawed.inline_cache_slot(o)))
1771 .collect();
1772 assert_eq!(live_slots, thawed_slots);
1773 }
1774
1775 #[test]
1776 fn inline_cache_index_agrees_with_btreemap_view() {
1777 let mut chunk = Chunk::new();
1783 chunk.emit(Op::Add, 1);
1784 chunk.emit_u16(Op::GetVar, 0, 1);
1785 chunk.emit(Op::LessInt, 1);
1786 chunk.emit_u8(Op::Call, 2, 1);
1787 chunk.emit(Op::Equal, 1);
1788 chunk.emit_u16(Op::GetProperty, 0, 1);
1789 chunk.emit_method_call_opt(0, 0, 1);
1790 for offset in 0..chunk.code.len() {
1791 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1792 let from_index = chunk.inline_cache_slot(offset);
1793 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1794 }
1795 }
1796
1797 #[test]
1808 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1809 let mut chunk = Chunk::new();
1810 chunk.emit(Op::Add, 1);
1811 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1812 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1814 }
1815
1816 #[test]
1817 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1818 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
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::AdaptiveBinary {
1825 op: AdaptiveBinaryOp::Add,
1826 state: AdaptiveBinaryState::Warmup {
1827 shape: BinaryShape::Int,
1828 hits: 2,
1829 },
1830 },
1831 );
1832 let (op, state) = chunk
1833 .peek_adaptive_binary_cache(slot)
1834 .expect("warmed slot peek");
1835 assert_eq!(op, AdaptiveBinaryOp::Add);
1836 assert!(matches!(
1837 state,
1838 AdaptiveBinaryState::Warmup {
1839 shape: BinaryShape::Int,
1840 hits: 2
1841 }
1842 ));
1843 }
1844
1845 #[test]
1846 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1847 use super::{InlineCacheEntry, PropertyCacheTarget};
1853 let mut chunk = Chunk::new();
1854 chunk.emit(Op::Add, 1);
1855 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1856 chunk.set_inline_cache_entry(
1857 slot,
1858 InlineCacheEntry::Property {
1859 name_idx: 0,
1860 target: PropertyCacheTarget::ListCount,
1861 },
1862 );
1863 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1864 }
1865
1866 #[test]
1867 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1868 let chunk = Chunk::new();
1873 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1874 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1875 }
1876
1877 #[test]
1878 fn peek_adaptive_binary_state_is_copy() {
1879 fn assert_copy<T: Copy>() {}
1885 assert_copy::<super::AdaptiveBinaryState>();
1886 assert_copy::<super::AdaptiveBinaryOp>();
1887 assert_copy::<super::BinaryShape>();
1888 }
1889
1890 #[test]
1901 fn peek_method_cache_returns_none_for_empty_slot() {
1902 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 assert!(chunk.peek_method_cache(slot).is_none());
1908 }
1909
1910 #[test]
1911 fn peek_method_cache_returns_triple_after_warmup() {
1912 let mut chunk = Chunk::new();
1913 chunk.emit_method_call(7, 2, 1);
1914 let slot = chunk
1915 .inline_cache_slot(0)
1916 .expect("MethodCall registers a slot");
1917 chunk.set_inline_cache_entry(
1918 slot,
1919 InlineCacheEntry::Method {
1920 name_idx: 7,
1921 argc: 2,
1922 target: MethodCacheTarget::ListContains,
1923 },
1924 );
1925 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1926 assert_eq!(name_idx, 7);
1927 assert_eq!(argc, 2);
1928 assert_eq!(target, MethodCacheTarget::ListContains);
1929 }
1930
1931 #[test]
1932 fn peek_method_cache_returns_none_for_non_method_variants() {
1933 let mut chunk = Chunk::new();
1937 chunk.emit_method_call(0, 0, 1);
1938 let slot = chunk
1939 .inline_cache_slot(0)
1940 .expect("MethodCall registers a slot");
1941
1942 chunk.set_inline_cache_entry(
1943 slot,
1944 InlineCacheEntry::Property {
1945 name_idx: 0,
1946 target: PropertyCacheTarget::ListCount,
1947 },
1948 );
1949 assert!(chunk.peek_method_cache(slot).is_none());
1950 }
1951
1952 #[test]
1953 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1954 let chunk = Chunk::new();
1955 assert!(chunk.peek_method_cache(0).is_none());
1956 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1957 }
1958
1959 #[test]
1960 fn peek_method_cache_target_is_copy() {
1961 fn assert_copy<T: Copy>() {}
1967 assert_copy::<super::MethodCacheTarget>();
1968 }
1969
1970 #[test]
1980 fn peek_property_cache_returns_none_for_empty_slot() {
1981 let mut chunk = Chunk::new();
1982 chunk.emit_u16(Op::GetProperty, 0, 1);
1983 let slot = chunk
1984 .inline_cache_slot(0)
1985 .expect("GetProperty registers a slot");
1986 assert!(chunk.peek_property_cache(slot).is_none());
1987 }
1988
1989 #[test]
1990 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1991 let mut chunk = Chunk::new();
1992 chunk.emit_u16(Op::GetProperty, 0, 1);
1993 let slot = chunk
1994 .inline_cache_slot(0)
1995 .expect("GetProperty registers a slot");
1996 chunk.set_inline_cache_entry(
1997 slot,
1998 InlineCacheEntry::Property {
1999 name_idx: 11,
2000 target: PropertyCacheTarget::DictField(Arc::from("count")),
2001 },
2002 );
2003 let (name_idx, target) = chunk
2004 .peek_property_cache(slot)
2005 .expect("warmed property slot peek");
2006 assert_eq!(name_idx, 11);
2007 match target {
2008 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
2009 other => panic!("expected DictField, got {other:?}"),
2010 }
2011 }
2012
2013 #[test]
2014 fn peek_property_cache_returns_pair_for_unit_target() {
2015 let mut chunk = Chunk::new();
2019 chunk.emit_u16(Op::GetProperty, 0, 1);
2020 let slot = chunk
2021 .inline_cache_slot(0)
2022 .expect("GetProperty registers a slot");
2023 chunk.set_inline_cache_entry(
2024 slot,
2025 InlineCacheEntry::Property {
2026 name_idx: 3,
2027 target: PropertyCacheTarget::ListCount,
2028 },
2029 );
2030 let (name_idx, target) = chunk
2031 .peek_property_cache(slot)
2032 .expect("warmed property slot peek");
2033 assert_eq!(name_idx, 3);
2034 assert_eq!(target, PropertyCacheTarget::ListCount);
2035 }
2036
2037 #[test]
2038 fn peek_property_cache_returns_none_for_non_property_variants() {
2039 let mut chunk = Chunk::new();
2040 chunk.emit_u16(Op::GetProperty, 0, 1);
2041 let slot = chunk
2042 .inline_cache_slot(0)
2043 .expect("GetProperty registers a slot");
2044 chunk.set_inline_cache_entry(
2045 slot,
2046 InlineCacheEntry::Method {
2047 name_idx: 0,
2048 argc: 0,
2049 target: MethodCacheTarget::ListCount,
2050 },
2051 );
2052 assert!(chunk.peek_property_cache(slot).is_none());
2053 }
2054
2055 #[test]
2056 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
2057 let chunk = Chunk::new();
2058 assert!(chunk.peek_property_cache(0).is_none());
2059 assert!(chunk.peek_property_cache(usize::MAX).is_none());
2060 }
2061
2062 #[test]
2072 fn add_constant_keeps_signed_zero_and_nan_distinct() {
2073 let mut chunk = Chunk::new();
2074 let pos = chunk.add_constant(Constant::Float(0.0));
2077 let neg = chunk.add_constant(Constant::Float(-0.0));
2078 assert_ne!(pos, neg, "+0.0 and -0.0 must get distinct constant slots");
2079 assert_eq!(pos, chunk.add_constant(Constant::Float(0.0)));
2081 assert_eq!(neg, chunk.add_constant(Constant::Float(-0.0)));
2082 let a = chunk.add_constant(Constant::Float(1.5));
2084 assert_eq!(a, chunk.add_constant(Constant::Float(1.5)));
2085 let nan_a = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)));
2086 let nan_b = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0002)));
2087 assert_ne!(
2088 nan_a, nan_b,
2089 "distinct NaN payloads must get distinct constant slots"
2090 );
2091 assert_eq!(
2092 nan_a,
2093 chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)))
2094 );
2095 let s = chunk.add_constant(Constant::Int(7));
2097 assert_eq!(s, chunk.add_constant(Constant::Int(7)));
2098 }
2099
2100 #[test]
2101 fn add_constant_uses_first_slot_after_many_unique_constants() {
2102 let mut chunk = Chunk::new();
2103 let first = chunk.add_constant(Constant::String("shared".to_string()));
2104 for index in 0..10_000 {
2105 let slot = chunk.add_constant(Constant::String(format!("unique_{index}")));
2106 assert_eq!(slot as usize, index + 1);
2107 }
2108 assert_eq!(
2109 first,
2110 chunk.add_constant(Constant::String("shared".to_string())),
2111 "duplicate lookup must return the original slot after index growth"
2112 );
2113 }
2114
2115 #[test]
2116 fn constant_index_round_trips_through_cached_chunk() {
2117 let mut chunk = Chunk::new();
2118 let shared = chunk.add_constant(Constant::String("shared".to_string()));
2119 for index in 0..128 {
2120 chunk.add_constant(Constant::Int(index));
2121 }
2122
2123 let frozen = chunk.freeze_for_cache();
2124 let mut thawed = Chunk::from_cached(&frozen);
2125 assert_eq!(
2126 shared,
2127 thawed.add_constant(Constant::String("shared".to_string())),
2128 "cache thaw must rebuild the constant side index"
2129 );
2130 let next = thawed.add_constant(Constant::String("new".to_string()));
2131 assert_eq!(next as usize, frozen.constants.len());
2132 }
2133
2134 #[test]
2135 fn peek_direct_call_state_returns_none_for_empty_slot() {
2136 let mut chunk = Chunk::new();
2137 chunk.emit_u8(Op::Call, 0, 1);
2138 let slot = chunk
2139 .inline_cache_slot(0)
2140 .expect("Op::Call registers a slot");
2141 assert!(chunk.peek_direct_call_state(slot).is_none());
2142 }
2143
2144 #[test]
2145 fn peek_direct_call_state_returns_warmup_state() {
2146 let mut chunk = Chunk::new();
2147 chunk.emit_u8(Op::Call, 0, 1);
2148 let slot = chunk
2149 .inline_cache_slot(0)
2150 .expect("Op::Call registers a slot");
2151 let target = synthetic_direct_call_target();
2152 chunk.set_inline_cache_entry(
2153 slot,
2154 InlineCacheEntry::DirectCall {
2155 state: DirectCallState::Warmup {
2156 argc: 2,
2157 target: target.clone(),
2158 hits: 1,
2159 },
2160 },
2161 );
2162 let state = chunk
2163 .peek_direct_call_state(slot)
2164 .expect("warmed direct-call slot peek");
2165 match state {
2166 DirectCallState::Warmup {
2167 argc,
2168 target: peeked_target,
2169 hits,
2170 } => {
2171 assert_eq!(argc, 2);
2172 assert_eq!(hits, 1);
2173 assert_eq!(peeked_target, target);
2174 }
2175 other => panic!("expected Warmup, got {other:?}"),
2176 }
2177 }
2178
2179 #[test]
2180 fn peek_direct_call_state_returns_specialized_state() {
2181 let mut chunk = Chunk::new();
2182 chunk.emit_u8(Op::Call, 0, 1);
2183 let slot = chunk
2184 .inline_cache_slot(0)
2185 .expect("Op::Call registers a slot");
2186 let target = synthetic_direct_call_target();
2187 chunk.set_inline_cache_entry(
2188 slot,
2189 InlineCacheEntry::DirectCall {
2190 state: DirectCallState::Specialized {
2191 argc: 3,
2192 target: target.clone(),
2193 hits: 100,
2194 misses: 0,
2195 },
2196 },
2197 );
2198 let state = chunk
2199 .peek_direct_call_state(slot)
2200 .expect("warmed direct-call slot peek");
2201 match state {
2202 DirectCallState::Specialized {
2203 argc,
2204 target: peeked_target,
2205 hits,
2206 misses,
2207 } => {
2208 assert_eq!(argc, 3);
2209 assert_eq!(hits, 100);
2210 assert_eq!(misses, 0);
2211 assert_eq!(peeked_target, target);
2212 }
2213 other => panic!("expected Specialized, got {other:?}"),
2214 }
2215 }
2216
2217 #[test]
2218 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2219 let mut chunk = Chunk::new();
2220 chunk.emit_u8(Op::Call, 0, 1);
2221 let slot = chunk
2222 .inline_cache_slot(0)
2223 .expect("Op::Call registers a slot");
2224
2225 chunk.set_inline_cache_entry(
2226 slot,
2227 InlineCacheEntry::Property {
2228 name_idx: 0,
2229 target: PropertyCacheTarget::ListCount,
2230 },
2231 );
2232 assert!(chunk.peek_direct_call_state(slot).is_none());
2233 }
2234
2235 #[test]
2236 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2237 let chunk = Chunk::new();
2238 assert!(chunk.peek_direct_call_state(0).is_none());
2239 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2240 }
2241
2242 fn synthetic_direct_call_target() -> DirectCallTarget {
2246 use crate::value::VmClosure;
2247 use crate::{CompiledFunction, VmEnv};
2248 let func = CompiledFunction {
2249 name: "synthetic".to_string(),
2250 type_params: Vec::new(),
2251 nominal_type_names: Vec::new(),
2252 params: Vec::new(),
2253 default_start: None,
2254 chunk: Arc::new(Chunk::new()),
2255 is_generator: false,
2256 is_stream: false,
2257 has_rest_param: false,
2258 has_runtime_type_checks: false,
2259 };
2260 DirectCallTarget::Closure(Arc::new(VmClosure {
2261 func: Arc::new(func),
2262 env: VmEnv::new(),
2263 source_dir: None,
2264 module_functions: None,
2265 module_state: None,
2266 }))
2267 }
2268}