Godbolt link: https://godbolt.org/z/G4hxdn4Wa
I tried this code (simplified to a ~minimal version):
struct Store {
ring: [Kind; 32],
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum Kind {
A,
B,
C
}
struct Ring(u32);
impl Ring {
fn peek(&self, store: &Store) -> Kind {
store.ring[(self.0 as usize) % 32]
}
pub fn peek_test(&self, store: &Store, expected: Kind) -> bool {
self.peek(store) == expected
}
#[inline]
pub fn consume_test(&mut self, store: &Store, expected: Kind) -> bool {
let x = self.peek_test(store, expected);
self.0 += if x { 1 } else { 0 };
x
}
// Uses a u32 cast instead of the
#[inline]
pub fn consume_test_borked(&mut self, store: &Store, expected: Kind) -> bool {
let x = self.peek_test(store, expected);
self.0 += x as u32;
x
}
}
#[unsafe(no_mangle)]
fn foo(store: &Store, mut lex: Ring) -> usize {
if !lex.consume_test(store, Kind::A) {
return 0;
}
match lex.peek(store) {
Kind::A => 17,
_ => 27,
}
}
#[unsafe(no_mangle)]
fn foo_borked(store: &Store, mut lex: Ring) -> usize {
if !lex.consume_test_borked(store, Kind::A) {
return 0;
}
match lex.peek(store) {
Kind::A => 17,
_ => 27,
}
}
I expected foo and foo_borked to have identical behavior/codegen.
Instead, this happened: foo_borked compiled into a function that always takes the Kind::A branch, never returning 27.
; rustc -Copt-level=3 [also =2]
foo:
mov eax, esi
and eax, 31
cmp byte ptr [rdi + rax], 0
je .LBB0_2
xor eax, eax
ret
.LBB0_2:
inc esi
and esi, 31
cmp byte ptr [rdi + rsi], 0
mov ecx, 17
mov eax, 27
cmove rax, rcx
ret
foo_borked:
and esi, 31
xor ecx, ecx
cmp byte ptr [rdi + rsi], 0
mov eax, 17
cmovne rax, rcx
ret
Looking at the LLVM IR with -Cno-prepopulate-passes it appears that this is a rustc bug, and given that -Zmir-opt-level=0 produces valid code (foo_borked = foo) this is likely a Mir opt issue.
define noundef i64 @foo_borked(ptr noalias nofree noundef readonly captures(address, read_provenance) dereferenceable(32) %store, i32 noundef %0) unnamed_addr {
start:
%__self_discr.dbg.spill = alloca [8 x i8], align 8
%__arg1_discr.dbg.spill = alloca [8 x i8], align 8
%expected.dbg.spill1 = alloca [1 x i8], align 1
%expected.dbg.spill = alloca [1 x i8], align 1
%store.dbg.spill = alloca [8 x i8], align 8
%x = alloca [1 x i8], align 1
%_0 = alloca [8 x i8], align 8
%lex = alloca [4 x i8], align 4
store i32 %0, ptr %lex, align 4
store ptr %store, ptr %store.dbg.spill, align 8
store i8 0, ptr %expected.dbg.spill, align 1
store i8 0, ptr %expected.dbg.spill1, align 1
store i64 0, ptr %__arg1_discr.dbg.spill, align 8
call void @llvm.lifetime.start.p0(ptr %x)
%_14 = load i32, ptr %lex, align 4
%_13 = zext i32 %_14 to i64
%_12 = urem i64 %_13, 32
%_15 = icmp ult i64 %_12, 32
br i1 %_15, label %bb7, label %panic
bb7:
%1 = getelementptr inbounds nuw i8, ptr %store, i64 %_12
%_10 = load i8, ptr %1, align 1
%__self_discr = zext i8 %_10 to i64
store i64 %__self_discr, ptr %__self_discr.dbg.spill, align 8
; %x is being loaded here without a prior store, becoming a poison
%2 = load i8, ptr %x, align 1
%3 = trunc nuw i8 %2 to i1
%4 = icmp ule i1 %3, true
call void @llvm.assume(i1 %4)
%_8 = zext i1 %3 to i32
%5 = load i32, ptr %lex, align 4
%6 = add i32 %5, %_8
store i32 %6, ptr %lex, align 4
%7 = icmp eq i64 %__self_discr, 0
br i1 %7, label %bb1, label %bb2
panic:
call void @core[dcb47395e1cc1d61]::panicking::panic_bounds_check(i64 noundef %_12, i64 noundef 32, ptr noalias nofree noundef readonly align 8 captures(address, read_provenance) dereferenceable(24) @alloc_7414d1c37f862b99e65b1576388dc6f7) #4
unreachable
bb1:
call void @llvm.lifetime.end.p0(ptr %x)
%_19 = load i32, ptr %lex, align 4
%_18 = zext i32 %_19 to i64
%_17 = urem i64 %_18, 32
%_20 = icmp ult i64 %_17, 32
br i1 %_20, label %bb8, label %panic2
bb2:
store i64 0, ptr %_0, align 8
call void @llvm.lifetime.end.p0(ptr %x)
br label %bb6
bb8:
%8 = getelementptr inbounds nuw i8, ptr %store, i64 %_17
%_5 = load i8, ptr %8, align 1
%_7 = zext i8 %_5 to i64
%9 = icmp eq i64 %_7, 0
br i1 %9, label %bb4, label %bb3
panic2:
call void @core[dcb47395e1cc1d61]::panicking::panic_bounds_check(i64 noundef %_17, i64 noundef 32, ptr noalias nofree noundef readonly align 8 captures(address, read_provenance) dereferenceable(24) @alloc_7414d1c37f862b99e65b1576388dc6f7) #4
unreachable
bb4:
store i64 17, ptr %_0, align 8
br label %bb5
bb3:
store i64 27, ptr %_0, align 8
br label %bb5
bb5:
br label %bb6
bb6:
%10 = load i64, ptr %_0, align 8
ret i64 %10
}
Meta
rustc --version --verbose:
rustc 1.98.0-nightly (bc2112ed5 2026-06-18)
binary: rustc
commit-hash: bc2112ed56c99fa649e09ab3ab286afab3d9059a
commit-date: 2026-06-18
host: x86_64-unknown-linux-gnu
release: 1.98.0-nightly
LLVM version: 22.1.7
Godbolt link: https://godbolt.org/z/G4hxdn4Wa
I tried this code (simplified to a ~minimal version):
I expected
fooandfoo_borkedto have identical behavior/codegen.Instead, this happened:
foo_borkedcompiled into a function that always takes theKind::Abranch, never returning 27.Looking at the LLVM IR with
-Cno-prepopulate-passesit appears that this is a rustc bug, and given that-Zmir-opt-level=0produces valid code (foo_borked = foo) this is likely a Mir opt issue.Meta
rustc --version --verbose: