Change:
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:

Discussion:
target | notes -------|------- `aarch64-unknown-linux-gnu` | ARM64 Linux (kernel 4.1, glibc 2.17+) [^missing-stack-probes] <del> `i686-pc-windows-gnu` | 32-bit MinGW (Windows 7+) [^windows-support] `i686-pc-windows-msvc` | 32-bit MSVC (Windows 7+) [^windows-support] `i686-unknown-linux-gnu` | 32-bit Linux (kernel 3.2+, glibc 2.17+) </del> <ins> `i686-pc-windows-gnu` | 32-bit MinGW (Windows 7+) [^windows-support] [^x86_32-floats-return-ABI] `i686-pc-windows-msvc` | 32-bit MSVC (Windows 7+) [^windows-support] [^x86_32-floats-return-ABI] `i686-unknown-linux-gnu` | 32-bit Linux (kernel 3.2+, glibc 2.17+) [^x86_32-floats-return-ABI] </ins> `x86_64-apple-darwin` | 64-bit macOS (10.12+, Sierra+) `x86_64-pc-windows-gnu` | 64-bit MinGW (Windows 7+) [^windows-support] `x86_64-pc-windows-msvc` | 64-bit MSVC (Windows 7+) [^windows-support]

[^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

`armv7r-none-eabi` | * | Bare ARMv7-R `armv7r-none-eabihf` | * | Bare ARMv7-R, hardfloat `asmjs-unknown-emscripten` | ✓ | asm.js via Emscripten <del> `i586-pc-windows-msvc` | * | 32-bit Windows w/o SSE `i586-unknown-linux-gnu` | ✓ | 32-bit Linux w/o SSE (kernel 3.2, glibc 2.17) `i586-unknown-linux-musl` | ✓ | 32-bit Linux w/o SSE, MUSL [`i686-linux-android`](platform-support/android.md) | ✓ | 32-bit x86 Android `i686-unknown-freebsd` | ✓ | 32-bit FreeBSD `i686-unknown-linux-musl` | ✓ | 32-bit Linux with MUSL </del> <ins> `i586-pc-windows-msvc` | * | 32-bit Windows w/o SSE [^x86_32-floats-x87] `i586-unknown-linux-gnu` | ✓ | 32-bit Linux w/o SSE (kernel 3.2, glibc 2.17) [^x86_32-floats-x87] `i586-unknown-linux-musl` | ✓ | 32-bit Linux w/o SSE, MUSL [^x86_32-floats-x87] [`i686-linux-android`](platform-support/android.md) | ✓ | 32-bit x86 Android [^x86_32-floats-return-ABI] `i686-unknown-freebsd` | ✓ | 32-bit FreeBSD [^x86_32-floats-return-ABI] `i686-unknown-linux-musl` | ✓ | 32-bit Linux with MUSL [^x86_32-floats-return-ABI] </ins> [`i686-unknown-uefi`](platform-support/unknown-uefi.md) | * | 32-bit UEFI [`loongarch64-unknown-none`](platform-support/loongarch-none.md) | * |  | LoongArch64 Bare-metal (LP64D ABI) [`loongarch64-unknown-none-softfloat`](platform-support/loongarch-none.md) | * |  | LoongArch64 Bare-metal (LP64S ABI)

`x86_64-unknown-redox` | ✓ | Redox OS [`x86_64-unknown-uefi`](platform-support/unknown-uefi.md) | * | 64-bit UEFI <ins> [^x86_32-floats-x87]: Floating-point support on `i586` targets is non-compliant: the `x87` registers and instructions used for these targets do not provide IEEE-754-compliant behavior, in particular when it comes to rounding and NaN payload bits. See [issue #114479][x86-32-float-issue]. </ins> [Fortanix ABI]: https://edp.fortanix.com/ ## Tier 3

`bpfel-unknown-none` | * |  | BPF (little endian) `csky-unknown-linux-gnuabiv2` | ✓ |  | C-SKY abiv2 Linux(little endian) `hexagon-unknown-linux-musl` | ? |  | <del> `i386-apple-ios` | ✓ |  | 32-bit x86 iOS [`i586-pc-nto-qnx700`](platform-support/nto-qnx.md) | * |  | 32-bit x86 QNX Neutrino 7.0 RTOS | `i686-apple-darwin` | ✓ | ✓ | 32-bit macOS (10.12+, Sierra+) `i686-pc-windows-msvc` | * |  | 32-bit Windows XP support [`i686-pc-windows-gnullvm`](platform-support/pc-windows-gnullvm.md) | ✓ | ✓ | `i686-unknown-haiku` | ✓ | ✓ | 32-bit Haiku [`i686-unknown-hurd-gnu`](platform-support/hurd.md) | ✓ | ✓ | 32-bit GNU/Hurd [`i686-unknown-netbsd`](platform-support/netbsd.md) | ✓ | ✓ | NetBSD/i386 with SSE2 [`i686-unknown-openbsd`](platform-support/openbsd.md) | ✓ | ✓ | 32-bit OpenBSD `i686-uwp-windows-gnu` | ? |  | `i686-uwp-windows-msvc` | ? |  | `i686-wrs-vxworks` | ? |  | </del> <ins> `i386-apple-ios` | ✓ |  | 32-bit x86 iOS [^x86_32-floats-return-ABI] [`i586-pc-nto-qnx700`](platform-support/nto-qnx.md) | * |  | 32-bit x86 QNX Neutrino 7.0 RTOS  [^x86_32-floats-return-ABI] `i686-apple-darwin` | ✓ | ✓ | 32-bit macOS (10.12+, Sierra+) [^x86_32-floats-return-ABI] `i686-pc-windows-msvc` | * |  | 32-bit Windows XP support [^x86_32-floats-return-ABI] [`i686-pc-windows-gnullvm`](platform-support/pc-windows-gnullvm.md) | ✓ | ✓ | [^x86_32-floats-return-ABI] `i686-unknown-haiku` | ✓ | ✓ | 32-bit Haiku [^x86_32-floats-return-ABI] [`i686-unknown-hurd-gnu`](platform-support/hurd.md) | ✓ | ✓ | 32-bit GNU/Hurd [^x86_32-floats-return-ABI] [`i686-unknown-netbsd`](platform-support/netbsd.md) | ✓ | ✓ | NetBSD/i386 with SSE2 [^x86_32-floats-return-ABI] [`i686-unknown-openbsd`](platform-support/openbsd.md) | ✓ | ✓ | 32-bit OpenBSD [^x86_32-floats-return-ABI] `i686-uwp-windows-gnu` | ? |  | [^x86_32-floats-return-ABI] `i686-uwp-windows-msvc` | ? |  | [^x86_32-floats-return-ABI] `i686-wrs-vxworks` | ? |  | [^x86_32-floats-return-ABI] </ins> [`m68k-unknown-linux-gnu`](platform-support/m68k-unknown-linux-gnu.md) | ? |  | Motorola 680x0 Linux `mips-unknown-linux-uclibc` | ✓ |  | MIPS Linux with uClibc [`mips64-openwrt-linux-musl`](platform-support/mips64-openwrt-linux-musl.md) | ? |  | MIPS64 for OpenWrt Linux MUSL