//! 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 std::fmt; /// A register index (0–255). pub type Reg = u8; /// An index into the constant pool. pub type ConstIdx = u16; /// An index into the name/string table. pub type NameIdx = u16; /// 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, // ── 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, // ── 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, } 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), 0x60 => Some(Op::GetProperty), 0x61 => Some(Op::SetProperty), 0x62 => Some(Op::CreateObject), 0x63 => Some(Op::CreateArray), 0x64 => Some(Op::GetPropertyByName), 0x65 => Some(Op::SetPropertyByName), 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), _ => 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: u8, } /// 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: u8, /// 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 (sorted by offset). pub source_map: Vec<(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, } 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, } } } /// 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_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_u8(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_u8(a); self.emit_u8(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_u8(a); self.emit_u8(b); self.emit_u8(c); self.emit_u8(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_u8(dst); self.emit_u8(lhs); self.emit_u8(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_u8(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_u8(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_u8(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_u8(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_u8(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_u8(dst); self.emit_u8(func); self.emit_u8(args_start); self.emit_u8(arg_count); } /// Emit: GetPropertyByName dst, obj, name_idx pub fn emit_get_prop_name(&mut self, dst: Reg, obj: Reg, name_idx: NameIdx) { self.emit_u8(Op::GetPropertyByName as u8); self.emit_u8(dst); self.emit_u8(obj); self.emit_u16(name_idx); } /// 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_u8(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 pub fn emit_set_prop_name(&mut self, obj: Reg, name_idx: NameIdx, val: Reg) { self.emit_u8(Op::SetPropertyByName as u8); self.emit_u8(obj); self.emit_u16(name_idx); self.emit_u8(val); } /// Emit: LoadInt8 dst, value pub fn emit_load_int8(&mut self, dst: Reg, value: i8) { self.emit_u8(Op::LoadInt8 as u8); self.emit_u8(dst); self.emit_u8(value as u8); } /// Emit: LoadUpvalue dst, idx pub fn emit_load_upvalue(&mut self, dst: Reg, idx: u8) { self.emit_u8(Op::LoadUpvalue as u8); self.emit_u8(dst); self.emit_u8(idx); } /// Emit: StoreUpvalue idx, src pub fn emit_store_upvalue(&mut self, idx: u8, src: Reg) { self.emit_u8(Op::StoreUpvalue as u8); self.emit_u8(idx); self.emit_u8(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_u8(dst); self.emit_u8(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_u8(dst_array); self.emit_u8(src); } /// Add a source map entry: current bytecode offset → source line. pub fn add_source_map(&mut self, line: u32) { let offset = self.offset() as u32; self.func.source_map.push((offset, line)); } /// Finalize and return the compiled function. pub fn finish(self) -> Function { self.func } } // ── Disassembler ──────────────────────────────────────────── 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 = code[pc]; pc += 1; let idx = u16::from_le_bytes([code[pc], code[pc + 1]]); pc += 2; format!("LoadConst r{dst}, #{idx}") } Op::LoadNull => { let dst = code[pc]; pc += 1; format!("LoadNull r{dst}") } Op::LoadUndefined => { let dst = code[pc]; pc += 1; format!("LoadUndefined r{dst}") } Op::LoadTrue => { let dst = code[pc]; pc += 1; format!("LoadTrue r{dst}") } Op::LoadFalse => { let dst = code[pc]; pc += 1; format!("LoadFalse r{dst}") } Op::Move => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("Move r{dst}, r{src}") } Op::LoadGlobal => { let dst = code[pc]; pc += 1; let idx = u16::from_le_bytes([code[pc], code[pc + 1]]); pc += 2; 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 = u16::from_le_bytes([code[pc], code[pc + 1]]); pc += 2; let src = code[pc]; pc += 1; 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 = code[pc]; let lhs = code[pc + 1]; let rhs = code[pc + 2]; pc += 3; format!("{op:?} r{dst}, r{lhs}, r{rhs}") } Op::Neg => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("Neg r{dst}, r{src}") } Op::BitAnd | Op::BitOr | Op::BitXor | Op::ShiftLeft | Op::ShiftRight | Op::UShiftRight => { let dst = code[pc]; let lhs = code[pc + 1]; let rhs = code[pc + 2]; pc += 3; format!("{op:?} r{dst}, r{lhs}, r{rhs}") } Op::BitNot => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; 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 = code[pc]; let lhs = code[pc + 1]; let rhs = code[pc + 2]; pc += 3; format!("{op:?} r{dst}, r{lhs}, r{rhs}") } Op::LogicalNot => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("LogicalNot r{dst}, r{src}") } Op::TypeOf => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("TypeOf r{dst}, r{src}") } Op::Void => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("Void r{dst}, r{src}") } Op::Jump => { let off = i32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]); pc += 4; let target = pc as i32 + off; format!("Jump {off:+} (-> {target:04X})") } Op::JumpIfTrue | Op::JumpIfFalse | Op::JumpIfNullish => { let reg = code[pc]; pc += 1; let off = i32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]); pc += 4; let target = pc as i32 + off; format!("{op:?} r{reg}, {off:+} (-> {target:04X})") } Op::Call => { let dst = code[pc]; let func = code[pc + 1]; let args_start = code[pc + 2]; let arg_count = code[pc + 3]; pc += 4; format!("Call r{dst}, r{func}, r{args_start}, {arg_count}") } Op::Return => { let reg = code[pc]; pc += 1; format!("Return r{reg}") } Op::Throw => { let reg = code[pc]; pc += 1; format!("Throw r{reg}") } Op::CreateClosure => { let dst = code[pc]; pc += 1; let idx = u16::from_le_bytes([code[pc], code[pc + 1]]); pc += 2; format!("CreateClosure r{dst}, func#{idx}") } Op::GetProperty => { let dst = code[pc]; let obj = code[pc + 1]; let key = code[pc + 2]; pc += 3; format!("GetProperty r{dst}, r{obj}, r{key}") } Op::SetProperty => { let obj = code[pc]; let key = code[pc + 1]; let val = code[pc + 2]; pc += 3; format!("SetProperty r{obj}, r{key}, r{val}") } Op::CreateObject => { let dst = code[pc]; pc += 1; format!("CreateObject r{dst}") } Op::CreateArray => { let dst = code[pc]; pc += 1; format!("CreateArray r{dst}") } Op::GetPropertyByName => { let dst = code[pc]; let obj = code[pc + 1]; pc += 2; let idx = u16::from_le_bytes([code[pc], code[pc + 1]]); pc += 2; let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("GetPropertyByName r{dst}, r{obj}, @{idx}(\"{name}\")") } Op::SetPropertyByName => { let obj = code[pc]; pc += 1; let idx = u16::from_le_bytes([code[pc], code[pc + 1]]); pc += 2; let val = code[pc]; pc += 1; let name = self .names .get(idx as usize) .map(|s| s.as_str()) .unwrap_or("?"); format!("SetPropertyByName r{obj}, @{idx}(\"{name}\"), r{val}") } Op::Delete => { let dst = code[pc]; let obj = code[pc + 1]; let key = code[pc + 2]; pc += 3; format!("Delete r{dst}, r{obj}, r{key}") } Op::LoadInt8 => { let dst = code[pc]; let val = code[pc + 1] as i8; pc += 2; format!("LoadInt8 r{dst}, {val}") } Op::ForInInit => { let dst = code[pc]; let obj = code[pc + 1]; pc += 2; format!("ForInInit r{dst}, r{obj}") } Op::ForInNext => { let dst_val = code[pc]; let dst_done = code[pc + 1]; let keys = code[pc + 2]; let idx = code[pc + 3]; pc += 4; format!("ForInNext r{dst_val}, r{dst_done}, r{keys}, r{idx}") } Op::SetPrototype => { let obj = code[pc]; let proto = code[pc + 1]; pc += 2; format!("SetPrototype r{obj}, r{proto}") } Op::GetPrototype => { let dst = code[pc]; let obj = code[pc + 1]; pc += 2; format!("GetPrototype r{dst}, r{obj}") } Op::PushExceptionHandler => { let catch_reg = code[pc]; let b0 = code[pc + 1] as i32; let b1 = code[pc + 2] as i32; let b2 = code[pc + 3] as i32; let b3 = code[pc + 4] as i32; let off = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); let target = (offset as i32 + 1 + 5 + off) as usize; pc += 5; format!("PushExceptionHandler r{catch_reg}, @{target:04X}") } Op::PopExceptionHandler => "PopExceptionHandler".to_string(), Op::NewCell => { let dst = code[pc]; pc += 1; format!("NewCell r{dst}") } Op::CellLoad => { let dst = code[pc]; let cell = code[pc + 1]; pc += 2; format!("CellLoad r{dst}, r{cell}") } Op::CellStore => { let cell = code[pc]; let src = code[pc + 1]; pc += 2; format!("CellStore r{cell}, r{src}") } Op::LoadUpvalue => { let dst = code[pc]; let idx = code[pc + 1]; pc += 2; format!("LoadUpvalue r{dst}, uv{idx}") } Op::StoreUpvalue => { let idx = code[pc]; let src = code[pc + 1]; pc += 2; format!("StoreUpvalue uv{idx}, r{src}") } Op::Yield => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("Yield r{dst}, r{src}") } Op::Spread => { let dst = code[pc]; let src = code[pc + 1]; pc += 2; format!("Spread r{dst}, r{src}") } }; 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::GetProperty, Op::SetProperty, Op::CreateObject, Op::CreateArray, Op::GetPropertyByName, Op::SetPropertyByName, 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, ]; 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")); } }