Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/fuzzing/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,
Expand All @@ -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,
Expand Down Expand Up @@ -582,6 +584,19 @@ impl Func {
)));
}

if opts.fixed_def_clobbers && bool::arbitrary(u)? {
// Exercise an impossible fixed output, not just allocatable functions.
if let (OperandKind::Def, OperandPos::Late, OperandConstraint::FixedReg(preg)) = (
operands[0].kind(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only operand 0?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used operand 0 because the generator starts each instruction with a def and appends uses after it. The additional defs in the callsite-like path are created with any_def, so they don't have fixed-register constraints.

That makes operand 0 the candidate here, but iterating over all operands and selecting late fixed defs would express the intent more directly. I'll change it to do that.

operands[0].pos(),
operands[0].constraint(),
) {
if preg.hw_enc() < 32 {
clobbers.push(preg);
}
}
}

builder.add_inst(
Block::new(block),
InstData {
Expand Down Expand Up @@ -638,6 +653,22 @@ impl Func {

Ok(builder.finalize())
}

pub fn has_fixed_def_clobber(&self) -> bool {
self.insts.iter().any(|inst| {
inst.clobbers
.iter()
.any(|&preg| inst.operands.iter().any(has_fixed_def_with(preg)))
})
}

pub fn remove_fixed_def_clobbers(&mut self) {
for inst in &mut self.insts {
let operands = &inst.operands;
inst.clobbers
.retain(|&preg| !operands.iter().any(has_fixed_def_with(preg)));
}
}
}

impl core::fmt::Debug for Func {
Expand Down
31 changes: 26 additions & 5 deletions src/fuzzing/ion.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Fuzz the `ion` register allocator.

use crate::{checker, fuzzing::func, ion};
use crate::{checker, fuzzing::func, ion, RegAllocError};
use arbitrary::{Arbitrary, Result, Unstructured};
use core::cell::RefCell;
use std::thread_local;
Expand All @@ -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
Expand Down Expand Up @@ -53,18 +54,38 @@ pub fn check(t: TestCase) {
log::trace!("func:\n{func:?}");

let env = func::machine_env();
let allocatable_func = if func.has_fixed_def_clobber() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing the fuzzer toplevel in a somewhat nontrivial way -- it's inventing a notion of (what I'd call) "differential feasibility" and doing allocation on two different versions of a function. I'm not sure I like introducing this complexity; I'd rather that we keep the simple logic of "create an arbitrary function, run the algorithm, check for expected result".

In the case of an explicitly introduced fixed-def + clobber, I'd expect that our fuzzing oracle would say "not allocatable", and we'd assert that we get that back out.

I also don't like that we spread logic between the toplevel driver here, which is supposed to be simple, and the function generator itself.

Perhaps have a func.expected_fail() -> Option<RegAllocError>, and if that gives a Some, then assert that we get that error, otherwise run the checker as before?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reason for checking the copy without the conflicting clobbers was to keep exercising successful allocation and output checking alongside rejection of the conflicting input. I see your point about the extra complexity in the driver.

Your expected_fail() approach makes sense to me. With that change, I want to make sure the generator still exercises both valid fixed-register constraints and fixed-def/clobber collisions, so we retain coverage of successful allocation as well as the expected error.

let mut allocatable_func = func.clone();
allocatable_func.remove_fixed_def_clobbers();
Some(allocatable_func)
} else {
None
};
let valid_func = allocatable_func.as_ref().unwrap_or(func);
thread_local! {
// We test that ctx is cleared properly between runs.
static CTX: RefCell<ion::Ctx> = RefCell::default();
}

CTX.with(|ctx| {
ion::run(func, &env, &mut *ctx.borrow_mut(), *annotate, *check_ssa)
let mut ctx = ctx.borrow_mut();
ion::run(valid_func, &env, &mut ctx, *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 checker = checker::Checker::new(valid_func, &env);
checker.prepare(&ctx.output);
checker.run().expect("checker failed");
}

if allocatable_func.is_some() {
let result = ion::run(func, &env, &mut ctx, *annotate, *check_ssa);
assert!(
matches!(result, Err(RegAllocError::TooManyLiveRegs)),
"expected TooManyLiveRegs for a fixed-def/clobber conflict, got {:?}",
result
);
}
});
}

Expand Down
10 changes: 10 additions & 0 deletions src/ion/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading