Skip to content

Incorrect codegen (instead of error) since 1.87.0, when all but one register is reserved on RISC-V. #146393

@zachs18

Description

@zachs18

Code

Note: This is very convoluted and does not reflect any at-all reasonable real-world use-case (i.e. reserving every register except one).

I tried this code:

#[inline(never)]
pub fn foo(x: &[u32; 2]) -> u32 {
  x[0] + x[1]
}

Compiler invocation:

rustc  lib.rs --crate-type=lib --target=riscv64gc-unknown-linux-gnu -Copt-level=3 -Ctarget-feature=+reserve-x1,+reserve-x3,+reserve-x4,+reserve-x5,+reserve-x6,+reserve-x7,+reserve-x8,+reserve-x9,+reserve-x11,+reserve-x12,+reserve-x13,+reserve-x14,+reserve-x15,+reserve-x16,+reserve-x17,+reserve-x18,+reserve-x19,+reserve-x20,+reserve-x21,+reserve-x22,+reserve-x23,+reserve-x24,+reserve-x25,+reserve-x26,+reserve-x27,+reserve-x28,+reserve-x29,+reserve-x30,+reserve-x31

Probably reproducible with any riscv32 or riscv64 target (I tried riscv32i-unknown-none-elf and riscv64gc-unknown-linux-gnu). The main requirements are: -Copt-level=3, and we tell LLVM that all general-purpose registers except zero/x0, sp/x2, and a0/x10 are reserved.

I expected to see this happen: Compiler error due to register allocation failure1. This used to be the case in Rust 1.86.0 and prior, before the LLVM 20 bump.

Instead, this happened: Successful compilation with incorrect assembly code.

0000000000000000 <_ZN3lib3foo17hc3dadf2c88cc8d04E>:
   0:	1141                	addi	sp,sp,-16 # 61302ff0 <_ZN3lib3foo17hc3dadf2c88cc8d04E+0x61302ff0>
   2:	e42a                	sd	a0,8(sp) # save x into a stack slot
   4:	6522                	ld	a0,8(sp) # load x from stack slot (inefficient, but not *wrong*)
   6:	4108                	lw	a0,0(a0) # load x[0] signext to i64 into a0
   8:	e02a                	sd	a0,0(sp) # save x[0] signext to i64 into a stack slot
   a:	6522                	ld	a0,8(sp) # load x from stack slot
   c:	4148                	lw	a0,4(a0) # load x[1] signext to i64 into a0
   e:	6502                	ld	a0,0(sp) # load x[0] signext to i64 from stack slot, *overwriting a0's previous value of x[1]*
  10:	9d29                	addw	a0,a0,a0 # this tries to add x[0] and x[1], but actually adds x[0] and x[0]
  12:	0141                	addi	sp,sp,16
  14:	8082                	ret # a0 is implicitly the return register

Version it worked on

It most recently worked on: Rust 1.86.0 and nightly-2025-02-17 (just before the LLVM 20 bump)

Version with regression

Rust 1.87.0 and nightly-2025-02-18 (just after the LLVM 20 bump), as well as Rust 1.89.0 stable, and nightly-2025-09-08

rustc --version --verbose:

rustc 1.87.0-nightly (ce36a966c 2025-02-17)
binary: rustc
commit-hash: ce36a966c79e109dabeef7a47fe68e5294c6d71e
commit-date: 2025-02-17
host: x86_64-unknown-linux-gnu
release: 1.87.0-nightly
LLVM version: 20.1.0

Backtrace

N/A

@rustbot modify labels: +regression-from-stable-to-stable -regression-untriaged A-LLVM A-codegen O-riscv


I assume this is an LLVM issue, but I could not reproduce it using local LLVM. I.e. I got the LLVM IR from affected rustc using --emit-llvm-ir and passed it to llc-20 (version 20.1.2), but that did give an error: error: <unknown>:0:0: ran out of registers during register allocation in function '_ZN13bullying_llvm3foo17h2329acc782955056E', so IDK what's different there. Maybe there was a bugfix between LLVM 20.1.0 and 20.1.2?

; ModuleID = 'e3s1mjt1cx7h2uznpt1s0mib7'
source_filename = "e3s1mjt1cx7h2uznpt1s0mib7"
target datalayout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128"
target triple = "riscv64-unknown-linux-gnu"

; repro::foo
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: read) uwtable
define noundef i32 @_ZN5repro3foo17h8e0ccca3a760da02E(ptr noalias noundef readonly align 4 captures(none) dereferenceable(8) %x) unnamed_addr #0 {
start:
  %_2 = load i32, ptr %x, align 4, !noundef !4
  %0 = getelementptr inbounds nuw i8, ptr %x, i64 4
  %_3 = load i32, ptr %0, align 4, !noundef !4
  %_0 = add i32 %_3, %_2
  ret i32 %_0
}

attributes #0 = { mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: read) uwtable "target-cpu"="generic-rv64" "target-features"="+m,+a,+f,+d,+c,+zicsr,+zifencei,+reserve-x1,+reserve-x3,+reserve-x4,+reserve-x5,+reserve-x6,+reserve-x7,+reserve-x8,+reserve-x9,+reserve-x11,+reserve-x12,+reserve-x13,+reserve-x14,+reserve-x15,+reserve-x16,+reserve-x17,+reserve-x18,+reserve-x19,+reserve-x20,+reserve-x21,+reserve-x22,+reserve-x23,+reserve-x24,+reserve-x25,+reserve-x26,+reserve-x27,+reserve-x28,+reserve-x29,+reserve-x30,+reserve-x31" }

!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}

!0 = !{i32 8, !"PIC Level", i32 2}
!1 = !{i32 1, !"Code Model", i32 3}
!2 = !{i32 1, !"target-abi", !"lp64d"}
!3 = !{!"rustc version 1.91.0-nightly (9c27f27ea 2025-09-08)"}
!4 = !{}
$ cargo +nightly rustc -- --emit=llvm-ir -Copt-level=3 -o nightly4.ll -Cdebuginfo=none
$ llc-20 nightly3-688d8f82623f4b82.ll -o out-20
error: <unknown>:0:0: ran out of registers during register allocation in function '_ZN5repro3foo17h8e0ccca3a760da02E'
# .cargo/config.toml
[build]
target = "riscv64gc-unknown-linux-gnu"

[target.riscv64gc-unknown-linux-gnu]
rustflags = ["-Ctarget-feature=+reserve-x1,+reserve-x3,+reserve-x4,+reserve-x5,+reserve-x6,+reserve-x7,+reserve-x8,+reserve-x9,+reserve-x11,+reserve-x12,+reserve-x13,+reserve-x14,+reserve-x15,+reserve-x16,+reserve-x17,+reserve-x18,+reserve-x19,+reserve-x20,+reserve-x21,+reserve-x22,+reserve-x23,+reserve-x24,+reserve-x25,+reserve-x26,+reserve-x27,+reserve-x28,+reserve-x29,+reserve-x30,+reserve-x31"]

Footnotes

  1. we need at least two GPRs to do an addition on RISC-V, and we have only one usable one, a0 (x0 is hardwired to zero, and sp is the stack pointer), so there's no reasonable way to codegen this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-codegenArea: Code generationC-bugCategory: This is a bug.O-riscvTarget: RISC-V architectureP-mediumMedium priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions