1use 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
73const NARROWED: &str = "arithmetic redone at the width the program truncates it to";
75
76const NO_FUEL: &str = "arithmetic left wide, the pass ran out of fuel";
78
79const DEPTH: u32 = 6;
86
87#[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 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 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
131struct Redo {
133 opcode: Opcode,
135 extra: Extra,
137 ty: Type,
139 lhs: Plan,
141 rhs: Plan,
143}
144
145enum Plan {
147 Already(Value),
149 Constant(i128),
151 Nested(Box<Redo>),
153}
154
155fn 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
172const fn narrowable(ty: Type) -> bool {
182 ty.is_int() && ty.is_scalar() && ty.bits() >= 8
183}
184
185fn 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 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
207fn 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
218const 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
235fn 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
269fn 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
280fn extended(func: &Func, value: Value, ty: Type) -> Option<Value> {
285 let (_, from, narrow) = widening(func, value)?;
286 (from == ty).then_some(narrow)
287}
288
289fn 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
301fn 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
312fn 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
325fn 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 data.flags = Flags::NONE;
345 data.args = args;
346 data.extra = redo.extra;
347}
348
349fn 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
370fn 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 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 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 fn left(func: &Func, block: Block) -> usize {
405 func.insts(block).count()
406 }
407
408 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 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 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 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 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 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 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 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 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 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 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 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}