feat: 支持 Date/Map/Set 的 new 运算符

This commit is contained in:
2026-06-08 03:42:47 +08:00
parent c66c3f0f50
commit e991e58297
6 changed files with 344 additions and 130 deletions
+36 -39
View File
@@ -76,6 +76,41 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
let timestamp = if args.len() < 2 {
Utc::now().timestamp_millis() as f64
} else if args.len() > 2 {
let y = coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as i32;
let m = coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as u32;
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)
} else {
let val = vm.reg(args[1]);
if val.is_string() {
@@ -120,45 +155,7 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
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)
f64::NAN
}
};
+20 -2
View File
@@ -717,6 +717,9 @@ fn bind_date(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
let sf = kernel.string_forge().as_ref();
let sh = kernel.shape_forge().as_ref();
ctor.set_native_fn(Some(crate::builtins::date::date_constructor as *const ()));
ctor.set_native_arg_count(7);
bind_method!(
kernel.builtin_world(),
ctor,
@@ -1053,7 +1056,11 @@ fn bind_date(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
fn bind_set(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
let ctor_ptr = kernel.builtin_world().set_constructor.as_ptr() as *mut JsObject;
let ctor = unsafe { &mut *ctor_ptr };
let proto_ptr = kernel.builtin_world().set_proto.as_ptr() as *mut JsObject;
ctor.set_native_fn(Some(crate::builtins::set::set_constructor as *const ()));
ctor.set_native_arg_count(1);
let proto = unsafe { &mut *proto_ptr };
let sf = kernel.string_forge().as_ref();
let sh = kernel.shape_forge().as_ref();
@@ -1114,7 +1121,11 @@ fn bind_set(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
fn bind_map(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
let ctor_ptr = kernel.builtin_world().map_constructor.as_ptr() as *mut JsObject;
let ctor = unsafe { &mut *ctor_ptr };
let proto_ptr = kernel.builtin_world().map_proto.as_ptr() as *mut JsObject;
ctor.set_native_fn(Some(crate::builtins::map::map_constructor as *const ()));
ctor.set_native_arg_count(1);
let proto = unsafe { &mut *proto_ptr };
let sf = kernel.string_forge().as_ref();
let sh = kernel.shape_forge().as_ref();
@@ -1182,7 +1193,7 @@ fn bind_map(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
global.bump_generation();
}
fn bind_boolean(kernel: &Arc<OxideKernel>) {
fn bind_boolean(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
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;
@@ -1213,6 +1224,13 @@ fn bind_boolean(kernel: &Arc<OxideKernel>) {
crate::builtins::boolean::boolean_prototype_to_string,
0
);
let si_b = kernel.string_forge().intern("Boolean").0;
let b_shape = kernel.shape_forge().make_shape(global.shape_id(), si_b);
let b_val = JsValue::from_js_object(ctor_ptr);
global.set_shape_id(b_shape);
global.ensure_hash_props().push(Box::new(b_val));
global.bump_generation();
}
fn bind_function(kernel: &Arc<OxideKernel>) {
@@ -1242,7 +1260,7 @@ pub fn init_kernel_builtins(kernel: &Arc<OxideKernel>) {
bind_date(kernel, global);
bind_set(kernel, global);
bind_map(kernel, global);
bind_boolean(kernel);
bind_boolean(kernel, global);
bind_function(kernel);
}
+64 -33
View File
@@ -1,3 +1,4 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
use oxide_vm::builtins::boolean::{
@@ -7,12 +8,31 @@ use oxide_vm::vm::Vm;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
fn eval(vm: &mut Vm, source: &str) -> Result<JsValue, String> {
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)
}
fn str_val(vm: &Vm, val: JsValue) -> String {
vm.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default()
}
// -- direct native fn tests --
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 {
fn bool_ctor_new(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);
@@ -23,60 +43,71 @@ fn tbool_wrap(vm: &mut Vm, arg: JsValue) -> JsValue {
}
#[test]
fn tboolean_call_true() {
fn tboolean_call_true_false() {
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() {
fn tboolean_call_empty_string() {
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() {
fn tboolean_new_value_of() {
let mut vm = Vm::new();
let wrapper = tbool_wrap(&mut vm, JsValue::float(1.0));
vm.set_reg(0, wrapper);
let w = bool_ctor_new(&mut vm, JsValue::float(1.0));
vm.set_reg(0, w);
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);
let w2 = bool_ctor_new(&mut vm, JsValue::float(0.0));
vm.set_reg(0, w2);
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));
let w = bool_ctor_new(&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));
assert_eq!(str_val(&vm, r), "true");
let w2 = bool_ctor_new(&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");
assert_eq!(str_val(&vm, r2), "false");
}
// -- JS eval tests using new keyword --
#[test]
fn boolean_call_true() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Boolean(1)").unwrap();
assert!(r.as_bool());
}
#[test]
fn boolean_call_false() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Boolean(0)").unwrap();
assert!(!r.as_bool());
}
#[test]
fn boolean_new_value_of() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Boolean(1).valueOf()").unwrap();
assert!(r.as_bool());
}
#[test]
fn boolean_new_to_string() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Boolean(true).toString()").unwrap();
assert_eq!(str_val(&vm, r), "true");
let r = eval(&mut vm, "new Boolean(false).toString()").unwrap();
assert_eq!(str_val(&vm, r), "false");
}
+116 -23
View File
@@ -11,35 +11,128 @@ fn eval(vm: &mut Vm, source: &str) -> Result<oxide_types::value::JsValue, String
vm.run(&module)
}
fn str_val(vm: &Vm, val: oxide_types::value::JsValue) -> String {
vm.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default()
}
// -- static methods --
#[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());
let r = eval(&mut vm, "Date.now()").unwrap();
assert!(r.is_double() && r.as_double() > 0.0);
}
#[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);
let r = eval(&mut vm, "Date.parse('2020-01-15')").unwrap();
assert!(!r.as_double().is_nan());
}
#[test]
fn date_parse_invalid() {
let mut vm = Vm::new();
let r = eval(&mut vm, "Date.parse('nope')").unwrap();
assert!(r.as_double().is_nan());
}
// -- new Date() constructor via JS --
#[test]
fn date_new_multi_arg() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 15).getFullYear()").unwrap();
assert_eq!(r.as_double(), 2020.0);
}
#[test]
fn date_new_epoch_zero() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(0).getTime()").unwrap();
assert_eq!(r.as_double(), 0.0);
}
#[test]
fn date_new_get_month() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 5, 15).getMonth()").unwrap();
assert_eq!(r.as_double(), 5.0);
}
#[test]
fn date_new_get_date() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 1, 29).getDate()").unwrap();
assert_eq!(r.as_double(), 29.0);
}
#[test]
fn date_new_get_hours_min_sec() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 1, 12, 30, 45).getHours()").unwrap();
assert_eq!(r.as_double(), 12.0);
let r = eval(&mut vm, "new Date(2020, 0, 1, 12, 30, 45).getMinutes()").unwrap();
assert_eq!(r.as_double(), 30.0);
let r = eval(&mut vm, "new Date(2020, 0, 1, 12, 30, 45).getSeconds()").unwrap();
assert_eq!(r.as_double(), 45.0);
}
#[test]
fn date_set_time() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var d = new Date(0); d.setTime(1000); d.getTime()").unwrap();
assert_eq!(r.as_double(), 1000.0);
}
#[test]
fn date_set_full_year() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 1); d.setFullYear(2025); d.getFullYear()",
)
.unwrap();
assert_eq!(r.as_double(), 2025.0);
}
#[test]
fn date_value_of() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 1).valueOf()").unwrap();
assert!(r.as_double() > 0.0);
}
#[test]
fn date_to_iso_string() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 1, 0, 0, 0).toISOString()").unwrap();
let s = str_val(&vm, r);
assert!(s.starts_with("2020-01-01"), "got: {s}");
}
#[test]
fn date_to_json() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(2020, 0, 1).toJSON()").unwrap();
let s = str_val(&vm, r);
assert!(s.starts_with("2020-01-01"), "got: {s}");
}
#[test]
fn date_to_string_not_empty() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(0).toString()").unwrap();
assert!(r.is_string());
}
#[test]
fn date_to_utc_string_not_empty() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Date(0).toUTCString()").unwrap();
assert!(r.is_string());
}
+59 -22
View File
@@ -1,9 +1,29 @@
use oxide_compiler::compiler::Compiler;
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;
fn eval(vm: &mut Vm, source: &str) -> Result<JsValue, String> {
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)
}
fn str_val(vm: &Vm, val: JsValue) -> String {
vm.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default()
}
// -- direct native fn tests --
#[test]
fn tmap_constructor_returns_object() {
let mut vm = Vm::new();
@@ -20,37 +40,18 @@ fn tmap_set_get() {
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);
assert!(map_get(&mut vm, &[0, 99]).unwrap().is_undefined());
}
#[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() {
fn tmap_has_and_delete() {
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_has(&mut vm, &[0, 1]).unwrap().as_bool());
assert!(map_delete(&mut vm, &[0, 1]).unwrap().as_bool());
assert!(!map_has(&mut vm, &[0, 1]).unwrap().as_bool());
}
@@ -71,3 +72,39 @@ fn tmap_size_and_clear() {
map_clear(&mut vm, &[0]).unwrap();
assert_eq!(map_size(&mut vm, &[0]).unwrap().as_double(), 0.0);
}
// -- JS eval tests using new keyword (all in one eval per test) --
#[test]
fn map_new_set_get() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var m = new Map(); m.set('k', 'v'); m.get('k')").unwrap();
assert_eq!(str_val(&vm, r), "v");
}
#[test]
fn map_new_get_missing() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var m = new Map(); m.get('nope')").unwrap();
assert!(r.is_undefined());
}
#[test]
fn map_new_has() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var m = new Map(); m.set('a', 1); m.has('a')").unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "var m2 = new Map(); m2.has('b')").unwrap();
assert!(!r.as_bool());
}
#[test]
fn map_new_delete() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var m = new Map(); m.set('x', 10); m.delete('x'); m.has('x')",
)
.unwrap();
assert!(!r.as_bool());
}
+49 -11
View File
@@ -1,9 +1,22 @@
use oxide_compiler::compiler::Compiler;
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;
fn eval(vm: &mut Vm, source: &str) -> Result<JsValue, String> {
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)
}
// -- direct native fn tests --
#[test]
fn tset_constructor_returns_object() {
let mut vm = Vm::new();
@@ -17,7 +30,7 @@ fn tset_add_has() {
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();
set_add(&mut vm, &[0, 1]).unwrap();
assert!(set_has(&mut vm, &[0, 1]).unwrap().as_bool());
}
@@ -42,7 +55,7 @@ fn tset_delete_works() {
}
#[test]
fn tset_size_increases() {
fn tset_size_and_clear() {
let mut vm = Vm::new();
let s = new_set(&mut vm, &[0]).unwrap();
vm.set_reg(0, s);
@@ -52,15 +65,6 @@ fn tset_size_increases() {
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);
}
@@ -88,3 +92,37 @@ fn tset_signed_zero() {
set_add(&mut vm, &[0, 1]).unwrap();
assert_eq!(set_size(&mut vm, &[0]).unwrap().as_double(), 1.0);
}
// -- JS eval tests using new keyword --
#[test]
fn set_new_empty() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var s = new Set(); s").unwrap();
assert!(r.is_object());
}
#[test]
fn set_new_add_has() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var s = new Set(); s.add(1); s.has(1)").unwrap();
assert!(r.as_bool());
}
#[test]
fn set_new_has_missing() {
let mut vm = Vm::new();
let r = eval(&mut vm, "var s = new Set(); s.has(99)").unwrap();
assert!(!r.as_bool());
}
#[test]
fn set_new_delete() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var s = new Set(); s.add(1); s.add(2); s.delete(1); s.has(1)",
)
.unwrap();
assert!(!r.as_bool());
}