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, Analysis, 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. Liveness is
103        // another matter: the narrow arithmetic is new values, and the wide values it was
104        // written from are read in one fewer place or in none.
105        Preserved::ALL.without(Analysis::Liveness)
106    }
107
108    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
109        let mut stats = Stats::new();
110        let mut uses = count(func);
111        for block in func.blocks().collect::<Vec<Block>>() {
112            for inst in func.insts(block).collect::<Vec<Inst>>() {
113                let Some(redo) = truncated_arithmetic(func, inst, &uses)
114                    .or_else(|| extended_comparison(func, inst))
115                else {
116                    continue;
117                };
118                if !fuel.take() {
119                    // Out of fuel, which stops the transforming rather than the looking, the
120                    // same way the other three passes treat it. The walk is the same walk at
121                    // every fuel setting, which is what makes bisecting over it monotonic.
122                    stats.missed(NO_FUEL);
123                    continue;
124                }
125                apply(func, inst, &redo, &mut uses);
126                stats.optimized(NARROWED);
127            }
128        }
129        stats
130    }
131}
132
133/// An instruction rewritten at the narrow width, with its operands narrowed too.
134struct Redo {
135    /// What the instruction becomes, which is the wide operation at the narrow width.
136    opcode: Opcode,
137    /// The predicate, for a comparison, and nothing for arithmetic.
138    extra: Extra,
139    /// The width everything under this is redone at.
140    ty: Type,
141    /// The left operand.
142    lhs: Plan,
143    /// The right operand.
144    rhs: Plan,
145}
146
147/// What an operand becomes at the narrow width.
148enum Plan {
149    /// A value that already has it, which is what an extension was extending.
150    Already(Value),
151    /// A constant, written down again at the narrow width.
152    Constant(i128),
153    /// An operation redone, which is the recursive case and the reason this is a tree.
154    Nested(Box<Redo>),
155}
156
157/// Whether this is a truncation of arithmetic that can be redone narrow, and what it becomes.
158///
159/// The truncation is the root because it is the only place the narrow width is written down. Its
160/// operand has to be read by nothing else, since a second reader would keep the wide operation
161/// alive and the rewrite would be a second instruction rather than a replacement.
162fn truncated_arithmetic(func: &Func, inst: Inst, uses: &[u32]) -> Option<Redo> {
163    let data = &func[inst];
164    if data.opcode != Opcode::Trunc {
165        return None;
166    }
167    let ty = func[data.results().next()?].ty;
168    if !narrowable(ty) {
169        return None;
170    }
171    redo(func, *func[data.args].first()?, ty, uses, DEPTH)
172}
173
174/// Whether a width is one this pass will redo an operation at.
175///
176/// An integer scalar of a byte or more. The lower bound is the interesting half. One bit is an
177/// integer type in the IR and a comparison against a zero extended truth is a comparison the
178/// argument narrows all the way down to it, and `spec/12-instruction-selection.md` says a one bit
179/// value is a truth rather than a width: `tamnd/rucc#352` is the list of what a target lowers at
180/// that width and it is `and`, `or`, `xor`, a constant and the widening out of one. Narrowing an
181/// `icmp` into it would be asking every target for something no target has, so the floor is the
182/// narrowest width a machine holds a number in.
183const fn narrowable(ty: Type) -> bool {
184    ty.is_int() && ty.is_scalar() && ty.bits() >= 8
185}
186
187/// Whether this value is arithmetic that can be redone at that width, and what it becomes.
188fn redo(func: &Func, value: Value, ty: Type, uses: &[u32], depth: u32) -> Option<Redo> {
189    if depth == 0 || uses[value.index()] != 1 {
190        return None;
191    }
192    let Def::Result { inst, .. } = func[value].def else { return None };
193    let data = &func[inst];
194    if !low_bits_only(data.opcode) {
195        return None;
196    }
197    let args = &func[data.args];
198    let (&left, &right) = (args.first()?, args.get(1)?);
199    let lhs = plan(func, left, ty, uses, depth)?;
200    // A shift is the one operation whose right operand is not a number of the same kind as its
201    // left one, and it is the one that is unsafe to narrow when that operand is not a constant.
202    let rhs = match data.opcode {
203        Opcode::Shl => Plan::Constant(count_below(func, right, ty)?),
204        _ => plan(func, right, ty, uses, depth)?,
205    };
206    Some(Redo { opcode: data.opcode, extra: Extra::None, ty, lhs, rhs })
207}
208
209/// What an operand becomes at that width, or `None` when it would cost something to get there.
210fn plan(func: &Func, value: Value, ty: Type, uses: &[u32], depth: u32) -> Option<Plan> {
211    if let Some(narrow) = extended(func, value, ty) {
212        return Some(Plan::Already(narrow));
213    }
214    if let Some((imm, wide)) = constant(func, value) {
215        return Some(Plan::Constant(imm.signed(wide)));
216    }
217    redo(func, value, ty, uses, depth - 1).map(|redo| Plan::Nested(Box::new(redo)))
218}
219
220/// Whether an operation's low bits depend only on the low bits of what went into it.
221///
222/// True of the four that carry left to right and of the three that work a bit at a time. Not true
223/// of a divide, a remainder or a shift right, all of which read bits above the ones they produce.
224const fn low_bits_only(opcode: Opcode) -> bool {
225    matches!(
226        opcode,
227        Opcode::Add
228            | Opcode::Sub
229            | Opcode::Mul
230            | Opcode::And
231            | Opcode::Or
232            | Opcode::Xor
233            | Opcode::Shl
234    )
235}
236
237/// Whether this is a comparison of two things extended from the same narrower width.
238///
239/// Sign extension keeps the order of what it extends under both readings of the bits, so every
240/// predicate survives it. Zero extension keeps the unsigned order and not the signed one, since it
241/// takes a negative byte to a positive word, so it carries the equalities and the unsigned
242/// predicates and refuses the signed ones.
243///
244/// The two sides have to be the same extension as well as from the same width. `(signed char) a <
245/// b` where `b` is an `unsigned char` is a sign extension against a zero extension, and comparing
246/// what they extended is comparing a byte against a byte at one predicate where the wide
247/// comparison had a signed byte against an unsigned one. Both readings of the narrow comparison
248/// are wrong, and the wide comparison is right, which is the whole reason C promotes.
249fn extended_comparison(func: &Func, inst: Inst) -> Option<Redo> {
250    let data = &func[inst];
251    if data.opcode != Opcode::ICmp {
252        return None;
253    }
254    let Extra::IntPred(pred) = data.extra else { return None };
255    let args = &func[data.args];
256    let (&left, &right) = (args.first()?, args.get(1)?);
257    let (kind, ty, narrow) = widening(func, left)?;
258    if !narrowable(ty) {
259        return None;
260    }
261    if kind == Opcode::ZExt && pred.is_signed() {
262        return None;
263    }
264    let rhs = match widening(func, right) {
265        Some((same, from, other)) if same == kind && from == ty => Plan::Already(other),
266        _ => Plan::Constant(survives(func, right, kind, ty)?),
267    };
268    Some(Redo { opcode: Opcode::ICmp, extra: data.extra, ty, lhs: Plan::Already(narrow), rhs })
269}
270
271/// The extension this value is, as the kind, the width it came from and the value it extended.
272fn widening(func: &Func, value: Value) -> Option<(Opcode, Type, Value)> {
273    let Def::Result { inst, .. } = func[value].def else { return None };
274    let data = &func[inst];
275    if data.opcode != Opcode::SExt && data.opcode != Opcode::ZExt {
276        return None;
277    }
278    let narrow = *func[data.args].first()?;
279    Some((data.opcode, func[narrow].ty, narrow))
280}
281
282/// What this value was before it was extended to that width, when that is what it is.
283///
284/// Which extension it was is not asked, because this is the arithmetic side and the arithmetic
285/// reads the low bits only. Those are the bits the extension copied, whichever one it was.
286fn extended(func: &Func, value: Value, ty: Type) -> Option<Value> {
287    let (_, from, narrow) = widening(func, value)?;
288    (from == ty).then_some(narrow)
289}
290
291/// The constant this value is, with the type it has.
292fn constant(func: &Func, value: Value) -> Option<(Imm, Type)> {
293    let Def::Result { inst, .. } = func[value].def else { return None };
294    let data = &func[inst];
295    let Extra::Imm(at) = data.extra else { return None };
296    if data.opcode != Opcode::IConst {
297        return None;
298    }
299    let ty = func[value].ty;
300    ty.is_int().then(|| (func[at], ty))
301}
302
303/// A shift count that is a constant below the narrow width, which is the only one that narrows.
304///
305/// A count at or above the width is poison at the narrow width and is a defined shift to zero at
306/// the wide one, so the guard is what keeps the rewrite from inventing undefined behaviour. A
307/// count that is not a constant cannot be guarded, since its value is what decides.
308fn count_below(func: &Func, value: Value, ty: Type) -> Option<i128> {
309    let (imm, wide) = constant(func, value)?;
310    let by = imm.signed(wide);
311    (by >= 0 && by < i128::from(ty.bits())).then_some(by)
312}
313
314/// A constant that is the extension of a constant at the narrow width, as that narrow constant.
315///
316/// Both extensions are injective, so a comparison against a constant in the image of one is the
317/// same comparison against what it is the image of. A constant outside the image is a comparison
318/// that is already decided, which is a thing for folding to say rather than for this to guess at.
319fn survives(func: &Func, value: Value, kind: Opcode, ty: Type) -> Option<i128> {
320    let (imm, wide) = constant(func, value)?;
321    let k = imm.signed(wide);
322    let back = Imm::int(k, ty).signed(ty);
323    let same = if kind == Opcode::SExt { back } else { Imm::int(k, ty).unsigned() as i128 };
324    (same == k).then_some(k)
325}
326
327/// Rewrites the instruction into what the plan says it is.
328///
329/// In place, because the result already has the narrow type and every use of it is already
330/// correct, which is the same reason folding and the peephole rewrite in place. What is left
331/// behind is the wide subtree, now read by nothing, which is what dead code elimination is for.
332fn apply(func: &mut Func, inst: Inst, redo: &Redo, uses: &mut Vec<u32>) {
333    let lhs = build(func, inst, redo.ty, &redo.lhs, uses);
334    let rhs = build(func, inst, redo.ty, &redo.rhs, uses);
335    for value in func[func[inst].args].iter().copied() {
336        uses[value.index()] -= 1;
337    }
338    let args = func.push_values(&[lhs, rhs]);
339    uses[lhs.index()] += 1;
340    uses[rhs.index()] += 1;
341    let data = &mut func[inst];
342    data.opcode = redo.opcode;
343    // No flags. An operation that could not overflow at the wide width can overflow at the narrow
344    // one, so `nsw` and `nuw` do not survive the narrowing, and dropping them makes the operation
345    // more defined rather than less.
346    data.flags = Flags::NONE;
347    data.args = args;
348    data.extra = redo.extra;
349}
350
351/// The value an operand's plan comes to, writing whatever it needs in front of the instruction.
352fn build(func: &mut Func, before: Inst, ty: Type, plan: &Plan, uses: &mut Vec<u32>) -> Value {
353    match plan {
354        Plan::Already(value) => *value,
355        Plan::Constant(value) => {
356            let at = func.add_imm(Imm::int(*value, ty.lane()));
357            let data = InstData { extra: Extra::Imm(at), ..InstData::new(Opcode::IConst) };
358            written(func, before, data, ty, uses)
359        }
360        Plan::Nested(redo) => {
361            let lhs = build(func, before, redo.ty, &redo.lhs, uses);
362            let rhs = build(func, before, redo.ty, &redo.rhs, uses);
363            let args = func.push_values(&[lhs, rhs]);
364            uses[lhs.index()] += 1;
365            uses[rhs.index()] += 1;
366            let data = InstData { args, extra: redo.extra, ..InstData::new(redo.opcode) };
367            written(func, before, data, redo.ty, uses)
368        }
369    }
370}
371
372/// Puts an instruction in front of another one and gives back the value it produces.
373fn written(func: &mut Func, before: Inst, data: InstData, ty: Type, uses: &mut Vec<u32>) -> Value {
374    let span = func.span(before);
375    let inst = func.create_inst(data, &[ty], span);
376    func.insert_before(inst, before);
377    uses.resize(func.counts().values, 0);
378    func[inst].first_result.expect("one result was asked for")
379}
380
381#[cfg(test)]
382mod tests {
383    use rucc_base::Interner;
384    use rucc_ir::{Block, Builder, Flags, Func, Inst, IntPred, Opcode, Signature, Type, Value};
385
386    use crate::narrow::Narrow;
387    use crate::{Fuel, Pass};
388
389    /// A function with one block, ready to have instructions appended to it.
390    fn blank() -> (Func, Block) {
391        let mut names = Interner::new();
392        let name = names.intern("f");
393        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
394        let block = func.create_block();
395        (func, block)
396    }
397
398    /// The opcode and the operand types of the instruction that produced a value.
399    fn shape(func: &Func, value: Value) -> (Opcode, Vec<Type>) {
400        let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("a result") };
401        let data = &func[inst];
402        (data.opcode, func[data.args].iter().map(|&arg| func[arg].ty).collect())
403    }
404
405    /// How many instructions are in a block.
406    fn left(func: &Func, block: Block) -> usize {
407        func.insts(block).count()
408    }
409
410    /// The last instruction of a block, which is the one every test here returns from.
411    fn last(func: &Func, block: Block) -> Inst {
412        func.insts(block).last().expect("a block with something in it")
413    }
414
415    #[test]
416    fn a_truncated_sum_of_two_extensions_is_the_sum_at_the_narrow_width() {
417        let (mut func, block) = blank();
418        let a = func.append_param(block, Type::int(8));
419        let b = func.append_param(block, Type::int(8));
420        let mut build = Builder::new(&mut func, block);
421        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
422        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
423        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
424        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
425        build.ret(&[narrow]);
426        assert!(
427            Narrow
428                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
429                .changed()
430        );
431        assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
432        // Nothing new was written. The two extensions and the wide add are still there, read by
433        // nothing, which is what dead code elimination takes out after this.
434        assert_eq!(left(&func, block), 5);
435    }
436
437    #[test]
438    fn a_constant_operand_is_written_down_again_at_the_narrow_width() {
439        let (mut func, block) = blank();
440        let a = func.append_param(block, Type::int(8));
441        let mut build = Builder::new(&mut func, block);
442        let wide = build.unary(Opcode::SExt, a, Type::int(32));
443        let one = build.iconst(Type::int(32), 1);
444        let sum = build.binary(Opcode::Add, wide, one, Flags::NONE);
445        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
446        build.ret(&[narrow]);
447        assert!(
448            Narrow
449                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
450                .changed()
451        );
452        assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
453    }
454
455    #[test]
456    fn a_chain_of_arithmetic_narrows_the_whole_way_down() {
457        let (mut func, block) = blank();
458        let a = func.append_param(block, Type::int(8));
459        let b = func.append_param(block, Type::int(8));
460        let c = func.append_param(block, Type::int(8));
461        let mut build = Builder::new(&mut func, block);
462        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
463        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
464        let wide_c = build.unary(Opcode::SExt, c, Type::int(32));
465        let inner = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
466        let outer = build.binary(Opcode::Mul, inner, wide_c, Flags::NONE);
467        let narrow = build.unary(Opcode::Trunc, outer, Type::int(8));
468        build.ret(&[narrow]);
469        assert!(
470            Narrow
471                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
472                .changed()
473        );
474        // The outer operation is the truncation rewritten, and the inner one is a new instruction
475        // written in front of it, which is the recursive case and the reason a plan is a tree.
476        assert_eq!(shape(&func, narrow), (Opcode::Mul, vec![Type::int(8), Type::int(8)]));
477        assert_eq!(left(&func, block), 8);
478    }
479
480    #[test]
481    fn an_operation_something_else_reads_stays_wide() {
482        let (mut func, block) = blank();
483        let a = func.append_param(block, Type::int(8));
484        let b = func.append_param(block, Type::int(8));
485        let mut build = Builder::new(&mut func, block);
486        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
487        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
488        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
489        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
490        let kept = build.unary(Opcode::SExt, narrow, Type::int(32));
491        build.ret(&[sum, kept]);
492        assert!(
493            !Narrow
494                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
495                .changed()
496        );
497        // The wide sum is read by the return as well as by the truncation, so narrowing would add
498        // an instruction rather than replace one.
499        assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
500    }
501
502    #[test]
503    fn a_divide_stays_wide_because_the_narrow_one_can_raise() {
504        let (mut func, block) = blank();
505        let a = func.append_param(block, Type::int(8));
506        let b = func.append_param(block, Type::int(8));
507        let mut build = Builder::new(&mut func, block);
508        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
509        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
510        let quotient = build.binary(Opcode::SDiv, wide_a, wide_b, Flags::NONE);
511        let narrow = build.unary(Opcode::Trunc, quotient, Type::int(8));
512        build.ret(&[narrow]);
513        assert!(
514            !Narrow
515                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
516                .changed()
517        );
518        // The most negative byte over minus one is a hundred and twenty eight at four bytes and
519        // is the overflow that raises at one, so this is the rewrite that would turn a working
520        // program into one that dies.
521        assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
522    }
523
524    #[test]
525    fn a_shift_by_a_constant_below_the_width_narrows_and_one_at_it_does_not() {
526        for (by, narrows) in [(3, true), (20, false)] {
527            let (mut func, block) = blank();
528            let a = func.append_param(block, Type::int(8));
529            let mut build = Builder::new(&mut func, block);
530            let wide = build.unary(Opcode::SExt, a, Type::int(32));
531            let count = build.iconst(Type::int(32), by);
532            let shifted = build.binary(Opcode::Shl, wide, count, Flags::NONE);
533            let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
534            build.ret(&[narrow]);
535            assert_eq!(
536                Narrow
537                    .run(
538                        &mut func,
539                        &mut crate::machine::fixtures::analyses(),
540                        &mut Fuel::unlimited()
541                    )
542                    .changed(),
543                narrows,
544                "shift by {by}"
545            );
546            // A count of twenty is a defined shift to zero at four bytes and is poison at one, so
547            // narrowing it would be inventing undefined behaviour rather than removing a widening.
548            let want = if narrows { Opcode::Shl } else { Opcode::Trunc };
549            assert_eq!(shape(&func, narrow).0, want, "shift by {by}");
550        }
551    }
552
553    #[test]
554    fn a_shift_by_a_value_stays_wide() {
555        let (mut func, block) = blank();
556        let a = func.append_param(block, Type::int(8));
557        let n = func.append_param(block, Type::int(8));
558        let mut build = Builder::new(&mut func, block);
559        let wide = build.unary(Opcode::SExt, a, Type::int(32));
560        let by = build.unary(Opcode::SExt, n, Type::int(32));
561        let shifted = build.binary(Opcode::Shl, wide, by, Flags::NONE);
562        let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
563        build.ret(&[narrow]);
564        assert!(
565            !Narrow
566                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
567                .changed()
568        );
569        assert_eq!(shape(&func, narrow).0, Opcode::Trunc);
570    }
571
572    #[test]
573    fn a_comparison_of_two_sign_extensions_is_the_comparison_of_what_they_extended() {
574        for pred in IntPred::all() {
575            let (mut func, block) = blank();
576            let a = func.append_param(block, Type::int(8));
577            let b = func.append_param(block, Type::int(8));
578            let mut build = Builder::new(&mut func, block);
579            let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
580            let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
581            let answer = build.icmp(pred, wide_a, wide_b);
582            build.ret(&[answer]);
583            assert!(
584                Narrow
585                    .run(
586                        &mut func,
587                        &mut crate::machine::fixtures::analyses(),
588                        &mut Fuel::unlimited()
589                    )
590                    .changed(),
591                "{pred}"
592            );
593            // Every predicate, because sign extension keeps the order of what it extends under
594            // the signed reading and under the unsigned one.
595            assert_eq!(shape(&func, answer).1, vec![Type::int(8), Type::int(8)], "{pred}");
596        }
597    }
598
599    #[test]
600    fn a_comparison_of_two_zero_extensions_narrows_at_every_predicate_but_the_signed_ones() {
601        for pred in IntPred::all() {
602            let (mut func, block) = blank();
603            let a = func.append_param(block, Type::int(8));
604            let b = func.append_param(block, Type::int(8));
605            let mut build = Builder::new(&mut func, block);
606            let wide_a = build.unary(Opcode::ZExt, a, Type::int(32));
607            let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
608            let answer = build.icmp(pred, wide_a, wide_b);
609            build.ret(&[answer]);
610            // Zero extension takes a negative byte to a positive word, so the signed order is not
611            // the order it came from and the four signed predicates do not survive it.
612            assert_eq!(
613                Narrow
614                    .run(
615                        &mut func,
616                        &mut crate::machine::fixtures::analyses(),
617                        &mut Fuel::unlimited()
618                    )
619                    .changed(),
620                !pred.is_signed(),
621                "{pred}"
622            );
623        }
624    }
625
626    #[test]
627    fn a_comparison_against_a_constant_narrows_when_the_constant_is_one_of_the_narrow_ones() {
628        for (k, narrows) in [(120, true), (-1, true), (200, false)] {
629            let (mut func, block) = blank();
630            let a = func.append_param(block, Type::int(8));
631            let mut build = Builder::new(&mut func, block);
632            let wide = build.unary(Opcode::SExt, a, Type::int(32));
633            let k = build.iconst(Type::int(32), k);
634            let answer = build.icmp(IntPred::Eq, wide, k);
635            build.ret(&[answer]);
636            // Two hundred is not the sign extension of any byte, so the comparison is already
637            // decided and saying so is folding's job rather than this pass's.
638            assert_eq!(
639                Narrow
640                    .run(
641                        &mut func,
642                        &mut crate::machine::fixtures::analyses(),
643                        &mut Fuel::unlimited()
644                    )
645                    .changed(),
646                narrows
647            );
648        }
649    }
650
651    #[test]
652    fn one_extension_against_the_other_kind_is_not_a_comparison_at_the_narrow_width() {
653        // `(signed char) a < b` with `b` an `unsigned char`, which is `tamnd/rucc#375`'s one
654        // wrong answer over the torture suite: sixteen is less than a hundred and ninety five at
655        // four bytes and is not less than minus sixty one at one, and neither is the byte
656        // comparison the other reading would give.
657        for pred in IntPred::all() {
658            let (mut func, block) = blank();
659            let a = func.append_param(block, Type::int(8));
660            let b = func.append_param(block, Type::int(8));
661            let mut build = Builder::new(&mut func, block);
662            let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
663            let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
664            let answer = build.icmp(pred, wide_a, wide_b);
665            build.ret(&[answer]);
666            assert!(
667                !Narrow
668                    .run(
669                        &mut func,
670                        &mut crate::machine::fixtures::analyses(),
671                        &mut Fuel::unlimited()
672                    )
673                    .changed(),
674                "{pred}"
675            );
676        }
677    }
678
679    #[test]
680    fn a_truth_is_not_a_width_to_narrow_to() {
681        // `!c != 0`, which is a comparison of a widened truth against a zero that survives the
682        // widening, so the argument narrows it the whole way to one bit. The answer would be
683        // right and no target lowers a one bit comparison, which is `tamnd/rucc#352`.
684        let (mut func, block) = blank();
685        let a = func.append_param(block, Type::int(1));
686        let mut build = Builder::new(&mut func, block);
687        let wide = build.unary(Opcode::ZExt, a, Type::int(32));
688        let zero = build.iconst(Type::int(32), 0);
689        let answer = build.icmp(IntPred::Ne, wide, zero);
690        build.ret(&[answer]);
691        assert!(
692            !Narrow
693                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
694                .changed()
695        );
696        assert_eq!(shape(&func, answer).1, vec![Type::int(32), Type::int(32)]);
697    }
698
699    #[test]
700    fn extensions_from_different_widths_are_not_a_comparison_at_either_of_them() {
701        let (mut func, block) = blank();
702        let a = func.append_param(block, Type::int(8));
703        let b = func.append_param(block, Type::int(16));
704        let mut build = Builder::new(&mut func, block);
705        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
706        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
707        let answer = build.icmp(IntPred::Slt, wide_a, wide_b);
708        build.ret(&[answer]);
709        assert!(
710            !Narrow
711                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
712                .changed()
713        );
714    }
715
716    #[test]
717    fn the_overflow_flags_do_not_come_along() {
718        let (mut func, block) = blank();
719        let a = func.append_param(block, Type::int(8));
720        let b = func.append_param(block, Type::int(8));
721        let mut build = Builder::new(&mut func, block);
722        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
723        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
724        let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NSW);
725        let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
726        build.ret(&[narrow]);
727        assert!(
728            Narrow
729                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
730                .changed()
731        );
732        // A sum of two bytes that cannot overflow four bytes can overflow one, so a promise made
733        // about the wide operation is not a promise about the narrow one.
734        let rucc_ir::Def::Result { inst, .. } = func[narrow].def else { panic!("a result") };
735        assert_eq!(func[inst].flags, Flags::NONE);
736    }
737
738    #[test]
739    fn fuel_stops_the_narrowing_and_not_the_looking() {
740        let (mut func, block) = blank();
741        let a = func.append_param(block, Type::int(8));
742        let b = func.append_param(block, Type::int(8));
743        let mut build = Builder::new(&mut func, block);
744        let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
745        let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
746        let first = build.icmp(IntPred::Slt, wide_a, wide_b);
747        let second = build.icmp(IntPred::Sgt, wide_a, wide_b);
748        build.ret(&[first, second]);
749        let mut fuel = Fuel::of(1);
750        assert!(
751            Narrow.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel).changed()
752        );
753        assert_eq!(shape(&func, first).1, vec![Type::int(8), Type::int(8)]);
754        assert_eq!(shape(&func, second).1, vec![Type::int(32), Type::int(32)]);
755    }
756
757    #[test]
758    fn a_block_that_narrows_nothing_is_left_exactly_as_it_was() {
759        let (mut func, block) = blank();
760        let a = func.append_param(block, Type::int(32));
761        let mut build = Builder::new(&mut func, block);
762        let sum = build.binary(Opcode::Add, a, a, Flags::NONE);
763        build.ret(&[sum]);
764        assert!(
765            !Narrow
766                .run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
767                .changed()
768        );
769        assert_eq!(left(&func, block), 2);
770        assert_eq!(func[last(&func, block)].opcode, Opcode::Return);
771    }
772}