Skip to content

Ensure floats are returned losslessly by the C ABI on 32-bit x86 - #161950

Open
beetrees wants to merge 1 commit into
rust-lang:mainfrom
beetrees:x86-ret-snan-c
Open

Ensure floats are returned losslessly by the C ABI on 32-bit x86#161950
beetrees wants to merge 1 commit into
rust-lang:mainfrom
beetrees:x86-ret-snan-c

Conversation

@beetrees

@beetrees beetrees commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

View all comments

The x86 C ABI returns f32 and f64 using the x87 extended precision floating point format (henceforth x86_fp80) on the x87 floating point stack. Currently, LLVM uses regular fld/fstp instructions to convert f32/f64 to and from x86_fp80 when returning from a function with the C ABI, however these instructions perform floating point format conversions that quieten signalling NaNs, which breaks Rust's (and LLVM's) guarantees. This is a long-standing LLVM bug (llvm/llvm-project#66803) but is difficult to fix on the LLVM side as it ties into larger problems with the way LLVM handles x87 registers holding f32/f64, combined with the general lack of developer interest in 32-bit x86 without SSE (although the ABI bug specifically also affects targets with SSE2 enabled). This problem has been tracked on the Rust side in #115567, with #123351 ensuring that Rustic ABIs avoid the x87 stack altogether and #113053 adding notes to the platform support page detailing how the targets are non-complaint (including the tier 1 targets i686-unknown-linux-gnu and i686-pc-windows-msvc).

This PR fixes #115567 by manually converting NaNs to and from x86_fp80 when a function returns f32/f64 (this was briefly discussed in #t-compiler > `x87_f80` is weird @ 💬).

r? @tgross35
cc @RalfJung

try-job: i686-*

@rustbot rustbot added A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Aug 28, 2026
@rustbot

rustbot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

tgross35 is currently at their maximum review capacity.
They may take a while to respond.

@rust-log-analyzer

This comment has been minimized.

@folkertdev

Copy link
Copy Markdown
Contributor

@bors delegate try

@rust-bors

rust-bors Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

✌️ @beetrees, you can now perform try builds on this pull request!

You can now post @bors try to start a try build.

@beetrees

Copy link
Copy Markdown
Contributor Author

@bors try

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 28, 2026
Ensure floats are returned losslessly by the C ABI on 32-bit x86

try-job: i686-*
@rust-log-analyzer

This comment has been minimized.

@rust-bors

rust-bors Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: a9e9507 (a9e95070267a88d3be44d2d95464f64d39bb751d)
Base parent: 17fd5b8 (17fd5b8a37b6667b6cc137f3cc35f09759768a3b)

@RalfJung RalfJung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I can't review this, but I love it. :) I see that there's a separate path for nnan, so if LLVM can infer nnan all the new code should optimize away. That might be worth a test as well if we don't already have one?

View changes since this review

C: HasDataLayout + HasTargetSpec,
{
if !fn_abi.ret.is_ignore() {
// "vectorcall" returns floats in `xmm0`, and soft float also does not use the x87 stack.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

vectorcall does not show up in the code below, so this comment is a bit confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've flipped the condition to make it more obvious what the comment is referring to.

Comment on lines 17 to 19

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't the FIXME and if be removed as this PR is supposed to fix exactly that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This FIXME isn't for the ABI issue, this is for the related but separate "i586 targets without SSE have horribly broken floats (#114479)". While this PR does fix the C ABI, it mostly doesn't have any noticeable affect on targets without SSE as LLVM will load f32/f64 into x87 registers (quietening NaNs) whenever it want to load them into a register, so f32/f64 are quietened mid function.

I've updated the test so it now runs on i586 targets while allowing NaNs to be quitened only on them to make it more obvious what's going on.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh I see.

I was looking for a pure-Rust test for the change; it shouldn't be necessary to use C and FFI for that, should it?

@beetrees beetrees Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The "C" (and other ABI) test cases I added in this PR to return-float.rs are a pure-Rust test for this change: the version of return-float.rs in this PR would fail on i686-* targets before this PR (before this PR return-float.rs only tested the "Rust" ABI). After this PR, x87-related float miscompilations will be entirely limited to the non-SSE i586-* targets.

/// 32-bit x86. This is needed as LLVM needs to generate extra code to ensure that signalling
/// NaNs are passed losslessly on the x87 floating point stack. Only valid on return types on
/// 32-bit x86 with a cast target of either `Reg::f32()` or `Reg::f64()`.
pub x87_floating_point_stack: bool,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should mention that the LLVM-level type is then changed to x86_fp80.

// any initialized bytes within a partially uninitialized value survive the round trip,
// freeze the value.
let value = if no_undef { value } else { self.freeze(value) };
let is_nan = self.fcmp(RealPredicate::RealUNO, value, value);

@RalfJung RalfJung Aug 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wouldn't the ideal test be "is it a signaling NaN"? Or do normal NaNs also get garbled?
Though I can imagine that that's annoying enough to implement that it's not worth it.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only signalling NaNs get quietened. I went with just checking for NaN as that is a single-instruction operation, whereas checking if a value is a signalling NaN requires many bit operations (more the larger the float). As the vast majority of values seem likely to be not NaNs at all, it felt more important to prioritise the fast path for non-NaNs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Makes sense. It's also unlikely LLVM would ever know "this is not a signaling NaN" (so the more complicated condition will also not have the branch eliminated more often).

Please add a comment explaining this.

@beetrees
beetrees force-pushed the x86-ret-snan-c branch 3 times, most recently from 68fdb80 to bb9b569 Compare August 29, 2026 14:05
Comment thread compiler/rustc_codegen_llvm/src/abi.rs Outdated
Comment on lines +386 to +387
let (fraction_bits, fraction) =
if num_bits != 64 { (23, self.zext(bits, self.type_i64())) } else { (52, bits) };

@folkertdev folkertdev Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this clearer as

Suggested change
let (fraction_bits, fraction) =
if num_bits != 64 { (23, self.zext(bits, self.type_i64())) } else { (52, bits) };
let (fraction_bits, fraction) = match num_bits {
32 => (f32::MANTISSA_DIGITS - 1, self.zext(bits, self.type_i64())),
64 => (f64::MANTISSA_DIGITS - 1, bits),
_ => bug!(),
};

View changes since the review

let fraction = self.zext(fraction, self.type_ix(80));

let is_nan_res = self.or(exp_and_sign, fraction);
let is_nan_res = self.bitcast(is_nan_res, self.type_x86_fp80());

@folkertdev folkertdev Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

slightly confusing that this works, but apparently it does? (because when stored x86_fp80 would use 12 or 16 bytes).

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The LLVM x86_fp80 type is exactly 80 bits - padding is added when needed by Clang AFAICT:

https://github.com/llvm/llvm-project/blob/05da143c63d333420b99eab198e1a89c97480929/llvm/lib/IR/Type.cpp#L208

Comment thread compiler/rustc_codegen_llvm/src/abi.rs Outdated
Comment on lines +426 to +430
let fraction_bits = match num_bits {
32 => 23,
64 => 52,
_ => bug!("attempt to return float on x87 floating point stack with width {num_bits}"),
};

@folkertdev folkertdev Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

dst: PlaceRef<'tcx, Self::Value>,
);
/// Losslessly convert a `f32` or `f64` to a float to be returned on the x87 floating point
/// stack. As `MaybeUninit` floats are returned on the floating point stack, `value` being

@folkertdev folkertdev Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// stack. As `MaybeUninit` floats are returned on the floating point stack, `value` being
/// stack. Because `MaybeUninit` floats are also returned on the floating point stack, `value` being

Maybe this comment should also mention what this is for?

View changes since the review

@@ -44,6 +72,12 @@ where
// float aggregates directly in a floating-point register.
if fn_abi.ret.layout.is_single_fp_element(cx) {

@folkertdev folkertdev Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmm, is this actually what we want for is_single_fp_element? Should it not also return true on f16 and f128 (or have a clearer name)?

pub fn is_single_fp_element<C>(self, cx: &C) -> bool
where
Ty: TyAbiInterface<'a, C>,
C: HasDataLayout,
{
match self.backend_repr {
BackendRepr::Scalar(scalar) => {
matches!(scalar.primitive(), Primitive::Float(Float::F32 | Float::F64))
}
BackendRepr::Memory { .. } => {
if self.fields.count() == 1 && self.fields.offset(0).bytes() == 0 {
self.field(cx, 0).is_single_fp_element(cx)
} else {
false
}
}
_ => false,
}
}

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking into it, it definitely isn't what we want, and at least on s390x #[repr(C)] struct Foo(f16) is passed incorrectly.

The rust version moves ldgr %f0, %r0 from a general into a float register. With the C compilers the value is already there.

I'll try to clean that up.

@beetrees beetrees Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've left it as is in this PR as I didn't want to put to many changes in one PR, but is_single_fp_element is definitely not correct here. Both Clang and GCC also match f16 on x86, and they also require the struct to have no padding (which isn't checked here at the moment). is_single_fp_element also doesn't handle #[repr(transparent)] structs containing 1ZST fields. is_single_fp_element is also used in the s390x ABI, where GCC and Clang compilers currently implement slightly different rules: padding is fine as long as the overall size if either 2, 4 bytes or 8 bytes. This appears to be GCC and Clang compilers violating the s390x ABI specification, which says "A structure equivalent to one of the above. A structure is equivalent to a type 𝑇 if and only if it has exactly one member, which is either of type 𝑇 itself or a structure equivalent to type 𝑇.", which implies that padding should actually be ignored.

(EDIT: That's strange, GitHub didn't show me your second comment until after I'd posted mine.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh yeah is_single_fp_element is completely wrong.
Strange that tests/ui/abi/compatibility.rs didn't catch it, it's meant exactly for things like this. Even when I add an f32 test, which will check whether a repr(transparent) wrapper around f32 has the same ABI as f32, nothing fails: #161991

@beetrees
beetrees force-pushed the x86-ret-snan-c branch 2 times, most recently from 26bbbba to 552a4cb Compare August 29, 2026 16:34
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking issue: 32bit x86 targets lose float NaN payload in return values

6 participants