I tried this code:
#![feature(f128)]
fn main() {
// 300.0 as u8 saturates to 255.
assert_eq!(std::hint::black_box(300.0f128) as u8, 255);
}
I expected to see this happen: the assert passes, like it does under LLVM.
Instead, this happened: under -Zcodegen-backend=cranelift the cast returns 44 (300 & 0xff), wrapped to the low byte instead of saturated. i8, i16, and u16 are wrong the same way. Wider targets (i32 and up) are correct.
$ rustc +nightly -Zcodegen-backend=cranelift repro.rs && ./repro
assertion `left == right` failed
left: 44
right: 255
When cg_clif is built with debug assertions (the verifier is on, as on rust-lang/rust CI) it's a hard error instead of a silent miscompile, and using two narrow widths together makes the shared libcall get declared with two return types and panic. Both are in the backtrace below.
Cause
In the f128 -> int branch of codegen_f16_f128.rs, narrow targets go through ret_ty = I32 and call __fixunstfsi, which returns a 32-bit int (the si suffix), but the libcall's return AbiParam is declared as to_ty:
let ret_ty = if to_ty.bits() < 32 { types::I32 } else { to_ty };
let name = format!("__fix{sign}tf{size}i", /* size from ret_ty */);
let ret = fx.lib_call(&name, vec![AbiParam::new(from_ty)],
vec![AbiParam::new(to_ty)], &[from])[0]; // to_ty, should be ret_ty
So ret comes back typed i8/i16 while the saturating clamp below it builds i32 constants, giving the type-mismatched select the verifier rejects (and the wrong value when the verifier is off). Declaring it with ret_ty is enough. f16 -> int is unaffected (it goes via f32), and wide f128 targets are fine (ret_ty == to_ty, no clamp).
Happy to send the one line fix.
Meta
Found by the CodegenCranelift CI job on rust-lang/rust#159954 (adding f128 conversion methods).
rustc --version --verbose:
rustc 1.97.0-nightly (d3cd04068 2026-05-16)
host: aarch64-apple-darwin
Also reproduces on current CI (aarch64-unknown-linux-gnu, 2026-07-26).
Backtrace
# cg_clif with debug assertions (verifier on):
error: VerifierErrors([VerifierError { location: inst22, context: Some("v19 = select.i32 v18, v16, v14 ; v16 = 255"), message: "arg 2 (v14) has type i8, expected i32" }])
# two narrow widths together (f128 as u8 and f128 as u16):
thread 'rustc' panicked at compiler/rustc_codegen_cranelift/src/abi/mod.rs:190:
called `Result::unwrap()` on an `Err` value: IncompatibleSignature("__fixunstfsi",
Signature { params: [F128], returns: [I8] ... },
Signature { params: [F128], returns: [I16] ... })
@rustbot label +A-cranelift +I-miscompile +A-floating-point
I tried this code:
I expected to see this happen: the assert passes, like it does under LLVM.
Instead, this happened: under
-Zcodegen-backend=craneliftthe cast returns 44 (300 & 0xff), wrapped to the low byte instead of saturated.i8,i16, andu16are wrong the same way. Wider targets (i32and up) are correct.When cg_clif is built with debug assertions (the verifier is on, as on rust-lang/rust CI) it's a hard error instead of a silent miscompile, and using two narrow widths together makes the shared libcall get declared with two return types and panic. Both are in the backtrace below.
Cause
In the
f128 -> intbranch ofcodegen_f16_f128.rs, narrow targets go throughret_ty = I32and call__fixunstfsi, which returns a 32-bit int (thesisuffix), but the libcall's returnAbiParamis declared asto_ty:So
retcomes back typedi8/i16while the saturating clamp below it buildsi32constants, giving the type-mismatchedselectthe verifier rejects (and the wrong value when the verifier is off). Declaring it withret_tyis enough.f16 -> intis unaffected (it goes via f32), and widef128targets are fine (ret_ty == to_ty, no clamp).Happy to send the one line fix.
Meta
Found by the
CodegenCraneliftCI job on rust-lang/rust#159954 (adding f128 conversion methods).rustc --version --verbose:Also reproduces on current CI (aarch64-unknown-linux-gnu, 2026-07-26).
Backtrace
@rustbot label +A-cranelift +I-miscompile +A-floating-point