diff --git a/Cargo.lock b/Cargo.lock index 1ec1c348..74eeb9f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,6 +808,7 @@ dependencies = [ "oxide_kernel", "oxide_parser", "proptest", + "rustc-hash", "ryu", ] diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 88f3ec97..bb5da96c 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -11,6 +11,7 @@ oxide_compiler = { path = "../oxide_compiler" } oxide_kernel = { path = "../oxide_kernel" } bumpalo.workspace = true hashbrown.workspace = true +rustc-hash.workspace = true fast-float.workspace = true ryu.workspace = true diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index 1e98c6b2..ad3be71c 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -1,4 +1,5 @@ use crate::value::JsValue; +use crate::vm::Vm; pub fn to_primitive(val: JsValue) -> JsValue { if val.is_object() { @@ -29,7 +30,7 @@ pub fn to_number(val: JsValue) -> f64 { f64::NAN } -pub fn to_string(val: JsValue) -> String { +pub fn to_string(vm: &Vm, val: JsValue) -> String { if val.is_int() { return val.as_int().to_string(); } @@ -57,6 +58,9 @@ pub fn to_string(val: JsValue) -> String { if val.is_undefined() { return "undefined".to_string(); } + if val.is_string() { + return vm.lookup_str(val).unwrap_or("").to_string(); + } if val.is_object() { panic_to_object(); } @@ -77,6 +81,9 @@ pub fn to_boolean(val: JsValue) -> bool { let d = val.as_double(); return !(d == 0.0 || d == -0.0 || d.is_nan()); } + if val.is_string() { + return true; + } if val.is_object() { return true; } @@ -90,6 +97,9 @@ pub fn abstract_eq(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_undefined() && rhs.is_null() { return true; } + if lhs.is_string() && rhs.is_string() { + return lhs == rhs; + } if lhs.is_int() && rhs.is_int() { return lhs.as_int() == rhs.as_int(); } @@ -142,6 +152,9 @@ pub fn strict_eq(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_undefined() && rhs.is_undefined() { return true; } + if lhs.is_string() && rhs.is_string() { + return lhs == rhs; + } if lhs.is_object() && rhs.is_object() { return lhs.as_ptr() == rhs.as_ptr(); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index cd885142..a37dc795 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use oxide_compiler::compiler::Constant; use oxide_compiler::module::CompiledModule; use oxide_compiler::opcode::{self, OpCode}; @@ -17,6 +19,8 @@ pub struct Vm { bytecode: Vec, constants: Vec, frames: Vec, + string_table: HashMap, + string_reverse: Vec, } impl Vm { @@ -27,11 +31,61 @@ impl Vm { bytecode: Vec::new(), constants: Vec::new(), frames: Vec::with_capacity(128), + string_table: HashMap::with_capacity(64), + string_reverse: Vec::with_capacity(64), } } + pub fn intern(&mut self, s: &str) -> JsValue { + if let Some(&idx) = self.string_table.get(s) { + return JsValue::string(idx, hash16(s)); + } + let idx = self.string_reverse.len() as u32; + self.string_reverse.push(s.to_string()); + self.string_table.insert(s.to_string(), idx); + JsValue::string(idx, hash16(s)) + } + + pub fn lookup_str(&self, val: JsValue) -> Option<&str> { + if !val.is_string() { + return None; + } + let idx = val.as_string_index() as usize; + self.string_reverse.get(idx).map(|s| s.as_str()) + } + pub fn run(&mut self, module: &CompiledModule) -> Result { - self.constants = module.constants.iter().map(convert_constant).collect(); + self.string_table.clear(); + self.string_reverse.clear(); + + let string_entries: Vec = module + .constants + .iter() + .filter_map(|c| { + if let Constant::String(s) = c { + Some(s.clone()) + } else { + None + } + }) + .collect(); + for s in &string_entries { + self.intern(s); + } + self.constants = module + .constants + .iter() + .map(|c| match c { + Constant::Number(v) => JsValue::float(*v), + Constant::String(s) => { + let idx = self.string_table.get(s.as_str()).copied().unwrap_or(0); + JsValue::string(idx, hash16(s)) + } + Constant::Boolean(b) => JsValue::bool(*b), + Constant::Null => JsValue::null(), + Constant::Undefined => JsValue::undefined(), + }) + .collect(); self.bytecode = module.bytecode.clone(); self.pc = 0; self.regs = [JsValue::undefined(); 256]; @@ -68,23 +122,18 @@ impl Vm { } OpCode::ADD => { - let lhs = coercion::to_primitive(self.regs[a]); - let rhs = coercion::to_primitive(self.regs[b]); - if lhs.is_object() || rhs.is_object() { - return Err("ADD with objects not yet supported".into()); + let lhs = self.regs[a]; + let rhs = self.regs[b]; + if lhs.is_string() || rhs.is_string() { + let ls = coercion::to_string(self, lhs); + let rs = coercion::to_string(self, rhs); + let concat = format!("{ls}{rs}"); + self.regs[rd] = self.intern(&concat); + } else { + let ln = coercion::to_number(lhs); + let rn = coercion::to_number(rhs); + self.regs[rd] = JsValue::float(ln + rn); } - if lhs.is_double() - && lhs.as_double().is_nan() - && lhs.as_double().to_bits() & 0x000F_FFFF_FFFF_FFFF == 0 - { - // Actually this is too complex. Simpler: just check if it's a string via our JsValue type system. - } - // String concat: JsValue doesn't have is_string yet (Phase 6). - // For Phase 5, strings come as NaN-boxed objects — we don't have string JsValues. - // Addition defaults to numeric. - let ln = coercion::to_number(lhs); - let rn = coercion::to_number(rhs); - self.regs[rd] = JsValue::float(ln + rn); } OpCode::SUB => { @@ -212,12 +261,9 @@ impl Default for Vm { } } -fn convert_constant(c: &Constant) -> JsValue { - match c { - Constant::Number(v) => JsValue::float(*v), - Constant::String(_s) => JsValue::null(), - Constant::Boolean(b) => JsValue::bool(*b), - Constant::Null => JsValue::null(), - Constant::Undefined => JsValue::undefined(), - } +fn hash16(s: &str) -> u16 { + use std::hash::{Hash, Hasher}; + let mut h = rustc_hash::FxHasher::default(); + s.hash(&mut h); + (h.finish() >> 48) as u16 }