Notes on numerics in Swift

Concrete binary floating-point types, part 3

Integer literals (redux)

All floating-point types in Swift conform to the protocol ExpressibleByIntegerLiteral. Therefore, it’s possible to create a new floating-point value using an integer literal.

Earlier, nuances about the use of integer literals were discussed that apply equally when they are used to express floating-point values. Two issues are salient for our purposes here:

  1. Recall that integer literals do not support signed zero (in other words, -0 as Float evaluates to positive zero). Either use parentheses, as in -(0 as Float), or use a float literal as discussed below, to obtain the desired value.

  2. Recall that using a converting initializer with an integer literal argument, such as Double(42), is not recommended until implementation of SE-0213 because it first coerces the literal value to type IntegerLiteralType and then converts that value to type Double. This is to be contrasted with using a type coercion operator, such as 42 as Double, where the literal value is directly coerced to type Double.

Float literals

A float literal in Swift is similar to analogous expressions in other “C family” languages. It can be written in either base 10 or base 16 (hexadecimal).

Background:

In Swift, as in other “C family” languages, the whole part of a base 10 float literal can be followed by a fractional part beginning with a decimal separator dot (.), a decimal exponent beginning with e or E, or both (in that order). The digits of the exponent can optionally be preceded by - or +.

As with integer literals, float literals can be prepended with the hyphen-minus character (-) to indicate a negative value.

In Swift, a float literal cannot begin or end with a decimal separator dot. For example:

let x = .5
// error: '.5' is not a valid floating point literal; it must be written '0.5'
let y = 5.
// error: expected member name following '.'

(Instead, Swift uses leading dot syntax for implicit member lookup.)

The same requirement does not apply to conversions from String. For example, Double(".5")! == 0.5.

Hexadecimal float literals

Unfamiliar to some users, hexadecimal float literals (also specified in C99 and and C++17) are supported in Swift. They can be useful when you want to represent the intended binary floating-point value exactly and a decimal literal is impractical or impossible for the purpose.

Hexadecimal float literals use the base prefix 0x. Then, the whole part (in base 16) can optionally be followed by a fractional part (also in base 16) beginning with the separator dot (.). Finally, hexadecimal float literals must end with a binary exponent beginning with p or P. The digits of the exponent can optionally be preceded by - or +. For example:

let x = 0x1p2
// 1.0 * (2 ** 2) == 4
// Here, we use `**` to represent exponentiation.

let y = 0x1p-2
// 1.0 * (2 ** -2) == 0.25 

let z = 0x1.8p-1
// (1.0 + 8/16) * (2 ** -1) == 0.75

let a = 0xf.fffp-3
// (15.0 + 15/16 + 15/256 + 15/4096) * (2 ** -3)
//   == 1.999969482421875 

In C, the binary exponent is not optional to avoid ambiguity between the hexadecimal digit f and a suffix f indicating that the constant has type float. In Swift, the binary exponent isn’t optional even though there is no possibility of ambiguity.

As with integer literals, hexadecimal float literals can be prepended with the hyphen-minus character (-) to indicate a negative value.

In Swift, the portion of a hexadecimal float literal between the required base prefix and the required binary exponent cannot begin or end with the separator dot (.). At the time of writing, error messages are not particularly helpful in diagnosing the issue:

let x = 0x1.p2
// error: value of type 'Int' has no member 'p2'
let y = 0x.1p2
// error: '.' is not a valid hexadecimal digit (0-9, A-F) in integer literal
// error: 'p' is not a valid digit in integer literal
// error: consecutive statements on a line must be separated by ';'
// error: expected identifier after '.' expression

Again, the same requirement does not apply to conversions from String.

Type inference

As previously discussed, literals have no type of their own in Swift. Instead, the type checker attempts to infer the type of a literal expression based on other available information such as explicit type annotations.

Besides using an explicit type annotation, the type coercion operator as (which is to be distinguished from dynamic cast operators as?, as!, and is) can be used to provide information for type inference:

let x = 42.0 as Float

In the absence of other available information, the inferred type of a float literal expression defaults to FloatLiteralType, which is a type alias for Double unless it is shadowed by the user.

The following caveat applies to current versions of Swift. It will not be applicable after changes described in SE-0213: Integer initialization via coercion, which was implemented in July 2018, are included in a future Swift release.

A frequent misunderstanding found even in the Swift project itself concerns the use of a type conversion initializer to indicate the desired type of a literal expression. For example:

// Avoid writing such code.
let x = Float(42.0)

This usage frequently gives the intended result, but the function call does not provide information for type inference. Instead, this statement creates an instance of type FloatLiteralType (which again, by default, is a type alias for Double) with the value 42.0, then converts that value to Float.

Since Float has less precision than Double, a literal value is rounded twice when that statement is evaluated, which can lead to double-rounding error:

