we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement inline caches for property access

Add polymorphic inline caches (ICs) to GetPropertyByName and SetPropertyByName
bytecode operations. Each property-access site caches up to 4 (shape, slot_index)
pairs for O(1) fast-path lookups, with megamorphic fallback when more shapes are
seen.

Key changes:
- Define InlineCache with Uninitialized/Monomorphic/Polymorphic/Megamorphic states
- Add Vec<InlineCache> to Function, allocated by the compiler per call site
- Extend GetPropertyByName/SetPropertyByName opcodes with a u16 IC index operand
- VM fast path: on IC hit, bypass shape table lookup and read/write slots directly
- Only own-property hits on shaped objects are cached (not prototype chain)
- IC updates on slow-path misses populate the cache for subsequent iterations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+486 -65
+213 -4
crates/js/src/bytecode.rs
··· 4 4 //! JIT compilation. Instructions are variable-length, encoded as a 1-byte opcode 5 5 //! followed by operand bytes. 6 6 7 + use crate::shape::ShapeId; 7 8 use std::fmt; 8 9 9 10 /// A register index (0–255). ··· 15 16 /// An index into the name/string table. 16 17 pub type NameIdx = u16; 17 18 19 + /// An index into the inline cache vector. 20 + pub type IcIdx = u16; 21 + 22 + /// Maximum number of entries in a polymorphic inline cache before going megamorphic. 23 + const IC_MAX_POLY: usize = 4; 24 + 25 + /// A single (shape, slot) mapping cached at a property-access site. 26 + #[derive(Debug, Clone, Copy)] 27 + pub struct InlineCacheEntry { 28 + pub shape_id: ShapeId, 29 + pub slot_index: u32, 30 + } 31 + 32 + /// State of an inline cache site. 33 + #[derive(Debug, Clone)] 34 + pub enum InlineCacheState { 35 + /// No shape cached yet. 36 + Uninitialized, 37 + /// Exactly one shape seen. 38 + Monomorphic(InlineCacheEntry), 39 + /// 2–4 shapes seen. 40 + Polymorphic(Vec<InlineCacheEntry>), 41 + /// Too many shapes — always use the slow path. 42 + Megamorphic, 43 + } 44 + 45 + /// An inline cache for a single property-access bytecode site. 46 + #[derive(Debug, Clone)] 47 + pub struct InlineCache { 48 + pub state: InlineCacheState, 49 + } 50 + 51 + impl InlineCache { 52 + /// Create an empty (uninitialized) inline cache. 53 + pub fn new() -> Self { 54 + Self { 55 + state: InlineCacheState::Uninitialized, 56 + } 57 + } 58 + 59 + /// Try to find a cached slot index for the given shape. 60 + /// Returns `Some(slot_index)` on a cache hit, `None` on miss. 61 + pub fn lookup(&self, shape: ShapeId) -> Option<u32> { 62 + match &self.state { 63 + InlineCacheState::Uninitialized | InlineCacheState::Megamorphic => None, 64 + InlineCacheState::Monomorphic(entry) => { 65 + if entry.shape_id == shape { 66 + Some(entry.slot_index) 67 + } else { 68 + None 69 + } 70 + } 71 + InlineCacheState::Polymorphic(entries) => { 72 + for entry in entries { 73 + if entry.shape_id == shape { 74 + return Some(entry.slot_index); 75 + } 76 + } 77 + None 78 + } 79 + } 80 + } 81 + 82 + /// Record a (shape, slot_index) mapping after a cache miss. 83 + /// Does nothing if already megamorphic. 84 + pub fn update(&mut self, shape: ShapeId, slot_index: u32) { 85 + match &mut self.state { 86 + InlineCacheState::Megamorphic => {} 87 + InlineCacheState::Uninitialized => { 88 + self.state = InlineCacheState::Monomorphic(InlineCacheEntry { 89 + shape_id: shape, 90 + slot_index, 91 + }); 92 + } 93 + InlineCacheState::Monomorphic(existing) => { 94 + if existing.shape_id == shape { 95 + return; 96 + } 97 + let entries = vec![ 98 + *existing, 99 + InlineCacheEntry { 100 + shape_id: shape, 101 + slot_index, 102 + }, 103 + ]; 104 + self.state = InlineCacheState::Polymorphic(entries); 105 + } 106 + InlineCacheState::Polymorphic(entries) => { 107 + for e in entries.iter() { 108 + if e.shape_id == shape { 109 + return; 110 + } 111 + } 112 + if entries.len() >= IC_MAX_POLY { 113 + self.state = InlineCacheState::Megamorphic; 114 + } else { 115 + entries.push(InlineCacheEntry { 116 + shape_id: shape, 117 + slot_index, 118 + }); 119 + } 120 + } 121 + } 122 + } 123 + 124 + /// Whether this IC has gone megamorphic. 125 + pub fn is_megamorphic(&self) -> bool { 126 + matches!(self.state, InlineCacheState::Megamorphic) 127 + } 128 + } 129 + 130 + impl Default for InlineCache { 131 + fn default() -> Self { 132 + Self::new() 133 + } 134 + } 135 + 18 136 /// Bytecode instruction set. 19 137 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 20 138 #[repr(u8)] ··· 291 409 pub is_generator: bool, 292 410 /// Whether this is an async function. 293 411 pub is_async: bool, 412 + /// Inline caches for property access sites. 413 + pub inline_caches: Vec<InlineCache>, 294 414 } 295 415 296 416 impl Function { ··· 307 427 upvalue_defs: Vec::new(), 308 428 is_generator: false, 309 429 is_async: false, 430 + inline_caches: Vec::new(), 310 431 } 311 432 } 312 433 } ··· 495 616 self.emit_u8(arg_count); 496 617 } 497 618 498 - /// Emit: GetPropertyByName dst, obj, name_idx 619 + /// Allocate a new inline cache slot, returning its index. 620 + pub fn alloc_ic(&mut self) -> IcIdx { 621 + let idx = self.func.inline_caches.len(); 622 + assert!(idx <= u16::MAX as usize, "inline cache overflow"); 623 + self.func.inline_caches.push(InlineCache::new()); 624 + idx as IcIdx 625 + } 626 + 627 + /// Emit: GetPropertyByName dst, obj, name_idx, ic_idx 499 628 pub fn emit_get_prop_name(&mut self, dst: Reg, obj: Reg, name_idx: NameIdx) { 629 + let ic = self.alloc_ic(); 500 630 self.emit_u8(Op::GetPropertyByName as u8); 501 631 self.emit_u8(dst); 502 632 self.emit_u8(obj); 503 633 self.emit_u16(name_idx); 634 + self.emit_u16(ic); 504 635 } 505 636 506 637 /// Emit: PushExceptionHandler catch_reg, offset (placeholder, returns patch position) ··· 517 648 self.emit_u8(Op::PopExceptionHandler as u8); 518 649 } 519 650 520 - /// Emit: SetPropertyByName obj, name_idx, val 651 + /// Emit: SetPropertyByName obj, name_idx, val, ic_idx 521 652 pub fn emit_set_prop_name(&mut self, obj: Reg, name_idx: NameIdx, val: Reg) { 653 + let ic = self.alloc_ic(); 522 654 self.emit_u8(Op::SetPropertyByName as u8); 523 655 self.emit_u8(obj); 524 656 self.emit_u16(name_idx); 525 657 self.emit_u8(val); 658 + self.emit_u16(ic); 526 659 } 527 660 528 661 /// Emit: LoadInt8 dst, value ··· 818 951 pc += 2; 819 952 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]); 820 953 pc += 2; 954 + let ic = u16::from_le_bytes([code[pc], code[pc + 1]]); 955 + pc += 2; 821 956 let name = self 822 957 .names 823 958 .get(idx as usize) 824 959 .map(|s| s.as_str()) 825 960 .unwrap_or("?"); 826 - format!("GetPropertyByName r{dst}, r{obj}, @{idx}(\"{name}\")") 961 + format!("GetPropertyByName r{dst}, r{obj}, @{idx}(\"{name}\"), ic{ic}") 827 962 } 828 963 Op::SetPropertyByName => { 829 964 let obj = code[pc]; ··· 832 967 pc += 2; 833 968 let val = code[pc]; 834 969 pc += 1; 970 + let ic = u16::from_le_bytes([code[pc], code[pc + 1]]); 971 + pc += 2; 835 972 let name = self 836 973 .names 837 974 .get(idx as usize) 838 975 .map(|s| s.as_str()) 839 976 .unwrap_or("?"); 840 - format!("SetPropertyByName r{obj}, @{idx}(\"{name}\"), r{val}") 977 + format!("SetPropertyByName r{obj}, @{idx}(\"{name}\"), r{val}, ic{ic}") 841 978 } 842 979 Op::Delete => { 843 980 let dst = code[pc]; ··· 1115 1252 let dis = func.disassemble(); 1116 1253 assert!(dis.contains("LoadInt8 r0, 42")); 1117 1254 assert!(dis.contains("LoadInt8 r0, -1")); 1255 + } 1256 + 1257 + #[test] 1258 + fn test_inline_cache_uninitialized() { 1259 + let ic = InlineCache::new(); 1260 + assert!(ic.lookup(ShapeId::ROOT).is_none()); 1261 + assert!(!ic.is_megamorphic()); 1262 + } 1263 + 1264 + #[test] 1265 + fn test_inline_cache_monomorphic() { 1266 + let mut ic = InlineCache::new(); 1267 + let shape = ShapeId::ROOT; 1268 + ic.update(shape, 0); 1269 + assert_eq!(ic.lookup(shape), Some(0)); 1270 + assert!(matches!(ic.state, InlineCacheState::Monomorphic(_))); 1271 + } 1272 + 1273 + #[test] 1274 + fn test_inline_cache_polymorphic() { 1275 + use crate::shape::{PropertyAttrs, ShapeTable}; 1276 + let mut table = ShapeTable::new(); 1277 + let s1 = table.add_property(ShapeId::ROOT, "x", PropertyAttrs::DEFAULT); 1278 + let s2 = table.add_property(ShapeId::ROOT, "y", PropertyAttrs::DEFAULT); 1279 + 1280 + let mut ic = InlineCache::new(); 1281 + ic.update(s1, 0); 1282 + ic.update(s2, 0); 1283 + assert!(matches!(ic.state, InlineCacheState::Polymorphic(_))); 1284 + assert_eq!(ic.lookup(s1), Some(0)); 1285 + assert_eq!(ic.lookup(s2), Some(0)); 1286 + } 1287 + 1288 + #[test] 1289 + fn test_inline_cache_megamorphic() { 1290 + use crate::shape::{PropertyAttrs, ShapeTable}; 1291 + let mut table = ShapeTable::new(); 1292 + let shapes: Vec<_> = (0..5) 1293 + .map(|i| table.add_property(ShapeId::ROOT, &format!("p{i}"), PropertyAttrs::DEFAULT)) 1294 + .collect(); 1295 + 1296 + let mut ic = InlineCache::new(); 1297 + for (i, &s) in shapes.iter().enumerate() { 1298 + ic.update(s, i as u32); 1299 + } 1300 + assert!(ic.is_megamorphic()); 1301 + // Megamorphic always returns None. 1302 + for &s in &shapes { 1303 + assert!(ic.lookup(s).is_none()); 1304 + } 1305 + } 1306 + 1307 + #[test] 1308 + fn test_inline_cache_duplicate_update() { 1309 + let mut ic = InlineCache::new(); 1310 + ic.update(ShapeId::ROOT, 0); 1311 + ic.update(ShapeId::ROOT, 0); 1312 + assert!(matches!(ic.state, InlineCacheState::Monomorphic(_))); 1313 + } 1314 + 1315 + #[test] 1316 + fn test_emit_prop_name_allocates_ic() { 1317 + let mut b = BytecodeBuilder::new("test".into(), 0); 1318 + b.func.register_count = 2; 1319 + let ni = b.add_name("x"); 1320 + b.emit_get_prop_name(0, 1, ni); 1321 + b.emit_set_prop_name(1, ni, 0); 1322 + let func = b.finish(); 1323 + assert_eq!(func.inline_caches.len(), 2); 1324 + let dis = func.disassemble(); 1325 + assert!(dis.contains("ic0")); 1326 + assert!(dis.contains("ic1")); 1118 1327 } 1119 1328 }
+273 -61
crates/js/src/vm.rs
··· 803 803 804 804 // ── Property access helpers ────────────────────────────────── 805 805 806 + /// Get an object's own property value using IC, also returning shape+slot for IC 807 + /// update if the property was found as an own property on a shaped object. 808 + fn gc_get_property_ic( 809 + gc: &Gc<HeapObject>, 810 + obj_ref: GcRef, 811 + key: &str, 812 + shapes: &ShapeTable, 813 + ) -> (Value, Option<(ShapeId, u32)>) { 814 + let proto = match gc.get(obj_ref) { 815 + Some(HeapObject::Object(data)) => { 816 + if let ObjectStorage::Shaped { shape, slots } = &data.storage { 817 + if let Some(desc) = shapes.lookup(*shape, key) { 818 + return ( 819 + slots[desc.index as usize].clone(), 820 + Some((*shape, desc.index)), 821 + ); 822 + } 823 + } else if let ObjectStorage::Dictionary(map) = &data.storage { 824 + if let Some(prop) = map.get(key) { 825 + return (prop.value.clone(), None); 826 + } 827 + } 828 + data.prototype 829 + } 830 + Some(HeapObject::Function(fdata)) => { 831 + if let Some(prop) = fdata.properties.get(key) { 832 + return (prop.value.clone(), None); 833 + } 834 + if key == "prototype" { 835 + if let Some(proto_ref) = fdata.prototype_obj { 836 + return (Value::Object(proto_ref), None); 837 + } 838 + return (Value::Undefined, None); 839 + } 840 + None 841 + } 842 + _ => return (Value::Undefined, None), 843 + }; 844 + if let Some(proto_ref) = proto { 845 + // Prototype chain hit — do NOT provide IC info (only own properties). 846 + let val = gc_get_property(gc, proto_ref, key, shapes); 847 + (val, None) 848 + } else { 849 + (Value::Undefined, None) 850 + } 851 + } 852 + 806 853 /// Get a property from an object, walking the prototype chain. 807 854 fn gc_get_property(gc: &Gc<HeapObject>, obj_ref: GcRef, key: &str, shapes: &ShapeTable) -> Value { 808 855 let proto = { ··· 3095 3142 match self.gc.get(func_gc_ref) { 3096 3143 Some(HeapObject::Function(fdata)) => match &fdata.kind { 3097 3144 FunctionKind::Native(n) => CallInfo::Native(n.callback), 3098 - FunctionKind::Bytecode(bc) => { 3099 - CallInfo::Bytecode(bc.func.clone(), fdata.upvalues.clone()) 3100 - } 3145 + FunctionKind::Bytecode(bc) => CallInfo::Bytecode( 3146 + Box::new(bc.func.clone()), 3147 + fdata.upvalues.clone(), 3148 + ), 3101 3149 }, 3102 3150 _ => { 3103 3151 let err = RuntimeError::type_error("not a function"); ··· 3434 3482 } 3435 3483 } 3436 3484 CallInfo::Bytecode(callee_func, callee_upvalues) => { 3485 + let callee_func = *callee_func; 3437 3486 // Async function: create generator + promise, drive async. 3438 3487 if callee_func.is_async && !callee_func.is_generator { 3439 3488 let gen_ref = ··· 3714 3763 let dst = Self::read_u8(&mut self.frames[fi]); 3715 3764 let obj_r = Self::read_u8(&mut self.frames[fi]); 3716 3765 let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; 3766 + let ic_idx = Self::read_u16(&mut self.frames[fi]) as usize; 3717 3767 let base = self.frames[fi].base; 3718 - let key = self.frames[fi].func.names[name_idx].clone(); 3719 - let obj_gc_ref = match self.registers[base + obj_r as usize] { 3720 - Value::Object(r) | Value::Function(r) => Some(r), 3721 - _ => None, 3722 - }; 3723 - let val = match self.registers[base + obj_r as usize] { 3724 - Value::Object(gc_ref) | Value::Function(gc_ref) => { 3725 - gc_get_property(&self.gc, gc_ref, &key, &self.shapes) 3726 - } 3727 - Value::String(ref s) => { 3728 - let v = string_get_property(s, &key); 3729 - if matches!(v, Value::Undefined) { 3730 - self.string_prototype 3731 - .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) 3732 - .unwrap_or(Value::Undefined) 3733 - } else { 3734 - v 3768 + 3769 + // ── IC fast path: shaped object with cached shape ── 3770 + let mut ic_hit = false; 3771 + if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 3772 + if let Some(HeapObject::Object(data)) = self.gc.get(gc_ref) { 3773 + if let ObjectStorage::Shaped { shape, slots } = &data.storage { 3774 + if let Some(slot_idx) = 3775 + self.frames[fi].func.inline_caches[ic_idx].lookup(*shape) 3776 + { 3777 + self.registers[base + dst as usize] = 3778 + slots[slot_idx as usize].clone(); 3779 + ic_hit = true; 3780 + } 3735 3781 } 3736 3782 } 3737 - Value::Number(_) => self 3738 - .number_prototype 3739 - .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) 3740 - .unwrap_or(Value::Undefined), 3741 - Value::Boolean(_) => self 3742 - .boolean_prototype 3743 - .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) 3744 - .unwrap_or(Value::Undefined), 3745 - _ => Value::Undefined, 3746 - }; 3747 - // DOM dynamic property interception. 3748 - let val = match val { 3749 - Value::Undefined if obj_gc_ref.is_some() => self 3750 - .resolve_dom_property(obj_gc_ref.unwrap(), &key) 3751 - .unwrap_or(Value::Undefined), 3752 - other => other, 3753 - }; 3754 - self.registers[base + dst as usize] = val; 3783 + } 3784 + 3785 + if !ic_hit { 3786 + let key = self.frames[fi].func.names[name_idx].clone(); 3787 + let obj_gc_ref = match self.registers[base + obj_r as usize] { 3788 + Value::Object(r) | Value::Function(r) => Some(r), 3789 + _ => None, 3790 + }; 3791 + let val = match self.registers[base + obj_r as usize] { 3792 + Value::Object(gc_ref) => { 3793 + // Try own-property lookup for IC update. 3794 + let (val, own_slot) = 3795 + gc_get_property_ic(&self.gc, gc_ref, &key, &self.shapes); 3796 + if let Some((shape, slot_index)) = own_slot { 3797 + self.frames[fi].func.inline_caches[ic_idx] 3798 + .update(shape, slot_index); 3799 + } 3800 + val 3801 + } 3802 + Value::Function(gc_ref) => { 3803 + gc_get_property(&self.gc, gc_ref, &key, &self.shapes) 3804 + } 3805 + Value::String(ref s) => { 3806 + let v = string_get_property(s, &key); 3807 + if matches!(v, Value::Undefined) { 3808 + self.string_prototype 3809 + .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) 3810 + .unwrap_or(Value::Undefined) 3811 + } else { 3812 + v 3813 + } 3814 + } 3815 + Value::Number(_) => self 3816 + .number_prototype 3817 + .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) 3818 + .unwrap_or(Value::Undefined), 3819 + Value::Boolean(_) => self 3820 + .boolean_prototype 3821 + .map(|p| gc_get_property(&self.gc, p, &key, &self.shapes)) 3822 + .unwrap_or(Value::Undefined), 3823 + _ => Value::Undefined, 3824 + }; 3825 + // DOM dynamic property interception. 3826 + let val = match val { 3827 + Value::Undefined if obj_gc_ref.is_some() => self 3828 + .resolve_dom_property(obj_gc_ref.unwrap(), &key) 3829 + .unwrap_or(Value::Undefined), 3830 + other => other, 3831 + }; 3832 + self.registers[base + dst as usize] = val; 3833 + } 3755 3834 } 3756 3835 Op::SetPropertyByName => { 3757 3836 let obj_r = Self::read_u8(&mut self.frames[fi]); 3758 3837 let name_idx = Self::read_u16(&mut self.frames[fi]) as usize; 3759 3838 let val_r = Self::read_u8(&mut self.frames[fi]); 3839 + let ic_idx = Self::read_u16(&mut self.frames[fi]) as usize; 3760 3840 let base = self.frames[fi].base; 3761 - let key = self.frames[fi].func.names[name_idx].clone(); 3762 - let val = self.registers[base + val_r as usize].clone(); 3763 - let obj_gc = match self.registers[base + obj_r as usize] { 3764 - Value::Object(r) => Some(r), 3765 - Value::Function(r) => Some(r), 3766 - _ => None, 3767 - }; 3768 - let dom_handled = obj_gc 3769 - .map(|r| self.handle_dom_property_set(r, &key, &val)) 3770 - .unwrap_or(false); 3771 - if !dom_handled { 3772 - if let Some(gc_ref) = obj_gc { 3773 - match self.gc.get_mut(gc_ref) { 3774 - Some(HeapObject::Object(data)) => { 3775 - data.set_property(key, val, &mut self.shapes); 3841 + 3842 + // ── IC fast path: shaped object, property exists ── 3843 + let mut ic_hit = false; 3844 + if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 3845 + // Check shape match via IC (read-only borrow first). 3846 + let slot_idx_opt = 3847 + if let Some(HeapObject::Object(data)) = self.gc.get(gc_ref) { 3848 + if let ObjectStorage::Shaped { shape, .. } = &data.storage { 3849 + self.frames[fi].func.inline_caches[ic_idx].lookup(*shape) 3850 + } else { 3851 + None 3776 3852 } 3777 - Some(HeapObject::Function(fdata)) => { 3778 - if let Some(prop) = fdata.properties.get_mut(&key) { 3779 - if prop.writable { 3780 - prop.value = val; 3853 + } else { 3854 + None 3855 + }; 3856 + if let Some(slot_idx) = slot_idx_opt { 3857 + let val = self.registers[base + val_r as usize].clone(); 3858 + if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 3859 + if let ObjectStorage::Shaped { slots, .. } = &mut data.storage { 3860 + slots[slot_idx as usize] = val; 3861 + ic_hit = true; 3862 + } 3863 + } 3864 + } 3865 + } 3866 + 3867 + if !ic_hit { 3868 + let key = self.frames[fi].func.names[name_idx].clone(); 3869 + let val = self.registers[base + val_r as usize].clone(); 3870 + let obj_gc = match self.registers[base + obj_r as usize] { 3871 + Value::Object(r) => Some(r), 3872 + Value::Function(r) => Some(r), 3873 + _ => None, 3874 + }; 3875 + let dom_handled = obj_gc 3876 + .map(|r| self.handle_dom_property_set(r, &key, &val)) 3877 + .unwrap_or(false); 3878 + if !dom_handled { 3879 + if let Some(gc_ref) = obj_gc { 3880 + match self.gc.get_mut(gc_ref) { 3881 + Some(HeapObject::Object(data)) => { 3882 + // Get shape before mutation for IC update. 3883 + let shape_before = 3884 + if let ObjectStorage::Shaped { shape, .. } = 3885 + &data.storage 3886 + { 3887 + Some(*shape) 3888 + } else { 3889 + None 3890 + }; 3891 + data.set_property(key.clone(), val, &mut self.shapes); 3892 + // Update IC: cache the slot if property existed 3893 + // (shape didn't change). 3894 + if let ObjectStorage::Shaped { shape, .. } = &data.storage { 3895 + if shape_before == Some(*shape) { 3896 + if let Some(desc) = self.shapes.lookup(*shape, &key) 3897 + { 3898 + self.frames[fi].func.inline_caches[ic_idx] 3899 + .update(*shape, desc.index); 3900 + } 3901 + } 3781 3902 } 3782 - } else { 3783 - fdata.properties.insert(key, Property::data(val)); 3784 3903 } 3904 + Some(HeapObject::Function(fdata)) => { 3905 + if let Some(prop) = fdata.properties.get_mut(&key) { 3906 + if prop.writable { 3907 + prop.value = val; 3908 + } 3909 + } else { 3910 + fdata.properties.insert(key, Property::data(val)); 3911 + } 3912 + } 3913 + _ => {} 3785 3914 } 3786 - _ => {} 3787 3915 } 3788 3916 } 3789 3917 } ··· 4199 4327 /// Internal enum to avoid holding a GC borrow across the call setup. 4200 4328 enum CallInfo { 4201 4329 Native(fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>), 4202 - Bytecode(Function, Vec<GcRef>), 4330 + Bytecode(Box<Function>, Vec<GcRef>), 4203 4331 } 4204 4332 4205 4333 // ── Generator native callbacks ────────────────────────────── ··· 8588 8716 match result.unwrap() { 8589 8717 Value::String(s) => assert_eq!(s, "ok"), 8590 8718 v => panic!("expected 'ok', got {v:?}"), 8719 + } 8720 + } 8721 + 8722 + // ── Inline cache tests ───────────────────────────────────── 8723 + 8724 + #[test] 8725 + fn test_ic_property_read_in_loop() { 8726 + // Repeated property access on same-shaped objects should use IC fast path. 8727 + let result = eval( 8728 + "var sum = 0; 8729 + for (var i = 0; i < 10; i++) { 8730 + var obj = {x: i}; 8731 + sum = sum + obj.x; 8732 + } 8733 + sum", 8734 + ); 8735 + match result.unwrap() { 8736 + Value::Number(n) => assert_eq!(n, 45.0), 8737 + v => panic!("expected 45, got {v:?}"), 8738 + } 8739 + } 8740 + 8741 + #[test] 8742 + fn test_ic_property_write_in_loop() { 8743 + // Repeated property writes on same-shaped objects. 8744 + let result = eval( 8745 + "var obj = {x: 0}; 8746 + for (var i = 0; i < 100; i++) { 8747 + obj.x = i; 8748 + } 8749 + obj.x", 8750 + ); 8751 + match result.unwrap() { 8752 + Value::Number(n) => assert_eq!(n, 99.0), 8753 + v => panic!("expected 99, got {v:?}"), 8754 + } 8755 + } 8756 + 8757 + #[test] 8758 + fn test_ic_different_shapes() { 8759 + // Property access on objects with different shapes still works correctly. 8760 + let result = eval( 8761 + "function getX(o) { return o.x; } 8762 + var a = {x: 1}; 8763 + var b = {y: 2, x: 3}; 8764 + var c = {z: 4, x: 5}; 8765 + getX(a) + getX(b) + getX(c)", 8766 + ); 8767 + match result.unwrap() { 8768 + Value::Number(n) => assert_eq!(n, 9.0), 8769 + v => panic!("expected 9, got {v:?}"), 8770 + } 8771 + } 8772 + 8773 + #[test] 8774 + fn test_ic_object_with_prototype() { 8775 + // Property access on objects with prototypes should still work 8776 + // (prototype chain properties are not cached by IC, but should resolve). 8777 + let result = eval( 8778 + "var proto = {x: 10}; 8779 + var obj = Object.create(proto); 8780 + obj.y = 20; 8781 + obj.x + obj.y", 8782 + ); 8783 + match result.unwrap() { 8784 + Value::Number(n) => assert_eq!(n, 30.0), 8785 + v => panic!("expected 30, got {v:?}"), 8786 + } 8787 + } 8788 + 8789 + #[test] 8790 + fn test_ic_multiple_properties() { 8791 + // Multiple property accesses in a loop on the same object. 8792 + let result = eval( 8793 + "var obj = {a: 1, b: 2, c: 3}; 8794 + var sum = 0; 8795 + for (var i = 0; i < 10; i++) { 8796 + sum = sum + obj.a + obj.b + obj.c; 8797 + } 8798 + sum", 8799 + ); 8800 + match result.unwrap() { 8801 + Value::Number(n) => assert_eq!(n, 60.0), 8802 + v => panic!("expected 60, got {v:?}"), 8591 8803 } 8592 8804 } 8593 8805 }