Notes on numerics in Swift

Concrete integer types, part 2

Operator precedence

In C, operators evolved over time, and their precedence is a product of that historical legacy. C++, Java, and other “C family” languages have generally preserved the relative precedence of operators found in C.

In Swift, operator precedence has been rationalized. The resulting precedence table is similar to that of Go. Consequently, an integer expression in Swift can appear to be similar to an expression in another language but evaluate quite differently:

var v = 42
v = v + (v >> 4) & 0x0F0F0F0F
// In Swift (and Go), `v` is equal to 44.
var v = 42
v = v + (v >> 4) & 0x0F0F0F0F
// In JavaScript, `v` is equal to 12.

The relative precedence of infix operators common to both C and Swift can be compared as follows:

C Swift
  << >>
* / % * / % &
+ - + - ^ |
<< >>  
< <= > >= < <= > >= == !=
== !=  
&  
^  
|  
&& &&
|| ||
= and other assignment operators = and other assignment operators

Overflow behavior

Rust now takes a similar approach to handling integer overflow; therefore, Swift users may find a write-up about Rust’s design evolution to be useful.

As previously mentioned, Swift’s standard arithmetic operators trap on integer overflow. Integer overflow checking is disabled in -Ounchecked mode, which is not recommended for general use.

A runtime error also occurs in case of overflow in methods such as:

abs(_:)
negate()
dividingFullWidth(_:)
quotientAndRemainder(dividingBy:)

At the time of writing, dividingFullWidth(_:) does not behave as documented in case of overflow.

Absolute value and magnitude

In Swift, abs(_:) returns a value of the same type as the argument. Specifically, the function returns the absolute value of the argument. Therefore, for a signed (and fixed-width) integer type T, a runtime error occurs when evaluating abs(T.min) because the result cannot be represented in T.

By contrast, evaluating T.min.magnitude does not cause a runtime error. However, the value is not always of type T but rather of the associated type T.Magnitude. For a signed type, T.Magnitude is the unsigned type of the same bit width; for an unsigned type, T.Magnitude == T.

Overflow operators

In C, the result of an unsigned integer operation that is too large to be represented “wraps around” (that is, the return value consists of the least significant bits of the result), while signed integer overflow is undefined behavior.

Sometimes, “wrapping” behavior can be desired in Swift (for example, when performing bitwise manipulations). The Swift standard library offers alternative facilities that can be used in these circumstances.

Three overflow operators allow the user to choose C-like “wrapping” behavior instead of trapping on overflow: &+ (overflow addition), &- (overflow subtraction), and &* (overflow multiplication). The behavior of these operations is fully defined for both unsigned and signed integer types.

In March 2018, the three corresponding overflow assignment operators (&+=, &-=, and &*=) were added to Swift. They are, of course, “wrapping” counterparts to assignment operators spelled without a leading &.

The “overflow” operators &/ and &% were removed in Swift 1.2 because they did not provide two’s complement behavior like other overflow operators.

Methods reporting overflow

Five methods reporting overflow are provided, largely analogous to Rust’s overflowing_* methods:

addingReportingOverflow(_:)
subtractingReportingOverflow(_:)
multipliedReportingOverflow(by:)
dividedReportingOverflow(by:)
remainderReportingOverflow(​dividingBy:)

In general, these operations return a tuple of a numeric value and a Boolean value. The numeric value is either the entire result if no overflow occurred during the operation or the “wrapped” partial result if overflow occurred; the Boolean value indicates whether or not overflow occurred.

Some caveats:

In Swift, x.dividedReportingOverflow(by: 0) is documented to return (x, true). Nonetheless, at time of writing, a division-by-zero error occurs if the right-hand side (RHS) is expressed as a literal 0:

let x = 42
let y = 0
x.dividedReportingOverflow(by: y)
// (partialValue: 42, overflow: true)

x.dividedReportingOverflow(by: 0)
// error: division by zero

In Swift, x.remainderReportingOverflow(​dividingBy: 0) returns (x, true), as the remainder is mathematically undefined. Otherwise, if the operation overflows (which only occurs when dividing by -1), the method returns (0, true). Mathematically, of course, the remainder of division by −1 is always zero. At the time of writing, a division-by-zero error occurs if the RHS is expressed as a literal 0.

Internally, there are no LLVM primitives for checking overflow after division, so checking is implemented in native Swift.

Prior to Swift 4.2, remainderReportingOverflow(​dividingBy:) did not return the correct remainder when dividing by -1. The behavior was fixed in early 2018.

Unsafe methods

Four unsafe methods were once provided:

