- Proposal: SE-0425
- Author: Stephen Canon
- Review Manager: Doug Gregor
- Status: Implemented (Swift 6.0)
- Review: (Pitch) (Review), (Acceptance)
128b integers are the largest fixed-size type that is currently commonly used in "general-purpose" code. They are much less common than 64b types, but common enough that adding them to the standard library makes sense. We use them internally in the standard library already (e.g. as an implementation detail of Duration).
Introduce two new structs, UInt128
and Int128
, conforming to all of the
usual fixed-width integer protocols.
The [U]Int128
types are 16B aligned on 64b targets¹ and have the same
alignment as [U]Int64
on 32b targets. They will match the endianness of
all other integer types.
The clang importer will be updated to bridge __uint128_t
to UInt128
and
__int128_t
to Int128
. We will not bridge _BitInt()
types until
the ABI problems with those types have been clearly resolved (see Alternatives
Considered for sordid history).
The [U]Int128
types conform to AtomicRepresentable
on targets with
_hasAtomicBitWidth(_128)
set (notably x86_64, arm64, and arm64_32).
The actual API of the types is uninteresting; they are entirely constrained by their protocol conformances. Notably, these types conform to the following protocols, and hence to any protocol that they refine:
- Hashable
- Equatable
- Comparable
- Codable
- Sendable
- LosslessStringConvertible
- ExpressibleByIntegerLiteral
- AdditiveArithmetic
- [Signed]Numeric
- BinaryInteger
- FixedWidthInteger
- [Unsigned|Signed]Integer
¹ For the purposes of this discussion, arm64_32 and similar architectures are "64b targets."
An earlier version of this proposal conformed [U]Int128
to Codable
with a representation as a pair of [U]Int64
values. During the first review
period several people made excellent points:
-
Making it possible for encoders to customize how they represent these types is desirable. Some cannot represent all 64b values or might prefer to use a string representation, others might prefer to treat 128b integers as native values to encode.
-
If we make it customizable but provide a default behavior, some decoders would have to support that default as well as their desired representation, for compatibility with any encodings created between when we added support and when they defined their preferred encoding.
For this reason, the proposal has been updated to add new protocol requirements
for encoders and decoders to support [U]Int128
, but with default
implementations that throw an EncodingError or DecodingError unconditionally,
allowing implementations to choose their preferred behavior when they add
support without worrying about compatibility with a defaulted implementation.
Thus, the following requirements will be added:
protocol KeyedEncodingContainerProtocol {
mutating func encode(_ value: Int128, forKey key: Key) throws
mutating func encode(_ value: UInt128, forKey key: Key) throws
mutating func encodeIfPresent(_ value: Int128?, forKey key: Key) throws
mutating func encodeIfPresent(_ value: UInt128?, forKey key: Key) throws
// And matching changes to KeyedEncodingContainer<Key>
}
protocol KeyedDecodingContainerProtocol {
func decode(_ type: Int128.Type, forKey key: Key) throws -> Int128
func decode(_ type: UInt128.Type, forKey key: Key) throws -> UInt128
func decodeIfPresent(_ type: Int128.Type, forKey key: Key) throws -> Int128?
func decodeIfPresent(_ type: UInt128.Type, forKey key: Key) throws -> UInt128?
// And matching changes to KeyedDecodingContainer<Key>
}
protocol UnkeyedEncodingContainer {
mutating func encode(_ value: Int128) throws
mutating func encode(_ value: UInt128) throws
mutating func encode<T: Sequence>(
contentsOf sequence: T
) throws where T.Element == Int128
mutating func encode<T: Sequence>(
contentsOf sequence: T
) throws where T.Element == UInt128
}
protocol UnkeyedDecodingContainer {
mutating func decode(_ type: Int128.Type) throws -> Int128
mutating func decode(_ type: UInt128.Type) throws -> UInt128
mutating func decodeIfPresent(_ type: Int128.Type) throws -> Int128?
mutating func decodeIfPresent(_ type: UInt128.Type) throws -> UInt128?
}
protocol SingleValueEncodingContainer {
mutating func encode(_ value: Int128) throws
mutating func encode(_ value: UInt128) throws
}
protocol SingleValueDecodingContainer {
func decode(_ type: Int128.Type) throws -> Int128
func decode(_ type: UInt128.Type) throws -> UInt128
}
and given default implementations. The default encode implementations throw
EncodingError.invalidValue
, and the default decode implementations throw
DecodingError.typeMismatch
.
This proposal has no effect on source compatibility.
This proposal has no effect on ABI compatibility.
Adopting this feature will require a target with runtime support.
Implement clang importer support for _BitInt(128)
on any platforms where
the finalized ABI is compatible with our layout.
Clang and GCC have historically exposed the extension types __uint128_t
and
__int128_t
on 64b platforms only. These types basically behave like C
builtin integer types--their size and alignment are 16B.
The C23 standard introduces _BitInt(N)
as a means to spell arbitrary-width
integer types, but these still have some warts. In particular, _BitInt(128)
as implemented in clang has 8B alignment on x86_64 and arm64. For arm64,
this is clearly a bug; the AAPCS specifies that it should have 16B alignment.
For x86_64, the situation is less clear. The x86_64 psABI document specifies
that it should have 8B alignment, but the authors of the proposal that added
the feature tell me that it should be 16B aligned and that they are
attempting to change the psABI.
We would like to be layout-compatible with _BitInt(128)
on all platforms,
but given the currently-murky state of the layout of those types, it makes
the most sense to guarantee compatibility with the widely-used but non-
standard __[u]int128_t
and find mechanisms to make _BitInt(128)
work
once its ABI has been finalized on Swift's targeted platforms.
Rather than adding [U]Int128
, we could implement some form of generic-
sized fixed-width integer (like \_BitInt()
in C). Given both the lack
of consensus around what integer generic parameters ought to look like in
Swift (or if they ought to exist at all), and the growing pains that
\_BitInt()
is currently going through, such a design would be premature.
While other fixed-width integer types are interesting, 128 bits is a couple
orders of magnitude more useful than all the others for general-purpose
software at this point in time.
[U]Int128
will not bridge to NSNumber
. In the future, Swift will need
a careful rethinking of how best to handle type-erased numbers, but we don't
want to pile on the debt by including ever more types in an existing system
that isn't supported on all platforms. In addition, the most common use for
such bridging, unpacking type-erased fields from encoded dictionaries, is
somewhat moot since most existing coders do not support 128b integers. We
will likely revisit this more holistically in the future.
It would be nice to introduce new unsupportedType
error cases for the
default Codable conformances, but we cannot add new cases with associated
values and constrained availability to existing enums, which prevents
attaching context or a useful debug description. It's more useful for users
if we use existing error cases invalidValue
and typeMismatch
but put an
actionable message in that description field.