//! Register-based bytecode format for the JavaScript engine. //! //! Each instruction encodes register operands directly, making it suitable for //! JIT compilation. Instructions are variable-length, encoded as a 1-byte opcode //! followed by operand bytes. use crate::shape::ShapeId; use std::fmt; /// A register index (0–65535). pub type Reg = u16; /// An index into the constant pool. pub type ConstIdx = u16; /// An index into the name/string table. pub type NameIdx = u16; /// An index into the inline cache vector. pub type IcIdx = u16; /// Maximum number of entries in a polymorphic inline cache before going megamorphic. const IC_MAX_POLY: usize = 4; /// A single (shape, slot) mapping cached at a property-access site. #[derive(Debug, Clone, Copy)] pub struct InlineCacheEntry { pub shape_id: ShapeId, pub slot_index: u32, } /// State of an inline cache site. #[derive(Debug, Clone)] pub enum InlineCacheState { /// No shape cached yet. Uninitialized, /// Exactly one shape seen. Monomorphic(InlineCacheEntry), /// 2–4 shapes seen. Polymorphic(Vec), /// Too many shapes — always use the slow path. Megamorphic, } /// An inline cache for a single property-access bytecode site. #[derive(Debug, Clone)] pub struct InlineCache { pub state: InlineCacheState, } impl InlineCache { /// Create an empty (uninitialized) inline cache. pub fn new() -> Self { Self { state: InlineCacheState::Uninitialized, } } /// Try to find a cached slot index for the given shape. /// Returns `Some(slot_index)` on a cache hit, `None` on miss. pub fn lookup(&self, shape: ShapeId) -> Option { match &self.state { InlineCacheState::Uninitialized | InlineCacheState::Megamorphic => None, InlineCacheState::Monomorphic(entry) => { if entry.shape_id == shape { Some(entry.slot_index) } else { None } } InlineCacheState::Polymorphic(entries) => { for entry in entries { if entry.shape_id == shape { return Some(entry.slot_index); } } None } } } /// Record a (shape, slot_index) mapping after a cache miss. /// Does nothing if already megamorphic. pub fn update(&mut self, shape: ShapeId, slot_index: u32) { match &mut self.state { InlineCacheState::Megamorphic => {} InlineCacheState::Uninitialized => { self.state = InlineCacheState::Monomorphic(InlineCacheEntry { shape_id: shape, slot_index, }); } InlineCacheState::Monomorphic(existing) => { if existing.shape_id == shape { return; } let entries = vec![ *existing, InlineCacheEntry { shape_id: shape, slot_index, }, ]; self.state = InlineCacheState::Polymorphic(entries); } InlineCacheState::Polymorphic(entries) => { for e in entries.iter() { if e.shape_id == shape { return; } } if entries.len() >= IC_MAX_POLY { self.state = InlineCacheState::Megamorphic; } else { entries.push(InlineCacheEntry { shape_id: shape, slot_index, }); } } } } /// Whether this IC has gone megamorphic. pub fn is_megamorphic(&self) -> bool { matches!(self.state, InlineCacheState::Megamorphic) } } impl Default for InlineCache { fn default() -> Self { Self::new() } } /// Bytecode instruction set. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum Op { // ── Register loads ────────────────────────────────────── /// LoadConst dst, const_idx(u16) — load constant pool entry into register LoadConst = 0x01, /// LoadNull dst LoadNull = 0x02, /// LoadUndefined dst LoadUndefined = 0x03, /// LoadTrue dst LoadTrue = 0x04, /// LoadFalse dst LoadFalse = 0x05, /// Move dst, src Move = 0x06, // ── Global variable access ────────────────────────────── /// LoadGlobal dst, name_idx(u16) LoadGlobal = 0x07, /// StoreGlobal name_idx(u16), src StoreGlobal = 0x08, // ── Arithmetic ────────────────────────────────────────── /// Add dst, lhs, rhs Add = 0x10, /// Sub dst, lhs, rhs Sub = 0x11, /// Mul dst, lhs, rhs Mul = 0x12, /// Div dst, lhs, rhs Div = 0x13, /// Rem dst, lhs, rhs Rem = 0x14, /// Exp dst, lhs, rhs Exp = 0x15, /// Neg dst, src (unary minus) Neg = 0x16, // ── Bitwise ───────────────────────────────────────────── /// BitAnd dst, lhs, rhs BitAnd = 0x20, /// BitOr dst, lhs, rhs BitOr = 0x21, /// BitXor dst, lhs, rhs BitXor = 0x22, /// ShiftLeft dst, lhs, rhs ShiftLeft = 0x23, /// ShiftRight dst, lhs, rhs ShiftRight = 0x24, /// UShiftRight dst, lhs, rhs UShiftRight = 0x25, /// BitNot dst, src BitNot = 0x26, // ── Comparison ────────────────────────────────────────── /// Eq dst, lhs, rhs (==) Eq = 0x30, /// StrictEq dst, lhs, rhs (===) StrictEq = 0x31, /// NotEq dst, lhs, rhs (!=) NotEq = 0x32, /// StrictNotEq dst, lhs, rhs (!==) StrictNotEq = 0x33, /// LessThan dst, lhs, rhs LessThan = 0x34, /// LessEq dst, lhs, rhs LessEq = 0x35, /// GreaterThan dst, lhs, rhs GreaterThan = 0x36, /// GreaterEq dst, lhs, rhs GreaterEq = 0x37, // ── Logical / unary ───────────────────────────────────── /// LogicalNot dst, src LogicalNot = 0x38, /// TypeOf dst, src TypeOf = 0x39, /// InstanceOf dst, lhs, rhs InstanceOf = 0x3A, /// In dst, lhs, rhs In = 0x3B, /// Void dst, src — evaluate src, produce undefined Void = 0x3C, // ── Control flow ──────────────────────────────────────── /// Jump offset(i32) — unconditional relative jump Jump = 0x40, /// JumpIfTrue reg, offset(i32) JumpIfTrue = 0x41, /// JumpIfFalse reg, offset(i32) JumpIfFalse = 0x42, /// JumpIfNullish reg, offset(i32) JumpIfNullish = 0x43, // ── Functions / calls ─────────────────────────────────── /// Call dst, func_reg, args_start, arg_count(u8) Call = 0x50, /// Return reg Return = 0x51, /// Throw reg Throw = 0x52, /// CreateClosure dst, func_idx(u16) — create a closure from a nested function CreateClosure = 0x53, /// NewCall dst, func_reg, args_start, arg_count(u8) NewCall = 0x54, /// CallSpread dst, func_reg, args_array_reg — call with arguments taken from /// a pre-built array (used when the call site contains a spread argument). CallSpread = 0x55, /// NewCallSpread dst, func_reg, args_array_reg — construct with arguments /// taken from a pre-built array (spread in a `new` argument list). NewCallSpread = 0x56, // ── Object / property ─────────────────────────────────── /// GetProperty dst, obj_reg, key_reg GetProperty = 0x60, /// SetProperty obj_reg, key_reg, val_reg SetProperty = 0x61, /// CreateObject dst CreateObject = 0x62, /// CreateArray dst CreateArray = 0x63, /// GetPropertyByName dst, obj_reg, name_idx(u16) — optimized named access GetPropertyByName = 0x64, /// SetPropertyByName obj_reg, name_idx(u16), val_reg — optimized named store SetPropertyByName = 0x65, /// DefineMethod obj_reg, name_idx(u16), fn_reg — install a non-enumerable /// method (data property) on a class constructor or its prototype. DefineMethod = 0x66, /// DefineAccessor obj_reg, name_idx(u16), kind(u8: 0=get,1=set), fn_reg — /// install/merge a getter or setter accessor on a class constructor or its /// prototype. DefineAccessor = 0x67, /// SetHomeObject fn_reg, obj_reg — record `obj` as the [[HomeObject]] of the /// method/constructor function in `fn_reg`, so `super` references inside it /// resolve against `obj`'s prototype. SetHomeObject = 0x68, /// LoadSuperBase dst — load the prototype of the current function's /// [[HomeObject]] (the object `super` property lookups start from). LoadSuperBase = 0x69, /// GetPrivate dst, obj_reg, name_idx(u16) — read a private field/method /// (`obj.#x`). Throws a TypeError if `obj` does not carry the private name /// (the ES2022 brand check). GetPrivate = 0x6A, /// SetPrivate obj_reg, name_idx(u16), val_reg — define-or-update a private /// field (`obj.#x = v`, and class field/method installation). Throws a /// TypeError if `obj` is not an object. SetPrivate = 0x6B, /// HasPrivate dst, obj_reg, name_idx(u16) — `#x in obj`: true iff `obj` /// carries the private name. Never throws. HasPrivate = 0x6C, /// CopyDataProperties target_reg, source_reg — object literal spread: /// copy source's own enumerable properties into target. CopyDataProperties = 0x6D, // ── Misc ──────────────────────────────────────────────── /// Delete dst, obj_reg, key_reg Delete = 0x70, /// LoadInt8 dst, i8 — load small integer without constant pool LoadInt8 = 0x71, /// ForInInit dst_keys, obj_reg — collect enumerable keys of object into an array ForInInit = 0x72, /// ForInNext dst_val, dst_done, keys_reg, idx_reg — get next key or signal done ForInNext = 0x73, /// SetPrototype obj_reg, proto_reg — set [[Prototype]] of obj to proto SetPrototype = 0x74, /// GetPrototype dst, obj_reg — get [[Prototype]] of obj GetPrototype = 0x75, /// PushExceptionHandler catch_reg, offset(i32) — push a try/catch handler PushExceptionHandler = 0x76, /// PopExceptionHandler — remove the current exception handler PopExceptionHandler = 0x77, /// NewCell dst — allocate a new GC cell initialized to undefined NewCell = 0x78, /// CellLoad dst, cell_reg — read the value stored in the cell CellLoad = 0x79, /// CellStore cell_reg, src — write a value into the cell CellStore = 0x7A, /// LoadUpvalue dst, idx(u8) — load from the closure's captured upvalue cell LoadUpvalue = 0x7B, /// StoreUpvalue idx(u8), src — store into the closure's captured upvalue cell StoreUpvalue = 0x7C, // ── Iterator / generator ─────────────────────────────── /// Yield dst, src — suspend generator, yield src value. On resume, dst gets the /// value passed to next(). The VM saves the frame state and returns {value, done: false}. Yield = 0x80, /// Spread dst_array, src — iterate src via @@iterator, append all elements to dst_array Spread = 0x81, /// Await dst, src — suspend async function, await src value. On resume, dst gets the /// settled value. The VM saves the frame state and returns to the async driver. Await = 0x82, /// BuildArguments dst — materialize the current frame's `arguments` object into dst. /// The resulting object has indexed properties 0..N-1, a `length` property of N, /// and inherits from Object.prototype. BuildArguments = 0x83, /// BuildRestArguments dst, start_idx(u16) — materialize arguments[start_idx..] /// as an Array object for a rest parameter. BuildRestArguments = 0x84, } impl Op { /// Decode an opcode from a byte, returning `None` for unrecognized values. pub fn from_byte(b: u8) -> Option { match b { 0x01 => Some(Op::LoadConst), 0x02 => Some(Op::LoadNull), 0x03 => Some(Op::LoadUndefined), 0x04 => Some(Op::LoadTrue), 0x05 => Some(Op::LoadFalse), 0x06 => Some(Op::Move), 0x07 => Some(Op::LoadGlobal), 0x08 => Some(Op::StoreGlobal), 0x10 => Some(Op::Add), 0x11 => Some(Op::Sub), 0x12 => Some(Op::Mul), 0x13 => Some(Op::Div), 0x14 => Some(Op::Rem), 0x15 => Some(Op::Exp), 0x16 => Some(Op::Neg), 0x20 => Some(Op::BitAnd), 0x21 => Some(Op::BitOr), 0x22 => Some(Op::BitXor), 0x23 => Some(Op::ShiftLeft), 0x24 => Some(Op::ShiftRight), 0x25 => Some(Op::UShiftRight), 0x26 => Some(Op::BitNot), 0x30 => Some(Op::Eq), 0x31 => Some(Op::StrictEq), 0x32 => Some(Op::NotEq), 0x33 => Some(Op::StrictNotEq), 0x34 => Some(Op::LessThan), 0x35 => Some(Op::LessEq), 0x36 => Some(Op::GreaterThan), 0x37 => Some(Op::GreaterEq), 0x38 => Some(Op::LogicalNot), 0x39 => Some(Op::TypeOf), 0x3A => Some(Op::InstanceOf), 0x3B => Some(Op::In), 0x3C => Some(Op::Void), 0x40 => Some(Op::Jump), 0x41 => Some(Op::JumpIfTrue), 0x42 => Some(Op::JumpIfFalse), 0x43 => Some(Op::JumpIfNullish), 0x50 => Some(Op::Call), 0x51 => Some(Op::Return), 0x52 => Some(Op::Throw), 0x53 => Some(Op::CreateClosure), 0x54 => Some(Op::NewCall), 0x55 => Some(Op::CallSpread), 0x56 => Some(Op::NewCallSpread), 0x60 => Some(Op::GetProperty), 0x61 => Some(Op::SetProperty), 0x62 => Some(Op::CreateObject), 0x63 => Some(Op::CreateArray), 0x64 => Some(Op::GetPropertyByName), 0x65 => Some(Op::SetPropertyByName), 0x66 => Some(Op::DefineMethod), 0x67 => Some(Op::DefineAccessor), 0x68 => Some(Op::SetHomeObject), 0x69 => Some(Op::LoadSuperBase), 0x6A => Some(Op::GetPrivate), 0x6B => Some(Op::SetPrivate), 0x6C => Some(Op::HasPrivate), 0x6D => Some(Op::CopyDataProperties), 0x70 => Some(Op::Delete), 0x71 => Some(Op::LoadInt8), 0x72 => Some(Op::ForInInit), 0x73 => Some(Op::ForInNext), 0x74 => Some(Op::SetPrototype), 0x75 => Some(Op::GetPrototype), 0x76 => Some(Op::PushExceptionHandler), 0x77 => Some(Op::PopExceptionHandler), 0x78 => Some(Op::NewCell), 0x79 => Some(Op::CellLoad), 0x7A => Some(Op::CellStore), 0x7B => Some(Op::LoadUpvalue), 0x7C => Some(Op::StoreUpvalue), 0x80 => Some(Op::Yield), 0x81 => Some(Op::Spread), 0x82 => Some(Op::Await), 0x83 => Some(Op::BuildArguments), 0x84 => Some(Op::BuildRestArguments), _ => None, } } } /// A constant value stored in the constant pool. #[derive(Debug, Clone, PartialEq)] pub enum Constant { Number(f64), String(String), } /// Describes how a closure captures a single variable from its enclosing scope. #[derive(Debug, Clone)] pub struct UpvalueDef { /// If true, `index` refers to a register in the immediately enclosing function /// (which must hold a cell GcRef). If false, `index` refers to an upvalue slot /// of the enclosing function (transitive capture). pub is_local: bool, /// Register index (if `is_local`) or upvalue index (if not) in the parent. pub index: u16, } /// A compiled bytecode function. #[derive(Debug, Clone)] pub struct Function { /// Name of the function (empty for anonymous / top-level). pub name: String, /// Number of named parameters. pub param_count: u8, /// Total register slots needed. pub register_count: u16, /// The bytecode bytes. pub code: Vec, /// Constant pool (numbers and strings). pub constants: Vec, /// Name table for global/property name lookups. pub names: Vec, /// Nested function definitions (referenced by CreateClosure). pub functions: Vec, /// Source map: bytecode offset → (source line, source column) sorted by /// offset. Lines and columns are 1-based, matching the lexer's Span. pub source_map: Vec<(u32, u32, u32)>, /// Upvalue definitions: how this function captures variables from its parent. pub upvalue_defs: Vec, /// Whether this is a generator function (function*). pub is_generator: bool, /// Whether this is an async function. pub is_async: bool, /// Inline caches for property access sites. pub inline_caches: Vec, } impl Function { pub fn new(name: String, param_count: u8) -> Self { Self { name, param_count, register_count: 0, code: Vec::new(), constants: Vec::new(), names: Vec::new(), functions: Vec::new(), source_map: Vec::new(), upvalue_defs: Vec::new(), is_generator: false, is_async: false, inline_caches: Vec::new(), } } /// Resolve a bytecode offset to its source line via `source_map`. /// Returns the line associated with the highest source_map entry whose /// offset is less than or equal to `pc`, or `None` if no entry covers it. pub fn source_line_for(&self, pc: usize) -> Option { self.source_location_for(pc).map(|(line, _)| line) } /// Resolve a bytecode offset to `(source_line, source_column)`. Useful /// when an error needs both pieces of information (e.g. minified bundles /// where everything sits on line 1). pub fn source_location_for(&self, pc: usize) -> Option<(u32, u32)> { if self.source_map.is_empty() { return None; } let pc = pc as u32; match self .source_map .binary_search_by_key(&pc, |(offset, _, _)| *offset) { Ok(i) => Some((self.source_map[i].1, self.source_map[i].2)), Err(0) => None, Err(i) => Some((self.source_map[i - 1].1, self.source_map[i - 1].2)), } } } /// A builder that emits bytecode into a `Function`. pub struct BytecodeBuilder { pub func: Function, } impl BytecodeBuilder { pub fn new(name: String, param_count: u8) -> Self { Self { func: Function::new(name, param_count), } } /// Current bytecode offset (position of next emitted byte). pub fn offset(&self) -> usize { self.func.code.len() } /// Intern a constant, returning its index. pub fn add_constant(&mut self, c: Constant) -> ConstIdx { // Reuse existing constant if it matches. for (i, existing) in self.func.constants.iter().enumerate() { match (existing, &c) { (Constant::String(a), Constant::String(b)) if a == b => return i as ConstIdx, (Constant::Number(a), Constant::Number(b)) if a.to_bits() == b.to_bits() => { return i as ConstIdx; } _ => {} } } let idx = self.func.constants.len(); assert!(idx <= u16::MAX as usize, "constant pool overflow"); self.func.constants.push(c); idx as ConstIdx } /// Intern a name, returning its index. pub fn add_name(&mut self, name: &str) -> NameIdx { for (i, existing) in self.func.names.iter().enumerate() { if existing == name { return i as NameIdx; } } let idx = self.func.names.len(); assert!(idx <= u16::MAX as usize, "name table overflow"); self.func.names.push(name.to_string()); idx as NameIdx } /// Add a nested function, returning its index. pub fn add_function(&mut self, f: Function) -> u16 { let idx = self.func.functions.len(); assert!(idx <= u16::MAX as usize, "function table overflow"); self.func.functions.push(f); idx as u16 } // ── Emit helpers ──────────────────────────────────────── fn emit_u8(&mut self, v: u8) { self.func.code.push(v); } fn emit_u16(&mut self, v: u16) { self.func.code.extend_from_slice(&v.to_le_bytes()); } fn emit_reg_operand(&mut self, reg: Reg) { self.emit_u16(reg); } fn emit_i32(&mut self, v: i32) { self.func.code.extend_from_slice(&v.to_le_bytes()); } // ── Single-operand instructions ───────────────────────── /// Emit: LoadNull dst | LoadUndefined dst | LoadTrue dst | LoadFalse dst pub fn emit_reg(&mut self, op: Op, dst: Reg) { self.emit_u8(op as u8); self.emit_reg_operand(dst); } /// Emit: Move dst, src pub fn emit_reg_reg(&mut self, op: Op, a: Reg, b: Reg) { self.emit_u8(op as u8); self.emit_reg_operand(a); self.emit_reg_operand(b); } /// Emit: ForInNext dst_val, dst_done, keys, idx (4-register instruction) pub fn emit_reg4(&mut self, op: Op, a: Reg, b: Reg, c: Reg, d: Reg) { self.emit_u8(op as u8); self.emit_reg_operand(a); self.emit_reg_operand(b); self.emit_reg_operand(c); self.emit_reg_operand(d); } /// Emit: Add dst, lhs, rhs (and other 3-register instructions) pub fn emit_reg3(&mut self, op: Op, dst: Reg, lhs: Reg, rhs: Reg) { self.emit_u8(op as u8); self.emit_reg_operand(dst); self.emit_reg_operand(lhs); self.emit_reg_operand(rhs); } /// Emit: LoadConst dst, const_idx | CreateClosure dst, func_idx pub fn emit_reg_u16(&mut self, op: Op, dst: Reg, idx: u16) { self.emit_u8(op as u8); self.emit_reg_operand(dst); self.emit_u16(idx); } /// Emit: LoadGlobal dst, name_idx pub fn emit_load_global(&mut self, dst: Reg, name_idx: NameIdx) { self.emit_u8(Op::LoadGlobal as u8); self.emit_reg_operand(dst); self.emit_u16(name_idx); } /// Emit: StoreGlobal name_idx, src pub fn emit_store_global(&mut self, name_idx: NameIdx, src: Reg) { self.emit_u8(Op::StoreGlobal as u8); self.emit_u16(name_idx); self.emit_reg_operand(src); } /// Emit: Jump offset (placeholder, returns patch position) pub fn emit_jump(&mut self, op: Op) -> usize { self.emit_u8(op as u8); if op == Op::JumpIfTrue || op == Op::JumpIfFalse || op == Op::JumpIfNullish { panic!("use emit_cond_jump for conditional jumps"); } let pos = self.offset(); self.emit_i32(0); // placeholder pos } /// Emit: JumpIfTrue/JumpIfFalse/JumpIfNullish reg, offset (placeholder) pub fn emit_cond_jump(&mut self, op: Op, reg: Reg) -> usize { self.emit_u8(op as u8); self.emit_reg_operand(reg); let pos = self.offset(); self.emit_i32(0); // placeholder pos } /// Patch a jump offset at the given position to point to the current offset. pub fn patch_jump(&mut self, pos: usize) { self.patch_jump_to(pos, self.offset()); } /// Patch a jump offset at the given position to point to an arbitrary target. pub fn patch_jump_to(&mut self, pos: usize, target: usize) { let offset = target as i32 - (pos as i32 + 4); // relative to after the i32 let bytes = offset.to_le_bytes(); self.func.code[pos] = bytes[0]; self.func.code[pos + 1] = bytes[1]; self.func.code[pos + 2] = bytes[2]; self.func.code[pos + 3] = bytes[3]; } /// Emit: Jump with a known target (backward jump). pub fn emit_jump_to(&mut self, target: usize) { self.emit_u8(Op::Jump as u8); let from = self.offset() as i32 + 4; let offset = target as i32 - from; self.emit_i32(offset); } /// Emit: JumpIfTrue/JumpIfFalse with a known target (backward jump). pub fn emit_cond_jump_to(&mut self, op: Op, reg: Reg, target: usize) { self.emit_u8(op as u8); self.emit_reg_operand(reg); let from = self.offset() as i32 + 4; let offset = target as i32 - from; self.emit_i32(offset); } /// Emit: Call dst, func_reg, args_start, arg_count pub fn emit_call(&mut self, dst: Reg, func: Reg, args_start: Reg, arg_count: u8) { self.emit_u8(Op::Call as u8); self.emit_reg_operand(dst); self.emit_reg_operand(func); self.emit_reg_operand(args_start); self.emit_u8(arg_count); } /// Emit: NewCall dst, func_reg, args_start, arg_count pub fn emit_new_call(&mut self, dst: Reg, func: Reg, args_start: Reg, arg_count: u8) { self.emit_u8(Op::NewCall as u8); self.emit_reg_operand(dst); self.emit_reg_operand(func); self.emit_reg_operand(args_start); self.emit_u8(arg_count); } /// Emit: CallSpread dst, func_reg, args_array_reg pub fn emit_call_spread(&mut self, dst: Reg, func: Reg, args_array: Reg) { self.emit_u8(Op::CallSpread as u8); self.emit_reg_operand(dst); self.emit_reg_operand(func); self.emit_reg_operand(args_array); } /// Emit: NewCallSpread dst, func_reg, args_array_reg pub fn emit_new_call_spread(&mut self, dst: Reg, func: Reg, args_array: Reg) { self.emit_u8(Op::NewCallSpread as u8); self.emit_reg_operand(dst); self.emit_reg_operand(func); self.emit_reg_operand(args_array); } /// Allocate a new inline cache slot, returning its index. pub fn alloc_ic(&mut self) -> IcIdx { let idx = self.func.inline_caches.len(); assert!(idx <= u16::MAX as usize, "inline cache overflow"); self.func.inline_caches.push(InlineCache::new()); idx as IcIdx } /// Emit: GetPropertyByName dst, obj, name_idx, ic_idx pub fn emit_get_prop_name(&mut self, dst: Reg, obj: Reg, name_idx: NameIdx) { let ic = self.alloc_ic(); self.emit_u8(Op::GetPropertyByName as u8); self.emit_reg_operand(dst); self.emit_reg_operand(obj); self.emit_u16(name_idx); self.emit_u16(ic); } /// Emit: PushExceptionHandler catch_reg, offset (placeholder, returns patch position) pub fn emit_push_exception_handler(&mut self, catch_reg: Reg) -> usize { self.emit_u8(Op::PushExceptionHandler as u8); self.emit_reg_operand(catch_reg); let pos = self.offset(); self.emit_i32(0); // placeholder for catch block offset pos } /// Emit: PopExceptionHandler pub fn emit_pop_exception_handler(&mut self) { self.emit_u8(Op::PopExceptionHandler as u8); } /// Emit: SetPropertyByName obj, name_idx, val, ic_idx pub fn emit_set_prop_name(&mut self, obj: Reg, name_idx: NameIdx, val: Reg) { let ic = self.alloc_ic(); self.emit_u8(Op::SetPropertyByName as u8); self.emit_reg_operand(obj); self.emit_u16(name_idx); self.emit_reg_operand(val); self.emit_u16(ic); } /// Emit: DefineMethod obj, name_idx, fn_reg pub fn emit_define_method(&mut self, obj: Reg, name_idx: NameIdx, fn_reg: Reg) { self.emit_u8(Op::DefineMethod as u8); self.emit_reg_operand(obj); self.emit_u16(name_idx); self.emit_reg_operand(fn_reg); } /// Emit: DefineAccessor obj, name_idx, kind (0=get, 1=set), fn_reg pub fn emit_define_accessor(&mut self, obj: Reg, name_idx: NameIdx, kind: u8, fn_reg: Reg) { self.emit_u8(Op::DefineAccessor as u8); self.emit_reg_operand(obj); self.emit_u16(name_idx); self.emit_u8(kind); self.emit_reg_operand(fn_reg); } /// Emit: GetPrivate dst, obj, name_idx pub fn emit_get_private(&mut self, dst: Reg, obj: Reg, name_idx: NameIdx) { self.emit_u8(Op::GetPrivate as u8); self.emit_reg_operand(dst); self.emit_reg_operand(obj); self.emit_u16(name_idx); } /// Emit: SetPrivate obj, name_idx, val pub fn emit_set_private(&mut self, obj: Reg, name_idx: NameIdx, val: Reg) { self.emit_u8(Op::SetPrivate as u8); self.emit_reg_operand(obj); self.emit_u16(name_idx); self.emit_reg_operand(val); } /// Emit: HasPrivate dst, obj, name_idx pub fn emit_has_private(&mut self, dst: Reg, obj: Reg, name_idx: NameIdx) { self.emit_u8(Op::HasPrivate as u8); self.emit_reg_operand(dst); self.emit_reg_operand(obj); self.emit_u16(name_idx); } /// Emit: CopyDataProperties target, source pub fn emit_copy_data_properties(&mut self, target: Reg, source: Reg) { self.emit_u8(Op::CopyDataProperties as u8); self.emit_reg_operand(target); self.emit_reg_operand(source); } /// Emit: BuildRestArguments dst, start_idx pub fn emit_build_rest_arguments(&mut self, dst: Reg, start_idx: u16) { self.emit_u8(Op::BuildRestArguments as u8); self.emit_reg_operand(dst); self.emit_u16(start_idx); } /// Emit: LoadInt8 dst, value pub fn emit_load_int8(&mut self, dst: Reg, value: i8) { self.emit_u8(Op::LoadInt8 as u8); self.emit_reg_operand(dst); self.emit_u8(value as u8); } /// Emit: LoadUpvalue dst, idx pub fn emit_load_upvalue(&mut self, dst: Reg, idx: u16) { self.emit_u8(Op::LoadUpvalue as u8); self.emit_reg_operand(dst); self.emit_u16(idx); } /// Emit: StoreUpvalue idx, src pub fn emit_store_upvalue(&mut self, idx: u16, src: Reg) { self.emit_u8(Op::StoreUpvalue as u8); self.emit_u16(idx); self.emit_reg_operand(src); } /// Emit: Yield dst, src — suspend generator pub fn emit_yield(&mut self, dst: Reg, src: Reg) { self.emit_u8(Op::Yield as u8); self.emit_reg_operand(dst); self.emit_reg_operand(src); } /// Emit: Spread dst_array, src — spread iterable into array pub fn emit_spread(&mut self, dst_array: Reg, src: Reg) { self.emit_u8(Op::Spread as u8); self.emit_reg_operand(dst_array); self.emit_reg_operand(src); } /// Emit: Await dst, src — suspend async function pub fn emit_await(&mut self, dst: Reg, src: Reg) { self.emit_u8(Op::Await as u8); self.emit_reg_operand(dst); self.emit_reg_operand(src); } /// Add a source map entry: current bytecode offset → (source line, column). /// Both line and column are 1-based. pub fn add_source_map(&mut self, line: u32, col: u32) { let offset = self.offset() as u32; self.func.source_map.push((offset, line, col)); } /// Finalize and return the compiled function. pub fn finish(self) -> Function { self.func } } // ── Disassembler ──────────────────────────────────────────── fn read_u16_operand(code: &[u8], pc: &mut usize) -> u16 { let value = u16::from_le_bytes([code[*pc], code[*pc + 1]]); *pc += 2; value } fn read_i32_operand(code: &[u8], pc: &mut usize) -> i32 { let value = i32::from_le_bytes([code[*pc], code[*pc + 1], code[*pc + 2], code[*pc + 3]]); *pc += 4; value } fn read_reg_operand(code: &[u8], pc: &mut usize) -> Reg { read_u16_operand(code, pc) } impl Function { /// Disassemble the bytecode to a human-readable string. pub fn disassemble(&self) -> String { let mut out = String::new(); out.push_str(&format!( "function {}({} params, {} regs):\n", if self.name.is_empty() { "" } else { &self.name }, self.param_count, self.register_count, )); // Constants if !self.constants.is_empty() { out.push_str(" constants:\n"); for (i, c) in self.constants.iter().enumerate() { out.push_str(&format!(" [{i}] {c:?}\n")); } } // Names if !self.names.is_empty() { out.push_str(" names:\n"); for (i, n) in self.names.iter().enumerate() { out.push_str(&format!(" [{i}] \"{n}\"\n")); } } // Instructions out.push_str(" code:\n"); let code = &self.code; let mut pc = 0; while pc < code.len() { let offset = pc; let byte = code[pc]; pc += 1; let Some(op) = Op::from_byte(byte) else { out.push_str(&format!(" {offset:04X} \n")); continue; }; let line = match op { Op::LoadConst => { let dst = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); format!("LoadConst r{dst}, #{idx}") } Op::LoadNull => { let dst = read_reg_operand(code, &mut pc); format!("LoadNull r{dst}") } Op::LoadUndefined => { let dst = read_reg_operand(code, &mut pc); format!("LoadUndefined r{dst}") } Op::LoadTrue => { let dst = read_reg_operand(code, &mut pc); format!("LoadTrue r{dst}") } Op::LoadFalse => { let dst = read_reg_operand(code, &mut pc); format!("LoadFalse r{dst}") } Op::Move => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("Move r{dst}, r{src}") } Op::LoadGlobal => { let dst = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("LoadGlobal r{dst}, @{idx}(\"{name}\")") } Op::StoreGlobal => { let idx = read_u16_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("StoreGlobal @{idx}(\"{name}\"), r{src}") } Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Rem | Op::Exp => { let dst = read_reg_operand(code, &mut pc); let lhs = read_reg_operand(code, &mut pc); let rhs = read_reg_operand(code, &mut pc); format!("{op:?} r{dst}, r{lhs}, r{rhs}") } Op::Neg => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("Neg r{dst}, r{src}") } Op::BitAnd | Op::BitOr | Op::BitXor | Op::ShiftLeft | Op::ShiftRight | Op::UShiftRight => { let dst = read_reg_operand(code, &mut pc); let lhs = read_reg_operand(code, &mut pc); let rhs = read_reg_operand(code, &mut pc); format!("{op:?} r{dst}, r{lhs}, r{rhs}") } Op::BitNot => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("BitNot r{dst}, r{src}") } Op::Eq | Op::StrictEq | Op::NotEq | Op::StrictNotEq | Op::LessThan | Op::LessEq | Op::GreaterThan | Op::GreaterEq | Op::InstanceOf | Op::In => { let dst = read_reg_operand(code, &mut pc); let lhs = read_reg_operand(code, &mut pc); let rhs = read_reg_operand(code, &mut pc); format!("{op:?} r{dst}, r{lhs}, r{rhs}") } Op::LogicalNot => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("LogicalNot r{dst}, r{src}") } Op::TypeOf => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("TypeOf r{dst}, r{src}") } Op::Void => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("Void r{dst}, r{src}") } Op::Jump => { let off = read_i32_operand(code, &mut pc); let target = pc as i32 + off; format!("Jump {off:+} (-> {target:04X})") } Op::JumpIfTrue | Op::JumpIfFalse | Op::JumpIfNullish => { let reg = read_reg_operand(code, &mut pc); let off = read_i32_operand(code, &mut pc); let target = pc as i32 + off; format!("{op:?} r{reg}, {off:+} (-> {target:04X})") } Op::Call | Op::NewCall => { let dst = read_reg_operand(code, &mut pc); let func = read_reg_operand(code, &mut pc); let args_start = read_reg_operand(code, &mut pc); let arg_count = code[pc]; pc += 1; format!("{op:?} r{dst}, r{func}, r{args_start}, {arg_count}") } Op::CallSpread | Op::NewCallSpread => { let dst = read_reg_operand(code, &mut pc); let func = read_reg_operand(code, &mut pc); let args_array = read_reg_operand(code, &mut pc); format!("{op:?} r{dst}, r{func}, r{args_array}") } Op::Return => { let reg = read_reg_operand(code, &mut pc); format!("Return r{reg}") } Op::Throw => { let reg = read_reg_operand(code, &mut pc); format!("Throw r{reg}") } Op::CreateClosure => { let dst = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); format!("CreateClosure r{dst}, func#{idx}") } Op::GetProperty => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); let key = read_reg_operand(code, &mut pc); format!("GetProperty r{dst}, r{obj}, r{key}") } Op::SetProperty => { let obj = read_reg_operand(code, &mut pc); let key = read_reg_operand(code, &mut pc); let val = read_reg_operand(code, &mut pc); format!("SetProperty r{obj}, r{key}, r{val}") } Op::CreateObject => { let dst = read_reg_operand(code, &mut pc); format!("CreateObject r{dst}") } Op::CreateArray => { let dst = read_reg_operand(code, &mut pc); format!("CreateArray r{dst}") } Op::GetPropertyByName => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let ic = read_u16_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("GetPropertyByName r{dst}, r{obj}, @{idx}(\"{name}\"), ic{ic}") } Op::SetPropertyByName => { let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let val = read_reg_operand(code, &mut pc); let ic = read_u16_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("SetPropertyByName r{obj}, @{idx}(\"{name}\"), r{val}, ic{ic}") } Op::DefineMethod => { let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let f = read_reg_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("DefineMethod r{obj}, @{idx}(\"{name}\"), r{f}") } Op::DefineAccessor => { let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let kind = code[pc]; pc += 1; let f = read_reg_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); let k = if kind & 1 == 0 { "get" } else { "set" }; format!("DefineAccessor r{obj}, @{idx}(\"{name}\"), {k}, r{f}") } Op::SetHomeObject => { let f = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); format!("SetHomeObject r{f}, r{obj}") } Op::LoadSuperBase => { let dst = read_reg_operand(code, &mut pc); format!("LoadSuperBase r{dst}") } Op::GetPrivate => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("GetPrivate r{dst}, r{obj}, @{idx}(\"{name}\")") } Op::SetPrivate => { let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let val = read_reg_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("SetPrivate r{obj}, @{idx}(\"{name}\"), r{val}") } Op::HasPrivate => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("HasPrivate r{dst}, r{obj}, @{idx}(\"{name}\")") } Op::CopyDataProperties => { let target = read_reg_operand(code, &mut pc); let source = read_reg_operand(code, &mut pc); format!("CopyDataProperties r{target}, r{source}") } Op::Delete => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); let key = read_reg_operand(code, &mut pc); format!("Delete r{dst}, r{obj}, r{key}") } Op::LoadInt8 => { let dst = read_reg_operand(code, &mut pc); let val = code[pc] as i8; pc += 1; format!("LoadInt8 r{dst}, {val}") } Op::ForInInit => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); format!("ForInInit r{dst}, r{obj}") } Op::ForInNext => { let dst_val = read_reg_operand(code, &mut pc); let dst_done = read_reg_operand(code, &mut pc); let keys = read_reg_operand(code, &mut pc); let idx = read_reg_operand(code, &mut pc); format!("ForInNext r{dst_val}, r{dst_done}, r{keys}, r{idx}") } Op::SetPrototype => { let obj = read_reg_operand(code, &mut pc); let proto = read_reg_operand(code, &mut pc); format!("SetPrototype r{obj}, r{proto}") } Op::GetPrototype => { let dst = read_reg_operand(code, &mut pc); let obj = read_reg_operand(code, &mut pc); format!("GetPrototype r{dst}, r{obj}") } Op::PushExceptionHandler => { let catch_reg = read_reg_operand(code, &mut pc); let off = read_i32_operand(code, &mut pc); let target = pc as i32 + off; format!("PushExceptionHandler r{catch_reg}, @{target:04X}") } Op::PopExceptionHandler => "PopExceptionHandler".to_string(), Op::NewCell => { let dst = read_reg_operand(code, &mut pc); format!("NewCell r{dst}") } Op::CellLoad => { let dst = read_reg_operand(code, &mut pc); let cell = read_reg_operand(code, &mut pc); format!("CellLoad r{dst}, r{cell}") } Op::CellStore => { let cell = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("CellStore r{cell}, r{src}") } Op::LoadUpvalue => { let dst = read_reg_operand(code, &mut pc); let idx = read_u16_operand(code, &mut pc); format!("LoadUpvalue r{dst}, uv{idx}") } Op::StoreUpvalue => { let idx = read_u16_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("StoreUpvalue uv{idx}, r{src}") } Op::Yield => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("Yield r{dst}, r{src}") } Op::Spread => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("Spread r{dst}, r{src}") } Op::Await => { let dst = read_reg_operand(code, &mut pc); let src = read_reg_operand(code, &mut pc); format!("Await r{dst}, r{src}") } Op::BuildArguments => { let dst = read_reg_operand(code, &mut pc); format!("BuildArguments r{dst}") } Op::BuildRestArguments => { let dst = read_reg_operand(code, &mut pc); let start = read_u16_operand(code, &mut pc); format!("BuildRestArguments r{dst}, {start}") } }; out.push_str(&format!(" {offset:04X} {line}\n")); } // Nested functions for (i, f) in self.functions.iter().enumerate() { out.push_str(&format!("\n --- nested function [{i}] ---\n")); let nested_dis = f.disassemble(); for line in nested_dis.lines() { out.push_str(&format!(" {line}\n")); } } out } } impl fmt::Display for Function { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.disassemble()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_op_roundtrip() { // Verify all opcodes survive byte conversion. let ops = [ Op::LoadConst, Op::LoadNull, Op::LoadUndefined, Op::LoadTrue, Op::LoadFalse, Op::Move, Op::LoadGlobal, Op::StoreGlobal, Op::Add, Op::Sub, Op::Mul, Op::Div, Op::Rem, Op::Exp, Op::Neg, Op::BitAnd, Op::BitOr, Op::BitXor, Op::ShiftLeft, Op::ShiftRight, Op::UShiftRight, Op::BitNot, Op::Eq, Op::StrictEq, Op::NotEq, Op::StrictNotEq, Op::LessThan, Op::LessEq, Op::GreaterThan, Op::GreaterEq, Op::LogicalNot, Op::TypeOf, Op::InstanceOf, Op::In, Op::Void, Op::Jump, Op::JumpIfTrue, Op::JumpIfFalse, Op::JumpIfNullish, Op::Call, Op::Return, Op::Throw, Op::CreateClosure, Op::NewCall, Op::CallSpread, Op::NewCallSpread, Op::GetProperty, Op::SetProperty, Op::CreateObject, Op::CreateArray, Op::GetPropertyByName, Op::SetPropertyByName, Op::DefineMethod, Op::DefineAccessor, Op::SetHomeObject, Op::LoadSuperBase, Op::GetPrivate, Op::SetPrivate, Op::HasPrivate, Op::CopyDataProperties, Op::Delete, Op::LoadInt8, Op::ForInInit, Op::ForInNext, Op::SetPrototype, Op::GetPrototype, Op::PushExceptionHandler, Op::PopExceptionHandler, Op::NewCell, Op::CellLoad, Op::CellStore, Op::LoadUpvalue, Op::StoreUpvalue, Op::Yield, Op::Spread, Op::Await, Op::BuildArguments, Op::BuildRestArguments, ]; for op in ops { assert_eq!( Op::from_byte(op as u8), Some(op), "roundtrip failed for {op:?}" ); } } #[test] fn test_unknown_opcode() { assert_eq!(Op::from_byte(0xFF), None); assert_eq!(Op::from_byte(0x00), None); } #[test] fn test_constant_pool_dedup() { let mut b = BytecodeBuilder::new("test".into(), 0); let i1 = b.add_constant(Constant::Number(42.0)); let i2 = b.add_constant(Constant::Number(42.0)); assert_eq!(i1, i2); let i3 = b.add_constant(Constant::String("hello".into())); let i4 = b.add_constant(Constant::String("hello".into())); assert_eq!(i3, i4); assert_ne!(i1, i3); assert_eq!(b.func.constants.len(), 2); } #[test] fn test_name_dedup() { let mut b = BytecodeBuilder::new("test".into(), 0); let i1 = b.add_name("foo"); let i2 = b.add_name("foo"); assert_eq!(i1, i2); let i3 = b.add_name("bar"); assert_ne!(i1, i3); } #[test] fn test_emit_and_disassemble() { let mut b = BytecodeBuilder::new("test".into(), 0); b.func.register_count = 3; let ci = b.add_constant(Constant::Number(10.0)); b.emit_reg_u16(Op::LoadConst, 0, ci); b.emit_reg(Op::LoadNull, 1); b.emit_reg3(Op::Add, 2, 0, 1); b.emit_reg(Op::Return, 2); let func = b.finish(); let dis = func.disassemble(); assert!(dis.contains("LoadConst r0, #0")); assert!(dis.contains("LoadNull r1")); assert!(dis.contains("Add r2, r0, r1")); assert!(dis.contains("Return r2")); } #[test] fn test_jump_patching() { let mut b = BytecodeBuilder::new("test".into(), 0); b.func.register_count = 1; b.emit_reg(Op::LoadTrue, 0); let patch = b.emit_cond_jump(Op::JumpIfFalse, 0); b.emit_reg(Op::LoadNull, 0); b.patch_jump(patch); b.emit_reg(Op::Return, 0); let func = b.finish(); let dis = func.disassemble(); // The jump should target the Return instruction assert!(dis.contains("JumpIfFalse")); assert!(dis.contains("Return")); } #[test] fn test_load_int8() { let mut b = BytecodeBuilder::new("test".into(), 0); b.func.register_count = 1; b.emit_load_int8(0, 42); b.emit_load_int8(0, -1); let func = b.finish(); let dis = func.disassemble(); assert!(dis.contains("LoadInt8 r0, 42")); assert!(dis.contains("LoadInt8 r0, -1")); } #[test] fn test_inline_cache_uninitialized() { let ic = InlineCache::new(); assert!(ic.lookup(ShapeId::ROOT).is_none()); assert!(!ic.is_megamorphic()); } #[test] fn test_inline_cache_monomorphic() { let mut ic = InlineCache::new(); let shape = ShapeId::ROOT; ic.update(shape, 0); assert_eq!(ic.lookup(shape), Some(0)); assert!(matches!(ic.state, InlineCacheState::Monomorphic(_))); } #[test] fn test_inline_cache_polymorphic() { use crate::shape::{PropertyAttrs, ShapeTable}; let mut table = ShapeTable::new(); let s1 = table.add_property(ShapeId::ROOT, "x", PropertyAttrs::DEFAULT); let s2 = table.add_property(ShapeId::ROOT, "y", PropertyAttrs::DEFAULT); let mut ic = InlineCache::new(); ic.update(s1, 0); ic.update(s2, 0); assert!(matches!(ic.state, InlineCacheState::Polymorphic(_))); assert_eq!(ic.lookup(s1), Some(0)); assert_eq!(ic.lookup(s2), Some(0)); } #[test] fn test_inline_cache_megamorphic() { use crate::shape::{PropertyAttrs, ShapeTable}; let mut table = ShapeTable::new(); let shapes: Vec<_> = (0..5) .map(|i| table.add_property(ShapeId::ROOT, &format!("p{i}"), PropertyAttrs::DEFAULT)) .collect(); let mut ic = InlineCache::new(); for (i, &s) in shapes.iter().enumerate() { ic.update(s, i as u32); } assert!(ic.is_megamorphic()); // Megamorphic always returns None. for &s in &shapes { assert!(ic.lookup(s).is_none()); } } #[test] fn test_inline_cache_duplicate_update() { let mut ic = InlineCache::new(); ic.update(ShapeId::ROOT, 0); ic.update(ShapeId::ROOT, 0); assert!(matches!(ic.state, InlineCacheState::Monomorphic(_))); } #[test] fn test_emit_prop_name_allocates_ic() { let mut b = BytecodeBuilder::new("test".into(), 0); b.func.register_count = 2; let ni = b.add_name("x"); b.emit_get_prop_name(0, 1, ni); b.emit_set_prop_name(1, ni, 0); let func = b.finish(); assert_eq!(func.inline_caches.len(), 2); let dis = func.disassemble(); assert!(dis.contains("ic0")); assert!(dis.contains("ic1")); } }