Skip to content

Commit

Permalink
[LangRef] Specify NaN behavior more precisely (#66579)
Browse files Browse the repository at this point in the history
The current docs don't say anything about the possible set of values
returned by a NaN-producing operation. However, there are some coding
patterns where such guarantees are strongly desired, such as NaN boxing
(as used e.g. in the Spidermonkey JS engine). And in fact the set of
possible payloads that can be observed today when compiling code via
LLVM is fairly limited (based on what optimizations and backends and
hardware currently do) -- but without a documented guarantee, languages
compiling to LLVM cannot really rely on this.

There was a long discussion about the exact shape that the guarantees
should take [on Discourse](https://discourse.llvm.org/t/stronger-floating-point-nan-guarantees/72165).
This PR transcribes what I perceived as the consensus, plus some further
clarifications: "unless specified otherwise", returned NaNs have a
non-deterministic sign, and the payload is generally either the
preferred (all-zero) payload or propagates the payload of one of the
inputs.
  • Loading branch information
RalfJung authored Oct 4, 2023
1 parent 1c2634e commit d688816
Showing 1 changed file with 89 additions and 6 deletions.
95 changes: 89 additions & 6 deletions llvm/docs/LangRef.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3394,17 +3394,81 @@ Floating-Point Environment
The default LLVM floating-point environment assumes that traps are disabled and
status flags are not observable. Therefore, floating-point math operations do
not have side effects and may be speculated freely. Results assume the
round-to-nearest rounding mode.
round-to-nearest rounding mode, and subnormals are assumed to be preserved.

Running LLVM code in an environment where these assumptions are not met can lead
to undefined behavior. The ``strictfp`` and ``denormal-fp-math`` attributes as
well as :ref:`Constrained Floating-Point Intrinsics <constrainedfp>` can be used
to weaken LLVM's assumptions and ensure defined behavior in non-default
floating-point environments; see their respective documentation for details.

.. _floatnan:

Behavior of Floating-Point NaN values
-------------------------------------

A floating-point NaN value consists of a sign bit, a quiet/signaling bit, and a
payload (which makes up the rest of the mantissa except for the quiet/signaling
bit). LLVM assumes that the quiet/signaling bit being set to ``1`` indicates a
quiet NaN (QNaN), and a value of ``0`` indicates a signaling NaN (SNaN). In the
following we will hence just call it the "quiet bit"

The representation bits of a floating-point value do not mutate arbitrarily; in
particular, if there is no floating-point operation being performed, NaN signs,
quiet bits, and payloads are preserved.

For the purpose of this section, ``bitcast`` as well as the following operations
are not "floating-point math operations": ``fneg``, ``llvm.fabs``, and
``llvm.copysign``. These operations act directly on the underlying bit
representation and never change anything except possibly for the sign bit.

For floating-point math operations, unless specified otherwise, the following
rules apply when a NaN value is returned: the result has a non-deterministic
sign; the quiet bit and payload are non-deterministically chosen from the
following set of options:

- The quiet bit is set and the payload is all-zero. ("Preferred NaN" case)
- The quiet bit is set and the payload is copied from any input operand that is
a NaN. ("Quieting NaN propagation" case)
- The quiet bit and payload are copied from any input operand that is a NaN.
("Unchanged NaN propagation" case)
- The quiet bit is set and the payload is picked from a target-specific set of
"extra" possible NaN payloads. The set can depend on the input operand values.
This set is empty on x86 and ARM, but can be non-empty on other architectures.
(For instance, on wasm, if any input NaN does not have the preferred all-zero
payload or any input NaN is an SNaN, then this set contains all possible
payloads; otherwise, it is empty. On SPARC, this set consists of the all-one
payload.)

In particular, if all input NaNs are quiet (or if there are no input NaNs), then
the output NaN is definitely quiet. Signaling NaN outputs can only occur if they
are provided as an input value. For example, "fmul SNaN, 1.0" may be simplified
to SNaN rather than QNaN. Similarly, if all input NaNs are preferred (or if
there are no input NaNs) and the target does not have any "extra" NaN payloads,
then the output NaN is guaranteed to be preferred.

Floating-point math operations are allowed to treat all NaNs as if they were
quiet NaNs. For example, "pow(1.0, SNaN)" may be simplified to 1.0. This also
means that SNaN may be passed through a math operation without quieting. For
example, "fmul SNaN, 1.0" may be simplified to SNaN rather than QNaN. However,
SNaN values are never created by math operations. They may only occur when
provided as a program input value.
quiet NaNs. For example, "pow(1.0, SNaN)" may be simplified to 1.0.

Code that requires different behavior than this should use the
:ref:`Constrained Floating-Point Intrinsics <constrainedfp>`.
In particular, constrained intrinsics rule out the "Unchanged NaN propagation"
case; they are guaranteed to return a QNaN.

Unfortunately, due to hard-or-impossible-to-fix issues, LLVM violates its own
specification on some architectures:
- x86-32 without SSE2 enabled may convert floating-point values to x86_fp80 and
back when performing floating-point math operations; this can lead to results
with different precision than expected and it can alter NaN values. Since
optimizations can make contradicting assumptions, this can lead to arbitrary
miscompilations. See `issue #44218
<https://github.com/llvm/llvm-project/issues/44218>`_.
- x86-32 (even with SSE2 enabled) may implicitly perform such a conversion on
values returned from a function for some calling conventions. See `issue
#66803 <https://github.com/llvm/llvm-project/issues/66803>`_.
- Older MIPS versions use the opposite polarity for the quiet/signaling bit, and
LLVM does not correctly represent this. See `issue #60796
<https://github.com/llvm/llvm-project/issues/60796>`_.

.. _fastmath:

Expand Down Expand Up @@ -9085,6 +9149,9 @@ Semantics:
""""""""""

The value produced is a copy of the operand with its sign bit flipped.
The value is otherwise completely identical; in particular, if the input is a
NaN, then the quiet/signaling bit and payload are perfectly preserved.

This instruction can also take any number of :ref:`fast-math
flags <fastmath>`, which are optimization hints to enable otherwise
unsafe floating-point optimizations:
Expand Down Expand Up @@ -11240,6 +11307,11 @@ The '``fptrunc``' instruction casts a ``value`` from a larger
This instruction is assumed to execute in the default :ref:`floating-point
environment <floatenv>`.

NaN values follow the usual :ref:`NaN behaviors <floatnan>`, except that _if_ a
NaN payload is propagated from the input ("Quieting NaN propagation" or
"Unchanged NaN propagation" cases), then the low order bits of the NaN payload
which cannot fit in the resulting type are discarded.

Example:
""""""""

Expand Down Expand Up @@ -11280,6 +11352,11 @@ The '``fpext``' instruction extends the ``value`` from a smaller
*no-op cast* because it always changes bits. Use ``bitcast`` to make a
*no-op cast* for a floating-point cast.

NaN values follow the usual :ref:`NaN behaviors <floatnan>`, except that _if_ a
NaN payload is propagated from the input ("Quieting NaN propagation" or
"Unchanged NaN propagation" cases), then it is copied to the high order bits of
the resulting payload, and the remaining low order bits are zero.

Example:
""""""""

Expand Down Expand Up @@ -15092,6 +15169,9 @@ Semantics:

This function returns the same values as the libm ``fabs`` functions
would, and handles error conditions in the same way.
The returned value is completely identical to the input except for the sign bit;
in particular, if the input is a NaN, then the quiet/signaling bit and payload
are perfectly preserved.

.. _i_minnum:

Expand Down Expand Up @@ -15307,6 +15387,9 @@ Semantics:

This function returns the same values as the libm ``copysign``
functions would, and handles error conditions in the same way.
The returned value is completely identical to the first operand except for the
sign bit; in particular, if the input is a NaN, then the quiet/signaling bit and
payload are perfectly preserved.

.. _int_floor:

Expand Down

0 comments on commit d688816

Please sign in to comment.