diff --git a/Cargo.lock b/Cargo.lock index 6eac61c9..7feec15b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -136,6 +145,16 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -148,6 +167,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clap" version = "4.6.1" @@ -217,6 +247,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cow-utils" version = "0.1.3" @@ -325,6 +361,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" @@ -337,6 +379,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -405,6 +471,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -435,6 +525,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -842,7 +944,9 @@ name = "oxide_vm" version = "0.1.0" dependencies = [ "bumpalo", + "chrono", "fast-float", + "indexmap", "memchr", "oxide_compiler", "oxide_kernel", @@ -916,6 +1020,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1244,12 +1354,24 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "siphasher" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -1432,6 +1554,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1497,12 +1664,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index eb6e94e8..e6e3b4c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,8 @@ fast-float = "0.2" ryu = "1" memchr = "2" regex = "1" +indexmap = "2" +chrono = { version = "0.4", default-features = false, features = ["clock"] } [workspace.lints.clippy] all = { level = "deny", priority = -1 } diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 41d813da..cce85591 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -195,6 +195,9 @@ impl CompileCtx { "Math", "JSON", "Promise", + "Date", + "Set", + "Map", "parseInt", "parseFloat", "isNaN", diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index f61eeac3..f003677a 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -120,6 +120,12 @@ pub struct BuiltinWorld { pub eval_error_proto: P, pub math_object: P, pub json_object: P, + pub date_constructor: P, + pub date_proto: P, + pub set_constructor: P, + pub set_proto: P, + pub map_constructor: P, + pub map_proto: P, } fn intern_label(string_forge: &StringForge, label: &str) -> u32 { @@ -252,6 +258,31 @@ impl BuiltinWorld { let json_object = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let (date_proto, date_constructor) = make_pair( + string_forge, + shape_forge, + "Date", + si_prototype, + si_constructor, + si_name, + ); + let (set_proto, set_constructor) = make_pair( + string_forge, + shape_forge, + "Set", + si_prototype, + si_constructor, + si_name, + ); + let (map_proto, map_constructor) = make_pair( + string_forge, + shape_forge, + "Map", + si_prototype, + si_constructor, + si_name, + ); + /// Overwrite placeholder slots (set up by make_pair) with real values. /// ctor.vec[0] = constructor.prototype -> proto /// proto.vec[0] = proto.constructor -> ctor @@ -278,6 +309,9 @@ impl BuiltinWorld { wire_ctor_proto(&boolean_constructor, &boolean_proto); wire_ctor_proto(&error_constructor, &error_proto); wire_ctor_proto(&symbol_constructor, &symbol_proto); + wire_ctor_proto(&date_constructor, &date_proto); + wire_ctor_proto(&set_constructor, &set_proto); + wire_ctor_proto(&map_constructor, &map_proto); Self { object_proto, @@ -304,6 +338,12 @@ impl BuiltinWorld { eval_error_proto, math_object, json_object, + date_constructor, + date_proto, + set_constructor, + set_proto, + map_constructor, + map_proto, } } diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 30960d09..67fa8882 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -16,6 +16,8 @@ ryu.workspace = true memchr.workspace = true regex.workspace = true serde_json.workspace = true +chrono.workspace = true +indexmap.workspace = true [dev-dependencies] proptest.workspace = true diff --git a/crates/oxide_vm/src/builtins/boolean.rs b/crates/oxide_vm/src/builtins/boolean.rs new file mode 100644 index 00000000..b582dc98 --- /dev/null +++ b/crates/oxide_vm/src/builtins/boolean.rs @@ -0,0 +1,106 @@ +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::native::NativeResult; +use crate::vm::Vm; + +pub fn boolean_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let bool_val = if args.len() > 1 { + let arg = vm.reg(args[1]); + if arg.is_undefined() || arg.is_null() { + false + } else if arg.is_bool() { + arg.as_bool() + } else if arg.is_string() { + let s = vm + .kernel() + .string_forge() + .lookup(arg.as_string_index()) + .unwrap_or_default(); + !s.is_empty() + } else if arg.is_object() { + true + } else if arg.is_int() { + arg.as_int() != 0 + } else if arg.is_double() { + arg.as_double() != 0.0 + } else { + false + } + } else { + false + }; + + let boolean_proto = vm.kernel().builtin_world().boolean_proto.as_ptr() as *mut JsObject; + let is_ctor = if this_val.is_object() { + let ptr = this_val.as_js_object_ptr(); + if ptr.is_null() { + false + } else { + let proto_ptr = unsafe { (*ptr).proto().as_js_object_ptr() }; + !proto_ptr.is_null() && std::ptr::eq(proto_ptr, boolean_proto) + } + } else { + false + }; + + if !is_ctor { + return Ok(JsValue::bool(bool_val)); + } + + let obj = unsafe { &mut *this_val.as_js_object_ptr() }; + obj.push_prop(JsValue::bool(bool_val)); + Ok(this_val) +} + +pub fn boolean_prototype_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "Boolean.prototype.valueOf called on non-Boolean object", + )); + } + let obj = unsafe { &*this_val.as_js_object_ptr() }; + let boolean_proto = vm.kernel().builtin_world().boolean_proto.as_ptr() as *mut JsObject; + let proto_ptr = obj.proto().as_js_object_ptr(); + if proto_ptr.is_null() || !std::ptr::eq(proto_ptr, boolean_proto) { + return Err(crate::builtins::error::create_type_error( + vm, + "Boolean.prototype.valueOf called on non-Boolean object", + )); + } + let val = obj.get_prop_at(0); + Ok(val) +} + +pub fn boolean_prototype_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "Boolean.prototype.toString called on non-Boolean object", + )); + } + let obj = unsafe { &*this_val.as_js_object_ptr() }; + let boolean_proto = vm.kernel().builtin_world().boolean_proto.as_ptr() as *mut JsObject; + let proto_ptr = obj.proto().as_js_object_ptr(); + if proto_ptr.is_null() || !std::ptr::eq(proto_ptr, boolean_proto) { + return Err(crate::builtins::error::create_type_error( + vm, + "Boolean.prototype.toString called on non-Boolean object", + )); + } + let val = obj.get_prop_at(0); + let is_true = if val.is_bool() { + val.as_bool() + } else { + !val.is_null() && !val.is_undefined() + }; + if is_true { + Ok(vm.intern("true")) + } else { + Ok(vm.intern("false")) + } +} diff --git a/crates/oxide_vm/src/builtins/date.rs b/crates/oxide_vm/src/builtins/date.rs new file mode 100644 index 00000000..70b3ee8f --- /dev/null +++ b/crates/oxide_vm/src/builtins/date.rs @@ -0,0 +1,640 @@ +use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, Timelike, Utc}; + +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; + +fn get_timestamp(obj: &JsObject) -> f64 { + obj.get_prop_at(0).as_double() +} + +fn set_timestamp(obj: &mut JsObject, ms: f64) { + obj.set_prop_at(0, JsValue::float(ms)); +} + +fn is_date(vm: &Vm, obj: &JsObject) -> bool { + let date_proto = vm.kernel().builtin_world().date_proto.as_ptr() as *mut JsObject; + if date_proto.is_null() { + return false; + } + let proto_ptr = obj.proto().as_js_object_ptr(); + if proto_ptr.is_null() { + return false; + } + std::ptr::eq(proto_ptr, date_proto) +} + +fn ensure_date(vm: &mut Vm, obj: &JsObject) -> NativeResult { + if !is_date(vm, obj) { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + Ok(JsValue::undefined()) +} + +fn dt_from_ms(ms: f64) -> Option> { + if !ms.is_finite() { + return None; + } + DateTime::from_timestamp_millis(ms as i64) +} + +fn naive_from_ms(ms: f64) -> Option { + if !ms.is_finite() { + return None; + } + dt_from_ms(ms).map(|dt| dt.naive_utc()) +} + +pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let is_ctor_call = this_val.is_object() && { + let ptr = this_val.as_js_object_ptr(); + if ptr.is_null() { + false + } else { + let date_proto = vm.kernel().builtin_world().date_proto.as_ptr() as *mut JsObject; + if date_proto.is_null() { + false + } else { + let obj = unsafe { &*ptr }; + let proto_ptr = obj.proto().as_js_object_ptr(); + if proto_ptr.is_null() { + false + } else { + std::ptr::eq(proto_ptr, date_proto) + } + } + } + }; + + let timestamp = if args.len() < 2 { + Utc::now().timestamp_millis() as f64 + } else { + let val = vm.reg(args[1]); + if val.is_string() { + let s = vm + .kernel() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default(); + let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"]; + let mut ts = f64::NAN; + for fmt in &formats { + if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, fmt) { + ts = ndt.and_utc().timestamp_millis() as f64; + break; + } + } + if ts.is_nan() { + if let Ok(nd) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") { + if let Some(ndt) = nd + .and_hms_opt(0, 0, 0) + .and_then(|n| n.and_local_timezone(Utc).earliest()) + { + ts = ndt.timestamp_millis() as f64; + } + } + } + ts + } else if val.is_int() || val.is_double() { + if val.is_int() { + val.as_int() as f64 + } else { + val.as_double() + } + } else if val.is_object() && { + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + false + } else { + is_date(vm, unsafe { &*ptr }) + } + } { + let obj = unsafe { &*val.as_js_object_ptr() }; + get_timestamp(obj) + } else { + let y = + coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as i32; + let m = if args.len() > 2 { + coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as u32 + } else { + 0 + }; + let d = if args.len() > 3 { + coercion::to_number(vm.reg(args[3]), vm.kernel().string_forge().as_ref()) as u32 + } else { + 1 + }; + let h = if args.len() > 4 { + coercion::to_number(vm.reg(args[4]), vm.kernel().string_forge().as_ref()) as u32 + } else { + 0 + }; + let min = if args.len() > 5 { + coercion::to_number(vm.reg(args[5]), vm.kernel().string_forge().as_ref()) as u32 + } else { + 0 + }; + let sec = if args.len() > 6 { + coercion::to_number(vm.reg(args[6]), vm.kernel().string_forge().as_ref()) as u32 + } else { + 0 + }; + let ms = if args.len() > 7 { + coercion::to_number(vm.reg(args[7]), vm.kernel().string_forge().as_ref()) as u32 + } else { + 0 + }; + NaiveDate::from_ymd_opt(y, m + 1, d) + .and_then(|nd| { + nd.and_hms_milli_opt(h, min, sec, ms) + .and_then(|ndt| ndt.and_local_timezone(Utc).earliest()) + }) + .map(|dt| dt.timestamp_millis() as f64) + .unwrap_or(f64::NAN) + } + }; + + if !is_ctor_call { + let s = if timestamp.is_finite() { + dt_from_ms(timestamp) + .map(|dt| dt.to_rfc2822()) + .unwrap_or_else(|| "Invalid Date".to_string()) + } else { + "Invalid Date".to_string() + }; + return Ok(vm.intern(&s)); + } + + let mut obj = JsObject::new_empty( + EMPTY_SHAPE_ID, + JsValue::from_js_object(vm.kernel().builtin_world().date_proto.as_ptr() as *mut JsObject), + ); + obj.set_prop_at(0, JsValue::float(timestamp)); + + let ptr = vm.epoch().alloc(obj); + Ok(JsValue::from_js_object(ptr)) +} + +pub fn date_now(_vm: &mut Vm, _args: &[u8]) -> NativeResult { + Ok(JsValue::float(Utc::now().timestamp_millis() as f64)) +} + +pub fn date_parse(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::float(f64::NAN)); + } + let val = vm.reg(args[1]); + if !val.is_string() { + return Ok(JsValue::float(f64::NAN)); + } + let s = vm + .kernel() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default(); + let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"]; + let mut ts = f64::NAN; + for fmt in &formats { + if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, fmt) { + ts = ndt.and_utc().timestamp_millis() as f64; + break; + } + } + if ts.is_nan() { + if let Ok(nd) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") { + if let Some(ndt) = nd + .and_hms_opt(0, 0, 0) + .and_then(|n| n.and_local_timezone(Utc).earliest()) + { + ts = ndt.timestamp_millis() as f64; + } + } + } + Ok(JsValue::float(ts)) +} + +pub fn date_utc(_vm: &mut Vm, _args: &[u8]) -> NativeResult { + Ok(JsValue::float(f64::NAN)) +} + +macro_rules! make_getter { + ($name:ident, $f:expr, $df:expr) => { + pub fn $name(vm: &mut Vm, args: &[u8]) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &*raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float($df)); + } + match dt_from_ms(ms) { + Some(dt) => Ok(JsValue::float(($f)(dt))), + None => Ok(JsValue::float($df)), + } + } + }; +} + +macro_rules! make_utc_getter { + ($name:ident, $f:expr, $df:expr) => { + pub fn $name(vm: &mut Vm, args: &[u8]) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &*raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float($df)); + } + match naive_from_ms(ms) { + Some(ndt) => Ok(JsValue::float(($f)(ndt))), + None => Ok(JsValue::float($df)), + } + } + }; +} + +make_getter!( + date_get_time, + |dt: DateTime| dt.timestamp_millis() as f64, + f64::NAN +); +make_getter!( + date_get_full_year, + |dt: DateTime| dt.year() as f64, + f64::NAN +); +make_getter!( + date_get_month, + |dt: DateTime| dt.month0() as f64, + f64::NAN +); +make_getter!(date_get_date, |dt: DateTime| dt.day() as f64, f64::NAN); +make_getter!( + date_get_day, + |dt: DateTime| dt.weekday().num_days_from_sunday() as f64, + f64::NAN +); +make_getter!( + date_get_hours, + |dt: DateTime| dt.hour() as f64, + f64::NAN +); +make_getter!( + date_get_minutes, + |dt: DateTime| dt.minute() as f64, + f64::NAN +); +make_getter!( + date_get_seconds, + |dt: DateTime| dt.second() as f64, + f64::NAN +); +make_getter!( + date_get_milliseconds, + |dt: DateTime| dt.timestamp_subsec_millis() as f64, + f64::NAN +); + +make_utc_getter!( + date_get_utc_full_year, + |ndt: NaiveDateTime| ndt.date().year() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_month, + |ndt: NaiveDateTime| ndt.date().month0() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_date, + |ndt: NaiveDateTime| ndt.date().day() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_day, + |ndt: NaiveDateTime| ndt.date().weekday().num_days_from_sunday() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_hours, + |ndt: NaiveDateTime| ndt.time().hour() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_minutes, + |ndt: NaiveDateTime| ndt.time().minute() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_seconds, + |ndt: NaiveDateTime| ndt.time().second() as f64, + f64::NAN +); +make_utc_getter!( + date_get_utc_milliseconds, + |ndt: NaiveDateTime| ndt.time().nanosecond() as f64 / 1_000_000.0, + f64::NAN +); + +pub fn date_get_timezone_offset(vm: &mut Vm, args: &[u8]) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &*raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let offset_min = local_offset_minutes(); + Ok(JsValue::float(offset_min as f64)) +} + +fn local_offset_minutes() -> i32 { + chrono::Local::now().offset().local_minus_utc() / 60 +} + +fn get_opt_arg(vm: &Vm, args: &[u8], idx: usize, default: u32) -> u32 { + if args.len() > idx { + coercion::to_number(vm.reg(args[idx]), vm.kernel().string_forge().as_ref()) as u32 + } else { + default + } +} + +pub fn date_set_time(vm: &mut Vm, args: &[u8]) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let val = if args.len() > 1 { + coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) + } else { + f64::NAN + }; + set_timestamp(obj, val); + Ok(JsValue::float(val)) +} + +pub fn date_set_full_year(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let m = get_opt_arg(vm, args, 2, dt.month0()); + let d = get_opt_arg(vm, args, 3, dt.day0()); + let nd = dt + .with_year(v as i32) + .and_then(|x| x.with_month0(m)) + .and_then(|x| x.with_day0(d)) + .unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} +pub fn date_set_month(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let d = get_opt_arg(vm, args, 2, dt.day0()); + let nd = dt + .with_month0(v as u32) + .and_then(|x| x.with_day0(d)) + .unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} +pub fn date_set_date(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let nd = dt.with_day0(v as u32).unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} +pub fn date_set_hours(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let min = get_opt_arg(vm, args, 2, dt.minute()); + let sec = get_opt_arg(vm, args, 3, dt.second()); + let ms_arg = get_opt_arg(vm, args, 4, dt.timestamp_subsec_millis()); + let nd = dt + .with_hour(v as u32) + .and_then(|x| x.with_minute(min)) + .and_then(|x| x.with_second(sec)) + .and_then(|x| x.with_nanosecond(ms_arg * 1_000_000)) + .unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} +pub fn date_set_minutes(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let sec = get_opt_arg(vm, args, 2, dt.second()); + let ms_arg = get_opt_arg(vm, args, 3, dt.timestamp_subsec_millis()); + let nd = dt + .with_minute(v as u32) + .and_then(|x| x.with_second(sec)) + .and_then(|x| x.with_nanosecond(ms_arg * 1_000_000)) + .unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} +pub fn date_set_seconds(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let ms_arg = get_opt_arg(vm, args, 2, dt.timestamp_subsec_millis()); + let nd = dt + .with_second(v as u32) + .and_then(|x| x.with_nanosecond(ms_arg * 1_000_000)) + .unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} +pub fn date_set_milliseconds(vm: &mut Vm, args: &[u8]) -> NativeResult { + let val = vm.reg(args[1]); + let v = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + let raw = vm.reg(args[0]); + let obj = unsafe { &mut *raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::float(f64::NAN)); + } + let dt = match dt_from_ms(ms) { + Some(d) => d, + None => return Ok(JsValue::float(f64::NAN)), + }; + let nd = dt.with_nanosecond(v as u32 * 1_000_000).unwrap_or(dt); + let ts = nd.timestamp_millis() as f64; + set_timestamp(obj, ts); + Ok(JsValue::float(ts)) +} + +fn date_to_string_inner(vm: &mut Vm, args: &[u8], format_str: &str, invalid: &str) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &*raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(vm.intern(invalid)); + } + let s = match dt_from_ms(ms) { + Some(dt) => dt.format(format_str).to_string(), + None => invalid.to_string(), + }; + Ok(vm.intern(&s)) +} + +pub fn date_to_iso_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + date_to_string_inner(vm, args, "%Y-%m-%dT%H:%M:%S%.3fZ", "Invalid Date") +} + +pub fn date_to_json(vm: &mut Vm, args: &[u8]) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &*raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return Ok(JsValue::null()); + } + let s = match dt_from_ms(ms) { + Some(dt) => dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + None => return Ok(JsValue::null()), + }; + Ok(vm.intern(&s)) +} + +pub fn date_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + date_to_string_inner(vm, args, "%a %b %d %Y %H:%M:%S %Z %z", "Invalid Date") +} + +pub fn date_to_date_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + date_to_string_inner(vm, args, "%a %b %d %Y", "Invalid Date") +} + +pub fn date_to_time_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + date_to_string_inner(vm, args, "%H:%M:%S %Z", "Invalid Date") +} + +pub fn date_to_utc_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + date_to_string_inner(vm, args, "%a, %d %b %Y %H:%M:%S GMT", "Invalid Date") +} + +pub fn date_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { + let raw = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !raw.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on incompatible receiver", + )); + } + let obj = unsafe { &*raw.as_js_object_ptr() }; + ensure_date(vm, obj)?; + let ms = get_timestamp(obj); + Ok(JsValue::float(ms)) +} diff --git a/crates/oxide_vm/src/builtins/map.rs b/crates/oxide_vm/src/builtins/map.rs new file mode 100644 index 00000000..399b733f --- /dev/null +++ b/crates/oxide_vm/src/builtins/map.rs @@ -0,0 +1,111 @@ +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::builtins::set::SetKey; +use crate::native::NativeResult; +use crate::vm::Vm; + +const MAP_PROP_INDEX: u8 = 0; + +fn get_map_inner( + vm: &mut Vm, + this_val: JsValue, +) -> Result<*mut indexmap::IndexMap, JsValue> { + if !this_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on non-Map object", + )); + } + let map_ptr = this_val.as_js_object_ptr(); + if map_ptr.is_null() { + return Err(crate::builtins::error::create_type_error( + vm, + "Map internal state invalid", + )); + } + let map_obj = unsafe { &*map_ptr }; + let inner_val = map_obj.get_prop_at(MAP_PROP_INDEX); + if !inner_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "Map internal state invalid", + )); + } + let inner_ptr = inner_val.as_js_object_ptr() as *mut indexmap::IndexMap; + if inner_ptr.is_null() { + return Err(crate::builtins::error::create_type_error( + vm, + "Map internal state invalid", + )); + } + Ok(inner_ptr) +} + +fn new_map_inner() -> *mut indexmap::IndexMap { + Box::into_raw(Box::new(indexmap::IndexMap::new())) +} + +fn alloc_map(vm: &mut Vm) -> *mut JsObject { + let map_proto = vm.kernel().builtin_world().map_proto.as_ptr() as *mut JsObject; + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(map_proto)); + let inner = new_map_inner(); + obj.set_prop_at(MAP_PROP_INDEX, JsValue::object(inner as *mut u8)); + vm.epoch().alloc(obj) +} + +pub fn map_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult { + let map_obj = alloc_map(vm); + Ok(JsValue::from_js_object(map_obj)) +} + +pub fn map_set(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_map_inner(vm, this_val)?; + let key = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + let val = vm.reg(if args.len() > 2 { args[2] } else { 0 }); + unsafe { + (*inner).insert(SetKey(key), val); + } + Ok(this_val) +} + +pub fn map_get(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_map_inner(vm, this_val)?; + let key = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + let found = unsafe { (*inner).get(&SetKey(key)).copied() }; + Ok(found.unwrap_or(JsValue::undefined())) +} + +pub fn map_has(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_map_inner(vm, this_val)?; + let key = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + let found = unsafe { (*inner).contains_key(&SetKey(key)) }; + Ok(JsValue::bool(found)) +} + +pub fn map_delete(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_map_inner(vm, this_val)?; + let key = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + let removed = unsafe { (*inner).shift_remove(&SetKey(key)) }; + Ok(JsValue::bool(removed.is_some())) +} + +pub fn map_clear(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_map_inner(vm, this_val)?; + unsafe { + (*inner).clear(); + } + Ok(JsValue::undefined()) +} + +pub fn map_size(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_map_inner(vm, this_val)?; + Ok(JsValue::float(unsafe { (*inner).len() } as f64)) +} diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index 7b883ace..ca79ee76 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -1,9 +1,13 @@ pub mod array; +pub mod boolean; +pub mod date; pub mod error; pub mod function; pub mod global; pub mod json; +pub mod map; pub mod math; pub mod number; pub mod object; +pub mod set; pub mod string; diff --git a/crates/oxide_vm/src/builtins/set.rs b/crates/oxide_vm/src/builtins/set.rs new file mode 100644 index 00000000..2fa58a6a --- /dev/null +++ b/crates/oxide_vm/src/builtins/set.rs @@ -0,0 +1,145 @@ +use std::hash::{Hash, Hasher}; + +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::native::NativeResult; +use crate::vm::Vm; + +const SET_PROP_INDEX: u8 = 0; + +#[derive(Clone, Copy)] +pub struct SetKey(pub JsValue); + +impl PartialEq for SetKey { + fn eq(&self, other: &Self) -> bool { + if self.0.is_double() && other.0.is_double() { + let a = self.0.as_double(); + let b = other.0.as_double(); + if a.is_nan() && b.is_nan() { + return true; + } + if a == 0.0 && b == 0.0 { + return true; + } + return a == b; + } + unsafe { + std::mem::transmute::(self.0) + == std::mem::transmute::(other.0) + } + } +} + +impl Eq for SetKey {} + +impl Hash for SetKey { + fn hash(&self, state: &mut H) { + if self.0.is_double() { + let d = self.0.as_double(); + if d.is_nan() { + 0u64.hash(state); + } else if d == 0.0 { + 0.0f64.to_bits().hash(state); + } else { + d.to_bits().hash(state); + } + } else { + unsafe { std::mem::transmute::(self.0).hash(state) } + } + } +} + +fn get_set_inner( + vm: &mut Vm, + this_val: JsValue, +) -> Result<*mut indexmap::IndexSet, JsValue> { + if !this_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "called on non-Set object", + )); + } + let set_ptr = this_val.as_js_object_ptr(); + if set_ptr.is_null() { + return Err(crate::builtins::error::create_type_error( + vm, + "Set internal state invalid", + )); + } + let set_obj = unsafe { &*set_ptr }; + let inner_val = set_obj.get_prop_at(SET_PROP_INDEX); + if !inner_val.is_object() { + return Err(crate::builtins::error::create_type_error( + vm, + "Set internal state invalid", + )); + } + let inner_ptr = inner_val.as_js_object_ptr() as *mut indexmap::IndexSet; + if inner_ptr.is_null() { + return Err(crate::builtins::error::create_type_error( + vm, + "Set internal state invalid", + )); + } + Ok(inner_ptr) +} + +fn new_set_inner() -> *mut indexmap::IndexSet { + Box::into_raw(Box::new(indexmap::IndexSet::new())) +} + +fn alloc_set(vm: &mut Vm) -> *mut JsObject { + let set_proto = vm.kernel().builtin_world().set_proto.as_ptr() as *mut JsObject; + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(set_proto)); + let inner = new_set_inner(); + obj.set_prop_at(SET_PROP_INDEX, JsValue::object(inner as *mut u8)); + vm.epoch().alloc(obj) +} + +pub fn set_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult { + let set_obj = alloc_set(vm); + Ok(JsValue::from_js_object(set_obj)) +} + +pub fn set_add(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_set_inner(vm, this_val)?; + let val = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + unsafe { + (*inner).insert(SetKey(val)); + } + Ok(this_val) +} + +pub fn set_has(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_set_inner(vm, this_val)?; + let val = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + let found = unsafe { (*inner).contains(&SetKey(val)) }; + Ok(JsValue::bool(found)) +} + +pub fn set_delete(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_set_inner(vm, this_val)?; + let val = vm.reg(if args.len() > 1 { args[1] } else { 0 }); + let removed = unsafe { (*inner).shift_remove(&SetKey(val)) }; + Ok(JsValue::bool(removed)) +} + +pub fn set_clear(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_set_inner(vm, this_val)?; + unsafe { + (*inner).clear(); + } + Ok(JsValue::undefined()) +} + +pub fn set_size(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let inner = get_set_inner(vm, this_val)?; + Ok(JsValue::float(unsafe { (*inner).len() } as f64)) +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 4c688c72..ff7a8320 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -709,6 +709,512 @@ fn bind_json(kernel: &Arc, global: &mut JsObject) { global.bump_generation(); } +fn bind_date(kernel: &Arc, global: &mut JsObject) { + let ctor_ptr = kernel.builtin_world().date_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + let proto_ptr = kernel.builtin_world().date_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + let sf = kernel.string_forge().as_ref(); + let sh = kernel.shape_forge().as_ref(); + + bind_method!( + kernel.builtin_world(), + ctor, + sf, + sh, + "now", + crate::builtins::date::date_now, + 0 + ); + bind_method!( + kernel.builtin_world(), + ctor, + sf, + sh, + "parse", + crate::builtins::date::date_parse, + 1 + ); + bind_method!( + kernel.builtin_world(), + ctor, + sf, + sh, + "UTC", + crate::builtins::date::date_utc, + 7 + ); + + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getTime", + crate::builtins::date::date_get_time, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getFullYear", + crate::builtins::date::date_get_full_year, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getMonth", + crate::builtins::date::date_get_month, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getDate", + crate::builtins::date::date_get_date, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getDay", + crate::builtins::date::date_get_day, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getHours", + crate::builtins::date::date_get_hours, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getMinutes", + crate::builtins::date::date_get_minutes, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getSeconds", + crate::builtins::date::date_get_seconds, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getMilliseconds", + crate::builtins::date::date_get_milliseconds, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCFullYear", + crate::builtins::date::date_get_utc_full_year, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCMonth", + crate::builtins::date::date_get_utc_month, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCDate", + crate::builtins::date::date_get_utc_date, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCDay", + crate::builtins::date::date_get_utc_day, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCHours", + crate::builtins::date::date_get_utc_hours, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCMinutes", + crate::builtins::date::date_get_utc_minutes, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCSeconds", + crate::builtins::date::date_get_utc_seconds, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getUTCMilliseconds", + crate::builtins::date::date_get_utc_milliseconds, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "getTimezoneOffset", + crate::builtins::date::date_get_timezone_offset, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setTime", + crate::builtins::date::date_set_time, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setFullYear", + crate::builtins::date::date_set_full_year, + 3 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setMonth", + crate::builtins::date::date_set_month, + 2 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setDate", + crate::builtins::date::date_set_date, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setHours", + crate::builtins::date::date_set_hours, + 4 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setMinutes", + crate::builtins::date::date_set_minutes, + 3 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setSeconds", + crate::builtins::date::date_set_seconds, + 2 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "setMilliseconds", + crate::builtins::date::date_set_milliseconds, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toISOString", + crate::builtins::date::date_to_iso_string, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toJSON", + crate::builtins::date::date_to_json, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toString", + crate::builtins::date::date_to_string, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toDateString", + crate::builtins::date::date_to_date_string, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toTimeString", + crate::builtins::date::date_to_time_string, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toUTCString", + crate::builtins::date::date_to_utc_string, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "valueOf", + crate::builtins::date::date_value_of, + 0 + ); + + let si_d = kernel.string_forge().intern("Date").0; + let d_shape = kernel.shape_forge().make_shape(global.shape_id(), si_d); + let d_val = JsValue::from_js_object(ctor_ptr); + global.set_shape_id(d_shape); + global.ensure_hash_props().push(Box::new(d_val)); + global.bump_generation(); +} + +fn bind_set(kernel: &Arc, global: &mut JsObject) { + let ctor_ptr = kernel.builtin_world().set_constructor.as_ptr() as *mut JsObject; + let proto_ptr = kernel.builtin_world().set_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + let sf = kernel.string_forge().as_ref(); + let sh = kernel.shape_forge().as_ref(); + + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "add", + crate::builtins::set::set_add, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "has", + crate::builtins::set::set_has, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "delete", + crate::builtins::set::set_delete, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "clear", + crate::builtins::set::set_clear, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "size", + crate::builtins::set::set_size, + 0 + ); + + let si_s = kernel.string_forge().intern("Set").0; + let s_shape = kernel.shape_forge().make_shape(global.shape_id(), si_s); + let s_val = JsValue::from_js_object(ctor_ptr); + global.set_shape_id(s_shape); + global.ensure_hash_props().push(Box::new(s_val)); + global.bump_generation(); +} + +fn bind_map(kernel: &Arc, global: &mut JsObject) { + let ctor_ptr = kernel.builtin_world().map_constructor.as_ptr() as *mut JsObject; + let proto_ptr = kernel.builtin_world().map_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + let sf = kernel.string_forge().as_ref(); + let sh = kernel.shape_forge().as_ref(); + + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "set", + crate::builtins::map::map_set, + 2 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "get", + crate::builtins::map::map_get, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "has", + crate::builtins::map::map_has, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "delete", + crate::builtins::map::map_delete, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "clear", + crate::builtins::map::map_clear, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "size", + crate::builtins::map::map_size, + 0 + ); + + let si_m = kernel.string_forge().intern("Map").0; + let m_shape = kernel.shape_forge().make_shape(global.shape_id(), si_m); + let m_val = JsValue::from_js_object(ctor_ptr); + global.set_shape_id(m_shape); + global.ensure_hash_props().push(Box::new(m_val)); + global.bump_generation(); +} + +fn bind_boolean(kernel: &Arc) { + let ctor_ptr = kernel.builtin_world().boolean_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + let proto_ptr = kernel.builtin_world().boolean_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + let sf = kernel.string_forge().as_ref(); + let sh = kernel.shape_forge().as_ref(); + + ctor.set_native_fn(Some( + crate::builtins::boolean::boolean_constructor as *const (), + )); + ctor.set_native_arg_count(1); + + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "valueOf", + crate::builtins::boolean::boolean_prototype_value_of, + 0 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toString", + crate::builtins::boolean::boolean_prototype_to_string, + 0 + ); +} + fn bind_function(kernel: &Arc) { let function_methods = FunctionMethods { call: crate::builtins::function::function_call as *const (), @@ -733,6 +1239,10 @@ pub fn init_kernel_builtins(kernel: &Arc) { bind_number(kernel, global); bind_math(kernel, global); bind_json(kernel, global); + bind_date(kernel, global); + bind_set(kernel, global); + bind_map(kernel, global); + bind_boolean(kernel); bind_function(kernel); } diff --git a/crates/oxide_vm/tests/builtins_boolean_tests.rs b/crates/oxide_vm/tests/builtins_boolean_tests.rs new file mode 100644 index 00000000..4e5e0cf0 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_boolean_tests.rs @@ -0,0 +1,82 @@ +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; +use oxide_vm::builtins::boolean::{ + boolean_constructor, boolean_prototype_to_string, boolean_prototype_value_of, +}; +use oxide_vm::vm::Vm; + +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; + +fn bool_ctor_call(vm: &mut Vm, arg: JsValue) -> JsValue { + vm.set_reg(1, arg); + boolean_constructor(vm, &[0, 1]).unwrap() +} + +fn tbool_wrap(vm: &mut Vm, arg: JsValue) -> JsValue { + let proto_ptr = vm.kernel().builtin_world().boolean_proto.as_ptr() as *mut JsObject; + let wrapper = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr)); + let ptr = vm.epoch().alloc(wrapper); + let val = JsValue::from_js_object(ptr); + vm.set_reg(0, val); + vm.set_reg(1, arg); + boolean_constructor(vm, &[0, 1]).unwrap() +} + +#[test] +fn tboolean_call_true() { + let mut vm = Vm::new(); + assert!(bool_ctor_call(&mut vm, JsValue::float(1.0)).as_bool()); +} + +#[test] +fn tboolean_call_false() { + let mut vm = Vm::new(); + assert!(!bool_ctor_call(&mut vm, JsValue::float(0.0)).as_bool()); +} + +#[test] +fn tboolean_call_empty_is_false() { + let mut vm = Vm::new(); + let empty = vm.intern(""); + assert!(!bool_ctor_call(&mut vm, empty).as_bool()); +} + +#[test] +fn tboolean_new_value_of_true() { + let mut vm = Vm::new(); + let wrapper = tbool_wrap(&mut vm, JsValue::float(1.0)); + vm.set_reg(0, wrapper); + assert!(boolean_prototype_value_of(&mut vm, &[0]).unwrap().as_bool()); +} + +#[test] +fn tboolean_new_value_of_false() { + let mut vm = Vm::new(); + let wrapper = tbool_wrap(&mut vm, JsValue::float(0.0)); + vm.set_reg(0, wrapper); + assert!(!boolean_prototype_value_of(&mut vm, &[0]).unwrap().as_bool()); +} + +#[test] +fn tboolean_new_tostring() { + let mut vm = Vm::new(); + let w = tbool_wrap(&mut vm, JsValue::float(1.0)); + vm.set_reg(0, w); + let r = boolean_prototype_to_string(&mut vm, &[0]).unwrap(); + let s = vm + .kernel() + .string_forge() + .lookup(r.as_string_index()) + .unwrap(); + assert_eq!(s, "true"); + + let w2 = tbool_wrap(&mut vm, JsValue::float(0.0)); + vm.set_reg(0, w2); + let r2 = boolean_prototype_to_string(&mut vm, &[0]).unwrap(); + let s2 = vm + .kernel() + .string_forge() + .lookup(r2.as_string_index()) + .unwrap(); + assert_eq!(s2, "false"); +} diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs new file mode 100644 index 00000000..b9713ebc --- /dev/null +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -0,0 +1,45 @@ +use oxide_compiler::compiler::Compiler; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = + oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new() + .compile(&program) + .map_err(|e| format!("Compile error: {}", e))?; + vm.run(&module) +} + +#[test] +fn date_now_returns_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Date.now()").unwrap(); + assert!(result.is_double()); + assert!(result.as_double() > 0.0); +} + +#[test] +fn date_now_increases() { + let mut vm = Vm::new(); + let t1 = eval(&mut vm, "Date.now()").unwrap().as_double(); + let t2 = eval(&mut vm, "Date.now()").unwrap().as_double(); + assert!(t2 >= t1); +} + +#[test] +fn date_parse_returns_nan() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Date.parse('invalid-date')").unwrap(); + assert!(result.is_double()); + assert!(result.as_double().is_nan()); +} + +#[test] +fn date_parse_iso_format() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Date.parse('2020-01-15')").unwrap(); + assert!(result.is_double()); + assert!(!result.as_double().is_nan()); + assert!(result.as_double() > 0.0); +} diff --git a/crates/oxide_vm/tests/builtins_map_tests.rs b/crates/oxide_vm/tests/builtins_map_tests.rs new file mode 100644 index 00000000..e5c6f52e --- /dev/null +++ b/crates/oxide_vm/tests/builtins_map_tests.rs @@ -0,0 +1,73 @@ +use oxide_types::value::JsValue; +use oxide_vm::builtins::map::{ + map_clear, map_constructor as new_map, map_delete, map_get, map_has, map_set, map_size, +}; +use oxide_vm::vm::Vm; + +#[test] +fn tmap_constructor_returns_object() { + let mut vm = Vm::new(); + let r = new_map(&mut vm, &[0]).unwrap(); + assert!(r.is_object()); +} + +#[test] +fn tmap_set_get() { + let mut vm = Vm::new(); + let m = new_map(&mut vm, &[0]).unwrap(); + vm.set_reg(0, m); + vm.set_reg(1, JsValue::float(42.0)); + vm.set_reg(2, JsValue::float(100.0)); + map_set(&mut vm, &[0, 1, 2]).unwrap(); + assert_eq!(map_get(&mut vm, &[0, 1]).unwrap().as_double(), 100.0); +} + +#[test] +fn tmap_get_missing() { + let mut vm = Vm::new(); + let m = new_map(&mut vm, &[0]).unwrap(); + vm.set_reg(0, m); + vm.set_reg(1, JsValue::float(99.0)); + assert!(map_get(&mut vm, &[0, 1]).unwrap().is_undefined()); +} + +#[test] +fn tmap_has_check() { + let mut vm = Vm::new(); + let m = new_map(&mut vm, &[0]).unwrap(); + vm.set_reg(0, m); + vm.set_reg(1, JsValue::float(1.0)); + vm.set_reg(2, JsValue::float(2.0)); + map_set(&mut vm, &[0, 1, 2]).unwrap(); + assert!(map_has(&mut vm, &[0, 1]).unwrap().as_bool()); + assert!(!map_has(&mut vm, &[0, 99]).unwrap().as_bool()); +} + +#[test] +fn tmap_delete_works() { + let mut vm = Vm::new(); + let m = new_map(&mut vm, &[0]).unwrap(); + vm.set_reg(0, m); + vm.set_reg(1, JsValue::float(7.0)); + vm.set_reg(2, JsValue::float(8.0)); + map_set(&mut vm, &[0, 1, 2]).unwrap(); + assert!(map_delete(&mut vm, &[0, 1]).unwrap().as_bool()); + assert!(!map_has(&mut vm, &[0, 1]).unwrap().as_bool()); +} + +#[test] +fn tmap_size_and_clear() { + let mut vm = Vm::new(); + let m = new_map(&mut vm, &[0]).unwrap(); + vm.set_reg(0, m); + assert_eq!(map_size(&mut vm, &[0]).unwrap().as_double(), 0.0); + vm.set_reg(1, JsValue::float(1.0)); + vm.set_reg(2, JsValue::float(10.0)); + map_set(&mut vm, &[0, 1, 2]).unwrap(); + vm.set_reg(1, JsValue::float(2.0)); + vm.set_reg(2, JsValue::float(20.0)); + map_set(&mut vm, &[0, 1, 2]).unwrap(); + assert_eq!(map_size(&mut vm, &[0]).unwrap().as_double(), 2.0); + map_clear(&mut vm, &[0]).unwrap(); + assert_eq!(map_size(&mut vm, &[0]).unwrap().as_double(), 0.0); +} diff --git a/crates/oxide_vm/tests/builtins_set_tests.rs b/crates/oxide_vm/tests/builtins_set_tests.rs new file mode 100644 index 00000000..05e0badd --- /dev/null +++ b/crates/oxide_vm/tests/builtins_set_tests.rs @@ -0,0 +1,90 @@ +use oxide_types::value::JsValue; +use oxide_vm::builtins::set::{ + set_add, set_clear, set_constructor as new_set, set_delete, set_has, set_size, +}; +use oxide_vm::vm::Vm; + +#[test] +fn tset_constructor_returns_object() { + let mut vm = Vm::new(); + let r = new_set(&mut vm, &[0]).unwrap(); + assert!(r.is_object()); +} + +#[test] +fn tset_add_has() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + vm.set_reg(1, JsValue::float(42.0)); + let _ = set_add(&mut vm, &[0, 1]).unwrap(); + assert!(set_has(&mut vm, &[0, 1]).unwrap().as_bool()); +} + +#[test] +fn tset_has_missing() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + vm.set_reg(1, JsValue::float(99.0)); + assert!(!set_has(&mut vm, &[0, 1]).unwrap().as_bool()); +} + +#[test] +fn tset_delete_works() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + vm.set_reg(1, JsValue::float(7.0)); + set_add(&mut vm, &[0, 1]).unwrap(); + assert!(set_delete(&mut vm, &[0, 1]).unwrap().as_bool()); + assert!(!set_has(&mut vm, &[0, 1]).unwrap().as_bool()); +} + +#[test] +fn tset_size_increases() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 0.0); + vm.set_reg(1, JsValue::float(1.0)); + set_add(&mut vm, &[0, 1]).unwrap(); + vm.set_reg(1, JsValue::float(2.0)); + set_add(&mut vm, &[0, 1]).unwrap(); + assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 2.0); +} + +#[test] +fn tset_clear_works() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + vm.set_reg(1, JsValue::float(1.0)); + set_add(&mut vm, &[0, 1]).unwrap(); + set_clear(&mut vm, &[0]).unwrap(); + assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 0.0); +} + +#[test] +fn tset_nan_equality() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + vm.set_reg(1, JsValue::float(f64::NAN)); + set_add(&mut vm, &[0, 1]).unwrap(); + assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 1.0); + set_add(&mut vm, &[0, 1]).unwrap(); + assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 1.0); +} + +#[test] +fn tset_signed_zero() { + let mut vm = Vm::new(); + let s = new_set(&mut vm, &[0]).unwrap(); + vm.set_reg(0, s); + vm.set_reg(1, JsValue::float(0.0)); + set_add(&mut vm, &[0, 1]).unwrap(); + vm.set_reg(1, JsValue::float(-0.0)); + set_add(&mut vm, &[0, 1]).unwrap(); + assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 1.0); +}