From 08f061dfd345c56d7de9406387f523ba5e02ef4f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 6 Aug 2026 17:44:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=87=E4=BB=A4=E6=94=B9=E5=86=99=20?= =?UTF-8?q?+=20=E5=85=83=E6=95=B0=E6=8D=AE=E5=9B=9E=E5=86=99=20+=20alloc?= =?UTF-8?q?=20=E5=85=A5=E5=8F=A3=EF=BC=8CRegAlloc=20on/off=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E7=AD=89=E4=BB=B7=EF=BC=88D-16/D-20=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_regalloc/Cargo.toml | 3 +- crates/oxide_regalloc/src/finish.rs | 122 +++++ crates/oxide_regalloc/src/lib.rs | 108 ++++- crates/oxide_regalloc/src/rewrite.rs | 519 ++++++++++++++++++++++ crates/oxide_regalloc/tests/reg_count.rs | 92 ++++ crates/oxide_regalloc/tests/runtime_eq.rs | 135 ++++++ 6 files changed, 974 insertions(+), 5 deletions(-) create mode 100644 crates/oxide_regalloc/src/finish.rs create mode 100644 crates/oxide_regalloc/src/rewrite.rs create mode 100644 crates/oxide_regalloc/tests/reg_count.rs create mode 100644 crates/oxide_regalloc/tests/runtime_eq.rs diff --git a/crates/oxide_regalloc/Cargo.toml b/crates/oxide_regalloc/Cargo.toml index 7bb57cb6..ccffcd52 100644 --- a/crates/oxide_regalloc/Cargo.toml +++ b/crates/oxide_regalloc/Cargo.toml @@ -10,9 +10,10 @@ description = "OxideJS - register allocation pass over IR (interference graph + oxide_ir = { path = "../oxide_ir" } oxide_liveness = { path = "../oxide_liveness" } oxide_bytecode = { path = "../oxide_bytecode" } +oxide_cfg = { path = "../oxide_cfg" } [dev-dependencies] oxide_parser = { path = "../oxide_parser" } oxide_emit = { path = "../oxide_emit" } oxide_vm = { path = "../oxide_vm" } -oxide_cfg = { path = "../oxide_cfg" } +oxide_dce = { path = "../oxide_dce" } diff --git a/crates/oxide_regalloc/src/finish.rs b/crates/oxide_regalloc/src/finish.rs new file mode 100644 index 00000000..47e1dc75 --- /dev/null +++ b/crates/oxide_regalloc/src/finish.rs @@ -0,0 +1,122 @@ +//! 元数据回写:n_registers / builtin_reg_map / param_layout / upvalue_captures(D-10/D-03)。 +//! +//! - n_registers = map.phys_peak(≤253;不含参数窗口——窗口是调用点暂存槽, +//! CALL handler 在 push 前收集 args 进 Vec,n_registers 只控 save_stack 窗口) +//! - builtin_reg_map:Phys → 新号;Spill → spilled_builtin_bindings 的自由色 R(与 rewrite 同源) +//! - param_layout 不动(参数段 pre-colored 恒等);upvalue_captures 不动(escaped 预着色恒等) + +use crate::alloc_map::{Alloc, AllocMap}; +use crate::rewrite::spilled_builtin_bindings; +use oxide_ir::IRFunction; + +/// 回写 IRFunction 元数据(不动 insts——rewrite 已完成改写)。 +pub(super) fn run(f: &mut IRFunction, map: &AllocMap) { + // (a) n_registers = phys_peak + debug_assert!(map.phys_peak <= 253, "phys_peak 超 253"); + f.n_registers = map.phys_peak; + + // (b) builtin_reg_map 回写 + let spilled = spilled_builtin_bindings(f, map); + for (name, vreg) in &mut f.builtin_reg_map { + let v = *vreg; + match map.map.get(&v) { + Some(Alloc::Phys(p)) => { + *vreg = *p; + } + Some(Alloc::Spill(_)) => { + if let Some((_, r, _)) = spilled.iter().find(|(n, _, _)| n == name) { + debug_assert!(*r <= 253); + *vreg = *r; + } else { + debug_assert!(false, "spilled builtin {name} 无对应入口 SPILL"); + } + } + None => { + debug_assert!(false, "builtin vreg {v} 不在 AllocMap"); + } + } + } + + // (c) param_layout 不动(参数段 pre-colored 恒等;窗口与参数段重叠仅 05-07 标注的罕见情形) + let pl = f.param_layout; + debug_assert!(pl.base + pl.count <= map.arg_window_base || map.arg_window_base == 254); + + // (d) upvalue_captures 不动。enclosing_reg 是编译期产物(MAKE_CELL 定位用),VM 运行时 + // 不读(CREATE_CLOSURE 走 cell_idx,cell 捕获后值在 cell 中)——无需跨函数同步。 + // 直接槽引用(class field 计算键等)的 escaped vreg 已由 05-07 预着色恒等保留。 + // 注:cell 捕获变量的父 vreg 可被 RegAlloc 移动(MAKE_CELL 后值入 cell,register 无关), + // 此处不做恒等断言。 +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_ir::inst::Inst; + use oxide_ir::operand::Operand; + use oxide_bytecode::opcode::OpCode; + + #[test] + fn n_registers_equals_phys_peak() { + let mut f = IRFunction::new(); + let mut map = AllocMap::new(); + map.phys_peak = 42; + run(&mut f, &map); + assert_eq!(f.n_registers, 42); + } + + #[test] + fn builtin_moved_rewritten() { + let mut f = IRFunction::new(); + f.builtin_reg_map = vec![("Math".to_string(), 5)]; + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(1), Operand::None, Operand::None)); + let mut map = AllocMap::new(); + map.map.insert(5, Alloc::Phys(9)); + map.map.insert(1, Alloc::Phys(1)); + run(&mut f, &map); + assert_eq!(f.builtin_reg_map[0].1, 9, "builtin 移动后回写新 phys"); + } + + #[test] + fn builtin_spilled_bound_to_free_color() { + let mut f = IRFunction::new(); + f.builtin_reg_map = vec![("Math".to_string(), 5)]; + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(1), Operand::None, Operand::None)); + let mut map = AllocMap::new(); + map.map.insert(5, Alloc::Spill(0)); + map.map.insert(1, Alloc::Phys(1)); + map.map.insert(20, Alloc::Phys(7)); + map.spills.push(crate::alloc_map::SpillPlan { + vreg: 5, + slot: 0, + defs: vec![], + uses: vec![(0, 20)], + }); + let binds = spilled_builtin_bindings(&f, &map); + run(&mut f, &map); + assert_eq!(f.builtin_reg_map[0].0, "Math"); + assert_eq!(f.builtin_reg_map[0].1, binds[0].1, "回写 R 与入口 SPILL 同源"); + } + + #[test] + fn param_layout_untouched() { + let mut f = IRFunction::new(); + f.param_layout = oxide_ir::ParamLayout { base: 1, count: 2 }; + let map = AllocMap::new(); + run(&mut f, &map); + assert_eq!(f.param_layout.base, 1); + assert_eq!(f.param_layout.count, 2); + } + + #[test] + fn enclosing_reg_unchanged() { + let mut f = IRFunction::new(); + f.upvalue_captures.push(oxide_bytecode::module::UpvalueCapture { + name: "x".to_string(), + enclosing_reg: 7, + cell_idx: 0, + }); + let map = AllocMap::new(); + run(&mut f, &map); + assert_eq!(f.upvalue_captures[0].enclosing_reg, 7); + } +} diff --git a/crates/oxide_regalloc/src/lib.rs b/crates/oxide_regalloc/src/lib.rs index 1184d845..f60e3e2a 100644 --- a/crates/oxide_regalloc/src/lib.rs +++ b/crates/oxide_regalloc/src/lib.rs @@ -1,13 +1,16 @@ -//! RegAlloc 改写 pass(前半):干涉图染色决策,产出 AllocMap。 +//! RegAlloc 改写 pass:干涉图染色决策(前半)+ 指令改写/元数据回写(后半)。 //! //! 消费 `oxide_liveness::LiveInfo`(inst_live_before 逐指令活集,05-04 交付)与 -//! `oxide_ir::IRFunction`(param_layout 参数段 + nested escaped 收集),产出 AllocMap -//! 分析视图(D-01:pass 输出不住进 IR)。染色:干涉图(graph.rs)+ Kemp 简化/贪心/ -//! spill 区间拆分(color.rs)。**本模块不改写指令**——05-08 rewrite/finish 消费 AllocMap。 +//! `oxide_ir::IRFunction`(param_layout 参数段 + nested escaped 收集)。染色:干涉图 +//! (graph.rs)+ Kemp 简化/贪心/spill 区间拆分(color.rs)→ AllocMap;改写:rewrite.rs +//! (指令槽 vreg→phys 重写 + spill 插入 + 参数连续性 MOV + label 重建)+ finish.rs +//! (元数据回写)。`alloc()` 是完整改写 pass 入口(D-16),`color()` 只产决策。 mod alloc_map; mod color; +mod finish; mod graph; +mod rewrite; pub use alloc_map::{Alloc, AllocMap, FreshKind, FreshVreg, SpillPlan}; @@ -24,3 +27,100 @@ pub fn color(f: &IRFunction, live: &LiveInfo) -> Result { let graph = graph::build(f, live, &std::collections::BTreeSet::new(), &[]); color::run(f, live, &graph) } + +/// 寄存器分配改写:`&mut IRFunction + &LiveInfo → Result<(), String>`(D-16)。 +/// +/// 流程:LiveInfo 维度守卫(inst 数不符 → 内部重跑 build_cfg + liveness,D-17 修正: +/// 精确 DCE 删指令后 LiveInfo 过期,重跑纯函数一次调用,不重复建分析引擎)→ +/// `color()`(Err 原样传播)→ `rewrite::run` → `finish::run` → nested 递归自顶向下 +/// (每子函数独立 vreg 空间独立 alloc,D-03/D-10)。 +pub fn alloc(f: &mut IRFunction, live: &LiveInfo) -> Result<(), String> { + if f.insts.is_empty() && f.nested.is_empty() { + return Ok(()); + } + // LiveInfo 维度守卫:删改后的过期 LiveInfo 索引错位会误删活指令 + let live = if live.inst_live_before.len() == f.insts.len() && !f.insts.is_empty() { + live.clone() + } else { + let cfg = oxide_cfg::build_cfg(f); + oxide_liveness::liveness(f, &cfg) + }; + let map = color(f, &live)?; + rewrite::run(f, &map); + finish::run(f, &map); + for child in &mut f.nested { + let cfg = oxide_cfg::build_cfg(child); + let l = oxide_liveness::liveness(child, &cfg); + alloc(child, &l)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_bytecode::opcode::OpCode; + use oxide_ir::inst::Inst; + use oxide_ir::operand::Operand; + + #[test] + fn alloc_empty_ok() { + let mut f = IRFunction::new(); + assert!(alloc(&mut f, &LiveInfo::new()).is_ok()); + } + + #[test] + fn alloc_stale_live_reruns() { + let mut f = IRFunction::new(); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(3), Operand::Reg(1), Operand::Reg(2))); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None)); + // 空 LiveInfo(维度不符)→ 内部重跑 + let stale = LiveInfo::new(); + assert!(alloc(&mut f, &stale).is_ok()); + // 重写后全部槽 ≤253 + for inst in &f.insts { + for o in [&inst.rd, &inst.a, &inst.b] { + if let Operand::Reg(r) = o { + assert!(*r <= 253, "重写后槽 {r} 超 253"); + } + } + } + } + + #[test] + fn alloc_err_propagates() { + // 255 参数 CALL:窗口吞并 → k=0 → 无可行染色 → Err + let mut f = IRFunction::new(); + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3000), 255)); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + let err = alloc(&mut f, &live).unwrap_err(); + assert!(err.contains("too many registers"), "Err: {err}"); + } + + #[test] + fn alloc_recurses_nested() { + let mut f = IRFunction::new(); + f.insts.push(Inst::load_const(Operand::Reg(1), 0)); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(1), Operand::None, Operand::None)); + let mut sub = IRFunction::new(); + sub.insts.push(Inst::load_const(Operand::Reg(3), 0)); + sub.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None)); + f.nested.push(sub); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + alloc(&mut f, &live).unwrap(); + // 父与子函数全部槽 ≤253 + for inst in &f.insts { + for o in [&inst.rd, &inst.a, &inst.b] { + if let Operand::Reg(r) = o { + assert!(*r <= 253); + } + } + } + assert!(!f.nested[0].insts.is_empty(), "子函数也被重写"); + assert!(f.nested[0].n_registers <= 253, "子函数 n_registers 已回写"); + } +} + diff --git a/crates/oxide_regalloc/src/rewrite.rs b/crates/oxide_regalloc/src/rewrite.rs new file mode 100644 index 00000000..8b7c5967 --- /dev/null +++ b/crates/oxide_regalloc/src/rewrite.rs @@ -0,0 +1,519 @@ +//! 指令改写:消费 AllocMap 就地重写 IR(D-05/D-07/D-20/RESEARCH 发现 4)。 +//! +//! - 槽位 vreg→phys 重写:仅 `Operand::Reg`;None/This/NewTarget/Const/Imm/Label 零改动 +//! (None→0、This→254、NewTarget→255 由 lower 映射,D-11) +//! - spill 落点(D-05 区间拆分 v1):def 点后 SPILL、use 点前 UNSPILL;RMW(COMPOUND 系 +//! rd 读旧值)UNSPILL 进 def-fresh 寄存器、use-fresh 弃用 +//! - 调用点参数连续性(RESEARCH 发现 4):参数分散则 MOV 搬进 ARG_WINDOW +//! - spilled builtin 入口 SPILL(builtin 无指令 def,VM 入口隐式绑定) +//! - label_pos mark-sweep 重建:label 落 group 起点(跳转先执行 before-插入) +//! +//! 确定性:全 BTreeMap/Vec 排序,禁 HashMap(B010)。 + +use std::collections::BTreeMap; + +use oxide_bytecode::opcode::OpCode; +use oxide_ir::inst::Inst; +use oxide_ir::operand::Operand; +use oxide_ir::IRFunction; + +use crate::alloc_map::{Alloc, AllocMap}; + +/// 就地改写 f.insts + f.label_pos。 +pub(super) fn run(f: &mut IRFunction, map: &AllocMap) { + // ── 1. spill 查询表 ── + // defs_at: (inst, vreg) → def-fresh 色;uses_at: (inst, vreg) → use-fresh 色;spill_slot: vreg → slot + let mut defs_at: BTreeMap<(usize, u32), u32> = BTreeMap::new(); + let mut uses_at: BTreeMap<(usize, u32), u32> = BTreeMap::new(); + let mut spill_slot: BTreeMap = BTreeMap::new(); + for sp in &map.spills { + spill_slot.insert(sp.vreg, sp.slot); + for &(inst, fresh_id) in &sp.defs { + if let Some(Alloc::Phys(c)) = map.map.get(&fresh_id) { + defs_at.insert((inst, sp.vreg), *c); + } + } + for &(inst, fresh_id) in &sp.uses { + if let Some(Alloc::Phys(c)) = map.map.get(&fresh_id) { + uses_at.insert((inst, sp.vreg), *c); + } + } + } + + // ── 2. spilled builtin 入口 SPILL ── + let spilled_builtins = spilled_builtin_bindings(f, map); + + // ── 3. 单遍构建新 insts + old_to_new ── + let mut new_insts: Vec = Vec::new(); + let mut old_to_new: Vec = vec![0; f.insts.len()]; + // 入口 SPILL(先于所有 before-插入与 inst 0) + for (_, r, slot) in &spilled_builtins { + new_insts.push(Inst::inst_spill(Operand::Reg(*r), *slot)); + } + for (i, inst) in f.insts.iter().enumerate() { + old_to_new[i] = new_insts.len(); // group 起点(before-插入首条位置) + + // RMW 判定:inst 同时 def 且 use 同一 spilled vreg + let rmw_v = inst + .def_reg() + .filter(|d| uses_at.contains_key(&(i, *d)) && spill_slot.contains_key(d)); + + // before-插入:非 RMW 的 use 点 UNSPILL + let mut use_unspills: Vec<(u32, u32)> = Vec::new(); // (vreg, fresh 色) + for (&(inst_at, vreg), &color) in &uses_at { + if inst_at == i && rmw_v != Some(vreg) { + use_unspills.push((vreg, color)); + } + } + use_unspills.sort_unstable(); + + // 重写后该 inst 各槽的 spill 色映射(vreg → fresh 色) + let mut slot_color: BTreeMap = BTreeMap::new(); + if let Some(v) = rmw_v { + // RMW:UNSPILL 进 def-fresh 寄存器(rd 读旧值写新值),use-fresh 弃用 + if let Some(&c) = defs_at.get(&(i, v)) { + slot_color.insert(v, c); + } + } + for &(vreg, color) in &use_unspills { + slot_color.insert(vreg, color); + } + // 纯 def 点:rd → def-fresh 色(值写入后立即 SPILL) + if rmw_v.is_none() { + if let Some(d) = inst.def_reg() { + if let Some(&c) = defs_at.get(&(i, d)) { + slot_color.insert(d, c); + } + } + } + + // 重写 inst 本体(含调用点参数连续性 MOV) + let (rewritten, arg_movs) = rewrite_inst(f, inst, map, &slot_color, i); + + // 发射顺序:UNSPILLs(按 vreg 升序)→ RMW UNSPILL → 连续性 MOVs → 指令 → SPILLs + for &(vreg, color) in &use_unspills { + let slot = spill_slot[&vreg]; + new_insts.push(Inst::inst_unspill(Operand::Reg(color), slot)); + } + if let Some(v) = rmw_v { + if let Some(&c) = defs_at.get(&(i, v)) { + let slot = spill_slot[&v]; + new_insts.push(Inst::inst_unspill(Operand::Reg(c), slot)); + } + } + for (dst, src) in arg_movs { + new_insts.push(Inst::inst_mov(Operand::Reg(dst), Operand::Reg(src))); + } + new_insts.push(rewritten); + + // after-插入:def 点 SPILL(RMW 与纯 def 都写 rd;terminator 跳过——def 结果必死) + if let Some(d) = inst.def_reg() { + if spill_slot.contains_key(&d) { + if let Some(&c) = defs_at.get(&(i, d)) { + if !is_terminator(inst.op) { + let slot = spill_slot[&d]; + new_insts.push(Inst::inst_spill(Operand::Reg(c), slot)); + } + } + } + } + } + + f.insts = new_insts; + + // ── 4. label_pos 重映射(label 落 group 起点)── + for pos in f.label_pos.iter_mut() { + if let Some(old) = *pos { + *pos = old_to_new.get(old).copied(); + } + } + debug_assert!(f.label_pos.iter().all(|p| p.map_or(true, |np| np <= f.insts.len()))); +} + +/// 重写单条指令:槽位映射 + 调用点参数连续性 MOV 补位。返回 (重写后指令, 参数 MOV 列表)。 +fn rewrite_inst( + f: &IRFunction, inst: &Inst, map: &AllocMap, slot_color: &BTreeMap, i: usize, +) -> (Inst, Vec<(u32, u32)>) { + let nargs = inst.ext.first().copied().unwrap_or(0); + let first_arg = match inst.op { + OpCode::CALL | OpCode::CALL_NATIVE | OpCode::NEW_EXPRESSION => Some(inst.b), + OpCode::SUPER_CALL => Some(inst.a), + _ => None, + }; + + // 调用点参数 phys 映射(arg k → 物理号) + let mut arg_movs: Vec<(u32, u32)> = Vec::new(); + let mut arg_base: Option = None; + if let Some(Operand::Reg(fr)) = first_arg { + if nargs > 0 { + let mut args_phys: Vec = Vec::with_capacity(nargs as usize); + for k in 0..nargs { + let v = fr + k; + let p = if let Some(&c) = slot_color.get(&v) { + c + } else { + match map.map.get(&v) { + Some(Alloc::Phys(p)) => *p, + _ => v, // 非真实 vreg / 防御:原样 + } + }; + args_phys.push(p); + } + // 连续性检查:phys[k+1] == phys[k] + 1 + let contiguous = args_phys.windows(2).all(|w| w[1] == w[0] + 1); + if !contiguous { + for (k, &p) in args_phys.iter().enumerate() { + arg_movs.push((map.arg_window_base + k as u32, p)); + } + arg_base = Some(map.arg_window_base); + } + } + } + let _ = i; + + // 槽位重写 + let rewrite = |o: Operand| -> Operand { + match o { + Operand::Reg(r) => { + if let Some(&c) = slot_color.get(&r) { + Operand::Reg(c) + } else { + match map.map.get(&r) { + Some(Alloc::Phys(p)) => Operand::Reg(*p), + Some(Alloc::Spill(_)) => { + // spilled vreg 出现在非 def/use 点(不应发生,防御保留原样) + debug_assert!(false, "spilled vreg {r} 出现在无插入点"); + Operand::Reg(r) + } + None => Operand::Reg(r), + } + } + } + other => other, // None/This/NewTarget/Const/Imm/Label 零改动(D-11) + } + }; + + let mut rd = rewrite(inst.rd); + let mut a = rewrite(inst.a); + let mut b = rewrite(inst.b); + // 调用点首参槽改指 arg_window_base(桥接后) + if let Some(ab) = arg_base { + match inst.op { + OpCode::CALL | OpCode::CALL_NATIVE | OpCode::NEW_EXPRESSION => { + if matches!(first_arg, Some(Operand::Reg(_))) { + b = Operand::Reg(ab); + } + } + OpCode::SUPER_CALL => { + if matches!(first_arg, Some(Operand::Reg(_))) { + a = Operand::Reg(ab); + } + } + _ => {} + } + } + let _ = &mut rd; + let _ = &mut a; + let _ = &mut b; + let _ = f; + (Inst { op: inst.op, rd, a, b, ext: inst.ext.clone() }, arg_movs) +} + +/// terminator 判定:跳转族 + RETURN/HALT/THROW(def 结果必死,SPILL 跳过)。 +fn is_terminator(op: OpCode) -> bool { + matches!( + op, + OpCode::JMP + | OpCode::JMP_IF_TRUE + | OpCode::JMP_IF_FALSE + | OpCode::JMP_IF_NULLISH + | OpCode::RETURN + | OpCode::HALT + | OpCode::THROW + ) +} + +/// spilled builtin:对 builtin_reg_map 中 map[v]=Spill 的项分配确定性自由色 R。 +/// 返回 (name, R, slot)。rewrite(入口 SPILL)与 finish(回写)共享唯一来源。 +pub(super) fn spilled_builtin_bindings(f: &IRFunction, map: &AllocMap) -> Vec<(String, u32, u16)> { + // 候选色排除集 + let mut excluded: Vec = Vec::new(); + for (v, a) in &map.map { + if let Alloc::Phys(c) = a { + excluded.push(*c); + } + let _ = v; + } + let pl = f.param_layout; + for c in pl.base..pl.base + pl.count { + excluded.push(c); + } + // escaped 色(递归 nested 的 LOAD_VAR.a / STORE_VAR.rd) + collect_escaped(&f.nested, &mut excluded); + // 窗口 + for c in map.arg_window_base..=253 { + excluded.push(c); + } + excluded.sort_unstable(); + excluded.dedup(); + let excluded_set: std::collections::BTreeSet = excluded.iter().copied().collect(); + + let mut result: Vec<(String, u32, u16)> = Vec::new(); + let mut used: Vec = Vec::new(); + // 按 name 升序确定性 + let mut candidates: Vec<&(String, u32)> = f + .builtin_reg_map + .iter() + .filter(|(_, v)| matches!(map.map.get(v), Some(Alloc::Spill(_)))) + .collect(); + candidates.sort_by(|a, b| a.0.cmp(&b.0)); + for (name, vreg) in candidates { + let slot = match map.map.get(vreg) { + Some(Alloc::Spill(s)) => *s, + _ => continue, + }; + // 自由色 = 1..=253 中最小未排除未使用 + let r = (1u32..=253) + .find(|c| !excluded_set.contains(c) && !used.contains(c)) + .unwrap_or_else(|| { + debug_assert!(false, "spilled builtin 无自由色"); + 1 + }); + used.push(r); + result.push((name.clone(), r, slot)); + } + result +} + +/// 递归收集 nested 树中变量槽引用(LOAD_VAR.a 读槽、STORE_VAR.rd 写槽)→ escaped 色。 +fn collect_escaped(nested: &[IRFunction], out: &mut Vec) { + for sub in nested { + for inst in &sub.insts { + let slot = match inst.op { + OpCode::LOAD_VAR => inst.a, + OpCode::STORE_VAR => inst.rd, + _ => Operand::None, + }; + if let Operand::Reg(r) = slot { + out.push(r); + } + } + collect_escaped(&sub.nested, out); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_ir::operand::Operand; + + fn empty_function() -> IRFunction { + IRFunction::new() + } + + fn rewrite_with_map(insts: Vec, map: AllocMap) -> IRFunction { + let mut f = empty_function(); + f.insts = insts; + run(&mut f, &map); + f + } + + #[test] + fn semantic_operands_untouched() { + let mut f = empty_function(); + f.insts.push(Inst::new(OpCode::ADD, Operand::This, Operand::NewTarget, Operand::None)); + f.insts.push(Inst::load_const(Operand::Reg(1), 3)); + let map = AllocMap::new(); + run(&mut f, &map); + assert_eq!(f.insts[0].rd, Operand::This); + assert_eq!(f.insts[0].a, Operand::NewTarget); + assert_eq!(f.insts[0].b, Operand::None); + } + + #[test] + fn slots_rewritten_per_map() { + let mut f = empty_function(); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(3), Operand::Reg(1), Operand::Reg(2))); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None)); + let mut map = AllocMap::new(); + map.map.insert(1, Alloc::Phys(10)); + map.map.insert(2, Alloc::Phys(11)); + map.map.insert(3, Alloc::Phys(12)); + run(&mut f, &map); + assert_eq!(f.insts[0].rd, Operand::Reg(12)); + assert_eq!(f.insts[0].a, Operand::Reg(10)); + assert_eq!(f.insts[0].b, Operand::Reg(11)); + assert_eq!(f.insts[1].rd, Operand::Reg(12)); + } + + #[test] + fn scattered_args_insert_mov_bridge() { + let insts = vec![Inst::call(Operand::Reg(2), Operand::Reg(3), Operand::Reg(4), 3)]; + let mut map = AllocMap::new(); + map.arg_window_base = 251; + map.map.insert(2, Alloc::Phys(2)); + map.map.insert(3, Alloc::Phys(3)); + map.map.insert(4, Alloc::Phys(7)); + map.map.insert(5, Alloc::Phys(9)); + map.map.insert(6, Alloc::Phys(8)); + let f = rewrite_with_map(insts, map); + // 期望:3 条 MOV(251←7, 252←9, 253←8)+ 调用点 b 槽 → Reg(251) + let movs: Vec<&Inst> = f.insts.iter().filter(|i| i.op == OpCode::MOV).collect(); + assert_eq!(movs.len(), 3, "分散参数应插 3 条 MOV"); + let call = f.insts.iter().find(|i| i.op == OpCode::CALL).unwrap(); + assert_eq!(call.b, Operand::Reg(251), "调用点 b 槽应改指窗口基址"); + // MOV 源分别为 7/9/8,目标 251/252/253 + let mv: Vec<(u32, u32)> = movs + .iter() + .map(|i| match (i.rd, i.a) { + (Operand::Reg(d), Operand::Reg(s)) => (d, s), + _ => panic!("MOV 槽位异常"), + }) + .collect(); + assert!(mv.contains(&(251, 7)) && mv.contains(&(252, 9)) && mv.contains(&(253, 8))); + } + + #[test] + fn spill_points_inserted_at_defs_and_uses() { + let insts = vec![ + Inst::load_const(Operand::Reg(1), 0), + Inst::load_const(Operand::Reg(2), 0), + Inst::new(OpCode::ADD, Operand::Reg(3), Operand::Reg(1), Operand::Reg(2)), + Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None), + ]; + let mut map = AllocMap::new(); + map.map.insert(1, Alloc::Spill(0)); + map.map.insert(2, Alloc::Phys(2)); + map.map.insert(3, Alloc::Phys(3)); + map.map.insert(10, Alloc::Phys(5)); // def-fresh + map.map.insert(11, Alloc::Phys(6)); // use-fresh + map.spills.push(crate::alloc_map::SpillPlan { + vreg: 1, + slot: 0, + defs: vec![(0, 10)], + uses: vec![(2, 11)], + }); + let f = rewrite_with_map(insts, map); + // inst 0 后 SPILL(Reg(5), 0);inst 2 前 UNSPILL(Reg(6), 0);inst 2 的 a 槽(r1) → Reg(6) + let ops: Vec = f.insts.iter().map(|i| i.op).collect(); + // 找 SPILL 与 UNSPILL 位置 + let spill_pos = ops.iter().position(|&o| o == OpCode::SPILL).unwrap(); + let unspill_pos = ops.iter().position(|&o| o == OpCode::UNSPILL).unwrap(); + assert_eq!(f.insts[spill_pos].rd, Operand::Reg(5)); + assert_eq!(f.insts[unspill_pos].rd, Operand::Reg(6)); + // 指令序:UNSPILL 在 ADD 前 + let add_pos = ops.iter().position(|&o| o == OpCode::ADD).unwrap(); + assert!(unspill_pos < add_pos, "UNSPILL 应在 ADD 前"); + let add = &f.insts[add_pos]; + assert_eq!(add.a, Operand::Reg(6), "ADD 的 a 槽(r1) → use-fresh 色 6"); + } + + #[test] + fn rmw_spill_reloads_def_fresh() { + // COMPOUND_ADD RMW(r1 读旧值写新值)→ def-fresh 5;RETURN 读新值 → use-fresh 8 + let insts = vec![ + Inst::new(OpCode::COMPOUND_ADD, Operand::Reg(1), Operand::Reg(2), Operand::None), + Inst::new(OpCode::RETURN, Operand::Reg(1), Operand::None, Operand::None), + ]; + let mut map = AllocMap::new(); + map.map.insert(1, Alloc::Spill(0)); + map.map.insert(2, Alloc::Phys(2)); + map.map.insert(5, Alloc::Phys(5)); // def-fresh + map.map.insert(6, Alloc::Phys(6)); // use-fresh@0(弃用) + map.map.insert(7, Alloc::Phys(8)); // use-fresh@1(RETURN) + map.spills.push(crate::alloc_map::SpillPlan { + vreg: 1, + slot: 0, + defs: vec![(0, 5)], + uses: vec![(0, 6), (1, 7)], + }); + let f = rewrite_with_map(insts, map); + let ops: Vec = f.insts.iter().map(|i| i.op).collect(); + let unspill_pos = ops.iter().position(|&o| o == OpCode::UNSPILL).unwrap(); + let compound_pos = ops.iter().position(|&o| o == OpCode::COMPOUND_ADD).unwrap(); + let spill_pos = ops.iter().position(|&o| o == OpCode::SPILL).unwrap(); + let ret_pos = ops.iter().position(|&o| o == OpCode::RETURN).unwrap(); + // UNSPILL(def-fresh 5) → COMPOUND(rd=5) → SPILL(5);RETURN 前 UNSPILL(use-fresh 8) + assert_eq!(f.insts[unspill_pos].rd, Operand::Reg(5)); + assert!(unspill_pos < compound_pos && compound_pos < spill_pos); + assert_eq!(f.insts[compound_pos].rd, Operand::Reg(5), "COMPOUND rd → def-fresh 色"); + assert_eq!(f.insts[spill_pos].rd, Operand::Reg(5)); + assert!(spill_pos < ret_pos, "SPILL 在 RETURN 前"); + // RETURN rd → use-fresh 8,其前有 UNSPILL(8) + assert_eq!(f.insts[ret_pos].rd, Operand::Reg(8)); + let unspill8 = f.insts.iter().position(|i| i.op == OpCode::UNSPILL && i.rd == Operand::Reg(8)).unwrap(); + assert!(unspill8 < ret_pos, "RETURN 前 UNSPILL use-fresh"); + // use-fresh 6(RMW 点弃用)不得产生 UNSPILL + assert!( + f.insts.iter().all(|i| !(i.op == OpCode::UNSPILL && i.rd == Operand::Reg(6))), + "RMW use-fresh 弃用" + ); + } + + #[test] + fn label_pos_lands_on_group_start() { + // 1: JMP_IF_FALSE(Reg(9), Label(0)) → 跳转目标 inst 3 是 spill use 点(有 before-UNSPILL) + let insts = vec![ + Inst::load_const(Operand::Reg(2), 0), + Inst::jmp_if_false(9, 0), + Inst::load_const(Operand::Reg(3), 0), + Inst::new(OpCode::ADD, Operand::Reg(4), Operand::Reg(1), Operand::Reg(2)), + Inst::new(OpCode::RETURN, Operand::Reg(4), Operand::None, Operand::None), + ]; + let mut f = empty_function(); + f.insts = insts; + f.label_pos = vec![Some(3)]; + f.label_count = 1; + let mut map = AllocMap::new(); + map.map.insert(1, Alloc::Spill(0)); + map.map.insert(2, Alloc::Phys(2)); + map.map.insert(3, Alloc::Phys(3)); + map.map.insert(4, Alloc::Phys(4)); + map.map.insert(9, Alloc::Phys(9)); + map.map.insert(11, Alloc::Phys(6)); // use-fresh for r1 at inst 3 + map.spills.push(crate::alloc_map::SpillPlan { + vreg: 1, + slot: 0, + defs: vec![], + uses: vec![(3, 11)], + }); + run(&mut f, &map); + // label 0 应指向 inst 3 的 group 起点(UNSPILL 位置) + let label_target = f.label_pos[0].expect("label 重映射"); + let target_inst = &f.insts[label_target]; + assert_eq!(target_inst.op, OpCode::UNSPILL, "label 落 group 起点(UNSPILL)"); + // 跳转 offset 落在 UNSPILL 前 → lower 成功 + let result = oxide_ir::lower::lower(&f); + assert!(result.is_ok(), "lower 应成功(label 解析到 group 起点)"); + } + + #[test] + fn spilled_builtin_entry_spill_inserted() { + let mut f = empty_function(); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + f.builtin_reg_map = vec![("Math".to_string(), 10)]; + let mut map = AllocMap::new(); + map.map.insert(10, Alloc::Spill(0)); + map.map.insert(5, Alloc::Phys(5)); + map.map.insert(20, Alloc::Phys(7)); // use-fresh + map.spills.push(crate::alloc_map::SpillPlan { + vreg: 10, + slot: 0, + defs: vec![], + uses: vec![(0, 20)], + }); + let binds = spilled_builtin_bindings(&f, &map); + assert_eq!(binds.len(), 1); + let (name, r, slot) = &binds[0]; + assert_eq!(name, "Math"); + assert_eq!(*slot, 0); + assert!(*r >= 1 && *r <= 253, "R 应在物理域"); + assert_ne!(*r, 5, "R 不得与 Phys 色冲突"); + run(&mut f, &map); + // 入口 SPILL 存在 + let first = f.insts.first().expect("入口 SPILL"); + assert_eq!(first.op, OpCode::SPILL); + assert_eq!(first.rd, Operand::Reg(*r)); + assert_eq!(first.ext.as_slice(), &[0]); + } +} diff --git a/crates/oxide_regalloc/tests/reg_count.rs b/crates/oxide_regalloc/tests/reg_count.rs new file mode 100644 index 00000000..a4b8729b --- /dev/null +++ b/crates/oxide_regalloc/tests/reg_count.rs @@ -0,0 +1,92 @@ +//! REG-04 断言:RegAlloc on ≤253 且 < off(寄存器收缩);B005 大函数 on 编译成功。 + +fn n_registers(src: &str, regalloc: bool) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, src).expect("parse failed"); + let mut ir = oxide_emit::Emitter::new().emit_program(&program).expect("emit failed"); + oxide_dce::dce(&mut ir); + if regalloc { + let cfg = oxide_cfg::build_cfg(&ir); + let live = oxide_liveness::liveness(&ir, &cfg); + oxide_dce::dce_precise(&mut ir, &live); + let cfg2 = oxide_cfg::build_cfg(&ir); + let live2 = oxide_liveness::liveness(&ir, &cfg2); + oxide_regalloc::alloc(&mut ir, &live2)?; + } + let module = oxide_ir::lower::lower(&ir)?; + // 返回模块树最大 n_registers(嵌套函数是实际寄存器压力来源) + Ok(module_max_n_registers(&module)) +} + +fn module_max_n_registers(m: &oxide_bytecode::module::CompiledModule) -> u8 { + let mut max = m.n_registers; + for sub in &m.sub_modules { + max = max.max(module_max_n_registers(sub)); + } + max +} + +fn gen_sum_function(n: u32) -> String { + let mut src = String::from("function f() { "); + for i in 0..n { + src.push_str(&format!("var v{i} = {i}; ")); + } + src.push_str("return "); + for i in 0..n { + if i > 0 { + src.push('+'); + } + src.push_str(&format!("v{i}")); + } + src.push_str("; } f();"); + src +} + +/// 长活 L + n 次短活:L 干扰 n 个短活临时值 → L degree ≥ k → spill L → fresh 单点可着色。 +fn gen_spill_function(n: u32) -> String { + let mut src = String::from("function f(a) { var L = a; var s = 0; "); + for _ in 0..n { + src.push_str("s = s + L; "); + } + src.push_str("return s; } f(1);"); + src +} + +/// 中等函数 on ≤253 且 < off(寄存器复用收缩)。 +#[test] +fn n_registers_shrinks_with_regalloc() { + let samples = [ + gen_sum_function(50), + "function f(a, b, c) { return a * 100 + b * 10 + c; } function g() { return f(1, 2, 3); } g();".to_string(), + "function f(n) { var s = 0; for (var i = 0; i < n; i++) { s += i * i; } return s; } f(10);".to_string(), + "function f() { try { throw 1; } catch (e) { return e + 1; } } f();".to_string(), + "function f() { var x = 1; return function() { return x++; }; } var g = f(); g();".to_string(), + ]; + for src in &samples { + let on = n_registers(src, true).expect("on 应成功"); + let off = n_registers(src, false).expect("off 应成功"); + assert!(on <= 253, "on ≤253: {on} for {src}"); + assert!(on < off, "on({on}) < off({off}) for {src}"); + } +} + +/// B005 大函数:on 编译成功 ≤253;off 报 RangeError(B005 解决证据)。 +#[test] +fn b005_large_function_fits_in_253() { + let src = gen_sum_function(220); + let on = n_registers(&src, true); + assert!(on.is_ok(), "B005 样例 on 应编译成功"); + assert!(on.unwrap() <= 253, "on ≤253"); + let off = n_registers(&src, false); + assert!(off.is_err(), "B005 样例 off 应报 RangeError"); + assert!(off.unwrap_err().contains("too many registers"), "off Err 消息"); +} + +/// spill 压力样例(长活 L + 254 短活)on ≤253 且成功。 +#[test] +fn spill_sample_under_limit() { + let src = gen_spill_function(254); + let on = n_registers(&src, true); + assert!(on.is_ok(), "spill 样例 on 应编译成功: {on:?}"); + assert!(on.unwrap() <= 253, "on ≤253"); +} diff --git a/crates/oxide_regalloc/tests/runtime_eq.rs b/crates/oxide_regalloc/tests/runtime_eq.rs new file mode 100644 index 00000000..bcc0b5aa --- /dev/null +++ b/crates/oxide_regalloc/tests/runtime_eq.rs @@ -0,0 +1,135 @@ +//! RegAlloc on/off 运行时等价(D-21 主验证)+ B005/spill 样例运行正确(REG-04)。 +//! +//! 同一 JS 源码走两条管线:parse → emit → dce(保守恒定)→(regalloc on/off)→ lower → run, +//! 对比顶层执行结果。**不经 Compiler::compile**(测试层直调底层函数)。 + +use oxide_bytecode::opcode::OpCode; +use oxide_vm::vm::Vm; +use oxide_vm::JsValue; + +/// parse→emit→dce→(regalloc?)→lower→run 全链路。 +fn run_source(src: &str, regalloc: bool) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, src).expect("parse failed"); + let mut ir = oxide_emit::Emitter::new().emit_program(&program).expect("emit failed"); + oxide_dce::dce(&mut ir); + if regalloc { + let cfg = oxide_cfg::build_cfg(&ir); + let live = oxide_liveness::liveness(&ir, &cfg); + oxide_dce::dce_precise(&mut ir, &live); + let cfg2 = oxide_cfg::build_cfg(&ir); + let live2 = oxide_liveness::liveness(&ir, &cfg2); + oxide_regalloc::alloc(&mut ir, &live2)?; + } + let module = oxide_ir::lower::lower(&ir).expect("lower failed"); + let mut vm = Vm::new(); + vm.run(&module) +} + +/// 结果规约成可断言形态:`(是否 Ok, 字符串化结果或错误消息)`。 +fn normalize(r: Result) -> (bool, String) { + match r { + Ok(v) => (true, v.to_string()), + Err(e) => (false, e), + } +} + +/// 主等价断言:RegAlloc on/off 运行时结果一致。 +#[test] +fn regalloc_preserves_semantics() { + let samples = [ + // switch+const(B011 回归) + "function f(x) { switch (x) { case 1: break; } const c = 5; return c; } f(1);", + // 类字段(B012 行为锚) + "var x = 0; class C { p = (x = 1); } new C(); x;", + // try-catch + "function f() { try { throw 1; } catch (e) { return e + 1; } } f();", + // 嵌套调用(参数连续性压力) + "function f(a, b, c) { return a * 100 + b * 10 + c; } function g() { return f(1, 2, 3) + f(4, 5, 6); } g();", + // 循环(label 重指向压力) + "function f(n) { var s = 0; for (var i = 0; i < n; i++) { s += i * i; } return s; } f(10);", + // 解构 + "function f() { var [a, b] = [1, 2]; var { c, d } = { c: 3, d: 4 }; return a + b + c + d; } f();", + // 闭包(upvalue cell 路径) + "function f() { var x = 1; return function() { return x++; }; } var g = f(); var r = [g(), g(), g()]; r[0] + r[1] + r[2];", + // 箭头函数 + "var f = (a, b) => a * b; f(6, 7);", + // 内置函数(builtin vreg 移动) + "function f(a) { return Math.max(a, Math.min(2, 3)); } f(5);", + // 递归 + "function f(n) { return n <= 1 ? 1 : n * f(n - 1); } f(8);", + // const 重赋值(两跑同抛) + "const x = 1; x = 2;", + ]; + for src in samples { + let off = normalize(run_source(src, false)); + let on = normalize(run_source(src, true)); + assert_eq!(on, off, "RegAlloc on/off 语义不一致: {src}"); + } +} + +/// 生成 N 个变量的求和函数源码(程序化,结果确定 = N*(N-1)/2)。 +fn gen_sum_function(n: u32) -> String { + let mut src = String::from("function f() { "); + for i in 0..n { + src.push_str(&format!("var v{i} = {i}; ")); + } + src.push_str("return "); + for i in 0..n { + if i > 0 { + src.push('+'); + } + src.push_str(&format!("v{i}")); + } + src.push_str("; } f();"); + src +} + +/// 长活 L + n 次短活:L 干扰 n 个短活临时值 → 触发 spill(L degree ≥ k)。 +fn gen_spill_function(n: u32) -> String { + let mut src = String::from("function f(a) { var L = a; var s = 0; "); + for _ in 0..n { + src.push_str("s = s + L; "); + } + src.push_str("return s; } f(1);"); + src +} + +/// B005 大函数(200+ 变量)与 spill 压力样例 on 运行正确。 +/// off 路径 lower 失败(B005 上限)——不能做 on/off 相等,on 运行正确即 REG-04 证据。 +#[test] +fn b005_and_spill_samples_run_correctly() { + // B005:220 变量 → vreg 数远超 253,但同时存活 < 253 → RegAlloc 复用解决 + let src_b005 = gen_sum_function(220); + let expected_b005 = 220 * 219 / 2; + let out = run_source(&src_b005, true).expect("B005 样例 on 应编译成功"); + assert_eq!(out.to_string(), expected_b005.to_string(), "B005 样例结果错误"); + + // spill 压力:长活 L + 254 短活 → L 溢出,验证 SPILL/UNSPILL 真实发生且结果正确 + let src_spill = gen_spill_function(254); + let out = run_source(&src_spill, true).expect("spill 样例 on 应编译成功"); + assert_eq!(out.to_string(), "254", "spill 样例结果错误"); + + // 字节码扫描:spill 样例经 alloc + lower 后应含 SPILL 与 UNSPILL opcode + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, &src_spill).expect("parse"); + let mut ir = oxide_emit::Emitter::new().emit_program(&program).expect("emit"); + oxide_dce::dce(&mut ir); + let cfg = oxide_cfg::build_cfg(&ir); + let live = oxide_liveness::liveness(&ir, &cfg); + oxide_dce::dce_precise(&mut ir, &live); + let cfg2 = oxide_cfg::build_cfg(&ir); + let live2 = oxide_liveness::liveness(&ir, &cfg2); + oxide_regalloc::alloc(&mut ir, &live2).expect("alloc"); + let module = oxide_ir::lower::lower(&ir).expect("lower"); + // 递归扫描模块树(spill 发生在嵌套函数 f 中,非顶层 f(1) 调用) + fn has_op(m: &oxide_bytecode::module::CompiledModule, target: OpCode) -> bool { + if m.bytecode.iter().any(|&instr| OpCode::try_from(instr as u8).ok() == Some(target)) { + return true; + } + m.sub_modules.iter().any(|s| has_op(s, target)) + } + let has_spill = has_op(&module, OpCode::SPILL); + let has_unspill = has_op(&module, OpCode::UNSPILL); + assert!(has_spill && has_unspill, "spill 样例应真实插入 SPILL/UNSPILL"); +}