diff --git a/crates/oxide_regalloc/src/alloc_map.rs b/crates/oxide_regalloc/src/alloc_map.rs index 5ab0a71..39b2317 100644 --- a/crates/oxide_regalloc/src/alloc_map.rs +++ b/crates/oxide_regalloc/src/alloc_map.rs @@ -47,7 +47,7 @@ pub struct AllocMap { pub map: BTreeMap, /// spill 决策表(按 vreg 升序) pub spills: Vec, - /// 物理峰值(finish 回写 n_registers,≤253) + /// 物理窗口大小(finish 回写 n_registers,最高槽 253 对应值 254)。 pub phys_peak: u32, /// 参数窗口基址(调用点 MOV 补位,max_nargs=0 时 = 254 无窗口) pub arg_window_base: u32, diff --git a/crates/oxide_regalloc/src/color.rs b/crates/oxide_regalloc/src/color.rs index 11b1996..65028ff 100644 --- a/crates/oxide_regalloc/src/color.rs +++ b/crates/oxide_regalloc/src/color.rs @@ -17,7 +17,12 @@ use crate::graph::{self, InterferenceGraph}; /// 主染色循环:产出 AllocMap。 pub(super) fn run(f: &IRFunction, live: &LiveInfo) -> Result { - 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 = BTreeSet::new(); @@ -64,7 +69,7 @@ pub(super) fn run(f: &IRFunction, live: &LiveInfo) -> Result { 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, Vec) /// 装配 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, + colors: BTreeMap, spill_set: BTreeSet, fresh: Vec, slot_for_vreg: BTreeMap, + arg_window_base: u32, ) -> Result { 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"); diff --git a/crates/oxide_regalloc/src/finish.rs b/crates/oxide_regalloc/src/finish.rs index 2f848e1..f4dce80 100644 --- a/crates/oxide_regalloc/src/finish.rs +++ b/crates/oxide_regalloc/src/finish.rs @@ -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(); diff --git a/crates/oxide_regalloc/src/graph.rs b/crates/oxide_regalloc/src/graph.rs index 5d06602..2bb7cf2 100644 --- a/crates/oxide_regalloc/src/graph.rs +++ b/crates/oxide_regalloc/src/graph.rs @@ -95,30 +95,10 @@ pub(super) fn collect_real_vregs(f: &IRFunction) -> BTreeSet { pub(super) fn build( f: &IRFunction, live: &LiveInfo, spill_set: &BTreeSet, 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 = 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 = 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 = BTreeMap::new(); + // 当前函数读取的父槽已经是父函数分配后的物理号,必须恒等保护。 + let mut escaped_colors: Vec = 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 = 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, out: &mut Vec) { +/// 递归收集 nested 树中的父槽虚拟号(LOAD_VAR.a 读槽、STORE_VAR.rd 写槽)。 +fn collect_escaped(nested: &[IRFunction], out: &mut Vec) { 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, 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(); diff --git a/crates/oxide_regalloc/src/lib.rs b/crates/oxide_regalloc/src/lib.rs index 8876e50..ffe5ea5 100644 --- a/crates/oxide_regalloc/src/lib.rs +++ b/crates/oxide_regalloc/src/lib.rs @@ -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 diff --git a/crates/oxide_regalloc/tests/smoke.rs b/crates/oxide_regalloc/tests/smoke.rs index 5e7604c..62f6717 100644 --- a/crates/oxide_regalloc/tests/smoke.rs +++ b/crates/oxide_regalloc/tests/smoke.rs @@ -46,7 +46,7 @@ fn smoke(src: &str) { 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); + assert!(m.phys_peak <= 254, "phys_peak {} 超 254", m.phys_peak); } fn is_precolored(ir: &oxide_ir::IRFunction, c: u32) -> bool {