Float.SignallingNaN becomes non-signalling when converted to Double

Double(Float.signalingNaN).isSignalingNaN is false.

Is this a bug?

extension Float {
    var bits: String { String(self.bitPattern, radix: 2) + String(repeating: " ", count: 32) }
}
extension Double {
    var bits: String { String(self.bitPattern, radix: 2) }
}


print("""
                     Value | Bit pattern                                                     | isNaN | isSignalingNaN
---------------------------|-----------------------------------------------------------------|-------|----------------
                Double.nan | \(Double.nan.bits                ) | \(Double.nan                .isNaN)  | \(Double.nan                .isSignalingNaN) âś…
       Double.signalingNaN | \(Double.signalingNaN.bits       ) | \(Double.signalingNaN       .isNaN)  | \(Double.signalingNaN       .isSignalingNaN) âś…
                 Float.nan | \( Float.nan.bits                ) | \( Float.nan                .isNaN)  | \(Float.nan                 .isSignalingNaN) âś…
        Float.signalingNaN | \( Float.signalingNaN.bits       ) | \( Float.signalingNaN       .isNaN)  | \(Float.signalingNaN        .isSignalingNaN) âś…
         Double(Float.nan) | \(Double(Float.nan).bits         ) | \(Double(Float.nan)         .isNaN)  | \(Double(Float.nan)         .isSignalingNaN) âś…
Double(Float.signalingNaN) | \(Double(Float.signalingNaN).bits) | \(Double(Float.signalingNaN).isNaN)  | \(Double(Float.signalingNaN).isSignalingNaN) âť“
                                        ^ Why is this bit set?
""")
Value Bit pattern isNaN isSignalingNaN
Double.nan 111111111111000000000000000000000000000000000000000000000000000 true false :white_check_mark:
Double.signalingNaN 111111111110100000000000000000000000000000000000000000000000000 true true :white_check_mark:
Float.nan 1111111110000000000000000000000 true false :white_check_mark:
Float.signalingNaN 1111111101000000000000000000000 true true :white_check_mark:
Double(Float.nan) 111111111111000000000000000000000000000000000000000000000000000 true false :white_check_mark:
Double(Float.signalingNaN) 111111111111100000000000000000000000000000000000000000000000000 true false :question:
-----------^ Why is this bit set?
1 Like

No, this is doing exactly what it’s supposed to do. Format conversion works like most other operations in that, upon encountering a signaling NaN, it signals an invalid operation floating-point exception and then evaluates with quiet NaN in place of the signaling NaN. This is specified by IEEE 754. (Swift doesn’t offer an API to observe floating-point exceptions.)

4 Likes

I suppose that makes sense.

What do you think the correct behaviour of a serialization framework should be here?

We have a wire protocol that only supports Double, but we want to allow our Encoder/Decoder to support Float (by pigging back off the support for Double).

Should we faithfully preserve the "signaling"ness of the Float, or make it quiet?

1 Like

Signaling NaNs are propagated as quiet NaNs by most floating-point operations, so unless you’re doing something very specific with floating-point exceptions that Swift doesn’t offer APIs for, I wouldn’t worry about it because almost any nontrivial manipulation of the value will lead to its being quieted at some point. By “don’t worry about it” I also mean, I would not rely on any specific behavior regarding signaling NaN for proper function of your code and would leave it deliberately underspecified. (Likewise NaN payloads.)

5 Likes

What Xiaodi said. If you're using signalingness as an extra semantic channel, you might want to preserve it, but:
(a) don't do that.
(b) no, don't do that.

4 Likes

I'm not, personally, I just want to be meet the expectations of users. "Principle of least astonishment" and all.

So, IEEE 754's guidance for any conversion operation on signaling NaNs (which would include conversion to/from a wire protocol) is that you either should preserve the signalingness of the NaN, or you should report that a signaling NaN was quieted. The usual mechanism for doing this is to raise the invalid flag, but Swift doesn't model FPU flags (largely because LLVM doesn't, yet, but also because the model is not really an ideal fit for floating-point operations outside of specific CPU models). If you have users who might benefit from this information, I would make it available to them in some out-of-band channel--an extra result parameter that they can ignore if they don't care, for example.

Hmmm, I'm on the fence.

As far as users are aware, there is no conversion going on, because they don't know that their floats are being wired across as doubles. That's a hidden implementation detail, but it's a leaky abstraction because of the way it removes the singallingness

There's still a "conversion" (in the view of IEEE 754) to a wire format.

1 Like

Would it be appropriate for you to wire the int32 bit pattern representation of float?