Curious NaN formatting inconsistency

Ran into some inconsistent behaviour around signed nan values, and not sure whether this is expected behaviour, a formatting inconsistency, or a bug:

let value: Double = -Double.nan

print(-0.0 / 0.0)                   //  nan
print(-(0.0 / 0.0))                 // -nan
print(value.description)            //  nan
print(value)                        // -nan
print(String(describing: value))    //  nan
print(value.debugDescription)       // -nan
ctest() // printf("%f\n", -NAN);    //  nan

Is this inconsistency unintended?


Edit:

Another one:

print(-0.0 * -.infinity) // -nan
print( 0.0 * -.infinity) // -nan
// yet
print(-Double.infinity / .infinity) // nan

There's not one "this" here.
It is intended that the result of negation is distinct from the result of division by zero.
It is intended that debug description shows the sign of NaN and description does not.
It is a bug that arose in 6.2, not 6.1, that print(value) differs from print(value.description).

1 Like

I noticed a similar issue in Swift 6.0, and tried to fix it with swiftlang/swift#76634, but the pull request was ignored.

There was another minor issue which might still be relevant. An overload of String.init(describing:) uses description rather than write(to:).

Right—this overload, like print, is referenced in the documented guarantee for CustomStringConvertible:

If the passed instance conforms to CustomStringConvertible , the String(describing:) initializer and the print(_:) function use the instance’s custom description property.

Because print in reality favors TextOutputStreamable.write over description, this documented guarantee effectively forces write(to:) and description to have matching behaviors. So long as conforming types uphold that contract, which now we do again for floating-point types, it's fine.

Even if a conforming type doesn't uphold that contract, it's explicitly documented that String(describing:) uses the description property, so the overload is absolutely right as-is. (By contrast, print would surface the discrepancy, as here.)

Are you sure? I expected from the documentation that TextOutputStreamable has precedence over CustomStringConvertible.

Also, there's a similar appendInterpolation(_:) overload that uses write(to:).

In any case, the issue is unlikely to be noticed, because so few types conform to TextOutputStreamable.

Ah, well then the documentation is internally inconsistent :man_shrugging: and the only way to square the circle is if the two implementations are indistinguishable—which, again, print already forces.

I'm pretty sure I introduced the behavior difference between init(describing:) and appendInterpolation(_:). My thought was to use whichever API would be most efficient: appendInterpolation(_:) is appending text to a larger buffer, so calling write(to:) allows the type to append text directly to the buffer rather than accumulating it into a temporary string, while init(describing:) is returning text as a String, so calling description allows the type to just directly return a String if it already has one rather than appending it to a temporary buffer.

In either case, I assumed that both conformances would have the same observable behavior. If we don't actually document a requirement that CustomStringConvertible and TextOutputStreamable produce the same string, we probably ought to.