diff --git a/crates/js/src/bytecode.rs b/crates/js/src/bytecode.rs new file mode 100644 index 0000000..5344e9f --- /dev/null +++ b/crates/js/src/bytecode.rs @@ -0,0 +1,892 @@ +//! 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, +} + +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), + _ => None, + } + } +} + +/// A constant value stored in the constant pool. +#[derive(Debug, Clone, PartialEq)] +pub enum Constant { + Number(f64), + String(String), +} + +/// 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)>, +} + +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(), + } + } +} + +/// 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: 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) { + let target = self.offset() as i32; + let offset = target - (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: 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); + } + + /// 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}") + } + }; + 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, + ]; + 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")); + } +} diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs new file mode 100644 index 0000000..dd9d200 --- /dev/null +++ b/crates/js/src/compiler.rs @@ -0,0 +1,1750 @@ +//! AST → register-based bytecode compiler. +//! +//! Walks the AST produced by the parser and emits bytecode instructions. +//! Uses a simple greedy register allocator: each new temporary gets the next +//! available register, and registers are freed when no longer needed. + +use crate::ast::*; +use crate::bytecode::*; +use crate::JsError; + +/// Compiler state for a single function scope. +struct FunctionCompiler { + builder: BytecodeBuilder, + /// Maps local variable names to their register slots. + locals: Vec, + /// Next free register index. + next_reg: u8, + /// Stack of loop contexts for break/continue. + loop_stack: Vec, +} + +#[derive(Debug, Clone)] +struct Local { + name: String, + reg: Reg, +} + +struct LoopCtx { + /// Label, if this is a labeled loop. + label: Option, + /// Bytecode offset of the loop condition check (for `continue`). + continue_target: usize, + /// Patch positions for break jumps. + break_patches: Vec, +} + +impl FunctionCompiler { + fn new(name: String, param_count: u8) -> Self { + Self { + builder: BytecodeBuilder::new(name, param_count), + locals: Vec::new(), + next_reg: 0, + loop_stack: Vec::new(), + } + } + + /// Allocate a register, updating the high-water mark. + fn alloc_reg(&mut self) -> Reg { + let r = self.next_reg; + self.next_reg = self.next_reg.checked_add(1).expect("register overflow"); + if self.next_reg > self.builder.func.register_count { + self.builder.func.register_count = self.next_reg; + } + r + } + + /// Free the last allocated register (must be called in reverse order). + fn free_reg(&mut self, r: Reg) { + debug_assert_eq!( + r, + self.next_reg - 1, + "registers must be freed in reverse order" + ); + self.next_reg -= 1; + } + + /// Look up a local variable by name. + fn find_local(&self, name: &str) -> Option { + self.locals + .iter() + .rev() + .find(|l| l.name == name) + .map(|l| l.reg) + } + + /// Define a local variable. + fn define_local(&mut self, name: &str) -> Reg { + let reg = self.alloc_reg(); + self.locals.push(Local { + name: name.to_string(), + reg, + }); + reg + } +} + +/// Compile a parsed program into a top-level bytecode function. +pub fn compile(program: &Program) -> Result { + let mut fc = FunctionCompiler::new("
".into(), 0); + + // Reserve r0 for the implicit return value. + let result_reg = fc.alloc_reg(); + fc.builder.emit_reg(Op::LoadUndefined, result_reg); + + compile_stmts(&mut fc, &program.body, result_reg)?; + + fc.builder.emit_reg(Op::Return, result_reg); + Ok(fc.builder.finish()) +} + +fn compile_stmts( + fc: &mut FunctionCompiler, + stmts: &[Stmt], + result_reg: Reg, +) -> Result<(), JsError> { + for stmt in stmts { + compile_stmt(fc, stmt, result_reg)?; + } + Ok(()) +} + +fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Result<(), JsError> { + match &stmt.kind { + StmtKind::Expr(expr) => { + // Expression statement: compile and store result in result_reg. + compile_expr(fc, expr, result_reg)?; + } + + StmtKind::Block(stmts) => { + let saved_locals = fc.locals.len(); + let saved_next = fc.next_reg; + compile_stmts(fc, stmts, result_reg)?; + // Pop locals from this block. + fc.locals.truncate(saved_locals); + fc.next_reg = saved_next; + } + + StmtKind::VarDecl { + kind: _, + declarators, + } => { + for decl in declarators { + compile_var_declarator(fc, decl)?; + } + } + + StmtKind::FunctionDecl(func_def) => { + compile_function_decl(fc, func_def)?; + } + + StmtKind::If { + test, + consequent, + alternate, + } => { + compile_if(fc, test, consequent, alternate.as_deref(), result_reg)?; + } + + StmtKind::While { test, body } => { + compile_while(fc, test, body, None, result_reg)?; + } + + StmtKind::DoWhile { body, test } => { + compile_do_while(fc, body, test, None, result_reg)?; + } + + StmtKind::For { + init, + test, + update, + body, + } => { + compile_for( + fc, + init.as_ref(), + test.as_ref(), + update.as_ref(), + body, + None, + result_reg, + )?; + } + + StmtKind::ForIn { left, right, body } => { + // For-in is complex; emit a stub that evaluates RHS, then TODO at runtime. + let _ = left; + let tmp = fc.alloc_reg(); + compile_expr(fc, right, tmp)?; + fc.free_reg(tmp); + // For now, just compile the body once (the VM will handle iteration). + compile_stmt(fc, body, result_reg)?; + } + + StmtKind::ForOf { + left, + right, + body, + is_await: _, + } => { + let _ = left; + let tmp = fc.alloc_reg(); + compile_expr(fc, right, tmp)?; + fc.free_reg(tmp); + compile_stmt(fc, body, result_reg)?; + } + + StmtKind::Return(expr) => { + let ret_reg = fc.alloc_reg(); + if let Some(e) = expr { + compile_expr(fc, e, ret_reg)?; + } else { + fc.builder.emit_reg(Op::LoadUndefined, ret_reg); + } + fc.builder.emit_reg(Op::Return, ret_reg); + fc.free_reg(ret_reg); + } + + StmtKind::Throw(expr) => { + let tmp = fc.alloc_reg(); + compile_expr(fc, expr, tmp)?; + fc.builder.emit_reg(Op::Throw, tmp); + fc.free_reg(tmp); + } + + StmtKind::Break(label) => { + // Find the matching loop context. + let idx = find_loop_ctx(&fc.loop_stack, label.as_deref()) + .ok_or_else(|| JsError::SyntaxError("break outside of loop".into()))?; + let patch = fc.builder.emit_jump(Op::Jump); + fc.loop_stack[idx].break_patches.push(patch); + } + + StmtKind::Continue(label) => { + let idx = find_loop_ctx(&fc.loop_stack, label.as_deref()) + .ok_or_else(|| JsError::SyntaxError("continue outside of loop".into()))?; + let target = fc.loop_stack[idx].continue_target; + fc.builder.emit_jump_to(target); + } + + StmtKind::Labeled { label, body } => { + // If body is a loop, propagate the label. + match &body.kind { + StmtKind::While { test, body: inner } => { + compile_while(fc, test, inner, Some(label.clone()), result_reg)?; + } + StmtKind::DoWhile { body: inner, test } => { + compile_do_while(fc, inner, test, Some(label.clone()), result_reg)?; + } + StmtKind::For { + init, + test, + update, + body: inner, + } => { + compile_for( + fc, + init.as_ref(), + test.as_ref(), + update.as_ref(), + inner, + Some(label.clone()), + result_reg, + )?; + } + _ => { + compile_stmt(fc, body, result_reg)?; + } + } + } + + StmtKind::Switch { + discriminant, + cases, + } => { + compile_switch(fc, discriminant, cases, result_reg)?; + } + + StmtKind::Try { + block, + handler, + finalizer, + } => { + // Simplified: compile blocks sequentially. + // Real try/catch needs exception table support from the VM. + compile_stmts(fc, block, result_reg)?; + if let Some(catch) = handler { + compile_stmts(fc, &catch.body, result_reg)?; + } + if let Some(fin) = finalizer { + compile_stmts(fc, fin, result_reg)?; + } + } + + StmtKind::Empty | StmtKind::Debugger => { + // No-op. + } + + StmtKind::With { object, body } => { + // Compile `with` as: evaluate object (discard), then run body. + // Proper `with` scope requires VM support. + let tmp = fc.alloc_reg(); + compile_expr(fc, object, tmp)?; + fc.free_reg(tmp); + compile_stmt(fc, body, result_reg)?; + } + + StmtKind::Import { .. } => { + // Module imports are resolved before execution; no bytecode needed. + } + + StmtKind::Export(export) => { + compile_export(fc, export, result_reg)?; + } + + StmtKind::ClassDecl(class_def) => { + compile_class_decl(fc, class_def)?; + } + } + Ok(()) +} + +// ── Variable declarations ─────────────────────────────────── + +fn compile_var_declarator(fc: &mut FunctionCompiler, decl: &VarDeclarator) -> Result<(), JsError> { + match &decl.pattern.kind { + PatternKind::Identifier(name) => { + let reg = fc.define_local(name); + if let Some(init) = &decl.init { + compile_expr(fc, init, reg)?; + } else { + fc.builder.emit_reg(Op::LoadUndefined, reg); + } + } + _ => { + // Destructuring: evaluate init, then bind patterns. + let tmp = fc.alloc_reg(); + if let Some(init) = &decl.init { + compile_expr(fc, init, tmp)?; + } else { + fc.builder.emit_reg(Op::LoadUndefined, tmp); + } + compile_destructuring_pattern(fc, &decl.pattern, tmp)?; + fc.free_reg(tmp); + } + } + Ok(()) +} + +fn compile_destructuring_pattern( + fc: &mut FunctionCompiler, + pattern: &Pattern, + src: Reg, +) -> Result<(), JsError> { + match &pattern.kind { + PatternKind::Identifier(name) => { + let reg = fc.define_local(name); + fc.builder.emit_reg_reg(Op::Move, reg, src); + } + PatternKind::Object { + properties, + rest: _, + } => { + for prop in properties { + let key_name = match &prop.key { + PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(), + _ => { + return Err(JsError::SyntaxError( + "computed destructuring keys not yet supported".into(), + )); + } + }; + let val_reg = fc.alloc_reg(); + let name_idx = fc.builder.add_name(&key_name); + fc.builder.emit_get_prop_name(val_reg, src, name_idx); + compile_destructuring_pattern(fc, &prop.value, val_reg)?; + fc.free_reg(val_reg); + } + } + PatternKind::Array { elements, rest: _ } => { + for (i, elem) in elements.iter().enumerate() { + if let Some(pat) = elem { + let idx_reg = fc.alloc_reg(); + if i <= 127 { + fc.builder.emit_load_int8(idx_reg, i as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(i as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + } + let val_reg = fc.alloc_reg(); + fc.builder.emit_reg3(Op::GetProperty, val_reg, src, idx_reg); + compile_destructuring_pattern(fc, pat, val_reg)?; + fc.free_reg(val_reg); + fc.free_reg(idx_reg); + } + } + } + PatternKind::Assign { left, right } => { + // Default value: if src is undefined, use default. + let val_reg = fc.alloc_reg(); + fc.builder.emit_reg_reg(Op::Move, val_reg, src); + // Check if undefined, if so use default. + let check_reg = fc.alloc_reg(); + let undef_reg = fc.alloc_reg(); + fc.builder.emit_reg(Op::LoadUndefined, undef_reg); + fc.builder + .emit_reg3(Op::StrictEq, check_reg, val_reg, undef_reg); + fc.free_reg(undef_reg); + let patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, check_reg); + fc.free_reg(check_reg); + // Is undefined → evaluate default. + compile_expr(fc, right, val_reg)?; + fc.builder.patch_jump(patch); + compile_destructuring_pattern(fc, left, val_reg)?; + fc.free_reg(val_reg); + } + } + Ok(()) +} + +// ── Function declarations ─────────────────────────────────── + +fn compile_function_decl(fc: &mut FunctionCompiler, func_def: &FunctionDef) -> Result<(), JsError> { + let name = func_def.id.clone().unwrap_or_default(); + let inner = compile_function_body(func_def)?; + let func_idx = fc.builder.add_function(inner); + + let reg = fc.define_local(&name); + fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx); + Ok(()) +} + +fn compile_function_body(func_def: &FunctionDef) -> Result { + let name = func_def.id.clone().unwrap_or_default(); + let param_count = func_def.params.len().min(255) as u8; + let mut inner = FunctionCompiler::new(name, param_count); + + // Allocate registers for parameters. + for p in &func_def.params { + if let PatternKind::Identifier(name) = &p.kind { + inner.define_local(name); + } else { + // Destructuring param: allocate a register for the whole param, + // then destructure from it. + let _ = inner.alloc_reg(); + } + } + + // Result register for the function body. + let result_reg = inner.alloc_reg(); + inner.builder.emit_reg(Op::LoadUndefined, result_reg); + + compile_stmts(&mut inner, &func_def.body, result_reg)?; + + // Implicit return undefined. + inner.builder.emit_reg(Op::Return, result_reg); + Ok(inner.builder.finish()) +} + +// ── Class declarations ────────────────────────────────────── + +fn compile_class_decl(fc: &mut FunctionCompiler, class_def: &ClassDef) -> Result<(), JsError> { + let name = class_def.id.clone().unwrap_or_default(); + let reg = fc.define_local(&name); + + // Find constructor or create empty one. + let ctor = class_def.body.iter().find(|m| { + matches!( + &m.kind, + ClassMemberKind::Method { + kind: MethodKind::Constructor, + .. + } + ) + }); + + if let Some(member) = ctor { + if let ClassMemberKind::Method { value, .. } = &member.kind { + let inner = compile_function_body(value)?; + let func_idx = fc.builder.add_function(inner); + fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx); + } + } else { + // No constructor: create an empty function. + let empty = Function::new(name.clone(), 0); + let func_idx = fc.builder.add_function(empty); + fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx); + } + + // Compile methods: set them as properties on the constructor's prototype. + // This is simplified — real class compilation needs prototype chain setup. + for member in &class_def.body { + match &member.kind { + ClassMemberKind::Method { + key, + value, + kind, + is_static: _, + computed: _, + } => { + if matches!(kind, MethodKind::Constructor) { + continue; + } + let method_name = match key { + PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(), + _ => continue, + }; + let inner = compile_function_body(value)?; + let func_idx = fc.builder.add_function(inner); + let method_reg = fc.alloc_reg(); + fc.builder + .emit_reg_u16(Op::CreateClosure, method_reg, func_idx); + let name_idx = fc.builder.add_name(&method_name); + fc.builder.emit_set_prop_name(reg, name_idx, method_reg); + fc.free_reg(method_reg); + } + ClassMemberKind::Property { .. } => { + // Class fields are set in constructor; skip here. + } + } + } + + Ok(()) +} + +// ── Export ─────────────────────────────────────────────────── + +fn compile_export( + fc: &mut FunctionCompiler, + export: &ExportDecl, + + result_reg: Reg, +) -> Result<(), JsError> { + match export { + ExportDecl::Declaration(stmt) => { + compile_stmt(fc, stmt, result_reg)?; + } + ExportDecl::Default(expr) => { + compile_expr(fc, expr, result_reg)?; + } + ExportDecl::Named { .. } | ExportDecl::AllFrom(_) => { + // Named re-exports are module-level; no bytecode needed. + } + } + Ok(()) +} + +// ── Control flow ──────────────────────────────────────────── + +fn compile_if( + fc: &mut FunctionCompiler, + test: &Expr, + consequent: &Stmt, + alternate: Option<&Stmt>, + + result_reg: Reg, +) -> Result<(), JsError> { + let cond = fc.alloc_reg(); + compile_expr(fc, test, cond)?; + let else_patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond); + fc.free_reg(cond); + + compile_stmt(fc, consequent, result_reg)?; + + if let Some(alt) = alternate { + let end_patch = fc.builder.emit_jump(Op::Jump); + fc.builder.patch_jump(else_patch); + compile_stmt(fc, alt, result_reg)?; + fc.builder.patch_jump(end_patch); + } else { + fc.builder.patch_jump(else_patch); + } + Ok(()) +} + +fn compile_while( + fc: &mut FunctionCompiler, + test: &Expr, + body: &Stmt, + label: Option, + + result_reg: Reg, +) -> Result<(), JsError> { + let loop_start = fc.builder.offset(); + + let cond = fc.alloc_reg(); + compile_expr(fc, test, cond)?; + let exit_patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond); + fc.free_reg(cond); + + fc.loop_stack.push(LoopCtx { + label, + continue_target: loop_start, + break_patches: Vec::new(), + }); + + compile_stmt(fc, body, result_reg)?; + fc.builder.emit_jump_to(loop_start); + fc.builder.patch_jump(exit_patch); + + let ctx = fc.loop_stack.pop().unwrap(); + for patch in ctx.break_patches { + fc.builder.patch_jump(patch); + } + Ok(()) +} + +fn compile_do_while( + fc: &mut FunctionCompiler, + body: &Stmt, + test: &Expr, + label: Option, + + result_reg: Reg, +) -> Result<(), JsError> { + let loop_start = fc.builder.offset(); + + fc.loop_stack.push(LoopCtx { + label, + continue_target: loop_start, + break_patches: Vec::new(), + }); + + compile_stmt(fc, body, result_reg)?; + + let cond = fc.alloc_reg(); + compile_expr(fc, test, cond)?; + fc.builder + .emit_cond_jump_to(Op::JumpIfTrue, cond, loop_start); + fc.free_reg(cond); + + let ctx = fc.loop_stack.pop().unwrap(); + for patch in ctx.break_patches { + fc.builder.patch_jump(patch); + } + Ok(()) +} + +fn compile_for( + fc: &mut FunctionCompiler, + init: Option<&ForInit>, + test: Option<&Expr>, + update: Option<&Expr>, + body: &Stmt, + label: Option, + + result_reg: Reg, +) -> Result<(), JsError> { + let saved_locals = fc.locals.len(); + let saved_next = fc.next_reg; + + // Init. + if let Some(init) = init { + match init { + ForInit::VarDecl { + kind: _, + declarators, + } => { + for decl in declarators { + compile_var_declarator(fc, decl)?; + } + } + ForInit::Expr(expr) => { + let tmp = fc.alloc_reg(); + compile_expr(fc, expr, tmp)?; + fc.free_reg(tmp); + } + } + } + + let loop_start = fc.builder.offset(); + + // Test. + let exit_patch = if let Some(test) = test { + let cond = fc.alloc_reg(); + compile_expr(fc, test, cond)?; + let patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond); + fc.free_reg(cond); + Some(patch) + } else { + None + }; + + // continue_target points to the update expression (or loop_start if no update). + // We'll set this after compiling the body. + let continue_placeholder = fc.builder.offset(); + fc.loop_stack.push(LoopCtx { + label, + continue_target: continue_placeholder, // will be updated + break_patches: Vec::new(), + }); + + compile_stmt(fc, body, result_reg)?; + + // Set the continue target to the update expression position. + let continue_target = fc.builder.offset(); + let loop_idx = fc.loop_stack.len() - 1; + fc.loop_stack[loop_idx].continue_target = continue_target; + + // Update. + if let Some(update) = update { + let tmp = fc.alloc_reg(); + compile_expr(fc, update, tmp)?; + fc.free_reg(tmp); + } + + fc.builder.emit_jump_to(loop_start); + + if let Some(patch) = exit_patch { + fc.builder.patch_jump(patch); + } + + let ctx = fc.loop_stack.pop().unwrap(); + for patch in ctx.break_patches { + fc.builder.patch_jump(patch); + } + + fc.locals.truncate(saved_locals); + fc.next_reg = saved_next; + Ok(()) +} + +fn compile_switch( + fc: &mut FunctionCompiler, + discriminant: &Expr, + cases: &[SwitchCase], + + result_reg: Reg, +) -> Result<(), JsError> { + let disc_reg = fc.alloc_reg(); + compile_expr(fc, discriminant, disc_reg)?; + + // Use a loop context for break statements. + fc.loop_stack.push(LoopCtx { + label: None, + continue_target: 0, // not applicable for switch + break_patches: Vec::new(), + }); + + let mut case_patches = Vec::new(); + let mut default_patch = None; + + // Phase 1: emit comparison jumps for each case. + for case in cases { + if let Some(test) = &case.test { + let test_reg = fc.alloc_reg(); + compile_expr(fc, test, test_reg)?; + let cmp_reg = fc.alloc_reg(); + fc.builder + .emit_reg3(Op::StrictEq, cmp_reg, disc_reg, test_reg); + let patch = fc.builder.emit_cond_jump(Op::JumpIfTrue, cmp_reg); + fc.free_reg(cmp_reg); + fc.free_reg(test_reg); + case_patches.push(patch); + } else { + // Default case: jump is emitted after all test comparisons. + case_patches.push(0); // placeholder + default_patch = Some(case_patches.len() - 1); + } + } + + // Jump to default or end if no case matched. + let end_jump = if default_patch.is_some() { + None + } else { + Some(fc.builder.emit_jump(Op::Jump)) + }; + + // Phase 2: emit case bodies. + for (i, case) in cases.iter().enumerate() { + if default_patch == Some(i) { + // Patch the "no match" jump to here if this is the default. + if let Some(end_j) = end_jump { + fc.builder.patch_jump(end_j); + } + // We need to update the default case patch to point here. + let default_jump_target = fc.builder.offset(); + // The default_patch entry is a placeholder; we don't jump TO it. + // Instead, if no case matched and there's a default, we jump here. + // Let's fix: emit a Jump before the first case body that jumps to default. + let _ = default_jump_target; + } + + fc.builder.patch_jump(case_patches[i]); + compile_stmts(fc, &case.consequent, result_reg)?; + } + + if let Some(end_j) = end_jump { + fc.builder.patch_jump(end_j); + } + + fc.free_reg(disc_reg); + + let ctx = fc.loop_stack.pop().unwrap(); + for patch in ctx.break_patches { + fc.builder.patch_jump(patch); + } + Ok(()) +} + +fn find_loop_ctx(stack: &[LoopCtx], label: Option<&str>) -> Option { + if let Some(label) = label { + stack + .iter() + .rposition(|ctx| ctx.label.as_deref() == Some(label)) + } else { + if stack.is_empty() { + None + } else { + Some(stack.len() - 1) + } + } +} + +// ── Expressions ───────────────────────────────────────────── + +fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), JsError> { + match &expr.kind { + ExprKind::Number(n) => { + // Optimize small integers. + let int_val = *n as i64; + if int_val as f64 == *n && (-128..=127).contains(&int_val) { + fc.builder.emit_load_int8(dst, int_val as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(*n)); + fc.builder.emit_reg_u16(Op::LoadConst, dst, ci); + } + } + + ExprKind::String(s) => { + let ci = fc.builder.add_constant(Constant::String(s.clone())); + fc.builder.emit_reg_u16(Op::LoadConst, dst, ci); + } + + ExprKind::Bool(true) => { + fc.builder.emit_reg(Op::LoadTrue, dst); + } + + ExprKind::Bool(false) => { + fc.builder.emit_reg(Op::LoadFalse, dst); + } + + ExprKind::Null => { + fc.builder.emit_reg(Op::LoadNull, dst); + } + + ExprKind::Identifier(name) => { + if let Some(local_reg) = fc.find_local(name) { + if local_reg != dst { + fc.builder.emit_reg_reg(Op::Move, dst, local_reg); + } + } else { + // Global lookup. + let ni = fc.builder.add_name(name); + fc.builder.emit_load_global(dst, ni); + } + } + + ExprKind::This => { + // `this` is loaded as a global named "this" (the VM binds it). + let ni = fc.builder.add_name("this"); + fc.builder.emit_load_global(dst, ni); + } + + ExprKind::Binary { op, left, right } => { + let lhs = fc.alloc_reg(); + compile_expr(fc, left, lhs)?; + let rhs = fc.alloc_reg(); + compile_expr(fc, right, rhs)?; + let bytecode_op = binary_op_to_opcode(*op); + fc.builder.emit_reg3(bytecode_op, dst, lhs, rhs); + fc.free_reg(rhs); + fc.free_reg(lhs); + } + + ExprKind::Unary { op, argument } => { + let src = fc.alloc_reg(); + compile_expr(fc, argument, src)?; + match op { + UnaryOp::Minus => fc.builder.emit_reg_reg(Op::Neg, dst, src), + UnaryOp::Plus => { + // Unary + is a no-op at the bytecode level (coerces to number at runtime). + fc.builder.emit_reg_reg(Op::Move, dst, src); + } + UnaryOp::Not => fc.builder.emit_reg_reg(Op::LogicalNot, dst, src), + UnaryOp::BitwiseNot => fc.builder.emit_reg_reg(Op::BitNot, dst, src), + UnaryOp::Typeof => fc.builder.emit_reg_reg(Op::TypeOf, dst, src), + UnaryOp::Void => fc.builder.emit_reg_reg(Op::Void, dst, src), + UnaryOp::Delete => { + // Simplified: `delete x` on a simple identifier. + // Real delete needs the object+key form. + fc.builder.emit_reg(Op::LoadTrue, dst); + } + } + fc.free_reg(src); + } + + ExprKind::Update { + op, + argument, + prefix, + } => { + // Get current value. + compile_expr(fc, argument, dst)?; + + let one = fc.alloc_reg(); + fc.builder.emit_load_int8(one, 1); + + if *prefix { + // ++x / --x: modify first, return modified. + match op { + UpdateOp::Increment => fc.builder.emit_reg3(Op::Add, dst, dst, one), + UpdateOp::Decrement => fc.builder.emit_reg3(Op::Sub, dst, dst, one), + } + // Store back. + compile_store(fc, argument, dst)?; + } else { + // x++ / x--: return original, then modify. + let tmp = fc.alloc_reg(); + fc.builder.emit_reg_reg(Op::Move, tmp, dst); + match op { + UpdateOp::Increment => fc.builder.emit_reg3(Op::Add, tmp, tmp, one), + UpdateOp::Decrement => fc.builder.emit_reg3(Op::Sub, tmp, tmp, one), + } + compile_store(fc, argument, tmp)?; + fc.free_reg(tmp); + } + fc.free_reg(one); + } + + ExprKind::Logical { op, left, right } => { + compile_expr(fc, left, dst)?; + match op { + LogicalOp::And => { + // Short-circuit: if falsy, skip right. + let skip = fc.builder.emit_cond_jump(Op::JumpIfFalse, dst); + compile_expr(fc, right, dst)?; + fc.builder.patch_jump(skip); + } + LogicalOp::Or => { + let skip = fc.builder.emit_cond_jump(Op::JumpIfTrue, dst); + compile_expr(fc, right, dst)?; + fc.builder.patch_jump(skip); + } + LogicalOp::Nullish => { + let skip = fc.builder.emit_cond_jump(Op::JumpIfNullish, dst); + // If NOT nullish, skip the right side. Wait — JumpIfNullish + // should mean "jump if nullish" so we want: evaluate left, + // if NOT nullish skip right. + // Let's invert: evaluate left, check if nullish → evaluate right. + // We need the jump to skip the "evaluate right" if NOT nullish. + // Since JumpIfNullish jumps when nullish, we need the inverse. + // Instead: use a two-step approach. + // + // Actually, rethink: for `a ?? b`: + // 1. evaluate a → dst + // 2. if dst is NOT null/undefined, jump to end + // 3. evaluate b → dst + // end: + // JumpIfNullish jumps when IS nullish. So we want jump when NOT nullish. + // Let's just use a "not nullish" check. + // For now: negate and use JumpIfFalse. + // Actually simpler: skip right when not nullish. + // JumpIfNullish jumps WHEN nullish. We want to jump over right when NOT nullish. + // So: + // evaluate a → dst + // JumpIfNullish dst → evaluate_right + // Jump → end + // evaluate_right: evaluate b → dst + // end: + // But we already emitted JumpIfNullish. Let's fix this. + // The JumpIfNullish we emitted jumps to "after patch", which is where + // we'll put the right-side code. We need another jump to skip right. + let end_patch = fc.builder.emit_jump(Op::Jump); + fc.builder.patch_jump(skip); // nullish → evaluate right + compile_expr(fc, right, dst)?; + fc.builder.patch_jump(end_patch); + } + } + } + + ExprKind::Assignment { op, left, right } => { + if *op == AssignOp::Assign { + compile_expr(fc, right, dst)?; + compile_store(fc, left, dst)?; + } else { + // Compound assignment: load current, operate, store. + compile_expr(fc, left, dst)?; + let rhs = fc.alloc_reg(); + compile_expr(fc, right, rhs)?; + let arith_op = compound_assign_op(*op); + fc.builder.emit_reg3(arith_op, dst, dst, rhs); + fc.free_reg(rhs); + compile_store(fc, left, dst)?; + } + } + + ExprKind::Conditional { + test, + consequent, + alternate, + } => { + let cond = fc.alloc_reg(); + compile_expr(fc, test, cond)?; + let else_patch = fc.builder.emit_cond_jump(Op::JumpIfFalse, cond); + fc.free_reg(cond); + compile_expr(fc, consequent, dst)?; + let end_patch = fc.builder.emit_jump(Op::Jump); + fc.builder.patch_jump(else_patch); + compile_expr(fc, alternate, dst)?; + fc.builder.patch_jump(end_patch); + } + + ExprKind::Call { callee, arguments } => { + let func_reg = fc.alloc_reg(); + compile_expr(fc, callee, func_reg)?; + + let args_start = fc.next_reg; + let arg_count = arguments.len().min(255) as u8; + for arg in arguments { + let arg_reg = fc.alloc_reg(); + compile_expr(fc, arg, arg_reg)?; + } + + fc.builder.emit_call(dst, func_reg, args_start, arg_count); + + // Free argument registers (in reverse). + for _ in 0..arg_count { + fc.next_reg -= 1; + } + fc.free_reg(func_reg); + } + + ExprKind::New { callee, arguments } => { + // For now, compile like a regular call. The VM will differentiate + // based on the `New` vs `Call` distinction (TODO: add NewCall opcode). + let func_reg = fc.alloc_reg(); + compile_expr(fc, callee, func_reg)?; + + let args_start = fc.next_reg; + let arg_count = arguments.len().min(255) as u8; + for arg in arguments { + let arg_reg = fc.alloc_reg(); + compile_expr(fc, arg, arg_reg)?; + } + + fc.builder.emit_call(dst, func_reg, args_start, arg_count); + + for _ in 0..arg_count { + fc.next_reg -= 1; + } + fc.free_reg(func_reg); + } + + ExprKind::Member { + object, + property, + computed, + } => { + let obj_reg = fc.alloc_reg(); + compile_expr(fc, object, obj_reg)?; + + if !computed { + // Static member: obj.prop → GetPropertyByName. + if let ExprKind::Identifier(name) = &property.kind { + let ni = fc.builder.add_name(name); + fc.builder.emit_get_prop_name(dst, obj_reg, ni); + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder.emit_reg3(Op::GetProperty, dst, obj_reg, key_reg); + fc.free_reg(key_reg); + } + } else { + // Computed member: obj[expr]. + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder.emit_reg3(Op::GetProperty, dst, obj_reg, key_reg); + fc.free_reg(key_reg); + } + fc.free_reg(obj_reg); + } + + ExprKind::Array(elements) => { + fc.builder.emit_reg(Op::CreateArray, dst); + for (i, elem) in elements.iter().enumerate() { + if let Some(el) = elem { + let val_reg = fc.alloc_reg(); + match el { + ArrayElement::Expr(e) => compile_expr(fc, e, val_reg)?, + ArrayElement::Spread(e) => { + // Spread in array: simplified, just compile the expression. + compile_expr(fc, e, val_reg)?; + } + } + let idx_reg = fc.alloc_reg(); + if i <= 127 { + fc.builder.emit_load_int8(idx_reg, i as i8); + } else { + let ci = fc.builder.add_constant(Constant::Number(i as f64)); + fc.builder.emit_reg_u16(Op::LoadConst, idx_reg, ci); + } + fc.builder.emit_reg3(Op::SetProperty, dst, idx_reg, val_reg); + fc.free_reg(idx_reg); + fc.free_reg(val_reg); + } + } + } + + ExprKind::Object(properties) => { + fc.builder.emit_reg(Op::CreateObject, dst); + for prop in properties { + let val_reg = fc.alloc_reg(); + if let Some(value) = &prop.value { + compile_expr(fc, value, val_reg)?; + } else { + // Shorthand: `{ x }` means `{ x: x }`. + if let PropertyKey::Identifier(name) = &prop.key { + if let Some(local) = fc.find_local(name) { + fc.builder.emit_reg_reg(Op::Move, val_reg, local); + } else { + let ni = fc.builder.add_name(name); + fc.builder.emit_load_global(val_reg, ni); + } + } else { + fc.builder.emit_reg(Op::LoadUndefined, val_reg); + } + } + + match &prop.key { + PropertyKey::Identifier(name) | PropertyKey::String(name) => { + let ni = fc.builder.add_name(name); + fc.builder.emit_set_prop_name(dst, ni, val_reg); + } + PropertyKey::Number(n) => { + let key_reg = fc.alloc_reg(); + let ci = fc.builder.add_constant(Constant::Number(*n)); + fc.builder.emit_reg_u16(Op::LoadConst, key_reg, ci); + fc.builder.emit_reg3(Op::SetProperty, dst, key_reg, val_reg); + fc.free_reg(key_reg); + } + PropertyKey::Computed(expr) => { + let key_reg = fc.alloc_reg(); + compile_expr(fc, expr, key_reg)?; + fc.builder.emit_reg3(Op::SetProperty, dst, key_reg, val_reg); + fc.free_reg(key_reg); + } + } + fc.free_reg(val_reg); + } + } + + ExprKind::Function(func_def) => { + let inner = compile_function_body(func_def)?; + let func_idx = fc.builder.add_function(inner); + fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + } + + ExprKind::Arrow { + params, + body, + is_async: _, + } => { + let param_count = params.len().min(255) as u8; + let mut inner = FunctionCompiler::new("".into(), param_count); + for p in params { + if let PatternKind::Identifier(name) = &p.kind { + inner.define_local(name); + } else { + let _ = inner.alloc_reg(); + } + } + let result = inner.alloc_reg(); + match body { + ArrowBody::Expr(e) => { + compile_expr(&mut inner, e, result)?; + } + ArrowBody::Block(stmts) => { + inner.builder.emit_reg(Op::LoadUndefined, result); + compile_stmts(&mut inner, stmts, result)?; + } + } + inner.builder.emit_reg(Op::Return, result); + let inner_func = inner.builder.finish(); + let func_idx = fc.builder.add_function(inner_func); + fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + } + + ExprKind::Class(class_def) => { + // Class expression: compile like class decl but into dst. + let name = class_def.id.clone().unwrap_or_default(); + // Find constructor. + let ctor = class_def.body.iter().find(|m| { + matches!( + &m.kind, + ClassMemberKind::Method { + kind: MethodKind::Constructor, + .. + } + ) + }); + if let Some(member) = ctor { + if let ClassMemberKind::Method { value, .. } = &member.kind { + let inner = compile_function_body(value)?; + let func_idx = fc.builder.add_function(inner); + fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + } + } else { + let empty = Function::new(name, 0); + let func_idx = fc.builder.add_function(empty); + fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); + } + } + + ExprKind::Sequence(exprs) => { + for e in exprs { + compile_expr(fc, e, dst)?; + } + } + + ExprKind::Spread(inner) => { + compile_expr(fc, inner, dst)?; + } + + ExprKind::TemplateLiteral { + quasis, + expressions, + } => { + // Compile template literal as string concatenation. + if quasis.len() == 1 && expressions.is_empty() { + let ci = fc.builder.add_constant(Constant::String(quasis[0].clone())); + fc.builder.emit_reg_u16(Op::LoadConst, dst, ci); + } else { + // Start with first quasi. + let ci = fc.builder.add_constant(Constant::String(quasis[0].clone())); + fc.builder.emit_reg_u16(Op::LoadConst, dst, ci); + for (i, expr) in expressions.iter().enumerate() { + let tmp = fc.alloc_reg(); + compile_expr(fc, expr, tmp)?; + fc.builder.emit_reg3(Op::Add, dst, dst, tmp); + fc.free_reg(tmp); + if i + 1 < quasis.len() { + let qi = fc + .builder + .add_constant(Constant::String(quasis[i + 1].clone())); + let tmp2 = fc.alloc_reg(); + fc.builder.emit_reg_u16(Op::LoadConst, tmp2, qi); + fc.builder.emit_reg3(Op::Add, dst, dst, tmp2); + fc.free_reg(tmp2); + } + } + } + } + + ExprKind::TaggedTemplate { tag, quasi } => { + // Simplified: call tag with the template as argument. + let func_reg = fc.alloc_reg(); + compile_expr(fc, tag, func_reg)?; + let arg_reg = fc.alloc_reg(); + compile_expr(fc, quasi, arg_reg)?; + fc.builder.emit_call(dst, func_reg, arg_reg, 1); + fc.free_reg(arg_reg); + fc.free_reg(func_reg); + } + + ExprKind::Yield { + argument, + delegate: _, + } => { + // Yield is a VM-level operation; for now compile the argument. + if let Some(arg) = argument { + compile_expr(fc, arg, dst)?; + } else { + fc.builder.emit_reg(Op::LoadUndefined, dst); + } + } + + ExprKind::Await(inner) => { + // Await is a VM-level operation; compile the argument. + compile_expr(fc, inner, dst)?; + } + + ExprKind::RegExp { .. } => { + // RegExp literals are created at runtime by the VM. + fc.builder.emit_reg(Op::LoadUndefined, dst); + } + + ExprKind::OptionalChain { base } => { + compile_expr(fc, base, dst)?; + } + } + Ok(()) +} + +/// Compile a store operation (assignment target). +fn compile_store(fc: &mut FunctionCompiler, target: &Expr, src: Reg) -> Result<(), JsError> { + match &target.kind { + ExprKind::Identifier(name) => { + if let Some(local) = fc.find_local(name) { + if local != src { + fc.builder.emit_reg_reg(Op::Move, local, src); + } + } else { + let ni = fc.builder.add_name(name); + fc.builder.emit_store_global(ni, src); + } + } + ExprKind::Member { + object, + property, + computed, + } => { + let obj_reg = fc.alloc_reg(); + compile_expr(fc, object, obj_reg)?; + if !computed { + if let ExprKind::Identifier(name) = &property.kind { + let ni = fc.builder.add_name(name); + fc.builder.emit_set_prop_name(obj_reg, ni, src); + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder.emit_reg3(Op::SetProperty, obj_reg, key_reg, src); + fc.free_reg(key_reg); + } + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder.emit_reg3(Op::SetProperty, obj_reg, key_reg, src); + fc.free_reg(key_reg); + } + fc.free_reg(obj_reg); + } + _ => { + // Other assignment targets (destructuring) not handled here. + } + } + Ok(()) +} + +fn binary_op_to_opcode(op: BinaryOp) -> Op { + match op { + BinaryOp::Add => Op::Add, + BinaryOp::Sub => Op::Sub, + BinaryOp::Mul => Op::Mul, + BinaryOp::Div => Op::Div, + BinaryOp::Rem => Op::Rem, + BinaryOp::Exp => Op::Exp, + BinaryOp::Eq => Op::Eq, + BinaryOp::Ne => Op::NotEq, + BinaryOp::StrictEq => Op::StrictEq, + BinaryOp::StrictNe => Op::StrictNotEq, + BinaryOp::Lt => Op::LessThan, + BinaryOp::Le => Op::LessEq, + BinaryOp::Gt => Op::GreaterThan, + BinaryOp::Ge => Op::GreaterEq, + BinaryOp::Shl => Op::ShiftLeft, + BinaryOp::Shr => Op::ShiftRight, + BinaryOp::Ushr => Op::UShiftRight, + BinaryOp::BitAnd => Op::BitAnd, + BinaryOp::BitOr => Op::BitOr, + BinaryOp::BitXor => Op::BitXor, + BinaryOp::In => Op::In, + BinaryOp::Instanceof => Op::InstanceOf, + } +} + +fn compound_assign_op(op: AssignOp) -> Op { + match op { + AssignOp::AddAssign => Op::Add, + AssignOp::SubAssign => Op::Sub, + AssignOp::MulAssign => Op::Mul, + AssignOp::DivAssign => Op::Div, + AssignOp::RemAssign => Op::Rem, + AssignOp::ExpAssign => Op::Exp, + AssignOp::ShlAssign => Op::ShiftLeft, + AssignOp::ShrAssign => Op::ShiftRight, + AssignOp::UshrAssign => Op::UShiftRight, + AssignOp::BitAndAssign => Op::BitAnd, + AssignOp::BitOrAssign => Op::BitOr, + AssignOp::BitXorAssign => Op::BitXor, + AssignOp::AndAssign => Op::BitAnd, // logical AND assignment uses short-circuit; simplified here + AssignOp::OrAssign => Op::BitOr, // likewise + AssignOp::NullishAssign => Op::Move, // simplified + AssignOp::Assign => unreachable!(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::Parser; + + /// Helper: parse and compile source, return the top-level function. + fn compile_src(src: &str) -> Function { + let program = Parser::parse(src).expect("parse failed"); + compile(&program).expect("compile failed") + } + + #[test] + fn test_compile_number_literal() { + let f = compile_src("42;"); + let dis = f.disassemble(); + assert!(dis.contains("LoadInt8 r0, 42"), "got:\n{dis}"); + assert!(dis.contains("Return r0")); + } + + #[test] + fn test_compile_large_number() { + let f = compile_src("3.14;"); + let dis = f.disassemble(); + assert!(dis.contains("LoadConst r0, #0"), "got:\n{dis}"); + assert!( + f.constants.contains(&Constant::Number(3.14)), + "constants: {:?}", + f.constants + ); + } + + #[test] + fn test_compile_string() { + let f = compile_src("\"hello\";"); + let dis = f.disassemble(); + assert!(dis.contains("LoadConst r0, #0")); + assert!(f.constants.contains(&Constant::String("hello".into()))); + } + + #[test] + fn test_compile_bool_null() { + let f = compile_src("true; false; null;"); + let dis = f.disassemble(); + assert!(dis.contains("LoadTrue r0")); + assert!(dis.contains("LoadFalse r0")); + assert!(dis.contains("LoadNull r0")); + } + + #[test] + fn test_compile_binary_arithmetic() { + let f = compile_src("1 + 2;"); + let dis = f.disassemble(); + assert!(dis.contains("Add r0, r1, r2"), "got:\n{dis}"); + } + + #[test] + fn test_compile_nested_arithmetic() { + let f = compile_src("(1 + 2) * 3;"); + let dis = f.disassemble(); + assert!(dis.contains("Add"), "got:\n{dis}"); + assert!(dis.contains("Mul"), "got:\n{dis}"); + } + + #[test] + fn test_compile_var_decl() { + let f = compile_src("var x = 10; x;"); + let dis = f.disassemble(); + // x should get a register, then be loaded from that register. + assert!(dis.contains("LoadInt8"), "got:\n{dis}"); + assert!( + dis.contains("Move") || dis.contains("LoadInt8"), + "got:\n{dis}" + ); + } + + #[test] + fn test_compile_let_const() { + let f = compile_src("let a = 1; const b = 2; a + b;"); + let dis = f.disassemble(); + assert!(dis.contains("Add"), "got:\n{dis}"); + } + + #[test] + fn test_compile_if_else() { + let f = compile_src("if (true) { 1; } else { 2; }"); + let dis = f.disassemble(); + assert!(dis.contains("JumpIfFalse"), "got:\n{dis}"); + assert!(dis.contains("Jump"), "got:\n{dis}"); + } + + #[test] + fn test_compile_while() { + let f = compile_src("var i = 0; while (i < 10) { i = i + 1; }"); + let dis = f.disassemble(); + assert!(dis.contains("LessThan"), "got:\n{dis}"); + assert!(dis.contains("JumpIfFalse"), "got:\n{dis}"); + assert!( + dis.contains("Jump"), + "backward jump should be present: {dis}" + ); + } + + #[test] + fn test_compile_do_while() { + let f = compile_src("var i = 0; do { i = i + 1; } while (i < 5);"); + let dis = f.disassemble(); + assert!(dis.contains("JumpIfTrue"), "got:\n{dis}"); + } + + #[test] + fn test_compile_for_loop() { + let f = compile_src("for (var i = 0; i < 10; i = i + 1) { i; }"); + let dis = f.disassemble(); + assert!(dis.contains("LessThan"), "got:\n{dis}"); + assert!(dis.contains("JumpIfFalse"), "got:\n{dis}"); + } + + #[test] + fn test_compile_function_decl() { + let f = compile_src("function add(a, b) { return a + b; }"); + let dis = f.disassemble(); + assert!(dis.contains("CreateClosure"), "got:\n{dis}"); + assert!(!f.functions.is_empty(), "should have nested function"); + let inner = &f.functions[0]; + assert_eq!(inner.name, "add"); + assert_eq!(inner.param_count, 2); + let inner_dis = inner.disassemble(); + assert!(inner_dis.contains("Add"), "inner:\n{inner_dis}"); + assert!(inner_dis.contains("Return"), "inner:\n{inner_dis}"); + } + + #[test] + fn test_compile_function_call() { + let f = compile_src("function f() { return 42; } f();"); + let dis = f.disassemble(); + assert!(dis.contains("Call"), "got:\n{dis}"); + } + + #[test] + fn test_compile_arrow_function() { + let f = compile_src("var add = (a, b) => a + b;"); + let dis = f.disassemble(); + assert!(dis.contains("CreateClosure"), "got:\n{dis}"); + let inner = &f.functions[0]; + assert_eq!(inner.param_count, 2); + } + + #[test] + fn test_compile_assignment() { + let f = compile_src("var x = 1; x = x + 2;"); + let dis = f.disassemble(); + assert!(dis.contains("Add"), "got:\n{dis}"); + assert!( + dis.contains("Move"), + "assignment should produce Move:\n{dis}" + ); + } + + #[test] + fn test_compile_compound_assignment() { + let f = compile_src("var x = 10; x += 5;"); + let dis = f.disassemble(); + assert!(dis.contains("Add"), "got:\n{dis}"); + } + + #[test] + fn test_compile_member_access() { + let f = compile_src("var obj = {}; obj.x;"); + let dis = f.disassemble(); + assert!(dis.contains("CreateObject"), "got:\n{dis}"); + assert!(dis.contains("GetPropertyByName"), "got:\n{dis}"); + } + + #[test] + fn test_compile_computed_member() { + let f = compile_src("var arr = []; arr[0];"); + let dis = f.disassemble(); + assert!(dis.contains("GetProperty"), "got:\n{dis}"); + } + + #[test] + fn test_compile_object_literal() { + let f = compile_src("var obj = { a: 1, b: 2 };"); + let dis = f.disassemble(); + assert!(dis.contains("CreateObject"), "got:\n{dis}"); + assert!(dis.contains("SetPropertyByName"), "got:\n{dis}"); + } + + #[test] + fn test_compile_array_literal() { + let f = compile_src("[1, 2, 3];"); + let dis = f.disassemble(); + assert!(dis.contains("CreateArray"), "got:\n{dis}"); + assert!(dis.contains("SetProperty"), "got:\n{dis}"); + } + + #[test] + fn test_compile_conditional() { + let f = compile_src("true ? 1 : 2;"); + let dis = f.disassemble(); + assert!(dis.contains("JumpIfFalse"), "got:\n{dis}"); + } + + #[test] + fn test_compile_logical_and() { + let f = compile_src("true && false;"); + let dis = f.disassemble(); + assert!(dis.contains("JumpIfFalse"), "short-circuit:\n{dis}"); + } + + #[test] + fn test_compile_logical_or() { + let f = compile_src("false || true;"); + let dis = f.disassemble(); + assert!(dis.contains("JumpIfTrue"), "short-circuit:\n{dis}"); + } + + #[test] + fn test_compile_typeof() { + let f = compile_src("typeof 42;"); + let dis = f.disassemble(); + assert!(dis.contains("TypeOf"), "got:\n{dis}"); + } + + #[test] + fn test_compile_unary_minus() { + let f = compile_src("-42;"); + let dis = f.disassemble(); + assert!(dis.contains("Neg"), "got:\n{dis}"); + } + + #[test] + fn test_compile_not() { + let f = compile_src("!true;"); + let dis = f.disassemble(); + assert!(dis.contains("LogicalNot"), "got:\n{dis}"); + } + + #[test] + fn test_compile_return() { + let f = compile_src("function f() { return 42; }"); + let inner = &f.functions[0]; + let dis = inner.disassemble(); + assert!(dis.contains("Return"), "got:\n{dis}"); + } + + #[test] + fn test_compile_empty_return() { + let f = compile_src("function f() { return; }"); + let inner = &f.functions[0]; + let dis = inner.disassemble(); + assert!(dis.contains("LoadUndefined"), "got:\n{dis}"); + assert!(dis.contains("Return"), "got:\n{dis}"); + } + + #[test] + fn test_compile_throw() { + let f = compile_src("function f() { throw 42; }"); + let inner = &f.functions[0]; + let dis = inner.disassemble(); + assert!(dis.contains("Throw"), "got:\n{dis}"); + } + + #[test] + fn test_compile_this() { + let f = compile_src("this;"); + let dis = f.disassemble(); + assert!(dis.contains("LoadGlobal"), "got:\n{dis}"); + assert!(f.names.contains(&"this".to_string())); + } + + #[test] + fn test_compile_global_var() { + let f = compile_src("console;"); + let dis = f.disassemble(); + assert!(dis.contains("LoadGlobal"), "got:\n{dis}"); + assert!(f.names.contains(&"console".to_string())); + } + + #[test] + fn test_compile_template_literal() { + let f = compile_src("`hello`;"); + assert!( + f.constants.contains(&Constant::String("hello".into())), + "constants: {:?}", + f.constants + ); + } + + #[test] + fn test_compile_switch() { + let f = compile_src("switch (1) { case 1: 42; break; case 2: 99; break; }"); + let dis = f.disassemble(); + assert!(dis.contains("StrictEq"), "got:\n{dis}"); + } + + #[test] + fn test_compile_class() { + let f = compile_src("class Foo { constructor() {} greet() { return 1; } }"); + let dis = f.disassemble(); + assert!(dis.contains("CreateClosure"), "got:\n{dis}"); + } + + #[test] + fn test_compile_update_prefix() { + let f = compile_src("var x = 0; ++x;"); + let dis = f.disassemble(); + assert!(dis.contains("Add"), "got:\n{dis}"); + } + + #[test] + fn test_compile_comparison() { + let f = compile_src("1 === 2;"); + let dis = f.disassemble(); + assert!(dis.contains("StrictEq"), "got:\n{dis}"); + } + + #[test] + fn test_compile_bitwise() { + let f = compile_src("1 & 2;"); + let dis = f.disassemble(); + assert!(dis.contains("BitAnd"), "got:\n{dis}"); + } + + #[test] + fn test_compile_void() { + let f = compile_src("void 0;"); + let dis = f.disassemble(); + assert!(dis.contains("Void"), "got:\n{dis}"); + } + + #[test] + fn test_disassembler_output_format() { + let f = compile_src("var x = 42; x + 1;"); + let dis = f.disassemble(); + // Should contain function header. + assert!(dis.contains("function
")); + // Should contain code section. + assert!(dis.contains("code:")); + // Should have hex offsets. + assert!(dis.contains("0000")); + } + + #[test] + fn test_register_allocation_is_minimal() { + // `var a = 1; var b = 2; a + b;` should use few registers. + let f = compile_src("var a = 1; var b = 2; a + b;"); + // r0 = result, r1 = a, r2 = b, r3/r4 = temps for addition + assert!( + f.register_count <= 6, + "too many registers: {}", + f.register_count + ); + } + + #[test] + fn test_nested_function_closure() { + let f = compile_src("function outer() { function inner() { return 1; } return inner; }"); + assert_eq!(f.functions.len(), 1); + let outer = &f.functions[0]; + assert_eq!(outer.name, "outer"); + assert_eq!(outer.functions.len(), 1); + let inner = &outer.functions[0]; + assert_eq!(inner.name, "inner"); + } + + #[test] + fn test_for_with_no_parts() { + // `for (;;) { break; }` — infinite loop with immediate break. + let f = compile_src("for (;;) { break; }"); + let dis = f.disassemble(); + assert!(dis.contains("Jump"), "got:\n{dis}"); + } +} diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 3aae7bd..171eab6 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -1,6 +1,8 @@ //! JavaScript engine — lexer, parser, bytecode, register VM, GC, JIT (AArch64). pub mod ast; +pub mod bytecode; +pub mod compiler; pub mod lexer; pub mod parser;