diff --git a/crates/oxide_emit/src/expr/object/array.rs b/crates/oxide_emit/src/expr/object/array.rs index 8999990d..0ebdab97 100644 --- a/crates/oxide_emit/src/expr/object/array.rs +++ b/crates/oxide_emit/src/expr/object/array.rs @@ -58,6 +58,9 @@ impl Emitter { let one_reg = ctx.alloc_reg(); let one_idx = ctx.add_constant(Constant::Int(1)); ctx.inst(Inst::load_const(Operand::Reg(one_reg), one_idx)); + let undef_reg = ctx.alloc_reg(); + let undef_idx = ctx.add_constant(Constant::Undefined); + ctx.inst(Inst::load_const(Operand::Reg(undef_reg), undef_idx)); for elem in &arr.elements { if let Some(e) = elem.as_expression() { @@ -75,7 +78,26 @@ impl Emitter { Operand::Reg(one_reg), )); } else if elem.is_elision() { - // hole:只推进 index,不写元素,length 由后续 SET_ELEM 越界扩容或初值覆盖。 + // hole:先写 present-undefined 把 length 扩到 I+1,再删除置洞。 + // 顺序不可换:DELETE 先跑对界外下标是空操作,随后的写会留下 present 槽。 + ctx.inst(Inst::new( + OpCode::SET_ELEM, + Operand::Reg(arr_reg), + Operand::Reg(index_reg), + Operand::Reg(undef_reg), + )); + + // 复制 arr 到 scratch 执行删除:DELETE_PROP_DYNAMIC 把结果写回 rd 槽, + // 直接用 arr_reg 会把数组寄存器覆盖成布尔值。 + let scratch = ctx.alloc_reg(); + ctx.inst(Inst::inst_mov(Operand::Reg(scratch), Operand::Reg(arr_reg))); + ctx.inst(Inst::new( + OpCode::DELETE_PROP_DYNAMIC, + Operand::Reg(scratch), + Operand::Reg(scratch), + Operand::Reg(index_reg), + )); + ctx.inst(Inst::new( OpCode::ADD, Operand::Reg(index_reg), diff --git a/crates/oxide_vm/tests/array_literal_elision_tests.rs b/crates/oxide_vm/tests/array_literal_elision_tests.rs new file mode 100644 index 00000000..905a8337 --- /dev/null +++ b/crates/oxide_vm/tests/array_literal_elision_tests.rs @@ -0,0 +1,103 @@ +//! 数组字面量 elision(hole 元素)引擎钉:静态字面量 elision 由 emit 侧删除置洞, +//! spread 字面量 elision 由 SET_ELEM 扩长 + DELETE_PROP_DYNAMIC 置洞, +//! 行为对齐 `in`、`length`、`Object.keys`、`join`、for-in 键集与原型链读取。 + +use std::sync::Arc; + +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(&Arc::new(module))?; + Ok((vm, result)) +} + +fn eval_str(source: &str) -> Result { + let (vm, v) = eval(source)?; + // 字符串结果须在同一 VM 上读:perm 串指向 VM 私有内核,VM drop 后指针悬垂。 + if v.is_string() { + Ok(vm.lookup_str(v).unwrap_or_default()) + } else { + Ok(format!("{v}")) + } +} + +// 钉 1:静态 elision 是真洞(`in` 为 false、`Object.keys` 不含洞下标)。 +#[test] +fn static_elision_is_hole() { + assert_eq!(eval_str("''+(0 in [,,])+':'+Object.keys([,,]).length").unwrap(), "false:0"); +} + +// 钉 2:界内静态 elision 读回为洞(length 含洞、`in` 为 false)。 +#[test] +fn static_inner_elision_is_hole() { + assert_eq!(eval_str("''+[1,,3].length+':'+(1 in [1,,3])").unwrap(), "3:false"); +} + +// 钉 3:首元素 elision(length 含洞、0 下标为洞)。 +#[test] +fn static_leading_elision_is_hole() { + assert_eq!(eval_str("''+[,1].length+':'+(0 in [,1])").unwrap(), "2:false"); +} + +// 钉 4:显式 undefined 元素是 present 属性(与 elision 区分)。 +#[test] +fn explicit_undefined_is_present() { + assert_eq!(eval_str("''+(0 in [undefined])").unwrap(), "true"); +} + +// 钉 5:静态 elision 落原型链(原型有 0 下标时 `in` 为 true、读回原型上的值)。 +#[test] +fn static_elision_falls_to_proto_chain() { + assert_eq!(eval_str("Array.prototype[0]='g'; ''+(0 in [,,])+':'+[,,][0]").unwrap(), "true:g",); +} + +// 钉 6:join 跳洞(两个洞产生一个分隔符)。 +#[test] +fn static_elision_join_skips_holes() { + assert_eq!(eval_str("[,,].join(\",\")").unwrap(), ","); +} + +// 钉 7:for-in 键集不含洞下标。 +#[test] +fn static_elision_for_in_no_keys() { + assert_eq!(eval_str("var c=0; for (var k in [,,]) c++; ''+c").unwrap(), "0"); +} + +// 钉 8:连续尾部 elision 计入 length。 +#[test] +fn static_trailing_elisions_count_in_length() { + assert_eq!(eval_str("''+[4,5,,,,].length").unwrap(), "5"); +} + +// 钉 9:spread 后尾逗号不产生 elision 元素(length 不变)。 +#[test] +fn spread_trailing_comma_not_elision() { + assert_eq!(eval_str("''+[...[1],].length").unwrap(), "1"); +} + +// 钉 10:spread 后尾逗号不产生 elision 元素(length = spread 长度)。 +#[test] +fn spread_trailing_comma_not_elision_dense() { + assert_eq!(eval_str("''+[...[1,2,3],].length").unwrap(), "3"); +} + +// 钉 11:spread 中间 elision 置洞并计入 length(界内形)。 +#[test] +fn spread_inner_elision_is_hole() { + assert_eq!(eval_str("const a=[0,...[1],,2]; ''+a.length+':'+(2 in a)").unwrap(), "4:false",); +} + +// 钉 12:spread 空展开后界外 elision 扩长并置洞。 +#[test] +fn spread_out_of_bounds_elision_extends_and_holes() { + assert_eq!( + eval_str("const b=[...[],,,]; ''+b.length+':'+(0 in b)+':'+(1 in b)").unwrap(), + "2:false:false", + ); +}