From 31bba82912adf449ed86ce8d1147e1ab059a2a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Thu, 6 Aug 2026 17:13:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(regalloc):=20=E6=96=B0=E5=A2=9E=20oxide=5F?= =?UTF-8?q?regalloc=20crate=EF=BC=8C=E5=B9=B2=E6=B6=89=E5=9B=BE=20+=20grap?= =?UTF-8?q?h=20coloring=20+=20spill=20=E6=8B=86=E5=88=86=EF=BC=88D-09/D-10?= =?UTF-8?q?/D-11=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 15 ++ Cargo.toml | 1 + crates/oxide_regalloc/Cargo.toml | 18 ++ crates/oxide_regalloc/src/alloc_map.rs | 87 +++++++ crates/oxide_regalloc/src/color.rs | 330 +++++++++++++++++++++++++ crates/oxide_regalloc/src/graph.rs | 297 ++++++++++++++++++++++ crates/oxide_regalloc/src/lib.rs | 26 ++ crates/oxide_regalloc/tests/smoke.rs | 102 ++++++++ 8 files changed, 876 insertions(+) create mode 100644 crates/oxide_regalloc/Cargo.toml create mode 100644 crates/oxide_regalloc/src/alloc_map.rs create mode 100644 crates/oxide_regalloc/src/color.rs create mode 100644 crates/oxide_regalloc/src/graph.rs create mode 100644 crates/oxide_regalloc/src/lib.rs create mode 100644 crates/oxide_regalloc/tests/smoke.rs diff --git a/Cargo.lock b/Cargo.lock index 20f1053..a96cd79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,6 +1139,7 @@ dependencies = [ "oxide_cfg", "oxide_emit", "oxide_ir", + "oxide_liveness", "oxide_parser", "oxide_vm", ] @@ -1207,6 +1208,19 @@ dependencies = [ "oxc_syntax", ] +[[package]] +name = "oxide_regalloc" +version = "0.1.0" +dependencies = [ + "oxide_bytecode", + "oxide_cfg", + "oxide_emit", + "oxide_ir", + "oxide_liveness", + "oxide_parser", + "oxide_vm", +] + [[package]] name = "oxide_runtime_api" version = "0.1.0" @@ -1254,6 +1268,7 @@ dependencies = [ "oxide_builtins", "oxide_bytecode", "oxide_compiler", + "oxide_ir", "oxide_kernel", "oxide_log", "oxide_parser", diff --git a/Cargo.toml b/Cargo.toml index 5a63569..8ddfdd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/oxide_cfg", "crates/oxide_dce", "crates/oxide_liveness", + "crates/oxide_regalloc", "crates/oxide_types", "crates/oxide_vm", "crates/oxide_kernel", diff --git a/crates/oxide_regalloc/Cargo.toml b/crates/oxide_regalloc/Cargo.toml new file mode 100644 index 0000000..7bb57cb --- /dev/null +++ b/crates/oxide_regalloc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oxide_regalloc" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "OxideJS - register allocation pass over IR (interference graph + graph coloring + spill splitting)" + +[dependencies] +oxide_ir = { path = "../oxide_ir" } +oxide_liveness = { path = "../oxide_liveness" } +oxide_bytecode = { path = "../oxide_bytecode" } + +[dev-dependencies] +oxide_parser = { path = "../oxide_parser" } +oxide_emit = { path = "../oxide_emit" } +oxide_vm = { path = "../oxide_vm" } +oxide_cfg = { path = "../oxide_cfg" } diff --git a/crates/oxide_regalloc/src/alloc_map.rs b/crates/oxide_regalloc/src/alloc_map.rs new file mode 100644 index 0000000..2534278 --- /dev/null +++ b/crates/oxide_regalloc/src/alloc_map.rs @@ -0,0 +1,87 @@ +//! AllocMap:寄存器染色决策输出视图(D-01:pass 输出,不住进 IR)。 +//! +//! `AllocMap.map` 含全部真实 + fresh vreg 的 Phys/Spill 去向;`spills` 为 +//! SPILL/UNSPILL 插入决策表(05-08 rewrite 消费);`phys_peak` 回写 n_registers; +//! `arg_window_base` 供调用点参数连续性 MOV 补位。BTreeMap 保确定性(B010,禁 HashMap)。 + +use std::collections::BTreeMap; + +/// vreg 最终去向:Phys(物理色) / Spill(spill 栈帧内 slot)。 +/// Phys 色域 1..=253(预着色节点可含保留号之外的值,见 graph.rs)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Alloc { + Phys(u32), + Spill(u16), +} + +/// 拆分 fresh vreg 的语义:Def = SPILL 在 def 指令后插入,Use = UNSPILL 在 use 指令前插入。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FreshKind { + Def, + Use, +} + +/// 单点活度新 vreg(活点 = inst 下标)。owner = 所属被 spill 的 vreg。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FreshVreg { + pub id: u32, + pub at: usize, + pub kind: FreshKind, + pub owner: u32, +} + +/// 溢出决策表:被 spill 的 vreg 的 def/use 点 + fresh vreg id 对照。 +/// defs/uses 两 Vec 按下标升序。05-08 rewrite 消费此结构插 SPILL/UNSPILL。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpillPlan { + pub vreg: u32, + pub slot: u16, + pub defs: Vec<(usize, u32)>, + pub uses: Vec<(usize, u32)>, +} + +/// 寄存器分配决策:vreg → Phys/Spill 映射 + spill 决策表 + 物理峰值 + 参数窗口基址。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AllocMap { + /// 含真实 + fresh 全部 vreg + pub map: BTreeMap, + /// spill 决策表(按 vreg 升序) + pub spills: Vec, + /// 物理峰值(05-08 回写 n_registers,≤253) + pub phys_peak: u32, + /// 参数窗口基址(05-08 调用点 MOV 补位,max_nargs=0 时 = 254 无窗口) + pub arg_window_base: u32, +} + +impl AllocMap { + /// 构造空 AllocMap:全空/置零,等价于 Default。 + pub fn new() -> Self { + Self { + map: BTreeMap::new(), + spills: Vec::new(), + phys_peak: 0, + arg_window_base: 254, + } + } +} + +impl Default for AllocMap { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn alloc_map_new_is_empty() { + let m = AllocMap::new(); + assert!(m.map.is_empty()); + assert!(m.spills.is_empty()); + assert_eq!(m.phys_peak, 0); + assert_eq!(m.arg_window_base, 254); + assert!(AllocMap::default().map.is_empty()); + } +} diff --git a/crates/oxide_regalloc/src/color.rs b/crates/oxide_regalloc/src/color.rs new file mode 100644 index 0000000..668205e --- /dev/null +++ b/crates/oxide_regalloc/src/color.rs @@ -0,0 +1,330 @@ +//! 染色:Kemp 简化 + 贪心选色 + spill 区间拆分循环 → AllocMap(D-09/D-05/D-02)。 +//! +//! 主循环:build 干涉图 → Kemp 简化(degree < k 入栈)→ 卡住选 spill 候选 → 被 spill 的 +//! vreg 按 def/use 点拆成单点活度 fresh vreg → 重建图重试(外循环固定点)→ 收敛后贪心 +//! 选色。fresh 染色失败 = 单点活度 ≥ k = 无可行染色 → Err(D-02 RangeError 路径)。 +//! +//! 确定性:BTreeMap/BTreeSet + Vec 排序,禁 HashMap(B010)。 + +use std::collections::{BTreeMap, BTreeSet}; + +use oxide_ir::IRFunction; +use oxide_liveness::LiveInfo; + +use crate::alloc_map::{Alloc, AllocMap, FreshKind, FreshVreg, SpillPlan}; +use crate::graph::{self, InterferenceGraph}; + +/// 主染色循环:产出 AllocMap。 +pub(super) fn run(f: &IRFunction, live: &LiveInfo, _graph: &InterferenceGraph) -> Result { + let max_real = graph::collect_real_vregs(f).into_iter().max().unwrap_or(0); + let mut next_fresh_id = max_real + 1; + let mut next_slot: u16 = 0; + let mut spill_set: BTreeSet = BTreeSet::new(); + let mut fresh: Vec = Vec::new(); + let mut slot_for_vreg: BTreeMap = BTreeMap::new(); + let mut iters = 0usize; + + let (colors, arg_window_base): (BTreeMap, u32) = loop { + let graph = graph::build(f, live, &spill_set, &fresh); + let (colors, failed) = kemp_and_select(&graph); + if failed.is_empty() { + break (colors, graph.arg_window_base); + } + for v in failed { + if fresh.iter().any(|fr| fr.id == v) { + // 单点活度 ≥ k:无可行染色(D-02 错误路径,消息与 lower 逐字一致) + return Err("RangeError: function body uses too many registers (max 253)".into()); + } + // 真实 vreg 溢出(D-05 区间拆分 v1) + spill_set.insert(v); + debug_assert!(u32::from(next_slot) <= u16::MAX as u32, "spill slot 超 u16 上限"); + slot_for_vreg.insert(v, next_slot); + next_slot += 1; + for d in def_points(f, v) { + fresh.push(FreshVreg { id: next_fresh_id, at: d, kind: FreshKind::Def, owner: v }); + next_fresh_id += 1; + } + for u in use_points(live, v) { + fresh.push(FreshVreg { id: next_fresh_id, at: u, kind: FreshKind::Use, owner: v }); + next_fresh_id += 1; + } + } + iters += 1; + debug_assert!( + iters < max_real as usize * 2 + fresh.len() + 8, + "RegAlloc 染色疑似不收敛" + ); + }; + + assemble(f, colors, spill_set, fresh, slot_for_vreg, arg_window_base) +} + +/// def 点:`def_reg(insts[i]) == Some(v)` 的指令下标。 +fn def_points(f: &IRFunction, v: u32) -> Vec { + f.insts + .iter() + .enumerate() + .filter(|(_, i)| i.def_reg() == Some(v)) + .map(|(i, _)| i) + .collect() +} + +/// use 点:`live_before[i][v]`(v 的活点)。RMW 指令同时是 def 与 use,各建一个 fresh。 +fn use_points(live: &LiveInfo, v: u32) -> Vec { + live.inst_live_before + .iter() + .enumerate() + .filter(|(_, set)| set.get(v as usize).copied().unwrap_or(false)) + .map(|(i, _)| i) + .collect() +} + +/// Kemp 简化 + 贪心选色。返回 (vreg → 色, 无色的节点 = spill 候选)。 +fn kemp_and_select(graph: &InterferenceGraph) -> (BTreeMap, Vec) { + let k = graph.k; + // 工作集 = 未入栈的非预着色节点 + let mut worklist: BTreeSet = graph + .nodes + .iter() + .filter(|(_, n)| n.pre_color.is_none()) + .map(|(v, _)| *v) + .collect(); + let mut stack: Vec = Vec::new(); + let mut failed: Vec = Vec::new(); + + loop { + // 找 degree < k 的非预着色节点入栈 + let found = worklist.iter().copied().find(|v| graph.nodes[v].adj.len() < k); + match found { + Some(v) => { + stack.push(v); + worklist.remove(&v); + } + None => { + if worklist.is_empty() { + break; + } + // 卡住:选 spill 候选(degree 最小 / vreg 号最小,确定性) + let cand = worklist + .iter() + .map(|v| (graph.nodes[v].adj.len(), *v)) + .min() + .map(|(_, v)| v) + .unwrap(); + failed.push(cand); + worklist.remove(&cand); + } + } + } + + // 选择阶段:预着色节点固定;栈顶弹出取最低可用色 + let mut colors: BTreeMap = BTreeMap::new(); + for (v, n) in &graph.nodes { + if let Some(pc) = n.pre_color { + colors.insert(*v, pc); + } + } + while let Some(v) = stack.pop() { + let mut used: BTreeSet = BTreeSet::new(); + for &n in &graph.nodes[&v].adj { + if let Some(&c) = colors.get(&n) { + used.insert(c); + } + } + match graph.allocatable.iter().copied().find(|c| !used.contains(c)) { + Some(c) => { + colors.insert(v, c); + } + None => failed.push(v), // 理论上不触发(degree < k 保证有色) + } + } + (colors, failed) +} + +/// 装配 AllocMap:map(真实 + fresh)+ spills 决策表 + phys_peak + arg_window_base。 +fn assemble( + f: &IRFunction, colors: BTreeMap, spill_set: BTreeSet, fresh: Vec, + slot_for_vreg: BTreeMap, arg_window_base: u32, +) -> Result { + let mut map = BTreeMap::new(); + // 已染色 vreg(含 fresh)→ Phys + for (v, c) in &colors { + map.insert(*v, Alloc::Phys(*c)); + } + // spill 决策表 + let mut spills: Vec = Vec::new(); + for v in spill_set { + let slot = slot_for_vreg[&v]; + // defs = 该 vreg 的 def 点(kind=Def 且 owner==v) + let defs: Vec<(usize, u32)> = fresh + .iter() + .filter(|fr| fr.owner == v && fr.kind == FreshKind::Def) + .map(|fr| (fr.at, fr.id)) + .collect(); + let uses: Vec<(usize, u32)> = fresh + .iter() + .filter(|fr| fr.owner == v && fr.kind == FreshKind::Use) + .map(|fr| (fr.at, fr.id)) + .collect(); + spills.push(SpillPlan { vreg: v, slot, defs, uses }); + } + spills.sort_by_key(|s| s.vreg); + + let phys_peak = colors + .values() + .copied() + .chain(std::iter::once(f.param_layout.base + f.param_layout.count)) + .chain(std::iter::once(1)) + .max() + .unwrap_or(1) + .saturating_add(1) + .min(254); + + Ok(AllocMap { map, spills, phys_peak, arg_window_base }) +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_bytecode::opcode::OpCode; + use oxide_ir::inst::Inst; + use oxide_ir::operand::Operand; + use oxide_ir::IRFunction; + + fn empty_function() -> IRFunction { + IRFunction::new() + } + + fn color_of( + insts: Vec, param_layout: oxide_ir::ParamLayout, nested: Vec, + ) -> Result { + let mut f = empty_function(); + f.insts = insts; + f.param_layout = param_layout; + f.nested = nested; + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + crate::color(&f, &live) + } + + #[test] + fn overlapping_vregs_get_distinct_colors() { + // r1/r2 同时 live(ADD 前)→ 必须不同色;r3(ADD 结果)与二者不相交 + let m = color_of( + 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), + ], + oxide_ir::ParamLayout { base: 0, count: 0 }, + Vec::new(), + ) + .unwrap(); + assert_ne!(m.map[&1], m.map[&2], "同时 live 的 r1/r2 必须不同色"); + } + + #[test] + fn disjoint_live_ranges_share_color() { + // r1 死于 ADD0,r4 生于 ADD1——活度不相交 → 可同色 + let m = color_of( + 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::load_const(Operand::Reg(4), 0), + Inst::load_const(Operand::Reg(5), 0), + Inst::new(OpCode::ADD, Operand::Reg(6), Operand::Reg(4), Operand::Reg(5)), + Inst::new(OpCode::RETURN, Operand::Reg(6), Operand::None, Operand::None), + ], + oxide_ir::ParamLayout { base: 0, count: 0 }, + Vec::new(), + ) + .unwrap(); + if let (Alloc::Phys(c1), Alloc::Phys(c4)) = (m.map[&1], m.map[&4]) { + assert_eq!(c1, c4, "不相交活度应共享颜色"); + } else { + panic!("r1/r4 应为 Phys"); + } + } + + #[test] + fn param_segment_keeps_colors() { + let m = color_of( + vec![ + Inst::new(OpCode::ADD, Operand::Reg(3), Operand::Reg(1), Operand::Reg(2)), + Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None), + ], + oxide_ir::ParamLayout { base: 1, count: 2 }, + Vec::new(), + ) + .unwrap(); + assert_eq!(m.map[&1], Alloc::Phys(1)); + assert_eq!(m.map[&2], Alloc::Phys(2)); + assert_eq!(m.map.values().filter(|&&a| a == Alloc::Phys(1)).count(), 1); + assert_eq!(m.map.values().filter(|&&a| a == Alloc::Phys(2)).count(), 1); + } + + #[test] + fn escaped_vreg_keeps_color_and_not_spilled() { + let mut f = empty_function(); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(5), Operand::Reg(3), Operand::Reg(4))); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + let mut sub = empty_function(); + sub.insts.push(Inst::new(OpCode::LOAD_VAR, Operand::Reg(9), Operand::Reg(3), Operand::None)); + f.nested.push(sub); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + let m = crate::color(&f, &live).unwrap(); + assert_eq!(m.map[&3], Alloc::Phys(3), "escaped 槽保持原色"); + assert!(m.spills.iter().all(|s| s.vreg != 3), "escaped vreg 不得被 spill"); + } + + #[test] + fn coloring_is_deterministic() { + let src_insts = vec![ + Inst::new(OpCode::ADD, Operand::Reg(2), Operand::Reg(0), Operand::Reg(1)), + Inst::new(OpCode::RETURN, Operand::Reg(2), Operand::None, Operand::None), + ]; + let a = color_of(src_insts.clone(), oxide_ir::ParamLayout { base: 0, count: 0 }, Vec::new()).unwrap(); + let b = color_of(src_insts, oxide_ir::ParamLayout { base: 0, count: 0 }, Vec::new()).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn spill_split_recolors_fresh() { + // 长活 L + 254 短活 B_i(每个 B_i 与 L 在同一 ADD 指令 live): + // L 的 degree = 254 ≥ k=253 → kemp 卡住 → spill L → 各 def/use 点拆成单点 fresh → + // fresh 全部单点活度 < k → 重染色成功。这是"成功 spill"的标准场景。 + let mut insts = Vec::new(); + insts.push(Inst::load_const(Operand::Reg(1000), 0)); // L + for i in 0..254u32 { + insts.push(Inst::load_const(Operand::Reg(2000 + i), 0)); // B_i + insts.push(Inst::new(OpCode::ADD, Operand::Reg(3000 + i), Operand::Reg(2000 + i), Operand::Reg(1000))); + } + insts.push(Inst::new(OpCode::RETURN, Operand::Reg(1000), Operand::None, Operand::None)); + let m = color_of(insts, oxide_ir::ParamLayout { base: 0, count: 0 }, Vec::new()).unwrap(); + assert!(!m.spills.is_empty(), "长活 L 应被 spill"); + assert!(m.spills.iter().any(|s| s.vreg == 1000), "spill 的是 L"); + assert!(m.phys_peak <= 253, "phys_peak ≤ 253"); + // 全部 fresh 应有 Phys 分配 + let fresh_phys: Vec<&Alloc> = m + .map + .values() + .filter(|a| matches!(a, Alloc::Phys(_))) + .collect(); + assert!(!fresh_phys.is_empty(), "fresh vreg 应着色为 Phys"); + } + + #[test] + fn uncolorable_returns_error() { + // 255 参数 CALL:nargs=255 → arg_window_base=0 → k=0 → 全部 spill → fresh 失败 → Err + let insts = vec![ + Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3000), 255), + Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None), + ]; + let result = color_of(insts, oxide_ir::ParamLayout { base: 0, count: 0 }, Vec::new()); + assert!(result.is_err(), "单点活度 ≥ k 应报 Err"); + assert!(result.unwrap_err().contains("too many registers"), "Err 消息应含 too many registers"); + } +} diff --git a/crates/oxide_regalloc/src/graph.rs b/crates/oxide_regalloc/src/graph.rs new file mode 100644 index 0000000..2342e06 --- /dev/null +++ b/crates/oxide_regalloc/src/graph.rs @@ -0,0 +1,297 @@ +//! 干涉图构建 + 预着色 + 可分配色集(D-09/D-10/D-11 + RESEARCH 发现 4)。 +//! +//! - 真实 vreg:字面 `Operand::Reg(r)`(rd/a/b)+ CALL 系参数连续区间 [first, first+nargs) +//! - 干涉边:逐指令 `live_before[i]` 中同时 live 的真实 vreg 两两加边(fresh vreg 按 at==i 并入) +//! - 预着色:参数段 [param_base, base+count) 钉死原号(VM 调用契约 D-10);escaped vreg +//! (nested `LOAD_VAR.a`/`STORE_VAR.rd` 直引父槽)钉死原槽号且排除 spill(B012) +//! - 可分配色集:`(1..=253) − 参数色 − escaped 色 − [arg_window_base, 253]` +//! (arg_window_base = 254 − max_nargs,为 05-08 调用点 MOV 补位留空槽) +//! +//! 确定性:全 BTreeMap + Vec 排序,禁 HashMap 迭代序(B010)。 + +use std::collections::{BTreeMap, BTreeSet}; + +use oxide_bytecode::opcode::OpCode; +use oxide_ir::operand::Operand; +use oxide_ir::IRFunction; +use oxide_liveness::LiveInfo; + +use crate::alloc_map::FreshVreg; + +/// 干涉图节点:vreg(以 map key 表达)+ 预着色 + 邻接表(排序去重)。 +#[derive(Debug, Clone)] +pub(super) struct Node { + pub pre_color: Option, + pub adj: Vec, +} + +/// 干涉图:节点表 + 可分配色集 + 色数 k + 参数窗口基址。 +#[derive(Debug, Clone)] +pub(super) struct InterferenceGraph { + pub nodes: BTreeMap, + pub allocatable: Vec, + pub k: usize, + pub arg_window_base: u32, +} + +/// 收集全部真实 vreg(BTreeSet 保确定性): +/// 字面 `Operand::Reg(r)`(rd/a/b 三槽)+ CALL/CALL_NATIVE/NEW_EXPRESSION b 槽参数区间 +/// + SUPER_CALL a 槽参数区间。0 无条件排除(reg 0 永非真实——emit alloc_reg 从 ≥1 起)。 +/// +/// 254/255 的 over-approx 规则:live bitset 中位 254/255 无法区分 This/NewTarget 占用与 +/// 真实 vreg 活度,若函数存在该号真实 vreg(大函数 B005 场景)则按真实 vreg 建节点染色—— +/// 多出的边只造成过度约束(安全方向),缺失边才会错值。 +pub(super) fn collect_real_vregs(f: &IRFunction) -> BTreeSet { + let mut real = BTreeSet::new(); + for inst in &f.insts { + for o in [&inst.rd, &inst.a, &inst.b] { + if let Operand::Reg(r) = o { + if *r != 0 { + real.insert(*r); + } + } + } + let nargs = inst.ext.first().copied().unwrap_or(0); + let first = match inst.op { + OpCode::CALL | OpCode::CALL_NATIVE | OpCode::NEW_EXPRESSION => match inst.b { + Operand::Reg(fr) => Some(fr), + _ => None, + }, + OpCode::SUPER_CALL => match inst.a { + Operand::Reg(fr) => Some(fr), + _ => None, + }, + _ => None, + }; + if let Some(fr) = first { + for r in fr..fr + nargs { + if r != 0 { + real.insert(r); + } + } + } + } + real +} + +/// 构建干涉图:`build(f, live, spill_set, fresh) -> InterferenceGraph`。 +/// spill_set 中的 vreg 不再是节点(其 fresh 替代品入图);fresh 按 at==i 并入该指令活集。 +pub(super) fn build( + f: &IRFunction, live: &LiveInfo, spill_set: &BTreeSet, fresh: &[FreshVreg], +) -> InterferenceGraph { + let real = collect_real_vregs(f); + let reg_count = live.inst_live_before.len(); + + // ── 预着色 ── + let mut pre_colors: BTreeMap = BTreeMap::new(); + // 参数段:VM 调用写 regs[param_base+i],钉死不可动(D-10) + let pl = f.param_layout; + if pl.count > 0 { + for i in 0..pl.count { + let v = pl.base + i; + pre_colors.insert(v, v); + } + } + // escaped:递归 nested 收集 LOAD_VAR.a / STORE_VAR.rd 直引的父槽(B012) + let mut escaped_colors: Vec = Vec::new(); + collect_escaped(&f.nested, &mut pre_colors, &mut escaped_colors); + + // ── 可分配色集 ── + let max_nargs = f + .insts + .iter() + .filter(|i| { + matches!( + i.op, + OpCode::CALL | OpCode::CALL_NATIVE | OpCode::NEW_EXPRESSION | OpCode::SUPER_CALL + ) + }) + .map(|i| i.ext.first().copied().unwrap_or(0)) + .max() + .unwrap_or(0); + let arg_window_base = 254u32.saturating_sub(max_nargs); + let mut allocatable: Vec = Vec::new(); + for c in 1u32..=253 { + if pre_colors.contains_key(&c) || escaped_colors.contains(&c) || c >= arg_window_base { + continue; + } + allocatable.push(c); + } + let k = allocatable.len(); + + // ── 节点 + 干涉边 ── + let mut nodes: BTreeMap = BTreeMap::new(); + for &v in real.iter() { + if !spill_set.contains(&v) { + nodes.insert( + v, + Node { pre_color: pre_colors.get(&v).copied(), adj: Vec::new() }, + ); + } + } + for fr in fresh { + nodes.entry(fr.id).or_insert(Node { pre_color: None, adj: Vec::new() }); + } + // 邻接表(BTreeMap> 去重后转 Vec 排序) + let mut adj_sets: BTreeMap> = BTreeMap::new(); + let node_ids: Vec = nodes.keys().copied().collect(); + for &v in &node_ids { + adj_sets.entry(v).or_default(); + } + for i in 0..reg_count { + let mut at_i: Vec = Vec::new(); + for &v in &node_ids { + if live.inst_live_before[i].get(v as usize).copied().unwrap_or(false) { + at_i.push(v); + } + } + for fr in fresh { + if fr.at == i { + at_i.push(fr.id); + } + } + // 两两加边 + for a in 0..at_i.len() { + for b in a + 1..at_i.len() { + let (x, y) = (at_i[a], at_i[b]); + adj_sets.entry(x).or_default().insert(y); + adj_sets.entry(y).or_default().insert(x); + } + } + } + for (v, set) in adj_sets { + if let Some(node) = nodes.get_mut(&v) { + node.adj = set.into_iter().collect(); + } + } + + InterferenceGraph { nodes, allocatable, k, arg_window_base } +} + +/// 递归收集 nested 树中变量槽引用(LOAD_VAR.a 读槽、STORE_VAR.rd 写槽), +/// 注入 pre_colors 并记录 escaped 色列表(可分配集排除 + spill 候选排除,B012)。 +fn collect_escaped(nested: &[IRFunction], pre_colors: &mut BTreeMap, 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 { + pre_colors.entry(r).or_insert(r); + if !out.contains(&r) { + out.push(r); + } + } + } + collect_escaped(&sub.nested, pre_colors, out); + } + out.sort_unstable(); +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_ir::inst::Inst; + use oxide_ir::operand::Operand; + + fn empty_function() -> IRFunction { + IRFunction::new() + } + + fn build_graph(f: &IRFunction) -> InterferenceGraph { + let cfg = oxide_cfg::build_cfg(f); + let live = oxide_liveness::liveness(f, &cfg); + build(f, &live, &BTreeSet::new(), &[]) + } + + #[test] + fn simultaneously_live_get_edge() { + // 0: LOAD_CONST r1 1: LOAD_CONST r2 2: ADD r3=r1+r2(r1/r2 同时 live) 3: RETURN r3 + let mut f = empty_function(); + f.insts.push(Inst::load_const(Operand::Reg(1), 0)); + f.insts.push(Inst::load_const(Operand::Reg(2), 0)); + 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 g = build_graph(&f); + assert!(g.nodes[&1].adj.contains(&2), "r1 与 r2 应邻接"); + assert!(g.nodes[&2].adj.contains(&1)); + assert!(!g.nodes[&3].adj.contains(&1), "r3 与 r1 无干涉(r1 死于 ADD)"); + } + + #[test] + fn disjoint_ranges_no_edge() { + // r1/r2 死于 ADD0,r4/r5 生于 ADD1——两段活度不相交 + let mut f = empty_function(); + f.insts.push(Inst::load_const(Operand::Reg(1), 0)); + f.insts.push(Inst::load_const(Operand::Reg(2), 0)); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(3), Operand::Reg(1), Operand::Reg(2))); + f.insts.push(Inst::load_const(Operand::Reg(4), 0)); + f.insts.push(Inst::load_const(Operand::Reg(5), 0)); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(6), Operand::Reg(4), Operand::Reg(5))); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(6), Operand::None, Operand::None)); + let g = build_graph(&f); + assert!(!g.nodes[&1].adj.contains(&4), "r1 与 r4 活度不相交,无边"); + } + + #[test] + fn param_segment_precolored() { + let mut f = empty_function(); + f.param_layout = oxide_ir::ParamLayout { base: 1, count: 2 }; + 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 g = build_graph(&f); + assert_eq!(g.nodes[&1].pre_color, Some(1)); + assert_eq!(g.nodes[&2].pre_color, Some(2)); + assert!(!g.allocatable.contains(&1) && !g.allocatable.contains(&2), "参数色排除"); + assert_eq!(g.k, g.allocatable.len()); + } + + #[test] + fn escaped_slots_precolored() { + let mut f = empty_function(); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(5), Operand::Reg(3), Operand::Reg(4))); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + let mut sub = empty_function(); + sub.insts.push(Inst::new(OpCode::LOAD_VAR, Operand::Reg(9), Operand::Reg(3), Operand::None)); + f.nested.push(sub); + let g = build_graph(&f); + assert_eq!(g.nodes[&3].pre_color, Some(3), "escaped 槽 r3 预着色"); + assert!(!g.allocatable.contains(&3), "escaped 色排除"); + } + + #[test] + fn reserved_colors_excluded() { + 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 g = build_graph(&f); + assert!(!g.allocatable.contains(&0), "色 0 排除(CALL 隐式 reg0)"); + assert!(!g.allocatable.contains(&254) && !g.allocatable.contains(&255), "254/255 排除"); + assert!(g.allocatable.iter().all(|&c| (1..=253).contains(&c))); + } + + #[test] + fn arg_window_reserved() { + let mut f = empty_function(); + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 3)); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + let g = build_graph(&f); + assert_eq!(g.arg_window_base, 251, "max_nargs=3 → base 251"); + assert!(!g.allocatable.contains(&251) && !g.allocatable.contains(&252) && !g.allocatable.contains(&253)); + } + + #[test] + fn high_numbered_vreg_254_is_real_node() { + // 大函数场景:真实 vreg 254 必须建节点(B005 正确性前提) + let mut f = empty_function(); + f.insts.push(Inst::new(OpCode::ADD, Operand::Reg(254), Operand::Reg(252), Operand::Reg(253))); + f.insts.push(Inst::new(OpCode::RETURN, Operand::Reg(254), Operand::None, Operand::None)); + let real = collect_real_vregs(&f); + assert!(real.contains(&254), "真实 vreg 254 必须被收集"); + let g = build_graph(&f); + assert!(g.nodes.contains_key(&254), "vreg 254 建节点"); + assert_eq!(g.nodes[&254].pre_color, None, "254 非预着色"); + } +} diff --git a/crates/oxide_regalloc/src/lib.rs b/crates/oxide_regalloc/src/lib.rs new file mode 100644 index 0000000..1184d84 --- /dev/null +++ b/crates/oxide_regalloc/src/lib.rs @@ -0,0 +1,26 @@ +//! RegAlloc 改写 pass(前半):干涉图染色决策,产出 AllocMap。 +//! +//! 消费 `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。 + +mod alloc_map; +mod color; +mod graph; + +pub use alloc_map::{Alloc, AllocMap, FreshKind, FreshVreg, SpillPlan}; + +use oxide_ir::IRFunction; +use oxide_liveness::LiveInfo; + +/// 染色:`&IRFunction + &LiveInfo → Result`。纯函数,只读 IR。 +/// 空 IRFunction 退化:Ok(AllocMap::new())。Err = 无可行染色(D-02 RangeError 路径, +/// 05-08 接线时转 lower 错误)。 +pub fn color(f: &IRFunction, live: &LiveInfo) -> Result { + if f.insts.is_empty() { + return Ok(AllocMap::new()); + } + let graph = graph::build(f, live, &std::collections::BTreeSet::new(), &[]); + color::run(f, live, &graph) +} diff --git a/crates/oxide_regalloc/tests/smoke.rs b/crates/oxide_regalloc/tests/smoke.rs new file mode 100644 index 0000000..5e7604c --- /dev/null +++ b/crates/oxide_regalloc/tests/smoke.rs @@ -0,0 +1,102 @@ +//! 集成冒烟:真实编译产物(parse→emit→build_cfg→liveness→color)不崩 + 完整性/确定性断言。 + +use oxide_regalloc::{Alloc, AllocMap}; +use std::collections::BTreeSet; + +fn run_color(src: &str) -> (AllocMap, oxide_ir::IRFunction) { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, src).expect("parse"); + let ir = oxide_emit::Emitter::new().emit_program(&program).expect("emit"); + let cfg = oxide_cfg::build_cfg(&ir); + let live = oxide_liveness::liveness(&ir, &cfg); + let m = oxide_regalloc::color(&ir, &live).expect("color"); + (m, ir) +} + +/// 收集函数内全部字面真实 vreg(对照 contract def/use,排除 0/254/255 保留位)。 +fn literal_regs(ir: &oxide_ir::IRFunction) -> BTreeSet { + let mut set = BTreeSet::new(); + for inst in &ir.insts { + for o in [&inst.rd, &inst.a, &inst.b] { + if let oxide_ir::operand::Operand::Reg(r) = o { + if *r != 0 && *r != 254 && *r != 255 { + set.insert(*r); + } + } + } + } + set +} + +fn smoke(src: &str) { + let (m, ir) = run_color(src); + // 完整性:全部真实 vreg 有条目 + for r in literal_regs(&ir) { + assert!(m.map.contains_key(&r), "vreg {r} 无分配"); + } + // phys ≤ 253;非预着色色 < arg_window_base + for a in m.map.values() { + if let Alloc::Phys(c) = a { + assert!(*c <= 253, "Phys 色 {c} 超 253"); + assert!(*c < m.arg_window_base || is_precolored(&ir, *c), "色 {c} 应在可分配集或为预着色"); + } + } + // spill slot 不重复 + let mut slots = BTreeSet::new(); + for s in &m.spills { + assert!(slots.insert(s.slot), "spill slot {} 重复", s.slot); + } + assert!(m.phys_peak <= 253, "phys_peak {} 超 253", m.phys_peak); +} + +fn is_precolored(ir: &oxide_ir::IRFunction, c: u32) -> bool { + let pl = ir.param_layout; + if c >= pl.base && c < pl.base + pl.count { + return true; + } + // escaped 预着色(简化:允许参数段色) + false +} + +#[test] +fn smoke_simple_function() { + smoke("function f(a, b) { return a + b; } f(1, 2)"); +} + +#[test] +fn smoke_if_else() { + smoke("function f(x) { if (x > 0) { return 1; } else { return 2; } } f(5)"); +} + +#[test] +fn smoke_loop() { + smoke("function f(n) { var s = 0; for (var i = 0; i < n; i++) { s = s + i; } return s; } f(10)"); +} + +#[test] +fn smoke_try_catch() { + smoke("function x() {} function y(e) {} function f() { try { x(); } catch (e) { y(e); } } f()"); +} + +#[test] +fn smoke_nested_calls() { + smoke("function g(x) { return x + 1; } function f(x) { return g(x) + g(x + 1); } f(1)"); +} + +#[test] +fn smoke_class_field() { + smoke("var outerVar = 42; class C { p = outerVar; } new C()"); +} + +#[test] +fn smoke_destructuring() { + smoke("function f() { var { a, b } = { a: 1, b: 2 }; return a + b; } f()"); +} + +#[test] +fn color_is_deterministic_on_real_ir() { + let src = "function f(n) { var s = 0; for (var i = 0; i < n; i++) { s = s + i; } return s; } f(10)"; + let (a, _) = run_color(src); + let (b, _) = run_color(src); + assert_eq!(a, b); +}