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, Stats};
41
42/// Recorded once for each negation folded into the comparison under it.
43const FLIPPED: &str = "comparison negated by an exclusive or rewritten as the opposite comparison";
44
45/// Recorded for a negation that would have folded if there had been fuel for it.
46const NO_FUEL: &str = "negated comparison left alone, the pass ran out of fuel";
47
48/// The pass. It holds nothing, because a peephole needs to know nothing beyond the pattern.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Simplify;
51
52impl Pass for Simplify {
53    fn name(&self) -> &'static str {
54        "simplify"
55    }
56
57    fn describe(&self) -> &'static str {
58        "a negated comparison becomes the comparison with the opposite predicate"
59    }
60
61    fn run(&self, func: &mut Func, fuel: &mut Fuel) -> Stats {
62        let mut stats = Stats::new();
63        for block in func.blocks().collect::<Vec<Block>>() {
64            for inst in func.insts(block).collect::<Vec<Inst>>() {
65                let Some(flip) = negated_comparison(func, inst) else { continue };
66                if !fuel.take() {
67                    // Out of fuel, which stops the transforming rather than the looking, the
68                    // same way the other two passes treat it. The walk is the same walk at
69                    // every fuel setting, which is what makes bisecting over it monotonic.
70                    stats.missed(NO_FUEL);
71                    continue;
72                }
73                let args = func.push_values(&[flip.lhs, flip.rhs]);
74                let data = &mut func[inst];
75                data.opcode = flip.opcode;
76                data.flags = flip.flags;
77                data.args = args;
78                data.extra = flip.extra;
79                stats.optimized(FLIPPED);
80            }
81        }
82        stats
83    }
84}
85
86/// What an instruction should become, when it is a comparison written as a negation.
87struct Flip {
88    /// `ICmp` or `FCmp`, whichever the comparison underneath was.
89    opcode: Opcode,
90    /// The flags of the comparison, which is where a fast math promise lives.
91    flags: Flags,
92    /// The opposite predicate.
93    extra: Extra,
94    /// The comparison's left operand.
95    lhs: Value,
96    /// Its right operand.
97    rhs: Value,
98}
99
100/// Whether this instruction is `xor (cmp p a b), true`, and what it becomes if it is.
101///
102/// The exclusive or is commutative, so the constant is looked for on both sides. Nothing else
103/// about the shape is negotiable: the result has to be an `i1`, because an exclusive or with one
104/// is a negation only at that width, and the constant has to be all ones, because the front end
105/// writes it as `iconst.i1 -1` and a reader who assumed the literal 1 would match nothing.
106fn negated_comparison(func: &Func, inst: Inst) -> Option<Flip> {
107    let data = &func[inst];
108    if data.opcode != Opcode::Xor {
109        return None;
110    }
111    let args = &func[data.args];
112    let (&first, &second) = (args.first()?, args.get(1)?);
113    if func[first].ty != Type::int(1) {
114        return None;
115    }
116    let cmp = match (all_ones(func, first), all_ones(func, second)) {
117        (true, false) => second,
118        (false, true) => first,
119        // Both, which folding would have turned into a constant, or neither, which is an
120        // exclusive or of two comparisons and is not this pattern.
121        _ => return None,
122    };
123    let Def::Result { inst: cmp, .. } = func[cmp].def else { return None };
124    let data = &func[cmp];
125    let extra = match (data.opcode, data.extra) {
126        (Opcode::ICmp, Extra::IntPred(pred)) => Extra::IntPred(pred.inverse()),
127        (Opcode::FCmp, Extra::FloatPred(pred)) => Extra::FloatPred(pred.inverse()),
128        _ => return None,
129    };
130    let args = &func[data.args];
131    Some(Flip {
132        opcode: data.opcode,
133        flags: data.flags,
134        extra,
135        lhs: *args.first()?,
136        rhs: *args.get(1)?,
137    })
138}
139
140/// Whether this value is a constant with every bit of its type set.
141fn all_ones(func: &Func, value: Value) -> bool {
142    let ty = func[value].ty;
143    let Def::Result { inst, .. } = func[value].def else { return false };
144    let data = &func[inst];
145    let Extra::Imm(at) = data.extra else { return false };
146    if data.opcode != Opcode::IConst {
147        return false;
148    }
149    // Read as signed, because an all ones value of any width is minus one that way and reading
150    // it unsigned would need the width to build the mask from.
151    func[at].signed(ty) == -1
152}
153
154#[cfg(test)]
155mod tests {
156    use rucc_base::Interner;
157    use rucc_ir::{
158        Block, Builder, Extra, Flags, Float, FloatPred, Func, IntPred, Opcode, Signature, Type,
159    };
160
161    use crate::stats::Kind;
162    use crate::{Fuel, Pass, simplify::Simplify};
163
164    /// A function with one block, ready to have instructions appended to it.
165    fn blank() -> (Interner, Func, Block) {
166        let mut names = Interner::new();
167        let name = names.intern("f");
168        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(1)]));
169        let block = func.create_block();
170        (names, func, block)
171    }
172
173    /// Runs the pass with as much fuel as it wants, and says whether it rewrote anything.
174    fn simplify(func: &mut Func) -> bool {
175        Simplify.run(func, &mut Fuel::unlimited()).changed()
176    }
177
178    /// The opcode and the predicate the value now comes from.
179    fn came_from(func: &Func, value: rucc_ir::Value) -> (Opcode, Extra) {
180        let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("not a result") };
181        (func[inst].opcode, func[inst].extra)
182    }
183
184    #[test]
185    fn a_negated_float_comparison_becomes_the_opposite_predicate() {
186        // Every ordered predicate and its opposite, which is the table `!(x < y)` is `x >= y`
187        // or unordered lives in, and the one place a sign error would hide.
188        for pred in FloatPred::all() {
189            let (_, mut func, block) = blank();
190            let mut build = Builder::new(&mut func, block);
191            let x = build.iconst(Type::int(64), 0);
192            let x = build.unary(Opcode::Bitcast, x, Type::float(Float::F64));
193            let cmp = build.fcmp(pred, x, x, Flags::NONE);
194            let ones = build.iconst(Type::int(1), -1);
195            let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
196            build.ret(&[not]);
197            assert!(simplify(&mut func), "{pred:?}");
198            assert_eq!(
199                came_from(&func, not),
200                (Opcode::FCmp, Extra::FloatPred(pred.inverse())),
201                "{pred:?}"
202            );
203        }
204    }
205
206    #[test]
207    fn a_negated_integer_comparison_becomes_the_opposite_predicate() {
208        for pred in IntPred::all() {
209            let (_, mut func, block) = blank();
210            let mut build = Builder::new(&mut func, block);
211            let x = build.iconst(Type::int(32), 3);
212            let cmp = build.icmp(pred, x, x);
213            let ones = build.iconst(Type::int(1), -1);
214            let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
215            build.ret(&[not]);
216            assert!(simplify(&mut func), "{pred:?}");
217            assert_eq!(
218                came_from(&func, not),
219                (Opcode::ICmp, Extra::IntPred(pred.inverse())),
220                "{pred:?}"
221            );
222        }
223    }
224
225    #[test]
226    fn the_constant_is_found_on_either_side() {
227        for swapped in [false, true] {
228            let (_, mut func, block) = blank();
229            let mut build = Builder::new(&mut func, block);
230            let x = build.iconst(Type::int(32), 3);
231            let cmp = build.icmp(IntPred::Slt, x, x);
232            let ones = build.iconst(Type::int(1), -1);
233            let (lhs, rhs) = if swapped { (ones, cmp) } else { (cmp, ones) };
234            let not = build.binary(Opcode::Xor, lhs, rhs, Flags::NONE);
235            build.ret(&[not]);
236            assert!(simplify(&mut func), "swapped {swapped}");
237            assert_eq!(came_from(&func, not).1, Extra::IntPred(IntPred::Sge));
238        }
239    }
240
241    #[test]
242    fn an_exclusive_or_of_two_comparisons_is_left_alone() {
243        let (_, mut func, block) = blank();
244        let mut build = Builder::new(&mut func, block);
245        let x = build.iconst(Type::int(32), 3);
246        let a = build.icmp(IntPred::Slt, x, x);
247        let b = build.icmp(IntPred::Sgt, x, x);
248        let differ = build.binary(Opcode::Xor, a, b, Flags::NONE);
249        build.ret(&[differ]);
250        assert!(!simplify(&mut func));
251        assert_eq!(came_from(&func, differ).0, Opcode::Xor);
252    }
253
254    #[test]
255    fn an_exclusive_or_of_something_that_is_not_a_comparison_is_left_alone() {
256        let (_, mut func, block) = blank();
257        let mut build = Builder::new(&mut func, block);
258        let x = build.iconst(Type::int(32), 3);
259        let narrow = build.unary(Opcode::Trunc, x, Type::int(1));
260        let ones = build.iconst(Type::int(1), -1);
261        let not = build.binary(Opcode::Xor, narrow, ones, Flags::NONE);
262        build.ret(&[not]);
263        assert!(!simplify(&mut func));
264        assert_eq!(came_from(&func, not).0, Opcode::Xor);
265    }
266
267    #[test]
268    fn a_wider_exclusive_or_with_one_is_not_a_negation_and_is_left_alone() {
269        let (_, mut func, block) = blank();
270        let mut build = Builder::new(&mut func, block);
271        let x = build.iconst(Type::int(32), 3);
272        let cmp = build.icmp(IntPred::Slt, x, x);
273        let wide = build.unary(Opcode::ZExt, cmp, Type::int(32));
274        let one = build.iconst(Type::int(32), 1);
275        let flipped = build.binary(Opcode::Xor, wide, one, Flags::NONE);
276        let narrow = build.unary(Opcode::Trunc, flipped, Type::int(1));
277        build.ret(&[narrow]);
278        assert!(!simplify(&mut func), "an i32 xor 1 flips one bit of thirty two");
279        assert_eq!(came_from(&func, flipped).0, Opcode::Xor);
280    }
281
282    #[test]
283    fn the_comparisons_flags_travel_with_the_predicate() {
284        let (_, mut func, block) = blank();
285        let mut build = Builder::new(&mut func, block);
286        let x = build.iconst(Type::int(64), 0);
287        let x = build.unary(Opcode::Bitcast, x, Type::float(Float::F64));
288        let cmp = build.fcmp(FloatPred::Olt, x, x, Flags::FAST);
289        let ones = build.iconst(Type::int(1), -1);
290        let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
291        build.ret(&[not]);
292        assert!(simplify(&mut func));
293        let rucc_ir::Def::Result { inst, .. } = func[not].def else { panic!("not a result") };
294        // The promise the original comparison was made under, not the exclusive or's absence of
295        // one. Dropping it would be correct and would quietly undo a fast math flag.
296        assert_eq!(func[inst].flags, Flags::FAST);
297    }
298
299    #[test]
300    fn fuel_stops_the_transformation_and_not_the_walk() {
301        let (_, mut func, block) = blank();
302        let mut build = Builder::new(&mut func, block);
303        let x = build.iconst(Type::int(32), 3);
304        let a = build.icmp(IntPred::Slt, x, x);
305        let b = build.icmp(IntPred::Sgt, x, x);
306        let ones = build.iconst(Type::int(1), -1);
307        let first = build.binary(Opcode::Xor, a, ones, Flags::NONE);
308        let second = build.binary(Opcode::Xor, b, ones, Flags::NONE);
309        let both = build.binary(Opcode::And, first, second, Flags::NONE);
310        build.ret(&[both]);
311        let stats = Simplify.run(&mut func, &mut Fuel::of(1));
312        assert!(stats.changed());
313        assert_eq!(stats.count(Kind::Optimized, super::FLIPPED), 1);
314        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
315        assert_eq!(came_from(&func, first).0, Opcode::ICmp);
316        assert_eq!(came_from(&func, second).0, Opcode::Xor);
317    }
318}