Literal expression
Literal Expressions
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swift には、整数リテラル「そのもの」しか書けない構文上の場所がいくつかあります。具体的には次の 3 つです。
これらの場所には、4096 のように事前に計算済みのリテラル値を書く必要がありました。「4 * 1024 だから 4096 だ」とか「16 * pageSize だから 65536 だ」といった、コードの意図を表しているはずの式は受け付けてもらえず、書き手の頭の中で計算した結果だけが残ります。これは次のような問題を生みます。
@section の初期化が「魔法の数」になる
@section 属性は組み込みやシステム向けの用途を意識して導入されたものですが、その文脈では「ページサイズ」「レジスタオフセット」「プロトコルのフィールド幅」のように、他の定数から導出される値を扱う場面が多くあります。今の @section ではこうした派生を式として書けません。
@section("__DATA,config") let pageSize = 4096
@section("__DATA,config") let bufferSize = 65536
@section("__DATA,config") let c = 1 + 1 // error: 演算子は使えない
bufferSize が「pageSize の 16 倍」であることはソースからは読み取れず、C ヘッダの PAGE_SIZE のような名前付き定数も引っ張ってこられないため、数値を手でコピーする運用になってしまいます。
enum の raw value にビット演算が書けない
ビットフラグのような enum では、各ケースの raw value を 1 << 0、1 << 1 のように書くのが自然ですが、現状はリテラル直書きしか許されません。
enum Permissions: Int {
case read = 1
case write = 2
case execute = 4
}
4 * 1024 のような派生も同じ事情で書けず、コメントに「4 * 1024」と書き添えるしかない状況でした。これは C / C++ の enum では昔から普通にできていたことなので、Swift の利用者にとっても自然に期待する書き方です。
整数 generic 引数も「裸のリテラル」のみ
SE-0452 で整数を generic 引数に渡せるようになりましたが、こちらも裸の整数リテラル限定で、他の定数からの派生を書けません。
let schemaRowSize = 32
// 本当に書きたいのは: InlineArray<(2 * schemaRowSize), UInt8>
let buffer: InlineArray<64, UInt8> // 64 == 2 * 32 であることを祈る
schemaRowSize を変えるたびに、それに依存する InlineArray<...> のサイズをコードベース全体から手で探して直す必要があり、コンパイラの助けはありません。SE-0452 自身も “use of constant bindings as generic parameters” や “arithmetic in generic parameters” を future direction として挙げていました。
これら 3 つに共通するのは、プログラマが手計算で値を出してリテラルとして転記しなければならない ことです。なぜその数になるのかという意図がソースから消えてしまい、読みにくく、メンテナンスしにくく、レビューもしにくいコードを生みます。
02 どのように解決されるのか
「コンパイル時に 1 つの整数値に畳み込める式」を literal expression と呼ぶ概念として導入し、これまで「裸のリテラル」しか受け付けなかった 3 つの場所(@section 変数の初期化式、enum の raw value、整数 generic 引数)を literal expression に拡張します。畳み込みはフロントエンドだけで完結し、生成される成果物は手で計算済みのリテラルを書いた場合と完全に同一です。ABI への影響もありません。
literal expression は実験的機能フラグ LiteralExpressions の背後に置かれます。
literal expression の文法と対応演算子
literal expression は、標準ライブラリの整数型の値を返す式のうち、次の文法で表せるものです。
literal-expression → integer-literal
literal-expression → unary-operator literal-expression
literal-expression → literal-expression binary-operator literal-expression
literal-expression → '(' literal-expression ')'
literal-expression → identifier
サポートされる二項演算子は次のとおりです。
- 算術:
+-*/% - ラップする算術:
&+&-&* - ビット演算:
&|^ - シフト:
<<>> - マスクシフト:
&<<&>>
単項前置演算子は + - ~ です。ラップなしの算術はコンパイル時にオーバーフローを診断します。ラップする算術は宣言された型のビット幅で剰余を取った結果を返し、マスクシフトは結果型のビット幅でシフト量に剰余を取ります(いずれも実行時のセマンティクスと一致)。優先順位と結合性は Swift 通常の規則どおりです。
literal expression として認められる演算子は、名前解決の結果が「標準ライブラリの演算子で、標準ライブラリの整数型に対するもの」である場合だけです。ユーザー定義のオーバーロードに解決される式は、たとえスコープに合致する標準ライブラリのオーバーロードがあったとしても、literal expression としては拒否されます。これは「literal expression に畳み込んだ結果が、その式を実行時に評価した値と必ず一致する」ことを保証するための制約です。
結果の型は標準ライブラリの整数型に限られます: Int / Int8 / Int16 / Int32 / Int64 / Int128 / UInt / UInt8 / UInt16 / UInt32 / UInt64 / UInt128。
let a = 4 * 1024 // OK: 算術
let b = 1 << 12 // OK: シフト
let c = (0xFF & mask) | base // OK: ビット演算と括弧
let d = -1 // OK: 単項マイナス
let w: UInt8 = 250 &+ 10 // OK: ラップ加算、4 に畳み込まれる
let e = Int.max / 2 // error: プロパティアクセスは未対応
let f = a +% b // error: ユーザー定義演算子は未対応
他の変数を名前で参照できる
literal expression からは、別の変数を識別子として参照できます。参照対象は、次の条件を満たす Swift の let 束縛である必要があります。
- デフォルトの初期化式を持っており、その初期化式自体も literal expression である
- アクセスレベルが
internal/fileprivate/privateのいずれか(public/package/openは不可)
参照は再帰的に解決されます。コンパイラは参照先の初期化式を畳み込み、得られたリテラル値を使って外側の式を畳み込みます。参照先に特別な注釈は不要で、初期化式の形だけから「畳み込めるかどうか」を判定します。
public / package / open への参照を許さないのは、参照を畳み込んでしまうと、参照先の初期化式の値がそのモジュールの ABI 表面の一部としてクライアントに焼き付くことになるためです。literal expression は「ABI に影響を与えない」という設計方針を取っているので、公開された変数への参照は別の opt-in 機構が用意される将来の Proposal に委ねられます。
モジュールスコープや static の let、@section / @objc / @c が付いた変数のほか、C / Objective-C / C++ からインポートされた定数も参照可能です。コンパイラから「定数で初期化されている」と見えるもの、たとえば C の static const int や、整数値の素朴な #define マクロは、そのまま literal expression に組み込めます。
let pageSize = 4 * 1024
let bufferSize = 16 * pageSize // OK: pageSize を参照
import CSystem
let systemBuffer = 4 * SYSTEM_PAGE_SIZE // OK: C 定数
var mutableSize = 4096
let derived = 2 * mutableSize // error: var は参照できない
let computed: Int = { 4096 }() // error: 初期化式が literal expression ではない
let derived2 = 2 * computed
@section 変数の初期化式
@section 付き変数の初期化式は、裸のリテラルから literal expression へ拡張されます。コンパイラが式を 1 つのリテラル値に畳み込み、static な初期化とセクション配置にそのまま使います。
@section("__TEXT,config") let pageSize = 4 * 1024
@section("__TEXT,config") let bufferSize = 16 * pageSize
@section("__TEXT,config") let systemPage = PAGE_SIZE // C 定数
元の式は AST 上に保持され、診断や IDE のインデクシングに利用されます。一方で module interface(.swiftinterface)には @section 変数の初期化式は出力されないため、元の式が公開ヘッダ的なものに漏れることはありません。
literal expression として畳み込めない式が書かれた場合、コンパイラはエラーを出します。
@section("__TEXT,config") let pageSize = 2 * Int.random(in: 0...512)
// error: not a literal expression
enum の raw value
整数型を raw type にする enum のケースは、raw value として literal expression を書けます。式は enum の raw type に対して型検査されたうえでリテラル値に畳み込まれます。
enum Permissions: UInt8 {
case read = 1 << 0 // 1
case write = 1 << 1 // 2
case execute = 1 << 2 // 4
}
raw value が明示されていないケースの「次の値」は、直前のケースを畳み込んだ値を起点に決まります。
enum Example: Int {
case a = 2 + 2 // 4
case b // 5
}
畳み込めない場合は診断されます。
enum Invalid: UInt8 {
case x = UInt8.random(in: 0...10)
// error: not a literal expression
}
.swiftinterface に明示的な raw value 式は出力されないので、こちらも初期化式が ABI に漏れることはありません。
整数 generic 引数
SE-0452 が導入した整数 generic 引数も、literal expression を受け付けるよう文法が拡張されます。
generic-argument → type
generic-argument → '-'? integer-literal
generic-argument → '(' literal-expression ')'
3 つめの形が新規追加です。型引数のパース文脈では < / > / , がトークン境界として使われるため、literal expression を generic 引数として置く場合は 括弧で囲む必要があります。裸の整数リテラル(オプションの単項マイナス付き)は、SE-0452 のときと同じく括弧なしで書けます。
let schemaRowSize = 32
let buffer: InlineArray<(2 * schemaRowSize), UInt8> // OK
let flags: InlineArray<(1 << 4), Bool> // OK
let small: InlineArray<5, Int> // OK(裸のリテラルは括弧不要)
SE-0452 の [N of T] シュガーも、括弧付きの literal expression を受け付けます。
let row: [(2 * schemaRowSize) of UInt8]
畳み込まれた値が型同一性を決めるので、InlineArray<(2 + 3), Int> と InlineArray<5, Int> は同じ型です。where N == 5 のような generic 制約も、(2 + 3) のような引数で満たされます。
generic 引数位置にある括弧付きの式は、コンパイラがまず「型式(とくにタプル型)」として解釈を試み、失敗したときに Int の文脈型で literal expression として畳み込みにかかります。S<(S<()>.X)> のような既存の型式は、tuple-first のルールでそのまま通ります。
コンパイル時の診断
literal expression 上の典型的な誤りはコンパイル時に検出されます。
整数オーバーフロー(ラップなしの演算で、結果が型のレンジに収まらない場合):
let x: UInt8 = 100 * 3 // error: integer overflow
ゼロ除算と剰余:
let y = 10 / 0 // error: division by zero
let z = 10 % 0 // error: division by zero
関数呼び出し、クロージャ、subscript など、literal expression でサポートされない構文を含む場合:
@section("__TEXT,data") let a = abs(-1)
// error: not supported in a literal expression
参照先の変数の初期化式が literal expression として畳み込めないために起きるエラーでは、初期化式の場所をエラー位置とし、literal expression 文脈での参照場所を note として示してくれます。
let runtimeValue = Int.random(in: 0...100)
// error: not supported in a literal expression
@section("__TEXT,data") let derived = runtimeValue + 1
// note: requested from reference in a literal expression
“literal expression” と “constant expression” の使い分け
SE-0492 では、@section 変数の初期化式を制限する用語として “constant expression” が使われていました。今回の Proposal はそれを literal expression に改名したうえで、対象を拡張する位置づけです。
ここで literal expression と呼ぶのは、対象となるどの文脈も「最終的にひとつのリテラル値」しか受け取らないためです。@section の初期化、enum の raw value、整数 generic 引数のいずれも、コンパイラが式を 1 つのリテラル値に畳み込んでから、まったく同じ生成コードに組み込みます。”literal expression” はこの「リテラル値に畳み込まれる」性質を素直に表す名前です。
一方で “constant expression” は、C++ の constexpr のようにユーザー定義関数や制御構造、ステートフルなオブジェクトまで含む、より大きな概念を指す余地のある言葉です。将来、より一般的なコンパイル時計算の仕組みが Swift に入る可能性に備えて、”constant expression” という名前はそちらに取っておく、という整理です。
03 今後の見通し
Proposal では、次のような発展方向が示されています。いずれも将来の構想であり、実現を約束するものではありません。
- 整数 generic 値の括弧の撤廃: 現状の Proposal は
InlineArray<(2 + 3), Int>のように括弧で囲むことを要求しますが、generic 引数リストの中での式パーサーで>/==/,をストップトークンとして扱うことで、InlineArray<2 + 3, Int>のように書けるよう拡張する設計が進められています。>や==のようにジェネリック引数の区切りと衝突する演算子だけ括弧が要る、という形に落とせるかどうかが議論の対象になっています。 - 浮動小数点リテラル式:
Float/Doubleのリテラルに対する+/-/*//を literal expression として認める拡張も自然な次のステップとされています。とくに組み込み向けの@section変数では浮動小数点定数も頻出するため、利用価値は高い一方、ターゲット依存の精度や丸めセマンティクスを丁寧に設計する必要があるとされています。 - 文字列リテラル式: コンパイル時定数同士の文字列連結(
"hello" + " world"など)や、コンパイル時定数の整数・文字列を埋め込む補間も検討対象です。@section変数や文字列を raw type にするenumの表現力を上げるだけでなく、URL 構築のような文字列ベース API のコンパイル時バリデーションへの道も開きます。 - サポート演算の拡大: 比較演算子(
==/</>=など)が返すBool値、min()/max()/abs()といった標準ライブラリ関数、浮動小数点が入った後の三角関数などへ広げる方向です。コンパイル時のBoolが扱えるようになれば、三項演算子condition ? a : bやif/elseを式として使う形に進めるとされています。 - コンパイル時プログラミング全般: literal expression は「コンパイラが特定の演算をコンパイル時に評価して具体的な値に畳み込む」基盤を提供します。これを土台に、ユーザー定義の純粋関数、より豊かなデータ型、コンパイル時バリデーションを含む、より一般的なコンパイル時プログラミングモデル(SE-0359 “Build-Time Constant Values” やそれを超える領域)へ広げる方向が示されています。