Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions compiler/rustc_mir_transform/src/match_branches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ struct SimplifyMatch<'tcx, 'a> {
discr: &'a Operand<'tcx>,
discr_local: Option<Local>,
discr_ty: Ty<'tcx>,
/// Extra statements to emit after the unified statements (e.g., range assumes).
extra_stmts: Vec<StatementKind<'tcx>>,
}

impl<'tcx, 'a> SimplifyMatch<'tcx, 'a> {
Expand Down Expand Up @@ -206,6 +208,48 @@ impl<'tcx, 'a> SimplifyMatch<'tcx, 'a> {
} else {
Rvalue::Cast(CastKind::IntToInt, operand, first_const.ty())
};

// Emit range assume so that subsequent passes (and LLVM) can
// eliminate bounds checks that depend on the cast result.
// We know the result is one of the constant values, so we can
// assert `dest <= max_value`.
let dest_ty = first_const.ty();
if !dest_ty.is_signed() {
let max_val = consts
.iter()
.filter_map(|(_, c)| {
c.const_.try_eval_scalar_int(self.tcx, self.typing_env).map(|s| {
s.to_uint(
self.tcx
.layout_of(self.typing_env.as_query_input(dest_ty))
.unwrap()
.size,
)
})
})
.max()
.unwrap();
let max_const = Operand::const_from_scalar(
self.tcx,
dest_ty,
rustc_const_eval::interpret::Scalar::from_uint(
max_val,
self.tcx.layout_of(self.typing_env.as_query_input(dest_ty)).unwrap().size,
),
rustc_span::DUMMY_SP,
);
let bool_local = self.patch.new_temp(
self.tcx.types.bool,
self.body.basic_blocks[self.switch_bb].terminator().source_info.span,
);
let cmp = Rvalue::BinaryOp(BinOp::Le, Box::new((Operand::Copy(dest), max_const)));
self.extra_stmts
.push(StatementKind::Assign(Box::new((Place::from(bool_local), cmp))));
self.extra_stmts.push(StatementKind::Intrinsic(Box::new(
NonDivergingIntrinsic::Assume(Operand::Move(Place::from(bool_local))),
)));
}

Some(StatementKind::Assign(Box::new((dest, rval))))
} else {
None
Expand Down Expand Up @@ -381,6 +425,7 @@ fn simplify_match<'tcx>(
discr,
discr_local: None,
discr_ty: discr.ty(body.local_decls(), tcx),
extra_stmts: Vec::new(),
};
let reachable_cases: Vec<_> =
targets.iter().filter(|&(_, bb)| !body.basic_blocks[bb].is_empty_unreachable()).collect();
Expand Down Expand Up @@ -432,6 +477,9 @@ fn simplify_match<'tcx>(
for new_stmt in new_stmts {
patch.add_statement(parent_end, new_stmt);
}
for extra_stmt in simplify_match.extra_stmts {
patch.add_statement(parent_end, extra_stmt);
}
if let Some(discr_local) = simplify_match.discr_local {
patch.add_statement(parent_end, StatementKind::StorageDead(discr_local));
}
Expand Down
53 changes: 53 additions & 0 deletions tests/codegen-llvm/exhaustive-match-bounds-check-issue-149480.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//@ compile-flags: -O
// Regression test for https://github.com/rust-lang/rust/issues/149480:
// the bounds check should be eliminated when indexing an array with
// the result of an exhaustive match over nested enums. The range
// assume emitted by MatchBranchSimplification after the IntToInt cast
// allows LLVM to prove the index is in-bounds.

#![crate_type = "lib"]

pub enum Foo {
A(A),
B(B),
}
pub enum A {
A0,
A1,
A2,
}
pub enum B {
B0,
B1,
}

