Skip to main content

rucc_opt/
fold.rs

1//! Constant folding: an instruction whose operands are all constants becomes a constant.
2//!
3//! The smallest transformation there is, and the one the rest of the middle end leans on. Every
4//! later pass produces constants where the source had none, and none of them should have to
5//! evaluate the arithmetic itself.
6//!
7//! It is worth having before any of them because the lowering walk produces constant arithmetic
8//! that nothing in the C asked for. The usual arithmetic conversions widen a literal to the type
9//! of the other operand, so `long y; y + 7` lowers to a 32 bit constant, a `sext` of it and an
10//! add, and nothing downstream can see that the operand of the add is a number. On x86-64 that
11//! costs two instructions and a register on every operation between a wide integer and a
12//! literal, which is most address arithmetic and most loop bounds in real code. That is issue
13//! 378.
14//!
15//! # How it rewrites
16//!
17//! In place. An instruction that folds keeps its result value and becomes an `iconst`, because
18//! the value it produced already has the right type and every use of it is already correct. So
19//! there is no rewriting of uses, no new value, and nothing for a later pass to have to know
20//! about. What is left behind is the old operand, now used by nothing, which costs nothing in
21//! the output because the backend materializes a constant where it is wanted rather than where
22//! the IR wrote it, and which dead code elimination will take out of the printed IR when there
23//! is one.
24//!
25//! # What it does not fold
26//!
27//! Not the divides and the remainders. Both have two cases the language leaves undefined, a zero
28//! divisor and the most negative value divided by minus one, and both want guarding rather than
29//! evaluating. They belong with the strength reduction that turns a division by a constant into
30//! a multiply, which is where somebody looking for division arithmetic will look.
31//!
32//! Not floating point. Folding it means deciding what rounding mode to fold under and what to do
33//! about a signalling NaN, and `rucc_base::float` has the arithmetic but the decision about the
34//! environment belongs with the rest of the floating point work rather than in the first pass.
35//!
36//! Not an operation that overflows under `nsw` or `nuw`. The result there is poison, so any
37//! answer would be a valid refinement, and quietly picking the wrapping one hides a program that
38//! has stepped outside the language from the sanitizer that should be reporting it.
39//!
40//! Not comparisons. An `icmp` produces an `i1`, the backend folds one that feeds a branch into
41//! the branch, and nothing lowers an `i1` that is left standing on its own, which is issue 352.
42//! Turning a comparison into a constant before that is fixed would turn working code into code
43//! that does not build.
44
45use rucc_ir::{Block, Def, Extra, Flags, Func, Imm, Inst, Opcode, Type, Value};
46
47use crate::{Analyses, Fuel, Pass, Preserved, Stats};
48
49/// Recorded once for each instruction that became a constant.
50const FOLDED: &str = "integer instruction folded to a constant";
51
52/// Recorded for an instruction that would have folded if there had been fuel for it.
53///
54/// Not a missed optimization in the ordinary sense, since the fuel is a person deliberately
55/// stopping the pass. It is here because it is the number a bisection is searching for: the count
56/// of sites past the cut is how far there is left to go.
57const NO_FUEL: &str = "integer instruction not folded, the pass ran out of fuel";
58
59/// The pass. It holds nothing, because folding needs to know nothing beyond the instruction.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Fold;
62
63impl Pass for Fold {
64    fn name(&self) -> &'static str {
65        "fold"
66    }
67
68    fn describe(&self) -> &'static str {
69        "an integer instruction whose operands are all constants becomes a constant"
70    }
71
72    fn preserves(&self) -> Preserved {
73        // An instruction becomes a constant where it stands. No block moves, no edge moves,
74        // and a terminator is not one of the instructions this folds, so every analysis in the
75        // cache is about the same graph afterwards as it was before.
76        Preserved::ALL
77    }
78
79    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
80        let blocks: Vec<Block> = func.blocks().collect();
81        let mut stats = Stats::new();
82        for block in blocks {
83            let insts: Vec<Inst> = func.insts(block).collect();
84            for inst in insts {
85                let Some(folded) = evaluate(func, inst) else { continue };
86                if !fuel.take() {
87                    // Out of fuel, which is a request to stop transforming rather than to stop
88                    // looking. Continuing the walk costs nothing and keeps the count of what
89                    // could have been folded the same at every fuel setting, which is what makes
90                    // a bisection over it monotonic.
91                    stats.missed(NO_FUEL);
92                    continue;
93                }
94                let ty = func[result_of(func, inst)].ty;
95                let at = func.add_imm(folded);
96                let data = &mut func[inst];
97                data.opcode = Opcode::IConst;
98                data.flags = Flags::NONE;
99                data.args = rucc_ir::ValueList::EMPTY;
100                data.extra = Extra::Imm(at);
101                debug_assert!(ty.is_int(), "only an integer instruction folds");
102                stats.optimized(FOLDED);
103            }
104        }
105        stats
106    }
107}
108
109/// The single result of an instruction that folded.
110fn result_of(func: &Func, inst: Inst) -> Value {
111    func[inst].results().next().expect("an instruction that folds produces a value")
112}
113
114/// What this instruction evaluates to, if it evaluates to anything.
115///
116/// `None` covers every reason not to fold and does not distinguish between them, because the
117/// answer to all of them is the same: leave the instruction alone.
118fn evaluate(func: &Func, inst: Inst) -> Option<Imm> {
119    let data = &func[inst];
120    if data.results != 1 {
121        return None;
122    }
123    let result = data.results().next()?;
124    let ty = func[result].ty;
125    // A vector constant is a `splat` rather than an `iconst`, so a vector fold would have to
126    // build a different instruction and would have to be right about the lane count as well.
127    if !ty.is_int() || !ty.is_scalar() {
128        return None;
129    }
130    let args = &func[data.args];
131    match data.opcode {
132        Opcode::Trunc | Opcode::SExt | Opcode::ZExt => {
133            let (value, from) = constant(func, *args.first()?)?;
134            Some(convert(data.opcode, value, from, ty))
135        }
136        Opcode::Shl | Opcode::LShr | Opcode::AShr => {
137            let (value, from) = constant(func, *args.first()?)?;
138            let (count, count_ty) = constant(func, *args.get(1)?)?;
139            shift(data.opcode, value, from, count, count_ty, ty, data.flags)
140        }
141        Opcode::Add | Opcode::Sub | Opcode::Mul | Opcode::And | Opcode::Or | Opcode::Xor => {
142            let (lhs, lhs_ty) = constant(func, *args.first()?)?;
143            let (rhs, _) = constant(func, *args.get(1)?)?;
144            binary(data.opcode, lhs, rhs, lhs_ty, ty, data.flags)
145        }
146        _ => None,
147    }
148}
149
150/// The constant this value is, with the type it has, if it is one.
151///
152/// Shared with [`crate::simplify_cfg`], which asks the same question about the condition of a
153/// branch. Asking it in two places would be two answers about what a constant is.
154pub(crate) fn constant(func: &Func, value: Value) -> Option<(Imm, Type)> {
155    let Def::Result { inst, .. } = func[value].def else { return None };
156    if func[inst].opcode != Opcode::IConst {
157        return None;
158    }
159    let Extra::Imm(at) = func[inst].extra else { return None };
160    let ty = func[value].ty;
161    ty.is_int().then(|| (func[at], ty))
162}
163
164/// A widening or a narrowing of a constant.
165fn convert(opcode: Opcode, value: Imm, from: Type, to: Type) -> Imm {
166    match opcode {
167        // Truncation is the masking that `Imm::int` does anyway, and sign extension is reading
168        // the value as signed at its own width and storing it at the wider one.
169        Opcode::Trunc | Opcode::SExt => Imm::int(value.signed(from), to),
170        // Zero extension reads the same bits as unsigned, which for a width below 128 is a
171        // non-negative number and survives the cast to the signed type `Imm::int` takes.
172        _ => Imm::int(value.unsigned() as i128, to),
173    }
174}
175
176/// A shift of a constant by a constant.
177///
178/// `None` when the count is not one the language defines, which is a count at or above the width
179/// of the value. The result there is poison and folding it would be picking an answer for a
180/// program that asked for none.
181fn shift(
182    opcode: Opcode,
183    value: Imm,
184    from: Type,
185    count: Imm,
186    count_ty: Type,
187    to: Type,
188    flags: Flags,
189) -> Option<Imm> {
190    let by = count.unsigned();
191    if by >= u128::from(to.bits()) || count.signed(count_ty) < 0 {
192        return None;
193    }
194    let by = by as u32;
195    let exact = match opcode {
196        Opcode::Shl => value.signed(from).checked_shl(by)?,
197        // A logical shift right is on the bits rather than on the number, so it reads unsigned
198        // and the cast back cannot lose anything: the value has at most `from.bits()` bits set
199        // and shifting right sets none.
200        Opcode::LShr => (value.unsigned() >> by) as i128,
201        _ => value.signed(from) >> by,
202    };
203    if opcode == Opcode::Shl && overflowed(exact, to, flags) {
204        return None;
205    }
206    Some(Imm::int(exact, to))
207}
208
209/// An arithmetic or bitwise operation on two constants.
210fn binary(opcode: Opcode, lhs: Imm, rhs: Imm, from: Type, to: Type, flags: Flags) -> Option<Imm> {
211    let (a, b) = (lhs.signed(from), rhs.signed(from));
212    let exact = match opcode {
213        // The bitwise three cannot overflow and are the same operation whichever way the
214        // operands are read, so they take the signed reading and are done.
215        Opcode::And => a & b,
216        Opcode::Or => a | b,
217        Opcode::Xor => a ^ b,
218        // The arithmetic three are computed at 128 bits and then asked whether they fit. A type
219        // of 128 bits is the one case where the checked form is doing real work rather than
220        // being a formality, and it is why these are checked rather than wrapping.
221        Opcode::Add => a.checked_add(b)?,
222        Opcode::Sub => a.checked_sub(b)?,
223        _ => a.checked_mul(b)?,
224    };
225    if overflowed(exact, to, flags) {
226        return None;
227    }
228    Some(Imm::int(exact, to))
229}
230
231/// Whether storing `exact` at `to` would lose something the flags promised would not happen.
232///
233/// An operation with neither flag wraps, and wrapping is defined, so the answer there is no
234/// however far outside the type the exact result is.
235fn overflowed(exact: i128, to: Type, flags: Flags) -> bool {
236    let stored = Imm::int(exact, to);
237    if flags.contains(Flags::NSW) && stored.signed(to) != exact {
238        return true;
239    }
240    flags.contains(Flags::NUW) && (exact < 0 || stored.unsigned() != exact as u128)
241}
242
243#[cfg(test)]
244mod tests {
245    use rucc_base::Interner;
246    use rucc_ir::{Block, Builder, Extra, Flags, Func, Module, Opcode, Signature, Type, Value};
247    use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
248
249    use crate::stats::Kind;
250    use crate::{Analyses, Fuel, Pass, fold::Fold};
251
252    /// A function with one block, ready to have instructions appended to it.
253    fn blank() -> (Interner, Func, Block) {
254        let mut names = Interner::new();
255        let name = names.intern("f");
256        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
257        let block = func.create_block();
258        (names, func, block)
259    }
260
261    /// Runs the pass over the function with as much fuel as it wants, and says whether it
262    /// rewrote anything.
263    fn fold(func: &mut Func) -> bool {
264        Fold.run(func, &mut Analyses::new(), &mut Fuel::unlimited()).changed()
265    }
266
267    /// The constant a value now holds, or `None` if it is not one.
268    fn value_of(func: &Func, value: Value, ty: Type) -> Option<i128> {
269        let rucc_ir::Def::Result { inst, .. } = func[value].def else { return None };
270        if func[inst].opcode != Opcode::IConst {
271            return None;
272        }
273        let Extra::Imm(at) = func[inst].extra else { return None };
274        Some(func[at].signed(ty))
275    }
276
277    #[test]
278    fn a_widened_constant_becomes_a_constant_of_the_wider_type() {
279        let (_, mut func, block) = blank();
280        let mut build = Builder::new(&mut func, block);
281        let narrow = build.iconst(Type::int(32), 7);
282        let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
283        build.ret(&[wide]);
284        assert!(fold(&mut func));
285        assert_eq!(value_of(&func, wide, Type::int(64)), Some(7));
286    }
287
288    #[test]
289    fn sign_extension_copies_the_sign_and_zero_extension_does_not() {
290        for (opcode, expected) in [(Opcode::SExt, -1_i128), (Opcode::ZExt, 0xffff_ffff)] {
291            let (_, mut func, block) = blank();
292            let mut build = Builder::new(&mut func, block);
293            let narrow = build.iconst(Type::int(32), -1);
294            let wide = build.unary(opcode, narrow, Type::int(64));
295            build.ret(&[wide]);
296            assert!(fold(&mut func));
297            assert_eq!(value_of(&func, wide, Type::int(64)), Some(expected), "{opcode:?}");
298        }
299    }
300
301    #[test]
302    fn truncation_keeps_the_low_bits_and_reads_them_at_the_narrow_width() {
303        let (_, mut func, block) = blank();
304        let mut build = Builder::new(&mut func, block);
305        let wide = build.iconst(Type::int(32), 0x1234_5680);
306        let narrow = build.unary(Opcode::Trunc, wide, Type::int(8));
307        build.ret(&[narrow]);
308        assert!(fold(&mut func));
309        assert_eq!(value_of(&func, narrow, Type::int(8)), Some(-128));
310    }
311
312    #[test]
313    fn the_arithmetic_and_the_bitwise_operations_are_evaluated() {
314        let cases = [
315            (Opcode::Add, 6_i128, 7_i128, 13_i128),
316            (Opcode::Sub, 6, 7, -1),
317            (Opcode::Mul, 6, 7, 42),
318            (Opcode::And, 0b1100, 0b1010, 0b1000),
319            (Opcode::Or, 0b1100, 0b1010, 0b1110),
320            (Opcode::Xor, 0b1100, 0b1010, 0b0110),
321        ];
322        for (opcode, a, b, want) in cases {
323            let (_, mut func, block) = blank();
324            let mut build = Builder::new(&mut func, block);
325            let lhs = build.iconst(Type::int(64), a);
326            let rhs = build.iconst(Type::int(64), b);
327            let out = build.binary(opcode, lhs, rhs, Flags::NONE);
328            build.ret(&[out]);
329            assert!(fold(&mut func), "{opcode:?}");
330            assert_eq!(value_of(&func, out, Type::int(64)), Some(want), "{opcode:?}");
331        }
332    }
333
334    #[test]
335    fn the_three_shifts_are_evaluated_and_the_two_right_ones_differ_on_the_sign() {
336        let cases = [(Opcode::Shl, -8_i128, 1_i128, -16_i128), (Opcode::AShr, -8, 1, -4)];
337        for (opcode, a, b, want) in cases {
338            let (_, mut func, block) = blank();
339            let mut build = Builder::new(&mut func, block);
340            let lhs = build.iconst(Type::int(64), a);
341            let rhs = build.iconst(Type::int(64), b);
342            let out = build.binary(opcode, lhs, rhs, Flags::NONE);
343            build.ret(&[out]);
344            assert!(fold(&mut func), "{opcode:?}");
345            assert_eq!(value_of(&func, out, Type::int(64)), Some(want), "{opcode:?}");
346        }
347        // The logical shift is the one that reads the value as bits, so minus eight shifted
348        // right by one is a very large positive number rather than minus four.
349        let (_, mut func, block) = blank();
350        let mut build = Builder::new(&mut func, block);
351        let lhs = build.iconst(Type::int(64), -8);
352        let rhs = build.iconst(Type::int(64), 1);
353        let out = build.binary(Opcode::LShr, lhs, rhs, Flags::NONE);
354        build.ret(&[out]);
355        assert!(fold(&mut func));
356        assert_eq!(value_of(&func, out, Type::int(64)), Some(i128::from(i64::MAX) - 3));
357    }
358
359    #[test]
360    fn a_shift_by_the_width_or_more_is_left_alone_because_the_language_does_not_define_it() {
361        for count in [64_i128, 65, -1] {
362            let (_, mut func, block) = blank();
363            let mut build = Builder::new(&mut func, block);
364            let lhs = build.iconst(Type::int(64), 1);
365            let rhs = build.iconst(Type::int(64), count);
366            let out = build.binary(Opcode::Shl, lhs, rhs, Flags::NONE);
367            build.ret(&[out]);
368            assert!(!fold(&mut func), "a shift by {count} was folded");
369        }
370    }
371
372    #[test]
373    fn an_operation_that_wraps_folds_and_the_same_one_promising_it_will_not_does_not() {
374        let big = i128::from(i32::MAX);
375        for (flags, folds) in [(Flags::NONE, true), (Flags::NSW, false)] {
376            let (_, mut func, block) = blank();
377            let mut build = Builder::new(&mut func, block);
378            let lhs = build.iconst(Type::int(32), big);
379            let rhs = build.iconst(Type::int(32), 1);
380            let out = build.binary(Opcode::Add, lhs, rhs, flags);
381            build.ret(&[out]);
382            assert_eq!(fold(&mut func), folds, "{flags}");
383            if folds {
384                assert_eq!(value_of(&func, out, Type::int(32)), Some(i128::from(i32::MIN)));
385            }
386        }
387    }
388
389    #[test]
390    fn an_unsigned_promise_is_broken_by_a_negative_result_as_well_as_by_a_large_one() {
391        let (_, mut func, block) = blank();
392        let mut build = Builder::new(&mut func, block);
393        let lhs = build.iconst(Type::int(32), 1);
394        let rhs = build.iconst(Type::int(32), 2);
395        let out = build.binary(Opcode::Sub, lhs, rhs, Flags::NUW);
396        build.ret(&[out]);
397        assert!(!fold(&mut func));
398    }
399
400    #[test]
401    fn an_operation_with_one_constant_operand_is_left_alone() {
402        let (_, mut func, block) = blank();
403        let param = func.append_param(block, Type::int(64));
404        let mut build = Builder::new(&mut func, block);
405        let rhs = build.iconst(Type::int(64), 7);
406        let out = build.binary(Opcode::Add, param, rhs, Flags::NONE);
407        build.ret(&[out]);
408        assert!(!fold(&mut func));
409        assert_eq!(func[out_inst(&func, out)].opcode, Opcode::Add);
410    }
411
412    #[test]
413    fn a_divide_is_not_folded_even_when_both_operands_are_constants() {
414        for opcode in [Opcode::SDiv, Opcode::UDiv, Opcode::SRem, Opcode::URem] {
415            let (_, mut func, block) = blank();
416            let mut build = Builder::new(&mut func, block);
417            let lhs = build.iconst(Type::int(64), 42);
418            let rhs = build.iconst(Type::int(64), 7);
419            let out = build.binary(opcode, lhs, rhs, Flags::NONE);
420            build.ret(&[out]);
421            assert!(!fold(&mut func), "{opcode:?}");
422        }
423    }
424
425    #[test]
426    fn a_comparison_is_not_folded_because_nothing_lowers_the_bit_it_would_leave_behind() {
427        let (_, mut func, block) = blank();
428        let mut build = Builder::new(&mut func, block);
429        let lhs = build.iconst(Type::int(64), 1);
430        let rhs = build.iconst(Type::int(64), 2);
431        let out = build.icmp(rucc_ir::IntPred::Slt, lhs, rhs);
432        build.ret(&[out]);
433        assert!(!fold(&mut func));
434    }
435
436    #[test]
437    fn folding_leaves_the_function_something_the_verifier_accepts() {
438        let mut names = Interner::new();
439        let name = names.intern("f");
440        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
441        let block = func.create_block();
442        let mut build = Builder::new(&mut func, block);
443        let narrow = build.iconst(Type::int(32), 7);
444        let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
445        build.ret(&[wide]);
446        assert!(fold(&mut func));
447        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
448        let module_name = names.intern("m");
449        let mut module = Module::new(module_name, &target);
450        module.add_func(func);
451        rucc_ir::verify(&module, &names).expect("folding does not break the IR");
452    }
453
454    #[test]
455    fn fuel_stops_the_transformation_and_not_the_walk() {
456        let build_two = |func: &mut Func, block: Block| {
457            let mut build = Builder::new(func, block);
458            let a = build.iconst(Type::int(32), 7);
459            let wide_a = build.unary(Opcode::SExt, a, Type::int(64));
460            let b = build.iconst(Type::int(32), 9);
461            let wide_b = build.unary(Opcode::SExt, b, Type::int(64));
462            let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
463            build.ret(&[sum]);
464            (wide_a, wide_b)
465        };
466
467        let (_, mut none, block) = blank();
468        let (first, _) = build_two(&mut none, block);
469        let stats = Fold.run(&mut none, &mut Analyses::new(), &mut Fuel::of(0));
470        assert!(!stats.changed());
471        assert_eq!(none[out_inst(&none, first)].opcode, Opcode::SExt);
472        // Both of them looked at and neither of them folded, which is the count a bisection is
473        // reading: how many sites are left past where the fuel ran out.
474        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 2);
475
476        let (_, mut one, block) = blank();
477        let (first, second) = build_two(&mut one, block);
478        let mut fuel = Fuel::of(1);
479        let stats = Fold.run(&mut one, &mut Analyses::new(), &mut fuel);
480        assert!(stats.changed());
481        assert_eq!(fuel.spent(), 1);
482        assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 1);
483        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
484        assert_eq!(one[out_inst(&one, first)].opcode, Opcode::IConst);
485        assert_eq!(one[out_inst(&one, second)].opcode, Opcode::SExt);
486    }
487
488    #[test]
489    fn folding_one_operation_uncovers_the_next() {
490        let (_, mut func, block) = blank();
491        let mut build = Builder::new(&mut func, block);
492        let a = build.iconst(Type::int(32), 7);
493        let wide = build.unary(Opcode::SExt, a, Type::int(64));
494        let b = build.iconst(Type::int(64), 9);
495        let sum = build.binary(Opcode::Add, wide, b, Flags::NONE);
496        build.ret(&[sum]);
497        assert!(fold(&mut func));
498        // One walk in order is enough for this shape, because a constant is written before it
499        // is used and the walk is in the same order.
500        assert_eq!(value_of(&func, sum, Type::int(64)), Some(16));
501    }
502
503    #[test]
504    fn a_constant_is_left_where_it_is_and_folding_it_again_changes_nothing() {
505        let (_, mut func, block) = blank();
506        let mut build = Builder::new(&mut func, block);
507        let a = build.iconst(Type::int(32), 7);
508        let wide = build.unary(Opcode::SExt, a, Type::int(64));
509        build.ret(&[wide]);
510        assert!(fold(&mut func));
511        assert!(!fold(&mut func), "a second run found something to do");
512    }
513
514    /// The instruction that defines a value, which every value in these tests has.
515    fn out_inst(func: &Func, value: Value) -> rucc_ir::Inst {
516        match func[value].def {
517            rucc_ir::Def::Result { inst, .. } => inst,
518            rucc_ir::Def::Param { .. } => panic!("a parameter has no instruction"),
519        }
520    }
521}