fix: 寄存器分配支持高位寄存器槽

This commit is contained in:
2026-08-11 12:37:56 +08:00
parent fab06a233d
commit 2287f9f4ce
6 changed files with 235 additions and 48 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ pub struct AllocMap {
pub map: BTreeMap<u32, Alloc>,
/// spill 决策表(按 vreg 升序)
pub spills: Vec<SpillPlan>,
/// 物理峰值(finish 回写 n_registers,≤253)
/// 物理窗口大小(finish 回写 n_registers,最高槽 253 对应值 254)。
pub phys_peak: u32,
/// 参数窗口基址(调用点 MOV 补位,max_nargs=0 时 = 254 无窗口)
pub arg_window_base: u32,
+23 -6
View File
@@ -17,7 +17,12 @@ use crate::graph::{self, InterferenceGraph};
/// 主染色循环:产出 AllocMap。
pub(super) fn run(f: &IRFunction, live: &LiveInfo) -> Result<AllocMap, String> {
let max_real = graph::collect_real_vregs(f).into_iter().max().unwrap_or(0);
let max_param = if f.param_layout.count == 0 {
0
} else {
f.param_layout.base + f.param_layout.count - 1
};
let max_real = graph::collect_real_vregs(f).into_iter().max().unwrap_or(0).max(max_param);
let mut next_fresh_id = max_real + 1;
let mut next_slot: u16 = 0;
let mut spill_set: BTreeSet<u32> = BTreeSet::new();
@@ -64,7 +69,7 @@ pub(super) fn run(f: &IRFunction, live: &LiveInfo) -> Result<AllocMap, String> {
debug_assert!(iters < max_real as usize * 2 + fresh.len() + 8, "RegAlloc 染色疑似不收敛");
};
assemble(f, colors, spill_set, fresh, slot_for_vreg, arg_window_base)
assemble(colors, spill_set, fresh, slot_for_vreg, arg_window_base)
}
/// def 点:`def_reg(insts[i]) == Some(v)` 的指令下标。
@@ -154,8 +159,8 @@ fn kemp_and_select(graph: &InterferenceGraph) -> (BTreeMap<u32, u32>, Vec<u32>)
/// 装配 AllocMap:map(真实 + fresh)+ spills 决策表 + phys_peak + arg_window_base。
fn assemble(
f: &IRFunction, colors: BTreeMap<u32, u32>, spill_set: BTreeSet<u32>, fresh: Vec<FreshVreg>,
slot_for_vreg: BTreeMap<u32, u16>, arg_window_base: u32,
colors: BTreeMap<u32, u32>, spill_set: BTreeSet<u32>, fresh: Vec<FreshVreg>, slot_for_vreg: BTreeMap<u32, u16>,
arg_window_base: u32,
) -> Result<AllocMap, String> {
let mut map = BTreeMap::new();
// 已染色 vreg(含 fresh)→ Phys
@@ -184,7 +189,6 @@ fn assemble(
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)
@@ -299,6 +303,19 @@ mod tests {
assert!(m.spills.iter().all(|s| s.vreg != 3), "escaped vreg 不得被 spill");
}
#[test]
fn highest_legal_color_uses_254_register_window() {
let mut f = empty_function();
f.builtin_reg_map = vec![("late".to_string(), 253)];
f.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(253), Operand::None, Operand::None));
let cfg = oxide_cfg::build_cfg(&f);
let live = oxide_liveness::liveness(&f, &cfg);
let map = crate::color(&f, &live).unwrap();
assert_eq!(map.map[&253], Alloc::Phys(253));
assert_eq!(map.phys_peak, 254);
}
#[test]
fn coloring_is_deterministic() {
let src_insts = vec![
@@ -325,7 +342,7 @@ mod tests {
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");
assert!(m.phys_peak <= 254, "phys_peak ≤ 254");
// 全部 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");
+43 -13
View File
@@ -1,9 +1,10 @@
//! 元数据回写:n_registers / builtin_reg_map / param_layout / upvalue_captures。
//!
//! - n_registers = map.phys_peak(≤253;不含参数窗口——窗口是调用点暂存槽,
//! - n_registers = map.phys_peak(≤254;最高合法物理槽 253 对应窗口大小 254;
//! 不含参数窗口——窗口是调用点暂存槽,
//! 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 预着色恒等)
//! - param_layout 按参数首槽物理色回写;upvalue_captures 不动(escaped 预着色恒等)
use crate::alloc_map::{Alloc, AllocMap};
use crate::rewrite::spilled_builtin_bindings;
@@ -12,36 +13,44 @@ 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");
debug_assert!(map.phys_peak <= 254, "phys_peak 超 254");
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 mut rewritten_builtins = Vec::with_capacity(f.builtin_reg_map.len());
for (name, vreg) in &f.builtin_reg_map {
let v = *vreg;
match map.map.get(&v) {
Some(Alloc::Phys(p)) => {
*vreg = *p;
rewritten_builtins.push((name.clone(), *p));
}
Some(Alloc::Spill(_)) => {
if let Some((_, r, _)) = spilled.iter().find(|(n, _, _)| n == name) {
debug_assert!(*r <= 253);
*vreg = *r;
rewritten_builtins.push((name.clone(), *r));
} else {
debug_assert!(false, "spilled builtin {name} 无对应入口 SPILL");
}
}
None => {
// 未使用的 builtin(引用被 DCE 删除,vreg 不在 AllocMap)。
// builtin 槽是预注册低号,保持原值无害(VM 入口写入,无读取者)。
// 保持原项(*vreg 不变)。
}
None => {}
}
}
f.builtin_reg_map = rewritten_builtins;
// (c) param_layout 不动(参数段预着色恒等;窗口与参数段重叠仅参数段贴 254 的罕见情形)
// (c) 参数段保持连续,但继承父上下文产生的高虚拟段会整体移动到低位物理段。
let pl = f.param_layout;
debug_assert!(pl.base + pl.count <= map.arg_window_base || map.arg_window_base == 254);
if pl.count > 0 {
if let Some(Alloc::Phys(base)) = map.map.get(&pl.base) {
for i in 0..pl.count {
debug_assert_eq!(map.map.get(&(pl.base + i)), Some(&Alloc::Phys(base + i)));
}
f.param_layout.base = *base;
}
debug_assert!(f.param_layout.base + f.param_layout.count <= map.arg_window_base || map.arg_window_base == 254);
} else {
f.param_layout.base = 0;
}
// (d) upvalue_captures 不动。enclosing_reg 是编译期产物(MAKE_CELL 定位用),VM 运行时
// 不读(CREATE_CLOSURE 走 cell_idx,cell 捕获后值在 cell 中)——无需跨函数同步。
@@ -101,6 +110,18 @@ mod tests {
assert_eq!(f.builtin_reg_map[0].1, binds[0].1, "回写 R 与入口 SPILL 同源");
}
#[test]
fn unused_builtin_is_removed_from_frame_metadata() {
let mut f = IRFunction::new();
f.builtin_reg_map = vec![("late".to_string(), 426)];
f.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(1), Operand::None, Operand::None));
let mut map = AllocMap::new();
map.map.insert(1, Alloc::Phys(1));
run(&mut f, &map);
assert!(f.builtin_reg_map.is_empty());
}
#[test]
fn param_layout_untouched() {
let mut f = IRFunction::new();
@@ -111,6 +132,15 @@ mod tests {
assert_eq!(f.param_layout.count, 2);
}
#[test]
fn zero_parameter_layout_discards_compile_time_boundary() {
let mut f = IRFunction::new();
f.param_layout = oxide_ir::ParamLayout { base: 426, count: 0 };
let map = AllocMap::new();
run(&mut f, &map);
assert_eq!(f.param_layout, oxide_ir::ParamLayout { base: 0, count: 0 });
}
#[test]
fn enclosing_reg_unchanged() {
let mut f = IRFunction::new();
+84 -27
View File
@@ -95,30 +95,10 @@ pub(super) fn collect_real_vregs(f: &IRFunction) -> BTreeSet<u32> {
pub(super) fn build(
f: &IRFunction, live: &LiveInfo, spill_set: &BTreeSet<u32>, fresh: &[FreshVreg],
) -> InterferenceGraph {
let real = collect_real_vregs(f);
let mut real = collect_real_vregs(f);
// inst_live_before 长度 = 指令数(逐指令活集索引)
let inst_count = live.inst_live_before.len();
// ── 预着色 ──
let mut pre_colors: BTreeMap<u32, u32> = BTreeMap::new();
// 参数段:VM 调用写 regs[param_base+i],钉死不可动(调用契约)
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 直引的父槽
let mut escaped_colors: Vec<u32> = Vec::new();
collect_escaped(&f.nested, &mut pre_colors, &mut escaped_colors);
// 对称缺口:子模块自身引用的父槽也必须预着色恒等。父侧 collect_escaped 只保证父不
// 移动槽;但子模块 alloc 时,它引用父槽的 LOAD_VAR.a / STORE_VAR.rd 会被当作子模块
// 自己的 vreg 参与染色而移走 → 子模块读错物理槽。分界线 = param_layout.base
// (emit 的 inherited_reg_start 继承机制:子模块 vreg ≥ base,父槽引用 < base)。
collect_own_escaped(f, &mut pre_colors, &mut escaped_colors);
// ── 可分配色集 ──
// 窗口只为参数连续性 MOV 桥预留;spread 调用实参经 ext 逐个读寄存器,无连续性要求。
let max_nargs = f
.insts
@@ -129,6 +109,68 @@ pub(super) fn build(
.unwrap_or(0);
let arg_window_base = 254u32.saturating_sub(max_nargs);
// ── 预着色 ──
let mut pre_colors: BTreeMap<u32, u32> = BTreeMap::new();
// 当前函数读取的父槽已经是父函数分配后的物理号,必须恒等保护。
let mut escaped_colors: Vec<u32> = Vec::new();
collect_own_escaped(f, &mut pre_colors, &mut escaped_colors);
// 被子函数捕获的当前函数槽需要固定颜色,但高虚拟槽不能恒等映射到 254+。
// 父函数完成分配后,alloc 会把选定物理色同步到整个 nested 树的父槽引用。
let mut nested_escaped = Vec::new();
collect_escaped(&f.nested, &mut nested_escaped);
let param_end = f.param_layout.base.saturating_add(f.param_layout.count);
for vreg in nested_escaped {
if !real.contains(&vreg) || (f.param_layout.base..param_end).contains(&vreg) {
continue;
}
let identity_available =
vreg >= 1 && vreg < arg_window_base && !pre_colors.values().any(|color| *color == vreg);
let physical = if identity_available {
vreg
} else {
(1..arg_window_base)
.find(|color| !pre_colors.values().any(|existing| existing == color))
.unwrap_or_else(|| {
debug_assert!(false, "escaped 槽无自由色(vreg={vreg})");
1
})
};
pre_colors.insert(vreg, physical);
escaped_colors.push(physical);
}
escaped_colors.sort_unstable();
escaped_colors.dedup();
// 参数段必须连续。继承父上下文后虚拟段可能高于 253,此时选择不与 escaped 槽
// 冲突的低位连续物理段;finish 按首参数颜色同步回写 param_layout.base。
let pl = f.param_layout;
if pl.count > 0 {
let identity_end = pl.base.saturating_add(pl.count);
let identity_available = pl.base >= 1
&& identity_end <= arg_window_base
&& (pl.base..identity_end).all(|color| !escaped_colors.contains(&color));
let physical_base = if identity_available {
pl.base
} else {
(1..=arg_window_base.saturating_sub(pl.count))
.find(|base| {
(*base..*base + pl.count).all(|color| {
!pre_colors.values().any(|existing| *existing == color) && !escaped_colors.contains(&color)
})
})
.unwrap_or_else(|| {
debug_assert!(false, "参数段无连续自由色(base={}, count={})", pl.base, pl.count);
1
})
};
for i in 0..pl.count {
let vreg = pl.base + i;
real.insert(vreg);
pre_colors.insert(vreg, physical_base + i);
}
}
// ── builtin 槽 ──
// VM 帧推入时写 regs[slot]=全局值(无指令 def 却活到入口),物理号须恒定且排除出
// 可分配集,否则 prologue 死定义临时(如解构 FOR_OF_DONE 结果)与它同色,在
@@ -166,7 +208,7 @@ pub(super) fn build(
let mut allocatable: Vec<u32> = Vec::new();
for c in 1u32..=253 {
if pre_colors.contains_key(&c) || escaped_colors.contains(&c) || c >= arg_window_base {
if pre_colors.values().any(|color| *color == c) || escaped_colors.contains(&c) || c >= arg_window_base {
continue;
}
allocatable.push(c);
@@ -249,9 +291,8 @@ pub(super) fn build(
}
}
/// 递归收集 nested 树中变量槽引用(LOAD_VAR.a 读槽、STORE_VAR.rd 写槽),
/// 注入 pre_colors 并记录 escaped 色列表(可分配集排除 + spill 候选排除)。
fn collect_escaped(nested: &[IRFunction], pre_colors: &mut BTreeMap<u32, u32>, out: &mut Vec<u32>) {
/// 递归收集 nested 树中的父槽虚拟号(LOAD_VAR.a 读槽、STORE_VAR.rd 写槽)。
fn collect_escaped(nested: &[IRFunction], out: &mut Vec<u32>) {
for sub in nested {
for inst in &sub.insts {
let slot = match inst.op {
@@ -260,13 +301,12 @@ fn collect_escaped(nested: &[IRFunction], pre_colors: &mut BTreeMap<u32, u32>, o
_ => 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);
collect_escaped(&sub.nested, out);
}
out.sort_unstable();
}
@@ -379,6 +419,23 @@ mod tests {
assert!(!g.allocatable.contains(&3), "escaped 色排除");
}
#[test]
fn high_escaped_slot_uses_encodable_color() {
let mut f = empty_function();
f.insts.push(Inst::load_const(Operand::Reg(254), 0));
f.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(254), Operand::None, Operand::None));
let mut sub = empty_function();
sub.param_layout = oxide_ir::ParamLayout { base: 300, count: 0 };
sub.insts
.push(Inst::new(OpCode::LOAD_VAR, Operand::Reg(301), Operand::Reg(254), Operand::None));
f.nested.push(sub);
let g = build_graph(&f);
let color = g.nodes[&254].pre_color.expect("escaped 槽应预着色");
assert!((1..=253).contains(&color));
assert_ne!(color, 254);
}
#[test]
fn reserved_colors_excluded() {
let mut f = empty_function();
+83
View File
@@ -54,6 +54,7 @@ pub fn alloc(f: &mut IRFunction, live: &LiveInfo) -> Result<(), String> {
regalloc_debug!("alloc: {} vregs -> {} phys", map.map.len(), map.phys_peak);
rewrite::run(f, &map);
finish::run(f, &map);
remap_nested_parent_slots(&mut f.nested, &map);
for child in &mut f.nested {
let cfg = oxide_cfg::build_cfg(child);
let l = oxide_liveness::liveness(child, &cfg);
@@ -62,6 +63,27 @@ pub fn alloc(f: &mut IRFunction, live: &LiveInfo) -> Result<(), String> {
Ok(())
}
fn remap_nested_parent_slots(nested: &mut [IRFunction], map: &AllocMap) {
for child in nested {
let inherited_limit = child.param_layout.base;
for inst in &mut child.insts {
let slot = match inst.op {
oxide_bytecode::opcode::OpCode::LOAD_VAR => &mut inst.a,
oxide_bytecode::opcode::OpCode::STORE_VAR => &mut inst.rd,
_ => continue,
};
if let oxide_ir::operand::Operand::Reg(vreg) = slot {
if *vreg < inherited_limit {
if let Some(Alloc::Phys(physical)) = map.map.get(vreg) {
*vreg = *physical;
}
}
}
}
remap_nested_parent_slots(&mut child.nested, map);
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -95,6 +117,67 @@ mod tests {
}
}
#[test]
fn alloc_remaps_high_parameter_segment() {
let mut f = IRFunction::new();
f.param_layout = oxide_ir::ParamLayout { base: 300, count: 1 };
f.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(300), Operand::None, Operand::None));
let cfg = oxide_cfg::build_cfg(&f);
let live = oxide_liveness::liveness(&f, &cfg);
alloc(&mut f, &live).unwrap();
assert_eq!(f.param_layout, oxide_ir::ParamLayout { base: 1, count: 1 });
assert!(matches!(f.insts[0].rd, Operand::Reg(1)));
assert_eq!(f.n_registers, 2);
}
#[test]
fn alloc_propagates_moved_parameter_to_nested_slot_reads() {
let mut f = IRFunction::new();
f.param_layout = oxide_ir::ParamLayout { base: 300, count: 1 };
f.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(300), Operand::None, Operand::None));
let mut child = IRFunction::new();
child.param_layout = oxide_ir::ParamLayout { base: 301, count: 0 };
child
.insts
.push(Inst::new(OpCode::LOAD_VAR, Operand::Reg(302), Operand::Reg(300), Operand::None));
child
.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(302), Operand::None, Operand::None));
f.nested.push(child);
let cfg = oxide_cfg::build_cfg(&f);
let live = oxide_liveness::liveness(&f, &cfg);
alloc(&mut f, &live).unwrap();
assert!(matches!(f.nested[0].insts[0].a, Operand::Reg(1)));
}
#[test]
fn alloc_propagates_high_escaped_slot_to_nested_reads() {
let mut f = IRFunction::new();
f.insts.push(Inst::load_const(Operand::Reg(254), 0));
f.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(254), Operand::None, Operand::None));
let mut child = IRFunction::new();
child.param_layout = oxide_ir::ParamLayout { base: 300, count: 0 };
child
.insts
.push(Inst::new(OpCode::LOAD_VAR, Operand::Reg(301), Operand::Reg(254), Operand::None));
child
.insts
.push(Inst::new(OpCode::RETURN, Operand::Reg(301), Operand::None, Operand::None));
f.nested.push(child);
let cfg = oxide_cfg::build_cfg(&f);
let live = oxide_liveness::liveness(&f, &cfg);
alloc(&mut f, &live).unwrap();
let physical = match f.insts[0].rd {
Operand::Reg(reg) => reg,
_ => panic!("LOAD_CONST 目标应为寄存器"),
};
assert!((1..=253).contains(&physical));
assert!(matches!(f.nested[0].insts[0].a, Operand::Reg(reg) if reg == physical));
}
#[test]
fn alloc_err_propagates() {
// 255 参数 CALL:参数窗口吞并全部可分配色 → k=0 → 无可行染色 → Err