diff --git a/Cargo.lock b/Cargo.lock index 523f835..a48769a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,12 +79,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.12.1" @@ -250,6 +271,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -283,6 +310,43 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -293,6 +357,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -317,6 +390,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -329,6 +420,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -699,6 +796,7 @@ dependencies = [ "hashbrown 0.14.5", "oxide_compiler", "oxide_kernel", + "proptest", ] [[package]] @@ -763,6 +861,25 @@ dependencies = [ "siphasher", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -772,6 +889,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.45" @@ -781,6 +923,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -791,6 +945,44 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.12.0" @@ -820,6 +1012,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -845,6 +1043,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rustyline" version = "14.0.0" @@ -879,6 +1089,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "seq-macro" version = "0.3.6" @@ -968,6 +1184,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -999,6 +1228,12 @@ dependencies = [ "syn", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-id-start" version = "1.4.0" @@ -1035,6 +1270,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1047,6 +1288,67 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1166,6 +1468,100 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "zerocopy" version = "0.8.50" diff --git a/Cargo.toml b/Cargo.toml index 9c67a16..2511cb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ dashmap = "6" rayon = "1" bumpalo = "3" hashbrown = "0.14" +proptest = "1" [workspace.lints.clippy] all = { level = "deny", priority = -1 } diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index db8635e..c329ef2 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -11,3 +11,6 @@ oxide_compiler = { path = "../oxide_compiler" } oxide_kernel = { path = "../oxide_kernel" } bumpalo.workspace = true hashbrown.workspace = true + +[dev-dependencies] +proptest.workspace = true diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index 137fb20..523dc41 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -1 +1,4 @@ #![doc = "OxideJS - Register-based VM with epoch arena memory"] + +pub mod value; +pub use value::JsValue; diff --git a/crates/oxide_vm/src/value.rs b/crates/oxide_vm/src/value.rs new file mode 100644 index 0000000..006da3a --- /dev/null +++ b/crates/oxide_vm/src/value.rs @@ -0,0 +1,174 @@ +use std::fmt; + +/// 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; + +/// 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; + +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, PartialEq)] +pub struct JsValue(u64); + +impl JsValue { + 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 | 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 is_double(&self) -> bool { + !is_nan_bits(self.0) + } + + pub fn is_int(&self) -> bool { + get_tag(self.0) == TAG_INT + } + + pub fn is_bool(&self) -> bool { + get_tag(self.0) == TAG_BOOL + } + + pub fn is_null(&self) -> bool { + self.0 == make_tag(TAG_NULL) + } + + pub fn is_undefined(&self) -> bool { + self.0 == make_tag(TAG_UNDEFINED) + } + + pub fn is_object(&self) -> bool { + get_tag(self.0) == TAG_OBJECT + } + + 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 + } +} + +fn is_nan_bits(bits: u64) -> bool { + (bits & EXP_MASK) == EXP_MASK && (bits & MANTISSA_MASK) != 0 +} + +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 { + 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 { + write!(f, "JsValue(Unknown)") + } + } +}