diff --git a/src/fuzzing/func.rs b/src/fuzzing/func.rs index b2a4817f..6bc8d067 100644 --- a/src/fuzzing/func.rs +++ b/src/fuzzing/func.rs @@ -5,7 +5,7 @@ use crate::{ domtree, postorder, Allocation, Block, Function, Inst, InstRange, MachineEnv, Operand, - OperandConstraint, OperandKind, OperandPos, PReg, PRegSet, RegClass, VReg, + OperandConstraint, OperandKind, OperandPos, PReg, PRegSet, RegAllocError, RegClass, VReg, }; use alloc::vec::Vec; @@ -367,6 +367,7 @@ pub struct Options { pub fixed_regs: bool, pub fixed_nonallocatable: bool, pub clobbers: bool, + pub fixed_def_clobbers: bool, pub reftypes: bool, pub callsite_ish_constraints: bool, pub num_blocks: RangeInclusive, @@ -383,6 +384,7 @@ impl Options { fixed_regs: false, fixed_nonallocatable: false, clobbers: false, + fixed_def_clobbers: false, reftypes: false, callsite_ish_constraints: false, num_blocks: 1..=100, @@ -582,6 +584,22 @@ impl Func { ))); } + if opts.fixed_def_clobbers && bool::arbitrary(u)? { + // Exercise an impossible fixed output, not just allocatable functions. + for operand in &operands { + if let ( + OperandKind::Def, + OperandPos::Late, + OperandConstraint::FixedReg(preg), + ) = (operand.kind(), operand.pos(), operand.constraint()) + { + if preg.hw_enc() < 32 { + clobbers.push(preg); + } + } + } + } + builder.add_inst( Block::new(block), InstData { @@ -638,6 +656,20 @@ impl Func { Ok(builder.finalize()) } + + /// An allocation error implied by the generated function's constraints. + pub fn expected_fail(&self) -> Option { + let fixed_def_clobber = self.insts.iter().any(|inst| { + inst.clobbers + .iter() + .any(|&preg| inst.operands.iter().any(has_fixed_def_with(preg))) + }); + if fixed_def_clobber { + Some(RegAllocError::TooManyLiveRegs) + } else { + None + } + } } impl core::fmt::Debug for Func { diff --git a/src/fuzzing/ion.rs b/src/fuzzing/ion.rs index e64b5a31..c76037cd 100644 --- a/src/fuzzing/ion.rs +++ b/src/fuzzing/ion.rs @@ -11,6 +11,7 @@ const OPTIONS: func::Options = func::Options { fixed_regs: true, fixed_nonallocatable: true, clobbers: true, + fixed_def_clobbers: true, reftypes: true, callsite_ish_constraints: true, ..func::Options::DEFAULT @@ -59,12 +60,22 @@ pub fn check(t: TestCase) { } CTX.with(|ctx| { - ion::run(func, &env, &mut *ctx.borrow_mut(), *annotate, *check_ssa) - .expect("regalloc did not succeed"); - - let mut checker = checker::Checker::new(func, &env); - checker.prepare(&ctx.borrow().output); - checker.run().expect("checker failed"); + let mut ctx = ctx.borrow_mut(); + let result = ion::run(func, &env, &mut ctx, *annotate, *check_ssa); + if let Some(expected) = func.expected_fail() { + // The oracle currently returns only the payload-free TooManyLiveRegs variant. + assert!( + matches!(&result, Err(actual) if core::mem::discriminant(actual) == core::mem::discriminant(&expected)), + "expected {:?}, got {:?}", + expected, + result + ); + } else { + result.expect("regalloc did not succeed"); + let mut checker = checker::Checker::new(func, &env); + checker.prepare(&ctx.output); + checker.run().expect("checker failed"); + } }); } diff --git a/src/ion/process.rs b/src/ion/process.rs index a6e5db90..57b81e69 100644 --- a/src/ion/process.rs +++ b/src/ion/process.rs @@ -1214,6 +1214,16 @@ impl<'a, F: Function> Env<'a, F> { || lowest_cost_evict_conflict_cost.is_none() || lowest_cost_evict_conflict_cost.unwrap() >= our_spill_weight) { + // A minimal bundle pinned to one physical register cannot + // move, and a fixed reservation on that register (a clobber + // is modeled as one) cannot be evicted. The overlap is + // illegal: a clobber must not collide with a fixed def or + // late use. Reject it instead of panicking. + if matches!(req, Requirement::FixedReg(_)) + && lowest_cost_evict_conflict_cost.is_none() + { + return Err(RegAllocError::TooManyLiveRegs); + } if matches!(req, Requirement::Register | Requirement::Limit(_)) { // Check if this is a too-many-live-registers situation. let range = self.ctx.bundles[bundle].ranges[0].range;