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::{Fuel, Pass};
48
49/// The pass. It holds nothing, because folding needs to know nothing beyond the instruction.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Fold;
52
53impl Pass for Fold {
54    fn name(&self) -> &'static str {
55        "fold"
56    }
57
58    fn describe(&self) -> &'static str {
59        "an integer instruction whose operands are all constants becomes a constant"
60    }
61
62    fn run(&self, func: &mut Func, fuel: &mut Fuel) -> bool {
63        let blocks: Vec<Block> = func.blocks().collect();
64        let mut changed = false;
65        for block in blocks {
66            let insts: Vec<Inst> = func.insts(block).collect();
67            for inst in insts {
68                let Some(folded) = evaluate(func, inst) else { continue };
69                if !fuel.take() {
70                    // Out of fuel, which is a request to stop transforming rather than to stop
71                    // looking. Continuing the walk costs nothing and keeps the count of what
72                    // could have been folded the same at every fuel setting, which is what makes
73                    // a bisection over it monotonic.
74                    continue;
75                }
76                let ty = func[result_of(func, inst)].ty;
77                let at = func.add_imm(folded);
78                let data = &mut func[inst];
79                data.opcode = Opcode::IConst;
80                data.flags = Flags::NONE;
81                data.args = rucc_ir::ValueList::EMPTY;
82                data.extra = Extra::Imm(at);
83                debug_assert!(ty.is_int(), "only an integer instruction folds");
84                changed = true;
85            }
86        }
87        changed
88    }
89}
90
91/// The single result of an instruction that folded.
92fn result_of(func: &Func, inst: Inst) -> Value {
93    func[inst].results().next().expect("an instruction that folds produces a value")
94}
95
96/// What this instruction evaluates to, if it evaluates to anything.
97///
98/// `None` covers every reason not to fold and does not distinguish between them, because the
99/// answer to all of them is the same: leave the instruction alone.
100fn evaluate(func: &Func, inst: Inst) -> Option<Imm> {
101    let data = &func[inst];
102    if data.results != 1 {
103        return None;
104    }
105    let result = data.results().next()?;
106    let ty = func[result].ty;
107    // A vector constant is a `splat` rather than an `iconst`, so a vector fold would have to
108    // build a different instruction and would have to be right about the lane count as well.
109    if !ty.is_int() || !ty.is_scalar() {
110        return None;
111    }
112    let args = &func[data.args];
113    match data.opcode {
114        Opcode::Trunc | Opcode::SExt | Opcode::ZExt => {
115            let (value, from) = constant(func, *args.first()?)?;
116            Some(convert(data.opcode, value, from, ty))
117        }
118        Opcode::Shl | Opcode::LShr | Opcode::AShr => {
119            let (value, from) = constant(func, *args.first()?)?;
120            let (count, count_ty) = constant(func, *args.get(1)?)?;
121            shift(data.opcode, value, from, count, count_ty, ty, data.flags)
122        }
123        Opcode::Add | Opcode::Sub | Opcode::Mul | Opcode::And | Opcode::Or | Opcode::Xor => {
124            let (lhs, lhs_ty) = constant(func, *args.first()?)?;
125            let (rhs, _) = constant(func, *args.get(1)?)?;
126            binary(data.opcode, lhs, rhs, lhs_ty, ty, data.flags)
127        }
128        _ => None,
129    }
130}
131
132/// The constant this value is, with the type it has, if it is one.
133fn constant(func: &Func, value: Value) -> Option<(Imm, Type)> {
134    let Def::Result { inst, .. } = func[value].def else { return None };
135    if func[inst].opcode != Opcode::IConst {
136        return None;
137    }
138    let Extra::Imm(at) = func[inst].extra else { return None };
139    let ty = func[value].ty;
140    ty.is_int().then(|| (func[at], ty))
141}
142
143/// A widening or a narrowing of a constant.
144fn convert(opcode: Opcode, value: Imm, from: Type, to: Type) -> Imm {
145    match opcode {
146        // Truncation is the masking that `Imm::int` does anyway, and sign extension is reading
147        // the value as signed at its own width and storing it at the wider one.
148        Opcode::Trunc | Opcode::SExt => Imm::int(value.signed(from), to),
149        // Zero extension reads the same bits as unsigned, which for a width below 128 is a
150        // non-negative number and survives the cast to the signed type `Imm::int` takes.
151        _ => Imm::int(value.unsigned() as i128, to),
152    }
153}
154
155/// A shift of a constant by a constant.
156///
157/// `None` when the count is not one the language defines, which is a count at or above the width
158/// of the value. The result there is poison and folding it would be picking an answer for a
159/// program that asked for none.
160fn shift(
161    opcode: Opcode,
162    value: Imm,
163    from: Type,
164    count: Imm,
165    count_ty: Type,
166    to: Type,
167    flags: Flags,
168) -> Option<Imm> {
169    let by = count.unsigned();
170    if by >= u128::from(to.bits()) || count.signed(count_ty) < 0 {
171        return None;
172    }
173    let by = by as u32;
174    let exact = match opcode {
175        Opcode::Shl => value.signed(from).checked_shl(by)?,
176        // A logical shift right is on the bits rather than on the number, so it reads unsigned
177        // and the cast back cannot lose anything: the value has at most `from.bits()` bits set
178        // and shifting right sets none.
179        Opcode::LShr => (value.unsigned() >> by) as i128,
180        _ => value.signed(from) >> by,
181    };
182    if opcode == Opcode::Shl && overflowed(exact, to, flags) {
183        return None;
184    }
185    Some(Imm::int(exact, to))
186}
187
188/// An arithmetic or bitwise operation on two constants.
189fn binary(opcode: Opcode, lhs: Imm, rhs: Imm, from: Type, to: Type, flags: Flags) -> Option<Imm> {
190    let (a, b) = (lhs.signed(from), rhs.signed(from));
191    let exact = match opcode {
192        // The bitwise three cannot overflow and are the same operation whichever way the
193        // operands are read, so they take the signed reading and are done.
194        Opcode::And => a & b,
195        Opcode::Or => a | b,
196        Opcode::Xor => a ^ b,
197        // The arithmetic three are computed at 128 bits and then asked whether they fit. A type
198        // of 128 bits is the one case where the checked form is doing real work rather than
199        // being a formality, and it is why these are checked rather than wrapping.
200        Opcode::Add => a.checked_add(b)?,
201        Opcode::Sub => a.checked_sub(b)?,
202        _ => a.checked_mul(b)?,
203    };
204    if overflowed(exact, to, flags) {
205        return None;
206    }
207    Some(Imm::int(exact, to))
208}
209
210/// Whether storing `exact` at `to` would lose something the flags promised would not happen.
211///
212/// An operation with neither flag wraps, and wrapping is defined, so the answer there is no
213/// however far outside the type the exact result is.
214fn overflowed(exact: i128, to: Type, flags: Flags) -> bool {
215    let stored = Imm::int(exact, to);
216    if flags.contains(Flags::NSW) && stored.signed(to) != exact {
217        return true;
218    }
219    flags.contains(Flags::NUW) && (exact < 0 || stored.unsigned() != exact as u128)
220}
221
222#[cfg(test)]
223mod tests {
224    use rucc_base::Interner;
225    use rucc_ir::{Block, Builder, Extra, Flags, Func, Module, Opcode, Signature, Type, Value};
226    use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
227
228    use crate::{Fuel, Pass, fold::Fold};
229
230    /// A function with one block, ready to have instructions appended to it.
231    fn blank() -> (Interner, Func, Block) {
232        let mut names = Interner::new();
233        let name = names.intern("f");
234        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
235        let block = func.create_block();
236        (names, func, block)
237    }
238
239    /// Runs the pass over the function with as much fuel as it wants.
240    fn fold(func: &mut Func) -> bool {
241        Fold.run(func, &mut Fuel::unlimited())
242    }
243
244    /// The constant a value now holds, or `None` if it is not one.
245    fn value_of(func: &Func, value: Value, ty: Type) -> Option<i128> {
246        let rucc_ir::Def::Result { inst, .. } = func[value].def else { return None };
247        if func[inst].opcode != Opcode::IConst {
248            return None;
249        }
250        let Extra::Imm(at) = func[inst].extra else { return None };
251        Some(func[at].signed(ty))
252    }
253
254    #[test]
255    fn a_widened_constant_becomes_a_constant_of_the_wider_type() {
256        let (_, mut func, block) = blank();
257        let mut build = Builder::new(&mut func, block);
258        let narrow = build.iconst(Type::int(32), 7);
259        let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
260        build.ret(&[wide]);
261        assert!(fold(&mut func));
262        assert_eq!(value_of(&func, wide, Type::int(64)), Some(7));
263    }
264
265    #[test]
266    fn sign_extension_copies_the_sign_and_zero_extension_does_not() {
267        for (opcode, expected) in [(Opcode::SExt, -1_i128), (Opcode::ZExt, 0xffff_ffff)] {
268            let (_, mut func, block) = blank();
269            let mut build = Builder::new(&mut func, block);
270            let narrow = build.iconst(Type::int(32), -1);
271            let wide = build.unary(opcode, narrow, Type::int(64));
272            build.ret(&[wide]);
273            assert!(fold(&mut func));
274            assert_eq!(value_of(&func, wide, Type::int(64)), Some(expected), "{opcode:?}");
275        }
276    }
277
278    #[test]
279    fn truncation_keeps_the_low_bits_and_reads_them_at_the_narrow_width() {
280        let (_, mut func, block) = blank();
281        let mut build = Builder::new(&mut func, block);
282        let wide = build.iconst(Type::int(32), 0x1234_5680);
283        let narrow = build.unary(Opcode::Trunc, wide, Type::int(8));
284        build.ret(&[narrow]);
285        assert!(fold(&mut func));
286        assert_eq!(value_of(&func, narrow, Type::int(8)), Some(-128));
287    }
288
289    #[test]
290    fn the_arithmetic_and_the_bitwise_operations_are_evaluated() {
291        let cases = [
292            (Opcode::Add, 6_i128, 7_i128, 13_i128),
293            (Opcode::Sub, 6, 7, -1),
294            (Opcode::Mul, 6, 7, 42),
295            (Opcode::And, 0b1100, 0b1010, 0b1000),
296            (Opcode::Or, 0b1100, 0b1010, 0b1110),
297            (Opcode::Xor, 0b1100, 0b1010, 0b0110),
298        ];
299        for (opcode, a, b, want) in cases {
300            let (_, mut func, block) = blank();
301            let mut build = Builder::new(&mut func, block);
302            let lhs = build.iconst(Type::int(64), a);
303            let rhs = build.iconst(Type::int(64), b);
304            let out = build.binary(opcode, lhs, rhs, Flags::NONE);
305            build.ret(&[out]);
306            assert!(fold(&mut func), "{opcode:?}");
307            assert_eq!(value_of(&func, out, Type::int(64)), Some(want), "{opcode:?}");
308        }
309    }
310
311    #[test]
312    fn the_three_shifts_are_evaluated_and_the_two_right_ones_differ_on_the_sign() {
313        let cases = [(Opcode::Shl, -8_i128, 1_i128, -16_i128), (Opcode::AShr, -8, 1, -4)];
314        for (opcode, a, b, want) in cases {
315            let (_, mut func, block) = blank();
316            let mut build = Builder::new(&mut func, block);
317            let lhs = build.iconst(Type::int(64), a);
318            let rhs = build.iconst(Type::int(64), b);
319            let out = build.binary(opcode, lhs, rhs, Flags::NONE);
320            build.ret(&[out]);
321            assert!(fold(&mut func), "{opcode:?}");
322            assert_eq!(value_of(&func, out, Type::int(64)), Some(want), "{opcode:?}");
323        }
324        // The logical shift is the one that reads the value as bits, so minus eight shifted
325        // right by one is a very large positive number rather than minus four.
326        let (_, mut func, block) = blank();
327        let mut build = Builder::new(&mut func, block);
328        let lhs = build.iconst(Type::int(64), -8);
329        let rhs = build.iconst(Type::int(64), 1);
330        let out = build.binary(Opcode::LShr, lhs, rhs, Flags::NONE);
331        build.ret(&[out]);
332        assert!(fold(&mut func));
333        assert_eq!(value_of(&func, out, Type::int(64)), Some(i128::from(i64::MAX) - 3));
334    }
335
336    #[test]
337    fn a_shift_by_the_width_or_more_is_left_alone_because_the_language_does_not_define_it() {
338        for count in [64_i128, 65, -1] {
339            let (_, mut func, block) = blank();
340            let mut build = Builder::new(&mut func, block);
341            let lhs = build.iconst(Type::int(64), 1);
342            let rhs = build.iconst(Type::int(64), count);
343            let out = build.binary(Opcode::Shl, lhs, rhs, Flags::NONE);
344            build.ret(&[out]);
345            assert!(!fold(&mut func), "a shift by {count} was folded");
346        }
347    }
348
349    #[test]
350    fn an_operation_that_wraps_folds_and_the_same_one_promising_it_will_not_does_not() {
351        let big = i128::from(i32::MAX);
352        for (flags, folds) in [(Flags::NONE, true), (Flags::NSW, false)] {
353            let (_, mut func, block) = blank();
354            let mut build = Builder::new(&mut func, block);
355            let lhs = build.iconst(Type::int(32), big);
356            let rhs = build.iconst(Type::int(32), 1);
357            let out = build.binary(Opcode::Add, lhs, rhs, flags);
358            build.ret(&[out]);
359            assert_eq!(fold(&mut func), folds, "{flags}");
360            if folds {
361                assert_eq!(value_of(&func, out, Type::int(32)), Some(i128::from(i32::MIN)));
362            }
363        }
364    }
365
366    #[test]
367    fn an_unsigned_promise_is_broken_by_a_negative_result_as_well_as_by_a_large_one() {
368        let (_, mut func, block) = blank();
369        let mut build = Builder::new(&mut func, block);
370        let lhs = build.iconst(Type::int(32), 1);
371        let rhs = build.iconst(Type::int(32), 2);
372        let out = build.binary(Opcode::Sub, lhs, rhs, Flags::NUW);
373        build.ret(&[out]);
374        assert!(!fold(&mut func));
375    }
376
377    #[test]
378    fn an_operation_with_one_constant_operand_is_left_alone() {
379        let (_, mut func, block) = blank();
380        let param = func.append_param(block, Type::int(64));
381        let mut build = Builder::new(&mut func, block);
382        let rhs = build.iconst(Type::int(64), 7);
383        let out = build.binary(Opcode::Add, param, rhs, Flags::NONE);
384        build.ret(&[out]);
385        assert!(!fold(&mut func));
386        assert_eq!(func[out_inst(&func, out)].opcode, Opcode::Add);
387    }
388
389    #[test]
390    fn a_divide_is_not_folded_even_when_both_operands_are_constants() {
391        for opcode in [Opcode::SDiv, Opcode::UDiv, Opcode::SRem, Opcode::URem] {
392            let (_, mut func, block) = blank();
393            let mut build = Builder::new(&mut func, block);
394            let lhs = build.iconst(Type::int(64), 42);
395            let rhs = build.iconst(Type::int(64), 7);
396            let out = build.binary(opcode, lhs, rhs, Flags::NONE);
397            build.ret(&[out]);
398            assert!(!fold(&mut func), "{opcode:?}");
399        }
400    }
401
402    #[test]
403    fn a_comparison_is_not_folded_because_nothing_lowers_the_bit_it_would_leave_behind() {
404        let (_, mut func, block) = blank();
405        let mut build = Builder::new(&mut func, block);
406        let lhs = build.iconst(Type::int(64), 1);
407        let rhs = build.iconst(Type::int(64), 2);
408        let out = build.icmp(rucc_ir::IntPred::Slt, lhs, rhs);
409        build.ret(&[out]);
410        assert!(!fold(&mut func));
411    }
412
413    #[test]
414    fn folding_leaves_the_function_something_the_verifier_accepts() {
415        let mut names = Interner::new();
416        let name = names.intern("f");
417        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
418        let block = func.create_block();
419        let mut build = Builder::new(&mut func, block);
420        let narrow = build.iconst(Type::int(32), 7);
421        let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
422        build.ret(&[wide]);
423        assert!(fold(&mut func));
424        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
425        let module_name = names.intern("m");
426        let mut module = Module::new(module_name, &target);
427        module.add_func(func);
428        rucc_ir::verify(&module, &names).expect("folding does not break the IR");
429    }
430
431    #[test]
432    fn fuel_stops_the_transformation_and_not_the_walk() {
433        let build_two = |func: &mut Func, block: Block| {
434            let mut build = Builder::new(func, block);
435            let a = build.iconst(Type::int(32), 7);
436            let wide_a = build.unary(Opcode::SExt, a, Type::int(64));
437            let b = build.iconst(Type::int(32), 9);
438            let wide_b = build.unary(Opcode::SExt, b, Type::int(64));
439            let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
440            build.ret(&[sum]);
441            (wide_a, wide_b)
442        };
443
444        let (_, mut none, block) = blank();
445        let (first, _) = build_two(&mut none, block);
446        assert!(!Fold.run(&mut none, &mut Fuel::of(0)));
447        assert_eq!(none[out_inst(&none, first)].opcode, Opcode::SExt);
448
449        let (_, mut one, block) = blank();
450        let (first, second) = build_two(&mut one, block);
451        let mut fuel = Fuel::of(1);
452        assert!(Fold.run(&mut one, &mut fuel));
453        assert_eq!(fuel.spent(), 1);
454        assert_eq!(one[out_inst(&one, first)].opcode, Opcode::IConst);
455        assert_eq!(one[out_inst(&one, second)].opcode, Opcode::SExt);
456    }
457
458    #[test]
459    fn folding_one_operation_uncovers_the_next() {
460        let (_, mut func, block) = blank();
461        let mut build = Builder::new(&mut func, block);
462        let a = build.iconst(Type::int(32), 7);
463        let wide = build.unary(Opcode::SExt, a, Type::int(64));
464        let b = build.iconst(Type::int(64), 9);
465        let sum = build.binary(Opcode::Add, wide, b, Flags::NONE);
466        build.ret(&[sum]);
467        assert!(fold(&mut func));
468        // One walk in order is enough for this shape, because a constant is written before it
469        // is used and the walk is in the same order.
470        assert_eq!(value_of(&func, sum, Type::int(64)), Some(16));
471    }
472
473    #[test]
474    fn a_constant_is_left_where_it_is_and_folding_it_again_changes_nothing() {
475        let (_, mut func, block) = blank();
476        let mut build = Builder::new(&mut func, block);
477        let a = build.iconst(Type::int(32), 7);
478        let wide = build.unary(Opcode::SExt, a, Type::int(64));
479        build.ret(&[wide]);
480        assert!(fold(&mut func));
481        assert!(!fold(&mut func), "a second run found something to do");
482    }
483
484    /// The instruction that defines a value, which every value in these tests has.
485    fn out_inst(func: &Func, value: Value) -> rucc_ir::Inst {
486        match func[value].def {
487            rucc_ir::Def::Result { inst, .. } => inst,
488            rucc_ir::Def::Param { .. } => panic!("a parameter has no instruction"),
489        }
490    }
491}