Skip to content

Latest commit

 

History

History
211 lines (172 loc) · 8.62 KB

0425-int128.md

File metadata and controls

211 lines (172 loc) · 8.62 KB

128-bit Integer Types

Motivation

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).

Proposed solution

Introduce two new structs, UInt128 and Int128, conforming to all of the usual fixed-width integer protocols.

While these will be implemented in the standard library, for the purposes of experimentation, I have put them on a branch of swift-numerics. To try these types out, use my branch int128:

.package(
  url: "https://github.com/stephentyrone/swift-numerics",
  branch: "int128"
)

and add Int128Demo as a dependency:

.target(name: "MyTarget", dependencies: [
  .product(name: "Int128Demo", package: "swift-numerics")
]),

This branch requires a recent nightly toolchain to build.

Detailed design

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."

Codable details

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.

Source compatibility

This proposal has no effect on source compatibility.

ABI compatibility

This proposal has no effect on ABI compatibility.

Implications on adoption

Adopting this feature will require a target with runtime support.

Future directions

Implement clang importer support for _BitInt(128) on any platforms where the finalized ABI is compatible with our layout.

Alternatives considered

Alignment and _BitInt() types

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.

Generic-sized fixed width integers

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.

NSNumber bridging

[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.

Codable errors

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.