unsafeAdding(_:)
unsafeSubtracting(_:)
unsafeMultiplied(by:)
unsafeDivided(by:)

The behavior of those methods was undefined in case of overflow. Therefore, they were useful only for avoiding the performance cost of overflow checking, and they were meant to be used only if it was certain that the result would not overflow. (In debug mode, however, overflow did cause a precondition failure.)

All four unsafe methods have been removed for Swift 5, as they were never approved as part of a proposal.

Full-width methods

Two primitive operations are exposed by the Swift standard library that can be useful for implementation of an arbitrary-width integer type:

multipliedFullWidth(by:) returns a tuple of the high and low parts of a product that overflows standard multiplication.

dividingFullWidth(_:) returns the quotient and remainder after the argument (a double-width value expressed as a tuple of high and low parts) is divided by the receiver. As mentioned above, a runtime error may occur if the quotient is not representable within the bounds of the type.

Notice that this method is named “dividing” instead of “divided by.” Here, the argument is the dividend (i.e., numerator). Although unique among Swift arithmetic operations, this arrangement is necessary because the dividend is a tuple; tuple types can’t themselves be extended in Swift.

At the time of writing, the implemented behavior of dividingFullWidth(_:) in case of overflow does not match the documented behavior.

Integer remainder

The remainder operator % (known as the modulo operator in other languages) adopts the same truncated division convention observed in many “C family” languages, including C99, C++11, C#, D, Java, JavaScript, and Rust. The result has the same sign as the dividend.

Evaluating x % 0 results in a division-by-zero error.

Bitwise operations

Every integer value has the instance property bitWidth, which is the number of bits in the binary representation of the value. All standard library integer types have a fixed bit width; fixed-width integer types have a static property bitWidth which, unsurprisingly, is equal to the instance property bitWidth for any value of that type.

In generic code, it can be useful to work with the bit width of a fixed-width integer type without having to instantiate an instance of that type. A key overarching goal of Swift’s protocol-based designs is to enable useful generic algorithms; this is the reason why fixed-width integers have an instance property and a static property that are equal in value.

The following properties of a value’s binary representation are also available in Swift:

Swift 4+ now has two sets of bit shifting operators to avoid undefined behavior in the case of overshift or undershift:

The result of a bit shift is always of the same type as the left-hand side (LHS). In Swift 4+, the type of the right-hand side (RHS) does not need to match that of the LHS.

In C, Java, and Rust, bit shifting operators are left-associative just like multiplication or addition operators. In Swift, bit shifting operators are non-associative. This means that parentheses are always required when an operand is both preceded and followed by bit shifting operators:

let x = 1 << 2 << 3
// error: adjacent operators are in non-associative precedence group
// 'BitwiseShiftPrecedence'

Smart shifts

As in Java, Go, and other languages, >> is a right arithmetic shift for signed integers. In other words, the result has the same sign bit as the LHS.

Undershift occurs when the RHS is negative. A right smart shift by a negative RHS value x is equivalent to a left smart shift by x.magnitude, and vice versa. For example, x >> -42 is equivalent to x << 42.

Overshift occurs when the RHS equals or exceeds the bit width of the LHS. A left smart shift by such a value is equivalent to filling in each bit of the result with zero; in other words, the result is 0. A right smart shift by such a value is equivalent to filling each bit of the result with the LHS sign bit; in other words, the result is either -1 if the LHS is negative or 0 if the LHS is non-negative.

Masking shifts

Masking shifts, introduced in Swift 4, offer an alternative when branches for handling undershift and overshift in smart shifts cannot be optimized away by the compiler and are of concern to performance.

In Rust, the same operations are known as wrapping shifts.

To obtain the result, the RHS is preprocessed to ensure that it is in the range 0..<lhs.bitWidth. For a LHS of arbitrary bit width, preprocessing the RHS requires computing the modulus after floored division. In other words, the amount by which to shift the LHS can be obtained as follows:

var shift = rhs % lhs.bitWidth
if shift < 0 { shift += lhs.bitWidth }

When the bit width of the LHS is a power of two (as is the case for all standard library types), the same result can be obtained using a bitwise operation:

var shift = rhs & (lhs.bitWidth - 1)

On most architectures, masking is performed by the CPU’s shift instructions and therefore incurs no additional performance cost.

Masking shifts and smart shift semantics were introduced as part of the Swift Evolution proposal SE-0104: Protocol-oriented integers.


Previous:
Concrete integer types, part 1

Next:
Concrete binary floating-point types, part 1

27 February–10 March 2018
Updated 6 July 2019