feat: 对象字面量 spread({...obj} 展开可枚举自有属性)
This commit is contained in:
@@ -14,7 +14,8 @@ fn is_integer_index(key: &str) -> bool {
|
||||
}
|
||||
|
||||
/// 收集对象全部自身属性(shape 链),按规范顺序排列:整数索引在前升序,其余保持插入序。
|
||||
pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||
/// 数组元素区(整数下标)不在此列,调用方需另行枚举。
|
||||
pub fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||
let mut keys: Vec<(u32, u32)> = Vec::new();
|
||||
let shape_id = obj.shape_id();
|
||||
let mut pos: u32 = 0;
|
||||
|
||||
@@ -65,6 +65,7 @@ define_opcodes! {
|
||||
MOV = 0x0C => "MOV", // rd=dst, a=src(寄存器复制,区间拆分搬值)
|
||||
SPILL = 0x0D => "SPILL", // rd=src, ext=[slot u16](寄存器值写 VM spill 栈)
|
||||
UNSPILL = 0x0E => "UNSPILL", // rd=dst, ext=[slot u16](从 VM spill 栈恢复寄存器)
|
||||
SPREAD_OBJECT = 0x0F => "SPREAD_OBJECT", // rd=目标对象, a=源(对象字面量 ... 展开,原地写目标)
|
||||
|
||||
// ── 比较 (0x10-0x1F) ──
|
||||
EQ = 0x10 => "EQ",
|
||||
@@ -331,12 +332,14 @@ mod tests {
|
||||
assert_eq!(OpCode::MOV as u8, 0x0C);
|
||||
assert_eq!(OpCode::SPILL as u8, 0x0D);
|
||||
assert_eq!(OpCode::UNSPILL as u8, 0x0E);
|
||||
assert_eq!(OpCode::SPREAD_OBJECT as u8, 0x0F);
|
||||
assert_eq!(OpCode::CREATE_ARGUMENTS as u8, 0x63);
|
||||
assert_eq!(OpCode::ADD.to_string(), "ADD");
|
||||
assert_eq!(OpCode::COMPOUND_MEMBER_EXP.to_string(), "COMPOUND_MEMBER_EXP");
|
||||
assert_eq!(OpCode::MOV.to_string(), "MOV");
|
||||
assert_eq!(OpCode::SPILL.to_string(), "SPILL");
|
||||
assert_eq!(OpCode::UNSPILL.to_string(), "UNSPILL");
|
||||
assert_eq!(OpCode::SPREAD_OBJECT.to_string(), "SPREAD_OBJECT");
|
||||
|
||||
assert!(OpCode::try_from(0x1B).is_err());
|
||||
assert!(OpCode::try_from(0xFF).is_err());
|
||||
|
||||
@@ -36,6 +36,9 @@ pub(super) fn hash_object_property_kind(
|
||||
hash_property_key(&p.key, h, include_binding_names);
|
||||
expression::hash_expression(&p.value, h, include_binding_names);
|
||||
}
|
||||
ObjectPropertyKind::SpreadProperty(_) => {}
|
||||
ObjectPropertyKind::SpreadProperty(spread) => {
|
||||
// spread 的源表达式计入哈希:`{...a}` 与 `{...b}` 结构不同,避免缓存碰撞。
|
||||
expression::hash_expression(&spread.argument, h, include_binding_names);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,6 +167,19 @@ fn structural_hash_distinguishes_optional_chain_shape() {
|
||||
assert_ne!(optional_c, plain_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_spread_source_shapes_differ() {
|
||||
// 编译模块哈希含绑定名:{...a} 与 {...b} 展开源不同,缓存键必须不同。
|
||||
let spread_a = parse_to_compiled_hash("var o = { ...a };");
|
||||
let spread_b = parse_to_compiled_hash("var o = { ...b };");
|
||||
assert_ne!(spread_a, spread_b, "different spread sources must produce different hashes");
|
||||
|
||||
// 结构哈希区分 spread 与非 spread 的对象字面量。
|
||||
let plain = parse_to_hash("var o = { a: 1 };");
|
||||
let spread_after = parse_to_hash("var o = { a: 1, ...x };");
|
||||
assert_ne!(plain, spread_after, "spread vs non-spread object must hash differently");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiled_module_rejects_optional_chain_assignment_target() {
|
||||
let allocator = Allocator::default();
|
||||
|
||||
@@ -14,69 +14,77 @@ impl Emitter {
|
||||
let obj_reg = ctx.alloc_reg();
|
||||
ctx.inst(Inst::new(OpCode::NEW_OBJECT, Operand::Reg(obj_reg), Operand::None, Operand::None));
|
||||
for prop in &obj.properties {
|
||||
let ObjectPropertyKind::ObjectProperty(p) = prop else {
|
||||
return Err("spread properties not yet supported".into());
|
||||
let ObjectPropertyKind::SpreadProperty(spread) = prop else {
|
||||
self.emit_object_property(obj_reg, prop, ctx)?;
|
||||
continue;
|
||||
};
|
||||
let computed = p.computed;
|
||||
let prop_name = if computed {
|
||||
"<computed>".to_string()
|
||||
} else {
|
||||
self.class_property_name(&p.key)?
|
||||
};
|
||||
match p.kind {
|
||||
PropertyKind::Get | PropertyKind::Set => {
|
||||
if computed {
|
||||
return Err("computed object accessors not yet supported".into());
|
||||
}
|
||||
let accessor_reg = self.emit_expression(&p.value, ctx)?;
|
||||
if let Some(sub_mod) = ctx.nested.last_mut() {
|
||||
sub_mod.function_name = Some(prop_name.to_string());
|
||||
}
|
||||
let undef_reg = self.emit_undefined(ctx);
|
||||
let (get_reg, set_reg) = if p.kind == PropertyKind::Get {
|
||||
(accessor_reg, undef_reg)
|
||||
} else {
|
||||
(undef_reg, accessor_reg)
|
||||
};
|
||||
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
|
||||
ctx.inst(Inst::define_accessor(
|
||||
Operand::Reg(obj_reg),
|
||||
Operand::Reg(get_reg),
|
||||
Operand::Reg(set_reg),
|
||||
idx as u32,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
let key_reg = if computed {
|
||||
self.emit_expression(p.key.to_expression(), ctx)?
|
||||
} else {
|
||||
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
|
||||
let reg = ctx.alloc_reg();
|
||||
ctx.inst(Inst::load_const(Operand::Reg(reg), idx));
|
||||
reg
|
||||
};
|
||||
let val_reg = self.emit_expression(&p.value, ctx)?;
|
||||
if crate::is_anonymous_function_definition(&p.value) {
|
||||
if let Some(sub_mod) = ctx.nested.last_mut() {
|
||||
sub_mod.function_name = Some(prop_name.to_string());
|
||||
}
|
||||
}
|
||||
let op = if computed { OpCode::SET_PROP_DYNAMIC } else { OpCode::SET_PROP };
|
||||
let operands = if computed {
|
||||
(Operand::Reg(key_reg), Operand::Reg(val_reg))
|
||||
} else {
|
||||
(Operand::Reg(val_reg), Operand::Reg(key_reg))
|
||||
};
|
||||
ctx.inst(Inst::new(
|
||||
op,
|
||||
Operand::Reg(obj_reg),
|
||||
operands.0,
|
||||
operands.1,
|
||||
));
|
||||
}
|
||||
}
|
||||
// spread 展开:把源表达式的可枚举自有属性写入目标对象(原地改)。
|
||||
// 顺序语义:{...b, a:1} 在 spread 之后定义 a,后者覆盖前者(从左到右求值)。
|
||||
let src_reg = self.emit_expression(&spread.argument, ctx)?;
|
||||
ctx.inst(Inst::spread_object(Operand::Reg(obj_reg), Operand::Reg(src_reg)));
|
||||
}
|
||||
Ok(obj_reg)
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_object_property(
|
||||
&self, obj_reg: u32, prop: &oxide_parser::ObjectPropertyKind, ctx: &mut CompileCtx,
|
||||
) -> Result<(), String> {
|
||||
let ObjectPropertyKind::ObjectProperty(p) = prop else {
|
||||
return Err("unsupported object property kind".into());
|
||||
};
|
||||
let computed = p.computed;
|
||||
let prop_name = if computed {
|
||||
"<computed>".to_string()
|
||||
} else {
|
||||
self.class_property_name(&p.key)?
|
||||
};
|
||||
match p.kind {
|
||||
PropertyKind::Get | PropertyKind::Set => {
|
||||
if computed {
|
||||
return Err("computed object accessors not yet supported".into());
|
||||
}
|
||||
let accessor_reg = self.emit_expression(&p.value, ctx)?;
|
||||
if let Some(sub_mod) = ctx.nested.last_mut() {
|
||||
sub_mod.function_name = Some(prop_name.to_string());
|
||||
}
|
||||
let undef_reg = self.emit_undefined(ctx);
|
||||
let (get_reg, set_reg) = if p.kind == PropertyKind::Get {
|
||||
(accessor_reg, undef_reg)
|
||||
} else {
|
||||
(undef_reg, accessor_reg)
|
||||
};
|
||||
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
|
||||
ctx.inst(Inst::define_accessor(
|
||||
Operand::Reg(obj_reg),
|
||||
Operand::Reg(get_reg),
|
||||
Operand::Reg(set_reg),
|
||||
idx as u32,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
let key_reg = if computed {
|
||||
self.emit_expression(p.key.to_expression(), ctx)?
|
||||
} else {
|
||||
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
|
||||
let reg = ctx.alloc_reg();
|
||||
ctx.inst(Inst::load_const(Operand::Reg(reg), idx));
|
||||
reg
|
||||
};
|
||||
let val_reg = self.emit_expression(&p.value, ctx)?;
|
||||
if crate::is_anonymous_function_definition(&p.value) {
|
||||
if let Some(sub_mod) = ctx.nested.last_mut() {
|
||||
sub_mod.function_name = Some(prop_name.to_string());
|
||||
}
|
||||
}
|
||||
let op = if computed { OpCode::SET_PROP_DYNAMIC } else { OpCode::SET_PROP };
|
||||
let operands = if computed {
|
||||
(Operand::Reg(key_reg), Operand::Reg(val_reg))
|
||||
} else {
|
||||
(Operand::Reg(val_reg), Operand::Reg(key_reg))
|
||||
};
|
||||
ctx.inst(Inst::new(op, Operand::Reg(obj_reg), operands.0, operands.1));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +266,11 @@ impl Inst {
|
||||
OpCode::FOR_IN_NEXT | OpCode::FOR_IN_DONE | OpCode::FOR_OF_NEXT | OpCode::FOR_OF_DONE => {}
|
||||
// REST_OBJECT:读 a(ext 是 excluded_idx 常量)
|
||||
OpCode::REST_OBJECT => push_operand(&mut uses, &self.a),
|
||||
// SPREAD_OBJECT:原地改目标对象,读目标(rd)与源(a)
|
||||
OpCode::SPREAD_OBJECT => {
|
||||
push_operand(&mut uses, &self.rd);
|
||||
push_operand(&mut uses, &self.a);
|
||||
}
|
||||
// TEMPLATE_STR:解析 ext,跳过 ext[0],后续 seg>>31==1 则低 8 位是 expr_reg
|
||||
OpCode::TEMPLATE_STR => {
|
||||
for seg in self.ext.iter().skip(1) {
|
||||
@@ -567,6 +572,16 @@ mod tests {
|
||||
assert_eq!(inst.use_regs().as_slice(), &[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spread_object_def_rd_uses_rd_and_a() {
|
||||
// SPREAD_OBJECT 原地改目标对象:def=rd,use=[rd, a],不可删除(有观察副作用)
|
||||
let inst = Inst::spread_object(Operand::Reg(3), Operand::Reg(7));
|
||||
assert_eq!(inst.def_reg(), Some(3));
|
||||
assert_eq!(inst.use_regs().as_slice(), &[3, 7]);
|
||||
let f = IRFunction::new();
|
||||
assert!(!inst.is_pure(&f));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_arguments_defs_rd_no_register_use_impure() {
|
||||
// CREATE_ARGUMENTS 写 rd;实参从 spill 栈读取,无寄存器 use;
|
||||
|
||||
@@ -153,6 +153,13 @@ impl Inst {
|
||||
Self::with_ext(OpCode::REST_OBJECT, rest, src, Operand::None, &[excluded_idx])
|
||||
}
|
||||
|
||||
/// 对象字面量 spread 展开:把源 `src` 的可枚举自有属性写入目标对象 `rd`(原地改)。
|
||||
/// 语义 = CopyDataProperties 的"非 null/undefined 源":null/undefined 合法(空展开),
|
||||
/// 与 REST_OBJECT(对 null/undefined 抛 TypeError)不同。
|
||||
pub fn spread_object(target: Operand, src: Operand) -> Self {
|
||||
Self::new(OpCode::SPREAD_OBJECT, target, src, Operand::None)
|
||||
}
|
||||
|
||||
/// TEMPLATE_STR:变长 ext。首字打包 `(segment_count<<16) | total_len_hint`,
|
||||
/// 后续每 quasi 一项 `quasi_const_idx & 0x7FFF_FFFF`,其后若跟表达式再一项 `0x8000_0000 | expr_reg`。
|
||||
pub fn template_str(dst: Operand, segment_count: u32, total_len_hint: u16, parts: &[u32]) -> Self {
|
||||
@@ -311,6 +318,13 @@ mod tests {
|
||||
|
||||
let rest = Inst::rest_object(Operand::Reg(0), Operand::Reg(1), 7);
|
||||
assert_eq!(rest.ext.as_slice(), &[7]);
|
||||
|
||||
let spread = Inst::spread_object(Operand::Reg(0), Operand::Reg(1));
|
||||
assert_eq!(spread.op, OpCode::SPREAD_OBJECT);
|
||||
assert_eq!(spread.rd, Operand::Reg(0));
|
||||
assert_eq!(spread.a, Operand::Reg(1));
|
||||
assert_eq!(spread.b, Operand::None);
|
||||
assert!(spread.ext.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1351,6 +1351,10 @@ impl Vm {
|
||||
self.dispatch_rest_object(rd, a)?;
|
||||
}
|
||||
|
||||
OpCode::SPREAD_OBJECT => {
|
||||
self.dispatch_spread_object(rd, a)?;
|
||||
}
|
||||
|
||||
OpCode::THROW => match self.dispatch_throw(rd) {
|
||||
Ok(true) => continue,
|
||||
Ok(false) => {}
|
||||
|
||||
@@ -657,4 +657,97 @@ impl Vm {
|
||||
self.regs[rd] = JsValue::from_js_object(rest_ptr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SPREAD_OBJECT:对象字面量 `...` 展开,把源的可枚举自有属性写入目标对象(原地改)。
|
||||
///
|
||||
/// 语义 = CopyDataProperties 的"非 null/undefined 源"分支:`{...null}` / `{...undefined}`
|
||||
/// 合法(产出空展开),与 REST_OBJECT 对 null/undefined 抛 TypeError 不同。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. null/undefined 源直接返回(空展开)
|
||||
/// 2. 字符串源按 UTF-16 code unit 下标复制为可枚举索引属性
|
||||
/// 3. 对象源先收集可枚举自有属性(数组元素区 + shape 链),取值经 ordinary_get
|
||||
/// 触发访问器 getter,再统一写入目标
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 其它原始值(number/boolean/symbol)无自有可枚举属性 → 空展开
|
||||
/// - 目标已有同名属性被覆盖(后定义/后展开者胜)
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 原地修改 rd 指向的对象;取值可能触发 getter 并抛异常(异常传播)
|
||||
pub(crate) fn dispatch_spread_object(&mut self, rd: usize, a: usize) -> Result<(), String> {
|
||||
vm_trace!("SPREAD_OBJECT rd={}", rd);
|
||||
let target_val = self.regs[rd];
|
||||
let src = self.regs[a];
|
||||
|
||||
// null/undefined 源合法:展开为空,直接返回。
|
||||
if src.is_null() || src.is_undefined() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 字符串源:按索引复制字符(可枚举索引属性)。
|
||||
if src.is_string() {
|
||||
let target = unsafe { &mut *target_val.as_js_object_ptr() };
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let s = char::from_u32(*unit as u32).map(|c| c.to_string()).unwrap_or_default();
|
||||
let si = self.kernel_core.perm_interner().intern(&i.to_string()).0;
|
||||
let ch_val = self.new_string(&s);
|
||||
self.set_or_create_prop_value(target, si, ch_val);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 其它原始值无自有可枚举属性 → 空展开。
|
||||
if !src.is_object() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src_obj = unsafe { &*src.as_js_object_ptr() };
|
||||
|
||||
// 收集-提交:先取齐 (键, 值),取值阶段触发 getter(可能抛异常),
|
||||
// 再统一写目标,避免提交写与取值互相交错。
|
||||
let mut assignments: Vec<(u32, JsValue)> = Vec::new();
|
||||
|
||||
// 数组元素区:整数下标可枚举元素(hole 跳过)。
|
||||
if src_obj.is_array() {
|
||||
for i in 0..src_obj.array_prop_count {
|
||||
if src_obj.prop_meta_at(i).is_some_and(|m| m.is_hole()) {
|
||||
continue;
|
||||
}
|
||||
let enumerable = src_obj
|
||||
.prop_meta_at(i)
|
||||
.map(|m| m.attributes.enumerable())
|
||||
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable());
|
||||
if enumerable {
|
||||
let si = self.kernel_core.perm_interner().intern(&i.to_string()).0;
|
||||
let val = self.ordinary_get(src_obj, si, src)?;
|
||||
assignments.push((si, val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 命名属性:shape 链(walk_own_keys 规范顺序),仅可枚举。
|
||||
let keys = oxide_builtins::object::walk_own_keys(self, src_obj);
|
||||
for (si, pos) in keys {
|
||||
let store = if src_obj.is_array() { src_obj.array_prop_count + pos } else { pos };
|
||||
let enumerable = src_obj
|
||||
.prop_meta_at(store)
|
||||
.map(|m| m.attributes.enumerable())
|
||||
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable());
|
||||
if !enumerable {
|
||||
continue;
|
||||
}
|
||||
let val = self.ordinary_get(src_obj, si, src)?;
|
||||
assignments.push((si, val));
|
||||
}
|
||||
|
||||
// 提交:覆盖目标已有同名属性(从左到右求值,后展开者胜)。
|
||||
let target = unsafe { &mut *target_val.as_js_object_ptr() };
|
||||
for (si, val) in assignments {
|
||||
let promoted = self.promote_if_needed_for_write_ptr(target, val);
|
||||
self.set_or_create_prop_value(target, si, promoted);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,3 +152,37 @@ fn object_literal_setter_updates_receiver() {
|
||||
fn object_literal_getter_setter_pair() {
|
||||
assert_eq!(eval("var o={ get x(){ return this.y }, set x(v){ this.y=v } }; o.x=9; o.x"), "9");
|
||||
}
|
||||
|
||||
fn eval_string(source: &str) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let mut vm = Vm::new();
|
||||
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||||
let module = Compiler::new().compile(&program).expect("compile failed");
|
||||
let result = vm.run(&module).expect("vm run failed");
|
||||
vm.lookup_str(result).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_literal_spread_copies_own_properties() {
|
||||
assert_eq!(eval_string("JSON.stringify({a:0,...{x:1,y:2}})"), r#"{"a":0,"x":1,"y":2}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_literal_spread_later_definitions_override() {
|
||||
assert_eq!(eval("({...{a:1},a:9}).a"), "9");
|
||||
assert_eq!(eval("({a:1,...{a:9}}).a"), "9");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_literal_spread_nullish_is_empty() {
|
||||
assert_eq!(eval_string("JSON.stringify({...null})"), "{}");
|
||||
assert_eq!(eval_string("JSON.stringify({...undefined})"), "{}");
|
||||
assert_eq!(eval_string("JSON.stringify({...42})"), "{}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_literal_spread_string_indexes_and_getters() {
|
||||
assert_eq!(eval_string("JSON.stringify({...'ab'})"), r#"{"0":"a","1":"b"}"#);
|
||||
assert_eq!(eval("({...{get x(){return 5}}}).x"), "5");
|
||||
assert_eq!(eval_string("JSON.stringify({...[],a:1})"), r#"{"a":1}"#);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user