diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index c3bd8ede..7b43b15f 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -450,6 +450,12 @@ define_opcodes! { SET_PROP_BATCH = 0x92 => "SET_PROP_BATCH", def = None, uses = [SlotSpec::Slot(Slot::Rd), SlotSpec::Slot(Slot::A), SlotSpec::Slot(Slot::B)], pure = false, jump = false, term = false, ic = false, + // 多操作数拼接(连续 `+` 左结合链摊平):rd=结果,a=首操作数,ext=[n, op2..opn], + // n=操作数总数,ext 字数 = 1+(n-1) = n;op 字无高位标记(纯 vreg,≤255), + // SpreadArgs 直接解析。与 ADD 同标纯(含 ToPrimitive 副作用仍静态可删,语义同 ADD)。 + CONCAT_N = 0x93 => "CONCAT_N", + def = Some(SlotSpec::Slot(Slot::Rd)), uses = [SlotSpec::Slot(Slot::A), SlotSpec::SpreadArgs], + pure = true, jump = false, term = false, ic = false, // ── 成员更新 (0x59-0x62) ── MEMBER_INC = 0x59 => "MEMBER_INC", // 结果写 a 槽(val 槽原地更新) @@ -783,6 +789,7 @@ mod tests { assert_eq!(OpCode::CREATE_REST_ARRAY as u8, 0x6E); assert_eq!(OpCode::DEFINE_ACCESSOR_DYNAMIC as u8, 0x7F); assert_eq!(OpCode::SET_PROP_BATCH as u8, 0x92); + assert_eq!(OpCode::CONCAT_N as u8, 0x93); 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"); diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 177b107b..36c38316 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -30,6 +30,25 @@ impl Emitter { fn emit_binary_expression( &self, bin: &oxide_parser::BinaryExpression, ctx: &mut CompileCtx, ) -> Result { + // Addition 左结合链摊平:≥3 操作数合并为单条 CONCAT_N(单趟预分配拼接, + // 消除 N-2 次中间串分配)。只下钻 left 链、right 括号不拆(a+(b+c) 退化 ADD), + // 保 f64 结合性与源码求值序。 + if bin.operator == BinaryOperator::Addition { + let mut operands = Vec::new(); + collect_add_operands(&bin.left, &mut operands); + operands.push(&bin.right); + // 上限防寄存器压力:CONCAT_N 单点读全部操作数(须同时存活),超长链回退 + // 左结合 ADD(寄存器随链复用,纯表达式寄存器占用有界)。 + if operands.len() >= 3 && operands.len() <= MAX_CONCAT_N_OPERANDS { + let mut regs = Vec::with_capacity(operands.len()); + for op in &operands { + regs.push(self.emit_expression(op, ctx)?); + } + let result_reg = ctx.alloc_reg(); + ctx.inst(Inst::concat_n(Operand::Reg(result_reg), ®s)); + return Ok(result_reg); + } + } let left = self.emit_expression(&bin.left, ctx)?; let right = self.emit_expression(&bin.right, ctx)?; let op = match bin.operator { @@ -554,3 +573,20 @@ impl Emitter { } } } + +/// CONCAT_N 摊平的操作数上限:CONCAT_N 单点读全部操作数(须同时存活),寄存器占用 +/// = 操作数 + 结果 ≤ 15;超过上限回退左结合 ADD(超长链保持寄存器随链复用属性)。 +const MAX_CONCAT_N_OPERANDS: usize = 14; + +/// 收集 Addition 左结合链的操作数(源码求值序):只下钻 left 链,right 恒为单个 +/// 操作数(即使自身是 Addition 也不拆——括号右结合保持 f64 结合性不引入偏差)。 +fn collect_add_operands<'expr, 'r>(expr: &'r Expression<'expr>, out: &mut Vec<&'r Expression<'expr>>) { + if let Expression::BinaryExpression(bin) = expr { + if bin.operator == BinaryOperator::Addition { + collect_add_operands(&bin.left, out); + out.push(&bin.right); + return; + } + } + out.push(expr); +} diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index 6b350180..3775c8e1 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -556,6 +556,7 @@ mod tests { Inst::compound_member_ushr(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3)) } OpCode::TEMPLATE_STR => Inst::template_str(Operand::Reg(1), 2, 10, &[0x1234, 0x8000_0000 | 5]), + OpCode::CONCAT_N => Inst::concat_n(Operand::Reg(1), &[2, 5, 9]), OpCode::GET_PRIVATE => { Inst::get_private(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 7, 99) } @@ -723,6 +724,8 @@ mod tests { assert_contract(OpCode::HALT, None, &[0], false); // 模板字符串:表达式寄存器来自 ext assert_contract(OpCode::TEMPLATE_STR, Some(1), &[5], true); + // 多操作数拼接:a 槽首操作数 + ext[1..] 后续操作数(SpreadArgs 解析) + assert_contract(OpCode::CONCAT_N, Some(1), &[2, 5, 9], true); // 小语言特性 assert_contract(OpCode::DELETE_PROP_STATIC, None, &[1], false); assert_contract(OpCode::DELETE_PROP_DYNAMIC, None, &[1, 3], false); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index 05a43a7d..b0494d94 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -258,6 +258,23 @@ impl Inst { Self::new(OpCode::SET_PROP_BATCH, target, value, Operand::Imm(slot)) } + /// 多操作数拼接(连续 `+` 表达式摊平):rd=结果,首操作数放 a 槽, + /// ext=[n, op2..opn](n=操作数总数,ext 字数 = 1+(n-1) = n)。 + /// 操作数按源码求值序排列,emit 层保证 ≥3 个(<3 退化 ADD);op 字无高位标记。 + pub fn concat_n(dst: Operand, operands: &[u32]) -> Self { + debug_assert!(operands.len() >= 3, "CONCAT_N 仅用于 ≥3 操作数摊平"); + let mut ext = SmallVec::with_capacity(operands.len()); + ext.push(operands.len() as u32); + ext.extend_from_slice(&operands[1..]); + Self { + op: OpCode::CONCAT_N, + rd: dst, + a: Operand::Reg(operands[0]), + b: Operand::None, + ext, + } + } + /// 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 { @@ -591,4 +608,15 @@ mod tests { assert_eq!(inst.ext[1], 0x1234); assert_eq!(inst.ext[2], 0x8000_0000 | 5); } + + #[test] + fn concat_n_packs_n_header_and_operand_regs() { + // 6 操作数:a 槽=op1,ext=[6, op2..op6] + let inst = Inst::concat_n(Operand::Reg(1), &[2, 5, 9, 12, 300, 7]); + assert_eq!(inst.op, OpCode::CONCAT_N); + assert_eq!(inst.rd, Operand::Reg(1)); + assert_eq!(inst.a, Operand::Reg(2)); + assert_eq!(inst.b, Operand::None); + assert_eq!(inst.ext.as_slice(), &[6, 5, 9, 12, 300, 7]); + } } diff --git a/crates/oxide_regalloc/src/graph.rs b/crates/oxide_regalloc/src/graph.rs index 2bb7cf2b..64844d29 100644 --- a/crates/oxide_regalloc/src/graph.rs +++ b/crates/oxide_regalloc/src/graph.rs @@ -86,6 +86,16 @@ pub(super) fn collect_real_vregs(f: &IRFunction) -> BTreeSet { real.insert(r); } } + // CONCAT_N:ext[1..] 每个字是完整操作数 vreg(无高位标记)——漏收集则 + // RegAlloc 不分配该 vreg,物理号冲突 → 错值。 + if inst.op == OpCode::CONCAT_N { + for &w in inst.ext.iter().skip(1) { + let r = w & 0x7FFF_FFFF; + if r != 0 { + real.insert(r); + } + } + } } real } @@ -474,4 +484,21 @@ mod tests { assert!(g.nodes.contains_key(&254), "vreg 254 建节点"); assert_eq!(g.nodes[&254].pre_color, None, "254 非预着色"); } + + #[test] + fn concat_n_ext_operands_collected_as_real() { + // CONCAT_N 的 ext 操作数 vreg 必须进入真实集合(漏收集 → 未染色 → 错值) + let mut f = empty_function(); + f.insts.push(Inst::concat_n(Operand::Reg(1), &[2, 5, 9, 300])); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(1), Operand::None, Operand::None)); + let real = collect_real_vregs(&f); + for r in [1u32, 2, 5, 9, 300] { + assert!(real.contains(&r), "vreg {r} 应被 CONCAT_N 收集"); + } + let g = build_graph(&f); + for r in [1u32, 2, 5, 9, 300] { + assert!(g.nodes.contains_key(&r), "vreg {r} 建节点"); + } + } } diff --git a/crates/oxide_regalloc/src/rewrite.rs b/crates/oxide_regalloc/src/rewrite.rs index 769cf2e5..28d8adb7 100644 --- a/crates/oxide_regalloc/src/rewrite.rs +++ b/crates/oxide_regalloc/src/rewrite.rs @@ -246,6 +246,15 @@ fn rewrite_inst( } ext } + // CONCAT_N:ext[1..] 每个字是完整操作数 vreg(无高位标记),直接重映射, + // 否则 RegAlloc 后读旧 vreg 号对应物理槽(错值)。 + OpCode::CONCAT_N => { + let mut ext = inst.ext.clone(); + for seg in ext.iter_mut().skip(1) { + *seg = remap_ext_reg(*seg, slot_color, map); + } + ext + } _ => inst.ext.clone(), }; // 调用点首参槽改指 arg_window_base(桥接后) @@ -457,6 +466,23 @@ mod tests { assert_eq!(f.insts[0].ext.as_slice(), &[3 << 16, 0, 0x8000_0000 | 200, 1]); } + #[test] + fn concat_n_ext_operands_remapped() { + // CONCAT_N ext[1..] 操作数 vreg 必须重映射到物理号(含 spill use 点 fresh 色) + let inst = Inst::concat_n(Operand::Reg(1), &[2, 300, 9]); + let mut map = AllocMap::new(); + map.map.insert(1, Alloc::Phys(2)); + map.map.insert(2, Alloc::Phys(3)); + map.map.insert(300, Alloc::Phys(200)); + map.map.insert(9, Alloc::Phys(11)); + let f = rewrite_with_map(vec![inst], map); + + assert_eq!(f.insts[0].op, OpCode::CONCAT_N); + assert_eq!(f.insts[0].rd, Operand::Reg(2)); + assert_eq!(f.insts[0].a, Operand::Reg(3)); + assert_eq!(f.insts[0].ext.as_slice(), &[3, 200, 11]); + } + #[test] fn scattered_args_insert_mov_bridge() { let insts = vec![Inst::call(Operand::Reg(2), Operand::Reg(3), Operand::Reg(4), 3)]; diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index e7e77ffe..41b65180 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -2,6 +2,7 @@ use crate::vm::Vm; use crate::vm_trace; use oxide_runtime_api as coercion; use oxide_types::value::JsValue; +use smallvec::SmallVec; /// 判断 JsValue 是否为 BigInt 且值为 0(num_bigint 零值判断)。 fn bigint_is_zero(v: &num_bigint::BigInt) -> bool { @@ -95,6 +96,120 @@ impl Vm { self.new_string_owned(buf) } + /// CONCAT_N:多操作数拼接(连续 `+` 左结合链摊平)的两阶段 dispatch。 + /// + /// # 步骤 + /// 1. 读 ext 收集全部操作数寄存器(pc 推进约定同 TEMPLATE_STR:主循环已越过指令, + /// 此处先读 ext[0]=n 并推进,再逐操作数推进,总推进 n 个扩展字)。 + /// 2. 阶段 1 数值前缀折叠:逐项复用 ADD 判定序列(int 溢出升 double / double / + /// BigInt 混合 TypeError / 对象 ToPrimitive 副作用顺序),与左结合两两 ADD + /// 逐点一致;遇字符串即转入阶段 2。 + /// 3. 阶段 2 字符串模式单趟预分配:剩余操作数按序 coerce 后一次写齐,消除 + /// N-2 次中间串分配。 + /// + /// # 边界与前提 + /// - emit 层保证 n ≥ 3;此处 n<2 防御退化(返回首操作数)。 + /// - Symbol 操作数行为与 concat_strings 一致(push_to_string 无 symbol 分支)。 + /// + /// # 副作用 + /// 写 regs[rd];字符串路径新建一个会话字符串。 + #[inline(never)] + pub(crate) fn dispatch_concat_n(&mut self, rd: usize, a: usize) -> Result<(), String> { + vm_trace!("CONCAT_N rd={}", rd); + let n = self.bytecode[self.pc] as usize; + self.pc += 1; + let mut ops = SmallVec::<[usize; 8]>::new(); + ops.push(a); + for _ in 0..n.saturating_sub(1) { + let reg = (self.bytecode[self.pc] & 0x7FFF_FFFF) as usize; + self.pc += 1; + ops.push(reg); + } + if ops.len() < 2 { + self.regs[rd] = self.regs[ops[0]]; + return Ok(()); + } + // ── 阶段 1:数值前缀折叠(与左结合两两 ADD 逐点一致)── + let mut acc = self.regs[ops[0]]; + let mut idx = 1; + while idx < ops.len() { + let next = self.regs[ops[idx]]; + if acc.is_int() && next.is_int() { + acc = int_add(acc.as_int(), next.as_int()); + idx += 1; + continue; + } + if acc.is_double() && next.is_double() { + acc = JsValue::float(acc.as_double() + next.as_double()); + idx += 1; + continue; + } + if acc.is_bigint() && next.is_bigint() { + acc = self.new_bigint(self.bigint_value(acc) + self.bigint_value(next)); + idx += 1; + continue; + } + // 快路径失败才 coerce:acc 已原语零副作用;next 对象 ToPrimitive 在此触发, + // 顺序与左结合 ADD 一致。 + let lhs = self.coerce_primitive_bounded(acc, false)?; + let rhs = self.coerce_primitive_bounded(next, false)?; + if lhs.is_string() || rhs.is_string() { + // ── 阶段 2:字符串模式单趟预分配 ── + self.regs[rd] = self.concat_n_strings(lhs, rhs, &ops[idx + 1..])?; + return Ok(()); + } + if lhs.is_bigint() && rhs.is_bigint() { + acc = self.new_bigint(self.bigint_value(lhs) + self.bigint_value(rhs)); + idx += 1; + continue; + } + if lhs.is_bigint() != rhs.is_bigint() { + // 包装对象 coerce 后暴露 BigInt:混合加法必须抛 TypeError(同 ADD 抛点)。 + return self.raise_type_error("Cannot mix BigInt and other types, use explicit conversions"); + } + let ln = coercion::to_number(lhs); + let rn = coercion::to_number(rhs); + acc = JsValue::float(ln + rn); + idx += 1; + } + self.regs[rd] = acc; + Ok(()) + } + + /// 字符串模式单趟预分配拼接:l/r 已 coerce,剩余操作数按序 coerce 后一次写齐。 + /// + /// # 步骤 + /// 1. 剩余操作数按序 coerce(对象 ToPrimitive 副作用顺序与左结合一致,抛错点先于分配)。 + /// 2. 精确总长:字符串操作数取字节长,非字符串给 32 字节余量(int/double/bool/null/ + /// undefined 的十进制文本上界;BigInt 超长时 String 自动扩容兜底)。 + /// 3. 单趟 push_to_string + new_string_owned(零二次拷贝)。 + /// + /// # 副作用 + /// 新建一个会话字符串。 + fn concat_n_strings(&mut self, lhs: JsValue, rhs: JsValue, rest: &[usize]) -> Result { + let mut parts = SmallVec::<[JsValue; 8]>::new(); + parts.push(lhs); + parts.push(rhs); + for ® in rest { + let v = self.regs[reg]; + let prim = if v.is_object() { self.coerce_primitive_bounded(v, false)? } else { v }; + parts.push(prim); + } + let cap = parts.iter().fold(0usize, |acc, p| { + acc + if p.is_string() { + // SAFETY: p 是字符串值。 + unsafe { (*p.as_string_ptr()).len() } + } else { + 32 + } + }); + let mut buf = String::with_capacity(cap); + for p in &parts { + coercion::push_to_string(*p, &mut buf); + } + Ok(self.new_string_owned(buf)) + } + #[inline(always)] pub(crate) fn dispatch_neg(&mut self, rd: usize, a: usize) -> Result<(), String> { vm_trace!("NEG rd={} r{}={:?}", rd, a, self.regs[a]); diff --git a/crates/oxide_vm/src/ic_helper.rs b/crates/oxide_vm/src/ic_helper.rs index 0826c8cd..2a036ccf 100644 --- a/crates/oxide_vm/src/ic_helper.rs +++ b/crates/oxide_vm/src/ic_helper.rs @@ -78,6 +78,8 @@ fn ext_word_count(bytecode: &[Instr], pc: usize) -> usize { let header = bytecode.get(pc + 1).copied().unwrap_or(0); 1 + ((header >> 16) & 0xFFFF) as usize } + // CONCAT_N:ext[0]=n=操作数总数,ext 字数 = 1+(n-1) = n。 + OpCode::CONCAT_N => bytecode.get(pc + 1).copied().unwrap_or(0) as usize, OpCode::NEW_OBJECT => opcode::a(bytecode[pc]) as usize, _ => 0, } @@ -287,3 +289,21 @@ fn ic_set_hit_own_poly(obj: &mut JsObject, bytecode: &[Instr], ext_pc: usize, va } false } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn concat_n_ext_word_count_advances_by_n() { + // CONCAT_N rd=1, a=2, ext=[3, 5, 9]:ext[0]=n=3,总 ext 字数 = 3 + let mut bytecode = vec![opcode::encode(OpCode::CONCAT_N, 1, 2, 0), 3, 5, 9]; + assert_eq!(ext_word_count(&bytecode, 0), 3, "ext 字数 = n"); + + // clear_ic_caches 跳过 CONCAT_N 扩展字,不误当 IC 字清零 + bytecode.push(opcode::encode(OpCode::NOP, 0, 0, 0)); + clear_ic_caches(&mut bytecode); + assert_eq!(bytecode[1..4], [3, 5, 9], "CONCAT_N ext 字保持原值"); + assert_eq!(bytecode.len(), 5, "CONCAT_N 指令 + 3 ext + NOP"); + } +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 22a62c84..5be87e94 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1342,6 +1342,10 @@ impl Vm { self.dispatch_add(rd, a, b)?; } + OpCode::CONCAT_N => { + self.dispatch_concat_n(rd, a)?; + } + OpCode::SUB => { binary_arith!(self, a, b, rd, -, false); } diff --git a/crates/oxide_vm/tests/concat_n_tests.rs b/crates/oxide_vm/tests/concat_n_tests.rs new file mode 100644 index 00000000..2af5fe5e --- /dev/null +++ b/crates/oxide_vm/tests/concat_n_tests.rs @@ -0,0 +1,157 @@ +//! CONCAT_N 多操作数拼接(连续 `+` 左结合链摊平)语义回归测试。 +//! +//! 覆盖:三形态混合锚点(数值前缀折叠 → 字符串模式单趟拼接)、BigInt 混合抛错点、 +//! 对象 ToPrimitive 副作用顺序、Symbol 行为与二元 ADD 一致、f64 结合性(右括号不拆)、 +//! 求值顺序、2 操作数与 3+ 操作数等价性、长链拼接正确性。 + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + 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}"))?; + vm.run(&module) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.lookup_str(val).unwrap_or_default() +} + +/// 按 CLI 语义格式化结果:字符串取内容(不带引号),数值/BigInt 走 Display。 +fn fmt(vm: &Vm, val: JsValue) -> String { + if val.is_string() { + // SAFETY: val 已确认是字符串值。 + unsafe { (*val.as_string_ptr()).data.clone() } + } else if val.is_bigint() { + format!("{}", vm.bigint_value(val)) + } else { + format!("{val}") + } +} + +fn eval_str(source: &str) -> String { + let mut vm = Vm::new(); + let val = eval(&mut vm, source).expect("eval ok"); + fmt(&vm, val) +} + +#[test] +fn concat_n_three_form_mixed_anchors() { + // 数值前缀折叠保证 1+2+"3" = "33"(非 "123");字符串开头整链拼接;中缀字符串同理。 + assert_eq!(eval_str("1 + 2 + \"3\""), "33"); + assert_eq!(eval_str("\"1\" + 2 + 3"), "123"); + assert_eq!(eval_str("1 + \"2\" + 3"), "123"); + assert_eq!(eval_str("\"a\" + 1 + 2"), "a12"); +} + +#[test] +fn concat_n_pure_numeric_folds_like_left_assoc() { + // 全数值链走阶段 1 折叠:int 溢出升 double 与两两 ADD 逐点一致。 + assert_eq!(eval_str("1 + 2 + 3 + 4 + 5"), "15"); + assert_eq!(eval_str("2147483647 + 1 + 2147483647"), "4294967295"); + assert_eq!(eval_str("1 + 2.5 + 3"), "6.5"); +} + +#[test] +fn concat_n_bigint_mix_semantics() { + // 全 BigInt 链折叠;BigInt 混合在第一个混合点抛 TypeError(与 (1+2n) 同位置); + // BigInt + 字符串开头走字符串模式。 + assert_eq!(eval_str("1n + 2n + 3n"), "6"); + let err = eval(&mut Vm::new(), "1 + 2n + \"a\"").unwrap_err(); + assert!(err.contains("TypeError"), "expected TypeError, got {err}"); + assert_eq!(eval_str("1n + \"a\" + 2"), "1a2"); +} + +#[test] +fn concat_n_object_valueof_side_effect_order() { + // 操作数按源码序 coerce(左结合顺序),valueOf 副作用序保持。 + assert_eq!( + eval_str( + "var log = []; var o1 = { valueOf: function () { log.push(1); return 'x' } }; \ + var o2 = { valueOf: function () { log.push(2); return 'y' } }; \ + (o1 + o2 + 'z') + '|' + log.join(',')" + ), + "xyz|1,2" + ); +} + +#[test] +fn concat_n_call_side_effect_order() { + // f()+g()+h() 求值序 f→g→h(摊平后按序 emit,顺序保持)。 + assert_eq!( + eval_str( + "var log = []; \ + function f() { log.push('f'); return 'f' } \ + function g() { log.push('g'); return 'g' } \ + function h() { log.push('h'); return 'h' } \ + (f() + g() + h()) + '|' + log.join(',')" + ), + "fgh|f,g,h" + ); +} + +#[test] +fn concat_n_symbol_matches_binary_add_behavior() { + // Symbol 在字符串模式下被静默忽略(既有 concat_strings 行为),CONCAT_N 与二元 + // ADD 结果逐字节一致,不放大不修复。 + let flat = eval_str("var s = Symbol(); 'a' + s + 'b'"); + let binary = eval_str("var s = Symbol(); ('a' + s) + 'b'"); + assert_eq!(flat, binary); + assert_eq!(flat, "ab"); +} + +#[test] +fn concat_n_right_parentheses_not_flattened() { + // 右括号不拆:0.1+0.2+0.3 左结合(CONCAT_N)与 0.1+(0.2+0.3)(ADD 链)结果不同, + // 证明摊平只发生在 left 链、右括号保持原结合性。 + assert_eq!(eval_str("0.1 + 0.2 + 0.3"), "0.6000000000000001"); + assert_eq!(eval_str("0.1 + (0.2 + 0.3)"), "0.6"); +} + +#[test] +fn concat_n_two_and_multi_operand_equivalence() { + // 同一语义表达式以 2 操作数(ADD)与 3+ 操作数(CONCAT_N)两种写法输出一致。 + assert_eq!(eval_str("1 + 2 + 3"), eval_str("(1 + 2) + 3")); + assert_eq!(eval_str("'a' + 'b' + 'c'"), eval_str("('a' + 'b') + 'c'")); + assert_eq!(eval_str("1.5 + 2.5 + 3.5"), eval_str("(1.5 + 2.5) + 3.5")); + assert_eq!(eval_str("1 + 2 + 3 + 4 + 5 + 6"), "21"); +} + +#[test] +fn concat_n_compound_rhs_chain() { + // `s += a+b+c`:rhs 摊平为单条 CONCAT_N,COMPOUND_ADD 读旧值拼接; + // 单操作数 rhs(`s += j`)不触达摊平,仍走 COMPOUND_ADD。 + assert_eq!(eval_str("var s = ''; s += 'a' + 'b' + 'c'; s"), "abc"); + assert_eq!(eval_str("var s = ''; for (var j = 0; j < 5; j++) { s += j } s"), "01234"); +} + +#[test] +fn concat_n_large_string_builds_correctly() { + // 长串多段拼接:长度与逐字符位置正确(结果串单趟成型)。 + assert_eq!(eval_str("('x'.repeat(1000) + 'a' + 'b' + 'c').length"), "1003"); + assert_eq!(eval_str("('x'.repeat(1000) + 'a' + 'b' + 'c').charCodeAt(1000)"), "97"); + assert_eq!(eval_str("('x'.repeat(1000) + 'a' + 'b' + 'c').charCodeAt(1002)"), "99"); +} + +#[test] +fn concat_n_single_and_two_operand_degrades() { + // 1/2 操作数不触达 CONCAT_N(emit 层退化 ADD),行为不变。 + let mut vm = Vm::new(); + assert_eq!(eval(&mut vm, "1").unwrap().as_int(), 1); + assert_eq!(eval(&mut vm, "1 + 2").unwrap().as_int(), 3); + let ab = eval(&mut vm, "\"a\" + \"b\"").unwrap(); + assert_eq!(to_str(&vm, ab), "ab"); +} + +#[test] +fn concat_n_emit_registers_bounded_for_long_chain() { + // 超长纯链部分摊平:CONCAT_N 摊平上限保证单点同时存活寄存器 ≤15,纯表达式 + // 链的寄存器复用属性保持(编译不因摊平而耗尽寄存器)。 + let allocator = oxide_parser::Allocator::default(); + let source = std::iter::repeat("1").take(40).collect::>().join(" + "); + let program = oxide_parser::parse(&allocator, &source).expect("parse ok"); + let module = Compiler::new().compile(&program).expect("compile ok"); + assert!(module.n_registers < 16, "long chain registers {}, expected < 16", module.n_registers); +}