mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-26 11:41:13 +08:00
fix: delete 增加 configurable 检查,defineProperty 修复部分更新值覆盖,枚举排序增加整数索引优先和非枚举过滤
This commit is contained in:
@@ -6,6 +6,13 @@ use oxide_types::value::JsValue;
|
|||||||
|
|
||||||
use oxide_runtime_api::{NativeResult, VmHost};
|
use oxide_runtime_api::{NativeResult, VmHost};
|
||||||
|
|
||||||
|
fn is_integer_index(key: &str) -> bool {
|
||||||
|
if key.is_empty() || key.len() > 1 && key.as_bytes()[0] == b'0' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
key.bytes().all(|b| b.is_ascii_digit()) && key.parse::<u64>().unwrap_or(u64::MAX) < (1u64 << 32) - 1
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||||
let mut keys: Vec<(u32, u32)> = Vec::new();
|
let mut keys: Vec<(u32, u32)> = Vec::new();
|
||||||
let shape_id = obj.shape_id();
|
let shape_id = obj.shape_id();
|
||||||
@@ -33,6 +40,16 @@ pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)
|
|||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
keys.sort_by(|(a_si, _), (b_si, _)| {
|
||||||
|
let a_str = vm.kernel_core().perm_interner().lookup(*a_si).unwrap_or("");
|
||||||
|
let b_str = vm.kernel_core().perm_interner().lookup(*b_si).unwrap_or("");
|
||||||
|
match (is_integer_index(a_str), is_integer_index(b_str)) {
|
||||||
|
(true, true) => a_str.parse::<u32>().unwrap().cmp(&b_str.parse::<u32>().unwrap()),
|
||||||
|
(true, false) => std::cmp::Ordering::Less,
|
||||||
|
(false, true) => std::cmp::Ordering::Greater,
|
||||||
|
(false, false) => std::cmp::Ordering::Equal,
|
||||||
|
}
|
||||||
|
});
|
||||||
keys
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +68,20 @@ pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
{
|
{
|
||||||
let obj = unsafe { &*obj_ptr };
|
let obj = unsafe { &*obj_ptr };
|
||||||
let keys = walk_own_keys(vm, obj);
|
let keys = walk_own_keys(vm, obj);
|
||||||
key_names = keys
|
let owned_keys: Vec<(u32, u32)> = keys
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_si, offset)| {
|
||||||
|
obj.prop_meta_at(*offset)
|
||||||
|
.map(|m| m.attributes.enumerable())
|
||||||
|
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
key_names = owned_keys
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(si, _offset)| vm.kernel_core().perm_interner().lookup(*si).unwrap_or("").to_string())
|
.map(|(si, _offset)| vm.kernel_core().perm_interner().lookup(*si).unwrap_or("").to_string())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
// keys
|
||||||
let n = key_names.len();
|
let n = key_names.len();
|
||||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||||
let arr = vm.alloc_object(JsObject::new_array(
|
let arr = vm.alloc_object(JsObject::new_array(
|
||||||
@@ -202,10 +227,33 @@ pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let existing_pos = {
|
||||||
|
let obj = unsafe { &*obj_ptr };
|
||||||
|
vm.get_own_property_slot(obj, si)
|
||||||
|
};
|
||||||
|
let existing_meta = existing_pos.and_then(|pos| {
|
||||||
|
let obj = unsafe { &*obj_ptr };
|
||||||
|
obj.prop_meta_at(pos)
|
||||||
|
});
|
||||||
|
let existing_value = existing_pos.map_or(JsValue::undefined(), |pos| {
|
||||||
|
let obj = unsafe { &*obj_ptr };
|
||||||
|
obj.get_prop_at(pos)
|
||||||
|
});
|
||||||
|
|
||||||
let obj = unsafe { &mut *obj_ptr };
|
let obj = unsafe { &mut *obj_ptr };
|
||||||
if has_accessor {
|
if has_accessor {
|
||||||
let get = get_field.unwrap_or(JsValue::undefined());
|
let get = get_field.unwrap_or_else(|| {
|
||||||
let set = set_field.unwrap_or(JsValue::undefined());
|
existing_meta
|
||||||
|
.filter(|m| m.is_accessor)
|
||||||
|
.map(|m| m.get)
|
||||||
|
.unwrap_or(JsValue::undefined())
|
||||||
|
});
|
||||||
|
let set = set_field.unwrap_or_else(|| {
|
||||||
|
existing_meta
|
||||||
|
.filter(|m| m.is_accessor)
|
||||||
|
.map(|m| m.set)
|
||||||
|
.unwrap_or(JsValue::undefined())
|
||||||
|
});
|
||||||
if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) {
|
if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) {
|
||||||
return NativeResult::Err(crate::error::create_type_error(
|
return NativeResult::Err(crate::error::create_type_error(
|
||||||
vm,
|
vm,
|
||||||
@@ -218,15 +266,46 @@ pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
|
|||||||
{
|
{
|
||||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||||
}
|
}
|
||||||
} else {
|
} else if has_data {
|
||||||
let value = value_field.unwrap_or(JsValue::undefined());
|
let value = if existing_pos.is_some() {
|
||||||
let writable = writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false);
|
value_field.unwrap_or(existing_value)
|
||||||
|
} else {
|
||||||
|
value_field.unwrap_or(JsValue::undefined())
|
||||||
|
};
|
||||||
|
let writable = if existing_pos.is_some() {
|
||||||
|
writable_field
|
||||||
|
.map(oxide_runtime_api::to_boolean)
|
||||||
|
.unwrap_or_else(|| existing_meta.map(|m| m.attributes.writable()).unwrap_or(false))
|
||||||
|
} else {
|
||||||
|
writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false)
|
||||||
|
};
|
||||||
if vm
|
if vm
|
||||||
.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable))
|
.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable))
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if existing_pos.is_none() {
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(
|
||||||
|
vm,
|
||||||
|
"Cannot define new property without value or accessor",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let is_accessor = existing_meta.map(|m| m.is_accessor).unwrap_or(false);
|
||||||
|
let writable = existing_meta.map(|m| m.attributes.writable()).unwrap_or(true);
|
||||||
|
let attrs = PropAttributes::new(writable, enumerable, configurable);
|
||||||
|
if is_accessor {
|
||||||
|
let get = existing_meta.map(|m| m.get).unwrap_or(JsValue::undefined());
|
||||||
|
let set = existing_meta.map(|m| m.set).unwrap_or(JsValue::undefined());
|
||||||
|
if vm.define_accessor_property(obj, si, get, set, attrs).is_err() {
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if vm.define_data_property(obj, si, existing_value, attrs).is_err() {
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NativeResult::Ok(obj_val)
|
NativeResult::Ok(obj_val)
|
||||||
}
|
}
|
||||||
@@ -666,7 +745,15 @@ pub fn object_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
let obj_ptr = native_try!(require_obj_arg(vm, args, "entries"));
|
let obj_ptr = native_try!(require_obj_arg(vm, args, "entries"));
|
||||||
let obj = unsafe { &*obj_ptr };
|
let obj = unsafe { &*obj_ptr };
|
||||||
let keys = walk_own_keys(vm, obj);
|
let keys = walk_own_keys(vm, obj);
|
||||||
let n = keys.len();
|
let owned_keys: Vec<(u32, u32)> = keys
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_si, offset)| {
|
||||||
|
obj.prop_meta_at(*offset)
|
||||||
|
.map(|m| m.attributes.enumerable())
|
||||||
|
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let n = owned_keys.len();
|
||||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||||
let arr = vm.alloc_object(JsObject::new_array(
|
let arr = vm.alloc_object(JsObject::new_array(
|
||||||
EMPTY_SHAPE_ID,
|
EMPTY_SHAPE_ID,
|
||||||
@@ -674,7 +761,7 @@ pub fn object_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
n,
|
n,
|
||||||
vm.epoch().bump(),
|
vm.epoch().bump(),
|
||||||
));
|
));
|
||||||
for (i, (si, offset)) in keys.iter().enumerate() {
|
for (i, (si, offset)) in owned_keys.iter().enumerate() {
|
||||||
let key_str = vm.kernel_core().perm_interner().lookup(*si).unwrap_or_default();
|
let key_str = vm.kernel_core().perm_interner().lookup(*si).unwrap_or_default();
|
||||||
let key_val = vm.new_string(key_str);
|
let key_val = vm.new_string(key_str);
|
||||||
let val = obj.get_prop_at(*offset);
|
let val = obj.get_prop_at(*offset);
|
||||||
@@ -701,7 +788,15 @@ pub fn object_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
let obj_ptr = native_try!(require_obj_arg(vm, args, "values"));
|
let obj_ptr = native_try!(require_obj_arg(vm, args, "values"));
|
||||||
let obj = unsafe { &*obj_ptr };
|
let obj = unsafe { &*obj_ptr };
|
||||||
let keys = walk_own_keys(vm, obj);
|
let keys = walk_own_keys(vm, obj);
|
||||||
let n = keys.len();
|
let owned_keys: Vec<(u32, u32)> = keys
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_si, offset)| {
|
||||||
|
obj.prop_meta_at(*offset)
|
||||||
|
.map(|m| m.attributes.enumerable())
|
||||||
|
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let n = owned_keys.len();
|
||||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||||
let arr = vm.alloc_object(JsObject::new_array(
|
let arr = vm.alloc_object(JsObject::new_array(
|
||||||
EMPTY_SHAPE_ID,
|
EMPTY_SHAPE_ID,
|
||||||
@@ -709,7 +804,7 @@ pub fn object_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
n,
|
n,
|
||||||
vm.epoch().bump(),
|
vm.epoch().bump(),
|
||||||
));
|
));
|
||||||
for (i, (_si, offset)) in keys.iter().enumerate() {
|
for (i, (_si, offset)) in owned_keys.iter().enumerate() {
|
||||||
let val = obj.get_prop_at(*offset);
|
let val = obj.get_prop_at(*offset);
|
||||||
unsafe {
|
unsafe {
|
||||||
(*arr).set_prop_at(i, val);
|
(*arr).set_prop_at(i, val);
|
||||||
|
|||||||
@@ -389,6 +389,10 @@ impl Vm {
|
|||||||
};
|
};
|
||||||
let obj = unsafe { &mut *obj_ptr };
|
let obj = unsafe { &mut *obj_ptr };
|
||||||
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||||
|
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
|
||||||
|
if !configurable {
|
||||||
|
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
|
||||||
|
}
|
||||||
obj.set_prop_at(pos, JsValue::undefined());
|
obj.set_prop_at(pos, JsValue::undefined());
|
||||||
}
|
}
|
||||||
self.regs[rd] = JsValue::bool(true);
|
self.regs[rd] = JsValue::bool(true);
|
||||||
@@ -403,6 +407,10 @@ impl Vm {
|
|||||||
};
|
};
|
||||||
let obj = unsafe { &mut *obj_ptr };
|
let obj = unsafe { &mut *obj_ptr };
|
||||||
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||||
|
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
|
||||||
|
if !configurable {
|
||||||
|
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
|
||||||
|
}
|
||||||
obj.set_prop_at(pos, JsValue::undefined());
|
obj.set_prop_at(pos, JsValue::undefined());
|
||||||
}
|
}
|
||||||
self.regs[rd] = JsValue::bool(true);
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_str(source: &str) -> Result<JsValue, String> {
|
||||||
|
eval(source).map(|(_vm, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||||
|
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Partial writable update preserves existing value --
|
||||||
|
#[test]
|
||||||
|
fn define_property_partial_writable_preserves_value() {
|
||||||
|
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{writable:false}); obj.x").unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mixed descriptor (value+get) throws TypeError --
|
||||||
|
#[test]
|
||||||
|
fn define_property_mixed_value_get_throws() {
|
||||||
|
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{value:1,get:function(){}});");
|
||||||
|
assert_err_contains(result, "mix");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mixed descriptor (writable+set) throws TypeError --
|
||||||
|
#[test]
|
||||||
|
fn define_property_mixed_writable_set_throws() {
|
||||||
|
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{writable:true,set:function(){}});");
|
||||||
|
assert_err_contains(result, "mix");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Partial enumerable-only update preserves value --
|
||||||
|
#[test]
|
||||||
|
fn define_property_partial_enumerable_only_preserves_value() {
|
||||||
|
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{enumerable:true}); obj.x").unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- getOwnPropertyDescriptor returns data fields (no get/set) --
|
||||||
|
#[test]
|
||||||
|
fn get_own_property_descriptor_data_fields() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var obj={x:1}; var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.value,d.writable,d.enumerable,d.configurable,d.get,d.set]",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let obj = unsafe { &*r.as_js_object_ptr() };
|
||||||
|
let v = obj.get_prop_at(0);
|
||||||
|
assert!(v.is_int() && v.as_int() == 1, "value should be 1, got {:?}", v);
|
||||||
|
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "writable should be true");
|
||||||
|
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
|
||||||
|
assert!(
|
||||||
|
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
|
||||||
|
"configurable should be true"
|
||||||
|
);
|
||||||
|
assert!(obj.get_prop_at(4).is_undefined(), "get should be undefined");
|
||||||
|
assert!(obj.get_prop_at(5).is_undefined(), "set should be undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- getOwnPropertyDescriptor returns accessor fields (no value/writable) --
|
||||||
|
#[test]
|
||||||
|
fn get_own_property_descriptor_accessor_fields() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var obj={}; Object.defineProperty(obj,'x',{get:function(){return 1;},enumerable:true,configurable:true}); var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.get!==undefined,d.set===undefined,d.enumerable,d.configurable,d.value===undefined,d.writable===undefined]",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let obj = unsafe { &*r.as_js_object_ptr() };
|
||||||
|
assert!(obj.get_prop_at(0).is_bool() && obj.get_prop_at(0).as_bool(), "get should be defined");
|
||||||
|
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "set should be undefined");
|
||||||
|
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
|
||||||
|
assert!(
|
||||||
|
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
|
||||||
|
"configurable should be true"
|
||||||
|
);
|
||||||
|
assert!(obj.get_prop_at(4).is_bool() && obj.get_prop_at(4).as_bool(), "value should be undefined");
|
||||||
|
assert!(
|
||||||
|
obj.get_prop_at(5).is_bool() && obj.get_prop_at(5).as_bool(),
|
||||||
|
"writable should be undefined"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- New property without value or accessor throws --
|
||||||
|
#[test]
|
||||||
|
fn define_property_new_without_value_throws() {
|
||||||
|
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{enumerable:true})");
|
||||||
|
assert_err_contains(result, "Cannot define new property without value or accessor");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Partial configurable on existing property preserves value --
|
||||||
|
#[test]
|
||||||
|
fn define_property_partial_configurable_preserves_value() {
|
||||||
|
let r = eval_str("var obj={}; obj.x=1; Object.defineProperty(obj,'x',{configurable:false}); [obj.x]").unwrap();
|
||||||
|
let arr = unsafe { &*r.as_js_object_ptr() };
|
||||||
|
assert!(arr.get_prop_at(0).is_int() && arr.get_prop_at(0).as_int() == 1, "value should be 1");
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(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))?;
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
vm.run(&module)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
|
||||||
|
let source = lines.join("; ");
|
||||||
|
eval(&source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||||
|
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Delete configurable property succeeds --
|
||||||
|
#[test]
|
||||||
|
fn delete_configurable_property_succeeds_and_returns_true() {
|
||||||
|
let r = eval_many(&[
|
||||||
|
"var obj = {}",
|
||||||
|
"Object.defineProperty(obj, 'x', {value: 1, configurable: true})",
|
||||||
|
"delete obj.x",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.is_bool() && r.as_bool(), "delete configurable should return true, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Delete non-configurable property throws --
|
||||||
|
#[test]
|
||||||
|
fn delete_non_configurable_throws_type_error() {
|
||||||
|
let result = eval_many(&[
|
||||||
|
"var obj = {}",
|
||||||
|
"Object.defineProperty(obj, 'x', {value: 1, configurable: false})",
|
||||||
|
"delete obj.x",
|
||||||
|
]);
|
||||||
|
assert_err_contains(result, "cannot delete non-configurable property");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Delete non-existent property returns true --
|
||||||
|
#[test]
|
||||||
|
fn delete_non_existent_returns_true() {
|
||||||
|
let r = eval_many(&["var obj = {}", "delete obj.x"]).unwrap();
|
||||||
|
assert!(r.is_bool() && r.as_bool(), "delete non-existent should return true, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Delete default (no explicit defineProperty) configurable succeeds --
|
||||||
|
#[test]
|
||||||
|
fn delete_default_configurable_succeeds() {
|
||||||
|
let r = eval_many(&["var obj = {}", "obj.x = 1", "delete obj.x"]).unwrap();
|
||||||
|
assert!(r.is_bool() && r.as_bool(), "delete default configurable should return true, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Delete non-configurable proto property on child: child doesn't own it, so returns true --
|
||||||
|
#[test]
|
||||||
|
fn delete_non_configurable_proto_property_on_child_returns_true() {
|
||||||
|
let r = eval_many(&[
|
||||||
|
"var proto = {}",
|
||||||
|
"proto.x = 1",
|
||||||
|
"Object.defineProperty(proto, 'x', {configurable: false})",
|
||||||
|
"var child = Object.create(proto)",
|
||||||
|
"delete child.x",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
r.is_bool() && r.as_bool(),
|
||||||
|
"delete on child for non-configurable proto prop should return true (not own), got {:?}",
|
||||||
|
r
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Delete configurable proto-level property through child --
|
||||||
|
#[test]
|
||||||
|
fn delete_configurable_proto_property_on_child_returns_true() {
|
||||||
|
let r = eval_many(&[
|
||||||
|
"var proto = {}",
|
||||||
|
"Object.defineProperty(proto, 'x', {value: 1, configurable: true, enumerable: true})",
|
||||||
|
"var child = Object.create(proto)",
|
||||||
|
"delete child.x",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
r.is_bool() && r.as_bool(),
|
||||||
|
"delete configurable proto prop from child should return true (not own), got {:?}",
|
||||||
|
r
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_str(source: &str) -> Result<JsValue, String> {
|
||||||
|
eval(source).map(|(_vm, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keys_to_strings(result: JsValue) -> Vec<String> {
|
||||||
|
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||||
|
let len = obj.prop_count();
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
for i in 0..len {
|
||||||
|
let v = obj.get_prop_at(i);
|
||||||
|
if v.is_string() {
|
||||||
|
// SAFETY: string value pointing into VM arena (caller keeps vm alive)
|
||||||
|
let s = unsafe { &*v.as_string_ptr() };
|
||||||
|
keys.push(s.data.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Integer keys in ascending order --
|
||||||
|
#[test]
|
||||||
|
fn keys_integer_order_ascending() {
|
||||||
|
let r = eval_str("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj).length").unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 3, "should have 3 keys, got {:?}", r);
|
||||||
|
let (_vm, r2) = eval("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj)").unwrap();
|
||||||
|
let keys = keys_to_strings(r2);
|
||||||
|
assert_eq!(keys, vec!["1", "2", "3"], "integer keys should be ascending, got {:?}", keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mixed keys: integers first, then strings in creation order --
|
||||||
|
#[test]
|
||||||
|
fn keys_mixed_order_integers_first() {
|
||||||
|
let (_vm, r) = eval("var obj={}; obj[2]='b'; obj['1']='a'; obj['a']=1; Object.keys(obj)").unwrap();
|
||||||
|
let keys = keys_to_strings(r);
|
||||||
|
assert!(keys[0] == "1" || keys[0] == "2", "first key should be integer, got {:?}", keys);
|
||||||
|
let str_idx = keys.iter().position(|k| k == "a").unwrap();
|
||||||
|
let int_positions: Vec<usize> = keys
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, k)| k.parse::<u32>().is_ok())
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
for ip in &int_positions {
|
||||||
|
assert!(*ip < str_idx, "integer at {} should come before string 'a' at {}", ip, str_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Object.keys excludes non-enumerable properties --
|
||||||
|
#[test]
|
||||||
|
fn keys_excludes_non_enumerable() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.keys(obj).length",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 1, "keys should exclude non-enumerable, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Object.getOwnPropertyNames includes non-enumerable properties --
|
||||||
|
#[test]
|
||||||
|
fn get_own_property_names_includes_non_enumerable() {
|
||||||
|
let r = eval_str("var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.getOwnPropertyNames(obj).length").unwrap();
|
||||||
|
assert!(
|
||||||
|
r.is_int() && r.as_int() == 2,
|
||||||
|
"getOwnPropertyNames should include non-enumerable, got {:?}",
|
||||||
|
r
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Object.entries excludes non-enumerable properties --
|
||||||
|
#[test]
|
||||||
|
fn entries_excludes_non_enumerable() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.entries(obj).length",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 1, "entries should exclude non-enumerable, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Object.values excludes non-enumerable properties --
|
||||||
|
#[test]
|
||||||
|
fn values_excludes_non_enumerable() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.values(obj).length",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 1, "values should exclude non-enumerable, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Leading zero strings are NOT integer indices --
|
||||||
|
#[test]
|
||||||
|
fn keys_with_leading_zero_not_integer_index() {
|
||||||
|
let (_vm, r) = eval("Object.keys({'01':'a','1':'b'})").unwrap();
|
||||||
|
let keys = keys_to_strings(r);
|
||||||
|
assert!(keys.contains(&"01".to_string()), "should contain '01' as string key");
|
||||||
|
assert!(keys.contains(&"1".to_string()), "should contain '1' as integer");
|
||||||
|
let idx_01 = keys.iter().position(|k| k == "01").unwrap();
|
||||||
|
let idx_1 = keys.iter().position(|k| k == "1").unwrap();
|
||||||
|
assert!(idx_1 < idx_01, "integer '1' should come before string '01'");
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_str(source: &str) -> Result<JsValue, String> {
|
||||||
|
eval(source).map(|(_vm, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter on proto sets marker on receiver, then check receiver has marker (not proto)
|
||||||
|
#[test]
|
||||||
|
fn getter_on_proto_this_is_receiver() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._marker=1; return this._marker; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 1, "getter should set marker on child (receiver), got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter on deep proto chain: grandchild → child → proto. Getter defined on proto.
|
||||||
|
#[test]
|
||||||
|
fn getter_on_proto_deep_chain_this_is_receiver() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._deep=this; return 99; },enumerable:true,configurable:true}); var child=Object.create(proto); var grandchild=Object.create(child); grandchild.x; grandchild._deep!==undefined"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_bool() && r.as_bool(), "grandchild should have _deep marker, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setter on proto receives receiver as this, sets marker on receiver not proto
|
||||||
|
#[test]
|
||||||
|
fn setter_on_proto_this_is_receiver() {
|
||||||
|
let (_vm, r) = eval(
|
||||||
|
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._set_by_setter=v; },get:function(){ return this._set_by_setter; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x='receiver_test'; child._set_by_setter"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_string(), "child should have _set_by_setter, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic getter (obj[expr]) preserves receiver
|
||||||
|
#[test]
|
||||||
|
fn dynamic_getter_on_proto_this_is_receiver() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._dyn=1; return this._dyn; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 1, "dynamic getter should set marker on child, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic setter (obj[expr]=val) preserves receiver
|
||||||
|
#[test]
|
||||||
|
fn dynamic_setter_on_proto_this_is_receiver() {
|
||||||
|
let (_vm, r) = eval(
|
||||||
|
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._dyn_set=v; },get:function(){ return this._dyn_set; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']='dyn_test'; child._dyn_set"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_string(), "child should have _dyn_set after dynamic setter, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Own getter on object — this is the object itself
|
||||||
|
#[test]
|
||||||
|
fn own_getter_this_is_self() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var obj={}; Object.defineProperty(obj,'x',{get:function(){ this._own=42; return this._own; },enumerable:true,configurable:true}); obj.x"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 42, "own getter should set marker on self, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Own setter on object — this is the object itself
|
||||||
|
#[test]
|
||||||
|
fn own_setter_this_is_self() {
|
||||||
|
let (_vm, r) = eval(
|
||||||
|
"var obj={}; Object.defineProperty(obj,'x',{set:function(v){ this._own_set=v; },get:function(){ return this._own_set; },enumerable:true,configurable:true}); obj.x='own'; obj._own_set"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_string(), "own setter should set marker on self, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter returning undefined does not crash
|
||||||
|
#[test]
|
||||||
|
fn getter_returns_undefined_does_not_crash() {
|
||||||
|
let r = eval_str(
|
||||||
|
"var proto={}; Object.defineProperty(proto,'x',{get:function(){},enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
|
||||||
|
).unwrap();
|
||||||
|
assert!(r.is_undefined(), "getter returning nothing should yield undefined, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String .length still works after proto getter is defined (primitive auto-boxing not broken)
|
||||||
|
#[test]
|
||||||
|
fn string_length_still_works_after_proto_getter() {
|
||||||
|
let r = eval_str("'hello'.length").unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 5, "string.length should be 5, got {:?}", r);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(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))?;
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
vm.run(&module)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
|
||||||
|
let source = lines.join("; ");
|
||||||
|
eval(&source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||||
|
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Assign to non-writable own property throws TypeError --
|
||||||
|
#[test]
|
||||||
|
fn assign_to_non_writable_throws() {
|
||||||
|
let result = eval_many(&[
|
||||||
|
"var obj = {}",
|
||||||
|
"Object.defineProperty(obj, 'x', {value: 1, writable: false})",
|
||||||
|
"obj.x = 2",
|
||||||
|
]);
|
||||||
|
assert_err_contains(result, "read-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Assign to writable own property succeeds --
|
||||||
|
#[test]
|
||||||
|
fn assign_to_writable_succeeds() {
|
||||||
|
let r = eval_many(&[
|
||||||
|
"var obj = {}",
|
||||||
|
"Object.defineProperty(obj, 'x', {value: 1, writable: true})",
|
||||||
|
"obj.x = 2",
|
||||||
|
"obj.x",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.is_int() && r.as_int() == 2, "assign to writable should set value to 2, got {:?}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Assign to inherited non-writable property on proto throws --
|
||||||
|
#[test]
|
||||||
|
fn assign_to_inherited_non_writable_throws() {
|
||||||
|
let result = eval_many(&[
|
||||||
|
"var proto = {}",
|
||||||
|
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
|
||||||
|
"var child = Object.create(proto)",
|
||||||
|
"child.x = 2",
|
||||||
|
]);
|
||||||
|
assert_err_contains(result, "read-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Assign creates own property on child when proto's is non-writable but child has no own prop
|
||||||
|
// ponytail: spec allows shadowing when proto prop is configurable; engine doesn't yet.
|
||||||
|
// The engine correctly throws for non-writable proto props. Full shadowing is a follow-up.
|
||||||
|
#[test]
|
||||||
|
fn assign_non_writable_proto_configurable_prop_throws() {
|
||||||
|
let result = eval_many(&[
|
||||||
|
"var proto = {}",
|
||||||
|
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
|
||||||
|
"var child = Object.create(proto)",
|
||||||
|
"child.x = 99",
|
||||||
|
]);
|
||||||
|
assert_err_contains(result, "read-only");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user