feat: 新增 to_integer_or_infinity/to_length/same_value_zero 三个 ES 抽象操作辅助函数,修复 objects_keys/create/defineProperty/assign/getOwnPropertyNames 五个方法的 TypeError 处理

This commit is contained in:
爱特每羊队
2026-06-27 14:42:16 +08:00
parent 78ecac5911
commit 5f8123f1f3
6 changed files with 270 additions and 36 deletions
Generated
+3
View File
@@ -1059,10 +1059,13 @@ dependencies = [
"fast-float", "fast-float",
"indexmap", "indexmap",
"memchr", "memchr",
"oxide_compiler",
"oxide_kernel", "oxide_kernel",
"oxide_log", "oxide_log",
"oxide_parser",
"oxide_runtime_api", "oxide_runtime_api",
"oxide_types", "oxide_types",
"oxide_vm",
"regex", "regex",
"ryu", "ryu",
"serde_json", "serde_json",
+5
View File
@@ -19,3 +19,8 @@ serde_json.workspace = true
chrono.workspace = true chrono.workspace = true
unicode-normalization.workspace = true unicode-normalization.workspace = true
indexmap.workspace = true indexmap.workspace = true
[dev-dependencies]
oxide_vm = { path = "../oxide_vm" }
oxide_compiler = { path = "../oxide_compiler" }
oxide_parser = { path = "../oxide_parser" }
+47 -36
View File
@@ -42,17 +42,10 @@ pub fn object_constructor<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
} }
pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 { let obj_ptr = match require_obj_arg(vm, args, "keys") {
return NativeResult::Err(JsValue::undefined()); Ok(ptr) => ptr,
} Err(err) => return NativeResult::Err(err),
let val = vm.reg(args[1]); };
if !val.is_object() {
return NativeResult::Err(JsValue::undefined());
}
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
return NativeResult::Err(JsValue::undefined());
}
let key_names: Vec<String>; let key_names: Vec<String>;
{ {
@@ -86,11 +79,18 @@ pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
pub fn object_create<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn object_create<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 { if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined()); return NativeResult::Err(crate::error::create_type_error(vm, "Object.create: at least 1 argument required"));
} }
let proto_val = vm.reg(args[1]); let proto_val = vm.reg(args[1]);
if !proto_val.is_null() && !proto_val.is_object() { if proto_val.is_null() {
return NativeResult::Ok(JsValue::undefined()); let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
return NativeResult::Ok(JsValue::from_js_object(obj));
}
if !proto_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.create: prototype must be an object or null",
));
} }
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val));
NativeResult::Ok(JsValue::from_js_object(obj)) NativeResult::Ok(JsValue::from_js_object(obj))
@@ -109,24 +109,32 @@ pub fn object_assign<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if target_ptr.is_null() { if target_ptr.is_null() {
return NativeResult::Ok(target_val); return NativeResult::Ok(target_val);
} }
{
let target = unsafe { &mut *target_ptr }; let mut all_assignments: Vec<(u32, JsValue, JsValue)> = Vec::new();
for &arg_reg in args.iter().skip(2) { for &arg_reg in args.iter().skip(2) {
let source_val = vm.reg(arg_reg); let source_val = vm.reg(arg_reg);
if !source_val.is_object() { if !source_val.is_object() {
continue; continue;
}
let source_ptr = source_val.as_js_object_ptr();
if source_ptr.is_null() {
continue;
}
let source = unsafe { &*source_ptr };
let source_keys = walk_own_keys(vm, source);
for (_si, offset) in source_keys {
let prop_val = source.get_prop_at(offset);
target.push_prop(prop_val);
}
} }
let source_ptr = source_val.as_js_object_ptr();
if source_ptr.is_null() {
continue;
}
let source_keys: Vec<(u32, u32)> = {
let source = unsafe { &*source_ptr };
walk_own_keys(vm, source)
};
for (si, offset) in source_keys {
let source = unsafe { &*source_ptr };
let val = source.get_prop_at(offset);
all_assignments.push((si, source_val, val));
}
}
let target = unsafe { &mut *target_ptr };
for (si, source_val, val) in all_assignments {
let promoted = vm.promote_if_needed_for_write_ptr(target_ptr, val);
let _ = vm.ordinary_set(target, si, promoted, source_val);
} }
NativeResult::Ok(target_val) NativeResult::Ok(target_val)
} }
@@ -139,23 +147,26 @@ pub fn object_is<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 4 { if args.len() < 4 {
return NativeResult::Err(JsValue::undefined()); return NativeResult::Err(crate::error::create_type_error(
vm,
"Object.defineProperty: expected at least 3 arguments",
));
} }
let obj_val = vm.reg(args[1]); let obj_val = vm.reg(args[1]);
if !obj_val.is_object() { if !obj_val.is_object() {
return NativeResult::Err(JsValue::undefined()); return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object"));
} }
let obj_ptr = obj_val.as_js_object_ptr(); let obj_ptr = obj_val.as_js_object_ptr();
if obj_ptr.is_null() { if obj_ptr.is_null() {
return NativeResult::Err(JsValue::undefined()); return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object"));
} }
let desc_val = vm.reg(args[3]); let desc_val = vm.reg(args[3]);
if !desc_val.is_object() { if !desc_val.is_object() {
return NativeResult::Err(JsValue::undefined()); return NativeResult::Err(crate::error::create_type_error(vm, "Property description must be an object"));
} }
let desc_ptr = desc_val.as_js_object_ptr(); let desc_ptr = desc_val.as_js_object_ptr();
if desc_ptr.is_null() { if desc_ptr.is_null() {
return NativeResult::Err(JsValue::undefined()); return NativeResult::Err(crate::error::create_type_error(vm, "Property description must be an object"));
} }
let prop_name_str = oxide_runtime_api::to_string(vm.reg(args[2])); let prop_name_str = oxide_runtime_api::to_string(vm.reg(args[2]));
let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0; let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0;
@@ -0,0 +1,101 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, 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))?;
let mut vm = Vm::new();
let result = vm.run(&module)?;
Ok((vm, result))
}
#[test]
fn object_keys_no_args_type_error() {
assert!(eval("Object.keys()").is_err(), "Object.keys() should throw TypeError");
}
#[test]
fn object_keys_non_object_type_error() {
assert!(eval("Object.keys(42)").is_err(), "Object.keys(42) should throw TypeError");
}
#[test]
fn object_create_null_proto() {
let (_vm, result) = eval("var o = Object.create(null); o").unwrap();
assert!(result.is_object());
let obj_ptr = result.as_js_object_ptr();
assert!(!obj_ptr.is_null());
let obj = unsafe { &*obj_ptr };
let proto = obj.proto();
assert!(proto.is_null(), "Object.create(null) should set prototype to null");
}
#[test]
fn object_create_number_proto_type_error() {
assert!(eval("Object.create(42)").is_err(), "Object.create(42) should throw TypeError");
}
#[test]
fn object_create_no_args_type_error() {
assert!(eval("Object.create()").is_err(), "Object.create() should throw TypeError");
}
#[test]
fn object_get_own_property_names_no_args_type_error() {
assert!(
eval("Object.getOwnPropertyNames()").is_err(),
"Object.getOwnPropertyNames() should throw TypeError"
);
}
#[test]
fn object_get_own_property_names_non_object_type_error() {
assert!(
eval("Object.getOwnPropertyNames(42)").is_err(),
"Object.getOwnPropertyNames(42) should throw TypeError"
);
}
#[test]
fn object_keys_valid_object() {
let (_vm, result) = eval("var o = {a:1, b:2}; Object.keys(o).join(',')").unwrap();
assert!(result.is_string(), "Object.keys should return an array");
}
#[test]
fn test_define_property_data_and_accessor_conflict() {
assert!(
eval("Object.defineProperty({}, 'x', {value: 1, get: function() {}})").is_err(),
"data+accessor conflict should throw TypeError"
);
}
#[test]
fn test_define_property_non_object_target() {
assert!(
eval("Object.defineProperty(null, 'x', {value: 1})").is_err(),
"non-object target should throw TypeError"
);
}
#[test]
fn test_define_property_missing_descriptor() {
assert!(
eval("Object.defineProperty({}, 'x')").is_err(),
"missing descriptor should throw TypeError"
);
}
#[test]
fn test_assign_skips_null_source() {
let (_vm, result) = eval("var t = {a:1}; Object.assign(t, null); t.a").unwrap();
assert_eq!(result.as_int(), 1, "Object.assign should skip null sources");
}
#[test]
fn test_assign_merges_two_sources() {
let (_vm, result) = eval("var t = {a:1}; Object.assign(t, {b:2}); t.b").unwrap();
assert!(result.is_int() || result.is_double(), "Object.assign should copy from source");
}
@@ -0,0 +1,82 @@
use oxide_runtime_api::{same_value_zero, to_integer_or_infinity, to_length};
use oxide_types::value::JsValue;
#[test]
fn test_to_integer_or_infinity_nan() {
assert_eq!(to_integer_or_infinity(JsValue::float(f64::NAN)), 0.0);
}
#[test]
fn test_to_integer_or_infinity_zero() {
assert_eq!(to_integer_or_infinity(JsValue::float(0.0)), 0.0);
assert_eq!(to_integer_or_infinity(JsValue::float(-0.0)), 0.0);
}
#[test]
fn test_to_integer_or_infinity_infinity() {
assert_eq!(to_integer_or_infinity(JsValue::float(f64::INFINITY)), f64::INFINITY);
assert_eq!(to_integer_or_infinity(JsValue::float(f64::NEG_INFINITY)), f64::NEG_INFINITY);
}
#[test]
fn test_to_integer_or_infinity_truncation() {
assert_eq!(to_integer_or_infinity(JsValue::float(3.7)), 3.0);
assert_eq!(to_integer_or_infinity(JsValue::float(-3.7)), -3.0);
}
#[test]
fn test_to_integer_or_infinity_int() {
assert_eq!(to_integer_or_infinity(JsValue::int(42)), 42.0);
}
#[test]
fn test_to_length_nan() {
assert_eq!(to_length(JsValue::float(f64::NAN)), 0);
}
#[test]
fn test_to_length_negative() {
assert_eq!(to_length(JsValue::float(-1.0)), 0);
assert_eq!(to_length(JsValue::int(-42)), 0);
}
#[test]
fn test_to_length_positive() {
assert_eq!(to_length(JsValue::float(100.0)), 100);
assert_eq!(to_length(JsValue::int(999)), 999);
}
#[test]
fn test_to_length_overflow_clamp() {
assert_eq!(to_length(JsValue::float(1e100)), 9_007_199_254_740_991);
}
#[test]
fn test_same_value_zero_nan() {
assert!(same_value_zero(JsValue::float(f64::NAN), JsValue::float(f64::NAN)));
}
#[test]
fn test_same_value_zero_positive_and_negative_zero() {
assert!(same_value_zero(JsValue::float(0.0), JsValue::float(-0.0)));
assert!(same_value_zero(JsValue::float(-0.0), JsValue::float(0.0)));
}
#[test]
fn test_same_value_zero_equal_numbers() {
assert!(same_value_zero(JsValue::float(42.0), JsValue::float(42.0)));
assert!(same_value_zero(JsValue::int(42), JsValue::int(42)));
assert!(same_value_zero(JsValue::int(42), JsValue::float(42.0)));
}
#[test]
fn test_same_value_zero_different_numbers() {
assert!(!same_value_zero(JsValue::float(1.0), JsValue::float(2.0)));
}
#[test]
fn test_same_value_zero_null_undefined() {
assert!(same_value_zero(JsValue::null(), JsValue::null()));
assert!(same_value_zero(JsValue::undefined(), JsValue::undefined()));
assert!(!same_value_zero(JsValue::null(), JsValue::undefined()));
}
+32
View File
@@ -442,6 +442,38 @@ pub fn same_value(lhs: JsValue, rhs: JsValue) -> bool {
false false
} }
/// ToIntegerOrInfinity(argument) — ECMA-262 §7.1.4.
pub fn to_integer_or_infinity(val: JsValue) -> f64 {
let n = to_number(val);
if n.is_nan() || n == 0.0 {
0.0
} else if n.is_infinite() {
n
} else {
n.trunc()
}
}
/// ToLength(argument) — ECMA-262 §7.1.20.
pub fn to_length(val: JsValue) -> u64 {
let n = to_number(val);
let len = if n.is_nan() || n <= 0.0 { 0.0 } else { n.min(9_007_199_254_740_991.0) };
len.trunc() as u64
}
/// SameValueZero(x, y) — ECMA-262 §7.2.11.
pub fn same_value_zero(lhs: JsValue, rhs: JsValue) -> bool {
if (lhs.is_double() || lhs.is_int()) && (rhs.is_double() || rhs.is_int()) {
let a = to_f64(lhs);
let b = to_f64(rhs);
if a.is_nan() && b.is_nan() {
return true;
}
return a == b;
}
same_value(lhs, rhs)
}
pub fn strict_equality(lhs: JsValue, rhs: JsValue) -> bool { pub fn strict_equality(lhs: JsValue, rhs: JsValue) -> bool {
if lhs.is_double() && rhs.is_double() { if lhs.is_double() && rhs.is_double() {
return strict_double_eq(lhs.as_double(), rhs.as_double()); return strict_double_eq(lhs.as_double(), rhs.as_double());