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::{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!(
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 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 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 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 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 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 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 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 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 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 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 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}