let correct = 8388608.5000000001 as Float
// 8388609
let incorrect = Float(8388608.5000000001)
// 8388608

Since Float80 has more precision than Double, the same misunderstanding causes loss of precision in floating-point values analogous to omission of the suffix l in C/C++ (which must be used to indicate that a constant should have long double type):

let precise = 3.14159265358979323846 as Float80
// 3.14159265358979323851
let imprecise = Float80(3.14159265358979323846)
// 3.141592653589793116

Float literal precision

Notionally, a numeric literal isn’t limited by the precision of any type because it has no type.

Under the hood, however, an integer literal is first used to create an internal 2048-bit value (of type _MaxBuiltinIntegerType) that is then converted to the intended type. Likewise, a float literal is first used to create an internal value of type _MaxBuiltinFloatType that is then converted to the intended type.

This design is more or less sufficient for integer literals (except that signed zero cannot be supported) because integers with more than 600 decimal digits can be represented in 2048 bits.

As of the time of writing, float literals may be incorrectly rounded because _MaxBuiltinFloatType is a type alias for Float80 if supported and Double otherwise. Consequently, float literals that cannot be represented exactly as a value of type _MaxBuiltinFloatType are subject to double-rounding error just as though the value were created using a converting initializer.

Hexadecimal float literals of no more than the maximum supported precision can be used to avoid this double-rounding error for binary floating-point types.

Double rounding of float literals is tracked by Swift bug SR-7124: Double rounding in floating-point literal conversion.

Since _MaxBuiltinFloatType is a binary floating-point type, a decimal floating-point type that conforms to the protocol ExpressibleByFloatLiteral cannot distinguish between two values that have the same binary floating-point representation when rounded to fit _MaxBuiltinFloatType:

import Foundation

(0.1 as Decimal).description
// "0.1"
(0.10000000000000001 as Decimal).description
// "0.1"

Decimal(string: "0.1")!.description
// "0.1"
Decimal(string: "0.10000000000000001")!.description
// "0.10000000000000001"

Conversions among floating-point types

Two different initializers are provided for conversions between standard library binary floating-point types. A value of source of type T can be converted to a value of type U as follows:

  1. U(source)
    Converts the given value to a representable value of type U.
    The result of an inexact conversion is rounded to the nearest representable value.
    The result of an overflowing conversion is infinite.
    The result of an underflowing conversion is zero.
    The result of converting NaN is some encoding of NaN that varies based on the underlying architecture; any signaling NaN is always converted to a quiet NaN.

  2. U(exactly: source)
    Failable initializer.
    Converts the given value if it can be represented “exactly” as a value of type U; any result that is not nil can be converted back to a value of type T that compares equal to source.
    The result of an inexact conversion is nil.
    The result of an overflowing conversion is nil.
    The result of an underflowing conversion is nil.
    The result of converting NaN (however encoded) is nil, since NaN never compares equal to NaN.

Other initializers

Conversions between integer types and floating-point types

Incomplete

Creating from a string

Standard library binary floating-point types provide an unlabeled failable initializer that creates a binary floating-point value based on a given string:

let pi = Double("3.14159265358979323846")!
// 3.1415926535897931

Any spelling that’s valid as an integer or float literal is valid as a string for conversion to a binary floating-point type. Likewise, any value obtained from the description or debugDescription property of a binary floating-point value is valid for conversion. Specifically:

Any string that would cause a range error when it is used as the argument of the C function strtof or strtod causes Float.init?(_: String) or Double.init?(_: String) (respectively) to return nil. Therefore:

Any invalid character, even if whitespace, causes the entire string to be invalid for conversion.
The result of an inexact conversion is rounded to the nearest representable value.
The result of an overflowing conversion is nil.
The result of an underflowing conversion is nil.
The result of converting NaN is encoded with the NaN payload (truncated if needed) if such a payload is specified.

Although an integer literal beginning with a leading zero isn’t considered to be written in base 8 (octal), a NaN payload beginning with a leading zero is considered to be written in base 8:

let x = Double("nan(123)")!
let y = Double("nan(0123)")!

String(x.bitPattern, radix: 16) // "7ff800000000007b"
String(y.bitPattern, radix: 16) // "7ff8000000000053"

String(123, radix: 16)          // "7b"
String(0o123, radix: 16)        // "53"

Some rules are more relaxed for string conversion than for float literals: a digit is not required to precede or follow the separator dot, and a binary exponent is not required to end a hexadecimal value:

let x = Double(".5")!
// 0.5
let y = Double("5.")!
// 5
let z = Double("0x1.p2")!
// 4
let a = Double("0x.1p2")!
// 0.25
let b = Double("0x1.")!
// 1

Creating from a sign, exponent, and significand

Incomplete


Previous:
Concrete binary floating-point types, part 2

Next:
Concrete binary floating-point types, part 4

Draft: 27 February–14 March 2018
Updated 19 August 2018