Swift Digest
SE-0368 | Swift Evolution

StaticBigInt

Proposal
SE-0368
Authors
Ben Rimmington
Review Manager
Doug Gregor, Holly Borla
Status
Implemented (Swift 5.8)

01 何が問題だったのか

Swiftのソースコード上、整数リテラルは任意の大きさの値を書くことができます。しかし、自作の整数型を整数リテラルで初期化できるようにしようとすると、実用上は標準ライブラリに存在する整数型の範囲までしか扱えないという制約がありました。

整数リテラルで初期化可能な型は ExpressibleByIntegerLiteral に適合させます。

public protocol ExpressibleByIntegerLiteral {
  associatedtype IntegerLiteralType: _ExpressibleByBuiltinIntegerLiteral
  init(integerLiteral value: IntegerLiteralType)
}

init(integerLiteral:) で受け取る値の型は _ExpressibleByBuiltinIntegerLiteral に適合している必要があり、このプロトコルはコンパイラと低レベルにやりとりする都合上、標準ライブラリの外では適合させられません。標準ライブラリ側で適合しているのは Int / UIntInt64 / UInt64 などの固定幅整数型だけなので、標準ライブラリ外の型はそれらのいずれかを IntegerLiteralType に選ばざるを得ず、その型に収まらない大きさのリテラル値は受け取れません

たとえば Swift Numerics パッケージに UInt256 のような大きな固定幅整数型を追加したとしても、IntegerLiteralTypeUInt64 を使っている限り、UInt64 に収まらないリテラルはコンパイルエラーになってしまいます。

let value: UInt256 = 0x1_0000_0000_0000_0000
//                   ^
// error: integer literal '18446744073709551616' overflows when stored into 'UInt256'

このため、標準ライブラリの外で BigIntUInt256DoubleWidth のような、Int64 / UInt64 を超える大きさを扱う整数型を書くのが難しい状況になっていました。

02 どのように解決されるのか

標準ライブラリに StaticBigInt 型を追加します。これは任意精度の符号付き整数を表し、ExpressibleByIntegerLiteralIntegerLiteralType として使えます。自作の整数型側では init(integerLiteral:)StaticBigInt を受け取り、その値から自分のビット表現を組み立てることで、リテラルの大きさに縛られずに整数リテラルからの初期化を実現できます。

extension UInt256: ExpressibleByIntegerLiteral {

  public init(integerLiteral value: StaticBigInt) {
    precondition(
      value.signum() >= 0 && value.bitWidth <= Self.bitWidth + 1,
      "integer literal '\(value)' overflows when stored into '\(Self.self)'"
    )
    self.words = Words()
    for wordIndex in 0..<Words.count {
      self.words[wordIndex] = value[wordIndex]
    }
  }
}

init(integerLiteral:) の実装では、Self 型のリテラルを使う API(Self に戻ってくるような算術など)を呼ばないよう注意が必要です。呼んでしまうと再帰的に init(integerLiteral:) が呼ばれ、無限再帰になります。

StaticBigInt の性質

StaticBigInt は数学的な整数をモデル化したもので、ソース上の基数(2進・16進など)や先頭のゼロといった情報は失われています。値は定数として与えられるものであり、ランタイムで新しい StaticBigInt の値を構築することはできません。そのため、Numeric などの数値プロトコルには適合しておらず、整数値を取り出すための最小限のAPIしか公開されていません。

提供されるAPI

public struct StaticBigInt:
  CustomDebugStringConvertible,
  CustomReflectable,
  _ExpressibleByBuiltinIntegerLiteral,
  ExpressibleByIntegerLiteral,
  Sendable
{
  /// 符号を返します。負なら -1、0 なら 0、正なら +1。
  public func signum() -> Int

  /// 2の補数表現での最小ビット幅(符号ビットを含み、符号拡張は除く)。
  public var bitWidth: Int { get }

  /// 値の2進表現を 32bit または 64bit のワード単位で取り出します。
  /// 下位ワード(index 0)から上位へ向かって並び、`bitWidth` を超えた先は
  /// 無限に符号拡張された状態として扱えます。負の値は2の補数表現です。
  public subscript(_ wordIndex: Int) -> UInt { get }
}

ワードの添字アクセスは「無限に符号拡張された」モデルになっていて、自分の型に必要な個数分だけ下位から順に読み出していけば、足りない上位ワードは signum() に応じて 0x0000... または 0xFFFF... として取得できます。たとえば 118 ビットで収まる負の値に対しては次のように振る舞います。

let negative: StaticBigInt = -0x0011223344556677_8899AABBCCDDEEFF
negative.signum()  // -1
negative.bitWidth  // 118
negative[0]        // 0x7766554433221101
negative[1]        // 0xFFEEDDCCBBAA9988
negative[2]        // 0xFFFFFFFFFFFFFFFF(以降の index もすべて 0xFFFF...)

bitWidthsignum() を使えば、受け取った値が自分の型の範囲に収まっているかをチェックできます。上の UInt256 の例のように、signum() >= 0bitWidth <= Self.bitWidth + 1 を確認したうえでワードを取り出していく、というのが典型的な実装パターンです。

想定される利用者

StaticBigInt はあくまで「標準ライブラリ外で大きな整数型を実装したい」ライブラリ作者向けの道具です。アプリケーション側で普段の整数リテラルの扱いが変わることはありません。Swift Numerics の BigIntUInt256DoubleWidth のような型を ExpressibleByIntegerLiteral に自然に適合させるのが主な用途です。

ABIと配備

StaticBigInt は標準ライブラリのABIに追加されるため、デフォルトではバックデプロイされません。また、整数リテラルの型(IntegerLiteralType)は associatedtype として静的に選ぶ必要があり、実行環境によって動的に切り替えることはできません。ライブラリ作者は自分の型に対してどのリテラル型を使うかを一度だけ決めて適合させる形になります。

小数リテラルなどは対象外

StaticBigInt は整数を表すための型で、小数点を含むリテラルや、16進で書かれた任意のビット列、といった用途には使えません。整数型を小数リテラルから構築できてしまうのは望ましくないため、これは意図的な制限です。浮動小数点リテラルを任意精度で扱う仕組みは将来的な課題として残されていますが、本提案のスコープ外です。