Skip to main content

shape_value/
shape_graph.rs

1//! Shape graph for HashMap hidden classes.
2//!
3//! A "shape" describes the ordered property layout of a HashMap, enabling
4//! O(1) index-based property access instead of hash-based lookup. This is
5//! the same concept as V8's "hidden classes" or "maps".
6//!
7//! Shapes form a transition tree: adding a property transitions to a child
8//! shape. Multiple HashMaps with the same property insertion order share
9//! the same shape, enabling inline caching.
10
11use std::collections::HashMap;
12use std::fmt;
13use std::sync::Mutex;
14
15/// Unique identifier for a shape in the transition graph.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct ShapeId(pub u32);
18
19impl fmt::Display for ShapeId {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(f, "Shape({})", self.0)
22    }
23}
24
25/// A shape describes the ordered property layout of a HashMap.
26///
27/// Properties are identified by u32 "string IDs" (hashed property names).
28/// The index of a property in the `properties` vec is its slot index for
29/// direct access.
30#[derive(Debug, Clone)]
31pub struct Shape {
32    pub id: ShapeId,
33    /// Ordered property names (as string IDs/hashes).
34    pub properties: Vec<u32>,
35    /// Parent shape in the transition chain (None for root).
36    pub parent: Option<ShapeId>,
37    /// Number of properties (== properties.len(), cached for speed).
38    pub property_count: u16,
39}
40
41/// Manages shape creation and transitions.
42///
43/// Thread-safety: This is NOT thread-safe. For multi-threaded use,
44/// wrap in a Mutex or use thread-local instances.
45pub struct ShapeTransitionTable {
46    /// Transition edges: (parent_shape, property_name_id) -> child_shape
47    transitions: HashMap<(ShapeId, u32), ShapeId>,
48    /// All shapes, indexed by ShapeId
49    shapes: Vec<Shape>,
50    /// Next shape ID to assign
51    next_id: u32,
52}
53
54impl ShapeTransitionTable {
55    /// Create a new transition table with a root shape (id=0, no properties).
56    pub fn new() -> Self {
57        let root = Shape {
58            id: ShapeId(0),
59            properties: Vec::new(),
60            parent: None,
61            property_count: 0,
62        };
63        Self {
64            transitions: HashMap::new(),
65            shapes: vec![root],
66            next_id: 1,
67        }
68    }
69
70    /// Returns the root shape ID (always ShapeId(0)).
71    #[inline]
72    pub fn root() -> ShapeId {
73        ShapeId(0)
74    }
75
76    /// Get a shape by its ID. Panics if the ID is invalid.
77    #[inline]
78    pub fn get_shape(&self, id: ShapeId) -> &Shape {
79        &self.shapes[id.0 as usize]
80    }
81
82    /// Transition from `from` shape by adding `property_name`.
83    ///
84    /// Returns the existing child shape if the transition already exists,
85    /// otherwise creates a new shape with the property appended.
86    pub fn transition(&mut self, from: ShapeId, property_name: u32) -> ShapeId {
87        if let Some(&existing) = self.transitions.get(&(from, property_name)) {
88            return existing;
89        }
90
91        let parent_shape = &self.shapes[from.0 as usize];
92        let mut properties = parent_shape.properties.clone();
93        properties.push(property_name);
94
95        let new_id = ShapeId(self.next_id);
96        self.next_id += 1;
97
98        let new_shape = Shape {
99            id: new_id,
100            properties,
101            parent: Some(from),
102            property_count: self.shapes[from.0 as usize].property_count + 1,
103        };
104
105        self.shapes.push(new_shape);
106        self.transitions.insert((from, property_name), new_id);
107        new_id
108    }
109
110    /// Build a shape by transitioning from root through all keys in order.
111    pub fn shape_for_keys(&mut self, keys: &[u32]) -> ShapeId {
112        let mut current = Self::root();
113        for &key in keys {
114            current = self.transition(current, key);
115        }
116        current
117    }
118
119    /// Find the slot index of `property_name` in the given shape.
120    ///
121    /// Returns `None` if the property is not part of this shape.
122    /// O(n) scan of the shape's properties list.
123    pub fn property_index(&self, shape_id: ShapeId, property_name: u32) -> Option<usize> {
124        let shape = &self.shapes[shape_id.0 as usize];
125        shape.properties.iter().position(|&p| p == property_name)
126    }
127
128    /// Total number of shapes in the table.
129    #[inline]
130    pub fn shape_count(&self) -> usize {
131        self.shapes.len()
132    }
133}
134
135// ── Global shape table ──────────────────────────────────────────────────────
136
137static GLOBAL_SHAPE_TABLE: std::sync::LazyLock<Mutex<ShapeTransitionTable>> =
138    std::sync::LazyLock::new(|| Mutex::new(ShapeTransitionTable::new()));
139
140// ── Shape transition event log ──────────────────────────────────────────────
141//
142// Records (parent_shape, new_shape) pairs whenever a shape transition occurs.
143// The TierManager drains this buffer periodically to detect shape changes
144// that invalidate JIT-compiled shape guards.
145
146static SHAPE_TRANSITION_LOG: std::sync::LazyLock<Mutex<Vec<(ShapeId, ShapeId)>>> =
147    std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
148
149/// Drain all pending shape transition events.
150///
151/// Returns the events accumulated since the last drain. Each event is
152/// `(parent_shape_id, new_child_shape_id)`.
153///
154/// Called by `TierManager::check_shape_invalidations()` during `poll_completions()`.
155pub fn drain_shape_transitions() -> Vec<(ShapeId, ShapeId)> {
156    SHAPE_TRANSITION_LOG
157        .lock()
158        .map(|mut log| std::mem::take(&mut *log))
159        .unwrap_or_default()
160}
161
162/// Compute a ShapeId for a HashMap with the given key hashes (in insertion order).
163///
164/// Uses the global shape transition table. Returns `None` if the lock is poisoned
165/// or if there are more than 64 properties (dictionary mode threshold).
166pub fn shape_for_hashmap_keys(key_hashes: &[u32]) -> Option<ShapeId> {
167    if key_hashes.len() > 64 {
168        return None; // Dictionary mode: too many properties
169    }
170    let mut table = GLOBAL_SHAPE_TABLE.lock().ok()?;
171    Some(table.shape_for_keys(key_hashes))
172}
173
174/// Look up the slot index of a property in a shape.
175///
176/// Uses the global shape transition table.
177pub fn shape_property_index(shape_id: ShapeId, property_hash: u32) -> Option<usize> {
178    let table = GLOBAL_SHAPE_TABLE.lock().ok()?;
179    table.property_index(shape_id, property_hash)
180}
181
182/// Transition a shape by adding a new property.
183///
184/// Uses the global shape transition table. Returns `None` if dictionary mode
185/// threshold (>64 properties) would be exceeded.
186pub fn shape_transition(from: ShapeId, property_hash: u32) -> Option<ShapeId> {
187    let mut table = GLOBAL_SHAPE_TABLE.lock().ok()?;
188    let shape = table.get_shape(from);
189    if shape.property_count >= 64 {
190        return None; // Dictionary mode threshold
191    }
192    let new_id = table.transition(from, property_hash);
193    // Log the transition for JIT shape guard invalidation
194    if let Ok(mut log) = SHAPE_TRANSITION_LOG.lock() {
195        log.push((from, new_id));
196    }
197    Some(new_id)
198}
199
200/// Hash a property name string to a u32 for shape transition keys.
201///
202/// Simple FNV-1a hash truncated to u32.
203#[inline]
204pub fn hash_property_name(name: &str) -> u32 {
205    let mut hash: u32 = 0x811c_9dc5;
206    for byte in name.as_bytes() {
207        hash ^= *byte as u32;
208        hash = hash.wrapping_mul(0x0100_0193);
209    }
210    hash
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_root_shape_exists() {
219        let table = ShapeTransitionTable::new();
220        let root = table.get_shape(ShapeTransitionTable::root());
221        assert_eq!(root.id, ShapeId(0));
222        assert!(root.properties.is_empty());
223        assert_eq!(root.parent, None);
224        assert_eq!(root.property_count, 0);
225    }
226
227    #[test]
228    fn test_transition_creates_new_shape() {
229        let mut table = ShapeTransitionTable::new();
230        let child = table.transition(ShapeTransitionTable::root(), 42);
231        assert_ne!(child, ShapeTransitionTable::root());
232
233        let shape = table.get_shape(child);
234        assert_eq!(shape.properties, vec![42]);
235        assert_eq!(shape.property_count, 1);
236        assert_eq!(shape.parent, Some(ShapeTransitionTable::root()));
237    }
238
239    #[test]
240    fn test_transition_deduplication() {
241        let mut table = ShapeTransitionTable::new();
242        let child1 = table.transition(ShapeTransitionTable::root(), 42);
243        let child2 = table.transition(ShapeTransitionTable::root(), 42);
244        assert_eq!(child1, child2);
245        // Only root + one child should exist
246        assert_eq!(table.shape_count(), 2);
247    }
248
249    #[test]
250    fn test_shape_for_keys() {
251        let mut table = ShapeTransitionTable::new();
252        let shape_id = table.shape_for_keys(&[10, 20, 30]);
253        let shape = table.get_shape(shape_id);
254        assert_eq!(shape.properties, vec![10, 20, 30]);
255        assert_eq!(shape.property_count, 3);
256    }
257
258    #[test]
259    fn test_property_index() {
260        let mut table = ShapeTransitionTable::new();
261        let shape_id = table.shape_for_keys(&[10, 20, 30]);
262        assert_eq!(table.property_index(shape_id, 10), Some(0));
263        assert_eq!(table.property_index(shape_id, 20), Some(1));
264        assert_eq!(table.property_index(shape_id, 30), Some(2));
265    }
266
267    #[test]
268    fn test_property_index_missing() {
269        let mut table = ShapeTransitionTable::new();
270        let shape_id = table.shape_for_keys(&[10, 20]);
271        assert_eq!(table.property_index(shape_id, 99), None);
272    }
273
274    #[test]
275    fn test_multiple_transition_paths() {
276        let mut table = ShapeTransitionTable::new();
277        // Path 1: root -> a -> b
278        let ab = table.shape_for_keys(&[1, 2]);
279        // Path 2: root -> b -> a
280        let ba = table.shape_for_keys(&[2, 1]);
281        // Different property orders must produce different shapes
282        assert_ne!(ab, ba);
283        let shape_ab = table.get_shape(ab);
284        let shape_ba = table.get_shape(ba);
285        assert_eq!(shape_ab.properties, vec![1, 2]);
286        assert_eq!(shape_ba.properties, vec![2, 1]);
287    }
288
289    #[test]
290    fn test_shape_count() {
291        let mut table = ShapeTransitionTable::new();
292        assert_eq!(table.shape_count(), 1); // root only
293        table.transition(ShapeTransitionTable::root(), 1);
294        assert_eq!(table.shape_count(), 2);
295        table.transition(ShapeTransitionTable::root(), 2);
296        assert_eq!(table.shape_count(), 3);
297        // Duplicate transition should not create new shape
298        table.transition(ShapeTransitionTable::root(), 1);
299        assert_eq!(table.shape_count(), 3);
300    }
301
302    #[test]
303    fn test_parent_chain() {
304        let mut table = ShapeTransitionTable::new();
305        let a = table.transition(ShapeTransitionTable::root(), 10);
306        let ab = table.transition(a, 20);
307        let abc = table.transition(ab, 30);
308
309        let shape_abc = table.get_shape(abc);
310        assert_eq!(shape_abc.parent, Some(ab));
311
312        let shape_ab = table.get_shape(ab);
313        assert_eq!(shape_ab.parent, Some(a));
314
315        let shape_a = table.get_shape(a);
316        assert_eq!(shape_a.parent, Some(ShapeTransitionTable::root()));
317    }
318
319    #[test]
320    fn test_shape_id_display() {
321        assert_eq!(format!("{}", ShapeId(0)), "Shape(0)");
322        assert_eq!(format!("{}", ShapeId(42)), "Shape(42)");
323    }
324
325    #[test]
326    fn test_hash_property_name_consistency() {
327        let h1 = hash_property_name("foo");
328        let h2 = hash_property_name("foo");
329        assert_eq!(h1, h2);
330        assert_ne!(hash_property_name("foo"), hash_property_name("bar"));
331    }
332
333    #[test]
334    fn test_global_shape_for_keys() {
335        let keys = &[hash_property_name("x"), hash_property_name("y")];
336        let id1 = shape_for_hashmap_keys(keys).unwrap();
337        let id2 = shape_for_hashmap_keys(keys).unwrap();
338        assert_eq!(id1, id2); // Same keys → same shape
339    }
340
341    #[test]
342    fn test_global_shape_transition() {
343        let root = ShapeTransitionTable::root();
344        let prop = hash_property_name("test_prop");
345        let child = shape_transition(root, prop).unwrap();
346        assert_ne!(child, root);
347    }
348
349    #[test]
350    fn test_dictionary_mode_threshold() {
351        // More than 64 properties → dictionary mode (None)
352        let keys: Vec<u32> = (0..65).collect();
353        assert!(shape_for_hashmap_keys(&keys).is_none());
354    }
355}