Skip to main content

shape_vm/
feedback.rs

1//! Feedback vectors for inline cache (IC) type profiling.
2//!
3//! Each function gets an optional `FeedbackVector` that records observed types
4//! at IC-eligible sites (calls, property accesses, arithmetic, method dispatch).
5//! The JIT compiler reads this feedback to generate speculative optimizations.
6
7use std::collections::HashMap;
8
9/// Maximum number of entries tracked before transitioning to Megamorphic.
10const MAX_POLYMORPHIC_ENTRIES: usize = 4;
11
12/// IC state machine: tracks how polymorphic a site has become.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ICState {
15    /// No observations yet.
16    Uninitialized,
17    /// Single type/target observed — optimal for speculation.
18    Monomorphic,
19    /// 2-4 types/targets observed — can use multi-way dispatch.
20    Polymorphic,
21    /// >4 types/targets — too many to specialize, use generic path.
22    Megamorphic,
23}
24
25/// A feedback slot records type observations at a single IC site.
26#[derive(Debug, Clone)]
27pub enum FeedbackSlot {
28    Uninitialized,
29    Call(CallFeedback),
30    Property(PropertyFeedback),
31    Arithmetic(ArithmeticFeedback),
32    Method(MethodFeedback),
33}
34
35/// Call site feedback: which function targets have been called.
36#[derive(Debug, Clone)]
37pub struct CallFeedback {
38    pub state: ICState,
39    pub targets: Vec<CallTarget>,
40    pub total_calls: u64,
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct CallTarget {
45    pub function_id: u16,
46    pub count: u64,
47}
48
49/// Property access feedback: which schemas and field indices observed.
50#[derive(Debug, Clone)]
51pub struct PropertyFeedback {
52    pub state: ICState,
53    pub entries: Vec<PropertyCacheEntry>,
54}
55
56/// Receiver kind discriminator for property feedback.
57/// Tells the JIT whether to emit a TypedObject schema guard or a HashMap shape guard.
58pub const RECEIVER_TYPED_OBJECT: u8 = 0;
59pub const RECEIVER_HASHMAP: u8 = 1;
60
61#[derive(Debug, Clone, PartialEq)]
62pub struct PropertyCacheEntry {
63    pub schema_id: u64,
64    pub field_idx: u16,
65    pub field_type_tag: u16,
66    pub hit_count: u64,
67    /// Receiver heap kind: 0 = TypedObject (schema guard), 1 = HashMap (shape guard).
68    pub receiver_kind: u8,
69}
70
71/// Arithmetic feedback: which operand type pairs observed.
72#[derive(Debug, Clone)]
73pub struct ArithmeticFeedback {
74    pub state: ICState,
75    pub type_pairs: Vec<ArithmeticTypePair>,
76}
77
78#[derive(Debug, Clone, PartialEq)]
79pub struct ArithmeticTypePair {
80    pub left_tag: u8,
81    pub right_tag: u8,
82    pub count: u64,
83}
84
85/// Method dispatch feedback: which receiver kinds and method handlers observed.
86#[derive(Debug, Clone)]
87pub struct MethodFeedback {
88    pub state: ICState,
89    pub entries: Vec<MethodCacheEntry>,
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub struct MethodCacheEntry {
94    pub receiver_kind: u8,
95    pub method_name_id: u32,
96    pub handler_ptr: usize,
97    pub hit_count: u64,
98}
99
100/// Per-function feedback vector: maps bytecode offsets to IC slots.
101#[derive(Debug, Clone)]
102pub struct FeedbackVector {
103    pub function_id: u16,
104    pub slots: HashMap<usize, FeedbackSlot>,
105    pub generation: u32,
106}
107
108/// Compute the next IC state given the current state and entry count.
109fn next_state(current: ICState, entry_count: usize) -> ICState {
110    match current {
111        ICState::Uninitialized => ICState::Monomorphic,
112        ICState::Monomorphic => {
113            if entry_count <= 1 {
114                ICState::Monomorphic
115            } else {
116                ICState::Polymorphic
117            }
118        }
119        ICState::Polymorphic => {
120            if entry_count > MAX_POLYMORPHIC_ENTRIES {
121                ICState::Megamorphic
122            } else {
123                ICState::Polymorphic
124            }
125        }
126        ICState::Megamorphic => ICState::Megamorphic,
127    }
128}
129
130impl FeedbackVector {
131    /// Creates an empty feedback vector with generation 0.
132    pub fn new(function_id: u16) -> Self {
133        Self {
134            function_id,
135            slots: HashMap::new(),
136            generation: 0,
137        }
138    }
139
140    /// Rebase slot keys by subtracting `base_offset`.
141    ///
142    /// The interpreter records feedback at absolute bytecode IPs, but the JIT
143    /// compiles sub-programs with 0-based instruction indices. This method
144    /// remaps slots so the JIT can look up feedback by local offset.
145    ///
146    /// Slots with offsets below `base_offset` are dropped (they belong to
147    /// a different function or the program preamble).
148    pub fn rebase(&mut self, base_offset: usize) {
149        if base_offset == 0 {
150            return;
151        }
152        let old_slots = std::mem::take(&mut self.slots);
153        for (offset, slot) in old_slots {
154            if offset >= base_offset {
155                self.slots.insert(offset - base_offset, slot);
156            }
157        }
158    }
159
160    /// Merge another feedback vector's slots into this one at a given offset.
161    ///
162    /// Used when inlining a callee: the callee's feedback slots are mapped
163    /// into the outer function's sub-program index space by adding `offset`
164    /// to each slot key.
165    pub fn merge_at_offset(&mut self, other: &FeedbackVector, offset: usize) {
166        for (ip, slot) in &other.slots {
167            self.slots.insert(ip + offset, slot.clone());
168        }
169    }
170
171    /// Records a call target at the given bytecode offset.
172    pub fn record_call(&mut self, offset: usize, target_function_id: u16) {
173        let slot = self
174            .slots
175            .entry(offset)
176            .or_insert(FeedbackSlot::Uninitialized);
177
178        match slot {
179            FeedbackSlot::Uninitialized => {
180                *slot = FeedbackSlot::Call(CallFeedback {
181                    state: ICState::Monomorphic,
182                    targets: vec![CallTarget {
183                        function_id: target_function_id,
184                        count: 1,
185                    }],
186                    total_calls: 1,
187                });
188            }
189            FeedbackSlot::Call(fb) => {
190                fb.total_calls += 1;
191                if let Some(target) = fb
192                    .targets
193                    .iter_mut()
194                    .find(|t| t.function_id == target_function_id)
195                {
196                    target.count += 1;
197                } else if fb.state != ICState::Megamorphic {
198                    fb.targets.push(CallTarget {
199                        function_id: target_function_id,
200                        count: 1,
201                    });
202                    fb.state = next_state(fb.state, fb.targets.len());
203                } else {
204                    // Megamorphic: don't add new entries
205                    fb.state = ICState::Megamorphic;
206                }
207            }
208            _ => {}
209        }
210    }
211
212    /// Records a property access at the given bytecode offset.
213    ///
214    /// `receiver_kind`: 0 = TypedObject (schema guard), 1 = HashMap (shape guard).
215    pub fn record_property(
216        &mut self,
217        offset: usize,
218        schema_id: u64,
219        field_idx: u16,
220        field_type_tag: u16,
221        receiver_kind: u8,
222    ) {
223        let slot = self
224            .slots
225            .entry(offset)
226            .or_insert(FeedbackSlot::Uninitialized);
227
228        match slot {
229            FeedbackSlot::Uninitialized => {
230                *slot = FeedbackSlot::Property(PropertyFeedback {
231                    state: ICState::Monomorphic,
232                    entries: vec![PropertyCacheEntry {
233                        schema_id,
234                        field_idx,
235                        field_type_tag,
236                        hit_count: 1,
237                        receiver_kind,
238                    }],
239                });
240            }
241            FeedbackSlot::Property(fb) => {
242                if let Some(entry) = fb
243                    .entries
244                    .iter_mut()
245                    .find(|e| e.schema_id == schema_id && e.receiver_kind == receiver_kind)
246                {
247                    entry.hit_count += 1;
248                } else if fb.state != ICState::Megamorphic {
249                    fb.entries.push(PropertyCacheEntry {
250                        schema_id,
251                        field_idx,
252                        field_type_tag,
253                        hit_count: 1,
254                        receiver_kind,
255                    });
256                    fb.state = next_state(fb.state, fb.entries.len());
257                }
258            }
259            _ => {}
260        }
261    }
262
263    /// Records arithmetic operand types at the given bytecode offset.
264    pub fn record_arithmetic(&mut self, offset: usize, left_tag: u8, right_tag: u8) {
265        let slot = self
266            .slots
267            .entry(offset)
268            .or_insert(FeedbackSlot::Uninitialized);
269
270        match slot {
271            FeedbackSlot::Uninitialized => {
272                *slot = FeedbackSlot::Arithmetic(ArithmeticFeedback {
273                    state: ICState::Monomorphic,
274                    type_pairs: vec![ArithmeticTypePair {
275                        left_tag,
276                        right_tag,
277                        count: 1,
278                    }],
279                });
280            }
281            FeedbackSlot::Arithmetic(fb) => {
282                if let Some(pair) = fb
283                    .type_pairs
284                    .iter_mut()
285                    .find(|p| p.left_tag == left_tag && p.right_tag == right_tag)
286                {
287                    pair.count += 1;
288                } else if fb.state != ICState::Megamorphic {
289                    fb.type_pairs.push(ArithmeticTypePair {
290                        left_tag,
291                        right_tag,
292                        count: 1,
293                    });
294                    fb.state = next_state(fb.state, fb.type_pairs.len());
295                }
296            }
297            _ => {}
298        }
299    }
300
301    /// Records a method dispatch at the given bytecode offset.
302    pub fn record_method(
303        &mut self,
304        offset: usize,
305        receiver_kind: u8,
306        method_name_id: u32,
307        handler_ptr: usize,
308    ) {
309        let slot = self
310            .slots
311            .entry(offset)
312            .or_insert(FeedbackSlot::Uninitialized);
313
314        match slot {
315            FeedbackSlot::Uninitialized => {
316                *slot = FeedbackSlot::Method(MethodFeedback {
317                    state: ICState::Monomorphic,
318                    entries: vec![MethodCacheEntry {
319                        receiver_kind,
320                        method_name_id,
321                        handler_ptr,
322                        hit_count: 1,
323                    }],
324                });
325            }
326            FeedbackSlot::Method(fb) => {
327                if let Some(entry) = fb.entries.iter_mut().find(|e| {
328                    e.receiver_kind == receiver_kind && e.method_name_id == method_name_id
329                }) {
330                    entry.hit_count += 1;
331                } else if fb.state != ICState::Megamorphic {
332                    fb.entries.push(MethodCacheEntry {
333                        receiver_kind,
334                        method_name_id,
335                        handler_ptr,
336                        hit_count: 1,
337                    });
338                    fb.state = next_state(fb.state, fb.entries.len());
339                }
340            }
341            _ => {}
342        }
343    }
344
345    /// Retrieves the feedback slot at the given bytecode offset.
346    pub fn get_slot(&self, offset: usize) -> Option<&FeedbackSlot> {
347        self.slots.get(&offset)
348    }
349
350    /// Returns true if the slot at the given offset is in Monomorphic state.
351    pub fn is_monomorphic(&self, offset: usize) -> bool {
352        match self.slots.get(&offset) {
353            Some(FeedbackSlot::Call(fb)) => fb.state == ICState::Monomorphic,
354            Some(FeedbackSlot::Property(fb)) => fb.state == ICState::Monomorphic,
355            Some(FeedbackSlot::Arithmetic(fb)) => fb.state == ICState::Monomorphic,
356            Some(FeedbackSlot::Method(fb)) => fb.state == ICState::Monomorphic,
357            _ => false,
358        }
359    }
360
361    /// Clears all slots and increments the generation counter.
362    pub fn reset(&mut self) {
363        self.slots.clear();
364        self.generation += 1;
365    }
366
367    /// Number of active IC slots in this vector.
368    pub fn slot_count(&self) -> usize {
369        self.slots
370            .values()
371            .filter(|s| !matches!(s, FeedbackSlot::Uninitialized))
372            .count()
373    }
374
375    /// Fraction of IC slots that are monomorphic (0.0 to 1.0).
376    ///
377    /// Used by the TierManager to decide whether feedback quality is
378    /// sufficient to warrant optimizing JIT compilation. A high ratio
379    /// (>0.7) suggests the function has stable types worth specializing.
380    pub fn monomorphic_ratio(&self) -> f64 {
381        let total = self.slot_count();
382        if total == 0 {
383            return 0.0;
384        }
385        let mono_count = self
386            .slots
387            .values()
388            .filter(|s| match s {
389                FeedbackSlot::Call(fb) => fb.state == ICState::Monomorphic,
390                FeedbackSlot::Property(fb) => fb.state == ICState::Monomorphic,
391                FeedbackSlot::Arithmetic(fb) => fb.state == ICState::Monomorphic,
392                FeedbackSlot::Method(fb) => fb.state == ICState::Monomorphic,
393                FeedbackSlot::Uninitialized => false,
394            })
395            .count();
396        mono_count as f64 / total as f64
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_new_creates_empty_vector() {
406        let fv = FeedbackVector::new(42);
407        assert_eq!(fv.function_id, 42);
408        assert!(fv.slots.is_empty());
409        assert_eq!(fv.generation, 0);
410    }
411
412    #[test]
413    fn test_record_call_first_observation_becomes_monomorphic() {
414        let mut fv = FeedbackVector::new(0);
415        fv.record_call(10, 1);
416
417        let slot = fv.get_slot(10).unwrap();
418        match slot {
419            FeedbackSlot::Call(fb) => {
420                assert_eq!(fb.state, ICState::Monomorphic);
421                assert_eq!(fb.targets.len(), 1);
422                assert_eq!(fb.targets[0].function_id, 1);
423                assert_eq!(fb.targets[0].count, 1);
424                assert_eq!(fb.total_calls, 1);
425            }
426            _ => panic!("expected Call slot"),
427        }
428    }
429
430    #[test]
431    fn test_record_call_same_target_stays_monomorphic() {
432        let mut fv = FeedbackVector::new(0);
433        fv.record_call(10, 1);
434        fv.record_call(10, 1);
435        fv.record_call(10, 1);
436
437        let slot = fv.get_slot(10).unwrap();
438        match slot {
439            FeedbackSlot::Call(fb) => {
440                assert_eq!(fb.state, ICState::Monomorphic);
441                assert_eq!(fb.targets.len(), 1);
442                assert_eq!(fb.targets[0].count, 3);
443                assert_eq!(fb.total_calls, 3);
444            }
445            _ => panic!("expected Call slot"),
446        }
447    }
448
449    #[test]
450    fn test_record_call_different_target_becomes_polymorphic() {
451        let mut fv = FeedbackVector::new(0);
452        fv.record_call(10, 1);
453        fv.record_call(10, 2);
454
455        let slot = fv.get_slot(10).unwrap();
456        match slot {
457            FeedbackSlot::Call(fb) => {
458                assert_eq!(fb.state, ICState::Polymorphic);
459                assert_eq!(fb.targets.len(), 2);
460            }
461            _ => panic!("expected Call slot"),
462        }
463    }
464
465    #[test]
466    fn test_record_call_five_targets_becomes_megamorphic() {
467        let mut fv = FeedbackVector::new(0);
468        for i in 0..5 {
469            fv.record_call(10, i);
470        }
471
472        let slot = fv.get_slot(10).unwrap();
473        match slot {
474            FeedbackSlot::Call(fb) => {
475                assert_eq!(fb.state, ICState::Megamorphic);
476                assert_eq!(fb.targets.len(), 5);
477            }
478            _ => panic!("expected Call slot"),
479        }
480
481        // 6th target should NOT be added
482        fv.record_call(10, 99);
483        match fv.get_slot(10).unwrap() {
484            FeedbackSlot::Call(fb) => {
485                assert_eq!(fb.state, ICState::Megamorphic);
486                assert_eq!(fb.targets.len(), 5);
487                assert_eq!(fb.total_calls, 6);
488            }
489            _ => panic!("expected Call slot"),
490        }
491    }
492
493    #[test]
494    fn test_record_property_state_transitions() {
495        let mut fv = FeedbackVector::new(0);
496
497        // First observation -> Monomorphic
498        fv.record_property(20, 100, 0, 1, 0);
499        match fv.get_slot(20).unwrap() {
500            FeedbackSlot::Property(fb) => {
501                assert_eq!(fb.state, ICState::Monomorphic);
502                assert_eq!(fb.entries.len(), 1);
503            }
504            _ => panic!("expected Property slot"),
505        }
506
507        // Same schema -> stays Monomorphic, increments count
508        fv.record_property(20, 100, 0, 1, 0);
509        match fv.get_slot(20).unwrap() {
510            FeedbackSlot::Property(fb) => {
511                assert_eq!(fb.state, ICState::Monomorphic);
512                assert_eq!(fb.entries[0].hit_count, 2);
513            }
514            _ => panic!("expected Property slot"),
515        }
516
517        // Different schemas -> Polymorphic
518        fv.record_property(20, 200, 1, 2, 0);
519        match fv.get_slot(20).unwrap() {
520            FeedbackSlot::Property(fb) => {
521                assert_eq!(fb.state, ICState::Polymorphic);
522                assert_eq!(fb.entries.len(), 2);
523            }
524            _ => panic!("expected Property slot"),
525        }
526
527        // Add more schemas until Megamorphic
528        fv.record_property(20, 300, 2, 3, 0);
529        fv.record_property(20, 400, 3, 4, 0);
530        fv.record_property(20, 500, 4, 5, 0);
531        match fv.get_slot(20).unwrap() {
532            FeedbackSlot::Property(fb) => {
533                assert_eq!(fb.state, ICState::Megamorphic);
534                assert_eq!(fb.entries.len(), 5);
535            }
536            _ => panic!("expected Property slot"),
537        }
538
539        // 6th schema not added
540        fv.record_property(20, 600, 5, 6, 0);
541        match fv.get_slot(20).unwrap() {
542            FeedbackSlot::Property(fb) => {
543                assert_eq!(fb.state, ICState::Megamorphic);
544                assert_eq!(fb.entries.len(), 5);
545            }
546            _ => panic!("expected Property slot"),
547        }
548    }
549
550    #[test]
551    fn test_record_arithmetic_state_transitions() {
552        let mut fv = FeedbackVector::new(0);
553
554        // First -> Monomorphic
555        fv.record_arithmetic(30, 1, 1);
556        match fv.get_slot(30).unwrap() {
557            FeedbackSlot::Arithmetic(fb) => {
558                assert_eq!(fb.state, ICState::Monomorphic);
559                assert_eq!(fb.type_pairs.len(), 1);
560            }
561            _ => panic!("expected Arithmetic slot"),
562        }
563
564        // Same pair -> stays Monomorphic
565        fv.record_arithmetic(30, 1, 1);
566        match fv.get_slot(30).unwrap() {
567            FeedbackSlot::Arithmetic(fb) => {
568                assert_eq!(fb.state, ICState::Monomorphic);
569                assert_eq!(fb.type_pairs[0].count, 2);
570            }
571            _ => panic!("expected Arithmetic slot"),
572        }
573
574        // Different pair -> Polymorphic
575        fv.record_arithmetic(30, 1, 2);
576        match fv.get_slot(30).unwrap() {
577            FeedbackSlot::Arithmetic(fb) => {
578                assert_eq!(fb.state, ICState::Polymorphic);
579                assert_eq!(fb.type_pairs.len(), 2);
580            }
581            _ => panic!("expected Arithmetic slot"),
582        }
583
584        // Fill to Megamorphic
585        fv.record_arithmetic(30, 2, 2);
586        fv.record_arithmetic(30, 3, 3);
587        fv.record_arithmetic(30, 4, 4);
588        match fv.get_slot(30).unwrap() {
589            FeedbackSlot::Arithmetic(fb) => {
590                assert_eq!(fb.state, ICState::Megamorphic);
591                assert_eq!(fb.type_pairs.len(), 5);
592            }
593            _ => panic!("expected Arithmetic slot"),
594        }
595    }
596
597    #[test]
598    fn test_record_method_state_transitions() {
599        let mut fv = FeedbackVector::new(0);
600
601        // First -> Monomorphic
602        fv.record_method(40, 10, 100, 0xDEAD);
603        match fv.get_slot(40).unwrap() {
604            FeedbackSlot::Method(fb) => {
605                assert_eq!(fb.state, ICState::Monomorphic);
606                assert_eq!(fb.entries.len(), 1);
607                assert_eq!(fb.entries[0].handler_ptr, 0xDEAD);
608            }
609            _ => panic!("expected Method slot"),
610        }
611
612        // Same receiver+method -> stays Monomorphic
613        fv.record_method(40, 10, 100, 0xDEAD);
614        match fv.get_slot(40).unwrap() {
615            FeedbackSlot::Method(fb) => {
616                assert_eq!(fb.state, ICState::Monomorphic);
617                assert_eq!(fb.entries[0].hit_count, 2);
618            }
619            _ => panic!("expected Method slot"),
620        }
621
622        // Different receiver -> Polymorphic
623        fv.record_method(40, 20, 100, 0xBEEF);
624        match fv.get_slot(40).unwrap() {
625            FeedbackSlot::Method(fb) => {
626                assert_eq!(fb.state, ICState::Polymorphic);
627                assert_eq!(fb.entries.len(), 2);
628            }
629            _ => panic!("expected Method slot"),
630        }
631
632        // Fill to Megamorphic
633        fv.record_method(40, 30, 100, 0xCAFE);
634        fv.record_method(40, 40, 100, 0xF00D);
635        fv.record_method(40, 50, 100, 0xBAAD);
636        match fv.get_slot(40).unwrap() {
637            FeedbackSlot::Method(fb) => {
638                assert_eq!(fb.state, ICState::Megamorphic);
639                assert_eq!(fb.entries.len(), 5);
640            }
641            _ => panic!("expected Method slot"),
642        }
643    }
644
645    #[test]
646    fn test_is_monomorphic() {
647        let mut fv = FeedbackVector::new(0);
648
649        // No slot -> false
650        assert!(!fv.is_monomorphic(10));
651
652        // Monomorphic call -> true
653        fv.record_call(10, 1);
654        assert!(fv.is_monomorphic(10));
655
656        // Polymorphic call -> false
657        fv.record_call(10, 2);
658        assert!(!fv.is_monomorphic(10));
659
660        // Monomorphic property -> true
661        fv.record_property(20, 100, 0, 1, 0);
662        assert!(fv.is_monomorphic(20));
663
664        // Monomorphic arithmetic -> true
665        fv.record_arithmetic(30, 1, 1);
666        assert!(fv.is_monomorphic(30));
667
668        // Monomorphic method -> true
669        fv.record_method(40, 10, 100, 0xDEAD);
670        assert!(fv.is_monomorphic(40));
671    }
672
673    #[test]
674    fn test_reset_clears_and_increments_generation() {
675        let mut fv = FeedbackVector::new(7);
676        fv.record_call(10, 1);
677        fv.record_property(20, 100, 0, 1, 0);
678        assert_eq!(fv.slots.len(), 2);
679        assert_eq!(fv.generation, 0);
680
681        fv.reset();
682        assert!(fv.slots.is_empty());
683        assert_eq!(fv.generation, 1);
684        assert_eq!(fv.function_id, 7);
685
686        fv.reset();
687        assert_eq!(fv.generation, 2);
688    }
689}