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<crate::value::HarnStr>>>>,
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 | ConcatAssignLocal | 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 | Op::ConcatAssignLocal
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<crate::value::HarnStr> {
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(existing.clone());
1040 }
1041 let materialized = match self.constants.get(idx)? {
1042 Constant::String(s) => crate::value::HarnStr::from(s.as_str()),
1043 _ => return None,
1044 };
1045 entries[idx] = Some(materialized.clone());
1046 Some(materialized)
1047 }
1048
1049 #[inline]
1052 #[cfg(test)]
1053 pub(crate) fn peek_adaptive_binary_cache(
1054 &self,
1055 slot: usize,
1056 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
1057 match self.inline_caches.lock().get(slot)? {
1058 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
1059 _ => None,
1060 }
1061 }
1062
1063 #[inline]
1066 #[cfg(test)]
1067 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1068 match self.inline_caches.lock().get(slot)? {
1069 &InlineCacheEntry::Method {
1070 name_idx,
1071 argc,
1072 target,
1073 } => Some((name_idx, argc, target)),
1074 _ => None,
1075 }
1076 }
1077
1078 #[inline]
1081 #[cfg(test)]
1082 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1083 match self.inline_caches.lock().get(slot)? {
1084 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1085 _ => None,
1086 }
1087 }
1088
1089 #[inline]
1092 #[cfg(test)]
1093 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1094 match self.inline_caches.lock().get(slot)? {
1095 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1096 _ => None,
1097 }
1098 }
1099
1100 #[cfg(test)]
1101 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1102 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1103 *existing = entry;
1104 }
1105 }
1106
1107 pub fn freeze_for_cache(&self) -> CachedChunk {
1108 CachedChunk {
1109 code: self.code.clone(),
1110 constants: self.constants.clone(),
1111 lines: self.lines.clone(),
1112 columns: self.columns.clone(),
1113 source_file: self.source_file.clone(),
1114 current_col: self.current_col,
1115 functions: self
1116 .functions
1117 .iter()
1118 .map(|function| function.freeze_for_cache())
1119 .collect(),
1120 inline_cache_slots: self.inline_cache_slots.clone(),
1121 local_slots: self.local_slots.clone(),
1122 references_outer_names: self.references_outer_names,
1123 }
1124 }
1125
1126 pub fn from_cached(cached: &CachedChunk) -> Self {
1127 let inline_cache_count = cached.inline_cache_slots.len();
1128 let constants_count = cached.constants.len();
1129 let mut inline_cache_index = Vec::new();
1136 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1137 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1138 if op_offset < inline_cache_index.len() {
1139 inline_cache_index[op_offset] = slot as u32;
1140 }
1141 }
1142 Self {
1143 cache_id: next_chunk_cache_id(),
1144 code: cached.code.clone(),
1145 constants: cached.constants.clone(),
1146 constant_index: build_constant_index(&cached.constants),
1147 lines: cached.lines.clone(),
1148 columns: cached.columns.clone(),
1149 source_file: cached.source_file.clone(),
1150 current_col: cached.current_col,
1151 functions: cached
1152 .functions
1153 .iter()
1154 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1155 .collect(),
1156 inline_cache_slots: cached.inline_cache_slots.clone(),
1157 inline_cache_index,
1158 inline_caches: Arc::new(Mutex::new(vec![
1159 InlineCacheEntry::Empty;
1160 inline_cache_count
1161 ])),
1162 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1163 local_slots: cached.local_slots.clone(),
1164 references_outer_names: cached.references_outer_names,
1165 #[cfg(debug_assertions)]
1166 balance_depth: 0,
1167 #[cfg(debug_assertions)]
1168 balance_nonlinear: 0,
1169 }
1170 }
1171
1172 pub(crate) fn add_local_slot(
1173 &mut self,
1174 name: String,
1175 mutable: bool,
1176 scope_depth: usize,
1177 ) -> u16 {
1178 let idx = self.local_slots.len();
1179 self.local_slots.push(LocalSlotInfo {
1180 name,
1181 mutable,
1182 scope_depth,
1183 });
1184 idx as u16
1185 }
1186
1187 pub fn read_u64(&self, pos: usize) -> u64 {
1189 u64::from_be_bytes([
1190 self.code[pos],
1191 self.code[pos + 1],
1192 self.code[pos + 2],
1193 self.code[pos + 3],
1194 self.code[pos + 4],
1195 self.code[pos + 5],
1196 self.code[pos + 6],
1197 self.code[pos + 7],
1198 ])
1199 }
1200
1201 pub fn disassemble(&self, name: &str) -> String {
1205 let mut out = format!("== {name} ==\n");
1206 let mut ip = 0;
1207 while ip < self.code.len() {
1208 let op_byte = self.code[ip];
1209 let line = self.lines.get(ip).copied().unwrap_or(0);
1210 out.push_str(&format!("{ip:04} [{line:>4}] "));
1211 ip += 1;
1212
1213 if let Some(op) = Op::from_byte(op_byte) {
1214 self.disassemble_op(op, &mut ip, &mut out);
1215 } else {
1216 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1217 }
1218 }
1219 out
1220 }
1221}
1222
1223pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1234 label.to_string()
1235}
1236
1237pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1238 let arg = chunk.code[*ip];
1239 *ip += 1;
1240 format!("{label} {arg:>4}")
1241}
1242
1243pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1244 let arg = chunk.read_u16(*ip);
1245 *ip += 2;
1246 format!("{label} {arg:>4}")
1247}
1248
1249pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1250 let catch_offset = chunk.read_u16(*ip);
1251 *ip += 2;
1252 let type_idx = chunk.read_u16(*ip);
1253 *ip += 2;
1254 if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1255 format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1256 } else {
1257 format!("{label} {catch_offset:>4} type {type_idx:>4}")
1258 }
1259}
1260
1261pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1262 let idx = chunk.read_u16(*ip);
1263 *ip += 2;
1264 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1265}
1266
1267pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1268 let slot = chunk.read_u16(*ip);
1269 *ip += 2;
1270 let mut out = format!("{label} {slot:>4}");
1271 if let Some(info) = chunk.local_slots.get(slot as usize) {
1272 out.push_str(&format!(" ({})", info.name));
1273 }
1274 out
1275}
1276
1277pub(crate) fn disasm_const_pool_local_slot(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1278 let prop = chunk.read_u16(*ip);
1279 *ip += 2;
1280 let slot = chunk.read_u16(*ip);
1281 *ip += 2;
1282 let mut out = format!(
1283 "{label} prop {prop:>4} ({}) slot {slot:>4}",
1284 chunk.constants[prop as usize]
1285 );
1286 if let Some(info) = chunk.local_slots.get(slot as usize) {
1287 out.push_str(&format!(" ({})", info.name));
1288 }
1289 out
1290}
1291
1292pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1293 let idx = chunk.read_u16(*ip);
1294 *ip += 2;
1295 let argc = chunk.code[*ip];
1296 *ip += 1;
1297 format!(
1298 "{label} {idx:>4} ({}) argc={argc}",
1299 chunk.constants[idx as usize]
1300 )
1301}
1302
1303pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1304 let enum_idx = chunk.read_u16(*ip);
1305 *ip += 2;
1306 let var_idx = chunk.read_u16(*ip);
1307 *ip += 2;
1308 format!(
1309 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1310 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1311 )
1312}
1313
1314pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1315 let enum_idx = chunk.read_u16(*ip);
1316 *ip += 2;
1317 let var_idx = chunk.read_u16(*ip);
1318 *ip += 2;
1319 let field_count = chunk.read_u16(*ip);
1320 *ip += 2;
1321 format!(
1322 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1323 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1324 )
1325}
1326
1327pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1328 let path_idx = chunk.read_u16(*ip);
1329 *ip += 2;
1330 let names_idx = chunk.read_u16(*ip);
1331 *ip += 2;
1332 format!(
1333 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1334 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1335 )
1336}
1337
1338pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1339 let var_idx = chunk.read_u16(*ip);
1340 *ip += 2;
1341 let type_idx = chunk.read_u16(*ip);
1342 *ip += 2;
1343 format!(
1344 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1345 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1346 )
1347}
1348
1349pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1350 let id = chunk.read_u64(*ip);
1351 *ip += 8;
1352 let idx = chunk.read_u16(*ip);
1353 *ip += 2;
1354 let argc = chunk.code[*ip];
1355 *ip += 1;
1356 format!(
1357 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1358 chunk.constants[idx as usize],
1359 )
1360}
1361
1362pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1363 let id = chunk.read_u64(*ip);
1364 *ip += 8;
1365 let idx = chunk.read_u16(*ip);
1366 *ip += 2;
1367 format!(
1368 "{label} {id:#018x} {idx:>4} ({})",
1369 chunk.constants[idx as usize],
1370 )
1371}
1372
1373pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1374 let idx = chunk.read_u16(*ip);
1380 *ip += 2;
1381 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1382}
1383
1384impl Default for Chunk {
1385 fn default() -> Self {
1386 Self::new()
1387 }
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392 use std::sync::Arc;
1393
1394 use super::{
1395 Chunk, Constant, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget,
1396 Op, PropertyCacheTarget,
1397 };
1398 use crate::BuiltinId;
1399
1400 #[test]
1401 fn op_from_byte_matches_repr_order() {
1402 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1403 assert_eq!(byte as u8, op as u8);
1404 assert_eq!(Op::from_byte(byte as u8), Some(op));
1405 }
1406 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1407 assert_eq!(Op::COUNT, Op::ALL.len());
1408 }
1409
1410 #[test]
1411 fn disassemble_covers_every_opcode_variant() {
1412 for op in Op::ALL.iter().copied() {
1422 let mut chunk = Chunk::new();
1423 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1424 for _ in 0..16 {
1428 chunk.code.push(0);
1429 }
1430 let mut ip: usize = 0;
1431 let mut out = String::new();
1432 chunk.disassemble_op(op, &mut ip, &mut out);
1433 assert!(
1434 !out.contains("UNKNOWN"),
1435 "disasm emitted UNKNOWN for {op:?}: {out}",
1436 );
1437 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1438 }
1439 }
1440
1441 #[test]
1450 fn empty_chunk_does_not_reference_outer_names() {
1451 let chunk = Chunk::new();
1452 assert!(!chunk.references_outer_names);
1453 }
1454
1455 #[test]
1456 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1457 let mut chunk = Chunk::new();
1462 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1463 chunk.emit_u16(Op::Constant, 0, 1);
1464 chunk.emit(Op::MulInt, 1);
1465 chunk.emit(Op::Pop, 1);
1466 chunk.emit(Op::Return, 1);
1467 assert!(!chunk.references_outer_names);
1468 }
1469
1470 #[test]
1471 fn slot_only_chunk_does_not_reference_outer_names() {
1472 let mut chunk = Chunk::new();
1474 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1475 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1476 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1477 assert!(!chunk.references_outer_names);
1478 }
1479
1480 #[test]
1481 fn get_var_flags_outer_name_reference() {
1482 let mut chunk = Chunk::new();
1483 chunk.emit_u16(Op::GetVar, 0, 1);
1484 assert!(chunk.references_outer_names);
1485 }
1486
1487 #[test]
1488 fn set_var_flags_outer_name_reference() {
1489 let mut chunk = Chunk::new();
1490 chunk.emit_u16(Op::SetVar, 0, 1);
1491 assert!(chunk.references_outer_names);
1492 }
1493
1494 #[test]
1495 fn check_type_flags_outer_name_reference() {
1496 let mut chunk = Chunk::new();
1497 chunk.emit_u16(Op::CheckType, 0, 1);
1498 assert!(chunk.references_outer_names);
1499 }
1500
1501 #[test]
1502 fn call_builtin_flags_outer_name_reference() {
1503 let mut chunk = Chunk::new();
1504 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1505 assert!(chunk.references_outer_names);
1506 }
1507
1508 #[test]
1509 fn call_builtin_spread_flags_outer_name_reference() {
1510 let mut chunk = Chunk::new();
1511 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1512 assert!(chunk.references_outer_names);
1513 }
1514
1515 #[test]
1516 fn tail_call_flags_outer_name_reference() {
1517 let mut chunk = Chunk::new();
1520 chunk.emit_u8(Op::TailCall, 1, 1);
1521 assert!(chunk.references_outer_names);
1522 }
1523
1524 #[test]
1525 fn call_flags_outer_name_reference() {
1526 let mut chunk = Chunk::new();
1529 chunk.emit_u8(Op::Call, 1, 1);
1530 assert!(chunk.references_outer_names);
1531 }
1532
1533 #[test]
1534 fn pipe_flags_outer_name_reference() {
1535 let mut chunk = Chunk::new();
1538 chunk.emit(Op::Pipe, 1);
1539 assert!(chunk.references_outer_names);
1540 }
1541
1542 #[test]
1543 fn method_call_does_not_flag_outer_name_reference() {
1544 let mut chunk = Chunk::new();
1547 chunk.emit_method_call(0, 1, 1);
1548 chunk.emit_method_call_opt(0, 1, 1);
1549 assert!(!chunk.references_outer_names);
1550 }
1551
1552 #[test]
1553 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1554 let mut chunk = Chunk::new();
1557 chunk.emit_u16(Op::Constant, 0, 1);
1558 chunk.emit(Op::JumpIfFalse, 1);
1559 chunk.emit(Op::Jump, 1);
1560 chunk.emit(Op::Return, 1);
1561 chunk.emit(Op::Pop, 1);
1562 assert!(!chunk.references_outer_names);
1563 }
1564
1565 #[test]
1566 fn references_outer_names_is_monotonic() {
1567 let mut chunk = Chunk::new();
1570 chunk.emit_u16(Op::GetVar, 0, 1);
1571 assert!(chunk.references_outer_names);
1572 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1573 chunk.emit(Op::MulInt, 1);
1574 assert!(chunk.references_outer_names);
1575 }
1576
1577 #[test]
1578 fn freeze_thaw_round_trips_references_outer_names() {
1579 let mut chunk = Chunk::new();
1583 chunk.emit_u16(Op::GetVar, 0, 1);
1584 assert!(chunk.references_outer_names);
1585 let frozen = chunk.freeze_for_cache();
1586 let thawed = Chunk::from_cached(&frozen);
1587 assert!(thawed.references_outer_names);
1588
1589 let plain = Chunk::new();
1590 assert!(!plain.references_outer_names);
1591 let frozen_plain = plain.freeze_for_cache();
1592 let thawed_plain = Chunk::from_cached(&frozen_plain);
1593 assert!(!thawed_plain.references_outer_names);
1594 }
1595
1596 #[test]
1608 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1609 let mut chunk = Chunk::new();
1612 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1613 chunk.emit(Op::Pop, 1);
1614 chunk.emit(Op::Return, 1);
1615 assert!(chunk.inline_cache_slot(0).is_none());
1616 assert!(chunk.inline_cache_slot(3).is_none());
1617 assert!(chunk.inline_cache_slot(4).is_none());
1618 }
1619
1620 #[test]
1621 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1622 let mut chunk = Chunk::new();
1626 chunk.emit(Op::Add, 1);
1627 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1628 }
1629
1630 #[test]
1631 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1632 let mut chunk = Chunk::new();
1637 chunk.emit(Op::Add, 1);
1638 chunk.emit(Op::Sub, 1);
1639 chunk.emit(Op::Mul, 1);
1640 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1641 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1642 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1643 assert_ne!(s0, s1);
1644 assert_ne!(s1, s2);
1645 assert_ne!(s0, s2);
1646 }
1647
1648 #[test]
1649 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1650 let mut chunk = Chunk::new();
1653 chunk.emit(Op::Add, 1);
1654 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1655 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1656 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1657 }
1658
1659 #[test]
1660 fn inline_cache_slot_for_get_property_and_method_call() {
1661 let mut chunk = Chunk::new();
1665 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");
1670 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1671 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1672 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1673 }
1674
1675 #[test]
1676 fn inline_cache_slot_for_call_and_call_builtin() {
1677 let mut chunk = Chunk::new();
1682 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1684 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1685 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1686 assert!(
1687 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1688 "Op::CallBuiltin IC slot"
1689 );
1690 }
1691
1692 #[test]
1693 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1694 let mut chunk = Chunk::new();
1700 chunk.emit(Op::Add, 1);
1701 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1702 chunk.register_inline_cache(0);
1704 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1705 assert_eq!(slot_before, slot_after);
1706 }
1707
1708 #[test]
1709 fn inline_cache_index_round_trips_through_cached_chunk() {
1710 let mut chunk = Chunk::new();
1717 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1718 chunk.emit_u16(Op::Constant, 0, 1);
1719 chunk.emit(Op::Add, 1);
1720 chunk.emit(Op::Sub, 1);
1721 chunk.emit_method_call(0, 1, 1);
1722 chunk.emit_u8(Op::Call, 1, 1);
1723 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1724 .map(|o| (o, chunk.inline_cache_slot(o)))
1725 .collect();
1726 let frozen = chunk.freeze_for_cache();
1727 let thawed = Chunk::from_cached(&frozen);
1728 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1729 .map(|o| (o, thawed.inline_cache_slot(o)))
1730 .collect();
1731 assert_eq!(live_slots, thawed_slots);
1732 }
1733
1734 #[test]
1735 fn inline_cache_index_agrees_with_btreemap_view() {
1736 let mut chunk = Chunk::new();
1742 chunk.emit(Op::Add, 1);
1743 chunk.emit_u16(Op::GetVar, 0, 1);
1744 chunk.emit(Op::LessInt, 1);
1745 chunk.emit_u8(Op::Call, 2, 1);
1746 chunk.emit(Op::Equal, 1);
1747 chunk.emit_u16(Op::GetProperty, 0, 1);
1748 chunk.emit_method_call_opt(0, 0, 1);
1749 for offset in 0..chunk.code.len() {
1750 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1751 let from_index = chunk.inline_cache_slot(offset);
1752 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1753 }
1754 }
1755
1756 #[test]
1767 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1768 let mut chunk = Chunk::new();
1769 chunk.emit(Op::Add, 1);
1770 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1771 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1773 }
1774
1775 #[test]
1776 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1777 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1778 let mut chunk = Chunk::new();
1779 chunk.emit(Op::Add, 1);
1780 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1781 chunk.set_inline_cache_entry(
1782 slot,
1783 InlineCacheEntry::AdaptiveBinary {
1784 op: AdaptiveBinaryOp::Add,
1785 state: AdaptiveBinaryState::Warmup {
1786 shape: BinaryShape::Int,
1787 hits: 2,
1788 },
1789 },
1790 );
1791 let (op, state) = chunk
1792 .peek_adaptive_binary_cache(slot)
1793 .expect("warmed slot peek");
1794 assert_eq!(op, AdaptiveBinaryOp::Add);
1795 assert!(matches!(
1796 state,
1797 AdaptiveBinaryState::Warmup {
1798 shape: BinaryShape::Int,
1799 hits: 2
1800 }
1801 ));
1802 }
1803
1804 #[test]
1805 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1806 use super::{InlineCacheEntry, PropertyCacheTarget};
1812 let mut chunk = Chunk::new();
1813 chunk.emit(Op::Add, 1);
1814 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1815 chunk.set_inline_cache_entry(
1816 slot,
1817 InlineCacheEntry::Property {
1818 name_idx: 0,
1819 target: PropertyCacheTarget::ListCount,
1820 },
1821 );
1822 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1823 }
1824
1825 #[test]
1826 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1827 let chunk = Chunk::new();
1832 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1833 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1834 }
1835
1836 #[test]
1837 fn peek_adaptive_binary_state_is_copy() {
1838 fn assert_copy<T: Copy>() {}
1844 assert_copy::<super::AdaptiveBinaryState>();
1845 assert_copy::<super::AdaptiveBinaryOp>();
1846 assert_copy::<super::BinaryShape>();
1847 }
1848
1849 #[test]
1860 fn peek_method_cache_returns_none_for_empty_slot() {
1861 let mut chunk = Chunk::new();
1862 chunk.emit_method_call(0, 0, 1);
1863 let slot = chunk
1864 .inline_cache_slot(0)
1865 .expect("MethodCall registers a slot");
1866 assert!(chunk.peek_method_cache(slot).is_none());
1867 }
1868
1869 #[test]
1870 fn peek_method_cache_returns_triple_after_warmup() {
1871 let mut chunk = Chunk::new();
1872 chunk.emit_method_call(7, 2, 1);
1873 let slot = chunk
1874 .inline_cache_slot(0)
1875 .expect("MethodCall registers a slot");
1876 chunk.set_inline_cache_entry(
1877 slot,
1878 InlineCacheEntry::Method {
1879 name_idx: 7,
1880 argc: 2,
1881 target: MethodCacheTarget::ListContains,
1882 },
1883 );
1884 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1885 assert_eq!(name_idx, 7);
1886 assert_eq!(argc, 2);
1887 assert_eq!(target, MethodCacheTarget::ListContains);
1888 }
1889
1890 #[test]
1891 fn peek_method_cache_returns_none_for_non_method_variants() {
1892 let mut chunk = Chunk::new();
1896 chunk.emit_method_call(0, 0, 1);
1897 let slot = chunk
1898 .inline_cache_slot(0)
1899 .expect("MethodCall registers a slot");
1900
1901 chunk.set_inline_cache_entry(
1902 slot,
1903 InlineCacheEntry::Property {
1904 name_idx: 0,
1905 target: PropertyCacheTarget::ListCount,
1906 },
1907 );
1908 assert!(chunk.peek_method_cache(slot).is_none());
1909 }
1910
1911 #[test]
1912 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1913 let chunk = Chunk::new();
1914 assert!(chunk.peek_method_cache(0).is_none());
1915 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1916 }
1917
1918 #[test]
1919 fn peek_method_cache_target_is_copy() {
1920 fn assert_copy<T: Copy>() {}
1926 assert_copy::<super::MethodCacheTarget>();
1927 }
1928
1929 #[test]
1939 fn peek_property_cache_returns_none_for_empty_slot() {
1940 let mut chunk = Chunk::new();
1941 chunk.emit_u16(Op::GetProperty, 0, 1);
1942 let slot = chunk
1943 .inline_cache_slot(0)
1944 .expect("GetProperty registers a slot");
1945 assert!(chunk.peek_property_cache(slot).is_none());
1946 }
1947
1948 #[test]
1949 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1950 let mut chunk = Chunk::new();
1951 chunk.emit_u16(Op::GetProperty, 0, 1);
1952 let slot = chunk
1953 .inline_cache_slot(0)
1954 .expect("GetProperty registers a slot");
1955 chunk.set_inline_cache_entry(
1956 slot,
1957 InlineCacheEntry::Property {
1958 name_idx: 11,
1959 target: PropertyCacheTarget::DictField(Arc::from("count")),
1960 },
1961 );
1962 let (name_idx, target) = chunk
1963 .peek_property_cache(slot)
1964 .expect("warmed property slot peek");
1965 assert_eq!(name_idx, 11);
1966 match target {
1967 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1968 other => panic!("expected DictField, got {other:?}"),
1969 }
1970 }
1971
1972 #[test]
1973 fn peek_property_cache_returns_pair_for_unit_target() {
1974 let mut chunk = Chunk::new();
1978 chunk.emit_u16(Op::GetProperty, 0, 1);
1979 let slot = chunk
1980 .inline_cache_slot(0)
1981 .expect("GetProperty registers a slot");
1982 chunk.set_inline_cache_entry(
1983 slot,
1984 InlineCacheEntry::Property {
1985 name_idx: 3,
1986 target: PropertyCacheTarget::ListCount,
1987 },
1988 );
1989 let (name_idx, target) = chunk
1990 .peek_property_cache(slot)
1991 .expect("warmed property slot peek");
1992 assert_eq!(name_idx, 3);
1993 assert_eq!(target, PropertyCacheTarget::ListCount);
1994 }
1995
1996 #[test]
1997 fn peek_property_cache_returns_none_for_non_property_variants() {
1998 let mut chunk = Chunk::new();
1999 chunk.emit_u16(Op::GetProperty, 0, 1);
2000 let slot = chunk
2001 .inline_cache_slot(0)
2002 .expect("GetProperty registers a slot");
2003 chunk.set_inline_cache_entry(
2004 slot,
2005 InlineCacheEntry::Method {
2006 name_idx: 0,
2007 argc: 0,
2008 target: MethodCacheTarget::ListCount,
2009 },
2010 );
2011 assert!(chunk.peek_property_cache(slot).is_none());
2012 }
2013
2014 #[test]
2015 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
2016 let chunk = Chunk::new();
2017 assert!(chunk.peek_property_cache(0).is_none());
2018 assert!(chunk.peek_property_cache(usize::MAX).is_none());
2019 }
2020
2021 #[test]
2031 fn add_constant_keeps_signed_zero_and_nan_distinct() {
2032 let mut chunk = Chunk::new();
2033 let pos = chunk.add_constant(Constant::Float(0.0));
2036 let neg = chunk.add_constant(Constant::Float(-0.0));
2037 assert_ne!(pos, neg, "+0.0 and -0.0 must get distinct constant slots");
2038 assert_eq!(pos, chunk.add_constant(Constant::Float(0.0)));
2040 assert_eq!(neg, chunk.add_constant(Constant::Float(-0.0)));
2041 let a = chunk.add_constant(Constant::Float(1.5));
2043 assert_eq!(a, chunk.add_constant(Constant::Float(1.5)));
2044 let nan_a = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)));
2045 let nan_b = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0002)));
2046 assert_ne!(
2047 nan_a, nan_b,
2048 "distinct NaN payloads must get distinct constant slots"
2049 );
2050 assert_eq!(
2051 nan_a,
2052 chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)))
2053 );
2054 let s = chunk.add_constant(Constant::Int(7));
2056 assert_eq!(s, chunk.add_constant(Constant::Int(7)));
2057 }
2058
2059 #[test]
2060 fn add_constant_uses_first_slot_after_many_unique_constants() {
2061 let mut chunk = Chunk::new();
2062 let first = chunk.add_constant(Constant::String("shared".to_string()));
2063 for index in 0..10_000 {
2064 let slot = chunk.add_constant(Constant::String(format!("unique_{index}")));
2065 assert_eq!(slot as usize, index + 1);
2066 }
2067 assert_eq!(
2068 first,
2069 chunk.add_constant(Constant::String("shared".to_string())),
2070 "duplicate lookup must return the original slot after index growth"
2071 );
2072 }
2073
2074 #[test]
2075 fn constant_index_round_trips_through_cached_chunk() {
2076 let mut chunk = Chunk::new();
2077 let shared = chunk.add_constant(Constant::String("shared".to_string()));
2078 for index in 0..128 {
2079 chunk.add_constant(Constant::Int(index));
2080 }
2081
2082 let frozen = chunk.freeze_for_cache();
2083 let mut thawed = Chunk::from_cached(&frozen);
2084 assert_eq!(
2085 shared,
2086 thawed.add_constant(Constant::String("shared".to_string())),
2087 "cache thaw must rebuild the constant side index"
2088 );
2089 let next = thawed.add_constant(Constant::String("new".to_string()));
2090 assert_eq!(next as usize, frozen.constants.len());
2091 }
2092
2093 #[test]
2094 fn peek_direct_call_state_returns_none_for_empty_slot() {
2095 let mut chunk = Chunk::new();
2096 chunk.emit_u8(Op::Call, 0, 1);
2097 let slot = chunk
2098 .inline_cache_slot(0)
2099 .expect("Op::Call registers a slot");
2100 assert!(chunk.peek_direct_call_state(slot).is_none());
2101 }
2102
2103 #[test]
2104 fn peek_direct_call_state_returns_warmup_state() {
2105 let mut chunk = Chunk::new();
2106 chunk.emit_u8(Op::Call, 0, 1);
2107 let slot = chunk
2108 .inline_cache_slot(0)
2109 .expect("Op::Call registers a slot");
2110 let target = synthetic_direct_call_target();
2111 chunk.set_inline_cache_entry(
2112 slot,
2113 InlineCacheEntry::DirectCall {
2114 state: DirectCallState::Warmup {
2115 argc: 2,
2116 target: target.clone(),
2117 hits: 1,
2118 },
2119 },
2120 );
2121 let state = chunk
2122 .peek_direct_call_state(slot)
2123 .expect("warmed direct-call slot peek");
2124 match state {
2125 DirectCallState::Warmup {
2126 argc,
2127 target: peeked_target,
2128 hits,
2129 } => {
2130 assert_eq!(argc, 2);
2131 assert_eq!(hits, 1);
2132 assert_eq!(peeked_target, target);
2133 }
2134 other => panic!("expected Warmup, got {other:?}"),
2135 }
2136 }
2137
2138 #[test]
2139 fn peek_direct_call_state_returns_specialized_state() {
2140 let mut chunk = Chunk::new();
2141 chunk.emit_u8(Op::Call, 0, 1);
2142 let slot = chunk
2143 .inline_cache_slot(0)
2144 .expect("Op::Call registers a slot");
2145 let target = synthetic_direct_call_target();
2146 chunk.set_inline_cache_entry(
2147 slot,
2148 InlineCacheEntry::DirectCall {
2149 state: DirectCallState::Specialized {
2150 argc: 3,
2151 target: target.clone(),
2152 hits: 100,
2153 misses: 0,
2154 },
2155 },
2156 );
2157 let state = chunk
2158 .peek_direct_call_state(slot)
2159 .expect("warmed direct-call slot peek");
2160 match state {
2161 DirectCallState::Specialized {
2162 argc,
2163 target: peeked_target,
2164 hits,
2165 misses,
2166 } => {
2167 assert_eq!(argc, 3);
2168 assert_eq!(hits, 100);
2169 assert_eq!(misses, 0);
2170 assert_eq!(peeked_target, target);
2171 }
2172 other => panic!("expected Specialized, got {other:?}"),
2173 }
2174 }
2175
2176 #[test]
2177 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2178 let mut chunk = Chunk::new();
2179 chunk.emit_u8(Op::Call, 0, 1);
2180 let slot = chunk
2181 .inline_cache_slot(0)
2182 .expect("Op::Call registers a slot");
2183
2184 chunk.set_inline_cache_entry(
2185 slot,
2186 InlineCacheEntry::Property {
2187 name_idx: 0,
2188 target: PropertyCacheTarget::ListCount,
2189 },
2190 );
2191 assert!(chunk.peek_direct_call_state(slot).is_none());
2192 }
2193
2194 #[test]
2195 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2196 let chunk = Chunk::new();
2197 assert!(chunk.peek_direct_call_state(0).is_none());
2198 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2199 }
2200
2201 fn synthetic_direct_call_target() -> DirectCallTarget {
2205 use crate::value::VmClosure;
2206 use crate::{CompiledFunction, VmEnv};
2207 let func = CompiledFunction {
2208 name: "synthetic".to_string(),
2209 type_params: Vec::new(),
2210 nominal_type_names: Vec::new(),
2211 params: Vec::new(),
2212 default_start: None,
2213 chunk: Arc::new(Chunk::new()),
2214 is_generator: false,
2215 is_stream: false,
2216 has_rest_param: false,
2217 has_runtime_type_checks: false,
2218 };
2219 DirectCallTarget::Closure(Arc::new(VmClosure {
2220 func: Arc::new(func),
2221 env: VmEnv::new(),
2222 source_dir: None,
2223 module_functions: None,
2224 module_state: None,
2225 }))
2226 }
2227}