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(crate) fn minimum_arg_count(&self) -> usize {
576 if self.has_rest_param {
577 self.required_param_count()
578 .min(self.params.len().saturating_sub(1))
579 } else {
580 self.required_param_count()
581 }
582 }
583
584 pub(crate) fn callee_arg_count(&self, supplied: usize) -> usize {
586 if self.has_rest_param {
587 supplied
588 } else {
589 supplied.min(self.params.len())
590 }
591 }
592
593 pub fn declares_type_param(&self, name: &str) -> bool {
594 self.type_params.iter().any(|param| param == name)
595 }
596
597 pub fn has_nominal_type(&self, name: &str) -> bool {
598 self.nominal_type_names.iter().any(|ty| ty == name)
599 }
600
601 pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
602 CachedCompiledFunction {
603 name: self.name.clone(),
604 type_params: self.type_params.clone(),
605 nominal_type_names: self.nominal_type_names.clone(),
606 params: self
607 .params
608 .iter()
609 .map(ParamSlot::freeze_for_cache)
610 .collect(),
611 default_start: self.default_start,
612 chunk: self.chunk.freeze_for_cache(),
613 is_generator: self.is_generator,
614 is_stream: self.is_stream,
615 has_rest_param: self.has_rest_param,
616 has_runtime_type_checks: self.has_runtime_type_checks,
617 }
618 }
619
620 pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
621 Self {
622 name: cached.name.clone(),
623 type_params: cached.type_params.clone(),
624 nominal_type_names: cached.nominal_type_names.clone(),
625 params: cached.params.iter().map(CachedParamSlot::thaw).collect(),
626 default_start: cached.default_start,
627 chunk: Arc::new(Chunk::from_cached(&cached.chunk)),
628 is_generator: cached.is_generator,
629 is_stream: cached.is_stream,
630 has_rest_param: cached.has_rest_param,
631 has_runtime_type_checks: cached.has_runtime_type_checks,
632 }
633 }
634}
635
636#[cfg(debug_assertions)]
639#[derive(Clone, Copy)]
640pub(crate) struct BalanceProbe {
641 depth: i32,
642 nonlinear: u32,
643}
644
645#[cfg(debug_assertions)]
662fn op_stack_delta(op: Op, count: u16) -> Option<i32> {
663 use Op::*;
664 let count = count as i32;
665 Some(match op {
666 Constant | Nil | True | False | GetVar | GetArgc | GetLocalSlot | Closure | Dup => 1,
668 DefLet | DefVar | SetVar | DefLocalSlot | SetLocalSlot | SetProperty
672 | SetLocalSlotProperty | ConcatAssignLocal | Pop => -1,
673 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
677 | PushScope | PopScope | PopIterator | PopHandler => 0,
678 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
680 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
681 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
682 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
683 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
684 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
685 IterInit => -1,
688 Slice | SetSubscript | SetLocalSlotSubscript => -2,
691 BuildList | Concat | CallBuiltin => 1 - count,
693 BuildDict => 1 - 2 * count,
694 Call | MethodCall | MethodCallOpt => -count,
696 Jump | JumpIfFalse | JumpIfTrue | IterNext | Return | TailCall | Throw | TryCatchSetup
699 | Spawn | Pipe | Parallel | ParallelMap | ParallelMapStream | ParallelSettle
700 | SyncMutexEnter | SyncMutexEnterKeyed | TaskScopeEnter | TaskScopeExit | Import
701 | SelectiveImport | DeadlineSetup | DeadlineEnd | BuildEnum | MatchEnum | Yield
702 | CallSpread | CallBuiltinSpread | MethodCallSpread => return None,
703 })
704}
705
706impl Chunk {
707 pub fn new() -> Self {
708 Self {
709 cache_id: next_chunk_cache_id(),
710 code: Vec::new(),
711 constants: Vec::new(),
712 constant_index: HashMap::new(),
713 lines: Vec::new(),
714 columns: Vec::new(),
715 source_file: None,
716 current_col: 0,
717 functions: Vec::new(),
718 inline_cache_slots: BTreeMap::new(),
719 inline_cache_index: Vec::new(),
720 inline_caches: Arc::new(Mutex::new(Vec::new())),
721 constant_strings: Arc::new(Mutex::new(Vec::new())),
722 local_slots: Vec::new(),
723 references_outer_names: false,
724 #[cfg(debug_assertions)]
725 balance_depth: 0,
726 #[cfg(debug_assertions)]
727 balance_nonlinear: 0,
728 }
729 }
730
731 pub fn set_column(&mut self, col: u32) {
733 self.current_col = col;
734 }
735
736 pub fn add_constant(&mut self, constant: Constant) -> u16 {
738 debug_assert!(
739 self.constant_index.len() <= self.constants.len(),
740 "constant side index cannot outgrow the constant pool"
741 );
742 let key = ConstantKey::from(&constant);
743 if let Some(index) = self.constant_index.get(&key) {
744 debug_assert!(
745 self.constants
746 .get(*index as usize)
747 .is_some_and(|existing| constants_identical(existing, &constant)),
748 "constant side index drifted from the constant pool"
749 );
750 return *index;
751 }
752 let idx = self.constants.len();
753 let idx = u16::try_from(idx).expect("constant pool exceeded u16 operand space");
754 self.constants.push(constant);
755 self.constant_index.insert(key, idx);
756 idx
757 }
758
759 pub fn emit(&mut self, op: Op, line: u32) {
761 #[cfg(debug_assertions)]
762 self.note_balance(op, 0);
763 let col = self.current_col;
764 let op_offset = self.code.len();
765 self.code.push(op as u8);
766 self.lines.push(line);
767 self.columns.push(col);
768 if is_adaptive_binary_op(op) {
769 self.register_inline_cache(op_offset);
770 }
771 if op_reads_outer_name(op) {
772 self.references_outer_names = true;
773 }
774 }
775
776 pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
778 #[cfg(debug_assertions)]
779 self.note_balance(op, arg);
780 let col = self.current_col;
781 let op_offset = self.code.len();
782 self.code.push(op as u8);
783 self.code.push((arg >> 8) as u8);
784 self.code.push((arg & 0xFF) as u8);
785 self.lines.push(line);
786 self.lines.push(line);
787 self.lines.push(line);
788 self.columns.push(col);
789 self.columns.push(col);
790 self.columns.push(col);
791 if matches!(
792 op,
793 Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread | Op::ConcatAssignLocal
794 ) {
795 self.register_inline_cache(op_offset);
796 }
797 if op_reads_outer_name(op) {
798 self.references_outer_names = true;
799 }
800 }
801
802 pub fn emit_set_local_slot_property(&mut self, prop_idx: u16, slot: u16, line: u32) {
805 #[cfg(debug_assertions)]
806 self.note_balance(Op::SetLocalSlotProperty, 0);
807 let col = self.current_col;
808 self.code.push(Op::SetLocalSlotProperty as u8);
809 self.code.push((prop_idx >> 8) as u8);
810 self.code.push((prop_idx & 0xFF) as u8);
811 self.code.push((slot >> 8) as u8);
812 self.code.push((slot & 0xFF) as u8);
813 for _ in 0..5 {
814 self.lines.push(line);
815 self.columns.push(col);
816 }
817 }
818
819 pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
821 #[cfg(debug_assertions)]
822 self.note_balance(op, arg as u16);
823 let col = self.current_col;
824 let op_offset = self.code.len();
825 self.code.push(op as u8);
826 self.code.push(arg);
827 self.lines.push(line);
828 self.lines.push(line);
829 self.columns.push(col);
830 self.columns.push(col);
831 if matches!(op, Op::Call) {
832 self.register_inline_cache(op_offset);
833 }
834 if op_reads_outer_name(op) {
835 self.references_outer_names = true;
836 }
837 }
838
839 pub fn emit_call_builtin(
841 &mut self,
842 id: crate::BuiltinId,
843 name_idx: u16,
844 arg_count: u8,
845 line: u32,
846 ) {
847 #[cfg(debug_assertions)]
848 self.note_balance(Op::CallBuiltin, arg_count as u16);
849 let col = self.current_col;
850 let op_offset = self.code.len();
851 self.code.push(Op::CallBuiltin as u8);
852 self.code.extend_from_slice(&id.raw().to_be_bytes());
853 self.code.push((name_idx >> 8) as u8);
854 self.code.push((name_idx & 0xFF) as u8);
855 self.code.push(arg_count);
856 for _ in 0..12 {
857 self.lines.push(line);
858 self.columns.push(col);
859 }
860 self.register_inline_cache(op_offset);
861 self.references_outer_names = true;
862 }
863
864 pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
866 #[cfg(debug_assertions)]
867 self.note_balance(Op::CallBuiltinSpread, 0);
868 let col = self.current_col;
869 self.code.push(Op::CallBuiltinSpread as u8);
870 self.code.extend_from_slice(&id.raw().to_be_bytes());
871 self.code.push((name_idx >> 8) as u8);
872 self.code.push((name_idx & 0xFF) as u8);
873 for _ in 0..11 {
874 self.lines.push(line);
875 self.columns.push(col);
876 }
877 self.references_outer_names = true;
878 }
879
880 pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
882 self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
883 }
884
885 pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
887 self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
888 }
889
890 fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
891 #[cfg(debug_assertions)]
892 self.note_balance(op, arg_count as u16);
893 let col = self.current_col;
894 let op_offset = self.code.len();
895 self.code.push(op as u8);
896 self.code.push((name_idx >> 8) as u8);
897 self.code.push((name_idx & 0xFF) as u8);
898 self.code.push(arg_count);
899 self.lines.push(line);
900 self.lines.push(line);
901 self.lines.push(line);
902 self.lines.push(line);
903 self.columns.push(col);
904 self.columns.push(col);
905 self.columns.push(col);
906 self.columns.push(col);
907 self.register_inline_cache(op_offset);
908 }
909
910 pub fn current_offset(&self) -> usize {
912 self.code.len()
913 }
914
915 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
917 #[cfg(debug_assertions)]
918 self.note_balance(op, 0);
919 let col = self.current_col;
920 self.code.push(op as u8);
921 let patch_pos = self.code.len();
922 self.code.push(0xFF);
923 self.code.push(0xFF);
924 self.lines.push(line);
925 self.lines.push(line);
926 self.lines.push(line);
927 self.columns.push(col);
928 self.columns.push(col);
929 self.columns.push(col);
930 patch_pos
931 }
932
933 pub fn patch_jump(&mut self, patch_pos: usize) {
935 let target = self.code.len() as u16;
936 self.code[patch_pos] = (target >> 8) as u8;
937 self.code[patch_pos + 1] = (target & 0xFF) as u8;
938 }
939
940 pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
942 let target = target as u16;
943 self.code[patch_pos] = (target >> 8) as u8;
944 self.code[patch_pos + 1] = (target & 0xFF) as u8;
945 }
946
947 pub fn read_u16(&self, pos: usize) -> u16 {
949 ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
950 }
951
952 #[cfg(debug_assertions)]
956 fn note_balance(&mut self, op: Op, count: u16) {
957 match op_stack_delta(op, count) {
958 Some(delta) => self.balance_depth += delta,
959 None => self.balance_nonlinear += 1,
960 }
961 }
962
963 #[cfg(debug_assertions)]
966 pub(crate) fn balance_probe(&self) -> BalanceProbe {
967 BalanceProbe {
968 depth: self.balance_depth,
969 nonlinear: self.balance_nonlinear,
970 }
971 }
972
973 #[cfg(debug_assertions)]
979 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
980 if self.balance_nonlinear == probe.nonlinear {
981 Some(self.balance_depth - probe.depth)
982 } else {
983 None
984 }
985 }
986
987 fn register_inline_cache(&mut self, op_offset: usize) {
988 if self.inline_cache_slots.contains_key(&op_offset) {
989 return;
990 }
991 let mut entries = self.inline_caches.lock();
992 let slot = entries.len();
993 entries.push(InlineCacheEntry::Empty);
994 self.inline_cache_slots.insert(op_offset, slot);
995 Self::write_inline_cache_index(&mut self.inline_cache_index, op_offset, slot);
996 }
997
998 fn write_inline_cache_index(index: &mut Vec<u32>, op_offset: usize, slot: usize) {
1003 if op_offset >= index.len() {
1004 index.resize(op_offset + 1, NO_INLINE_CACHE_SLOT);
1005 }
1006 index[op_offset] = slot as u32;
1007 }
1008
1009 #[inline]
1018 pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
1019 match self.inline_cache_index.get(op_offset).copied() {
1020 None | Some(NO_INLINE_CACHE_SLOT) => None,
1021 Some(slot) => Some(slot as usize),
1022 }
1023 }
1024
1025 pub(crate) fn inline_cache_slot_count(&self) -> usize {
1026 self.inline_cache_slots.len()
1027 }
1028
1029 pub(crate) fn cache_id(&self) -> u64 {
1030 self.cache_id
1031 }
1032
1033 #[cfg(feature = "vm-bench-internals")]
1040 pub fn inline_cache_slot_via_btreemap_for_bench(&self, op_offset: usize) -> Option<usize> {
1041 self.inline_cache_slots.get(&op_offset).copied()
1042 }
1043
1044 pub(crate) fn constant_string_rc(&self, idx: usize) -> Option<crate::value::HarnStr> {
1049 let mut entries = self.constant_strings.lock();
1054 if entries.len() < self.constants.len() {
1055 entries.resize(self.constants.len(), None);
1056 }
1057 if let Some(Some(existing)) = entries.get(idx) {
1058 return Some(existing.clone());
1059 }
1060 let materialized = match self.constants.get(idx)? {
1061 Constant::String(s) => crate::value::HarnStr::from(s.as_str()),
1062 _ => return None,
1063 };
1064 entries[idx] = Some(materialized.clone());
1065 Some(materialized)
1066 }
1067
1068 #[inline]
1071 #[cfg(test)]
1072 pub(crate) fn peek_adaptive_binary_cache(
1073 &self,
1074 slot: usize,
1075 ) -> Option<(AdaptiveBinaryOp, AdaptiveBinaryState)> {
1076 match self.inline_caches.lock().get(slot)? {
1077 &InlineCacheEntry::AdaptiveBinary { op, state } => Some((op, state)),
1078 _ => None,
1079 }
1080 }
1081
1082 #[inline]
1085 #[cfg(test)]
1086 pub(crate) fn peek_method_cache(&self, slot: usize) -> Option<(u16, usize, MethodCacheTarget)> {
1087 match self.inline_caches.lock().get(slot)? {
1088 &InlineCacheEntry::Method {
1089 name_idx,
1090 argc,
1091 target,
1092 } => Some((name_idx, argc, target)),
1093 _ => None,
1094 }
1095 }
1096
1097 #[inline]
1100 #[cfg(test)]
1101 pub(crate) fn peek_property_cache(&self, slot: usize) -> Option<(u16, PropertyCacheTarget)> {
1102 match self.inline_caches.lock().get(slot)? {
1103 InlineCacheEntry::Property { name_idx, target } => Some((*name_idx, target.clone())),
1104 _ => None,
1105 }
1106 }
1107
1108 #[inline]
1111 #[cfg(test)]
1112 pub(crate) fn peek_direct_call_state(&self, slot: usize) -> Option<DirectCallState> {
1113 match self.inline_caches.lock().get(slot)? {
1114 InlineCacheEntry::DirectCall { state } => Some(state.clone()),
1115 _ => None,
1116 }
1117 }
1118
1119 #[cfg(test)]
1120 pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
1121 if let Some(existing) = self.inline_caches.lock().get_mut(slot) {
1122 *existing = entry;
1123 }
1124 }
1125
1126 pub fn freeze_for_cache(&self) -> CachedChunk {
1127 CachedChunk {
1128 code: self.code.clone(),
1129 constants: self.constants.clone(),
1130 lines: self.lines.clone(),
1131 columns: self.columns.clone(),
1132 source_file: self.source_file.clone(),
1133 current_col: self.current_col,
1134 functions: self
1135 .functions
1136 .iter()
1137 .map(|function| function.freeze_for_cache())
1138 .collect(),
1139 inline_cache_slots: self.inline_cache_slots.clone(),
1140 local_slots: self.local_slots.clone(),
1141 references_outer_names: self.references_outer_names,
1142 }
1143 }
1144
1145 pub fn from_cached(cached: &CachedChunk) -> Self {
1146 let inline_cache_count = cached.inline_cache_slots.len();
1147 let constants_count = cached.constants.len();
1148 let mut inline_cache_index = Vec::new();
1155 inline_cache_index.resize(cached.code.len(), NO_INLINE_CACHE_SLOT);
1156 for (&op_offset, &slot) in cached.inline_cache_slots.iter() {
1157 if op_offset < inline_cache_index.len() {
1158 inline_cache_index[op_offset] = slot as u32;
1159 }
1160 }
1161 Self {
1162 cache_id: next_chunk_cache_id(),
1163 code: cached.code.clone(),
1164 constants: cached.constants.clone(),
1165 constant_index: build_constant_index(&cached.constants),
1166 lines: cached.lines.clone(),
1167 columns: cached.columns.clone(),
1168 source_file: cached.source_file.clone(),
1169 current_col: cached.current_col,
1170 functions: cached
1171 .functions
1172 .iter()
1173 .map(|function| Arc::new(CompiledFunction::from_cached(function)))
1174 .collect(),
1175 inline_cache_slots: cached.inline_cache_slots.clone(),
1176 inline_cache_index,
1177 inline_caches: Arc::new(Mutex::new(vec![
1178 InlineCacheEntry::Empty;
1179 inline_cache_count
1180 ])),
1181 constant_strings: Arc::new(Mutex::new(vec![None; constants_count])),
1182 local_slots: cached.local_slots.clone(),
1183 references_outer_names: cached.references_outer_names,
1184 #[cfg(debug_assertions)]
1185 balance_depth: 0,
1186 #[cfg(debug_assertions)]
1187 balance_nonlinear: 0,
1188 }
1189 }
1190
1191 pub(crate) fn add_local_slot(
1192 &mut self,
1193 name: String,
1194 mutable: bool,
1195 scope_depth: usize,
1196 ) -> u16 {
1197 let idx = self.local_slots.len();
1198 self.local_slots.push(LocalSlotInfo {
1199 name,
1200 mutable,
1201 scope_depth,
1202 });
1203 idx as u16
1204 }
1205
1206 pub fn read_u64(&self, pos: usize) -> u64 {
1208 u64::from_be_bytes([
1209 self.code[pos],
1210 self.code[pos + 1],
1211 self.code[pos + 2],
1212 self.code[pos + 3],
1213 self.code[pos + 4],
1214 self.code[pos + 5],
1215 self.code[pos + 6],
1216 self.code[pos + 7],
1217 ])
1218 }
1219
1220 pub fn disassemble(&self, name: &str) -> String {
1224 let mut out = format!("== {name} ==\n");
1225 let mut ip = 0;
1226 while ip < self.code.len() {
1227 let op_byte = self.code[ip];
1228 let line = self.lines.get(ip).copied().unwrap_or(0);
1229 out.push_str(&format!("{ip:04} [{line:>4}] "));
1230 ip += 1;
1231
1232 if let Some(op) = Op::from_byte(op_byte) {
1233 self.disassemble_op(op, &mut ip, &mut out);
1234 } else {
1235 out.push_str(&format!("UNKNOWN(0x{op_byte:02x})\n"));
1236 }
1237 }
1238 out
1239 }
1240}
1241
1242pub(crate) fn disasm_bare(_chunk: &Chunk, _ip: &mut usize, label: &str) -> String {
1253 label.to_string()
1254}
1255
1256pub(crate) fn disasm_u8(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1257 let arg = chunk.code[*ip];
1258 *ip += 1;
1259 format!("{label} {arg:>4}")
1260}
1261
1262pub(crate) fn disasm_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1263 let arg = chunk.read_u16(*ip);
1264 *ip += 2;
1265 format!("{label} {arg:>4}")
1266}
1267
1268pub(crate) fn disasm_try_catch_setup(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1269 let catch_offset = chunk.read_u16(*ip);
1270 *ip += 2;
1271 let type_idx = chunk.read_u16(*ip);
1272 *ip += 2;
1273 if let Some(type_name) = chunk.constants.get(type_idx as usize) {
1274 format!("{label} {catch_offset:>4} type {type_idx:>4} ({type_name})")
1275 } else {
1276 format!("{label} {catch_offset:>4} type {type_idx:>4}")
1277 }
1278}
1279
1280pub(crate) fn disasm_const_pool_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1281 let idx = chunk.read_u16(*ip);
1282 *ip += 2;
1283 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1284}
1285
1286pub(crate) fn disasm_local_slot_u16(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1287 let slot = chunk.read_u16(*ip);
1288 *ip += 2;
1289 let mut out = format!("{label} {slot:>4}");
1290 if let Some(info) = chunk.local_slots.get(slot as usize) {
1291 out.push_str(&format!(" ({})", info.name));
1292 }
1293 out
1294}
1295
1296pub(crate) fn disasm_const_pool_local_slot(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1297 let prop = chunk.read_u16(*ip);
1298 *ip += 2;
1299 let slot = chunk.read_u16(*ip);
1300 *ip += 2;
1301 let mut out = format!(
1302 "{label} prop {prop:>4} ({}) slot {slot:>4}",
1303 chunk.constants[prop as usize]
1304 );
1305 if let Some(info) = chunk.local_slots.get(slot as usize) {
1306 out.push_str(&format!(" ({})", info.name));
1307 }
1308 out
1309}
1310
1311pub(crate) fn disasm_method_call(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1312 let idx = chunk.read_u16(*ip);
1313 *ip += 2;
1314 let argc = chunk.code[*ip];
1315 *ip += 1;
1316 format!(
1317 "{label} {idx:>4} ({}) argc={argc}",
1318 chunk.constants[idx as usize]
1319 )
1320}
1321
1322pub(crate) fn disasm_match_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1323 let enum_idx = chunk.read_u16(*ip);
1324 *ip += 2;
1325 let var_idx = chunk.read_u16(*ip);
1326 *ip += 2;
1327 format!(
1328 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({})",
1329 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1330 )
1331}
1332
1333pub(crate) fn disasm_build_enum(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1334 let enum_idx = chunk.read_u16(*ip);
1335 *ip += 2;
1336 let var_idx = chunk.read_u16(*ip);
1337 *ip += 2;
1338 let field_count = chunk.read_u16(*ip);
1339 *ip += 2;
1340 format!(
1341 "{label} {enum_idx:>4} ({}) {var_idx:>4} ({}) fields={field_count}",
1342 chunk.constants[enum_idx as usize], chunk.constants[var_idx as usize],
1343 )
1344}
1345
1346pub(crate) fn disasm_selective_import(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1347 let path_idx = chunk.read_u16(*ip);
1348 *ip += 2;
1349 let names_idx = chunk.read_u16(*ip);
1350 *ip += 2;
1351 format!(
1352 "{label} {path_idx:>4} ({}) names: {names_idx:>4} ({})",
1353 chunk.constants[path_idx as usize], chunk.constants[names_idx as usize],
1354 )
1355}
1356
1357pub(crate) fn disasm_check_type(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1358 let var_idx = chunk.read_u16(*ip);
1359 *ip += 2;
1360 let type_idx = chunk.read_u16(*ip);
1361 *ip += 2;
1362 format!(
1363 "{label} {var_idx:>4} ({}) -> {type_idx:>4} ({})",
1364 chunk.constants[var_idx as usize], chunk.constants[type_idx as usize],
1365 )
1366}
1367
1368pub(crate) fn disasm_call_builtin(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1369 let id = chunk.read_u64(*ip);
1370 *ip += 8;
1371 let idx = chunk.read_u16(*ip);
1372 *ip += 2;
1373 let argc = chunk.code[*ip];
1374 *ip += 1;
1375 format!(
1376 "{label} {id:#018x} {idx:>4} ({}) argc={argc}",
1377 chunk.constants[idx as usize],
1378 )
1379}
1380
1381pub(crate) fn disasm_call_builtin_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1382 let id = chunk.read_u64(*ip);
1383 *ip += 8;
1384 let idx = chunk.read_u16(*ip);
1385 *ip += 2;
1386 format!(
1387 "{label} {id:#018x} {idx:>4} ({})",
1388 chunk.constants[idx as usize],
1389 )
1390}
1391
1392pub(crate) fn disasm_method_call_spread(chunk: &Chunk, ip: &mut usize, label: &str) -> String {
1393 let idx = chunk.read_u16(*ip);
1399 *ip += 2;
1400 format!("{label} {idx:>4} ({})", chunk.constants[idx as usize])
1401}
1402
1403impl Default for Chunk {
1404 fn default() -> Self {
1405 Self::new()
1406 }
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use std::sync::Arc;
1412
1413 use super::{
1414 Chunk, Constant, DirectCallState, DirectCallTarget, InlineCacheEntry, MethodCacheTarget,
1415 Op, PropertyCacheTarget,
1416 };
1417 use crate::BuiltinId;
1418
1419 #[test]
1420 fn op_from_byte_matches_repr_order() {
1421 for (byte, op) in Op::ALL.iter().copied().enumerate() {
1422 assert_eq!(byte as u8, op as u8);
1423 assert_eq!(Op::from_byte(byte as u8), Some(op));
1424 }
1425 assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1426 assert_eq!(Op::COUNT, Op::ALL.len());
1427 }
1428
1429 #[test]
1430 fn disassemble_covers_every_opcode_variant() {
1431 for op in Op::ALL.iter().copied() {
1441 let mut chunk = Chunk::new();
1442 chunk.add_constant(super::Constant::String("__probe__".to_string()));
1443 for _ in 0..16 {
1447 chunk.code.push(0);
1448 }
1449 let mut ip: usize = 0;
1450 let mut out = String::new();
1451 chunk.disassemble_op(op, &mut ip, &mut out);
1452 assert!(
1453 !out.contains("UNKNOWN"),
1454 "disasm emitted UNKNOWN for {op:?}: {out}",
1455 );
1456 assert!(!out.is_empty(), "disasm produced no output for {op:?}");
1457 }
1458 }
1459
1460 #[test]
1469 fn empty_chunk_does_not_reference_outer_names() {
1470 let chunk = Chunk::new();
1471 assert!(!chunk.references_outer_names);
1472 }
1473
1474 #[test]
1475 fn arithmetic_only_chunk_does_not_reference_outer_names() {
1476 let mut chunk = Chunk::new();
1481 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1482 chunk.emit_u16(Op::Constant, 0, 1);
1483 chunk.emit(Op::MulInt, 1);
1484 chunk.emit(Op::Pop, 1);
1485 chunk.emit(Op::Return, 1);
1486 assert!(!chunk.references_outer_names);
1487 }
1488
1489 #[test]
1490 fn slot_only_chunk_does_not_reference_outer_names() {
1491 let mut chunk = Chunk::new();
1493 chunk.emit_u16(Op::DefLocalSlot, 0, 1);
1494 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1495 chunk.emit_u16(Op::SetLocalSlot, 0, 1);
1496 assert!(!chunk.references_outer_names);
1497 }
1498
1499 #[test]
1500 fn get_var_flags_outer_name_reference() {
1501 let mut chunk = Chunk::new();
1502 chunk.emit_u16(Op::GetVar, 0, 1);
1503 assert!(chunk.references_outer_names);
1504 }
1505
1506 #[test]
1507 fn set_var_flags_outer_name_reference() {
1508 let mut chunk = Chunk::new();
1509 chunk.emit_u16(Op::SetVar, 0, 1);
1510 assert!(chunk.references_outer_names);
1511 }
1512
1513 #[test]
1514 fn check_type_flags_outer_name_reference() {
1515 let mut chunk = Chunk::new();
1516 chunk.emit_u16(Op::CheckType, 0, 1);
1517 assert!(chunk.references_outer_names);
1518 }
1519
1520 #[test]
1521 fn call_builtin_flags_outer_name_reference() {
1522 let mut chunk = Chunk::new();
1523 chunk.emit_call_builtin(BuiltinId::from_name("any_name"), 0, 1, 1);
1524 assert!(chunk.references_outer_names);
1525 }
1526
1527 #[test]
1528 fn call_builtin_spread_flags_outer_name_reference() {
1529 let mut chunk = Chunk::new();
1530 chunk.emit_call_builtin_spread(BuiltinId::from_name("any_name"), 0, 1);
1531 assert!(chunk.references_outer_names);
1532 }
1533
1534 #[test]
1535 fn tail_call_flags_outer_name_reference() {
1536 let mut chunk = Chunk::new();
1539 chunk.emit_u8(Op::TailCall, 1, 1);
1540 assert!(chunk.references_outer_names);
1541 }
1542
1543 #[test]
1544 fn call_flags_outer_name_reference() {
1545 let mut chunk = Chunk::new();
1548 chunk.emit_u8(Op::Call, 1, 1);
1549 assert!(chunk.references_outer_names);
1550 }
1551
1552 #[test]
1553 fn pipe_flags_outer_name_reference() {
1554 let mut chunk = Chunk::new();
1557 chunk.emit(Op::Pipe, 1);
1558 assert!(chunk.references_outer_names);
1559 }
1560
1561 #[test]
1562 fn method_call_does_not_flag_outer_name_reference() {
1563 let mut chunk = Chunk::new();
1566 chunk.emit_method_call(0, 1, 1);
1567 chunk.emit_method_call_opt(0, 1, 1);
1568 assert!(!chunk.references_outer_names);
1569 }
1570
1571 #[test]
1572 fn jump_and_control_flow_do_not_flag_outer_name_reference() {
1573 let mut chunk = Chunk::new();
1576 chunk.emit_u16(Op::Constant, 0, 1);
1577 chunk.emit(Op::JumpIfFalse, 1);
1578 chunk.emit(Op::Jump, 1);
1579 chunk.emit(Op::Return, 1);
1580 chunk.emit(Op::Pop, 1);
1581 assert!(!chunk.references_outer_names);
1582 }
1583
1584 #[test]
1585 fn references_outer_names_is_monotonic() {
1586 let mut chunk = Chunk::new();
1589 chunk.emit_u16(Op::GetVar, 0, 1);
1590 assert!(chunk.references_outer_names);
1591 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1592 chunk.emit(Op::MulInt, 1);
1593 assert!(chunk.references_outer_names);
1594 }
1595
1596 #[test]
1597 fn freeze_thaw_round_trips_references_outer_names() {
1598 let mut chunk = Chunk::new();
1602 chunk.emit_u16(Op::GetVar, 0, 1);
1603 assert!(chunk.references_outer_names);
1604 let frozen = chunk.freeze_for_cache();
1605 let thawed = Chunk::from_cached(&frozen);
1606 assert!(thawed.references_outer_names);
1607
1608 let plain = Chunk::new();
1609 assert!(!plain.references_outer_names);
1610 let frozen_plain = plain.freeze_for_cache();
1611 let thawed_plain = Chunk::from_cached(&frozen_plain);
1612 assert!(!thawed_plain.references_outer_names);
1613 }
1614
1615 #[test]
1627 fn inline_cache_slot_returns_none_for_non_cacheable_offsets() {
1628 let mut chunk = Chunk::new();
1631 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1632 chunk.emit(Op::Pop, 1);
1633 chunk.emit(Op::Return, 1);
1634 assert!(chunk.inline_cache_slot(0).is_none());
1635 assert!(chunk.inline_cache_slot(3).is_none());
1636 assert!(chunk.inline_cache_slot(4).is_none());
1637 }
1638
1639 #[test]
1640 fn inline_cache_slot_registered_for_adaptive_binary_op() {
1641 let mut chunk = Chunk::new();
1645 chunk.emit(Op::Add, 1);
1646 assert_eq!(chunk.inline_cache_slot(0), Some(0));
1647 }
1648
1649 #[test]
1650 fn inline_cache_slot_distinct_for_sequential_adaptive_binary_ops() {
1651 let mut chunk = Chunk::new();
1656 chunk.emit(Op::Add, 1);
1657 chunk.emit(Op::Sub, 1);
1658 chunk.emit(Op::Mul, 1);
1659 let s0 = chunk.inline_cache_slot(0).expect("Add slot");
1660 let s1 = chunk.inline_cache_slot(1).expect("Sub slot");
1661 let s2 = chunk.inline_cache_slot(2).expect("Mul slot");
1662 assert_ne!(s0, s1);
1663 assert_ne!(s1, s2);
1664 assert_ne!(s0, s2);
1665 }
1666
1667 #[test]
1668 fn inline_cache_slot_returns_none_for_out_of_bounds_offset() {
1669 let mut chunk = Chunk::new();
1672 chunk.emit(Op::Add, 1);
1673 assert!(chunk.inline_cache_slot(usize::MAX).is_none());
1674 assert!(chunk.inline_cache_slot(chunk.code.len()).is_none());
1675 assert!(chunk.inline_cache_slot(chunk.code.len() + 16).is_none());
1676 }
1677
1678 #[test]
1679 fn inline_cache_slot_for_get_property_and_method_call() {
1680 let mut chunk = Chunk::new();
1684 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");
1689 assert!(chunk.inline_cache_slot(3).is_some(), "MethodCall");
1690 assert!(chunk.inline_cache_slot(7).is_some(), "MethodCallOpt");
1691 assert!(chunk.inline_cache_slot(11).is_some(), "GetPropertyOpt");
1692 }
1693
1694 #[test]
1695 fn inline_cache_slot_for_call_and_call_builtin() {
1696 let mut chunk = Chunk::new();
1701 chunk.emit_u8(Op::Call, 1, 1); let call_builtin_offset = chunk.code.len();
1703 chunk.emit_call_builtin(BuiltinId::from_name("any"), 0, 1, 1);
1704 assert!(chunk.inline_cache_slot(0).is_some(), "Op::Call IC slot");
1705 assert!(
1706 chunk.inline_cache_slot(call_builtin_offset).is_some(),
1707 "Op::CallBuiltin IC slot"
1708 );
1709 }
1710
1711 #[test]
1712 fn inline_cache_slot_register_is_idempotent_for_same_offset() {
1713 let mut chunk = Chunk::new();
1719 chunk.emit(Op::Add, 1);
1720 let slot_before = chunk.inline_cache_slot(0).expect("first registration");
1721 chunk.register_inline_cache(0);
1723 let slot_after = chunk.inline_cache_slot(0).expect("re-registration");
1724 assert_eq!(slot_before, slot_after);
1725 }
1726
1727 #[test]
1728 fn inline_cache_index_round_trips_through_cached_chunk() {
1729 let mut chunk = Chunk::new();
1736 chunk.emit_u16(Op::GetLocalSlot, 0, 1);
1737 chunk.emit_u16(Op::Constant, 0, 1);
1738 chunk.emit(Op::Add, 1);
1739 chunk.emit(Op::Sub, 1);
1740 chunk.emit_method_call(0, 1, 1);
1741 chunk.emit_u8(Op::Call, 1, 1);
1742 let live_slots: Vec<(usize, Option<usize>)> = (0..chunk.code.len())
1743 .map(|o| (o, chunk.inline_cache_slot(o)))
1744 .collect();
1745 let frozen = chunk.freeze_for_cache();
1746 let thawed = Chunk::from_cached(&frozen);
1747 let thawed_slots: Vec<(usize, Option<usize>)> = (0..thawed.code.len())
1748 .map(|o| (o, thawed.inline_cache_slot(o)))
1749 .collect();
1750 assert_eq!(live_slots, thawed_slots);
1751 }
1752
1753 #[test]
1754 fn inline_cache_index_agrees_with_btreemap_view() {
1755 let mut chunk = Chunk::new();
1761 chunk.emit(Op::Add, 1);
1762 chunk.emit_u16(Op::GetVar, 0, 1);
1763 chunk.emit(Op::LessInt, 1);
1764 chunk.emit_u8(Op::Call, 2, 1);
1765 chunk.emit(Op::Equal, 1);
1766 chunk.emit_u16(Op::GetProperty, 0, 1);
1767 chunk.emit_method_call_opt(0, 0, 1);
1768 for offset in 0..chunk.code.len() {
1769 let from_map = chunk.inline_cache_slots.get(&offset).copied();
1770 let from_index = chunk.inline_cache_slot(offset);
1771 assert_eq!(from_index, from_map, "parity broken at offset {offset}");
1772 }
1773 }
1774
1775 #[test]
1786 fn peek_adaptive_binary_returns_none_for_empty_slot() {
1787 let mut chunk = Chunk::new();
1788 chunk.emit(Op::Add, 1);
1789 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1790 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1792 }
1793
1794 #[test]
1795 fn peek_adaptive_binary_returns_op_and_state_after_warmup() {
1796 use super::{AdaptiveBinaryOp, AdaptiveBinaryState, BinaryShape, InlineCacheEntry};
1797 let mut chunk = Chunk::new();
1798 chunk.emit(Op::Add, 1);
1799 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1800 chunk.set_inline_cache_entry(
1801 slot,
1802 InlineCacheEntry::AdaptiveBinary {
1803 op: AdaptiveBinaryOp::Add,
1804 state: AdaptiveBinaryState::Warmup {
1805 shape: BinaryShape::Int,
1806 hits: 2,
1807 },
1808 },
1809 );
1810 let (op, state) = chunk
1811 .peek_adaptive_binary_cache(slot)
1812 .expect("warmed slot peek");
1813 assert_eq!(op, AdaptiveBinaryOp::Add);
1814 assert!(matches!(
1815 state,
1816 AdaptiveBinaryState::Warmup {
1817 shape: BinaryShape::Int,
1818 hits: 2
1819 }
1820 ));
1821 }
1822
1823 #[test]
1824 fn peek_adaptive_binary_returns_none_for_non_binary_variants() {
1825 use super::{InlineCacheEntry, PropertyCacheTarget};
1831 let mut chunk = Chunk::new();
1832 chunk.emit(Op::Add, 1);
1833 let slot = chunk.inline_cache_slot(0).expect("Add registers a slot");
1834 chunk.set_inline_cache_entry(
1835 slot,
1836 InlineCacheEntry::Property {
1837 name_idx: 0,
1838 target: PropertyCacheTarget::ListCount,
1839 },
1840 );
1841 assert!(chunk.peek_adaptive_binary_cache(slot).is_none());
1842 }
1843
1844 #[test]
1845 fn peek_adaptive_binary_returns_none_for_out_of_bounds_slot() {
1846 let chunk = Chunk::new();
1851 assert!(chunk.peek_adaptive_binary_cache(0).is_none());
1852 assert!(chunk.peek_adaptive_binary_cache(usize::MAX).is_none());
1853 }
1854
1855 #[test]
1856 fn peek_adaptive_binary_state_is_copy() {
1857 fn assert_copy<T: Copy>() {}
1863 assert_copy::<super::AdaptiveBinaryState>();
1864 assert_copy::<super::AdaptiveBinaryOp>();
1865 assert_copy::<super::BinaryShape>();
1866 }
1867
1868 #[test]
1879 fn peek_method_cache_returns_none_for_empty_slot() {
1880 let mut chunk = Chunk::new();
1881 chunk.emit_method_call(0, 0, 1);
1882 let slot = chunk
1883 .inline_cache_slot(0)
1884 .expect("MethodCall registers a slot");
1885 assert!(chunk.peek_method_cache(slot).is_none());
1886 }
1887
1888 #[test]
1889 fn peek_method_cache_returns_triple_after_warmup() {
1890 let mut chunk = Chunk::new();
1891 chunk.emit_method_call(7, 2, 1);
1892 let slot = chunk
1893 .inline_cache_slot(0)
1894 .expect("MethodCall registers a slot");
1895 chunk.set_inline_cache_entry(
1896 slot,
1897 InlineCacheEntry::Method {
1898 name_idx: 7,
1899 argc: 2,
1900 target: MethodCacheTarget::ListContains,
1901 },
1902 );
1903 let (name_idx, argc, target) = chunk.peek_method_cache(slot).expect("warmed slot peek");
1904 assert_eq!(name_idx, 7);
1905 assert_eq!(argc, 2);
1906 assert_eq!(target, MethodCacheTarget::ListContains);
1907 }
1908
1909 #[test]
1910 fn peek_method_cache_returns_none_for_non_method_variants() {
1911 let mut chunk = Chunk::new();
1915 chunk.emit_method_call(0, 0, 1);
1916 let slot = chunk
1917 .inline_cache_slot(0)
1918 .expect("MethodCall registers a slot");
1919
1920 chunk.set_inline_cache_entry(
1921 slot,
1922 InlineCacheEntry::Property {
1923 name_idx: 0,
1924 target: PropertyCacheTarget::ListCount,
1925 },
1926 );
1927 assert!(chunk.peek_method_cache(slot).is_none());
1928 }
1929
1930 #[test]
1931 fn peek_method_cache_returns_none_for_out_of_bounds_slot() {
1932 let chunk = Chunk::new();
1933 assert!(chunk.peek_method_cache(0).is_none());
1934 assert!(chunk.peek_method_cache(usize::MAX).is_none());
1935 }
1936
1937 #[test]
1938 fn peek_method_cache_target_is_copy() {
1939 fn assert_copy<T: Copy>() {}
1945 assert_copy::<super::MethodCacheTarget>();
1946 }
1947
1948 #[test]
1958 fn peek_property_cache_returns_none_for_empty_slot() {
1959 let mut chunk = Chunk::new();
1960 chunk.emit_u16(Op::GetProperty, 0, 1);
1961 let slot = chunk
1962 .inline_cache_slot(0)
1963 .expect("GetProperty registers a slot");
1964 assert!(chunk.peek_property_cache(slot).is_none());
1965 }
1966
1967 #[test]
1968 fn peek_property_cache_returns_pair_after_warmup_for_dict_field() {
1969 let mut chunk = Chunk::new();
1970 chunk.emit_u16(Op::GetProperty, 0, 1);
1971 let slot = chunk
1972 .inline_cache_slot(0)
1973 .expect("GetProperty registers a slot");
1974 chunk.set_inline_cache_entry(
1975 slot,
1976 InlineCacheEntry::Property {
1977 name_idx: 11,
1978 target: PropertyCacheTarget::DictField(Arc::from("count")),
1979 },
1980 );
1981 let (name_idx, target) = chunk
1982 .peek_property_cache(slot)
1983 .expect("warmed property slot peek");
1984 assert_eq!(name_idx, 11);
1985 match target {
1986 PropertyCacheTarget::DictField(field) => assert_eq!(field.as_ref(), "count"),
1987 other => panic!("expected DictField, got {other:?}"),
1988 }
1989 }
1990
1991 #[test]
1992 fn peek_property_cache_returns_pair_for_unit_target() {
1993 let mut chunk = Chunk::new();
1997 chunk.emit_u16(Op::GetProperty, 0, 1);
1998 let slot = chunk
1999 .inline_cache_slot(0)
2000 .expect("GetProperty registers a slot");
2001 chunk.set_inline_cache_entry(
2002 slot,
2003 InlineCacheEntry::Property {
2004 name_idx: 3,
2005 target: PropertyCacheTarget::ListCount,
2006 },
2007 );
2008 let (name_idx, target) = chunk
2009 .peek_property_cache(slot)
2010 .expect("warmed property slot peek");
2011 assert_eq!(name_idx, 3);
2012 assert_eq!(target, PropertyCacheTarget::ListCount);
2013 }
2014
2015 #[test]
2016 fn peek_property_cache_returns_none_for_non_property_variants() {
2017 let mut chunk = Chunk::new();
2018 chunk.emit_u16(Op::GetProperty, 0, 1);
2019 let slot = chunk
2020 .inline_cache_slot(0)
2021 .expect("GetProperty registers a slot");
2022 chunk.set_inline_cache_entry(
2023 slot,
2024 InlineCacheEntry::Method {
2025 name_idx: 0,
2026 argc: 0,
2027 target: MethodCacheTarget::ListCount,
2028 },
2029 );
2030 assert!(chunk.peek_property_cache(slot).is_none());
2031 }
2032
2033 #[test]
2034 fn peek_property_cache_returns_none_for_out_of_bounds_slot() {
2035 let chunk = Chunk::new();
2036 assert!(chunk.peek_property_cache(0).is_none());
2037 assert!(chunk.peek_property_cache(usize::MAX).is_none());
2038 }
2039
2040 #[test]
2050 fn add_constant_keeps_signed_zero_and_nan_distinct() {
2051 let mut chunk = Chunk::new();
2052 let pos = chunk.add_constant(Constant::Float(0.0));
2055 let neg = chunk.add_constant(Constant::Float(-0.0));
2056 assert_ne!(pos, neg, "+0.0 and -0.0 must get distinct constant slots");
2057 assert_eq!(pos, chunk.add_constant(Constant::Float(0.0)));
2059 assert_eq!(neg, chunk.add_constant(Constant::Float(-0.0)));
2060 let a = chunk.add_constant(Constant::Float(1.5));
2062 assert_eq!(a, chunk.add_constant(Constant::Float(1.5)));
2063 let nan_a = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)));
2064 let nan_b = chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0002)));
2065 assert_ne!(
2066 nan_a, nan_b,
2067 "distinct NaN payloads must get distinct constant slots"
2068 );
2069 assert_eq!(
2070 nan_a,
2071 chunk.add_constant(Constant::Float(f64::from_bits(0x7ff8_0000_0000_0001)))
2072 );
2073 let s = chunk.add_constant(Constant::Int(7));
2075 assert_eq!(s, chunk.add_constant(Constant::Int(7)));
2076 }
2077
2078 #[test]
2079 fn add_constant_uses_first_slot_after_many_unique_constants() {
2080 let mut chunk = Chunk::new();
2081 let first = chunk.add_constant(Constant::String("shared".to_string()));
2082 for index in 0..10_000 {
2083 let slot = chunk.add_constant(Constant::String(format!("unique_{index}")));
2084 assert_eq!(slot as usize, index + 1);
2085 }
2086 assert_eq!(
2087 first,
2088 chunk.add_constant(Constant::String("shared".to_string())),
2089 "duplicate lookup must return the original slot after index growth"
2090 );
2091 }
2092
2093 #[test]
2094 fn constant_index_round_trips_through_cached_chunk() {
2095 let mut chunk = Chunk::new();
2096 let shared = chunk.add_constant(Constant::String("shared".to_string()));
2097 for index in 0..128 {
2098 chunk.add_constant(Constant::Int(index));
2099 }
2100
2101 let frozen = chunk.freeze_for_cache();
2102 let mut thawed = Chunk::from_cached(&frozen);
2103 assert_eq!(
2104 shared,
2105 thawed.add_constant(Constant::String("shared".to_string())),
2106 "cache thaw must rebuild the constant side index"
2107 );
2108 let next = thawed.add_constant(Constant::String("new".to_string()));
2109 assert_eq!(next as usize, frozen.constants.len());
2110 }
2111
2112 #[test]
2113 fn peek_direct_call_state_returns_none_for_empty_slot() {
2114 let mut chunk = Chunk::new();
2115 chunk.emit_u8(Op::Call, 0, 1);
2116 let slot = chunk
2117 .inline_cache_slot(0)
2118 .expect("Op::Call registers a slot");
2119 assert!(chunk.peek_direct_call_state(slot).is_none());
2120 }
2121
2122 #[test]
2123 fn peek_direct_call_state_returns_warmup_state() {
2124 let mut chunk = Chunk::new();
2125 chunk.emit_u8(Op::Call, 0, 1);
2126 let slot = chunk
2127 .inline_cache_slot(0)
2128 .expect("Op::Call registers a slot");
2129 let target = synthetic_direct_call_target();
2130 chunk.set_inline_cache_entry(
2131 slot,
2132 InlineCacheEntry::DirectCall {
2133 state: DirectCallState::Warmup {
2134 argc: 2,
2135 target: target.clone(),
2136 hits: 1,
2137 },
2138 },
2139 );
2140 let state = chunk
2141 .peek_direct_call_state(slot)
2142 .expect("warmed direct-call slot peek");
2143 match state {
2144 DirectCallState::Warmup {
2145 argc,
2146 target: peeked_target,
2147 hits,
2148 } => {
2149 assert_eq!(argc, 2);
2150 assert_eq!(hits, 1);
2151 assert_eq!(peeked_target, target);
2152 }
2153 other => panic!("expected Warmup, got {other:?}"),
2154 }
2155 }
2156
2157 #[test]
2158 fn peek_direct_call_state_returns_specialized_state() {
2159 let mut chunk = Chunk::new();
2160 chunk.emit_u8(Op::Call, 0, 1);
2161 let slot = chunk
2162 .inline_cache_slot(0)
2163 .expect("Op::Call registers a slot");
2164 let target = synthetic_direct_call_target();
2165 chunk.set_inline_cache_entry(
2166 slot,
2167 InlineCacheEntry::DirectCall {
2168 state: DirectCallState::Specialized {
2169 argc: 3,
2170 target: target.clone(),
2171 hits: 100,
2172 misses: 0,
2173 },
2174 },
2175 );
2176 let state = chunk
2177 .peek_direct_call_state(slot)
2178 .expect("warmed direct-call slot peek");
2179 match state {
2180 DirectCallState::Specialized {
2181 argc,
2182 target: peeked_target,
2183 hits,
2184 misses,
2185 } => {
2186 assert_eq!(argc, 3);
2187 assert_eq!(hits, 100);
2188 assert_eq!(misses, 0);
2189 assert_eq!(peeked_target, target);
2190 }
2191 other => panic!("expected Specialized, got {other:?}"),
2192 }
2193 }
2194
2195 #[test]
2196 fn peek_direct_call_state_returns_none_for_non_direct_call_variants() {
2197 let mut chunk = Chunk::new();
2198 chunk.emit_u8(Op::Call, 0, 1);
2199 let slot = chunk
2200 .inline_cache_slot(0)
2201 .expect("Op::Call registers a slot");
2202
2203 chunk.set_inline_cache_entry(
2204 slot,
2205 InlineCacheEntry::Property {
2206 name_idx: 0,
2207 target: PropertyCacheTarget::ListCount,
2208 },
2209 );
2210 assert!(chunk.peek_direct_call_state(slot).is_none());
2211 }
2212
2213 #[test]
2214 fn peek_direct_call_state_returns_none_for_out_of_bounds_slot() {
2215 let chunk = Chunk::new();
2216 assert!(chunk.peek_direct_call_state(0).is_none());
2217 assert!(chunk.peek_direct_call_state(usize::MAX).is_none());
2218 }
2219
2220 fn synthetic_direct_call_target() -> DirectCallTarget {
2224 use crate::value::VmClosure;
2225 use crate::{CompiledFunction, VmEnv};
2226 let func = CompiledFunction {
2227 name: "synthetic".to_string(),
2228 type_params: Vec::new(),
2229 nominal_type_names: Vec::new(),
2230 params: Vec::new(),
2231 default_start: None,
2232 chunk: Arc::new(Chunk::new()),
2233 is_generator: false,
2234 is_stream: false,
2235 has_rest_param: false,
2236 has_runtime_type_checks: false,
2237 };
2238 DirectCallTarget::Closure(Arc::new(VmClosure {
2239 func: Arc::new(func),
2240 env: VmEnv::new(),
2241 source_dir: None,
2242 module_functions: None,
2243 module_state: None,
2244 }))
2245 }
2246}