refactor: 将基础类型定义放在单独 crate 以解决循环依赖

This commit is contained in:
爱特每羊队
2026-06-05 22:50:06 +08:00
parent 2d3ced1a7b
commit 91d4d135fa
20 changed files with 64 additions and 65 deletions
+2 -2
View File
@@ -8,8 +8,8 @@ description = "OxideJS - Register-based VM with epoch arena memory"
[dependencies]
oxide_compiler = { path = "../oxide_compiler" }
bumpalo.workspace = true
hashbrown.workspace = true
oxide_kernel = { path = "../oxide_kernel" }
oxide_types = { path = "../oxide_types" }
rustc-hash.workspace = true
fast-float.workspace = true
ryu.workspace = true
+3 -3
View File
@@ -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() {
+2 -5
View File
@@ -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;
-131
View File
@@ -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()
}
}
-270
View File
@@ -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);
}
}
-230
View File
@@ -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));
}
}
-369
View File
@@ -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();
}
}
+4 -4
View File
@@ -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,
+179
View File
@@ -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);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use std::time::Instant;
use oxide_vm::mem::{Epoch, PersistentHeap};
use oxide_types::mem::{Epoch, PersistentHeap};
#[test]
fn epoch_alloc_and_read() {
+1 -1
View File
@@ -1,6 +1,6 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::value::JsValue;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {