Skip to main content

rucc_opt/
narrow.rs

1//! Width narrowing: arithmetic redone at the width the program actually uses.
2//!
3//! The lowering rule set is written at an opcode and a width together, so `add.i8` and `add.i32`
4//! are two rules and the machine can be asked to add two bytes as easily as two words. C never
5//! asks it to. The integer promotions say the operands of an arithmetic operator go to `int`
6//! first, so `char a, b; a + b` is an `int` addition of two sign extended bytes, and the front end
7//! is right to write it that way because that is what the language says the expression means.
8//!
9//! That leaves the promoted form as the only form, and on x86-64 it is often the wrong one. A
10//! byte compare against a byte is a `cmpb`, and two `movsbl` are not needed to reach it. A byte
11//! add whose result is stored back into a `char` throws away every bit the promotion computed.
12//! The promoted shape exists because C says so and not because the machine wants it. This is
13//! issue 375.
14//!
15//! # The two shapes
16//!
17//! A truncation of arithmetic. The low bits of a sum, a difference, a product, a bitwise
18//! operation or a shift by a constant depend only on the low bits of what went into it, so
19//! `trunc.i8 (add.i32 (sext a) (sext b))` is `add.i8 a b` and the two extensions are left with
20//! nothing reading them. That is the arithmetic half, and it is what `char c = a + b;` is.
21//!
22//! A comparison of extensions. Sign extension is an order isomorphism onto its image under both
23//! readings of the bits, so a comparison of two of them at any predicate is the same comparison of
24//! what they extended. Zero extension is one under the unsigned reading and is not one under the
25//! signed reading, since it takes a negative byte to a positive word, so it carries the equalities
26//! and the unsigned predicates over and not the signed ones. That is what `char a, b; a < b` is.
27//!
28//! Both are written so that one side may be a constant instead, because `if (c == 'x')` is the
29//! common case and the constant is representable at the narrow width whenever the comparison is
30//! not already decided.
31//!
32//! # Why it always pays
33//!
34//! Neither shape is applied unless every leaf it reaches narrows for nothing. A leaf is what an
35//! extension extended, which is already the narrow value, or a constant, which is written down
36//! again. So the rewrite replaces a wide operation, its extensions and the truncation with one
37//! narrow operation and never leaves a widening behind to pay for a narrowing. Everything in
38//! between is required to have exactly one reader, which is the operation above it, so the whole
39//! subtree it replaces is dead the moment it is replaced.
40//!
41//! That is the whole profitability argument, and it is deliberately a structural one rather than
42//! a cost model. A pass whose payoff has to be estimated is a pass whose payoff can be wrong.
43//!
44//! # What it does not narrow
45//!
46//! Not a divide or a remainder. `char a = -128, b = -1; char c = a / b;` is well defined in C: the
47//! division happens at `int`, gives 128, and the conversion back to `char` is what makes it minus
48//! 128 again. The same division at one byte is the overflow case that raises on this machine, so
49//! narrowing it turns a program that works into a program that dies. It needs a range that says
50//! the operands miss that one pair, and ranges are the analysis this pass does not have.
51//!
52//! Not a shift by a value. `char c; c <<= n;` shifts at `int`, so a count of twenty is a defined
53//! shift whose low eight bits are zero, and the same count at one byte is poison. A shift by a
54//! constant below the narrow width has neither problem and is narrowed.
55//!
56//! Not a signed operation's overflow flags. A sum that could not overflow at four bytes can
57//! overflow at one, so `nsw` and `nuw` do not come along. Dropping them is a refinement in the
58//! safe direction: it makes the operation more defined rather than less.
59//!
60//! # What is left for the analysis
61//!
62//! The width here is the one the truncation names. A real demanded bits analysis would let it
63//! shrink further, so that `(x & 0xff) + 1` narrows on the strength of the mask rather than on the
64//! strength of a truncation that is not written, and so that a value read at three widths is
65//! narrowed to the widest of them rather than to none. That is the first box of issue 375 and it
66//! wants the analysis manager, which wants the dominator tree, which is the next thing to build.
67
68use rucc_ir::{Block, Def, Extra, Flags, Func, Imm, Inst, InstData, Opcode, Type, Value};
69
70use crate::uses::count;
71use crate::{Analyses, Fuel, Pass, Preserved, Stats};
72
73/// Recorded once for each subtree redone at the narrow width.
74const NARROWED: &str = "arithmetic redone at the width the program truncates it to";
75
76/// Recorded for a subtree that would have been redone if there had been fuel for it.
77const NO_FUEL: &str = "arithmetic left wide, the pass ran out of fuel";
78
79/// How deep the walk from a truncation goes before it gives up.
80///
81/// A chain of arithmetic is as long as the expression somebody wrote, and generated C writes long
82/// ones, so a walk with no limit is a stack overflow waiting for the right input file. Six is
83/// deeper than hand written C reaches and shallow enough that the recursion cannot cost anything,
84/// and an expression deeper than this narrows from whatever truncation is nearer to its leaves.
85const DEPTH: u32 = 6;
86
87/// The pass. It holds nothing, because the width it narrows to is the one the truncation names.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct Narrow;
90
91impl Pass for Narrow {
92    fn name(&self) -> &'static str {
93        "narrow"
94    }
95
96    fn describe(&self) -> &'static str {
97        "arithmetic the program truncates is redone at the width it truncates to"
98    }
99
100    fn preserves(&self) -> Preserved {
101        // The arithmetic is redone at another width in the block it was already in. Widths are
102        // not something the graph, the trees or the forest have an opinion about.
103        Preserved::ALL
104    }
105
106    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
107        let mut stats = Stats::new();
108        let mut uses = count(func);
109        for block in func.blocks().collect::<Vec<Block>>() {
110            for inst in func.insts(block).collect::<Vec<Inst>>() {
111                let Some(redo) = truncated_arithmetic(func, inst, &uses)
112                    .or_else(|| extended_comparison(func, inst))
113                else {
114                    continue;
115                };
116                if !fuel.take() {
117                    // Out of fuel, which stops the transforming rather than the looking, the
118                    // same way the other three passes treat it. The walk is the same walk at
119                    // every fuel setting, which is what makes bisecting over it monotonic.
120                    stats.missed(NO_FUEL);
121                    continue;
122                }
123                apply(func, inst, &redo, &mut uses);
124                stats.optimized(NARROWED);
125            }
126        }
127        stats
128    }
129}
130
131/// An instruction rewritten at the narrow width, with its operands narrowed too.
132struct Redo {
133    /// What the instruction becomes, which is the wide operation at the narrow width.
134    opcode: Opcode,
135    /// The predicate, for a comparison, and nothing for arithmetic.
136    extra: Extra,
137    /// The width everything under this is redone at.
138    ty: Type,
139    /// The left operand.
140    lhs: Plan,
141    /// The right operand.
142    rhs: Plan,
143}
144
145/// What an operand becomes at the narrow width.
146enum Plan {
147    /// A value that already has it, which is what an extension was extending.
148    Already(Value),
149    /// A constant, written down again at the narrow width.
150    Constant(i128),
151    /// An operation redone, which is the recursive case and the reason this is a tree.
152    Nested(Box<Redo>),
153}
154
155/// Whether this is a truncation of arithmetic that can be redone narrow, and what it becomes.
156///
157/// The truncation is the root because it is the only place the narrow width is written down. Its
158/// operand has to be read by nothing else, since a second reader would keep the wide operation
159/// alive and the rewrite would be a second instruction rather than a replacement.
160fn truncated_arithmetic(func: &Func, inst: Inst, uses: &[u32]) -> Option<Redo> {
161    let data = &func[inst];
162    if data.opcode != Opcode::Trunc {
163        return None;
164    }
165    let ty = func[data.results().next()?].ty;
166    if !narrowable(ty) {
167        return None;
168    }
169    redo(func, *func[data.args].first()?, ty, uses, DEPTH)
170}
171
172/// Whether a width is one this pass will redo an operation at.
173///
174/// An integer scalar of a byte or more. The lower bound is the interesting half. One bit is an
175/// integer type in the IR and a comparison against a zero extended truth is a comparison the
176/// argument narrows all the way down to it, and `spec/12-instruction-selection.md` says a one bit
177/// value is a truth rather than a width: `tamnd/rucc#352` is the list of what a target lowers at
178/// that width and it is `and`, `or`, `xor`, a constant and the widening out of one. Narrowing an
179/// `icmp` into it would be asking every target for something no target has, so the floor is the
180/// narrowest width a machine holds a number in.
181const fn narrowable(ty: Type) -> bool {
182    ty.is_int() && ty.is_scalar() && ty.bits() >= 8
183}
184
185/// Whether this value is arithmetic that can be redone at that width, and what it becomes.
186fn redo(func: &Func, value: Value, ty: Type, uses: &[u32], depth: u32) -> Option<Redo> {
187    if depth == 0 || uses[value.index()] != 1 {
188        return None;
189    }
190    let Def::Result { inst, .. } = func[value].def else { return None };
191    let data = &func[inst];
192    if !low_bits_only(data.opcode) {
193        return None;
194    }
195    let args = &func[data.args];
196    let (&left, &right) = (args.first()?, args.get(1)?);
197    let lhs = plan(func, left, ty, uses, depth)?;
198    // A shift is the one operation whose right operand is not a number of the same kind as its
199    // left one, and it is the one that is unsafe to narrow when that operand is not a constant.
200    let rhs = match data.opcode {
201        Opcode::Shl => Plan::Constant(count_below(func, right, ty)?),
202        _ => plan(func, right, ty, uses, depth)?,
203    };
204    Some(Redo { opcode: data.opcode, extra: Extra::None, ty, lhs, rhs })
205}
206
207/// What an operand becomes at that width, or `None` when it would cost something to get there.
208fn plan(func: &Func, value: Value, ty: Type, uses: &[u32], depth: u32) -> Option<Plan> {
209    if let Some(narrow) = extended(func, value, ty) {
210        return Some(Plan::Already(narrow));
211    }
212    if let Some((imm, wide)) = constant(func, value) {
213        return Some(Plan::Constant(imm.signed(wide)));
214    }
215    redo(func, value, ty, uses, depth - 1).map(|redo| Plan::Nested(Box::new(redo)))
216}
217
218/// Whether an operation's low bits depend only on the low bits of what went into it.
219///
220/// True of the four that carry left to right and of the three that work a bit at a time. Not true
221/// of a divide, a remainder or a shift right, all of which read bits above the ones they produce.
222const fn low_bits_only(opcode: Opcode) -> bool {
223    matches!(
224        opcode,
225        Opcode::Add
226            | Opcode::Sub
227            | Opcode::Mul
228            | Opcode::And
229            | Opcode::Or
230            | Opcode::Xor
231            | Opcode::Shl
232    )
233}
234
235/// Whether this is a comparison of two things extended from the same narrower width.
236///
237/// Sign extension keeps the order of what it extends under both readings of the bits, so every
238/// predicate survives it. Zero extension keeps the unsigned order and not the signed one, since it
239/// takes a negative byte to a positive word, so it carries the equalities and the unsigned
240/// predicates and refuses the signed ones.
241///
242/// The two sides have to be the same extension as well as from the same width. `(signed char) a <
243/// b` where `b` is an `unsigned char` is a sign extension against a zero extension, and comparing
244/// what they extended is comparing a byte against a byte at one predicate where the wide
245/// comparison had a signed byte against an unsigned one. Both readings of the narrow comparison
246/// are wrong, and the wide comparison is right, which is the whole reason C promotes.
247fn extended_comparison(func: &Func, inst: Inst) -> Option<Redo> {
248    let data = &func[inst];
249    if data.opcode != Opcode::ICmp {
250        return None;
251    }
252    let Extra::IntPred(pred) = data.extra else { return None };
253    let args = &func[data.args];
254    let (&left, &right) = (args.first()?, args.get(1)?);
255    let (kind, ty, narrow) = widening(func, left)?;
256    if !narrowable(ty) {
257        return None;
258    }
259    if kind == Opcode::ZExt && pred.is_signed() {
260        return None;
261    }
262    let rhs = match widening(func, right) {
263        Some((same, from, other)) if same == kind && from == ty => Plan::Already(other),
264        _ => Plan::Constant(survives(func, right, kind, ty)?),
265    };
266    Some(Redo { opcode: Opcode::ICmp, extra: data.extra, ty, lhs: Plan::Already(narrow), rhs })
267}
268
269/// The extension this value is, as the kind, the width it came from and the value it extended.
270fn widening(func: &Func, value: Value) -> Option<(Opcode, Type, Value)> {
271    let Def::Result { inst, .. } = func[value].def else { return None };
272    let data = &func[inst];
273    if data.opcode != Opcode::SExt && data.opcode != Opcode::ZExt {
274        return None;
275    }
276    let narrow = *func[data.args].first()?;
277    Some((data.opcode, func[narrow].ty, narrow))
278}
279
280/// What this value was before it was extended to that width, when that is what it is.
281///
282/// Which extension it was is not asked, because this is the arithmetic side and the arithmetic
283/// reads the low bits only. Those are the bits the extension copied, whichever one it was.
284fn extended(func: &Func, value: Value, ty: Type) -> Option<Value> {
285    let (_, from, narrow) = widening(func, value)?;
286    (from == ty).then_some(narrow)
287}
288
289/// The constant this value is, with the type it has.
290fn constant(func: &Func, value: Value) -> Option<(Imm, Type)> {
291    let Def::Result { inst, .. } = func[value].def else { return None };
292    let data = &func[inst];
293    let Extra::Imm(at) = data.extra else { return None };
294    if data.opcode != Opcode::IConst {
295        return None;
296    }
297    let ty = func[value].ty;
298    ty.is_int().then(|| (func[at], ty))
299}
300
301/// A shift count that is a constant below the narrow width, which is the only one that narrows.
302///
303/// A count at or above the width is poison at the narrow width and is a defined shift to zero at
304/// the wide one, so the guard is what keeps the rewrite from inventing undefined behaviour. A
305/// count that is not a constant cannot be guarded, since its value is what decides.
306fn count_below(func: &Func, value: Value, ty: Type) -> Option<i128> {
307    let (imm, wide) = constant(func, value)?;
308    let by = imm.signed(wide);
309    (by >= 0 && by < i128::from(ty.bits())).then_some(by)
310}
311
312/// A constant that is the extension of a constant at the narrow width, as that narrow constant.
313///
314/// Both extensions are injective, so a comparison against a constant in the image of one is the
315/// same comparison against what it is the image of. A constant outside the image is a comparison
316/// that is already decided, which is a thing for folding to say rather than for this to guess at.
317fn survives(func: &Func, value: Value, kind: Opcode, ty: Type) -> Option<i128> {
318    let (imm, wide) = constant(func, value)?;
319    let k = imm.signed(wide);
320    let back = Imm::int(k, ty).signed(ty);
321    let same = if kind == Opcode::SExt { back } else { Imm::int(k, ty).unsigned() as i128 };
322    (same == k).then_some(k)
323}
324
325/// Rewrites the instruction into what the plan says it is.
326///
327/// In place, because the result already has the narrow type and every use of it is already
328/// correct, which is the same reason folding and the peephole rewrite in place. What is left
329/// behind is the wide subtree, now read by nothing, which is what dead code elimination is for.
330fn apply(func: &mut Func, inst: Inst, redo: &Redo, uses: &mut Vec<u32>) {
331    let lhs = build(func, inst, redo.ty, &redo.lhs, uses);
332    let rhs = build(func, inst, redo.ty, &redo.rhs, uses);
333    for value in func[func[inst].args].iter().copied() {
334        uses[value.index()] -= 1;
335    }
336    let args = func.push_values(&[lhs, rhs]);
337    uses[lhs.index()] += 1;
338    uses[rhs.index()] += 1;
339    let data = &mut func[inst];
340    data.opcode = redo.opcode;
341    // No flags. An operation that could not overflow at the wide width can overflow at the narrow
342    // one, so `nsw` and `nuw` do not survive the narrowing, and dropping them makes the operation
343    // more defined rather than less.
344    data.flags = Flags::NONE;
345    data.args = args;
346    data.extra = redo.extra;
347}
348
349/// The value an operand's plan comes to, writing whatever it needs in front of the instruction.
350fn build(func: &mut Func, before: Inst, ty: Type, plan: &Plan, uses: &mut Vec<u32>) -> Value {
351    match plan {
352        Plan::Already(value) => *value,
353        Plan::Constant(value) => {
354            let at = func.add_imm(Imm::int(*value, ty.lane()));
355            let data = InstData { extra: Extra::Imm(at), ..InstData::new(Opcode::IConst) };
356            written(func, before, data, ty, uses)
357        }
358        Plan::Nested(redo) => {
359            let lhs = build(func, before, redo.ty, &redo.lhs, uses);
360            let rhs = build(func, before, redo.ty, &redo.rhs, uses);
361            let args = func.push_values(&[lhs, rhs]);
362            uses[lhs.index()] += 1;
363            uses[rhs.index()] += 1;
364            let data = InstData { args, extra: redo.extra, ..InstData::new(redo.opcode) };
365            written(func, before, data, redo.ty, uses)
366        }
367    }
368}
369
370/// Puts an instruction in front of another one and gives back the value it produces.
371fn written(func: &mut Func, before: Inst, data: InstData, ty: Type, uses: &mut Vec<u32>) -> Value {
372    let span = func.span(before);
373    let inst = func.create_inst(data, &[ty], span);
374    func.insert_before(inst, before);
375    uses.resize(func.counts().values, 0);
376    func[inst].first_result.expect("one result was asked for")
377}
378
379#[cfg(test)]
380mod tests {
381    use rucc_base::Interner;
382    use rucc_ir::{Block, Builder, Flags, Func, Inst, IntPred, Opcode, Signature, Type, Value};
383
384    use crate::narrow::Narrow;
385    use crate::{Analyses, Fuel, Pass};
386
387    /// A function with one block, ready to have instructions appended to it.
388    fn blank() -> (Func, Block) {
389        let mut names = Interner::new();
390        let name = names.intern("f");
391        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
392        let block = func.create_block();
393        (func, block)
394    }
395
396    /// The opcode and the operand types of the instruction that produced a value.
397    fn shape(func: &Func, value: Value) -> (Opcode, Vec<Type>) {
398        let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("a result") };
399        let data = &func[inst];
400        (data.opcode, func[data.args].iter().map(|&arg| func[arg].ty).collect())
401    }
402
403    /// How many instructions are in a block.
404    fn left(func: &Func, block: Block) -> usize {
405        func.insts(block).count()
406    }
407
408    /// The last instruction of a block, which is the one every test here returns from.
409    fn last(func: &Func, block: Block) -> Inst {
410        func.insts(block).last().expect("a block with something in it")
411    }
412
413    #[test]
414    fn a_truncated_sum_of_two_extensions_is_the_sum_at_the_narrow_width() {
415        let (mut func, block) = blank();
416        let a = func.append_param(block, Type::int(8));
417        let b = func.append_param(block, Type::int(8));
418        let mut build = Builder::new(&mut func, block);
419        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
420        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
421        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
422        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
423        build.ret(&[narrow]);
424        assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
425        assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
426        // Nothing new was written. The two extensions and the wide add are still there, read by
427        // nothing, which is what dead code elimination takes out after this.
428        assert_eq!(left(&func, block), 5);
429    }
430
431    #[test]
432    fn a_constant_operand_is_written_down_again_at_the_narrow_width() {
433        let (mut func, block) = blank();
434        let a = func.append_param(block, Type::int(8));
435        let mut build = Builder::new(&mut func, block);
436        let wide = build.unary(Opcode::SExt, a, Type::int(32));
437        let one = build.iconst(Type::int(32), 1);
438        let sum = build.binary(Opcode::Add, wide, one, Flags::NONE);
439        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
440        build.ret(&[narrow]);
441        assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
442        assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
443    }
444
445    #[test]
446    fn a_chain_of_arithmetic_narrows_the_whole_way_down() {
447        let (mut func, block) = blank();
448        let a = func.append_param(block, Type::int(8));
449        let b = func.append_param(block, Type::int(8));
450        let c = func.append_param(block, Type::int(8));
451        let mut build = Builder::new(&mut func, block);
452        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
453        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
454        let wide_c = build.unary(Opcode::SExt, c, Type::int(32));
455        let inner = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
456        let outer = build.binary(Opcode::Mul, inner, wide_c, Flags::NONE);
457        let narrow = build.unary(Opcode::Trunc, outer, Type::int(8));
458        build.ret(&[narrow]);
459        assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
460        // The outer operation is the truncation rewritten, and the inner one is a new instruction
461        // written in front of it, which is the recursive case and the reason a plan is a tree.
462        assert_eq!(shape(&func, narrow), (Opcode::Mul, vec![Type::int(8), Type::int(8)]));
463        assert_eq!(left(&func, block), 8);
464    }
465
466    #[test]
467    fn an_operation_something_else_reads_stays_wide() {
468        let (mut func, block) = blank();
469        let a = func.append_param(block, Type::int(8));
470        let b = func.append_param(block, Type::int(8));
471        let mut build = Builder::new(&mut func, block);
472        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
473        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
474        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
475        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
476        let kept = build.unary(Opcode::SExt, narrow, Type::int(32));
477        build.ret(&[sum, kept]);
478        assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
479        // The wide sum is read by the return as well as by the truncation, so narrowing would add
480        // an instruction rather than replace one.
481        assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
482    }
483
484    #[test]
485    fn a_divide_stays_wide_because_the_narrow_one_can_raise() {
486        let (mut func, block) = blank();
487        let a = func.append_param(block, Type::int(8));
488        let b = func.append_param(block, Type::int(8));
489        let mut build = Builder::new(&mut func, block);
490        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
491        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
492        let quotient = build.binary(Opcode::SDiv, wide_a, wide_b, Flags::NONE);
493        let narrow = build.unary(Opcode::Trunc, quotient, Type::int(8));
494        build.ret(&[narrow]);
495        assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
496        // The most negative byte over minus one is a hundred and twenty eight at four bytes and
497        // is the overflow that raises at one, so this is the rewrite that would turn a working
498        // program into one that dies.
499        assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
500    }
501
502    #[test]
503    fn a_shift_by_a_constant_below_the_width_narrows_and_one_at_it_does_not() {
504        for (by, narrows) in [(3, true), (20, false)] {
505            let (mut func, block) = blank();
506            let a = func.append_param(block, Type::int(8));
507            let mut build = Builder::new(&mut func, block);
508            let wide = build.unary(Opcode::SExt, a, Type::int(32));
509            let count = build.iconst(Type::int(32), by);
510            let shifted = build.binary(Opcode::Shl, wide, count, Flags::NONE);
511            let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
512            build.ret(&[narrow]);
513            assert_eq!(
514                Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
515                narrows,
516                "shift by {by}"
517            );
518            // A count of twenty is a defined shift to zero at four bytes and is poison at one, so
519            // narrowing it would be inventing undefined behaviour rather than removing a widening.
520            let want = if narrows { Opcode::Shl } else { Opcode::Trunc };
521            assert_eq!(shape(&func, narrow).0, want, "shift by {by}");
522        }
523    }
524
525    #[test]
526    fn a_shift_by_a_value_stays_wide() {
527        let (mut func, block) = blank();
528        let a = func.append_param(block, Type::int(8));
529        let n = func.append_param(block, Type::int(8));
530        let mut build = Builder::new(&mut func, block);
531        let wide = build.unary(Opcode::SExt, a, Type::int(32));
532        let by = build.unary(Opcode::SExt, n, Type::int(32));
533        let shifted = build.binary(Opcode::Shl, wide, by, Flags::NONE);
534        let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
535        build.ret(&[narrow]);
536        assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
537        assert_eq!(shape(&func, narrow).0, Opcode::Trunc);
538    }
539
540    #[test]
541    fn a_comparison_of_two_sign_extensions_is_the_comparison_of_what_they_extended() {
542        for pred in IntPred::all() {
543            let (mut func, block) = blank();
544            let a = func.append_param(block, Type::int(8));
545            let b = func.append_param(block, Type::int(8));
546            let mut build = Builder::new(&mut func, block);
547            let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
548            let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
549            let answer = build.icmp(pred, wide_a, wide_b);
550            build.ret(&[answer]);
551            assert!(
552                Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
553                "{pred}"
554            );
555            // Every predicate, because sign extension keeps the order of what it extends under
556            // the signed reading and under the unsigned one.
557            assert_eq!(shape(&func, answer).1, vec![Type::int(8), Type::int(8)], "{pred}");
558        }
559    }
560
561    #[test]
562    fn a_comparison_of_two_zero_extensions_narrows_at_every_predicate_but_the_signed_ones() {
563        for pred in IntPred::all() {
564            let (mut func, block) = blank();
565            let a = func.append_param(block, Type::int(8));
566            let b = func.append_param(block, Type::int(8));
567            let mut build = Builder::new(&mut func, block);
568            let wide_a = build.unary(Opcode::ZExt, a, Type::int(32));
569            let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
570            let answer = build.icmp(pred, wide_a, wide_b);
571            build.ret(&[answer]);
572            // Zero extension takes a negative byte to a positive word, so the signed order is not
573            // the order it came from and the four signed predicates do not survive it.
574            assert_eq!(
575                Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
576                !pred.is_signed(),
577                "{pred}"
578            );
579        }
580    }
581
582    #[test]
583    fn a_comparison_against_a_constant_narrows_when_the_constant_is_one_of_the_narrow_ones() {
584        for (k, narrows) in [(120, true), (-1, true), (200, false)] {
585            let (mut func, block) = blank();
586            let a = func.append_param(block, Type::int(8));
587            let mut build = Builder::new(&mut func, block);
588            let wide = build.unary(Opcode::SExt, a, Type::int(32));
589            let k = build.iconst(Type::int(32), k);
590            let answer = build.icmp(IntPred::Eq, wide, k);
591            build.ret(&[answer]);
592            // Two hundred is not the sign extension of any byte, so the comparison is already
593            // decided and saying so is folding's job rather than this pass's.
594            assert_eq!(
595                Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
596                narrows
597            );
598        }
599    }
600
601    #[test]
602    fn one_extension_against_the_other_kind_is_not_a_comparison_at_the_narrow_width() {
603        // `(signed char) a < b` with `b` an `unsigned char`, which is `tamnd/rucc#375`'s one
604        // wrong answer over the torture suite: sixteen is less than a hundred and ninety five at
605        // four bytes and is not less than minus sixty one at one, and neither is the byte
606        // comparison the other reading would give.
607        for pred in IntPred::all() {
608            let (mut func, block) = blank();
609            let a = func.append_param(block, Type::int(8));
610            let b = func.append_param(block, Type::int(8));
611            let mut build = Builder::new(&mut func, block);
612            let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
613            let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
614            let answer = build.icmp(pred, wide_a, wide_b);
615            build.ret(&[answer]);
616            assert!(
617                !Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
618                "{pred}"
619            );
620        }
621    }
622
623    #[test]
624    fn a_truth_is_not_a_width_to_narrow_to() {
625        // `!c != 0`, which is a comparison of a widened truth against a zero that survives the
626        // widening, so the argument narrows it the whole way to one bit. The answer would be
627        // right and no target lowers a one bit comparison, which is `tamnd/rucc#352`.
628        let (mut func, block) = blank();
629        let a = func.append_param(block, Type::int(1));
630        let mut build = Builder::new(&mut func, block);
631        let wide = build.unary(Opcode::ZExt, a, Type::int(32));
632        let zero = build.iconst(Type::int(32), 0);
633        let answer = build.icmp(IntPred::Ne, wide, zero);
634        build.ret(&[answer]);
635        assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
636        assert_eq!(shape(&func, answer).1, vec![Type::int(32), Type::int(32)]);
637    }
638
639    #[test]
640    fn extensions_from_different_widths_are_not_a_comparison_at_either_of_them() {
641        let (mut func, block) = blank();
642        let a = func.append_param(block, Type::int(8));
643        let b = func.append_param(block, Type::int(16));
644        let mut build = Builder::new(&mut func, block);
645        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
646        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
647        let answer = build.icmp(IntPred::Slt, wide_a, wide_b);
648        build.ret(&[answer]);
649        assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
650    }
651
652    #[test]
653    fn the_overflow_flags_do_not_come_along() {
654        let (mut func, block) = blank();
655        let a = func.append_param(block, Type::int(8));
656        let b = func.append_param(block, Type::int(8));
657        let mut build = Builder::new(&mut func, block);
658        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
659        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
660        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NSW);
661        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
662        build.ret(&[narrow]);
663        assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
664        // A sum of two bytes that cannot overflow four bytes can overflow one, so a promise made
665        // about the wide operation is not a promise about the narrow one.
666        let rucc_ir::Def::Result { inst, .. } = func[narrow].def else { panic!("a result") };
667        assert_eq!(func[inst].flags, Flags::NONE);
668    }
669
670    #[test]
671    fn fuel_stops_the_narrowing_and_not_the_looking() {
672        let (mut func, block) = blank();
673        let a = func.append_param(block, Type::int(8));
674        let b = func.append_param(block, Type::int(8));
675        let mut build = Builder::new(&mut func, block);
676        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
677        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
678        let first = build.icmp(IntPred::Slt, wide_a, wide_b);
679        let second = build.icmp(IntPred::Sgt, wide_a, wide_b);
680        build.ret(&[first, second]);
681        let mut fuel = Fuel::of(1);
682        assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut fuel).changed());
683        assert_eq!(shape(&func, first).1, vec![Type::int(8), Type::int(8)]);
684        assert_eq!(shape(&func, second).1, vec![Type::int(32), Type::int(32)]);
685    }
686
687    #[test]
688    fn a_block_that_narrows_nothing_is_left_exactly_as_it_was() {
689        let (mut func, block) = blank();
690        let a = func.append_param(block, Type::int(32));
691        let mut build = Builder::new(&mut func, block);
692        let sum = build.binary(Opcode::Add, a, a, Flags::NONE);
693        build.ret(&[sum]);
694        assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
695        assert_eq!(left(&func, block), 2);
696        assert_eq!(func[last(&func, block)].opcode, Opcode::Return);
697    }
698}