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::{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!(
425            Narrow
426                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
427                .changed()
428        );
429        assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
430        // Nothing new was written. The two extensions and the wide add are still there, read by
431        // nothing, which is what dead code elimination takes out after this.
432        assert_eq!(left(&func, block), 5);
433    }
434
435    #[test]
436    fn a_constant_operand_is_written_down_again_at_the_narrow_width() {
437        let (mut func, block) = blank();
438        let a = func.append_param(block, Type::int(8));
439        let mut build = Builder::new(&mut func, block);
440        let wide = build.unary(Opcode::SExt, a, Type::int(32));
441        let one = build.iconst(Type::int(32), 1);
442        let sum = build.binary(Opcode::Add, wide, one, Flags::NONE);
443        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
444        build.ret(&[narrow]);
445        assert!(
446            Narrow
447                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
448                .changed()
449        );
450        assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
451    }
452
453    #[test]
454    fn a_chain_of_arithmetic_narrows_the_whole_way_down() {
455        let (mut func, block) = blank();
456        let a = func.append_param(block, Type::int(8));
457        let b = func.append_param(block, Type::int(8));
458        let c = func.append_param(block, Type::int(8));
459        let mut build = Builder::new(&mut func, block);
460        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
461        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
462        let wide_c = build.unary(Opcode::SExt, c, Type::int(32));
463        let inner = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
464        let outer = build.binary(Opcode::Mul, inner, wide_c, Flags::NONE);
465        let narrow = build.unary(Opcode::Trunc, outer, Type::int(8));
466        build.ret(&[narrow]);
467        assert!(
468            Narrow
469                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
470                .changed()
471        );
472        // The outer operation is the truncation rewritten, and the inner one is a new instruction
473        // written in front of it, which is the recursive case and the reason a plan is a tree.
474        assert_eq!(shape(&func, narrow), (Opcode::Mul, vec![Type::int(8), Type::int(8)]));
475        assert_eq!(left(&func, block), 8);
476    }
477
478    #[test]
479    fn an_operation_something_else_reads_stays_wide() {
480        let (mut func, block) = blank();
481        let a = func.append_param(block, Type::int(8));
482        let b = func.append_param(block, Type::int(8));
483        let mut build = Builder::new(&mut func, block);
484        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
485        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
486        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
487        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
488        let kept = build.unary(Opcode::SExt, narrow, Type::int(32));
489        build.ret(&[sum, kept]);
490        assert!(
491            !Narrow
492                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
493                .changed()
494        );
495        // The wide sum is read by the return as well as by the truncation, so narrowing would add
496        // an instruction rather than replace one.
497        assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
498    }
499
500    #[test]
501    fn a_divide_stays_wide_because_the_narrow_one_can_raise() {
502        let (mut func, block) = blank();
503        let a = func.append_param(block, Type::int(8));
504        let b = func.append_param(block, Type::int(8));
505        let mut build = Builder::new(&mut func, block);
506        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
507        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
508        let quotient = build.binary(Opcode::SDiv, wide_a, wide_b, Flags::NONE);
509        let narrow = build.unary(Opcode::Trunc, quotient, Type::int(8));
510        build.ret(&[narrow]);
511        assert!(
512            !Narrow
513                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
514                .changed()
515        );
516        // The most negative byte over minus one is a hundred and twenty eight at four bytes and
517        // is the overflow that raises at one, so this is the rewrite that would turn a working
518        // program into one that dies.
519        assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
520    }
521
522    #[test]
523    fn a_shift_by_a_constant_below_the_width_narrows_and_one_at_it_does_not() {
524        for (by, narrows) in [(3, true), (20, false)] {
525            let (mut func, block) = blank();
526            let a = func.append_param(block, Type::int(8));
527            let mut build = Builder::new(&mut func, block);
528            let wide = build.unary(Opcode::SExt, a, Type::int(32));
529            let count = build.iconst(Type::int(32), by);
530            let shifted = build.binary(Opcode::Shl, wide, count, Flags::NONE);
531            let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
532            build.ret(&[narrow]);
533            assert_eq!(
534                Narrow
535                    .run(
536                        &mut func,
537                        &mut crate::machine::fixtures::analyses(),
538                        &mut Fuel::unlimited()
539                    )
540                    .changed(),
541                narrows,
542                "shift by {by}"
543            );
544            // A count of twenty is a defined shift to zero at four bytes and is poison at one, so
545            // narrowing it would be inventing undefined behaviour rather than removing a widening.
546            let want = if narrows { Opcode::Shl } else { Opcode::Trunc };
547            assert_eq!(shape(&func, narrow).0, want, "shift by {by}");
548        }
549    }
550
551    #[test]
552    fn a_shift_by_a_value_stays_wide() {
553        let (mut func, block) = blank();
554        let a = func.append_param(block, Type::int(8));
555        let n = func.append_param(block, Type::int(8));
556        let mut build = Builder::new(&mut func, block);
557        let wide = build.unary(Opcode::SExt, a, Type::int(32));
558        let by = build.unary(Opcode::SExt, n, Type::int(32));
559        let shifted = build.binary(Opcode::Shl, wide, by, Flags::NONE);
560        let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
561        build.ret(&[narrow]);
562        assert!(
563            !Narrow
564                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
565                .changed()
566        );
567        assert_eq!(shape(&func, narrow).0, Opcode::Trunc);
568    }
569
570    #[test]
571    fn a_comparison_of_two_sign_extensions_is_the_comparison_of_what_they_extended() {
572        for pred in IntPred::all() {
573            let (mut func, block) = blank();
574            let a = func.append_param(block, Type::int(8));
575            let b = func.append_param(block, Type::int(8));
576            let mut build = Builder::new(&mut func, block);
577            let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
578            let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
579            let answer = build.icmp(pred, wide_a, wide_b);
580            build.ret(&[answer]);
581            assert!(
582                Narrow
583                    .run(
584                        &mut func,
585                        &mut crate::machine::fixtures::analyses(),
586                        &mut Fuel::unlimited()
587                    )
588                    .changed(),
589                "{pred}"
590            );
591            // Every predicate, because sign extension keeps the order of what it extends under
592            // the signed reading and under the unsigned one.
593            assert_eq!(shape(&func, answer).1, vec![Type::int(8), Type::int(8)], "{pred}");
594        }
595    }
596
597    #[test]
598    fn a_comparison_of_two_zero_extensions_narrows_at_every_predicate_but_the_signed_ones() {
599        for pred in IntPred::all() {
600            let (mut func, block) = blank();
601            let a = func.append_param(block, Type::int(8));
602            let b = func.append_param(block, Type::int(8));
603            let mut build = Builder::new(&mut func, block);
604            let wide_a = build.unary(Opcode::ZExt, a, Type::int(32));
605            let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
606            let answer = build.icmp(pred, wide_a, wide_b);
607            build.ret(&[answer]);
608            // Zero extension takes a negative byte to a positive word, so the signed order is not
609            // the order it came from and the four signed predicates do not survive it.
610            assert_eq!(
611                Narrow
612                    .run(
613                        &mut func,
614                        &mut crate::machine::fixtures::analyses(),
615                        &mut Fuel::unlimited()
616                    )
617                    .changed(),
618                !pred.is_signed(),
619                "{pred}"
620            );
621        }
622    }
623
624    #[test]
625    fn a_comparison_against_a_constant_narrows_when_the_constant_is_one_of_the_narrow_ones() {
626        for (k, narrows) in [(120, true), (-1, true), (200, false)] {
627            let (mut func, block) = blank();
628            let a = func.append_param(block, Type::int(8));
629            let mut build = Builder::new(&mut func, block);
630            let wide = build.unary(Opcode::SExt, a, Type::int(32));
631            let k = build.iconst(Type::int(32), k);
632            let answer = build.icmp(IntPred::Eq, wide, k);
633            build.ret(&[answer]);
634            // Two hundred is not the sign extension of any byte, so the comparison is already
635            // decided and saying so is folding's job rather than this pass's.
636            assert_eq!(
637                Narrow
638                    .run(
639                        &mut func,
640                        &mut crate::machine::fixtures::analyses(),
641                        &mut Fuel::unlimited()
642                    )
643                    .changed(),
644                narrows
645            );
646        }
647    }
648
649    #[test]
650    fn one_extension_against_the_other_kind_is_not_a_comparison_at_the_narrow_width() {
651        // `(signed char) a < b` with `b` an `unsigned char`, which is `tamnd/rucc#375`'s one
652        // wrong answer over the torture suite: sixteen is less than a hundred and ninety five at
653        // four bytes and is not less than minus sixty one at one, and neither is the byte
654        // comparison the other reading would give.
655        for pred in IntPred::all() {
656            let (mut func, block) = blank();
657            let a = func.append_param(block, Type::int(8));
658            let b = func.append_param(block, Type::int(8));
659            let mut build = Builder::new(&mut func, block);
660            let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
661            let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
662            let answer = build.icmp(pred, wide_a, wide_b);
663            build.ret(&[answer]);
664            assert!(
665                !Narrow
666                    .run(
667                        &mut func,
668                        &mut crate::machine::fixtures::analyses(),
669                        &mut Fuel::unlimited()
670                    )
671                    .changed(),
672                "{pred}"
673            );
674        }
675    }
676
677    #[test]
678    fn a_truth_is_not_a_width_to_narrow_to() {
679        // `!c != 0`, which is a comparison of a widened truth against a zero that survives the
680        // widening, so the argument narrows it the whole way to one bit. The answer would be
681        // right and no target lowers a one bit comparison, which is `tamnd/rucc#352`.
682        let (mut func, block) = blank();
683        let a = func.append_param(block, Type::int(1));
684        let mut build = Builder::new(&mut func, block);
685        let wide = build.unary(Opcode::ZExt, a, Type::int(32));
686        let zero = build.iconst(Type::int(32), 0);
687        let answer = build.icmp(IntPred::Ne, wide, zero);
688        build.ret(&[answer]);
689        assert!(
690            !Narrow
691                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
692                .changed()
693        );
694        assert_eq!(shape(&func, answer).1, vec![Type::int(32), Type::int(32)]);
695    }
696
697    #[test]
698    fn extensions_from_different_widths_are_not_a_comparison_at_either_of_them() {
699        let (mut func, block) = blank();
700        let a = func.append_param(block, Type::int(8));
701        let b = func.append_param(block, Type::int(16));
702        let mut build = Builder::new(&mut func, block);
703        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
704        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
705        let answer = build.icmp(IntPred::Slt, wide_a, wide_b);
706        build.ret(&[answer]);
707        assert!(
708            !Narrow
709                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
710                .changed()
711        );
712    }
713
714    #[test]
715    fn the_overflow_flags_do_not_come_along() {
716        let (mut func, block) = blank();
717        let a = func.append_param(block, Type::int(8));
718        let b = func.append_param(block, Type::int(8));
719        let mut build = Builder::new(&mut func, block);
720        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
721        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
722        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NSW);
723        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
724        build.ret(&[narrow]);
725        assert!(
726            Narrow
727                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
728                .changed()
729        );
730        // A sum of two bytes that cannot overflow four bytes can overflow one, so a promise made
731        // about the wide operation is not a promise about the narrow one.
732        let rucc_ir::Def::Result { inst, .. } = func[narrow].def else { panic!("a result") };
733        assert_eq!(func[inst].flags, Flags::NONE);
734    }
735
736    #[test]
737    fn fuel_stops_the_narrowing_and_not_the_looking() {
738        let (mut func, block) = blank();
739        let a = func.append_param(block, Type::int(8));
740        let b = func.append_param(block, Type::int(8));
741        let mut build = Builder::new(&mut func, block);
742        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
743        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
744        let first = build.icmp(IntPred::Slt, wide_a, wide_b);
745        let second = build.icmp(IntPred::Sgt, wide_a, wide_b);
746        build.ret(&[first, second]);
747        let mut fuel = Fuel::of(1);
748        assert!(
749            Narrow.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel).changed()
750        );
751        assert_eq!(shape(&func, first).1, vec![Type::int(8), Type::int(8)]);
752        assert_eq!(shape(&func, second).1, vec![Type::int(32), Type::int(32)]);
753    }
754
755    #[test]
756    fn a_block_that_narrows_nothing_is_left_exactly_as_it_was() {
757        let (mut func, block) = blank();
758        let a = func.append_param(block, Type::int(32));
759        let mut build = Builder::new(&mut func, block);
760        let sum = build.binary(Opcode::Add, a, a, Flags::NONE);
761        build.ret(&[sum]);
762        assert!(
763            !Narrow
764                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
765                .changed()
766        );
767        assert_eq!(left(&func, block), 2);
768        assert_eq!(func[last(&func, block)].opcode, Opcode::Return);
769    }
770}