fix: spread 字面量 elision 置洞并扩长

This commit is contained in:
2026-09-19 12:44:47 +00:00
parent 5d99f5eb7c
commit ae71adcaab
2 changed files with 126 additions and 1 deletions
+23 -1
View File
@@ -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),
@@ -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<String, String> {
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",
);
}