From 551695bfadf7f17d1406c1d90d5a7d2b685d7a6a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 13 Jun 2026 23:59:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=89=81=E5=B9=B3=E5=8C=96?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=B1=9E=E6=80=A7=E5=AD=98=E5=82=A8=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B8=B8=E9=87=8F=E6=B1=A0=E4=B8=8E=20IC=20?= =?UTF-8?q?=E7=83=AD=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + Cargo.toml | 1 + crates/oxide_compiler/src/compiler.rs | 40 ++++++++++- .../tests/compile_expr_tests.rs | 11 +++ crates/oxide_kernel/src/builtin.rs | 14 ++-- crates/oxide_kernel/src/code_forge.rs | 10 ++- crates/oxide_kernel/src/kernel.rs | 6 +- crates/oxide_kernel/src/shape_forge.rs | 9 ++- crates/oxide_kernel/src/string_forge.rs | 8 +++ crates/oxide_types/src/error.rs | 71 +++++++++++++++++++ crates/oxide_types/src/lib.rs | 3 + crates/oxide_types/src/object.rs | 61 +++++++++------- crates/oxide_vm/Cargo.toml | 1 + crates/oxide_vm/src/bindings/bind_error.rs | 10 +-- crates/oxide_vm/src/bindings/bind_math.rs | 4 +- crates/oxide_vm/src/bindings/bind_number.rs | 4 +- crates/oxide_vm/src/bindings/bind_string.rs | 2 +- crates/oxide_vm/src/bindings/bind_symbol.rs | 2 +- crates/oxide_vm/src/bindings/mod.rs | 4 +- crates/oxide_vm/src/builtins/error.rs | 4 +- crates/oxide_vm/src/builtins/function.rs | 10 +-- crates/oxide_vm/src/builtins/json.rs | 2 +- crates/oxide_vm/src/builtins/regexp.rs | 12 ++-- crates/oxide_vm/src/coercion.rs | 2 +- crates/oxide_vm/src/dispatch/property.rs | 34 +++++++-- crates/oxide_vm/src/vm.rs | 68 +++++++++++++----- 26 files changed, 307 insertions(+), 87 deletions(-) create mode 100644 crates/oxide_types/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 9580e923..984a2ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -982,6 +982,7 @@ dependencies = [ "regex", "ryu", "serde_json", + "smallvec", "tracing", "unicode-normalization", ] diff --git a/Cargo.toml b/Cargo.toml index 0982b186..2801c597 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ ryu = "1" memchr = "2" regex = "1" indexmap = "2" +smallvec = { version = "1", features = ["union"] } chrono = { version = "0.4", default-features = false, features = ["clock"] } unicode-normalization = "0.1" tracing = "0.1.44" diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 8438f662..0c09b3ed 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -54,6 +54,7 @@ pub(crate) enum Label { pub(crate) struct CompileCtx { pub(crate) bytecode: Vec, pub(crate) constants: Vec, + constant_map: HashMap, next_reg: u8, pub(crate) max_regs: u8, reserved_reg_start: u8, @@ -85,11 +86,23 @@ pub(crate) enum FunctionBodyContext { ClassElement, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum ConstantKey { + Number(u64), + Int(i32), + String(String), + Boolean(bool), + Null, + Undefined, + BytecodeFunc(u32), +} + impl CompileCtx { pub(crate) fn new() -> Self { Self { bytecode: Vec::new(), constants: Vec::new(), + constant_map: HashMap::new(), next_reg: 0, max_regs: 0, reserved_reg_start: 0, @@ -159,9 +172,17 @@ impl CompileCtx { } pub(crate) fn add_constant(&mut self, c: Constant) -> u16 { - if let Some(idx) = self.constants.iter().position(|x| x == &c) { - return idx as u16; + if let Some(key) = ConstantKey::from_constant(&c) { + if let Some(&idx) = self.constant_map.get(&key) { + return idx; + } + + let idx = self.constants.len() as u16; + self.constants.push(c); + self.constant_map.insert(key, idx); + return idx; } + let idx = self.constants.len(); self.constants.push(c); idx as u16 @@ -290,6 +311,21 @@ impl CompileCtx { } } +impl ConstantKey { + fn from_constant(value: &Constant) -> Option { + match value { + Constant::Number(v) => Some(Self::Number(v.to_bits())), + Constant::Int(v) => Some(Self::Int(*v)), + Constant::String(v) => Some(Self::String(v.clone())), + Constant::Boolean(v) => Some(Self::Boolean(*v)), + Constant::Null => Some(Self::Null), + Constant::Undefined => Some(Self::Undefined), + Constant::BytecodeFunc(v) => Some(Self::BytecodeFunc(*v)), + Constant::RegExp(_, _) => None, + } + } +} + impl Compiler { pub fn new() -> Self { Self diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index f3c735f3..7746e9bd 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -52,6 +52,17 @@ fn compile_constants() { assert_eq!(module.constants[0], oxide_compiler::compiler::Constant::Int(42)); } +#[test] +fn compile_dedups_identical_string_constants() { + let module = compile_source("var a = 'hello'; var b = 'hello';"); + let count = module + .constants + .iter() + .filter(|c| matches!(c, oxide_compiler::compiler::Constant::String(s) if s == "hello")) + .count(); + assert_eq!(count, 1, "identical string constants should be interned once per module"); +} + #[test] fn compile_negation() { let module = compile_source("-5"); diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index dec7f0a4..60181968 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -227,9 +227,9 @@ fn make_pair( // Pre-allocate Vec slots so wire_ctor_proto can overwrite vec[0] later. // Proto: 1 property ("constructor"), Ctor: 2 properties ("prototype", "name"). - proto.ensure_hash_props().push(Box::new(JsValue::undefined())); // slot 0: "constructor" placeholder - ctor.ensure_hash_props().push(Box::new(JsValue::undefined())); // slot 0: "prototype" placeholder - ctor.ensure_hash_props().push(Box::new(JsValue::undefined())); // slot 1: "name" placeholder + proto.ensure_hash_props().push(JsValue::undefined()); // slot 0: "constructor" placeholder + ctor.ensure_hash_props().push(JsValue::undefined()); // slot 0: "prototype" placeholder + ctor.ensure_hash_props().push(JsValue::undefined()); // slot 1: "name" placeholder (P::new(proto), P::new(ctor)) } @@ -299,13 +299,13 @@ impl BuiltinWorld { let ctor = unsafe { &mut *ctor_ptr }; let vec = ctor.ensure_hash_props(); if !vec.is_empty() { - *vec[0] = JsValue::from_js_object(proto.as_ptr() as *mut JsObject); + vec[0] = JsValue::from_js_object(proto.as_ptr() as *mut JsObject); } let proto_ptr = proto.as_ptr() as *mut JsObject; let proto = unsafe { &mut *proto_ptr }; let pvec = proto.ensure_hash_props(); if !pvec.is_empty() { - *pvec[0] = JsValue::from_js_object(ctor_ptr); + pvec[0] = JsValue::from_js_object(ctor_ptr); } } @@ -626,11 +626,11 @@ impl BuiltinWorld { wrapper.set_shape_id(name_shape); wrapper .ensure_hash_props() - .push(Box::new(JsValue::string(string_forge.intern(method_name).0, 0))); + .push(JsValue::string(string_forge.intern(method_name).0, 0)); let wrapper_val = JsValue::from_js_object(Box::into_raw(wrapper)); let new_shape = shape_forge.make_shape(proto.shape_id(), si); proto.set_shape_id(new_shape); - proto.ensure_hash_props().push(Box::new(wrapper_val)); + proto.ensure_hash_props().push(wrapper_val); proto.bump_generation(); Ok(()) } diff --git a/crates/oxide_kernel/src/code_forge.rs b/crates/oxide_kernel/src/code_forge.rs index d71fdc1d..78eac9b0 100644 --- a/crates/oxide_kernel/src/code_forge.rs +++ b/crates/oxide_kernel/src/code_forge.rs @@ -9,13 +9,21 @@ use crate::{kernel_debug, kernel_info}; pub use oxide_compiler::compiler::structural_hash; +/// Shared compiled-module cache. +/// +/// Concurrency model: +/// - Read-heavy after warmup: cache hits dominate steady-state execution. +/// - Backed by a fixed-shard `DashMap` so lookup contention stays predictable across +/// developer machines and CI hosts. pub struct CodeForge { map: DashMap>, } impl CodeForge { pub fn new() -> Self { - Self { map: DashMap::new() } + Self { + map: DashMap::with_shard_amount(16), + } } pub fn get_or_compile( diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index b534522c..0676e1d3 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -103,15 +103,15 @@ impl OxideKernel { let nan_shape = shape_forge.make_shape(EMPTY_SHAPE_ID, si_nan); global_obj.set_shape_id(nan_shape); - global_obj.ensure_hash_props().push(Box::new(JsValue::float(f64::NAN))); + global_obj.ensure_hash_props().push(JsValue::float(f64::NAN)); let undef_shape = shape_forge.make_shape(nan_shape, si_undef); global_obj.set_shape_id(undef_shape); - global_obj.ensure_hash_props().push(Box::new(JsValue::undefined())); + global_obj.ensure_hash_props().push(JsValue::undefined()); let inf_shape = shape_forge.make_shape(undef_shape, si_infinity); global_obj.set_shape_id(inf_shape); - global_obj.ensure_hash_props().push(Box::new(JsValue::float(f64::INFINITY))); + global_obj.ensure_hash_props().push(JsValue::float(f64::INFINITY)); let global_object = P::new(global_obj); diff --git a/crates/oxide_kernel/src/shape_forge.rs b/crates/oxide_kernel/src/shape_forge.rs index 51291c94..88568a0c 100644 --- a/crates/oxide_kernel/src/shape_forge.rs +++ b/crates/oxide_kernel/src/shape_forge.rs @@ -18,6 +18,13 @@ pub struct Shape { pub parent: Option, } +/// Shared hidden-class store. +/// +/// Concurrency model: +/// - `shapes` is append-mostly behind an `RwLock`. +/// - `transitions` is a sharded `DashMap` keyed by `(parent_shape, property_name)`. +/// - Shard count is fixed for reproducible contention behavior across machines instead +/// of relying on DashMap's CPU-dependent default. pub struct ShapeForge { shapes: RwLock>>>, transitions: DashMap, @@ -28,7 +35,7 @@ impl ShapeForge { pub fn new() -> Self { let forge = Self { shapes: RwLock::new(Vec::with_capacity(256)), - transitions: DashMap::with_capacity(256), + transitions: DashMap::with_capacity_and_shard_amount(256, 16), next_id: AtomicU32::new(2), }; { diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index ff3f734a..a30f9e29 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -24,6 +24,14 @@ struct StringForgeInner { entries: Vec, } +/// Global string interning pool shared by all VMs. +/// +/// Concurrency model: +/// `StringForge` keeps a single `RwLock` around the interner state. +/// - `lookup()` takes a shared read lock and scales across readers. +/// - `intern()` uses a read-check then write-check pattern to keep hot-path hits cheap. +/// - New string insertion is serialized, which matches the workload here: most strings +/// are interned early and later accesses are dominated by reads. pub struct StringForge { inner: RwLock, } diff --git a/crates/oxide_types/src/error.rs b/crates/oxide_types/src/error.rs new file mode 100644 index 00000000..f26f21e5 --- /dev/null +++ b/crates/oxide_types/src/error.rs @@ -0,0 +1,71 @@ +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum JsErrorKind { + TypeError, + RangeError, + ReferenceError, + SyntaxError, + Error, + URIError, + EvalError, +} + +impl fmt::Display for JsErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Self::TypeError => "TypeError", + Self::RangeError => "RangeError", + Self::ReferenceError => "ReferenceError", + Self::SyntaxError => "SyntaxError", + Self::Error => "Error", + Self::URIError => "URIError", + Self::EvalError => "EvalError", + }; + f.write_str(name) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JsError { + pub kind: JsErrorKind, + pub message: String, +} + +impl JsError { + pub fn type_error(msg: impl Into) -> Self { + Self::new(JsErrorKind::TypeError, msg) + } + + pub fn range_error(msg: impl Into) -> Self { + Self::new(JsErrorKind::RangeError, msg) + } + + pub fn reference_error(msg: impl Into) -> Self { + Self::new(JsErrorKind::ReferenceError, msg) + } + + pub fn syntax_error(msg: impl Into) -> Self { + Self::new(JsErrorKind::SyntaxError, msg) + } + + pub fn generic(msg: impl Into) -> Self { + Self::new(JsErrorKind::Error, msg) + } + + pub fn new(kind: JsErrorKind, msg: impl Into) -> Self { + Self { kind, message: msg.into() } + } +} + +impl fmt::Display for JsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.kind, self.message) + } +} + +impl From for String { + fn from(value: JsError) -> Self { + value.to_string() + } +} diff --git a/crates/oxide_types/src/lib.rs b/crates/oxide_types/src/lib.rs index 65526cca..38f3613d 100644 --- a/crates/oxide_types/src/lib.rs +++ b/crates/oxide_types/src/lib.rs @@ -1,6 +1,9 @@ #![doc = "OxideJS - Shared core types (JsValue, JsObject, Shape, P, Epoch)"] +pub mod error; pub mod mem; pub mod object; pub mod shape; pub mod value; + +pub use error::{JsError, JsErrorKind}; diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 6ca8f181..2397c9ec 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -152,7 +152,7 @@ impl PropIndex for i32 { /// [30] is_extensible /// [31] is_function /// native_arg_count: u8 (1 byte + 3 pad) -/// hash_props: *mut u8 (8 bytes, points to Box>>) +/// hash_props: *mut u8 (8 bytes, points to Box>) /// prop_meta: *mut u8 (8 bytes, points to Box>>) /// proto: JsValue (8 bytes) /// generation: u32 (4 bytes + 4 pad) @@ -213,7 +213,7 @@ impl JsObject { captured_this: JsValue::undefined(), home_object: JsValue::undefined(), }; - let vec = Box::new(vec![Box::new(JsValue::undefined()); n_elements]); + let vec = Box::new(vec![JsValue::undefined(); n_elements]); obj.hash_props = Box::into_raw(vec) as *mut u8; obj } @@ -232,7 +232,9 @@ impl JsObject { if self.hash_props.is_null() { 0 } else { - let vec = unsafe { &*(self.hash_props as *const Vec>) }; + // SAFETY: hash_props is either null or was created from Box> + // in ensure_hash_props/new_array and remains owned by this object. + let vec = unsafe { &*(self.hash_props as *const Vec) }; vec.len() as u32 } } @@ -246,7 +248,7 @@ impl JsObject { vec.truncate(target); } else { while vec.len() < target { - vec.push(Box::new(JsValue::undefined())); + vec.push(JsValue::undefined()); } } } @@ -271,6 +273,8 @@ impl JsObject { let vec = Box::new(vec![None::; len]); self.prop_meta = Box::into_raw(vec) as *mut u8; } + // SAFETY: prop_meta was set from Box>> in + // ensure_prop_meta and remains valid while this object owns it. unsafe { &mut *(self.prop_meta as *mut Vec>) } } @@ -278,6 +282,8 @@ impl JsObject { if self.prop_meta.is_null() { None } else { + // SAFETY: prop_meta was set from Box>> in + // ensure_prop_meta and remains valid while this object owns it. unsafe { Some(&*(self.prop_meta as *const Vec>)) } } } @@ -286,6 +292,8 @@ impl JsObject { if self.prop_meta.is_null() { None } else { + // SAFETY: prop_meta was set from Box>> in + // ensure_prop_meta and remains valid while this object owns it. unsafe { Some(&mut *(self.prop_meta as *mut Vec>)) } } } @@ -375,20 +383,22 @@ impl JsObject { } /// Initialize hash_props if null, return mutable reference to Vec. - pub fn ensure_hash_props(&mut self) -> &mut Vec> { + pub fn ensure_hash_props(&mut self) -> &mut Vec { if self.hash_props.is_null() { - let vec = Box::new(Vec::>::new()); + let vec = Box::new(Vec::::new()); self.hash_props = Box::into_raw(vec) as *mut u8; } - unsafe { &mut *(self.hash_props as *mut Vec>) } + // SAFETY: hash_props was set from Box> in this method or new_array. + unsafe { &mut *(self.hash_props as *mut Vec) } } /// Safe read access to hash_props vec. Returns None if not allocated. - pub fn hash_props_vec(&self) -> Option<&Vec>> { + pub fn hash_props_vec(&self) -> Option<&Vec> { if self.hash_props.is_null() { None } else { - unsafe { Some(&*(self.hash_props as *const Vec>)) } + // SAFETY: hash_props was set from Box> in ensure_hash_props/new_array. + unsafe { Some(&*(self.hash_props as *const Vec)) } } } @@ -398,8 +408,9 @@ impl JsObject { if self.hash_props.is_null() { return JsValue::undefined(); } - let vec = unsafe { &*(self.hash_props as *const Vec>) }; - vec.get(position.to_u32() as usize).map(|b| **b).unwrap_or(JsValue::undefined()) + // SAFETY: hash_props was set from Box> in ensure_hash_props/new_array. + let vec = unsafe { &*(self.hash_props as *const Vec) }; + vec.get(position.to_u32() as usize).copied().unwrap_or(JsValue::undefined()) } /// Set property value at position index. Vec auto-grows if needed. @@ -408,12 +419,12 @@ impl JsObject { { let vec = self.ensure_hash_props(); if pos < vec.len() { - *vec[pos] = val; + vec[pos] = val; } else { while vec.len() < pos { - vec.push(Box::new(JsValue::undefined())); + vec.push(JsValue::undefined()); } - vec.push(Box::new(val)); + vec.push(val); } } if let Some(meta) = self.prop_meta_vec_mut() { @@ -427,21 +438,22 @@ impl JsObject { pub fn push_prop(&mut self, val: JsValue) -> u32 { let vec = self.ensure_hash_props(); let pos = vec.len(); - vec.push(Box::new(val)); + vec.push(val); if let Some(meta) = self.prop_meta_vec_mut() { meta.push(None); } pos as u32 } - /// Get a stable pointer to the Box at position. + /// Get a pointer to the JsValue at position. /// Returns None if hash_props not allocated or position out of bounds. pub fn prop_ptr_at(&self, position: impl PropIndex) -> Option<*const JsValue> { if self.hash_props.is_null() { return None; } - let vec = unsafe { &*(self.hash_props as *const Vec>) }; - vec.get(position.to_u32() as usize).map(|b| &**b as *const JsValue) + // SAFETY: hash_props was set from Box> in ensure_hash_props/new_array. + let vec = unsafe { &*(self.hash_props as *const Vec) }; + vec.get(position.to_u32() as usize).map(|v| v as *const JsValue) } /// Get the length of hash_props vec (returns 0 if not allocated). @@ -449,7 +461,8 @@ impl JsObject { if self.hash_props.is_null() { 0 } else { - unsafe { &*(self.hash_props as *const Vec>) }.len() + // SAFETY: hash_props was set from Box> in ensure_hash_props/new_array. + unsafe { &*(self.hash_props as *const Vec) }.len() } } @@ -473,6 +486,8 @@ impl JsObject { if std::ptr::eq(cursor_ptr, self_ptr) { return Err("cyclic __proto__ value"); } + debug_assert!(!cursor_ptr.is_null(), "prototype cursor pointer must not be null"); + // SAFETY: cursor is known to be an object JsValue, so it encodes a valid JsObject pointer. let obj = unsafe { &*cursor_ptr }; cursor = obj.proto; } @@ -607,7 +622,7 @@ mod tests { fn prop_count_roundtrip() { let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); assert_eq!(obj.prop_count(), 0); - obj.ensure_hash_props().push(Box::new(JsValue::int(17))); + obj.ensure_hash_props().push(JsValue::int(17)); assert_eq!(obj.prop_count(), 1); } @@ -654,14 +669,10 @@ mod tests { } #[test] - fn hash_props_stable_pointer() { + fn hash_props_flat_storage_roundtrip() { let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); obj.set_prop_at(0, JsValue::int(100)); - let ptr = obj.prop_ptr_at(0); obj.set_prop_at(1, JsValue::int(200)); - // Pointer to first element should be stable (per Box allocation) - let ptr2 = obj.prop_ptr_at(0); - assert_eq!(ptr, ptr2); assert_eq!(obj.get_prop_at(0), JsValue::int(100)); assert_eq!(obj.get_prop_at(1), JsValue::int(200)); } diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index addab5ee..245560ba 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -18,6 +18,7 @@ regex.workspace = true serde_json.workspace = true chrono.workspace = true indexmap.workspace = true +smallvec.workspace = true unicode-normalization.workspace = true tracing.workspace = true diff --git a/crates/oxide_vm/src/bindings/bind_error.rs b/crates/oxide_vm/src/bindings/bind_error.rs index 52e2fa2b..b7a2d12e 100644 --- a/crates/oxide_vm/src/bindings/bind_error.rs +++ b/crates/oxide_vm/src/bindings/bind_error.rs @@ -26,19 +26,19 @@ fn bind_error_subtype_constructor( let ctor_shape1 = sh.make_shape(EMPTY_SHAPE_ID, si_prototype); let ctor_shape2 = sh.make_shape(ctor_shape1, si_name); ctor.set_shape_id(ctor_shape2); - ctor.ensure_hash_props().push(Box::new(JsValue::from_js_object(proto_ptr))); - ctor.ensure_hash_props().push(Box::new(JsValue::string(name_si, 0))); + ctor.ensure_hash_props().push(JsValue::from_js_object(proto_ptr)); + ctor.ensure_hash_props().push(JsValue::string(name_si, 0)); let ctor_ptr = Box::into_raw(ctor); let proto = unsafe { &mut *proto_ptr }; let proto_ctor_shape = sh.make_shape(proto.shape_id(), sf.intern("constructor").0); proto.set_shape_id(proto_ctor_shape); - proto.ensure_hash_props().push(Box::new(JsValue::from_js_object(ctor_ptr))); + proto.ensure_hash_props().push(JsValue::from_js_object(ctor_ptr)); let global_shape = sh.make_shape(global.shape_id(), name_si); global.set_shape_id(global_shape); - global.ensure_hash_props().push(Box::new(JsValue::from_js_object(ctor_ptr))); + global.ensure_hash_props().push(JsValue::from_js_object(ctor_ptr)); global.bump_generation(); } @@ -64,7 +64,7 @@ pub fn bind_error(kernel: &Arc, global: &mut JsObject) { let err_shape = kernel.shape_forge().make_shape(global.shape_id(), si_err); let err_val = JsValue::from_js_object(kernel.builtin_world().error_constructor.as_ptr() as *mut JsObject); global.set_shape_id(err_shape); - global.ensure_hash_props().push(Box::new(err_val)); + global.ensure_hash_props().push(err_val); global.bump_generation(); bind_error_subtype_constructor( diff --git a/crates/oxide_vm/src/bindings/bind_math.rs b/crates/oxide_vm/src/bindings/bind_math.rs index e3dc813f..8bb36345 100644 --- a/crates/oxide_vm/src/bindings/bind_math.rs +++ b/crates/oxide_vm/src/bindings/bind_math.rs @@ -68,13 +68,13 @@ pub fn bind_math(kernel: &Arc, global: &mut JsObject) { let si = kernel.string_forge().as_ref().intern(name).0; let sh_c = kernel.shape_forge().as_ref().make_shape(math.shape_id(), si); math.set_shape_id(sh_c); - math.ensure_hash_props().push(Box::new(JsValue::float(val))); + math.ensure_hash_props().push(JsValue::float(val)); } let si_m = kernel.string_forge().intern("Math").0; let m_shape = kernel.shape_forge().make_shape(global.shape_id(), si_m); let m_val = JsValue::from_js_object(kernel.builtin_world().math_object.as_ptr() as *mut JsObject); global.set_shape_id(m_shape); - global.ensure_hash_props().push(Box::new(m_val)); + global.ensure_hash_props().push(m_val); global.bump_generation(); } diff --git a/crates/oxide_vm/src/bindings/bind_number.rs b/crates/oxide_vm/src/bindings/bind_number.rs index 8c8ef5aa..88629779 100644 --- a/crates/oxide_vm/src/bindings/bind_number.rs +++ b/crates/oxide_vm/src/bindings/bind_number.rs @@ -30,7 +30,7 @@ pub fn bind_number(kernel: &Arc, global: &mut JsObject) { let num_shape = kernel.shape_forge().make_shape(global.shape_id(), si_num); let num_val = JsValue::from_js_object(kernel.builtin_world().number_constructor.as_ptr() as *mut JsObject); global.set_shape_id(num_shape); - global.ensure_hash_props().push(Box::new(num_val)); + global.ensure_hash_props().push(num_val); global.bump_generation(); { @@ -52,7 +52,7 @@ pub fn bind_number(kernel: &Arc, global: &mut JsObject) { ("NEGATIVE_INFINITY", JsValue::float(f64::NEG_INFINITY)), ("POSITIVE_INFINITY", JsValue::float(f64::INFINITY)), ] { - num_ctor.ensure_hash_props().push(Box::new(value)); + num_ctor.ensure_hash_props().push(value); let prop_si = kernel.string_forge().intern(name).0; let next_shape = kernel.shape_forge().make_shape(num_ctor.shape_id(), prop_si); num_ctor.set_shape_id(next_shape); diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index 1a222b5a..96a461ec 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -44,6 +44,6 @@ pub fn bind_string(kernel: &Arc, global: &mut JsObject) { let str_shape = kernel.shape_forge().make_shape(global.shape_id(), si_str); let str_val = JsValue::from_js_object(kernel.builtin_world().string_constructor.as_ptr() as *mut JsObject); global.set_shape_id(str_shape); - global.ensure_hash_props().push(Box::new(str_val)); + global.ensure_hash_props().push(str_val); global.bump_generation(); } diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index 3fd8c35e..b77928e3 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -54,6 +54,6 @@ fn bind_well_known_symbol(kernel: &Arc, ctor: &mut JsObject, name: let si = kernel.string_forge().intern(name).0; let shape_id = kernel.shape_forge().make_shape(ctor.shape_id(), si); ctor.set_shape_id(shape_id); - ctor.ensure_hash_props().push(Box::new(val)); + ctor.ensure_hash_props().push(val); ctor.bump_generation(); } diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 321e602b..368f50a4 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -42,7 +42,7 @@ macro_rules! bind_constructor_hash { let shape = $kernel.shape_forge().make_shape($global.shape_id(), si); let val = $crate::JsValue::from_js_object($ctor_ptr); $global.set_shape_id(shape); - $global.ensure_hash_props().push(Box::new(val)); + $global.ensure_hash_props().push(val); $global.bump_generation(); let ctor = unsafe { &mut *$ctor_ptr }; // SAFETY: $ctor_fn is a fn-item expression; casting to *const () preserves validity. @@ -73,7 +73,7 @@ pub(crate) fn bind_global_value(kernel: &Arc, global: &mut JsObject let si = kernel.string_forge().intern(name).0; let shape = kernel.shape_forge().make_shape(global.shape_id(), si); global.set_shape_id(shape); - global.ensure_hash_props().push(Box::new(value)); + global.ensure_hash_props().push(value); global.bump_generation(); } diff --git a/crates/oxide_vm/src/builtins/error.rs b/crates/oxide_vm/src/builtins/error.rs index c5df8e65..8616c5aa 100644 --- a/crates/oxide_vm/src/builtins/error.rs +++ b/crates/oxide_vm/src/builtins/error.rs @@ -118,12 +118,12 @@ pub fn error_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { let name_val = obj .hash_props_vec() .and_then(|v| v.first()) - .map(|b| **b) + .copied() .unwrap_or(JsValue::undefined()); let msg_val = obj .hash_props_vec() .and_then(|v| v.get(1)) - .map(|b| **b) + .copied() .unwrap_or(JsValue::undefined()); ( vm.lookup_str(name_val).unwrap_or_else(|| "Error".to_string()), diff --git a/crates/oxide_vm/src/builtins/function.rs b/crates/oxide_vm/src/builtins/function.rs index db8268da..ed02ae0d 100644 --- a/crates/oxide_vm/src/builtins/function.rs +++ b/crates/oxide_vm/src/builtins/function.rs @@ -19,11 +19,11 @@ fn bind_dispatcher(vm: &mut Vm, args: &[u8]) -> NativeResult { let wrapper = unsafe { &*wrapper_val.as_js_object_ptr() }; let bound_target = wrapper .hash_props_vec() - .and_then(|v| v.first().map(|b| **b)) + .and_then(|v| v.first().copied()) .unwrap_or(JsValue::undefined()); let bound_this = wrapper .hash_props_vec() - .and_then(|v| v.get(1).map(|b| **b)) + .and_then(|v| v.get(1).copied()) .unwrap_or(JsValue::undefined()); // Forward bound call arguments (skip args[0], the bound-wrapper receiver). @@ -63,7 +63,7 @@ pub fn function_apply(vm: &mut Vm, args: &[u8]) -> NativeResult { for i in 0..n { let vec = arr.hash_props_vec(); if let Some(v) = vec.and_then(|v| v.get(i)) { - vm.set_reg(base + i as u8, **v); + vm.set_reg(base + i as u8, *v); } } } else { @@ -97,8 +97,8 @@ pub fn function_bind(vm: &mut Vm, args: &[u8]) -> NativeResult { // SAFETY: bind_dispatcher is a NativeFn fn-item; valid to store as NativeFnPtr. (*wrapper).set_native_fn(Some(NativeFnPtr::from_raw(bind_dispatcher as *const ()))); (*wrapper).set_native_arg_count(0); - (*wrapper).ensure_hash_props().push(Box::new(target_val)); - (*wrapper).ensure_hash_props().push(Box::new(bound_this)); + (*wrapper).ensure_hash_props().push(target_val); + (*wrapper).ensure_hash_props().push(bound_this); } NativeResult::Ok(JsValue::from_js_object(wrapper)) diff --git a/crates/oxide_vm/src/builtins/json.rs b/crates/oxide_vm/src/builtins/json.rs index 0fd9c35f..ebb2b43a 100644 --- a/crates/oxide_vm/src/builtins/json.rs +++ b/crates/oxide_vm/src/builtins/json.rs @@ -70,7 +70,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue { let jsv = value_to_jsvalue(vm, val); let new_shape = vm.kernel().shape_forge().make_shape(obj.shape_id(), si); obj.set_shape_id(new_shape); - obj.ensure_hash_props().push(Box::new(jsv)); + obj.ensure_hash_props().push(jsv); } let obj_ptr = vm.epoch().alloc(obj); JsValue::from_js_object(obj_ptr) diff --git a/crates/oxide_vm/src/builtins/regexp.rs b/crates/oxide_vm/src/builtins/regexp.rs index 313e3e73..26109cf3 100644 --- a/crates/oxide_vm/src/builtins/regexp.rs +++ b/crates/oxide_vm/src/builtins/regexp.rs @@ -43,13 +43,13 @@ fn set_prop(obj: &mut JsObject, name: &str, val: JsValue, vm: &Vm) { let si = vm.kernel().string_forge().intern(name).0; let shape_id = vm.kernel().shape_forge().make_shape(obj.shape_id(), si); obj.set_shape_id(shape_id); - obj.ensure_hash_props().push(Box::new(val)); + obj.ensure_hash_props().push(val); } fn get_prop(obj: &JsObject, idx: usize) -> JsValue { obj.hash_props_vec() .and_then(|v| v.get(idx)) - .map(|b| **b) + .copied() .unwrap_or(JsValue::undefined()) } @@ -57,9 +57,9 @@ fn set_prop_at(obj: *mut JsObject, idx: usize, val: JsValue) { unsafe { let re = &mut *obj; if let Some(vec) = re.hash_props_vec() { - let ptr = vec.as_ptr() as *mut Box; + let ptr = vec.as_ptr() as *mut JsValue; let v = &mut *ptr.add(idx); - **v = val; + *v = val; } } } @@ -201,14 +201,14 @@ pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult { let full_match = &haystack[m.start()..m.end()]; let full_si = vm.kernel().string_forge().intern(full_match).0; - match_obj.ensure_hash_props().push(Box::new(JsValue::string(full_si, 0))); + match_obj.ensure_hash_props().push(JsValue::string(full_si, 0)); let captures = regex.captures(&haystack[m.start()..m.end()]); if let Some(caps) = &captures { for i in 1..caps.len() { let cap_str = caps.get(i).map(|cm| cm.as_str()).unwrap_or(""); let si = vm.kernel().string_forge().intern(cap_str).0; - match_obj.ensure_hash_props().push(Box::new(JsValue::string(si, 0))); + match_obj.ensure_hash_props().push(JsValue::string(si, 0)); } } diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index 5a6358cb..a26a2261 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -306,7 +306,7 @@ pub fn to_object(val: JsValue, vm: &mut Vm) -> Result { let obj = vm.epoch.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); let obj_val = JsValue::from_js_object(obj); let obj_ref = unsafe { &mut *obj }; - obj_ref.ensure_hash_props().push(Box::new(val)); + obj_ref.ensure_hash_props().push(val); obj_ref.set_prop_count(1); Ok(obj_val) } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 5e46ac74..1bb0aaa6 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -2,9 +2,32 @@ use oxide_compiler::opcode::OpCode; use oxide_kernel::prop_forge::PropTemplate; use oxide_kernel::{ic_debug, ic_trace}; use oxide_types::object::JsObject; +use oxide_types::value::JsValue; use crate::vm::Vm; +#[inline(always)] +fn ic_get_hit(obj: &JsObject, shape_id: u32, slot_index: u32) -> Option { + if shape_id != 0 && obj.shape_id() == shape_id && slot_index < obj.prop_vec_len() as u32 { + Some(obj.get_prop_at(slot_index)) + } else { + None + } +} + +#[inline(always)] +fn ic_set_hit(obj: &mut JsObject, shape_id: u32, slot_index: u32, value: JsValue) -> bool { + if shape_id != 0 && obj.shape_id() == shape_id && slot_index < obj.prop_vec_len() as u32 { + obj.set_prop_at(slot_index, value); + true + } else { + false + } +} + +#[cold] +fn prop_cache_miss() {} + impl Vm { pub(crate) fn dispatch_property_op(&mut self, op: OpCode, rd: usize, a: usize, b: usize) -> Result<(), String> { match op { @@ -70,10 +93,11 @@ impl Vm { let cached_shape_id = ext0 & 0x00FF_FFFF; let cached_slot = ext1; - if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_slot < obj.prop_vec_len() as u32 { - self.regs[a] = obj.get_prop_at(cached_slot); + if let Some(value) = ic_get_hit(obj, cached_shape_id, cached_slot) { + self.regs[a] = value; ic_trace!("IC_GET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(template) = self.kernel.prop_forge().get_template(obj.shape_id()) { + prop_cache_miss(); if template.prop_name == prop_name_si { if template.position < obj.prop_vec_len() as u32 { self.write_ic_back(obj.shape_id(), template.position); @@ -93,6 +117,7 @@ impl Vm { self.regs[a] = self.ordinary_get(obj, prop_name_si, val)?; } } else { + prop_cache_miss(); ic_debug!("IC_GET miss shape={} prop={}", obj.shape_id(), prop_name_si); let resolved = self.ordinary_get(obj, prop_name_si, val)?; if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { @@ -136,14 +161,15 @@ impl Vm { let cached_shape_id = ext0 & 0x00FF_FFFF; let cached_slot = ext1; - if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_slot < obj.prop_vec_len() as u32 { - obj.set_prop_at(cached_slot, value); + if ic_set_hit(obj, cached_shape_id, cached_slot, value) { ic_trace!("IC_SET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + prop_cache_miss(); obj.set_prop_at(pos, value); self.write_ic_back(obj.shape_id(), pos); ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); } else { + prop_cache_miss(); let old_shape = obj.shape_id(); self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; if old_shape != obj.shape_id() { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index f71e26db..1ec5a531 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use oxide_compiler::compiler::Constant; use oxide_compiler::module::CompiledModule; use oxide_compiler::opcode::{self, OpCode}; +use smallvec::SmallVec; use crate::bindings; pub use crate::bindings::init_kernel_builtins; @@ -13,6 +14,7 @@ use crate::native::{NativeFn, NativeResult}; use oxide_kernel::kernel::{KernelConfig, OxideKernel}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_kernel::{vm_debug, vm_trace}; +use oxide_types::error::{JsError, JsErrorKind}; use oxide_types::mem::{Epoch, P}; use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; @@ -27,6 +29,30 @@ pub(crate) unsafe fn native_fn_ptr_to_fn(ptr: NativeFnPtr) -> NativeFn { std::mem::transmute::<*const (), NativeFn>(ptr.as_ptr()) } +fn js_error_kind(kind: &'static str) -> JsErrorKind { + match kind { + "TypeError" => JsErrorKind::TypeError, + "RangeError" => JsErrorKind::RangeError, + "ReferenceError" => JsErrorKind::ReferenceError, + "SyntaxError" => JsErrorKind::SyntaxError, + "URIError" => JsErrorKind::URIError, + "EvalError" => JsErrorKind::EvalError, + _ => JsErrorKind::Error, + } +} + +fn js_error_kind_name(kind: JsErrorKind) -> &'static str { + match kind { + JsErrorKind::TypeError => "TypeError", + JsErrorKind::RangeError => "RangeError", + JsErrorKind::ReferenceError => "ReferenceError", + JsErrorKind::SyntaxError => "SyntaxError", + JsErrorKind::Error => "Error", + JsErrorKind::URIError => "URIError", + JsErrorKind::EvalError => "EvalError", + } +} + #[allow(unused_macros)] macro_rules! throw_err { ($self:ident, $kind:ident, $msg:expr) => {{ @@ -96,7 +122,7 @@ struct InlineSyncState { active_reg_limit: u8, root_reg_limit: u8, try_stack: Vec, - frames: Vec, + frames: SmallVec<[CallFrame; 16]>, exception_value: Option, pending_exception: Option, pending_error_kind: Option<&'static str>, @@ -110,7 +136,7 @@ pub struct Vm { pub(crate) pc: usize, pub(crate) bytecode: Vec, pub(crate) constants: Vec, - pub(crate) frames: Vec, + pub(crate) frames: SmallVec<[CallFrame; 16]>, pub for_in_iters: Vec<*mut ForInIter<'static>>, pub(crate) kernel: Arc, pub(crate) interned_strings: Vec, @@ -156,11 +182,16 @@ impl Vm { } pub(crate) fn raise_error_kind(&mut self, kind: &'static str, msg: &str) -> Result<(), String> { + self.raise_js_error(JsError::new(js_error_kind(kind), msg)) + } + + pub(crate) fn raise_js_error(&mut self, err: JsError) -> Result<(), String> { + let kind = js_error_kind_name(err.kind); let error = match kind { - "TypeError" => crate::builtins::error::create_type_error(self, msg), - "ReferenceError" => crate::builtins::error::create_reference_error(self, msg), - "SyntaxError" => crate::builtins::error::create_syntax_error(self, msg), - _ => crate::builtins::error::create_error(self, msg), + "TypeError" => crate::builtins::error::create_type_error(self, &err.message), + "ReferenceError" => crate::builtins::error::create_reference_error(self, &err.message), + "SyntaxError" => crate::builtins::error::create_syntax_error(self, &err.message), + _ => crate::builtins::error::create_error(self, &err.message), }; self.exception_value = Some(error); self.pending_error_kind = Some(kind); @@ -180,7 +211,7 @@ impl Vm { pc: 0, bytecode: Vec::new(), constants: Vec::new(), - frames: Vec::with_capacity(128), + frames: SmallVec::new(), for_in_iters: Vec::new(), kernel, interned_strings: Vec::new(), @@ -212,7 +243,7 @@ impl Vm { pc: 0, bytecode: Vec::new(), constants: Vec::new(), - frames: Vec::with_capacity(128), + frames: SmallVec::new(), for_in_iters: Vec::new(), kernel, interned_strings: Vec::new(), @@ -284,6 +315,11 @@ impl Vm { } fn clear_execution_state(&mut self) { + // Reset contract: + // - Clears register file, pc, frame/iterator stacks, saved execution stacks, + // try handlers, pending exceptions, and native call depth. + // - Leaves kernel-owned shared state intact. + // - `reset()` additionally clears bytecode/constants and resets epoch ownership. self.regs = [JsValue::undefined(); 256]; self.pc = 0; self.frames.clear(); @@ -356,7 +392,7 @@ impl Vm { let func = unsafe { &mut *obj_ptr }; let prototype_shape = self.kernel.shape_forge().make_shape(func.shape_id(), prototype_si); func.set_shape_id(prototype_shape); - func.ensure_hash_props().push(Box::new(prototype_val)); + func.ensure_hash_props().push(prototype_val); func.bump_generation(); } @@ -384,7 +420,7 @@ impl Vm { format!("{val}") } - fn convert_constant(&mut self, constant: &Constant) -> Result { + fn convert_constant(&mut self, constant: &Constant) -> Result { match constant { Constant::Number(v) => Ok(JsValue::float(*v)), Constant::Int(v) => Ok(JsValue::int(*v)), @@ -423,7 +459,7 @@ impl Vm { let ctor_ptr = self.kernel.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; let ctor = unsafe { &*ctor_ptr }; let Some(native_fn) = ctor.native_fn() else { - return Err("SyntaxError: RegExp constructor unavailable".into()); + return Err(JsError::syntax_error("RegExp constructor unavailable")); }; let saved_0 = self.regs[0]; @@ -439,13 +475,13 @@ impl Vm { self.regs[0] = saved_0; self.regs[1] = saved_1; self.regs[2] = saved_2; - result.map_err(|err| self.error_text(err)) + result.map_err(|err| JsError::syntax_error(self.error_text(err))) } } } /// Convert a module constant pool into runtime values. - fn convert_constants(&mut self, constants: &[Constant]) -> Result, String> { + fn convert_constants(&mut self, constants: &[Constant]) -> Result, JsError> { let mut values = Vec::with_capacity(constants.len()); for constant in constants { values.push(self.convert_constant(constant)?); @@ -609,7 +645,7 @@ impl Vm { } let sub = self.sub_modules[sub_idx].clone(); - let converted_constants = self.convert_constants(&sub.constants)?; + let converted_constants = self.convert_constants(&sub.constants).map_err(String::from)?; // Heap-allocate the saved state so this function's Rust stack frame stays small. let saved = Box::new(InlineSyncState { @@ -712,7 +748,7 @@ impl Vm { self.regs[254] = if sub_is_arrow { obj.captured_this() } else { this_value }; self.regs[255] = new_target; - let converted_sub_constants = self.convert_constants(&sub_constants)?; + let converted_sub_constants = self.convert_constants(&sub_constants).map_err(String::from)?; self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); self.saved_constants_stack.push(std::mem::take(&mut self.constants)); @@ -1114,7 +1150,7 @@ impl Vm { pub fn run(&mut self, module: &CompiledModule) -> Result { self.clear_execution_state(); self.sub_modules = module.sub_modules.clone(); - self.constants = self.convert_constants(&module.constants)?; + self.constants = self.convert_constants(&module.constants).map_err(String::from)?; self.sub_module_constants = vec![Vec::new(); self.sub_modules.len()]; self.bytecode = module.bytecode.clone(); self.root_reg_limit = module.n_registers.max(1);