Coverage Report

Created: 2026-08-28 08:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/regalloc2/src/fuzzing/moves.rs
Line
Count
Source
1
//! Fuzz the parallel-move resolver.
2
3
use crate::moves::{MoveAndScratchResolver, ParallelMoves};
4
use crate::{Allocation, PReg, RegClass, SpillSlot};
5
use arbitrary::{Arbitrary, Result, Unstructured};
6
use std::collections::{HashMap, HashSet};
7
use std::{vec, vec::Vec};
8
9
0
fn is_stack_alloc(alloc: Allocation) -> bool {
10
    // Treat registers 20..=29 as fixed stack slots.
11
0
    if let Some(reg) = alloc.as_reg() {
12
0
        reg.index() > 20
13
    } else {
14
0
        alloc.is_stack()
15
    }
16
0
}
17
18
///
19
#[derive(Clone, Debug)]
20
pub struct TestCase {
21
    moves: Vec<(Allocation, Allocation)>,
22
    available_pregs: Vec<Allocation>,
23
}
24
25
impl Arbitrary<'_> for TestCase {
26
0
    fn arbitrary(u: &mut Unstructured) -> Result<Self> {
27
0
        let mut ret = TestCase {
28
0
            moves: vec![],
29
0
            available_pregs: vec![],
30
0
        };
31
0
        let mut written = HashSet::new();
32
        // An arbitrary sequence of moves between registers 0 to 29
33
        // inclusive.
34
0
        while bool::arbitrary(u)? {
35
0
            let src = if bool::arbitrary(u)? {
36
0
                let reg = u.int_in_range(0..=29)?;
37
0
                Allocation::reg(PReg::new(reg, RegClass::Int))
38
            } else {
39
0
                let slot = u.int_in_range(0..=31)?;
40
0
                Allocation::stack(SpillSlot::new(slot))
41
            };
42
0
            let dst = if bool::arbitrary(u)? {
43
0
                let reg = u.int_in_range(0..=29)?;
44
0
                Allocation::reg(PReg::new(reg, RegClass::Int))
45
            } else {
46
0
                let slot = u.int_in_range(0..=31)?;
47
0
                Allocation::stack(SpillSlot::new(slot))
48
            };
49
50
            // Stop if we are going to write a reg more than once:
51
            // that creates an invalid parallel move set.
52
0
            if written.contains(&dst) {
53
0
                break;
54
0
            }
55
0
            written.insert(dst);
56
57
0
            ret.moves.push((src, dst));
58
        }
59
60
        // We might have some unallocated registers free for scratch
61
        // space...
62
0
        for i in 0..u.int_in_range(0..=2)? {
63
0
            let reg = PReg::new(30 + i, RegClass::Int);
64
0
            ret.available_pregs.push(Allocation::reg(reg));
65
0
        }
66
0
        Ok(ret)
67
0
    }
68
}
69
70
0
pub fn check(t: TestCase) {
71
0
    let mut par = ParallelMoves::new();
72
0
    for &(src, dst) in &t.moves {
73
0
        par.add(src, dst, ());
74
0
    }
75
76
0
    let moves = par.resolve();
77
0
    log::trace!("raw resolved moves: {:?}", moves);
78
79
    // Resolve uses of scratch reg and stack-to-stack moves with the scratch
80
    // resolver.
81
0
    let mut avail = t.available_pregs.clone();
82
0
    let find_free_reg = || avail.pop();
83
0
    let mut next_slot = 32;
84
0
    let get_stackslot = || {
85
0
        let slot = next_slot;
86
0
        next_slot += 1;
87
0
        Allocation::stack(SpillSlot::new(slot))
88
0
    };
89
0
    let preferred_victim = PReg::new(0, RegClass::Int);
90
0
    let scratch_resolver = MoveAndScratchResolver {
91
0
        find_free_reg,
92
0
        get_stackslot,
93
0
        is_stack_alloc,
94
0
        borrowed_scratch_reg: preferred_victim,
95
0
    };
96
0
    let moves = scratch_resolver.compute(moves);
97
0
    log::trace!("resolved moves: {:?}", moves);
98
99
    // Compute the final source reg for each dest reg in the original
100
    // parallel-move set.
101
0
    let mut final_src_per_dest: HashMap<Allocation, Allocation> = HashMap::new();
102
0
    for &(src, dst) in &t.moves {
103
0
        final_src_per_dest.insert(dst, src);
104
0
    }
105
0
    log::trace!("expected final state: {:?}", final_src_per_dest);
106
107
    // Simulate the sequence of moves.
108
0
    let mut locations: HashMap<Allocation, Allocation> = HashMap::new();
109
0
    for (src, dst, _) in moves {
110
0
        let data = locations.get(&src).cloned().unwrap_or(src);
111
0
        locations.insert(dst, data);
112
0
    }
113
0
    log::trace!("simulated final state: {:?}", locations);
114
115
    // Assert that the expected register-moves occurred.
116
0
    for (reg, data) in locations {
117
0
        if let Some(&expected_data) = final_src_per_dest.get(&reg) {
118
0
            assert_eq!(expected_data, data);
119
        } else {
120
0
            if data != reg {
121
                // If not just the original value, then this location has been
122
                // modified, but it was not part of the original parallel move.
123
                // It must have been an available preg or a scratch stackslot.
124
0
                assert!(
125
0
                    t.available_pregs.contains(&reg)
126
0
                        || (reg.is_stack() && reg.as_stack().unwrap().index() >= 32)
127
                );
128
0
            }
129
        }
130
    }
131
0
}
132
133
#[test]
134
fn smoke() {
135
    arbtest::arbtest(|u| {
136
        let test_case = TestCase::arbitrary(u)?;
137
        check(test_case);
138
        Ok(())
139
    })
140
    .budget_ms(1_000);
141
}