Change:
[^windows-support]: Only Windows 10 currently undergoes automated testing. Earlier versions of Windows rely on testing and support from the community. <ins> [^x86_32-floats-return-ABI]: Due to limitations of the C ABI, floating-point support on `i686` targets is non-compliant: floating-point return values are passed via an x87 register, so NaN payload bits can be lost. See [issue #114479][x86-32-float-issue]. </ins> [77071]: https://github.com/rust-lang/rust/issues/77071 <ins> [x86-32-float-issue]: https://github.com/rust-lang/rust/issues/114479 </ins> ## Tier 1

Discussion:
Compiling and running the following code with for 32-bit Linux on a recent nightly (2020-06-07) results in a failing assertion. Output: However, this code is taken directly from a unit test for in , so presumably it should succeed.
This is probably caused by x87 instructions... I think we have an issue about that somewhere already, not sure where though. Cc
Indeed. After the mask, the value in is the following. On x86, this value represents a signalling NaN. The call to puts the same value onto the top of the x87 FPU stack with an instruction before returning. After this is called, the #IA flag (invalid arithmetic operand exception) is set in the x87 status register, and the value on top of the stack is an 80-bit quiet NaN (). According to the reference manual, this occurs only if the operand to is not also a double extended-precision float. When the return value is popped off the FPU stack in and stored in memory with an instruction, the signalling/quiet bit (the most-significant bit of the significand) is set. On x86, this represents a quiet NaN.
Independently of x87, the intent behind the test is on shaky ground. LLVM optimizations do not generally preserve NaN bit strings, see e.g. .
Seems like we should update the docs for then? Currently, they claim and
The first part isn't wrong, as far as I can tell. It literally is implemented as transmute, it's just that handling any float values in general does not always "preserves the exact bits".
This succeeds in debug mode unless the last line is uncommented, so the docs are misleading at the very least.
I believe that difference is due to the getting const-eval'd, the LLVM IR just stores the same bit pattern to the stack several times. While this does end up making a difference today: changes to rustc could make the difference disappear again (enabling MIR inlining, not running const-prop in debug builds, forbidding const-prop from evaluating s, etc.) , all sorts of harmless changes to the version that uses directly can break it. For example, anything that stops constant propagation. Or introducing your own function boundaries.

It's not that the functions in std do something different from , it's that -- contrary to all usual intuition -- function boundaries and IR optimizations matter for this behavior. This whole situation is an incredibly mess and headache, and picking on this one sentence from the docs is not going to help clarify it. If we want to give users proper guidelines about this, we need to go back to the drawing board and figure out what the rules actually are and should be.
If you mean MIR const propagated, it does not. Not sure about LLVM const propagation, but adding and wrapping the round trip in a function call did not change behavior. I believe the fact that is direct re-export of the intrinsic accounts for the difference. I'm not saying that this area isn't a mess, but the docs don't acknowledge this at all, and are trivially wrong in the case I've shown above. I'm going to update them to reflect this.
Besides the unresolved problems around NaN payloads, this raises another issue: Should we be running unit tests for the standard library in debug mode on CI? As far as I know, we don't distribute a non-optimized , but maybe we should be building one and running its tests as part of the CI jobs?
I am fairly certain (hopeful, I guess) that most of our builders do have debug assertions enabled... however, it sounds like what you actually want here is a debug libstd. I don't know how feasible that is from a performance perspective (I imagine running those tests is considerably slower?). We do actually have a i686-gnu-nopt builder, and I think some other nopt builders, (). That's currently configured to not enable optimizations for tests, but that only affects compiletest-run tests. For tests like this which are heavily dependent on such, we can move them to run-pass tests to get benefits of debug testing. Note that debug assertions are currently disabled on that builder due to CI time concerns.
Basically, as with all such questions, it comes down to someone sitting down and timing how long a debug build of std/std tests + running those tests would take, then timing some current task (e.g. release std tests) and scaling that to CI time. If that's sufficiently small in CI timescales, we can add it, if not we'll have to not do so.

In terms of the bug here, should we have a proper meta-bug (maybe even labelled I-unsound) about LLVM FP semantics around NaN payloads being a mess? This is not immediately actionable, but it is certainly something we should track in a single place. Or should we convert either this or into that meta-bug? Cc -- the short version is, looking at the bits of NaNs in LLVM is broken. :/
I went ahead and opened to track documenting our guarantees around NaN. I think we can close this issue once we remove the tests that are relying on unspecified behavior. I can look into running unit tests in debug mode on CI. Perhaps we just want to make the existing floating point unit tests into UI tests to see if there's any other issues like this?
This is a duplicate of .
Don't our existing debug CI builders already do that? Maybe not for i686 though. EDIT: Ah I see, the point is that we do it for ui tests but not unit tests. FWIW, unit tests are in general preferable because we can more easily run them in Miri.
So which of these would you prefer to close?
From reading the thread, it sounds like the bits of a NaN can basically change arbitrarily, as a result of LLVM optimizations that are not guaranteed to preserve them? (And other things, I suppose)
I always thought the x86-specific problems that arise due to x87 are side-stepped when SSE2 is enabled. But now points out that this bug still remains. How is that possible, isn't SSE2 replacing the x87 mess?
So I guess that also alleviates any question about whether LLVM could adjust codegen to not have this problem. But this also points to a way out, doesn't it? If we adopt a model where we say something like "floating-point operations may change the NaN bits in a non-deterministic way" (which is what wasm does so arguably LLVM has to support it), then we "just" have to also say that as a special exception, on x86-32, returning floating-point values from a function also is considered a "floating-point operation". (Does this also affect arguments passed to a function?)
The x87 stack is only used for the return value, not for passing arguments. That formulation would be correct on i686 AFAIK.

An alternative would be to use a different calling convention for functions on 32-bit x86. I think this would break a lot of tooling, however, and we would still have this problem across an FFI boundary. It's also a lot more work.
the above is true with some important exceptions: no operation (other than a bitcast from an integer or conversion from text to fp) produces a signaling NaN where the input did not have a signaling NaN, and all arithmetic operations (other than sign-bit manipulation) only produce quiet NaNs when they produce NaNs.
The doesn't guarantee anything like that, does it? We cannot guarantee more than our compilation targets do. Or maybe that's what the "arithmetic NaN" stuff is about, which I didn't really understand.
arithmetic NaN is another name for quiet NaN on all but a few unusual platforms: HPPA and MIPS pre-2008. See the definitions for  in the simple-soft-float crate, which I wrote.
Is it possible for us to adjust the Rust float calling convention for the pentium4-* ("i686") targets so that they always use the xmm registers for Rust code (essentially requiring they follow the x86-64 ABI conventions), never passing through the x87 stack-registers (unless a value is given to us from outside of Rust, which is of course not something we can meaningfully control)?
It should be possible for the rust abi, though I don't know how to do this with LLVM. For the C abi I think the only option that doesn't break compatibility with existing C compilers would be to roundtrip between x87 and xmm registers to remove the excess precision.
I think that would be fine. If we implemented that strategy, that would resolve this bug for Rust for the i686 target, and any other unusual behavior would be functionally Not A Bug: if you want to be sure you are actually moving 64 bits across an FFI barrier, then you can pass it as a u64 and remake it as a float on the other side, otherwise we should follow whatever idiosyncratic behavior a given ABI mandates. About the previous in this thread, I am actually somewhat skeptical as to how much tooling it would break (not that I think it would be zero) as opposed to how many issues would be closed (which would be at least one).

For instance, any debuggers should be looking at all the registers, not just the x87 stack, and MSVC actually sets the x87 stack to a different precision than other C compilers, apparently, so we should be avoiding it in general if we want portable results:
apparently the x87 stack can be avoided by using the calling convention or by marking the floating-point return value as :
Belatedly: if that is the case, then that answers why this changes! Our functions wind up annotated with in LLVM after we run optimizations, but not before, so we actually change our calling conventions when calling into Rust code. We can close this bug simply by tuning rustc to be more insistent and consistent in passing arguments.
I propose that we close this issue by documenting this as a known non-compliance on x86-32 targets:
Closing in favor of a dedicated tracking issue that summarizes what we know about the issue:

When code is compiled using the X87 FPU, comparing to infinity can lead to unexpected behavior. I tried this code: I expected to see this happen: This should either print "infinite or nan" or "is finite, exp = ...". The should never fail because the exponent bits are 0x7FF if and only if the number is infinity or nan, so the condition should have been false. Instead, this happened: When compiled for X86 without SSE, the assert will fail. This could also cause safety issues if code relies on it. The following command can be used to compile without SSE: : and
For reference, can you post the machine code this results in? I'm particularly curious if there's any calls to functions already built in the distributed libstd, since those would potentially suffer from ABI mismatches (see ) that could explain the weird behavior.
Sure, the assembly of the function is (after removing the s to simplify it): Generated with: I think I forgot to mention in the OP that this happens in both debug and release mode.
Uh-oh... should this be labelled "unsound"? Any time codegen'd behavior does not match the spec, that can be considered a soundness issue as well.
I just found out that this can be reproduced without using .
I was searching for open floating point issues related to and stumbled upon this one, which seems to have slipped through the cracks. Marking as "unsound" and "needs prioritization". I'm still not sure exactly what causes this. cc
I believe this happens because x87 internally works on 80-bits numbers. This format can handle numbers up to about 10^4932, which means that the result of the multiplication is not infinity (when it is on the FP stack), hence the comparisons do not succeed. On the other hand, the code that extracts the exponent casts it to 64-bits, so the number is rounded (truncated?) to the positive infinity.
In a way, this is what is happening behind the scenes: Note that in this case the downcast is explicit.
Indeed. The underlying cause is clear. I wonder what we should do here, though? Does Rust currently guarantee that extended precision is not used for operations on ? If so, this is technically a miscompilation. However, I don't know whether it's worth fixing.

Maybe we should just document the status quo and move on?
Shouldn't we rather tag instead of tagging the 4 issues that are all instances of the same problem?
This is wholly distinct from . There's no NaN anywhere in this program.
Oh, good point. I had missed that. So this is truly an i686-only artifact caused by using the x87 instructions, and has nothing to do with LLVM's sloppiness around NaNs?
Could we argue that this is a bug in LLVM, or is there some statement in LLVM's LangRef that actually makes this a correct compilation of the LLVM IR rustc produces?
Not sure. The LLVM reference says: I don't have a copy of the IEEE spec, so I don't know if sharing a "binary format" implies that they must not use a higher precision for intermediate results. I think that the IEEE-754 may explicitly allow for this, however? In any case, it's unlikely that LLVM will guarantee the same precision everywhere as long as x87 instructions are supported. C has had the same issue for 20 years now. The GCC wiki discusses what would be required to make floating point math for . Presumably this kind of approach was also considered by LLVM and rejected. As long as 32-bit x86 is a tier 1 target, I believe we won't be able to guarantee at the language level that floating point math is always performed at 64-bit precision for (same goes for ). Instead, we will have to say something like "the precision of intermediate floating point calculations is platform-defined but guarantees that all tier 1 platforms besides i686 use the same precision as the floating-point type for all computations".
We could require SSE. (No idea if that is even remotely realistic, but it felt worth mentioning.)
The original SSE only supported single-precision floating point arithmetic. You need at least SSE2 for the semantics we want. I believe that all i686 processors have SSE but not all have SSE2. Not sure what we do by default here? But yes, I think that would be fine.
FWIW, the target triples i686-* are a misnomer, they include SSE2 already.
See also: (“Allow floating-point operations to provide extra precision than specified, as an optimization”)
Ah, that's why the OP had to do .

Can we declare non-SSE2-i686 "unsupported" to "fix" the problem here? I am not sure what that would actually mean in practice, maybe error or at least warn when SSE2 is disabled, or so. Without proper hardware support, it seems hard to do anything better, and if it's a warning people can still proceed with caution. there were many concerns in that RFC about enabling such optimizations by default (, ). Also, would that RFC really lead to problems such as this? is still a 64-bit type, so the assertion here should still hold, right? The issue arises because is actually represented as 80bits on x87 and thus there are more possible bit patterns than there should be?
Is this related to or a duplicate of Both are about x87 floating point weirdness, from what I can tell.
Related to (in the sense of "it's about x86 and the x87 FPU"), but not a duplicate of, .
I do! IEEE 754 doesn't directly concern itself with expression evaluation or intermediates. It only mandates how individual arithmetical operations are to be performed. The existence of the x87's 80-bit format is not even mentioned in the spec. Section 11 (Reproducible floating-point results) of IEEE 754-2019 concerns itself with reproducible programming, but that's an opt-in. Section 11 stipulates that the reproducible operations are those defined in Section 5 (Operations). The definition of arithmetic operations in Section 5 requires that they are correctly rounded for their destination format. That means that if you are claiming to be producing a 64-bit floating-point product according to the spec's definition of multiplication, you must use that for further operations, and not use your higher intermediate precision to perform further operations on it. It's possible to cajole the x87 unit into compliance with IEEE 754, but it can be painful. You need to: the precision flag to match your target precision (example: ). sure the exponents of intermediates stay within your destination's exponent range. In the worst case, this can be accomplished by storing and reloading intermediates before they are used again. signaling NaNs are in the 80-bit format before loading them into the x87 registers.

As long as you only load/store 80-bit floats onto the x87 stack, all bits are preserved (example: ). Luckily, this usually doesn't matter anyway since signalling NaNs must be quieted if you actually do anything with them. Mostly, just don't use the x87 registers for temporary storage of floats that might be signalling NaNs. Unfortunately, the best way to use the x87 is to stick to its native 80-bit format, which is not portable.
I propose that we close this issue by documenting this as a known non-compliance on x86-32 targets:
I'm not sure this should be closed. This is an LLVM codegen bug, the same bug as in
Ah, that's a fun one -- is that exploiting the fact that LLVM optimizes as if floats work according to IEEE 754, but then at runtime they behave differently because x87? rustc's own MIR optimizations might well do something similar. Fundamentally this is still a target bug, the optimization is just correctly doing source-level reasoning -- but the backend fails to correctly implement the source behavior.
What's happening in the linked issue is that the sum is initially computed with 80-bit precision, but the comparison is evaluated twice with different precisions. The value of z passed to printf is evaluated at 80-bit precision, and then stored on the stack to be passed as a vararg. Calling convention means the x87 registers are not preserved, so the sum that went into the comparison gets spilled to a 64 bit floating point temporary. After the printf, the comparison then gets repeated using the spilled (now rounded) value, because LLVM erroneously believes that this is the same value as was in the floating point register.
Closing in favor of a dedicated tracking issue that summarizes what we know about the issue: