refactor: 将基础类型定义放在单独 crate 以解决循环依赖
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use crate::object::JsObject;
|
||||
use crate::shape::EMPTY_SHAPE_ID;
|
||||
use crate::value::JsValue;
|
||||
use crate::vm::Vm;
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::shape::EMPTY_SHAPE_ID;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
pub fn to_primitive(val: JsValue) -> JsValue {
|
||||
if val.is_object() {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#![doc = "OxideJS - Register-based VM with epoch arena memory"]
|
||||
|
||||
pub mod coercion;
|
||||
pub mod mem;
|
||||
pub mod object;
|
||||
pub mod shape;
|
||||
pub mod value;
|
||||
pub mod vm;
|
||||
pub use value::JsValue;
|
||||
pub mod vm_pool;
|
||||
pub use oxide_types::value::JsValue;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Reference-counted persistent pointer.
|
||||
///
|
||||
/// Wraps `Arc<T>` for cross-epoch object storage. `Clone` increments the
|
||||
/// reference count; `Drop` decrements it. Objects wrapped in `P<T>` survive
|
||||
/// `Epoch::reset()` — they live on the global heap.
|
||||
#[repr(transparent)]
|
||||
pub struct P<T>(Arc<T>);
|
||||
|
||||
impl<T> P<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
Self(Arc::new(value))
|
||||
}
|
||||
|
||||
pub fn from_arena(value: &T) -> Self
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
Self(Arc::new(value.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for P<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(Arc::clone(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for P<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for P<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "P({:?})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Display> fmt::Display for P<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent heap for cross-epoch object storage.
|
||||
///
|
||||
/// Phase 3 provides a minimal API: `promote(value)` moves `value` to the
|
||||
/// global heap under `Arc` and returns `P<T>`. Phase 7 (OxideKernel) expands
|
||||
/// with typed storage for shapes, code, IC templates, and strings.
|
||||
pub struct PersistentHeap;
|
||||
|
||||
impl PersistentHeap {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Move a value to the persistent heap and return a reference-counted pointer.
|
||||
/// The returned `P<T>` survives epoch resets — it's stored on the global heap
|
||||
/// under `Arc`.
|
||||
pub fn promote<T>(&self, value: T) -> P<T> {
|
||||
P::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PersistentHeap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
///
|
||||
/// Wraps `bumpalo::Bump` with an epoch ID counter for dangling pointer detection.
|
||||
/// All Agent-call objects are allocated here; `reset()` clears them in O(1)
|
||||
/// at the end of each call.
|
||||
pub struct Epoch {
|
||||
bump: bumpalo::Bump,
|
||||
epoch_id: u64,
|
||||
}
|
||||
|
||||
impl Epoch {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bump: bumpalo::Bump::new(),
|
||||
epoch_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bump-allocate a value and return a raw pointer to it.
|
||||
/// The pointer is valid until `reset()` is called.
|
||||
pub fn alloc<T>(&self, value: T) -> *mut T {
|
||||
self.bump.alloc(value)
|
||||
}
|
||||
|
||||
pub fn bump(&self) -> &bumpalo::Bump {
|
||||
&self.bump
|
||||
}
|
||||
|
||||
/// Bump-allocate a value using a closure for initialization.
|
||||
/// Better for compiler optimization — construct directly on arena.
|
||||
pub fn alloc_with<T, F>(&self, f: F) -> *mut T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
self.bump.alloc_with(f)
|
||||
}
|
||||
|
||||
/// O(1) mass deallocation. All previous allocations become invalid.
|
||||
/// Increments epoch ID to invalidate stale pointers (debug_assert guard).
|
||||
pub fn reset(&mut self) {
|
||||
self.bump.reset();
|
||||
self.epoch_id += 1;
|
||||
}
|
||||
|
||||
/// Current epoch ID. Arena-allocated objects store this value;
|
||||
/// dereference checks they match the current ID (debug_assert only).
|
||||
pub fn current_id(&self) -> u64 {
|
||||
self.epoch_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Epoch {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
use crate::value::JsValue;
|
||||
|
||||
pub type ShapeId = u32;
|
||||
|
||||
/// Layout:
|
||||
/// header: u32 bits
|
||||
/// [0:23] shape_id
|
||||
/// [24:28] prop_count (5 bits, 0-31)
|
||||
/// [29] is_array
|
||||
/// [30] is_extensible
|
||||
/// [31] is_function
|
||||
/// inline[0..=3]: 4 x JsValue (32 bytes)
|
||||
/// overflow: *mut JsValue (8 bytes, null if ≤4 properties)
|
||||
/// proto: JsValue (8 bytes)
|
||||
/// generation: u32 (4 bytes)
|
||||
/// _pad: u32 (4 bytes, 64B total = one cache line)
|
||||
#[repr(C)]
|
||||
pub struct JsObject {
|
||||
header: u32,
|
||||
inline: [JsValue; 4],
|
||||
overflow: *mut JsValue,
|
||||
proto: JsValue,
|
||||
generation: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
impl JsObject {
|
||||
pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self {
|
||||
Self {
|
||||
header: (shape_id & 0x00FF_FFFF) | (1 << 30),
|
||||
inline: [JsValue::undefined(); 4],
|
||||
overflow: std::ptr::null_mut(),
|
||||
proto,
|
||||
generation: 1,
|
||||
_pad: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_array(shape_id: ShapeId, proto: JsValue, n_elements: usize) -> Self {
|
||||
assert!(
|
||||
n_elements <= 4,
|
||||
"arrays with >4 inline elements not yet supported"
|
||||
);
|
||||
Self {
|
||||
header: (shape_id & 0x00FF_FFFF) | (1 << 30) | (1 << 29),
|
||||
inline: [JsValue::undefined(); 4],
|
||||
overflow: std::ptr::null_mut(),
|
||||
proto,
|
||||
generation: 1,
|
||||
_pad: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape_id(&self) -> ShapeId {
|
||||
self.header & 0x00FF_FFFF
|
||||
}
|
||||
|
||||
pub fn set_shape_id(&mut self, id: ShapeId) {
|
||||
self.header = (self.header & !0x00FF_FFFF) | (id & 0x00FF_FFFF);
|
||||
}
|
||||
|
||||
pub fn prop_count(&self) -> u8 {
|
||||
((self.header >> 24) & 0x1F) as u8
|
||||
}
|
||||
|
||||
pub fn set_prop_count(&mut self, count: u8) {
|
||||
let count = count.min(31);
|
||||
self.header = (self.header & !(0x1F << 24)) | ((count as u32) << 24);
|
||||
}
|
||||
|
||||
pub fn is_array(&self) -> bool {
|
||||
(self.header >> 29) & 1 != 0
|
||||
}
|
||||
|
||||
pub fn is_extensible(&self) -> bool {
|
||||
(self.header >> 30) & 1 != 0
|
||||
}
|
||||
|
||||
pub fn set_extensible(&mut self, ext: bool) {
|
||||
if ext {
|
||||
self.header |= 1 << 30;
|
||||
} else {
|
||||
self.header &= !(1 << 30);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_function(&self) -> bool {
|
||||
(self.header >> 31) & 1 != 0
|
||||
}
|
||||
|
||||
pub fn set_function(&mut self, f: bool) {
|
||||
if f {
|
||||
self.header |= 1 << 31;
|
||||
} else {
|
||||
self.header &= !(1 << 31);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_inline_prop(&self, offset: u8) -> JsValue {
|
||||
debug_assert!(offset < 4, "inline offset out of range");
|
||||
self.inline[offset as usize]
|
||||
}
|
||||
|
||||
pub fn set_inline_prop(&mut self, offset: u8, val: JsValue) {
|
||||
debug_assert!(offset < 4, "inline offset out of range");
|
||||
self.inline[offset as usize] = val;
|
||||
}
|
||||
|
||||
pub fn get_overflow_prop(&self, offset: u8) -> JsValue {
|
||||
debug_assert!(offset >= 4);
|
||||
if self.overflow.is_null() {
|
||||
return JsValue::undefined();
|
||||
}
|
||||
unsafe { *self.overflow.add((offset - 4) as usize) }
|
||||
}
|
||||
|
||||
pub fn set_overflow_prop(&mut self, offset: u8, val: JsValue) {
|
||||
debug_assert!(offset >= 4);
|
||||
debug_assert!(!self.overflow.is_null(), "overflow buffer not allocated");
|
||||
unsafe {
|
||||
*self.overflow.add((offset - 4) as usize) = val;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_prop(&self, offset: u8) -> JsValue {
|
||||
if offset < 4 {
|
||||
self.get_inline_prop(offset)
|
||||
} else {
|
||||
self.get_overflow_prop(offset)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_prop(&mut self, offset: u8, val: JsValue) {
|
||||
if offset < 4 {
|
||||
self.set_inline_prop(offset, val);
|
||||
} else {
|
||||
self.set_overflow_prop(offset, val);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_prop_expand(&mut self, offset: u8, val: JsValue, bump: &bumpalo::Bump) {
|
||||
if offset >= 4 && (self.overflow.is_null() || offset >= self.prop_count()) {
|
||||
self.alloc_overflow(bump, (offset as usize) + 1);
|
||||
}
|
||||
self.set_prop(offset, val);
|
||||
}
|
||||
|
||||
pub fn proto(&self) -> JsValue {
|
||||
self.proto
|
||||
}
|
||||
|
||||
pub fn set_proto(&mut self, proto: JsValue) -> Result<(), &'static str> {
|
||||
if proto.is_null() {
|
||||
self.proto = proto;
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
return Ok(());
|
||||
}
|
||||
if !proto.is_object() {
|
||||
return Err("__proto__ must be an object or null");
|
||||
}
|
||||
let mut cursor = proto;
|
||||
let self_ptr = self as *const JsObject;
|
||||
while cursor.is_object() {
|
||||
let cursor_ptr = cursor.as_js_object_ptr();
|
||||
if std::ptr::eq(cursor_ptr, self_ptr) {
|
||||
return Err("cyclic __proto__ value");
|
||||
}
|
||||
let obj = unsafe { &*cursor_ptr };
|
||||
cursor = obj.proto;
|
||||
}
|
||||
self.proto = proto;
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generation(&self) -> u32 {
|
||||
self.generation
|
||||
}
|
||||
|
||||
pub fn bump_generation(&mut self) {
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn alloc_overflow(&mut self, _bump: &bumpalo::Bump, new_count: usize) -> *mut JsValue {
|
||||
use std::alloc::Layout;
|
||||
debug_assert!(new_count > 4);
|
||||
debug_assert!(new_count <= 32, "overflow limited to 32 slots max");
|
||||
let n = new_count - 4;
|
||||
let layout = Layout::array::<JsValue>(n).unwrap();
|
||||
let ptr: *mut JsValue;
|
||||
unsafe {
|
||||
ptr = _bump.alloc_layout(layout).as_ptr() as *mut JsValue;
|
||||
for i in 0..n {
|
||||
*ptr.add(i) = JsValue::undefined();
|
||||
}
|
||||
}
|
||||
self.overflow = ptr;
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shape::EMPTY_SHAPE_ID;
|
||||
|
||||
#[test]
|
||||
fn object_size_64_bytes() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<JsObject>(),
|
||||
64,
|
||||
"JsObject layout mismatch — expected 64B (one cache line)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_empty_defaults() {
|
||||
let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
assert_eq!(obj.shape_id(), EMPTY_SHAPE_ID);
|
||||
assert_eq!(obj.prop_count(), 0);
|
||||
assert!(obj.is_extensible());
|
||||
assert!(!obj.is_array());
|
||||
assert!(!obj.is_function());
|
||||
assert_eq!(obj.generation(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_id_roundtrip() {
|
||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
obj.set_shape_id(0x00AB_CDEF);
|
||||
assert_eq!(obj.shape_id(), 0x00AB_CDEF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_count_roundtrip() {
|
||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
obj.set_prop_count(17);
|
||||
assert_eq!(obj.prop_count(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_individual() {
|
||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
assert!(obj.is_extensible());
|
||||
obj.set_extensible(false);
|
||||
assert!(!obj.is_extensible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_prop_read_write() {
|
||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
obj.set_inline_prop(0, JsValue::int(42));
|
||||
assert_eq!(obj.get_inline_prop(0), JsValue::int(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_array_flags() {
|
||||
let obj = JsObject::new_array(5, JsValue::null(), 3);
|
||||
assert!(obj.is_array());
|
||||
assert_eq!(obj.shape_id(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generation_bump() {
|
||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
assert_eq!(obj.generation(), 1);
|
||||
obj.bump_generation();
|
||||
assert_eq!(obj.generation(), 2);
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
pub type ShapeId = u32;
|
||||
pub type StringIndex = u32;
|
||||
|
||||
pub const EMPTY_SHAPE_ID: ShapeId = 1;
|
||||
const EMPTY_SENTINEL: StringIndex = u32::MAX;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shape {
|
||||
pub id: ShapeId,
|
||||
pub property_name: StringIndex,
|
||||
pub property_offset: u8,
|
||||
pub parent: Option<ShapeId>,
|
||||
}
|
||||
|
||||
struct ShapeStore {
|
||||
shapes: Vec<Option<Arc<Shape>>>,
|
||||
transitions: HashMap<(ShapeId, StringIndex), ShapeId>,
|
||||
next_id: ShapeId,
|
||||
}
|
||||
|
||||
impl ShapeStore {
|
||||
fn new() -> Self {
|
||||
let mut store = Self {
|
||||
shapes: Vec::with_capacity(256),
|
||||
transitions: HashMap::with_capacity(256),
|
||||
next_id: 1,
|
||||
};
|
||||
store.create_empty_shape();
|
||||
store
|
||||
}
|
||||
|
||||
fn create_empty_shape(&mut self) {
|
||||
let empty = Arc::new(Shape {
|
||||
id: EMPTY_SHAPE_ID,
|
||||
property_name: EMPTY_SENTINEL,
|
||||
property_offset: 0,
|
||||
parent: None,
|
||||
});
|
||||
debug_assert_eq!(self.shapes.len(), 0);
|
||||
self.shapes.push(Some(empty));
|
||||
self.next_id = 2;
|
||||
}
|
||||
|
||||
fn get_shape(&self, id: ShapeId) -> Option<Arc<Shape>> {
|
||||
self.shapes.get((id - 1) as usize)?.clone()
|
||||
}
|
||||
|
||||
fn make_shape(&mut self, parent_id: ShapeId, prop_name: StringIndex) -> ShapeId {
|
||||
let key = (parent_id, prop_name);
|
||||
if let Some(&existing) = self.transitions.get(&key) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
let prop_offset = self.compute_offset(parent_id);
|
||||
let new_id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
let shape = Arc::new(Shape {
|
||||
id: new_id,
|
||||
property_name: prop_name,
|
||||
property_offset: prop_offset,
|
||||
parent: Some(parent_id),
|
||||
});
|
||||
|
||||
while self.shapes.len() < new_id as usize {
|
||||
self.shapes.push(None);
|
||||
}
|
||||
self.shapes[(new_id - 1) as usize] = Some(shape);
|
||||
self.transitions.insert(key, new_id);
|
||||
|
||||
new_id
|
||||
}
|
||||
|
||||
fn compute_offset(&self, shape_id: ShapeId) -> u8 {
|
||||
let mut count = 0u8;
|
||||
let mut cursor = Some(shape_id);
|
||||
while let Some(id) = cursor {
|
||||
match self.get_shape(id) {
|
||||
Some(s) => {
|
||||
if s.property_name != EMPTY_SENTINEL {
|
||||
count += 1;
|
||||
}
|
||||
cursor = s.parent;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
count.min(31)
|
||||
}
|
||||
|
||||
fn lookup_offset(&self, shape_id: ShapeId, prop_name: StringIndex) -> Option<u8> {
|
||||
let mut cursor = Some(shape_id);
|
||||
while let Some(id) = cursor {
|
||||
match self.get_shape(id) {
|
||||
Some(s) => {
|
||||
if s.property_name == prop_name && s.property_name != EMPTY_SENTINEL {
|
||||
return Some(s.property_offset);
|
||||
}
|
||||
cursor = s.parent;
|
||||
}
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn shape_prop_count(&self, shape_id: ShapeId) -> u8 {
|
||||
let mut count = 0u8;
|
||||
let mut cursor = Some(shape_id);
|
||||
while let Some(id) = cursor {
|
||||
match self.get_shape(id) {
|
||||
Some(s) => {
|
||||
if s.property_name != EMPTY_SENTINEL {
|
||||
count += 1;
|
||||
}
|
||||
cursor = s.parent;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
static SHAPE_STORE: OnceLock<Mutex<ShapeStore>> = OnceLock::new();
|
||||
|
||||
fn store() -> &'static Mutex<ShapeStore> {
|
||||
SHAPE_STORE.get_or_init(|| Mutex::new(ShapeStore::new()))
|
||||
}
|
||||
|
||||
pub fn get_shape(id: ShapeId) -> Option<Arc<Shape>> {
|
||||
store().lock().unwrap().get_shape(id)
|
||||
}
|
||||
|
||||
pub fn make_shape(parent_id: ShapeId, prop_name: StringIndex) -> ShapeId {
|
||||
store().lock().unwrap().make_shape(parent_id, prop_name)
|
||||
}
|
||||
|
||||
pub fn lookup_offset(shape_id: ShapeId, prop_name: StringIndex) -> Option<u8> {
|
||||
store().lock().unwrap().lookup_offset(shape_id, prop_name)
|
||||
}
|
||||
|
||||
pub fn shape_prop_count(shape_id: ShapeId) -> u8 {
|
||||
store().lock().unwrap().shape_prop_count(shape_id)
|
||||
}
|
||||
|
||||
pub fn init_shape_store() {
|
||||
let _ = store();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_shape_exists() {
|
||||
let s = get_shape(EMPTY_SHAPE_ID);
|
||||
assert!(s.is_some());
|
||||
let s = s.unwrap();
|
||||
assert_eq!(s.id, EMPTY_SHAPE_ID);
|
||||
assert!(s.parent.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_shape_creates_new_id() {
|
||||
let key: StringIndex = 1_000_000;
|
||||
let s1 = make_shape(EMPTY_SHAPE_ID, key);
|
||||
assert!(s1 > EMPTY_SHAPE_ID);
|
||||
let shape = get_shape(s1).unwrap();
|
||||
assert_eq!(shape.property_name, key);
|
||||
assert_eq!(shape.parent, Some(EMPTY_SHAPE_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_cons_returns_same_id() {
|
||||
let key: StringIndex = 1_000_001;
|
||||
let a = make_shape(EMPTY_SHAPE_ID, key);
|
||||
let b = make_shape(EMPTY_SHAPE_ID, key);
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_props_different_ids() {
|
||||
let a = make_shape(EMPTY_SHAPE_ID, 1_000_002);
|
||||
let b = make_shape(EMPTY_SHAPE_ID, 1_000_003);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_of_three() {
|
||||
let s1 = make_shape(EMPTY_SHAPE_ID, 1_000_010);
|
||||
let s2 = make_shape(s1, 1_000_020);
|
||||
let s3 = make_shape(s2, 1_000_030);
|
||||
|
||||
assert_eq!(shape_prop_count(s3), 3);
|
||||
|
||||
assert_eq!(lookup_offset(s3, 1_000_030), Some(2));
|
||||
assert_eq!(lookup_offset(s3, 1_000_020), Some(1));
|
||||
assert_eq!(lookup_offset(s3, 1_000_010), Some(0));
|
||||
assert_eq!(lookup_offset(s3, 99), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_branches_share_ancestor() {
|
||||
let base = make_shape(EMPTY_SHAPE_ID, 1_000_040);
|
||||
let branch_a = make_shape(base, 1_000_050);
|
||||
let branch_b = make_shape(base, 1_000_060);
|
||||
assert_ne!(branch_a, branch_b);
|
||||
let a_shape = get_shape(branch_a).unwrap();
|
||||
let b_shape = get_shape(branch_b).unwrap();
|
||||
assert_eq!(a_shape.parent, Some(base));
|
||||
assert_eq!(b_shape.parent, Some(base));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_same_structure_different_names() {
|
||||
let s1 = make_shape(EMPTY_SHAPE_ID, 1_000_070);
|
||||
let s2 = make_shape(s1, 1_000_080);
|
||||
let s3 = make_shape(EMPTY_SHAPE_ID, 1_000_090);
|
||||
let s4 = make_shape(s3, 1_000_100);
|
||||
assert_ne!(s1, s3);
|
||||
assert_ne!(s2, s4);
|
||||
assert_eq!(lookup_offset(s2, 1_000_070), Some(0));
|
||||
assert_eq!(lookup_offset(s4, 1_000_090), Some(0));
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::object::JsObject;
|
||||
|
||||
/// Quiet NaN prefix — bits 63-51 = sign(1) + exponent(0x7FF) + quiet_bit(1)
|
||||
const QNAN_PREFIX: u64 = 0xFFF8_0000_0000_0000;
|
||||
|
||||
/// NaN exponent mask — bits 62-52
|
||||
const EXP_MASK: u64 = 0x7FF0_0000_0000_0000;
|
||||
|
||||
/// Mantissa mask — bits 51-0
|
||||
const MANTISSA_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
|
||||
|
||||
/// Tag sits in bits 50-48 of mantissa
|
||||
const TAG_MASK: u64 = 0x0007_0000_0000_0000;
|
||||
const TAG_SHIFT: u64 = 48;
|
||||
const TAG_INT: u64 = 0;
|
||||
const TAG_BOOL: u64 = 1;
|
||||
const TAG_NULL: u64 = 2;
|
||||
const TAG_UNDEFINED: u64 = 3;
|
||||
const TAG_OBJECT: u64 = 4;
|
||||
const TAG_STRING: u64 = 5;
|
||||
|
||||
/// 48-bit pointer mask (x86-64 canonical VA)
|
||||
const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
|
||||
|
||||
/// 32-bit integer payload mask
|
||||
const INT_MASK: u64 = 0x0000_0000_FFFF_FFFF;
|
||||
|
||||
/// String payload: [hash_prefix:16bit][table_index:32bit]
|
||||
const STRING_HASH_SHIFT: u64 = 32;
|
||||
const STRING_HASH_MASK: u64 = 0xFFFF_0000_0000;
|
||||
const STRING_INDEX_MASK: u64 = 0x0000_FFFF_FFFF;
|
||||
|
||||
fn make_tag(tag: u64) -> u64 {
|
||||
QNAN_PREFIX | (tag << TAG_SHIFT)
|
||||
}
|
||||
|
||||
fn get_tag(bits: u64) -> u64 {
|
||||
(bits & TAG_MASK) >> TAG_SHIFT
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct JsValue(u64);
|
||||
|
||||
impl PartialEq for JsValue {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.is_int() && other.is_int() {
|
||||
return self.as_int() == other.as_int();
|
||||
}
|
||||
if self.is_double() && other.is_double() {
|
||||
let a = self.as_double();
|
||||
let b = other.as_double();
|
||||
if a.is_nan() || b.is_nan() {
|
||||
return false;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
if self.is_bool() && other.is_bool() {
|
||||
return self.as_bool() == other.as_bool();
|
||||
}
|
||||
if self.is_null() && other.is_null() {
|
||||
return true;
|
||||
}
|
||||
if self.is_undefined() && other.is_undefined() {
|
||||
return true;
|
||||
}
|
||||
if self.is_object() && other.is_object() {
|
||||
return self.as_ptr() == other.as_ptr();
|
||||
}
|
||||
if self.is_string() && other.is_string() {
|
||||
let ha = self.as_string_hash();
|
||||
let hb = other.as_string_hash();
|
||||
if ha != hb {
|
||||
return false;
|
||||
}
|
||||
return self.as_string_index() == other.as_string_index();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JsValue {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn from_bits(bits: u64) -> Self {
|
||||
Self(bits)
|
||||
}
|
||||
|
||||
pub fn int(v: i32) -> Self {
|
||||
Self(make_tag(TAG_INT) | (v as u32 as u64))
|
||||
}
|
||||
|
||||
pub fn float(v: f64) -> Self {
|
||||
let bits = v.to_bits();
|
||||
if is_nan_bits(bits) {
|
||||
Self(QNAN_PREFIX | (7u64 << TAG_SHIFT) | 1)
|
||||
} else {
|
||||
Self(bits)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bool(v: bool) -> Self {
|
||||
Self(make_tag(TAG_BOOL) | (v as u64))
|
||||
}
|
||||
|
||||
pub fn null() -> Self {
|
||||
Self(make_tag(TAG_NULL))
|
||||
}
|
||||
|
||||
pub fn undefined() -> Self {
|
||||
Self(make_tag(TAG_UNDEFINED))
|
||||
}
|
||||
|
||||
pub fn object(ptr: *const u8) -> Self {
|
||||
let addr = ptr as u64;
|
||||
debug_assert!(addr <= PTR_MASK, "object pointer must fit in 48 bits");
|
||||
Self(make_tag(TAG_OBJECT) | addr)
|
||||
}
|
||||
|
||||
pub fn string(index: u32, hash: u16) -> Self {
|
||||
let payload = ((hash as u64) << STRING_HASH_SHIFT) | (index as u64);
|
||||
debug_assert!(
|
||||
payload & !(STRING_HASH_MASK | STRING_INDEX_MASK) == 0,
|
||||
"string payload overflow"
|
||||
);
|
||||
Self(make_tag(TAG_STRING) | payload)
|
||||
}
|
||||
|
||||
pub fn is_double(&self) -> bool {
|
||||
!is_nan_boxed(self.0)
|
||||
}
|
||||
|
||||
pub fn is_int(&self) -> bool {
|
||||
is_nan_boxed(self.0) && get_tag(self.0) == TAG_INT
|
||||
}
|
||||
|
||||
pub fn is_bool(&self) -> bool {
|
||||
is_nan_boxed(self.0) && get_tag(self.0) == TAG_BOOL
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
is_nan_boxed(self.0) && get_tag(self.0) == TAG_NULL
|
||||
}
|
||||
|
||||
pub fn is_undefined(&self) -> bool {
|
||||
is_nan_boxed(self.0) && get_tag(self.0) == TAG_UNDEFINED
|
||||
}
|
||||
|
||||
pub fn is_object(&self) -> bool {
|
||||
is_nan_boxed(self.0) && get_tag(self.0) == TAG_OBJECT
|
||||
}
|
||||
|
||||
pub fn is_string(&self) -> bool {
|
||||
is_nan_boxed(self.0) && get_tag(self.0) == TAG_STRING
|
||||
}
|
||||
|
||||
pub fn as_double(&self) -> f64 {
|
||||
debug_assert!(self.is_double(), "JsValue is not a double");
|
||||
f64::from_bits(self.0)
|
||||
}
|
||||
|
||||
pub fn as_int(&self) -> i32 {
|
||||
debug_assert!(self.is_int(), "JsValue is not an int");
|
||||
(self.0 & INT_MASK) as i32
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> bool {
|
||||
debug_assert!(self.is_bool(), "JsValue is not a bool");
|
||||
(self.0 & 1) != 0
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *const u8 {
|
||||
debug_assert!(self.is_object(), "JsValue is not an object");
|
||||
(self.0 & PTR_MASK) as *const u8
|
||||
}
|
||||
|
||||
pub fn as_object_ptr(&self) -> *mut u8 {
|
||||
debug_assert!(self.is_object(), "JsValue is not an object");
|
||||
(self.0 & PTR_MASK) as *mut u8
|
||||
}
|
||||
|
||||
pub fn as_js_object_ptr(&self) -> *mut JsObject {
|
||||
debug_assert!(self.is_object(), "JsValue is not an object");
|
||||
(self.0 & PTR_MASK) as *mut JsObject
|
||||
}
|
||||
|
||||
pub fn from_js_object(ptr: *mut JsObject) -> Self {
|
||||
let addr = ptr as u64;
|
||||
debug_assert!(addr <= PTR_MASK, "object pointer must fit in 48 bits");
|
||||
Self(make_tag(TAG_OBJECT) | addr)
|
||||
}
|
||||
|
||||
pub fn as_string_index(&self) -> u32 {
|
||||
debug_assert!(self.is_string(), "JsValue is not a string");
|
||||
(self.0 & STRING_INDEX_MASK) as u32
|
||||
}
|
||||
|
||||
pub fn as_string_hash(&self) -> u16 {
|
||||
debug_assert!(self.is_string(), "JsValue is not a string");
|
||||
((self.0 & STRING_HASH_MASK) >> STRING_HASH_SHIFT) as u16
|
||||
}
|
||||
}
|
||||
|
||||
fn is_nan_bits(bits: u64) -> bool {
|
||||
(bits & EXP_MASK) == EXP_MASK && (bits & MANTISSA_MASK) != 0
|
||||
}
|
||||
|
||||
fn is_nan_boxed(bits: u64) -> bool {
|
||||
(0xFFF8..=0xFFFD).contains(&((bits >> 48) as u16))
|
||||
}
|
||||
|
||||
impl fmt::Display for JsValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_int() {
|
||||
write!(f, "{}", self.as_int())
|
||||
} else if self.is_double() {
|
||||
let d = self.as_double();
|
||||
if d.is_nan() {
|
||||
write!(f, "NaN")
|
||||
} else if d.is_infinite() {
|
||||
if d.is_sign_positive() {
|
||||
write!(f, "Infinity")
|
||||
} else {
|
||||
write!(f, "-Infinity")
|
||||
}
|
||||
} else {
|
||||
write!(f, "{d}")
|
||||
}
|
||||
} else if self.is_bool() {
|
||||
write!(f, "{}", self.as_bool())
|
||||
} else if self.is_null() {
|
||||
write!(f, "null")
|
||||
} else if self.is_undefined() {
|
||||
write!(f, "undefined")
|
||||
} else if self.is_object() {
|
||||
write!(f, "{{object}}")
|
||||
} else if self.is_string() {
|
||||
write!(f, "{{string}}")
|
||||
} else {
|
||||
write!(f, "{{unknown}}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for JsValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_int() {
|
||||
write!(f, "JsValue(Int({}))", self.as_int())
|
||||
} else if self.is_double() {
|
||||
let d = self.as_double();
|
||||
if d.is_nan() {
|
||||
write!(f, "JsValue(Double(NaN))")
|
||||
} else {
|
||||
write!(f, "JsValue(Double({d}))")
|
||||
}
|
||||
} else if self.is_bool() {
|
||||
write!(f, "JsValue(Bool({}))", self.as_bool())
|
||||
} else if self.is_null() {
|
||||
write!(f, "JsValue(Null)")
|
||||
} else if self.is_undefined() {
|
||||
write!(f, "JsValue(Undefined)")
|
||||
} else if self.is_object() {
|
||||
write!(f, "JsValue(Object({:p}))", self.as_ptr())
|
||||
} else if self.is_string() {
|
||||
write!(
|
||||
f,
|
||||
"JsValue(String(idx={}, hash={:#06x}))",
|
||||
self.as_string_index(),
|
||||
self.as_string_hash()
|
||||
)
|
||||
} else {
|
||||
write!(f, "JsValue(Unknown)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::JsValue;
|
||||
use proptest::prelude::*;
|
||||
use proptest::test_runner::TestRunner;
|
||||
|
||||
#[test]
|
||||
fn int_roundtrip_prop() {
|
||||
let mut runner = TestRunner::default();
|
||||
runner
|
||||
.run(&(i32::MIN..=i32::MAX), |v| {
|
||||
let val = JsValue::int(v);
|
||||
assert!(val.is_int());
|
||||
assert_eq!(val.as_int(), v);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_roundtrip_prop() {
|
||||
let mut runner = TestRunner::default();
|
||||
runner
|
||||
.run(&any::<f64>(), |v| {
|
||||
let val = JsValue::float(v);
|
||||
assert!(val.is_double());
|
||||
if !v.is_nan() {
|
||||
assert_eq!(val.as_double(), v);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bool_roundtrip_prop() {
|
||||
let mut runner = TestRunner::default();
|
||||
runner
|
||||
.run(&any::<bool>(), |v| {
|
||||
let val = JsValue::bool(v);
|
||||
assert!(val.is_bool());
|
||||
assert_eq!(val.as_bool(), v);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_u64_type_safety() {
|
||||
let mut runner = TestRunner::default();
|
||||
runner
|
||||
.run(&any::<u64>(), |bits| {
|
||||
let val = JsValue::from_bits(bits);
|
||||
let matched = [
|
||||
val.is_double(),
|
||||
val.is_int(),
|
||||
val.is_bool(),
|
||||
val.is_null(),
|
||||
val.is_undefined(),
|
||||
val.is_object(),
|
||||
val.is_string(),
|
||||
];
|
||||
let count = matched.iter().filter(|&&x| x).count();
|
||||
assert_eq!(count, 1, "bits={bits:#018x} matched {count} types");
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_identity() {
|
||||
assert_eq!(JsValue::null(), JsValue::null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_identity() {
|
||||
assert_eq!(JsValue::undefined(), JsValue::undefined());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalization_idempotent() {
|
||||
let mut runner = TestRunner::default();
|
||||
runner
|
||||
.run(&any::<f64>(), |v| {
|
||||
let a = JsValue::float(v);
|
||||
let b = JsValue::float(a.as_double());
|
||||
assert_eq!(a, b);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ use oxide_compiler::module::CompiledModule;
|
||||
use oxide_compiler::opcode::{self, OpCode};
|
||||
|
||||
use crate::coercion;
|
||||
use crate::mem::{Epoch, P};
|
||||
use crate::object::JsObject;
|
||||
use crate::shape::{self, EMPTY_SHAPE_ID};
|
||||
use crate::value::JsValue;
|
||||
use oxide_types::mem::{Epoch, P};
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::shape::{self, EMPTY_SHAPE_ID};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
pub struct CallFrame {
|
||||
pub return_addr: usize,
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#![allow(clippy::arc_with_non_send_sync)]
|
||||
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use crate::vm::Vm;
|
||||
use oxide_kernel::kernel::OxideKernel;
|
||||
use oxide_types::mem::P;
|
||||
|
||||
struct VmPoolInner {
|
||||
available: Vec<Vm>,
|
||||
total_count: usize,
|
||||
}
|
||||
|
||||
pub struct VmPool {
|
||||
kernel: Arc<OxideKernel>,
|
||||
inner: Mutex<VmPoolInner>,
|
||||
condvar: Condvar,
|
||||
#[allow(dead_code)]
|
||||
min_size: usize,
|
||||
max_size: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct VmGuard {
|
||||
vm: Option<Vm>,
|
||||
pool: Arc<VmPool>,
|
||||
dirty: bool,
|
||||
interned_strings: Vec<u32>,
|
||||
}
|
||||
|
||||
impl VmPool {
|
||||
pub fn new(
|
||||
kernel: Arc<OxideKernel>,
|
||||
#[allow(dead_code)] min_size: usize,
|
||||
max_size: Option<usize>,
|
||||
) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
kernel,
|
||||
inner: Mutex::new(VmPoolInner {
|
||||
available: Vec::with_capacity(min_size),
|
||||
total_count: 0,
|
||||
}),
|
||||
condvar: Condvar::new(),
|
||||
min_size,
|
||||
max_size,
|
||||
});
|
||||
|
||||
{
|
||||
let mut inner = pool.inner.lock().unwrap();
|
||||
for _ in 0..min_size {
|
||||
let vm = Self::new_vm(&pool.kernel);
|
||||
inner.available.push(vm);
|
||||
inner.total_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
fn new_vm(kernel: &Arc<OxideKernel>) -> Vm {
|
||||
let mut vm = Vm::new();
|
||||
vm.object_prototype = P::clone(&kernel.builtin_world().object_proto);
|
||||
vm
|
||||
}
|
||||
|
||||
fn replace_vm(&self) -> Vm {
|
||||
Self::new_vm(&self.kernel)
|
||||
}
|
||||
|
||||
pub fn spawn(self: &Arc<Self>) -> VmGuard {
|
||||
loop {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
if let Some(vm) = inner.available.pop() {
|
||||
return VmGuard {
|
||||
vm: Some(vm),
|
||||
pool: Arc::clone(self),
|
||||
dirty: false,
|
||||
interned_strings: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let can_grow = match self.max_size {
|
||||
Some(max) => inner.total_count < max,
|
||||
None => true,
|
||||
};
|
||||
|
||||
if can_grow {
|
||||
inner.total_count += 1;
|
||||
drop(inner);
|
||||
let vm = Self::new_vm(&self.kernel);
|
||||
return VmGuard {
|
||||
vm: Some(vm),
|
||||
pool: Arc::clone(self),
|
||||
dirty: false,
|
||||
interned_strings: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
inner = self.condvar.wait(inner).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VmGuard {
|
||||
pub fn vm(&self) -> &Vm {
|
||||
self.vm.as_ref().expect("VmGuard has no VM")
|
||||
}
|
||||
|
||||
pub fn vm_mut(&mut self) -> &mut Vm {
|
||||
self.vm.as_mut().expect("VmGuard has no VM")
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VmGuard {
|
||||
fn drop(&mut self) {
|
||||
let Some(vm) = self.vm.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut inner = self.pool.inner.lock().unwrap();
|
||||
|
||||
if self.dirty {
|
||||
let new_vm = self.pool.replace_vm();
|
||||
inner.available.push(new_vm);
|
||||
} else {
|
||||
for &idx in &self.interned_strings {
|
||||
self.pool.kernel.string_forge().decref(idx);
|
||||
}
|
||||
|
||||
let mut returned_vm = vm;
|
||||
returned_vm.reset();
|
||||
inner.available.push(returned_vm);
|
||||
}
|
||||
|
||||
self.pool.condvar.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use oxide_kernel::kernel::KernelConfig;
|
||||
|
||||
fn test_kernel() -> Arc<OxideKernel> {
|
||||
Arc::new(OxideKernel::new(KernelConfig::minimal()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_spawn_returns_guard() {
|
||||
let kernel = test_kernel();
|
||||
let pool = VmPool::new(kernel, 1, None);
|
||||
let guard = pool.spawn();
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_recycle_on_drop() {
|
||||
let kernel = test_kernel();
|
||||
let pool = VmPool::new(kernel, 1, None);
|
||||
let guard = pool.spawn();
|
||||
drop(guard);
|
||||
let guard2 = pool.spawn();
|
||||
drop(guard2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_grows_if_empty() {
|
||||
let kernel = test_kernel();
|
||||
let pool = VmPool::new(Arc::clone(&kernel), 1, Some(3));
|
||||
let g1 = pool.spawn();
|
||||
let g2 = pool.spawn();
|
||||
drop(g1);
|
||||
drop(g2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user