// CHECK-LABEL: @bar
#[no_mangle]
pub fn bar(foo: Foo, arr: &[u8; 5]) -> u8 {
let offset: usize = match foo {
Foo::A(A::A0) => 0,
Foo::A(A::A1) => 1,
Foo::A(A::A2) => 2,
Foo::B(B::B0) => 3,
Foo::B(B::B1) => 4,
};
// The bounds check must be eliminated.
// CHECK-NOT: panic_bounds_check
Copy link
Copy Markdown
Member

@saethlin saethlin Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View changes since the review

It's good to have a negative check but without a positive test as well in here (checking for the IR sequence that should appear) this test could incorrectly pass if the function is renamed or if it's optimized to some other kind of panicking.

You should be able to draw inspiration from other codegen tests. Not having a positive check is a common flaw in the existing test suite, but it's not universal :)

// Positive check: the indexing must lower to a plain load from `arr`,
// so the test cannot pass accidentally if `bar` is optimized into
// another kind of panicking path or if `panic_bounds_check` is
// renamed.
// CHECK: load i8, ptr
// CHECK: ret i8
arr[offset]
}

// Sanity check: make sure `panic_bounds_check` is still the symbol LLVM
// emits for a non-elidable out-of-bounds index, so the `CHECK-NOT` above
// is guarding against something real.
// CHECK-LABEL: @test_check
#[no_mangle]
pub fn test_check(arr: &[u8], i: usize) -> u8 {
// CHECK: panic_bounds_check
arr[i]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let mut _0: u128;
let mut _2: i128;
+ let mut _3: i128;
+ let mut _4: bool;

bb0: {
_2 = discriminant(_1);
Expand Down Expand Up @@ -40,6 +41,8 @@
+ StorageLive(_3);
+ _3 = move _2;
+ _0 = copy _3 as u128 (IntToInt);
+ _4 = Le(copy _0, const u128::MAX);
+ assume(move _4);
+ StorageDead(_3);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let mut _0: u16;
let mut _2: i8;
+ let mut _3: i8;
+ let mut _4: bool;

bb0: {
_2 = discriminant(_1);
Expand Down Expand Up @@ -45,6 +46,8 @@
+ StorageLive(_3);
+ _3 = move _2;
+ _0 = copy _3 as u16 (IntToInt);
+ _4 = Le(copy _0, const u16::MAX);
+ assume(move _4);
+ StorageDead(_3);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let mut _0: u8;
let mut _2: i16;
+ let mut _3: i16;
+ let mut _4: bool;

bb0: {
_2 = discriminant(_1);
Expand Down Expand Up @@ -70,6 +71,8 @@
+ StorageLive(_3);
+ _3 = move _2;
+ _0 = copy _3 as u8 (IntToInt);
+ _4 = Le(copy _0, const u8::MAX);
+ assume(move _4);
+ StorageDead(_3);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let mut _0: u8;
let mut _2: u16;
+ let mut _3: u16;
+ let mut _4: bool;

bb0: {
_2 = discriminant(_1);
Expand Down Expand Up @@ -60,6 +61,8 @@
+ StorageLive(_3);
+ _3 = move _2;
+ _0 = copy _3 as u8 (IntToInt);
+ _4 = Le(copy _0, const u8::MAX);
+ assume(move _4);
+ StorageDead(_3);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let mut _0: u16;
let mut _2: u8;
+ let mut _3: u8;
+ let mut _4: bool;

bb0: {
_2 = discriminant(_1);
Expand Down Expand Up @@ -40,6 +41,8 @@
+ StorageLive(_3);
+ _3 = move _2;
+ _0 = copy _3 as u16 (IntToInt);
+ _4 = Le(copy _0, const 255_u16);
+ assume(move _4);
+ StorageDead(_3);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
let mut _0: u8;
let mut _2: isize;
+ let mut _3: isize;
+ let mut _4: bool;

bb0: {
_2 = discriminant(_1);
Expand All @@ -30,6 +31,8 @@
+ StorageLive(_3);
+ _3 = move _2;
+ _0 = copy _3 as u8 (IntToInt);
+ _4 = Le(copy _0, const 1_u8);
+ assume(move _4);
+ StorageDead(_3);
return;
}
Expand Down
Loading