Skip to main content

rucc_opt/
simplify.rs

1//! Peephole rewrites: a small pattern of instructions becomes a smaller one.
2//!
3//! The third pass, and the one that will eventually not exist. Section 9.3 of
4//! `spec/09-optimizer.md` says the value level optimizer is an acyclic e-graph, and that an
5//! e-graph replaces what would otherwise be a folding pass, a peephole pass, a GVN pass, a
6//! reassociation pass and an instcombine pass, all with a pass ordering problem between them.
7//! This is the peephole pass, written now because the e-graph is a milestone away and because
8//! there is a rewrite that unblocks twelve lowering rules today.
9//!
10//! Every rewrite here has to survive being moved into the rule set later, so each one is stated
11//! as a pattern and a replacement in its own function and nothing shares state with anything.
12//!
13//! # The rewrites
14//!
15//! One so far: an exclusive or of a comparison with an `i1` of all ones is that comparison with
16//! the opposite predicate. That is issue 379, and it is worth more than the instruction it saves.
17//!
18//! C spells eight of the sixteen floating point predicates. The six relational and equality
19//! operators give the six ordered ones, `!=` gives `une`, and `__builtin_isunordered` gives `uno`.
20//! The other eight are what the negation of one of those means, and the front end writes a
21//! negation as an exclusive or rather than as a flipped predicate, so `!(x < y)` lowers to an
22//! `fcmp olt` and an `xor` where the machine has an `fcmp uge`. Twelve rules in the x86-64 rule
23//! set are written on those predicates and none of them has ever fired, over the whole torture
24//! suite at every optimization level, because no IR that reaches selection contains one.
25//!
26//! The integer case comes with it. `!(a < b)` on integers is the same shape, the same rewrite and
27//! the same saving, and leaving it out because the coverage report did not complain about it would
28//! be picking the rewrite by what measures it rather than by what it does.
29//!
30//! # Why it needs dead code elimination after it
31//!
32//! The rewrite turns the `xor` into the comparison and leaves the original comparison where it
33//! was, used by nothing when the negation was its only reader. Rewriting in place keeps the
34//! result value, so every use of it is already correct and there is nothing to rewrite, and what
35//! is left over is exactly what [`crate::dce`] takes out. That is why the pipeline runs the two in
36//! this order, and it is why the pass before the dead code eliminator was written first.
37
38use rucc_ir::{Block, Def, Extra, Flags, Func, Inst, Opcode, Type, Value};
39
40use crate::{Fuel, Pass};
41
42/// The pass. It holds nothing, because a peephole needs to know nothing beyond the pattern.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Simplify;
45
46impl Pass for Simplify {
47    fn name(&self) -> &'static str {
48        "simplify"
49    }
50
51    fn describe(&self) -> &'static str {
52        "a negated comparison becomes the comparison with the opposite predicate"
53    }
54
55    fn run(&self, func: &mut Func, fuel: &mut Fuel) -> bool {
56        let mut changed = false;
57        for block in func.blocks().collect::<Vec<Block>>() {
58            for inst in func.insts(block).collect::<Vec<Inst>>() {
59                let Some(flip) = negated_comparison(func, inst) else { continue };
60                if !fuel.take() {
61                    // Out of fuel, which stops the transforming rather than the looking, the
62                    // same way the other two passes treat it. The walk is the same walk at
63                    // every fuel setting, which is what makes bisecting over it monotonic.
64                    continue;
65                }
66                let args = func.push_values(&[flip.lhs, flip.rhs]);
67                let data = &mut func[inst];
68                data.opcode = flip.opcode;
69                data.flags = flip.flags;
70                data.args = args;
71                data.extra = flip.extra;
72                changed = true;
73            }
74        }
75        changed
76    }
77}
78
79/// What an instruction should become, when it is a comparison written as a negation.
80struct Flip {
81    /// `ICmp` or `FCmp`, whichever the comparison underneath was.
82    opcode: Opcode,
83    /// The flags of the comparison, which is where a fast math promise lives.
84    flags: Flags,
85    /// The opposite predicate.
86    extra: Extra,
87    /// The comparison's left operand.
88    lhs: Value,
89    /// Its right operand.
90    rhs: Value,
91}
92
93/// Whether this instruction is `xor (cmp p a b), true`, and what it becomes if it is.
94///
95/// The exclusive or is commutative, so the constant is looked for on both sides. Nothing else
96/// about the shape is negotiable: the result has to be an `i1`, because an exclusive or with one
97/// is a negation only at that width, and the constant has to be all ones, because the front end
98/// writes it as `iconst.i1 -1` and a reader who assumed the literal 1 would match nothing.
99fn negated_comparison(func: &Func, inst: Inst) -> Option<Flip> {
100    let data = &func[inst];
101    if data.opcode != Opcode::Xor {
102        return None;
103    }
104    let args = &func[data.args];
105    let (&first, &second) = (args.first()?, args.get(1)?);
106    if func[first].ty != Type::int(1) {
107        return None;
108    }
109    let cmp = match (all_ones(func, first), all_ones(func, second)) {
110        (true, false) => second,
111        (false, true) => first,
112        // Both, which folding would have turned into a constant, or neither, which is an
113        // exclusive or of two comparisons and is not this pattern.
114        _ => return None,
115    };
116    let Def::Result { inst: cmp, .. } = func[cmp].def else { return None };
117    let data = &func[cmp];
118    let extra = match (data.opcode, data.extra) {
119        (Opcode::ICmp, Extra::IntPred(pred)) => Extra::IntPred(pred.inverse()),
120        (Opcode::FCmp, Extra::FloatPred(pred)) => Extra::FloatPred(pred.inverse()),
121        _ => return None,
122    };
123    let args = &func[data.args];
124    Some(Flip {
125        opcode: data.opcode,
126        flags: data.flags,
127        extra,
128        lhs: *args.first()?,
129        rhs: *args.get(1)?,
130    })
131}
132
133/// Whether this value is a constant with every bit of its type set.
134fn all_ones(func: &Func, value: Value) -> bool {
135    let ty = func[value].ty;
136    let Def::Result { inst, .. } = func[value].def else { return false };
137    let data = &func[inst];
138    let Extra::Imm(at) = data.extra else { return false };
139    if data.opcode != Opcode::IConst {
140        return false;
141    }
142    // Read as signed, because an all ones value of any width is minus one that way and reading
143    // it unsigned would need the width to build the mask from.
144    func[at].signed(ty) == -1
145}
146
147#[cfg(test)]
148mod tests {
149    use rucc_base::Interner;
150    use rucc_ir::{
151        Block, Builder, Extra, Flags, Float, FloatPred, Func, IntPred, Opcode, Signature, Type,
152    };
153
154    use crate::{Fuel, Pass, simplify::Simplify};
155
156    /// A function with one block, ready to have instructions appended to it.
157    fn blank() -> (Interner, Func, Block) {
158        let mut names = Interner::new();
159        let name = names.intern("f");
160        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(1)]));
161        let block = func.create_block();
162        (names, func, block)
163    }
164
165    /// Runs the pass with as much fuel as it wants.
166    fn simplify(func: &mut Func) -> bool {
167        Simplify.run(func, &mut Fuel::unlimited())
168    }
169
170    /// The opcode and the predicate the value now comes from.
171    fn came_from(func: &Func, value: rucc_ir::Value) -> (Opcode, Extra) {
172        let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("not a result") };
173        (func[inst].opcode, func[inst].extra)
174    }
175
176    #[test]
177    fn a_negated_float_comparison_becomes_the_opposite_predicate() {
178        // Every ordered predicate and its opposite, which is the table `!(x < y)` is `x >= y`
179        // or unordered lives in, and the one place a sign error would hide.
180        for pred in FloatPred::all() {
181            let (_, mut func, block) = blank();
182            let mut build = Builder::new(&mut func, block);
183            let x = build.iconst(Type::int(64), 0);
184            let x = build.unary(Opcode::Bitcast, x, Type::float(Float::F64));
185            let cmp = build.fcmp(pred, x, x, Flags::NONE);
186            let ones = build.iconst(Type::int(1), -1);
187            let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
188            build.ret(&[not]);
189            assert!(simplify(&mut func), "{pred:?}");
190            assert_eq!(
191                came_from(&func, not),
192                (Opcode::FCmp, Extra::FloatPred(pred.inverse())),
193                "{pred:?}"
194            );
195        }
196    }
197
198    #[test]
199    fn a_negated_integer_comparison_becomes_the_opposite_predicate() {
200        for pred in IntPred::all() {
201            let (_, mut func, block) = blank();
202            let mut build = Builder::new(&mut func, block);
203            let x = build.iconst(Type::int(32), 3);
204            let cmp = build.icmp(pred, x, x);
205            let ones = build.iconst(Type::int(1), -1);
206            let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
207            build.ret(&[not]);
208            assert!(simplify(&mut func), "{pred:?}");
209            assert_eq!(
210                came_from(&func, not),
211                (Opcode::ICmp, Extra::IntPred(pred.inverse())),
212                "{pred:?}"
213            );
214        }
215    }
216
217    #[test]
218    fn the_constant_is_found_on_either_side() {
219        for swapped in [false, true] {
220            let (_, mut func, block) = blank();
221            let mut build = Builder::new(&mut func, block);
222            let x = build.iconst(Type::int(32), 3);
223            let cmp = build.icmp(IntPred::Slt, x, x);
224            let ones = build.iconst(Type::int(1), -1);
225            let (lhs, rhs) = if swapped { (ones, cmp) } else { (cmp, ones) };
226            let not = build.binary(Opcode::Xor, lhs, rhs, Flags::NONE);
227            build.ret(&[not]);
228            assert!(simplify(&mut func), "swapped {swapped}");
229            assert_eq!(came_from(&func, not).1, Extra::IntPred(IntPred::Sge));
230        }
231    }
232
233    #[test]
234    fn an_exclusive_or_of_two_comparisons_is_left_alone() {
235        let (_, mut func, block) = blank();
236        let mut build = Builder::new(&mut func, block);
237        let x = build.iconst(Type::int(32), 3);
238        let a = build.icmp(IntPred::Slt, x, x);
239        let b = build.icmp(IntPred::Sgt, x, x);
240        let differ = build.binary(Opcode::Xor, a, b, Flags::NONE);
241        build.ret(&[differ]);
242        assert!(!simplify(&mut func));
243        assert_eq!(came_from(&func, differ).0, Opcode::Xor);
244    }
245
246    #[test]
247    fn an_exclusive_or_of_something_that_is_not_a_comparison_is_left_alone() {
248        let (_, mut func, block) = blank();
249        let mut build = Builder::new(&mut func, block);
250        let x = build.iconst(Type::int(32), 3);
251        let narrow = build.unary(Opcode::Trunc, x, Type::int(1));
252        let ones = build.iconst(Type::int(1), -1);
253        let not = build.binary(Opcode::Xor, narrow, ones, Flags::NONE);
254        build.ret(&[not]);
255        assert!(!simplify(&mut func));
256        assert_eq!(came_from(&func, not).0, Opcode::Xor);
257    }
258
259    #[test]
260    fn a_wider_exclusive_or_with_one_is_not_a_negation_and_is_left_alone() {
261        let (_, mut func, block) = blank();
262        let mut build = Builder::new(&mut func, block);
263        let x = build.iconst(Type::int(32), 3);
264        let cmp = build.icmp(IntPred::Slt, x, x);
265        let wide = build.unary(Opcode::ZExt, cmp, Type::int(32));
266        let one = build.iconst(Type::int(32), 1);
267        let flipped = build.binary(Opcode::Xor, wide, one, Flags::NONE);
268        let narrow = build.unary(Opcode::Trunc, flipped, Type::int(1));
269        build.ret(&[narrow]);
270        assert!(!simplify(&mut func), "an i32 xor 1 flips one bit of thirty two");
271        assert_eq!(came_from(&func, flipped).0, Opcode::Xor);
272    }
273
274    #[test]
275    fn the_comparisons_flags_travel_with_the_predicate() {
276        let (_, mut func, block) = blank();
277        let mut build = Builder::new(&mut func, block);
278        let x = build.iconst(Type::int(64), 0);
279        let x = build.unary(Opcode::Bitcast, x, Type::float(Float::F64));
280        let cmp = build.fcmp(FloatPred::Olt, x, x, Flags::FAST);
281        let ones = build.iconst(Type::int(1), -1);
282        let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
283        build.ret(&[not]);
284        assert!(simplify(&mut func));
285        let rucc_ir::Def::Result { inst, .. } = func[not].def else { panic!("not a result") };
286        // The promise the original comparison was made under, not the exclusive or's absence of
287        // one. Dropping it would be correct and would quietly undo a fast math flag.
288        assert_eq!(func[inst].flags, Flags::FAST);
289    }
290
291    #[test]
292    fn fuel_stops_the_transformation_and_not_the_walk() {
293        let (_, mut func, block) = blank();
294        let mut build = Builder::new(&mut func, block);
295        let x = build.iconst(Type::int(32), 3);
296        let a = build.icmp(IntPred::Slt, x, x);
297        let b = build.icmp(IntPred::Sgt, x, x);
298        let ones = build.iconst(Type::int(1), -1);
299        let first = build.binary(Opcode::Xor, a, ones, Flags::NONE);
300        let second = build.binary(Opcode::Xor, b, ones, Flags::NONE);
301        let both = build.binary(Opcode::And, first, second, Flags::NONE);
302        build.ret(&[both]);
303        assert!(Simplify.run(&mut func, &mut Fuel::of(1)));
304        assert_eq!(came_from(&func, first).0, Opcode::ICmp);
305        assert_eq!(came_from(&func, second).0, Opcode::Xor);
306    }
307}