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 JS object model: property descriptors, prototype chain, operators

Add property descriptors with writable/enumerable/configurable flags,
replacing the plain HashMap<String, Value> with HashMap<String, Property>.
This gives correct JavaScript semantics for property access, deletion,
and enumeration.

Key changes:
- Property struct with data descriptor flags (writable, enumerable, configurable)
- ObjectData gains extensible flag for future Object.preventExtensions
- FunctionData gains prototype_obj for instanceof support
- instanceof walks the prototype chain checking constructor.prototype
- delete respects the configurable flag (non-configurable = returns false)
- Compiler properly emits Delete bytecode for member expressions
- for-in loops fully implemented with ForInInit/ForInNext bytecode ops
- Property enumeration: integer indices first (sorted), then string keys
- Non-enumerable properties (e.g. array length) excluded from for-in
- SetPrototype/GetPrototype bytecode ops for prototype manipulation
- CreateClosure creates a .prototype object with constructor back-reference

15 new tests covering property descriptors, prototype chain, typeof,
instanceof, in, delete, for-in, enumeration order, and reference semantics.
All 241 JS tests pass (was 226).

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

+741 -43
+51
crates/js/src/bytecode.rs
··· 140 140 Delete = 0x70, 141 141 /// LoadInt8 dst, i8 — load small integer without constant pool 142 142 LoadInt8 = 0x71, 143 + /// ForInInit dst_keys, obj_reg — collect enumerable keys of object into an array 144 + ForInInit = 0x72, 145 + /// ForInNext dst_val, dst_done, keys_reg, idx_reg — get next key or signal done 146 + ForInNext = 0x73, 147 + /// SetPrototype obj_reg, proto_reg — set [[Prototype]] of obj to proto 148 + SetPrototype = 0x74, 149 + /// GetPrototype dst, obj_reg — get [[Prototype]] of obj 150 + GetPrototype = 0x75, 143 151 } 144 152 145 153 impl Op { ··· 197 205 0x65 => Some(Op::SetPropertyByName), 198 206 0x70 => Some(Op::Delete), 199 207 0x71 => Some(Op::LoadInt8), 208 + 0x72 => Some(Op::ForInInit), 209 + 0x73 => Some(Op::ForInNext), 210 + 0x74 => Some(Op::SetPrototype), 211 + 0x75 => Some(Op::GetPrototype), 200 212 _ => None, 201 213 } 202 214 } ··· 328 340 self.emit_u8(op as u8); 329 341 self.emit_u8(a); 330 342 self.emit_u8(b); 343 + } 344 + 345 + /// Emit: ForInNext dst_val, dst_done, keys, idx (4-register instruction) 346 + pub fn emit_reg4(&mut self, op: Op, a: Reg, b: Reg, c: Reg, d: Reg) { 347 + self.emit_u8(op as u8); 348 + self.emit_u8(a); 349 + self.emit_u8(b); 350 + self.emit_u8(c); 351 + self.emit_u8(d); 331 352 } 332 353 333 354 /// Emit: Add dst, lhs, rhs (and other 3-register instructions) ··· 728 749 pc += 2; 729 750 format!("LoadInt8 r{dst}, {val}") 730 751 } 752 + Op::ForInInit => { 753 + let dst = code[pc]; 754 + let obj = code[pc + 1]; 755 + pc += 2; 756 + format!("ForInInit r{dst}, r{obj}") 757 + } 758 + Op::ForInNext => { 759 + let dst_val = code[pc]; 760 + let dst_done = code[pc + 1]; 761 + let keys = code[pc + 2]; 762 + let idx = code[pc + 3]; 763 + pc += 4; 764 + format!("ForInNext r{dst_val}, r{dst_done}, r{keys}, r{idx}") 765 + } 766 + Op::SetPrototype => { 767 + let obj = code[pc]; 768 + let proto = code[pc + 1]; 769 + pc += 2; 770 + format!("SetPrototype r{obj}, r{proto}") 771 + } 772 + Op::GetPrototype => { 773 + let dst = code[pc]; 774 + let obj = code[pc + 1]; 775 + pc += 2; 776 + format!("GetPrototype r{dst}, r{obj}") 777 + } 731 778 }; 732 779 out.push_str(&format!(" {offset:04X} {line}\n")); 733 780 } ··· 810 857 Op::SetPropertyByName, 811 858 Op::Delete, 812 859 Op::LoadInt8, 860 + Op::ForInInit, 861 + Op::ForInNext, 862 + Op::SetPrototype, 863 + Op::GetPrototype, 813 864 ]; 814 865 for op in ops { 815 866 assert_eq!(
+121 -22
crates/js/src/compiler.rs
··· 172 172 } 173 173 174 174 StmtKind::ForIn { left, right, body } => { 175 - // For-in is complex; emit a stub that evaluates RHS, then TODO at runtime. 176 - let _ = left; 177 - let tmp = fc.alloc_reg(); 178 - compile_expr(fc, right, tmp)?; 179 - fc.free_reg(tmp); 180 - // For now, just compile the body once (the VM will handle iteration). 175 + let saved_locals = fc.locals.len(); 176 + let saved_next = fc.next_reg; 177 + 178 + // Evaluate the RHS object. 179 + let obj_r = fc.alloc_reg(); 180 + compile_expr(fc, right, obj_r)?; 181 + 182 + // ForInInit: collect enumerable keys into an array. 183 + let keys_r = fc.alloc_reg(); 184 + fc.builder.emit_reg_reg(Op::ForInInit, keys_r, obj_r); 185 + // obj_r is no longer needed but we don't free it (LIFO constraint). 186 + 187 + // Index register (starts at 0). 188 + let idx_r = fc.alloc_reg(); 189 + fc.builder.emit_load_int8(idx_r, 0); 190 + 191 + // Key and done registers (reused each iteration). 192 + let key_r = fc.alloc_reg(); 193 + let done_r = fc.alloc_reg(); 194 + 195 + let loop_start = fc.builder.offset(); 196 + 197 + // ForInNext: get next key or done flag. 198 + fc.builder 199 + .emit_reg4(Op::ForInNext, key_r, done_r, keys_r, idx_r); 200 + 201 + // Exit loop if done. 202 + let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, done_r); 203 + 204 + // Bind the loop variable. 205 + match left { 206 + ForInOfLeft::VarDecl { kind: _, pattern } => { 207 + if let PatternKind::Identifier(name) = &pattern.kind { 208 + let var_r = fc.define_local(name); 209 + fc.builder.emit_reg_reg(Op::Move, var_r, key_r); 210 + } 211 + } 212 + ForInOfLeft::Pattern(pattern) => { 213 + if let PatternKind::Identifier(name) = &pattern.kind { 214 + if let Some(local) = fc.find_local(name) { 215 + fc.builder.emit_reg_reg(Op::Move, local, key_r); 216 + } else { 217 + let ni = fc.builder.add_name(name); 218 + fc.builder.emit_store_global(ni, key_r); 219 + } 220 + } 221 + } 222 + } 223 + 224 + // Compile the body. 225 + fc.loop_stack.push(LoopCtx { 226 + label: None, 227 + break_patches: Vec::new(), 228 + continue_patches: Vec::new(), 229 + }); 181 230 compile_stmt(fc, body, result_reg)?; 231 + 232 + // Increment index: idx = idx + 1. 233 + // Use a temp register for the constant 1. Since we allocate it 234 + // after the loop body, we can't free it with LIFO either — the 235 + // saved_next restoration handles cleanup. 236 + let one_r = fc.alloc_reg(); 237 + fc.builder.emit_load_int8(one_r, 1); 238 + fc.builder.emit_reg3(Op::Add, idx_r, idx_r, one_r); 239 + 240 + // Jump back to loop start. 241 + fc.builder.emit_jump_to(loop_start); 242 + 243 + // Patch exit and continue jumps. 244 + fc.builder.patch_jump(exit_patch); 245 + let ctx = fc.loop_stack.pop().unwrap(); 246 + for patch in ctx.break_patches { 247 + fc.builder.patch_jump(patch); 248 + } 249 + for patch in ctx.continue_patches { 250 + fc.builder.patch_jump_to(patch, loop_start); 251 + } 252 + 253 + // Restore locals/regs — frees all temporaries at once. 254 + fc.locals.truncate(saved_locals); 255 + fc.next_reg = saved_next; 182 256 } 183 257 184 258 StmtKind::ForOf { ··· 884 958 } 885 959 886 960 ExprKind::Unary { op, argument } => { 887 - let src = fc.alloc_reg(); 888 - compile_expr(fc, argument, src)?; 889 - match op { 890 - UnaryOp::Minus => fc.builder.emit_reg_reg(Op::Neg, dst, src), 891 - UnaryOp::Plus => { 892 - // Unary + is a no-op at the bytecode level (coerces to number at runtime). 893 - fc.builder.emit_reg_reg(Op::Move, dst, src); 961 + if *op == UnaryOp::Delete { 962 + // Handle delete specially: need object + key form for member expressions. 963 + match &argument.kind { 964 + ExprKind::Member { 965 + object, 966 + property, 967 + computed, 968 + } => { 969 + let obj_r = fc.alloc_reg(); 970 + compile_expr(fc, object, obj_r)?; 971 + let key_r = fc.alloc_reg(); 972 + if *computed { 973 + compile_expr(fc, property, key_r)?; 974 + } else if let ExprKind::Identifier(name) = &property.kind { 975 + let ci = fc.builder.add_constant(Constant::String(name.clone())); 976 + fc.builder.emit_reg_u16(Op::LoadConst, key_r, ci); 977 + } else { 978 + compile_expr(fc, property, key_r)?; 979 + } 980 + fc.builder.emit_reg3(Op::Delete, dst, obj_r, key_r); 981 + fc.free_reg(key_r); 982 + fc.free_reg(obj_r); 983 + } 984 + _ => { 985 + // `delete x` on a simple identifier: always true in non-strict mode. 986 + fc.builder.emit_reg(Op::LoadTrue, dst); 987 + } 894 988 } 895 - UnaryOp::Not => fc.builder.emit_reg_reg(Op::LogicalNot, dst, src), 896 - UnaryOp::BitwiseNot => fc.builder.emit_reg_reg(Op::BitNot, dst, src), 897 - UnaryOp::Typeof => fc.builder.emit_reg_reg(Op::TypeOf, dst, src), 898 - UnaryOp::Void => fc.builder.emit_reg_reg(Op::Void, dst, src), 899 - UnaryOp::Delete => { 900 - // Simplified: `delete x` on a simple identifier. 901 - // Real delete needs the object+key form. 902 - fc.builder.emit_reg(Op::LoadTrue, dst); 989 + } else { 990 + let src = fc.alloc_reg(); 991 + compile_expr(fc, argument, src)?; 992 + match op { 993 + UnaryOp::Minus => fc.builder.emit_reg_reg(Op::Neg, dst, src), 994 + UnaryOp::Plus => { 995 + fc.builder.emit_reg_reg(Op::Move, dst, src); 996 + } 997 + UnaryOp::Not => fc.builder.emit_reg_reg(Op::LogicalNot, dst, src), 998 + UnaryOp::BitwiseNot => fc.builder.emit_reg_reg(Op::BitNot, dst, src), 999 + UnaryOp::Typeof => fc.builder.emit_reg_reg(Op::TypeOf, dst, src), 1000 + UnaryOp::Void => fc.builder.emit_reg_reg(Op::Void, dst, src), 1001 + UnaryOp::Delete => unreachable!(), 903 1002 } 1003 + fc.free_reg(src); 904 1004 } 905 - fc.free_reg(src); 906 1005 } 907 1006 908 1007 ExprKind::Update {
+569 -21
crates/js/src/vm.rs
··· 22 22 fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { 23 23 match self { 24 24 HeapObject::Object(data) => { 25 - for val in data.properties.values() { 26 - if let Some(r) = val.gc_ref() { 25 + for prop in data.properties.values() { 26 + if let Some(r) = prop.value.gc_ref() { 27 27 visitor(r); 28 28 } 29 29 } ··· 31 31 visitor(proto); 32 32 } 33 33 } 34 - HeapObject::Function(_) => { 35 - // Bytecode and native callbacks don't hold GC references. 34 + HeapObject::Function(fdata) => { 35 + if let Some(proto) = fdata.prototype_obj { 36 + visitor(proto); 37 + } 36 38 } 37 39 } 38 40 } 39 41 } 40 42 41 - /// A JS plain object (properties stored as a HashMap). 43 + /// A property descriptor stored in an object's property map. 44 + #[derive(Clone)] 45 + pub struct Property { 46 + /// The property's value (for data properties). 47 + pub value: Value, 48 + /// Whether the value can be changed via assignment. 49 + pub writable: bool, 50 + /// Whether the property shows up in `for...in` and `Object.keys`. 51 + pub enumerable: bool, 52 + /// Whether the property can be deleted or its attributes changed. 53 + pub configurable: bool, 54 + } 55 + 56 + impl Property { 57 + /// Create a new data property with all flags set to true (the JS default for 58 + /// properties created by assignment). 59 + pub fn data(value: Value) -> Self { 60 + Self { 61 + value, 62 + writable: true, 63 + enumerable: true, 64 + configurable: true, 65 + } 66 + } 67 + 68 + /// Create a non-enumerable, non-configurable property (e.g. built-in methods). 69 + pub fn builtin(value: Value) -> Self { 70 + Self { 71 + value, 72 + writable: true, 73 + enumerable: false, 74 + configurable: false, 75 + } 76 + } 77 + } 78 + 79 + /// A JS plain object (properties stored as a HashMap with descriptors). 42 80 pub struct ObjectData { 43 - pub properties: HashMap<String, Value>, 81 + pub properties: HashMap<String, Property>, 44 82 pub prototype: Option<GcRef>, 83 + /// Whether new properties can be added (Object.preventExtensions). 84 + pub extensible: bool, 45 85 } 46 86 47 87 impl ObjectData { ··· 49 89 Self { 50 90 properties: HashMap::new(), 51 91 prototype: None, 92 + extensible: true, 52 93 } 53 94 } 54 95 } ··· 63 104 pub struct FunctionData { 64 105 pub name: String, 65 106 pub kind: FunctionKind, 107 + /// The `.prototype` property object (for use as a constructor with `instanceof`). 108 + pub prototype_obj: Option<GcRef>, 66 109 } 67 110 68 111 #[derive(Clone)] ··· 313 356 /// Convert to a JS Value (an error object). Allocates through the GC. 314 357 pub fn to_value(&self, gc: &mut Gc<HeapObject>) -> Value { 315 358 let mut obj = ObjectData::new(); 316 - obj.properties 317 - .insert("message".to_string(), Value::String(self.message.clone())); 359 + obj.properties.insert( 360 + "message".to_string(), 361 + Property::data(Value::String(self.message.clone())), 362 + ); 318 363 let name = match self.kind { 319 364 ErrorKind::TypeError => "TypeError", 320 365 ErrorKind::ReferenceError => "ReferenceError", ··· 322 367 ErrorKind::SyntaxError => "SyntaxError", 323 368 ErrorKind::Error => "Error", 324 369 }; 325 - obj.properties 326 - .insert("name".to_string(), Value::String(name.to_string())); 370 + obj.properties.insert( 371 + "name".to_string(), 372 + Property::data(Value::String(name.to_string())), 373 + ); 327 374 Value::Object(gc.alloc(HeapObject::Object(obj))) 328 375 } 329 376 } ··· 335 382 let proto = { 336 383 match gc.get(obj_ref) { 337 384 Some(HeapObject::Object(data)) => { 338 - if let Some(val) = data.properties.get(key) { 339 - return val.clone(); 385 + if let Some(prop) = data.properties.get(key) { 386 + return prop.value.clone(); 340 387 } 341 388 data.prototype 342 389 } 390 + Some(HeapObject::Function(fdata)) => { 391 + // Functions have a `.prototype` property. 392 + if key == "prototype" { 393 + if let Some(proto_ref) = fdata.prototype_obj { 394 + return Value::Object(proto_ref); 395 + } 396 + return Value::Undefined; 397 + } 398 + None 399 + } 343 400 _ => return Value::Undefined, 344 401 } 345 402 }; ··· 368 425 } else { 369 426 false 370 427 } 428 + } 429 + 430 + /// Collect all enumerable string keys of an object (own + inherited), in proper order. 431 + /// Integer indices first (sorted numerically), then string keys in insertion order. 432 + fn gc_enumerate_keys(gc: &Gc<HeapObject>, obj_ref: GcRef) -> Vec<String> { 433 + let mut seen = std::collections::HashSet::new(); 434 + let mut integer_keys: Vec<(u32, String)> = Vec::new(); 435 + let mut string_keys: Vec<String> = Vec::new(); 436 + let mut current = Some(obj_ref); 437 + 438 + while let Some(cur_ref) = current { 439 + match gc.get(cur_ref) { 440 + Some(HeapObject::Object(data)) => { 441 + for (key, prop) in &data.properties { 442 + if prop.enumerable && seen.insert(key.clone()) { 443 + if let Ok(idx) = key.parse::<u32>() { 444 + integer_keys.push((idx, key.clone())); 445 + } else { 446 + string_keys.push(key.clone()); 447 + } 448 + } 449 + } 450 + current = data.prototype; 451 + } 452 + _ => break, 453 + } 454 + } 455 + 456 + // Integer indices sorted numerically first, then string keys in collected order. 457 + integer_keys.sort_by_key(|(idx, _)| *idx); 458 + let mut result: Vec<String> = integer_keys.into_iter().map(|(_, k)| k).collect(); 459 + result.extend(string_keys); 460 + result 461 + } 462 + 463 + /// Check if `obj_ref` is an instance of the constructor at `ctor_ref`. 464 + /// Walks the prototype chain of `obj_ref` looking for `ctor.prototype`. 465 + fn gc_instanceof(gc: &Gc<HeapObject>, obj_ref: GcRef, ctor_ref: GcRef) -> bool { 466 + // Get the constructor's .prototype object. 467 + let ctor_proto = match gc.get(ctor_ref) { 468 + Some(HeapObject::Function(fdata)) => match fdata.prototype_obj { 469 + Some(p) => p, 470 + None => return false, 471 + }, 472 + _ => return false, 473 + }; 474 + 475 + // Walk the prototype chain of obj_ref. 476 + let mut current = match gc.get(obj_ref) { 477 + Some(HeapObject::Object(data)) => data.prototype, 478 + _ => None, 479 + }; 480 + 481 + while let Some(proto_ref) = current { 482 + if proto_ref == ctor_proto { 483 + return true; 484 + } 485 + current = match gc.get(proto_ref) { 486 + Some(HeapObject::Object(data)) => data.prototype, 487 + _ => None, 488 + }; 489 + } 490 + false 371 491 } 372 492 373 493 /// Get a string property (length, index access). ··· 868 988 } 869 989 Op::InstanceOf => { 870 990 let dst = Self::read_u8(&mut self.frames[fi]); 871 - let _lhs_r = Self::read_u8(&mut self.frames[fi]); 872 - let _rhs_r = Self::read_u8(&mut self.frames[fi]); 991 + let lhs_r = Self::read_u8(&mut self.frames[fi]); 992 + let rhs_r = Self::read_u8(&mut self.frames[fi]); 873 993 let base = self.frames[fi].base; 874 - // Simplified: always false (needs constructor/prototype support). 875 - self.registers[base + dst as usize] = Value::Boolean(false); 994 + let result = match ( 995 + &self.registers[base + lhs_r as usize], 996 + &self.registers[base + rhs_r as usize], 997 + ) { 998 + (Value::Object(obj_ref), Value::Function(ctor_ref)) => { 999 + gc_instanceof(&self.gc, *obj_ref, *ctor_ref) 1000 + } 1001 + (_, Value::Function(_)) => false, 1002 + _ => { 1003 + return Err(RuntimeError::type_error( 1004 + "Right-hand side of instanceof is not callable", 1005 + )); 1006 + } 1007 + }; 1008 + self.registers[base + dst as usize] = Value::Boolean(result); 876 1009 } 877 1010 Op::In => { 878 1011 let dst = Self::read_u8(&mut self.frames[fi]); ··· 1054 1187 let base = self.frames[fi].base; 1055 1188 let inner_func = self.frames[fi].func.functions[func_idx].clone(); 1056 1189 let name = inner_func.name.clone(); 1190 + // Create a .prototype object for the function (for instanceof). 1191 + let proto_obj = self.gc.alloc(HeapObject::Object(ObjectData::new())); 1057 1192 let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { 1058 1193 name, 1059 1194 kind: FunctionKind::Bytecode(BytecodeFunc { func: inner_func }), 1195 + prototype_obj: Some(proto_obj), 1060 1196 })); 1197 + // Set .prototype.constructor = this function. 1198 + if let Some(HeapObject::Object(data)) = self.gc.get_mut(proto_obj) { 1199 + data.properties.insert( 1200 + "constructor".to_string(), 1201 + Property { 1202 + value: Value::Function(gc_ref), 1203 + writable: true, 1204 + enumerable: false, 1205 + configurable: true, 1206 + }, 1207 + ); 1208 + } 1061 1209 self.registers[base + dst as usize] = Value::Function(gc_ref); 1062 1210 1063 1211 // Trigger GC if needed. ··· 1090 1238 let val = self.registers[base + val_r as usize].clone(); 1091 1239 if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1092 1240 if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1093 - data.properties.insert(key, val); 1241 + if let Some(prop) = data.properties.get_mut(&key) { 1242 + if prop.writable { 1243 + prop.value = val; 1244 + } 1245 + } else { 1246 + data.properties.insert(key, Property::data(val)); 1247 + } 1094 1248 } 1095 1249 } 1096 1250 } ··· 1109 1263 let dst = Self::read_u8(&mut self.frames[fi]); 1110 1264 let base = self.frames[fi].base; 1111 1265 let mut obj = ObjectData::new(); 1112 - obj.properties 1113 - .insert("length".to_string(), Value::Number(0.0)); 1266 + obj.properties.insert( 1267 + "length".to_string(), 1268 + Property { 1269 + value: Value::Number(0.0), 1270 + writable: true, 1271 + enumerable: false, 1272 + configurable: false, 1273 + }, 1274 + ); 1114 1275 let gc_ref = self.gc.alloc(HeapObject::Object(obj)); 1115 1276 self.registers[base + dst as usize] = Value::Object(gc_ref); 1116 1277 ··· 1141 1302 let val = self.registers[base + val_r as usize].clone(); 1142 1303 if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1143 1304 if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1144 - data.properties.insert(key, val); 1305 + if let Some(prop) = data.properties.get_mut(&key) { 1306 + if prop.writable { 1307 + prop.value = val; 1308 + } 1309 + } else { 1310 + data.properties.insert(key, Property::data(val)); 1311 + } 1145 1312 } 1146 1313 } 1147 1314 } ··· 1156 1323 let result = 1157 1324 if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1158 1325 if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1159 - data.properties.remove(&key).is_some() 1326 + match data.properties.get(&key) { 1327 + Some(prop) if !prop.configurable => false, 1328 + Some(_) => { 1329 + data.properties.remove(&key); 1330 + true 1331 + } 1332 + None => true, 1333 + } 1160 1334 } else { 1161 1335 true 1162 1336 } ··· 1165 1339 }; 1166 1340 self.registers[base + dst as usize] = Value::Boolean(result); 1167 1341 } 1342 + Op::ForInInit => { 1343 + let dst = Self::read_u8(&mut self.frames[fi]); 1344 + let obj_r = Self::read_u8(&mut self.frames[fi]); 1345 + let base = self.frames[fi].base; 1346 + let keys = match self.registers[base + obj_r as usize] { 1347 + Value::Object(gc_ref) => gc_enumerate_keys(&self.gc, gc_ref), 1348 + _ => Vec::new(), 1349 + }; 1350 + // Store keys as an array object. 1351 + let mut arr = ObjectData::new(); 1352 + for (i, key) in keys.iter().enumerate() { 1353 + arr.properties 1354 + .insert(i.to_string(), Property::data(Value::String(key.clone()))); 1355 + } 1356 + arr.properties.insert( 1357 + "length".to_string(), 1358 + Property { 1359 + value: Value::Number(keys.len() as f64), 1360 + writable: true, 1361 + enumerable: false, 1362 + configurable: false, 1363 + }, 1364 + ); 1365 + let gc_ref = self.gc.alloc(HeapObject::Object(arr)); 1366 + self.registers[base + dst as usize] = Value::Object(gc_ref); 1367 + } 1368 + Op::ForInNext => { 1369 + let dst_val = Self::read_u8(&mut self.frames[fi]); 1370 + let dst_done = Self::read_u8(&mut self.frames[fi]); 1371 + let keys_r = Self::read_u8(&mut self.frames[fi]); 1372 + let idx_r = Self::read_u8(&mut self.frames[fi]); 1373 + let base = self.frames[fi].base; 1374 + let idx = self.registers[base + idx_r as usize].to_number() as usize; 1375 + let len = match self.registers[base + keys_r as usize] { 1376 + Value::Object(gc_ref) => { 1377 + gc_get_property(&self.gc, gc_ref, "length").to_number() as usize 1378 + } 1379 + _ => 0, 1380 + }; 1381 + if idx >= len { 1382 + self.registers[base + dst_done as usize] = Value::Boolean(true); 1383 + self.registers[base + dst_val as usize] = Value::Undefined; 1384 + } else { 1385 + let key_str = idx.to_string(); 1386 + let key = match self.registers[base + keys_r as usize] { 1387 + Value::Object(gc_ref) => gc_get_property(&self.gc, gc_ref, &key_str), 1388 + _ => Value::Undefined, 1389 + }; 1390 + self.registers[base + dst_val as usize] = key; 1391 + self.registers[base + dst_done as usize] = Value::Boolean(false); 1392 + } 1393 + } 1394 + Op::SetPrototype => { 1395 + let obj_r = Self::read_u8(&mut self.frames[fi]); 1396 + let proto_r = Self::read_u8(&mut self.frames[fi]); 1397 + let base = self.frames[fi].base; 1398 + let proto = match &self.registers[base + proto_r as usize] { 1399 + Value::Object(r) => Some(*r), 1400 + Value::Null => None, 1401 + _ => None, 1402 + }; 1403 + if let Value::Object(gc_ref) = self.registers[base + obj_r as usize] { 1404 + if let Some(HeapObject::Object(data)) = self.gc.get_mut(gc_ref) { 1405 + data.prototype = proto; 1406 + } 1407 + } 1408 + } 1409 + Op::GetPrototype => { 1410 + let dst = Self::read_u8(&mut self.frames[fi]); 1411 + let obj_r = Self::read_u8(&mut self.frames[fi]); 1412 + let base = self.frames[fi].base; 1413 + let proto = match self.registers[base + obj_r as usize] { 1414 + Value::Object(gc_ref) => match self.gc.get(gc_ref) { 1415 + Some(HeapObject::Object(data)) => { 1416 + data.prototype.map(Value::Object).unwrap_or(Value::Null) 1417 + } 1418 + _ => Value::Null, 1419 + }, 1420 + _ => Value::Null, 1421 + }; 1422 + self.registers[base + dst as usize] = proto; 1423 + } 1168 1424 } 1169 1425 } 1170 1426 } ··· 1205 1461 let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { 1206 1462 name: name.to_string(), 1207 1463 kind: FunctionKind::Native(NativeFunc { callback }), 1464 + prototype_obj: None, 1208 1465 })); 1209 1466 self.globals 1210 1467 .insert(name.to_string(), Value::Function(gc_ref)); ··· 1778 2035 let a = Value::Object(r); 1779 2036 let b = a.clone(); 1780 2037 assert!(strict_eq(&a, &b)); // Same GcRef → strict equal. 2038 + } 2039 + 2040 + // ── Object model tests ────────────────────────────────── 2041 + 2042 + #[test] 2043 + fn test_prototype_chain_lookup() { 2044 + // Property lookup walks the prototype chain. 2045 + let src = r#" 2046 + function Animal() {} 2047 + var a = {}; 2048 + a.sound = "woof"; 2049 + a.sound 2050 + "#; 2051 + match eval(src).unwrap() { 2052 + Value::String(s) => assert_eq!(s, "woof"), 2053 + v => panic!("expected 'woof', got {v:?}"), 2054 + } 2055 + } 2056 + 2057 + #[test] 2058 + fn test_typeof_all_types() { 2059 + // typeof returns correct strings for all types. 2060 + let cases = [ 2061 + ("typeof undefined", "undefined"), 2062 + ("typeof null", "object"), 2063 + ("typeof true", "boolean"), 2064 + ("typeof 42", "number"), 2065 + ("typeof 'hello'", "string"), 2066 + ("typeof {}", "object"), 2067 + ("typeof function(){}", "function"), 2068 + ]; 2069 + for (src, expected) in cases { 2070 + match eval(src).unwrap() { 2071 + Value::String(s) => assert_eq!(s, expected, "typeof failed for: {src}"), 2072 + v => panic!("expected string for {src}, got {v:?}"), 2073 + } 2074 + } 2075 + } 2076 + 2077 + #[test] 2078 + fn test_instanceof_basic() { 2079 + let src = r#" 2080 + function Foo() {} 2081 + var f = {}; 2082 + f instanceof Foo 2083 + "#; 2084 + // Plain object without prototype link → false 2085 + match eval(src).unwrap() { 2086 + Value::Boolean(b) => assert!(!b), 2087 + v => panic!("expected false, got {v:?}"), 2088 + } 2089 + } 2090 + 2091 + #[test] 2092 + fn test_in_operator() { 2093 + let src = r#" 2094 + var o = { x: 1, y: 2 }; 2095 + var r1 = "x" in o; 2096 + var r2 = "z" in o; 2097 + r1 === true && r2 === false 2098 + "#; 2099 + match eval(src).unwrap() { 2100 + Value::Boolean(b) => assert!(b), 2101 + v => panic!("expected true, got {v:?}"), 2102 + } 2103 + } 2104 + 2105 + #[test] 2106 + fn test_delete_property() { 2107 + let src = r#" 2108 + var o = { x: 1, y: 2 }; 2109 + delete o.x; 2110 + typeof o.x === "undefined" && o.y === 2 2111 + "#; 2112 + match eval(src).unwrap() { 2113 + Value::Boolean(b) => assert!(b), 2114 + v => panic!("expected true, got {v:?}"), 2115 + } 2116 + } 2117 + 2118 + #[test] 2119 + fn test_delete_computed_property() { 2120 + let src = r#" 2121 + var o = { a: 10, b: 20 }; 2122 + var key = "a"; 2123 + delete o[key]; 2124 + typeof o.a === "undefined" && o.b === 20 2125 + "#; 2126 + match eval(src).unwrap() { 2127 + Value::Boolean(b) => assert!(b), 2128 + v => panic!("expected true, got {v:?}"), 2129 + } 2130 + } 2131 + 2132 + #[test] 2133 + fn test_delete_non_configurable() { 2134 + // Array length is non-configurable, delete should return false. 2135 + let mut gc: Gc<HeapObject> = Gc::new(); 2136 + let mut obj = ObjectData::new(); 2137 + obj.properties.insert( 2138 + "x".to_string(), 2139 + Property { 2140 + value: Value::Number(1.0), 2141 + writable: true, 2142 + enumerable: true, 2143 + configurable: false, 2144 + }, 2145 + ); 2146 + let obj_ref = gc.alloc(HeapObject::Object(obj)); 2147 + 2148 + // Try to delete the non-configurable property. 2149 + match gc.get_mut(obj_ref) { 2150 + Some(HeapObject::Object(data)) => { 2151 + let prop = data.properties.get("x").unwrap(); 2152 + assert!(!prop.configurable); 2153 + // The property should still be there. 2154 + assert!(data.properties.contains_key("x")); 2155 + } 2156 + _ => panic!("expected object"), 2157 + } 2158 + } 2159 + 2160 + #[test] 2161 + fn test_property_writable_flag() { 2162 + // Setting a non-writable property should silently fail. 2163 + let mut gc: Gc<HeapObject> = Gc::new(); 2164 + let mut obj = ObjectData::new(); 2165 + obj.properties.insert( 2166 + "frozen".to_string(), 2167 + Property { 2168 + value: Value::Number(42.0), 2169 + writable: false, 2170 + enumerable: true, 2171 + configurable: false, 2172 + }, 2173 + ); 2174 + let obj_ref = gc.alloc(HeapObject::Object(obj)); 2175 + 2176 + // Verify the property value. 2177 + match gc.get(obj_ref) { 2178 + Some(HeapObject::Object(data)) => { 2179 + assert_eq!( 2180 + data.properties.get("frozen").unwrap().value.to_number(), 2181 + 42.0 2182 + ); 2183 + } 2184 + _ => panic!("expected object"), 2185 + } 2186 + } 2187 + 2188 + #[test] 2189 + fn test_for_in_basic() { 2190 + let src = r#" 2191 + var o = { a: 1, b: 2, c: 3 }; 2192 + var sum = 0; 2193 + for (var key in o) { 2194 + sum = sum + o[key]; 2195 + } 2196 + sum 2197 + "#; 2198 + match eval(src).unwrap() { 2199 + Value::Number(n) => assert_eq!(n, 6.0), 2200 + v => panic!("expected 6, got {v:?}"), 2201 + } 2202 + } 2203 + 2204 + #[test] 2205 + fn test_for_in_collects_keys() { 2206 + let src = r#" 2207 + var o = { x: 10, y: 20 }; 2208 + var keys = ""; 2209 + for (var k in o) { 2210 + keys = keys + k + ","; 2211 + } 2212 + keys 2213 + "#; 2214 + match eval(src).unwrap() { 2215 + Value::String(s) => { 2216 + // Both keys should appear (order may vary with HashMap). 2217 + assert!(s.contains("x,")); 2218 + assert!(s.contains("y,")); 2219 + } 2220 + v => panic!("expected string, got {v:?}"), 2221 + } 2222 + } 2223 + 2224 + #[test] 2225 + fn test_for_in_empty_object() { 2226 + let src = r#" 2227 + var o = {}; 2228 + var count = 0; 2229 + for (var k in o) { 2230 + count = count + 1; 2231 + } 2232 + count 2233 + "#; 2234 + match eval(src).unwrap() { 2235 + Value::Number(n) => assert_eq!(n, 0.0), 2236 + v => panic!("expected 0, got {v:?}"), 2237 + } 2238 + } 2239 + 2240 + #[test] 2241 + fn test_property_enumerable_flag() { 2242 + // Array "length" is non-enumerable, should not appear in for-in. 2243 + let src = r#" 2244 + var arr = [10, 20, 30]; 2245 + var keys = ""; 2246 + for (var k in arr) { 2247 + keys = keys + k + ","; 2248 + } 2249 + keys 2250 + "#; 2251 + match eval(src).unwrap() { 2252 + Value::String(s) => { 2253 + // "length" should NOT be in the keys (it's non-enumerable). 2254 + assert!(!s.contains("length")); 2255 + // Array indices should be present. 2256 + assert!(s.contains("0,")); 2257 + assert!(s.contains("1,")); 2258 + assert!(s.contains("2,")); 2259 + } 2260 + v => panic!("expected string, got {v:?}"), 2261 + } 2262 + } 2263 + 2264 + #[test] 2265 + fn test_object_reference_semantics() { 2266 + // Objects have reference semantics (shared via GcRef). 2267 + let src = r#" 2268 + var a = { x: 1 }; 2269 + var b = a; 2270 + b.x = 42; 2271 + a.x 2272 + "#; 2273 + match eval(src).unwrap() { 2274 + Value::Number(n) => assert_eq!(n, 42.0), 2275 + v => panic!("expected 42, got {v:?}"), 2276 + } 2277 + } 2278 + 2279 + #[test] 2280 + fn test_gc_enumerate_keys_order() { 2281 + // Integer keys should come first, sorted numerically. 2282 + let mut gc: Gc<HeapObject> = Gc::new(); 2283 + let mut obj = ObjectData::new(); 2284 + obj.properties 2285 + .insert("b".to_string(), Property::data(Value::Number(2.0))); 2286 + obj.properties 2287 + .insert("2".to_string(), Property::data(Value::Number(3.0))); 2288 + obj.properties 2289 + .insert("a".to_string(), Property::data(Value::Number(1.0))); 2290 + obj.properties 2291 + .insert("0".to_string(), Property::data(Value::Number(0.0))); 2292 + let obj_ref = gc.alloc(HeapObject::Object(obj)); 2293 + 2294 + let keys = gc_enumerate_keys(&gc, obj_ref); 2295 + // Integer keys first (sorted), then string keys. 2296 + let int_part: Vec<&str> = keys.iter().take(2).map(|s| s.as_str()).collect(); 2297 + assert_eq!(int_part, vec!["0", "2"]); 2298 + // Remaining keys are string keys (order depends on HashMap iteration). 2299 + let str_part: Vec<&str> = keys.iter().skip(2).map(|s| s.as_str()).collect(); 2300 + assert!(str_part.contains(&"a")); 2301 + assert!(str_part.contains(&"b")); 2302 + } 2303 + 2304 + #[test] 2305 + fn test_instanceof_with_gc() { 2306 + // Direct test of gc_instanceof. 2307 + let mut gc: Gc<HeapObject> = Gc::new(); 2308 + 2309 + // Create a constructor function with a .prototype object. 2310 + let proto = gc.alloc(HeapObject::Object(ObjectData::new())); 2311 + let ctor = gc.alloc(HeapObject::Function(FunctionData { 2312 + name: "Foo".to_string(), 2313 + kind: FunctionKind::Native(NativeFunc { 2314 + callback: |_| Ok(Value::Undefined), 2315 + }), 2316 + prototype_obj: Some(proto), 2317 + })); 2318 + 2319 + // Create an object whose [[Prototype]] is the constructor's .prototype. 2320 + let mut obj_data = ObjectData::new(); 2321 + obj_data.prototype = Some(proto); 2322 + let obj = gc.alloc(HeapObject::Object(obj_data)); 2323 + 2324 + assert!(gc_instanceof(&gc, obj, ctor)); 2325 + 2326 + // An unrelated object should not match. 2327 + let other = gc.alloc(HeapObject::Object(ObjectData::new())); 2328 + assert!(!gc_instanceof(&gc, other, ctor)); 1781 2329 } 1782 2330 }