perf: CONCAT_N 多操作数拼接单趟精确预分配替代逐次 ADD

新增 CONCAT_N opcode(0x93):连续加号左结合链摊平为单条指令,
字符串模式单趟精确总长预分配,消除 N-2 次中间串分配与逐次整串重拷贝。

- emit 只下钻 left 链、right 括号不拆(a+(b+c) 退化 ADD),求值序与
  f64 结合性保持左结合原义;摊平上限 14 防超长链寄存器压力
- dispatch 两阶段:数值前缀逐项复用 ADD 判定序列(int 溢出升 double、
  BigInt 混合 TypeError、对象 ToPrimitive 副作用序),遇字符串转入
  字符串模式单趟预分配拼接(concat_n_strings)
- RegAlloc graph/rewrite 登记 CONCAT_N ext 操作数(漏收集即错值);
  ic_helper ext_word_count 登记(clear_ic_caches 字节定位)
- contract golden 显式 CONCAT_N 分支(规避 canonical_inst _ 兜底)
This commit is contained in:
2026-08-15 09:08:05 +08:00
parent aa4c28cdf0
commit 38a4f20743
10 changed files with 423 additions and 0 deletions
+7
View File
@@ -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");
@@ -30,6 +30,25 @@ impl Emitter {
fn emit_binary_expression(
&self, bin: &oxide_parser::BinaryExpression, ctx: &mut CompileCtx,
) -> Result<u32, String> {
// 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), &regs));
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);
}
+3
View File
@@ -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);
+28
View File
@@ -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]);
}
}
+27
View File
@@ -86,6 +86,16 @@ pub(super) fn collect_real_vregs(f: &IRFunction) -> BTreeSet<u32> {
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} 建节点");
}
}
}
+26
View File
@@ -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)];
+115
View File
@@ -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<JsValue, String> {
let mut parts = SmallVec::<[JsValue; 8]>::new();
parts.push(lhs);
parts.push(rhs);
for &reg 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]);
+20
View File
@@ -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");
}
}
+4
View File
@@ -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);
}
+157
View File
@@ -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<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}"))?;
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::<Vec<_>>().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);
}