diff --git a/crates/rue-codegen/src/aarch64/cfg_lower.rs b/crates/rue-codegen/src/aarch64/cfg_lower.rs index 94de1c6f0..092dbbda0 100644 --- a/crates/rue-codegen/src/aarch64/cfg_lower.rs +++ b/crates/rue-codegen/src/aarch64/cfg_lower.rs @@ -375,8 +375,13 @@ impl<'a> CfgLower<'a> { } } + // The manifest's control contract travels with the call: a trap helper + // aborts, so liveness must not propagate anything past it (RUE-1224). let symbol_id = self.intern_symbol(plan.symbol()); - self.mir.push(Aarch64Inst::Bl { symbol_id }); + self.mir.push(Aarch64Inst::Bl { + symbol_id, + returns: plan.return_behavior(), + }); match plan.result() { RuntimeCallResult::OutPointer(shape) => { @@ -880,7 +885,7 @@ impl<'a> CfgLower<'a> { }); } let symbol_id = self.intern_symbol(plan.target.symbol()); - self.mir.push(Aarch64Inst::Bl { symbol_id }); + self.mir.push(Aarch64Inst::call(symbol_id)); if plan.stack_bytes > 0 { self.mir.push(Aarch64Inst::AddImm { dst: Operand::Physical(Reg::Sp), @@ -1153,7 +1158,7 @@ impl<'a> CfgLower<'a> { }); } let symbol_id = self.intern_symbol(inputs.symbol_ref()); - self.mir.push(Aarch64Inst::Bl { symbol_id }); + self.mir.push(Aarch64Inst::call(symbol_id)); if stack_bytes > 0 { self.mir.push(Aarch64Inst::AddImm { dst: Operand::Physical(Reg::Sp), @@ -4263,7 +4268,7 @@ mod tests { mir.instructions() .iter() .position(|inst| { - matches!(inst, Aarch64Inst::Bl { symbol_id } if mir.get_symbol(*symbol_id) == helper.symbol()) + matches!(inst, Aarch64Inst::Bl { symbol_id, .. } if mir.get_symbol(*symbol_id) == helper.symbol()) }) .unwrap_or_else(|| panic!("missing call to {helper:?}")) } diff --git a/crates/rue-codegen/src/aarch64/emit.rs b/crates/rue-codegen/src/aarch64/emit.rs index 9f6088c25..cfbfe8514 100644 --- a/crates/rue-codegen/src/aarch64/emit.rs +++ b/crates/rue-codegen/src/aarch64/emit.rs @@ -1383,7 +1383,7 @@ impl<'a> Emitter<'a> { self.record_label(format!("L{}", id)); } - Aarch64Inst::Bl { symbol_id } => { + Aarch64Inst::Bl { symbol_id, .. } => { let symbol = self.mir.get_symbol(*symbol_id); self.begin_inst(); self.emit_bl(symbol); @@ -3386,7 +3386,7 @@ mod tests { let mut mir = Aarch64Mir::new(); let symbol_id = mir.intern_symbol("test_func"); - mir.push(Aarch64Inst::Bl { symbol_id }); + mir.push(Aarch64Inst::call(symbol_id)); let (code, relocs) = Emitter::new(&mir, 0, 0, 0, &[], &[]) .without_frame() @@ -3428,7 +3428,7 @@ mod tests { // (RUE-1195). let mut mir = Aarch64Mir::new(); let symbol_id = mir.intern_symbol("callee"); - mir.push(Aarch64Inst::Bl { symbol_id }); + mir.push(Aarch64Inst::call(symbol_id)); mir.push(Aarch64Inst::Ret); let expected: Vec = vec![ diff --git a/crates/rue-codegen/src/aarch64/liveness.rs b/crates/rue-codegen/src/aarch64/liveness.rs index cc68aaad3..f9615e53e 100644 --- a/crates/rue-codegen/src/aarch64/liveness.rs +++ b/crates/rue-codegen/src/aarch64/liveness.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; -use super::mir::{Aarch64Inst, Aarch64Mir, Operand, Reg}; +use super::mir::{Aarch64Inst, Aarch64Mir, Operand, Reg, ReturnBehavior}; use crate::liveness::{ LivenessAdapter, branch_successor, conditional_successors, fallthrough_successor, }; @@ -57,6 +57,10 @@ impl LivenessAdapter for Aarch64LivenessAdapter<'_> { fn clobbers(&self, inst: &Self::Inst) -> Vec { inst.clobbers().to_vec() } + + fn is_non_returning(&self, inst: &Self::Inst) -> bool { + inst.is_non_returning() + } } /// Compute liveness information for Aarch64Mir. @@ -111,7 +115,14 @@ fn get_successors( } // Return and trap have no successors Aarch64Inst::Ret | Aarch64Inst::Brk => Vec::new(), - // Function calls fall through (callee returns) + // A call to a helper the runtime ABI manifest declares + // `ReturnBehavior::Never` aborts the process. Control never comes back, + // so it has no successors and nothing is live after it (RUE-1224). + Aarch64Inst::Bl { + returns: ReturnBehavior::Never, + .. + } => Vec::new(), + // Ordinary calls fall through (the callee returns) Aarch64Inst::Bl { .. } => fallthrough_successor(idx, num_insts), // All other instructions fall through to the next _ => fallthrough_successor(idx, num_insts), diff --git a/crates/rue-codegen/src/aarch64/mir.rs b/crates/rue-codegen/src/aarch64/mir.rs index 9f5edbac4..a68446fd0 100644 --- a/crates/rue-codegen/src/aarch64/mir.rs +++ b/crates/rue-codegen/src/aarch64/mir.rs @@ -30,6 +30,8 @@ use std::collections::HashMap; use std::fmt; +pub use rue_runtime_abi::ReturnBehavior; + // Compile-time size assertions to prevent silent size growth during refactoring. // These limits are set slightly above current sizes to allow minor changes, // but will catch significant size regressions. @@ -893,7 +895,16 @@ pub enum Aarch64Inst { /// `bl symbol` - Branch with link (call). /// /// The `symbol_id` is an index into the symbol table stored in `Aarch64Mir`. - Bl { symbol_id: u32 }, + /// + /// `returns` carries the callee's control contract. Every Rue-to-Rue call + /// returns; a runtime helper carries whatever the ABI manifest declares, so + /// the trap helpers (`__rue_overflow` and siblings) are `Never`. Liveness + /// gives a `Never` call no successors, and allocation does not count its + /// clobbers against a value that is only live around it (RUE-1224). + Bl { + symbol_id: u32, + returns: ReturnBehavior, + }, /// `ret` - Return (branch to LR). Ret, @@ -939,6 +950,30 @@ pub enum Aarch64Inst { } impl Aarch64Inst { + /// A call to a callee that returns normally. + /// + /// Every Rue-to-Rue call and every returning runtime helper uses this. A + /// call to a helper the ABI manifest declares `ReturnBehavior::Never` must + /// build [`Aarch64Inst::Bl`] directly with the manifest's behavior so + /// liveness sees it (RUE-1224). + pub const fn call(symbol_id: u32) -> Self { + Self::Bl { + symbol_id, + returns: ReturnBehavior::Returns, + } + } + + /// Whether this instruction never returns control to the next one. + pub const fn is_non_returning(&self) -> bool { + matches!( + self, + Aarch64Inst::Bl { + returns: ReturnBehavior::Never, + .. + } + ) + } + /// Returns physical registers clobbered by this instruction. /// /// This information is used by the register allocator to avoid assigning @@ -1252,7 +1287,7 @@ impl fmt::Display for Aarch64Inst { Aarch64Inst::Bvs { label } => write!(f, "b.vs {}", label), Aarch64Inst::Bvc { label } => write!(f, "b.vc {}", label), Aarch64Inst::Label { id } => write!(f, "{}:", id), - Aarch64Inst::Bl { symbol_id } => write!(f, "bl sym{}", symbol_id), + Aarch64Inst::Bl { symbol_id, .. } => write!(f, "bl sym{}", symbol_id), Aarch64Inst::Ret => write!(f, "ret"), Aarch64Inst::Brk => write!(f, "brk #0x1"), Aarch64Inst::Svc { imm } => write!(f, "svc #{:#x}", imm), @@ -1456,7 +1491,7 @@ impl fmt::Display for Aarch64Mir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for inst in &self.instructions { // Special handling for Bl to show actual symbol name - if let Aarch64Inst::Bl { symbol_id } = inst { + if let Aarch64Inst::Bl { symbol_id, .. } = inst { writeln!(f, " bl {}", self.get_symbol(*symbol_id))?; } else { writeln!(f, " {}", inst)?; diff --git a/crates/rue-codegen/src/aarch64/regalloc.rs b/crates/rue-codegen/src/aarch64/regalloc.rs index 37f36f277..221de37a6 100644 --- a/crates/rue-codegen/src/aarch64/regalloc.rs +++ b/crates/rue-codegen/src/aarch64/regalloc.rs @@ -1050,7 +1050,9 @@ impl RegAlloc { Aarch64Inst::Bvs { label } => mir.push(Aarch64Inst::Bvs { label }), Aarch64Inst::Bvc { label } => mir.push(Aarch64Inst::Bvc { label }), Aarch64Inst::Label { id } => mir.push(Aarch64Inst::Label { id }), - Aarch64Inst::Bl { symbol_id } => mir.push(Aarch64Inst::Bl { symbol_id }), + Aarch64Inst::Bl { symbol_id, returns } => { + mir.push(Aarch64Inst::Bl { symbol_id, returns }) + } Aarch64Inst::Ret => mir.push(Aarch64Inst::Ret), Aarch64Inst::Brk => mir.push(Aarch64Inst::Brk), Aarch64Inst::Svc { imm } => mir.push(Aarch64Inst::Svc { imm }), @@ -1353,6 +1355,12 @@ impl RegAllocBackend for Aarch64Backend { } } + fn physical_operands(inst: &Self::Inst) -> Vec { + let mut regs = super::schedule::regs_read(inst); + regs.extend(super::schedule::regs_written(inst)); + regs + } + fn new_mir() -> Self::Mir { Aarch64Mir::new() } @@ -1944,7 +1952,7 @@ mod tests { .collect(); define_loaded_values(&mut mir, &vregs); - mir.push(Aarch64Inst::Bl { symbol_id: symbol }); + mir.push(Aarch64Inst::call(symbol)); for &vreg in &vregs { mir.push(Aarch64Inst::MovRR { dst: Operand::Physical(Reg::X0), @@ -1976,6 +1984,99 @@ mod tests { } } + #[test] + fn trap_crossing_values_still_take_caller_saved_registers() { + // The same fixture as `cross_call_values_never_take_a_caller_saved_register`, + // except the call is the overflow trap. It never returns, so no value + // is live across it and the caller-saved class stays available + // (RUE-1224). + let mut mir = Aarch64Mir::new(); + let symbol = mir.intern_symbol(rue_runtime_abi::RuntimeHelperId::Overflow.symbol()); + let vregs: Vec = (0..ALLOCATABLE_REGS.len()) + .map(|_| mir.alloc_vreg()) + .collect(); + + define_loaded_values(&mut mir, &vregs); + mir.push(Aarch64Inst::Bl { + symbol_id: symbol, + returns: rue_runtime_abi::ReturnBehavior::Never, + }); + for &vreg in &vregs { + mir.push(Aarch64Inst::MovRR { + dst: Operand::Physical(Reg::X0), + src: Operand::Virtual(vreg), + }); + } + + let (mir, num_spills, _) = RegAlloc::new(mir, 0).allocate_with_spills().unwrap(); + + assert_eq!( + num_spills, 0, + "the trap is not a barrier, so every value should still fit in a register" + ); + let assigned: Vec = mir + .instructions() + .iter() + .filter_map(|inst| match inst { + Aarch64Inst::Ldr { + dst: Operand::Physical(reg), + base: Reg::X1, + .. + } => Some(*reg), + _ => None, + }) + .collect(); + for ® in CALLER_SAVED_REGS { + assert!( + assigned.contains(®), + "a value spanning only the trap should be allowed caller-saved {reg}" + ); + } + } + + #[test] + fn a_never_returning_call_ends_liveness() { + // Modelling the trap as non-returning is what makes the clobber test + // above correct: nothing propagates past it. + let mut mir = Aarch64Mir::new(); + let value = mir.alloc_vreg(); + let symbol = mir.intern_symbol(rue_runtime_abi::RuntimeHelperId::Overflow.symbol()); + + mir.push(Aarch64Inst::MovImm { + dst: Operand::Virtual(value), + imm: 42, + }); + mir.push(Aarch64Inst::Bl { + symbol_id: symbol, + returns: rue_runtime_abi::ReturnBehavior::Never, + }); + mir.push(Aarch64Inst::MovRR { + dst: Operand::Physical(Reg::X0), + src: Operand::Virtual(value), + }); + mir.push(Aarch64Inst::Ret); + + let debug = liveness::analyze_debug(&mir); + assert!( + debug.instructions[1].live_out.is_empty(), + "a never-returning call must have an empty live-out set; found: {:?}", + debug.instructions[1].live_out + ); + } + + #[test] + #[should_panic(expected = "lowering named the allocatable register")] + fn lowering_may_not_name_an_allocatable_register_as_a_physical_operand() { + let mut mir = Aarch64Mir::new(); + let value = mir.alloc_vreg(); + mir.push(Aarch64Inst::MovRR { + dst: Operand::Virtual(value), + src: Operand::Physical(ALLOCATABLE_REGS[0]), + }); + + let _ = RegAlloc::new(mir, 0).allocate(); + } + #[test] fn test_call_survival_and_symbol_reconstruction() { let mut mir = Aarch64Mir::new(); @@ -1986,7 +2087,7 @@ mod tests { dst: Operand::Virtual(value), imm: 42, }); - mir.push(Aarch64Inst::Bl { symbol_id: symbol }); + mir.push(Aarch64Inst::call(symbol)); mir.push(Aarch64Inst::MovRR { dst: Operand::Physical(Reg::X0), src: Operand::Virtual(value), @@ -2007,9 +2108,9 @@ mod tests { assert!(value_reg.is_callee_saved()); assert!( - mir.instructions() - .iter() - .any(|inst| matches!(inst, Aarch64Inst::Bl { symbol_id } if *symbol_id == symbol)) + mir.instructions().iter().any( + |inst| matches!(inst, Aarch64Inst::Bl { symbol_id, .. } if *symbol_id == symbol) + ) ); assert!(mir.instructions().iter().any(|inst| matches!( inst, diff --git a/crates/rue-codegen/src/aarch64/schedule.rs b/crates/rue-codegen/src/aarch64/schedule.rs index 2ace68d39..a80b3ac4c 100644 --- a/crates/rue-codegen/src/aarch64/schedule.rs +++ b/crates/rue-codegen/src/aarch64/schedule.rs @@ -235,7 +235,7 @@ fn accesses_memory(inst: &Aarch64Inst) -> bool { } /// Get registers read by an instruction (for dependency analysis). -fn regs_read(inst: &Aarch64Inst) -> Vec { +pub(super) fn regs_read(inst: &Aarch64Inst) -> Vec { let mut result = Vec::new(); let add_if_phys = |op: &Operand, vec: &mut Vec| { @@ -364,7 +364,7 @@ fn regs_read(inst: &Aarch64Inst) -> Vec { } /// Get registers written by an instruction (for dependency analysis). -fn regs_written(inst: &Aarch64Inst) -> Vec { +pub(super) fn regs_written(inst: &Aarch64Inst) -> Vec { let mut result = Vec::new(); let add_if_phys = |op: &Operand, vec: &mut Vec| { @@ -620,7 +620,7 @@ mod tests { id: LabelId::new(0) })); assert!(is_barrier(&Aarch64Inst::Ret)); - assert!(is_barrier(&Aarch64Inst::Bl { symbol_id: 0 })); + assert!(is_barrier(&Aarch64Inst::call(0))); assert!(!is_barrier(&Aarch64Inst::MovRR { dst: Operand::Physical(Reg::X0), diff --git a/crates/rue-codegen/src/aarch64/verify.rs b/crates/rue-codegen/src/aarch64/verify.rs index 2966d8392..a6a19cccc 100644 --- a/crates/rue-codegen/src/aarch64/verify.rs +++ b/crates/rue-codegen/src/aarch64/verify.rs @@ -222,7 +222,7 @@ mod tests { src2: Operand::Physical(Reg::X22), offset: -16, }); - mir.push(Aarch64Inst::Bl { symbol_id: sym_id }); + mir.push(Aarch64Inst::call(sym_id)); mir.push(Aarch64Inst::LdpPost { dst1: Operand::Physical(Reg::X21), dst2: Operand::Physical(Reg::X22), @@ -242,7 +242,7 @@ mod tests { fn test_aligned_call() { let mut mir = Aarch64Mir::new(); let sym_id = mir.intern_symbol("test_func"); - mir.push(Aarch64Inst::Bl { symbol_id: sym_id }); + mir.push(Aarch64Inst::call(sym_id)); mir.push(Aarch64Inst::Ret); assert!(verify_stack_alignment(&mir).is_ok()); @@ -261,7 +261,7 @@ mod tests { imm: 32, }); // Call (should be aligned) - mir.push(Aarch64Inst::Bl { symbol_id: sym_id }); + mir.push(Aarch64Inst::call(sym_id)); // Deallocate mir.push(Aarch64Inst::AddImm { dst: Operand::Physical(Reg::Sp), @@ -286,7 +286,7 @@ mod tests { imm: 8, }); // Call without proper alignment - should fail - mir.push(Aarch64Inst::Bl { symbol_id: sym_id }); + mir.push(Aarch64Inst::call(sym_id)); mir.push(Aarch64Inst::Ret); let result = verify_stack_alignment(&mir); @@ -321,8 +321,8 @@ mod tests { let sym1 = mir.intern_symbol("func1"); let sym2 = mir.intern_symbol("func2"); - mir.push(Aarch64Inst::Bl { symbol_id: sym1 }); - mir.push(Aarch64Inst::Bl { symbol_id: sym2 }); + mir.push(Aarch64Inst::call(sym1)); + mir.push(Aarch64Inst::call(sym2)); mir.push(Aarch64Inst::Ret); assert!(verify_stack_alignment(&mir).is_ok()); diff --git a/crates/rue-codegen/src/liveness.rs b/crates/rue-codegen/src/liveness.rs index 9fbed4a9f..eb6504b1b 100644 --- a/crates/rue-codegen/src/liveness.rs +++ b/crates/rue-codegen/src/liveness.rs @@ -63,6 +63,16 @@ pub trait LivenessAdapter { /// Return physical registers clobbered by `inst`. fn clobbers(&self, inst: &Self::Inst) -> Vec; + + /// Whether `inst` never returns control to the instruction after it. + /// + /// This is true only for a call to a runtime helper the ABI manifest + /// declares `ReturnBehavior::Never`. Such an instruction reports no + /// successors, so nothing is live after it, and its clobbers cannot reach + /// any value whose later uses execute (RUE-1224). Instructions that are + /// terminal without being calls — `ret`, `ud2`, `brk` — clobber nothing, so + /// classifying them either way is immaterial and they answer `false`. + fn is_non_returning(&self, inst: &Self::Inst) -> bool; } /// Compute liveness for any backend implementing [`LivenessAdapter`]. @@ -78,6 +88,7 @@ where |inst| adapter.uses(inst), |inst| adapter.defs(inst), |inst| adapter.clobbers(inst), + |inst| adapter.is_non_returning(inst), ) } @@ -111,6 +122,7 @@ where |inst| adapter.uses(inst), |inst| adapter.defs(inst), |inst| adapter.clobbers(inst), + |inst| adapter.is_non_returning(inst), ) } @@ -182,6 +194,9 @@ pub fn conditional_successors( /// * `get_uses` - Returns the virtual registers used (read) by the instruction /// * `get_defs` - Returns the virtual registers defined (written) by the instruction /// * `get_clobbers` - Returns the physical registers clobbered by the instruction +/// * `get_non_returning` - Returns whether the instruction never returns control +/// to the instruction after it +#[allow(clippy::too_many_arguments)] pub fn analyze( instructions: &[I], vreg_count: u32, @@ -190,6 +205,7 @@ pub fn analyze( get_uses: impl Fn(&I) -> Vec, get_defs: impl Fn(&I) -> Vec, get_clobbers: impl Fn(&I) -> Vec, + get_non_returning: impl Fn(&I) -> bool, ) -> LivenessInfo where R: Copy + Eq + std::hash::Hash, @@ -202,6 +218,7 @@ where get_uses, get_defs, get_clobbers, + get_non_returning, false, ) .0 @@ -209,6 +226,7 @@ where /// Compute production liveness and its diagnostic projection in one dataflow /// execution. +#[allow(clippy::too_many_arguments)] pub fn analyze_with_debug( instructions: &[I], vreg_count: u32, @@ -217,6 +235,7 @@ pub fn analyze_with_debug( get_uses: impl Fn(&I) -> Vec, get_defs: impl Fn(&I) -> Vec, get_clobbers: impl Fn(&I) -> Vec, + get_non_returning: impl Fn(&I) -> bool, ) -> (LivenessInfo, LivenessDebugInfo) where R: Copy + Eq + std::hash::Hash, @@ -229,6 +248,7 @@ where get_uses, get_defs, get_clobbers, + get_non_returning, true, ); ( @@ -246,6 +266,7 @@ fn analyze_inner( get_uses: impl Fn(&I) -> Vec, get_defs: impl Fn(&I) -> Vec, get_clobbers: impl Fn(&I) -> Vec, + get_non_returning: impl Fn(&I) -> bool, collect_debug: bool, ) -> (LivenessInfo, Option) where @@ -259,6 +280,7 @@ where ranges: IndexMap::new(), live_at: Vec::new(), clobbers_at: Vec::new(), + non_returning_at: Vec::new(), }, collect_debug.then(|| LivenessDebugInfo { instructions: Vec::new(), @@ -297,8 +319,9 @@ where // Step 6: Compute live_at for each instruction (union of live_in and live_out) let live_at = compute_live_at(num_insts, vreg_count, &live_in, &live_out); - // Step 7: Collect clobbers + // Step 7: Collect clobbers and the never-returning call sites (RUE-1224) let clobbers_at: Vec> = instructions.iter().map(|i| get_clobbers(i)).collect(); + let non_returning_at: Vec = instructions.iter().map(&get_non_returning).collect(); let debug = collect_debug.then(|| { let bitset_to_hashset = |bs: &FixedBitSet| -> std::collections::HashSet { @@ -325,6 +348,7 @@ where ranges, live_at, clobbers_at, + non_returning_at, }, debug, ) @@ -353,6 +377,7 @@ where get_uses, get_defs, |_| Vec::::new(), + |_| false, ) .1 } @@ -782,6 +807,7 @@ mod tests { test_get_uses, test_get_defs, test_get_clobbers, + |_| false, ); // v0: defined at 0, used at 1 @@ -811,6 +837,7 @@ mod tests { test_get_uses, test_get_defs, test_get_clobbers, + |_| false, ); // v0: defined at 0, last used at 4 @@ -832,6 +859,7 @@ mod tests { test_get_uses, test_get_defs, test_get_clobbers, + |_| false, ); assert!(info.ranges.is_empty()); @@ -857,6 +885,7 @@ mod tests { test_get_uses, test_get_defs, test_get_clobbers, + |_| false, ); // v0 and v1 should interfere (both live at instruction 2) diff --git a/crates/rue-codegen/src/place_lower.rs b/crates/rue-codegen/src/place_lower.rs index 7db7437a9..0ed5c0cd3 100644 --- a/crates/rue-codegen/src/place_lower.rs +++ b/crates/rue-codegen/src/place_lower.rs @@ -549,7 +549,7 @@ mod tests { .position(|inst| { matches!( inst, - X86Inst::CallRel { symbol_id } + X86Inst::CallRel { symbol_id, .. } if pair_x86.get_symbol(*symbol_id) == "__rue_bounds_check" ) }) @@ -578,7 +578,7 @@ mod tests { .position(|inst| { matches!( inst, - Aarch64Inst::Bl { symbol_id } + Aarch64Inst::Bl { symbol_id, .. } if pair_arm.get_symbol(*symbol_id) == "__rue_bounds_check" ) }) @@ -677,10 +677,10 @@ mod tests { .lower() .expect("AArch64 indexed ZST fixture should lower"); assert!(unit_index_x86.instructions().iter().any(|inst| { - matches!(inst, X86Inst::CallRel { symbol_id } if unit_index_x86.get_symbol(*symbol_id) == "__rue_bounds_check") + matches!(inst, X86Inst::CallRel { symbol_id, .. } if unit_index_x86.get_symbol(*symbol_id) == "__rue_bounds_check") })); assert!(unit_index_arm.instructions().iter().any(|inst| { - matches!(inst, Aarch64Inst::Bl { symbol_id } if unit_index_arm.get_symbol(*symbol_id) == "__rue_bounds_check") + matches!(inst, Aarch64Inst::Bl { symbol_id, .. } if unit_index_arm.get_symbol(*symbol_id) == "__rue_bounds_check") })); let unit_write_cfg = build_cfg("write_unit_index"); @@ -697,10 +697,10 @@ mod tests { .lower() .expect("AArch64 indexed ZST write fixture should lower"); assert!(unit_write_x86.instructions().iter().any(|inst| { - matches!(inst, X86Inst::CallRel { symbol_id } if unit_write_x86.get_symbol(*symbol_id) == "__rue_bounds_check") + matches!(inst, X86Inst::CallRel { symbol_id, .. } if unit_write_x86.get_symbol(*symbol_id) == "__rue_bounds_check") })); assert!(unit_write_arm.instructions().iter().any(|inst| { - matches!(inst, Aarch64Inst::Bl { symbol_id } if unit_write_arm.get_symbol(*symbol_id) == "__rue_bounds_check") + matches!(inst, Aarch64Inst::Bl { symbol_id, .. } if unit_write_arm.get_symbol(*symbol_id) == "__rue_bounds_check") })); } diff --git a/crates/rue-codegen/src/regalloc.rs b/crates/rue-codegen/src/regalloc.rs index 13ed2fc97..8560b0b5e 100644 --- a/crates/rue-codegen/src/regalloc.rs +++ b/crates/rue-codegen/src/regalloc.rs @@ -384,6 +384,15 @@ pub struct LivenessInfo { /// For each instruction index, the physical registers clobbered by that instruction. /// This is used to prevent allocating vregs to registers that would be clobbered. pub clobbers_at: Vec>, + /// For each instruction index, whether control can reach the instruction + /// after it once it executes. + /// + /// Only a call to a helper the runtime ABI manifest declares + /// `ReturnBehavior::Never` sets this — the overflow, bounds-check, + /// divide-by-zero, panic, and exit traps. Such a call has no successors, so + /// no value is live after it; see [`ClobberIndex::build`] for why the + /// distinction matters to allocation (RUE-1224). + pub non_returning_at: Vec, } impl LivenessInfo { @@ -393,6 +402,7 @@ impl LivenessInfo { ranges: IndexMap::new(), live_at: Vec::new(), clobbers_at: Vec::new(), + non_returning_at: Vec::new(), } } @@ -404,6 +414,7 @@ impl LivenessInfo { ranges, live_at: Vec::new(), clobbers_at: Vec::new(), + non_returning_at: Vec::new(), } } @@ -432,6 +443,19 @@ impl LivenessInfo { pub fn clobbers_at(&self, inst_idx: usize) -> &[Reg] { &self.clobbers_at[inst_idx] } + + /// Whether the instruction at `inst_idx` never returns control to the + /// instruction after it. + /// + /// Indices outside the analyzed instruction sequence answer `false`; so + /// does any liveness built without this information, which keeps the + /// pre-RUE-1224 behavior for hand-constructed test liveness. + pub fn is_non_returning(&self, inst_idx: usize) -> bool { + self.non_returning_at + .get(inst_idx) + .copied() + .unwrap_or(false) + } } // ============================================================================ @@ -453,6 +477,9 @@ impl LivenessInfo { /// counts at its two endpoints agree. Building the index is O(tracked × /// instructions) once per function; each query is O(tracked) lookup plus two /// array reads. +/// +/// A never-returning call contributes no clobber event (RUE-1224). See +/// [`ClobberIndex::build`]. pub struct ClobberIndex { /// One entry per tracked register: the register, and prefix counts where /// `counts[i]` is the number of instructions before `i` that clobber it. @@ -464,6 +491,23 @@ impl ClobberIndex { /// Build an index over `liveness`'s clobber data for `tracked`. /// /// Only the tracked registers get an answer; see [`Self::is_clobbered_during`]. + /// + /// A never-returning call is skipped. Rue lowers every checked `+`/`*` as + /// `jno .L; call __rue_overflow; .L:`, so that trap call sits textually + /// inside almost every arithmetic value's live range — but the value is + /// live *around* the call, not through it. `__rue_overflow` and its sibling + /// traps are declared `ReturnBehavior::Never` by the runtime ABI manifest + /// and abort the process, so on every path where a later use of the value + /// executes, the call did not. It cannot destroy a value that is already + /// dead if it runs, and a live range is a textual interval that cannot + /// express that (RUE-1224). + /// + /// This is deliberately narrower than "the call is on a cold path": the + /// call's *arguments* are ordinary uses, live at the call and honored by + /// the ranges above; only the clobber event is dropped. If a trap does + /// fire, a register holding a user value may hold anything by the time the + /// handler runs. Nothing observes that: Rue emits no DWARF and the traps + /// print a fixed message and exit without a backtrace (RUE-1146's audit). pub fn build(liveness: &LivenessInfo, tracked: &[Reg]) -> Self where Reg: std::hash::Hash, @@ -476,7 +520,7 @@ impl ClobberIndex { let mut running = 0_u32; counts.push(running); for idx in 0..num_insts { - if liveness.clobbers_at(idx).contains(®) { + if !liveness.is_non_returning(idx) && liveness.clobbers_at(idx).contains(®) { running += 1; } counts.push(running); @@ -1058,6 +1102,16 @@ pub trait RegAllocBackend { /// instruction clobbers while they are live (RUE-1146). fn register_classes() -> RegisterClasses<'static, Self::Reg>; + /// Every physical register `inst` names as an operand — read or written. + /// + /// This is the exhaustive per-instruction enumeration each backend's + /// scheduler already maintains (`regs_read` + `regs_written`), reused here + /// so [`RegAllocDriver::new_with_artifacts`] can prove lowering never named + /// an allocatable register directly. Implicit clobbers are deliberately not + /// included: a call destroys the caller-saved registers without naming any + /// of them as an operand, and the allocator models that separately. + fn physical_operands(inst: &Self::Inst) -> Vec; + fn new_mir() -> Self::Mir; fn take_symbols(mir: &mut Self::Mir) -> Vec; fn set_symbols(mir: &mut Self::Mir, symbols: Vec); @@ -1200,6 +1254,35 @@ impl RewriteBuffer { } } +/// Fail loudly if pre-allocation MIR names an allocatable register directly. +/// +/// Lowering names physical registers for ABI positions, fixed instruction +/// operands, and the stack and frame pointers. The allocator hands out a +/// disjoint set, and each backend proves the two sets are disjoint at compile +/// time from its `RESERVED_REGS` table. That proof is only as good as the +/// table: a lowering site that names, say, `r11` as a raw operand would collide +/// with whatever value the allocator put there, silently, and only on the +/// programs where the allocator happened to pick that register. +/// +/// So this checks the actual instruction stream instead of the table. It runs +/// before assignment, when every value still lives in a virtual register, so +/// any physical operand present is one lowering wrote. It is an always-on +/// assertion rather than a `debug_assert!` because a violation changes emitted +/// code, and `docs/process/ci.md` gives code generation no debug-assert +/// allowance (RUE-1224). +fn assert_no_allocatable_physical_operands(mir: &B::Mir) { + let classes = B::register_classes(); + for inst in B::instructions(mir) { + for reg in B::physical_operands(inst) { + assert!( + !classes.caller_saved.contains(®) && !classes.callee_saved.contains(®), + "lowering named the allocatable register {reg} as a physical operand; \ + allocation may put an unrelated value there" + ); + } + } +} + /// Shared assignment, rewrite, and spill orchestration for one target. pub struct RegAllocDriver { mir: B::Mir, @@ -1223,6 +1306,7 @@ impl RegAllocDriver { /// Create allocator state while optionally retaining the diagnostic /// projection of the same liveness dataflow used for allocation. pub fn new_with_artifacts(mir: B::Mir, existing_locals: u32, capture_liveness: bool) -> Self { + assert_no_allocatable_physical_operands::(&mir); let vreg_count = B::vreg_count(&mir) as usize; let (mut liveness, liveness_debug) = if capture_liveness { let (liveness, debug) = B::analyze_with_debug(&mir); @@ -2155,6 +2239,83 @@ mod tests { info } + fn mark_non_returning(info: &mut LivenessInfo, indices: &[usize]) { + info.non_returning_at = vec![false; info.clobbers_at.len()]; + for &idx in indices { + info.non_returning_at[idx] = true; + } + } + + #[test] + fn clobber_index_ignores_a_never_returning_call_site() { + // The shape Rue emits for every checked add: the value is defined + // before the guard branch and used after the label, so the trap call at + // instruction 2 sits textually inside its range — but the trap aborts, + // so on the path reaching the later use the call never ran. + let mut liveness = make_liveness_with_clobbers(vec![(0, 0, 4)], vec![(2, TestReg(0))]); + mark_non_returning(&mut liveness, &[2]); + let index = ClobberIndex::build(&liveness, &[TestReg(0)]); + + assert!(!index.is_clobbered_during(TestReg(0), &LiveRange::new(0, 4))); + assert!(!index.is_clobbered_during(TestReg(0), &LiveRange::new(2, 2))); + } + + #[test] + fn clobber_index_still_sees_a_returning_call_beside_a_trap() { + // A returning call at 1 and a trap at 3: only the returning one can + // destroy a value whose later uses execute. + let mut liveness = + make_liveness_with_clobbers(vec![(0, 0, 4)], vec![(1, TestReg(0)), (3, TestReg(0))]); + mark_non_returning(&mut liveness, &[3]); + let index = ClobberIndex::build(&liveness, &[TestReg(0)]); + + assert!(index.is_clobbered_during(TestReg(0), &LiveRange::new(0, 4))); + assert!(index.is_clobbered_during(TestReg(0), &LiveRange::new(0, 1))); + assert!( + !index.is_clobbered_during(TestReg(0), &LiveRange::new(2, 4)), + "a range that clears the returning call is clobber-free despite the trap" + ); + } + + #[test] + fn a_trap_spanning_interval_takes_a_caller_saved_register() { + // Same allocation shape as `caller_saved_is_preferred_...`, except the + // clobber site is a never-returning call: now both intervals fit in the + // caller-saved class and the prologue saves nothing. + let mut liveness = make_liveness_with_clobbers( + vec![(0, 0, 4), (1, 3, 4)], + vec![(2, TestReg(9)), (2, TestReg(8))], + ); + mark_non_returning(&mut liveness, &[2]); + let classes = RegisterClasses { + caller_saved: &[TestReg(9), TestReg(8)], + callee_saved: &[TestReg(0)], + }; + let (allocation, num_spills, used_callee_saved, _) = linear_scan_impl( + 2, + &liveness, + classes, + 0, + false, + &CostModel::default(), + &LoopInfo::no_loops(liveness.live_at.len()), + ); + + assert_eq!(num_spills, 0); + assert_eq!( + allocation[VReg::new(0)], + Some(Allocation::Register(TestReg(9))) + ); + assert_eq!( + allocation[VReg::new(1)], + Some(Allocation::Register(TestReg(8))) + ); + assert!( + used_callee_saved.is_empty(), + "no interval needed a callee-saved register, so the prologue saves nothing" + ); + } + #[test] fn clobber_index_answers_only_for_tracked_registers() { let liveness = make_liveness_with_clobbers(vec![(0, 0, 4)], vec![(2, TestReg(0))]); diff --git a/crates/rue-codegen/src/x86_64/cfg_lower.rs b/crates/rue-codegen/src/x86_64/cfg_lower.rs index 28fc5e818..f205f1788 100644 --- a/crates/rue-codegen/src/x86_64/cfg_lower.rs +++ b/crates/rue-codegen/src/x86_64/cfg_lower.rs @@ -382,8 +382,13 @@ impl<'a> CfgLower<'a> { } } + // The manifest's control contract travels with the call: a trap helper + // aborts, so liveness must not propagate anything past it (RUE-1224). let symbol_id = self.intern_symbol(plan.symbol()); - self.mir.push(X86Inst::CallRel { symbol_id }); + self.mir.push(X86Inst::CallRel { + symbol_id, + returns: plan.return_behavior(), + }); match plan.result() { RuntimeCallResult::OutPointer(shape) => { @@ -1075,7 +1080,7 @@ impl<'a> CfgLower<'a> { }); } let symbol_id = self.intern_symbol(plan.target.symbol()); - self.mir.push(X86Inst::CallRel { symbol_id }); + self.mir.push(X86Inst::call(symbol_id)); if num_stack_args > 0 || needs_alignment { self.mir.push(X86Inst::AddRI { dst: Operand::Physical(Reg::Rsp), @@ -1331,7 +1336,7 @@ impl<'a> CfgLower<'a> { }); } let symbol_id = self.intern_symbol(inputs.symbol_ref()); - self.mir.push(X86Inst::CallRel { symbol_id }); + self.mir.push(X86Inst::call(symbol_id)); if num_stack > 0 || needs_alignment { self.mir.push(X86Inst::AddRI { dst: Operand::Physical(Reg::Rsp), @@ -4110,7 +4115,7 @@ mod tests { mir.instructions() .iter() .position(|inst| { - matches!(inst, X86Inst::CallRel { symbol_id } if mir.get_symbol(*symbol_id) == helper.symbol()) + matches!(inst, X86Inst::CallRel { symbol_id, .. } if mir.get_symbol(*symbol_id) == helper.symbol()) }) .unwrap_or_else(|| panic!("missing call to {helper:?}")) } diff --git a/crates/rue-codegen/src/x86_64/emit.rs b/crates/rue-codegen/src/x86_64/emit.rs index b3adb05e4..64177edb3 100644 --- a/crates/rue-codegen/src/x86_64/emit.rs +++ b/crates/rue-codegen/src/x86_64/emit.rs @@ -1248,7 +1248,7 @@ impl<'a> Emitter<'a> { self.labels.insert(*id, self.code.len()); self.record_label(format!("{}", id)); } - X86Inst::CallRel { symbol_id } => { + X86Inst::CallRel { symbol_id, .. } => { let symbol = self.mir.get_symbol(*symbol_id); self.begin_inst(); self.emit_call_rel(symbol); @@ -3185,7 +3185,7 @@ mod tests { let mut mir = X86Mir::new(); let symbol_id = mir.intern_symbol("__rue_exit"); - mir.push(X86Inst::CallRel { symbol_id }); + mir.push(X86Inst::call(symbol_id)); let (code, relocs) = Emitter::new(&mir, 0, 0, 0, &[], &[]) .without_frame() @@ -3523,7 +3523,7 @@ mod tests { // 16-byte aligned at the call site (RUE-1195). let mut mir = X86Mir::new(); let symbol_id = mir.intern_symbol("callee"); - mir.push(X86Inst::CallRel { symbol_id }); + mir.push(X86Inst::call(symbol_id)); mir.push(X86Inst::Ret); let expected = vec![ diff --git a/crates/rue-codegen/src/x86_64/liveness.rs b/crates/rue-codegen/src/x86_64/liveness.rs index 266f38070..8e70bdf41 100644 --- a/crates/rue-codegen/src/x86_64/liveness.rs +++ b/crates/rue-codegen/src/x86_64/liveness.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; -use super::mir::{Operand, Reg, X86Inst, X86Mir}; +use super::mir::{Operand, Reg, ReturnBehavior, X86Inst, X86Mir}; use crate::liveness::{ LivenessAdapter, branch_successor, conditional_successors, fallthrough_successor, }; @@ -57,6 +57,10 @@ impl LivenessAdapter for X86LivenessAdapter<'_> { fn clobbers(&self, inst: &Self::Inst) -> Vec { inst.clobbers().to_vec() } + + fn is_non_returning(&self, inst: &Self::Inst) -> bool { + inst.is_non_returning() + } } /// Compute liveness information for X86Mir. @@ -113,7 +117,14 @@ fn get_successors( | X86Inst::Jle { label } => conditional_successors(idx, *label, label_to_idx, num_insts), // Return and trap have no successors X86Inst::Ret | X86Inst::Ud2 => Vec::new(), - // Function calls fall through (callee returns) + // A call to a helper the runtime ABI manifest declares + // `ReturnBehavior::Never` aborts the process. Control never comes back, + // so it has no successors and nothing is live after it (RUE-1224). + X86Inst::CallRel { + returns: ReturnBehavior::Never, + .. + } => Vec::new(), + // Ordinary calls fall through (the callee returns) X86Inst::CallRel { .. } => fallthrough_successor(idx, num_insts), // All other instructions fall through to the next _ => fallthrough_successor(idx, num_insts), diff --git a/crates/rue-codegen/src/x86_64/mir.rs b/crates/rue-codegen/src/x86_64/mir.rs index cc939d753..b86390e8a 100644 --- a/crates/rue-codegen/src/x86_64/mir.rs +++ b/crates/rue-codegen/src/x86_64/mir.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::fmt; +pub use rue_runtime_abi::ReturnBehavior; + // Compile-time size assertions to prevent silent size growth during refactoring. // These limits are set slightly above current sizes to allow minor changes, // but will catch significant size regressions. @@ -531,7 +533,16 @@ pub enum X86Inst { /// instruction with a relocation for the target address. /// /// The `symbol_id` is an index into the symbol table stored in `X86Mir`. - CallRel { symbol_id: u32 }, + /// + /// `returns` carries the callee's control contract. Every Rue-to-Rue call + /// returns; a runtime helper carries whatever the ABI manifest declares, so + /// the trap helpers (`__rue_overflow` and siblings) are `Never`. Liveness + /// gives a `Never` call no successors, and allocation does not count its + /// clobbers against a value that is only live around it (RUE-1224). + CallRel { + symbol_id: u32, + returns: ReturnBehavior, + }, /// `syscall` - Invoke system call. Syscall, @@ -664,6 +675,30 @@ pub enum X86Inst { } impl X86Inst { + /// A call to a callee that returns normally. + /// + /// Every Rue-to-Rue call and every returning runtime helper uses this. A + /// call to a helper the ABI manifest declares `ReturnBehavior::Never` must + /// build [`X86Inst::CallRel`] directly with the manifest's behavior so + /// liveness sees it (RUE-1224). + pub const fn call(symbol_id: u32) -> Self { + Self::CallRel { + symbol_id, + returns: ReturnBehavior::Returns, + } + } + + /// Whether this instruction never returns control to the next one. + pub const fn is_non_returning(&self) -> bool { + matches!( + self, + X86Inst::CallRel { + returns: ReturnBehavior::Never, + .. + } + ) + } + /// Returns physical registers clobbered by this instruction. /// /// This information is used by the register allocator to avoid assigning @@ -815,7 +850,7 @@ impl fmt::Display for X86Inst { X86Inst::Jle { label } => write!(f, "jle {}", label), X86Inst::Jmp { label } => write!(f, "jmp {}", label), X86Inst::Label { id } => write!(f, "{}:", id), - X86Inst::CallRel { symbol_id } => write!(f, "call sym{}", symbol_id), + X86Inst::CallRel { symbol_id, .. } => write!(f, "call sym{}", symbol_id), X86Inst::Syscall => write!(f, "syscall"), X86Inst::Ret => write!(f, "ret"), X86Inst::Ud2 => write!(f, "ud2"), @@ -1121,7 +1156,7 @@ impl fmt::Display for X86Mir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for inst in &self.instructions { // Special handling for CallRel to show actual symbol name - if let X86Inst::CallRel { symbol_id } = inst { + if let X86Inst::CallRel { symbol_id, .. } = inst { writeln!(f, " call {}", self.get_symbol(*symbol_id))?; } else { writeln!(f, " {}", inst)?; diff --git a/crates/rue-codegen/src/x86_64/peephole.rs b/crates/rue-codegen/src/x86_64/peephole.rs index a273ac2c7..ac3490f6e 100644 --- a/crates/rue-codegen/src/x86_64/peephole.rs +++ b/crates/rue-codegen/src/x86_64/peephole.rs @@ -968,7 +968,7 @@ mod tests { dst: Operand::Physical(Reg::Rax), imm: 0, }, - X86Inst::CallRel { symbol_id: 0 }, + X86Inst::call(0), X86Inst::Jz { label: LabelId::new(0), }, diff --git a/crates/rue-codegen/src/x86_64/regalloc.rs b/crates/rue-codegen/src/x86_64/regalloc.rs index 361433d6f..75426e4d8 100644 --- a/crates/rue-codegen/src/x86_64/regalloc.rs +++ b/crates/rue-codegen/src/x86_64/regalloc.rs @@ -989,7 +989,9 @@ impl RegAlloc { X86Inst::Jle { label } => mir.push(X86Inst::Jle { label }), X86Inst::Jmp { label } => mir.push(X86Inst::Jmp { label }), X86Inst::Label { id } => mir.push(X86Inst::Label { id }), - X86Inst::CallRel { symbol_id } => mir.push(X86Inst::CallRel { symbol_id }), + X86Inst::CallRel { symbol_id, returns } => { + mir.push(X86Inst::CallRel { symbol_id, returns }) + } X86Inst::Syscall => mir.push(X86Inst::Syscall), X86Inst::Ret => mir.push(X86Inst::Ret), X86Inst::Ud2 => mir.push(X86Inst::Ud2), @@ -1376,6 +1378,12 @@ impl RegAllocBackend for X86Backend { } } + fn physical_operands(inst: &Self::Inst) -> Vec { + let mut regs = super::schedule::regs_read(inst); + regs.extend(super::schedule::regs_written(inst)); + regs + } + fn new_mir() -> Self::Mir { X86Mir::new() } @@ -1988,7 +1996,7 @@ mod tests { .collect(); define_loaded_values(&mut mir, &vregs); - mir.push(X86Inst::CallRel { symbol_id: symbol }); + mir.push(X86Inst::call(symbol)); for &vreg in &vregs { mir.push(X86Inst::MovRR { dst: Operand::Physical(Reg::Rdi), @@ -2020,6 +2028,99 @@ mod tests { } } + #[test] + fn trap_crossing_values_still_take_caller_saved_registers() { + // The same fixture as `cross_call_values_never_take_a_caller_saved_register`, + // except the call is the overflow trap. It never returns, so no value + // is live across it and the caller-saved class stays available + // (RUE-1224). + let mut mir = X86Mir::new(); + let symbol = mir.intern_symbol(rue_runtime_abi::RuntimeHelperId::Overflow.symbol()); + let vregs: Vec = (0..ALLOCATABLE_REGS.len()) + .map(|_| mir.alloc_vreg()) + .collect(); + + define_loaded_values(&mut mir, &vregs); + mir.push(X86Inst::CallRel { + symbol_id: symbol, + returns: rue_runtime_abi::ReturnBehavior::Never, + }); + for &vreg in &vregs { + mir.push(X86Inst::MovRR { + dst: Operand::Physical(Reg::Rdi), + src: Operand::Virtual(vreg), + }); + } + + let (mir, num_spills, _) = RegAlloc::new(mir, 0).allocate_with_spills().unwrap(); + + assert_eq!( + num_spills, 0, + "the trap is not a barrier, so every value should still fit in a register" + ); + let assigned: Vec = mir + .instructions() + .iter() + .filter_map(|inst| match inst { + X86Inst::MovRM { + dst: Operand::Physical(reg), + base: Reg::Rsi, + .. + } => Some(*reg), + _ => None, + }) + .collect(); + for ® in CALLER_SAVED_REGS { + assert!( + assigned.contains(®), + "a value spanning only the trap should be allowed caller-saved {reg}" + ); + } + } + + #[test] + fn a_never_returning_call_ends_liveness() { + // Modelling the trap as non-returning is what makes the clobber test + // above correct: nothing propagates past it. + let mut mir = X86Mir::new(); + let value = mir.alloc_vreg(); + let symbol = mir.intern_symbol(rue_runtime_abi::RuntimeHelperId::Overflow.symbol()); + + mir.push(X86Inst::MovRI32 { + dst: Operand::Virtual(value), + imm: 42, + }); + mir.push(X86Inst::CallRel { + symbol_id: symbol, + returns: rue_runtime_abi::ReturnBehavior::Never, + }); + mir.push(X86Inst::MovRR { + dst: Operand::Physical(Reg::Rdi), + src: Operand::Virtual(value), + }); + mir.push(X86Inst::Ret); + + let debug = liveness::analyze_debug(&mir); + assert!( + debug.instructions[1].live_out.is_empty(), + "a never-returning call must have an empty live-out set; found: {:?}", + debug.instructions[1].live_out + ); + } + + #[test] + #[should_panic(expected = "lowering named the allocatable register")] + fn lowering_may_not_name_an_allocatable_register_as_a_physical_operand() { + let mut mir = X86Mir::new(); + let value = mir.alloc_vreg(); + mir.push(X86Inst::MovRR { + dst: Operand::Virtual(value), + src: Operand::Physical(ALLOCATABLE_REGS[0]), + }); + + let _ = RegAlloc::new(mir, 0).allocate(); + } + #[test] fn test_call_survival_and_symbol_reconstruction() { let mut mir = X86Mir::new(); @@ -2030,7 +2131,7 @@ mod tests { dst: Operand::Virtual(value), imm: 42, }); - mir.push(X86Inst::CallRel { symbol_id: symbol }); + mir.push(X86Inst::call(symbol)); mir.push(X86Inst::MovRR { dst: Operand::Physical(Reg::Rdi), src: Operand::Virtual(value), @@ -2053,11 +2154,9 @@ mod tests { CALLEE_SAVED_REGS.contains(&value_reg), "a value live across a call must land in a callee-saved register, got {value_reg}" ); - assert!( - mir.instructions() - .iter() - .any(|inst| matches!(inst, X86Inst::CallRel { symbol_id } if *symbol_id == symbol)) - ); + assert!(mir.instructions().iter().any( + |inst| matches!(inst, X86Inst::CallRel { symbol_id, .. } if *symbol_id == symbol) + )); assert!(mir.instructions().iter().any(|inst| matches!( inst, X86Inst::MovRR { diff --git a/crates/rue-codegen/src/x86_64/schedule.rs b/crates/rue-codegen/src/x86_64/schedule.rs index fb8c7bfa8..21eff8ea9 100644 --- a/crates/rue-codegen/src/x86_64/schedule.rs +++ b/crates/rue-codegen/src/x86_64/schedule.rs @@ -257,7 +257,7 @@ fn accesses_memory(inst: &X86Inst) -> bool { } /// Get registers read by an instruction (for dependency analysis). -fn regs_read(inst: &X86Inst) -> Vec { +pub(super) fn regs_read(inst: &X86Inst) -> Vec { let mut result = Vec::new(); let add_if_phys = |op: &Operand, vec: &mut Vec| { @@ -421,7 +421,7 @@ fn regs_read(inst: &X86Inst) -> Vec { } /// Get registers written by an instruction (for dependency analysis). -fn regs_written(inst: &X86Inst) -> Vec { +pub(super) fn regs_written(inst: &X86Inst) -> Vec { let mut result = Vec::new(); let add_if_phys = |op: &Operand, vec: &mut Vec| { @@ -713,7 +713,7 @@ mod tests { id: LabelId::new(0) })); assert!(is_barrier(&X86Inst::Ret)); - assert!(is_barrier(&X86Inst::CallRel { symbol_id: 0 })); + assert!(is_barrier(&X86Inst::call(0))); assert!(!is_barrier(&X86Inst::MovRR { dst: Operand::Physical(Reg::Rax), diff --git a/crates/rue-codegen/src/x86_64/verify.rs b/crates/rue-codegen/src/x86_64/verify.rs index 7b0a199bf..d8eff0f10 100644 --- a/crates/rue-codegen/src/x86_64/verify.rs +++ b/crates/rue-codegen/src/x86_64/verify.rs @@ -219,7 +219,7 @@ mod tests { mir.push(X86Inst::Push { src: Operand::Physical(Reg::Rbx), }); - mir.push(X86Inst::CallRel { symbol_id: sym_id }); + mir.push(X86Inst::call(sym_id)); mir.push(X86Inst::Pop { dst: Operand::Physical(Reg::Rbx), }); @@ -239,7 +239,7 @@ mod tests { mir.push(X86Inst::Push { src: Operand::Physical(Reg::Rax), }); - mir.push(X86Inst::CallRel { symbol_id: sym_id }); + mir.push(X86Inst::call(sym_id)); mir.push(X86Inst::Pop { dst: Operand::Physical(Reg::Rax), }); @@ -255,7 +255,7 @@ mod tests { fn test_aligned_call() { let mut mir = X86Mir::new(); let sym_id = mir.intern_symbol("test_func"); - mir.push(X86Inst::CallRel { symbol_id: sym_id }); + mir.push(X86Inst::call(sym_id)); mir.push(X86Inst::Ret); assert!(verify_stack_alignment(&mir).is_ok()); @@ -284,8 +284,8 @@ mod tests { let sym1 = mir.intern_symbol("func1"); let sym2 = mir.intern_symbol("func2"); - mir.push(X86Inst::CallRel { symbol_id: sym1 }); - mir.push(X86Inst::CallRel { symbol_id: sym2 }); + mir.push(X86Inst::call(sym1)); + mir.push(X86Inst::call(sym2)); mir.push(X86Inst::Ret); assert!(verify_stack_alignment(&mir).is_ok()); @@ -307,7 +307,7 @@ mod tests { src: Operand::Physical(Reg::Rax), }); // call (should be aligned now) - mir.push(X86Inst::CallRel { symbol_id: sym_id }); + mir.push(X86Inst::call(sym_id)); // cleanup: add rsp, 16 (arg + padding) mir.push(X86Inst::AddRI { dst: Operand::Physical(Reg::Rsp), @@ -330,7 +330,7 @@ mod tests { imm: -8, }); // call without proper alignment - mir.push(X86Inst::CallRel { symbol_id: sym_id }); + mir.push(X86Inst::call(sym_id)); mir.push(X86Inst::Ret); let result = verify_stack_alignment(&mir);