Swift Language Version Build Configuration
01 何が問題だったのか
Swift 2.x 当時、Swiftの言語仕様(構文)は改訂を重ねて変化を続けていました。ライブラリやパッケージの作者が複数バージョンのSwiftに対応しようとすると、バージョンごとにリリースブランチを分けて管理する、あるいはソースツリー自体を分割するといった対処しか手がなく、コード共有のコストが高い状態でした。
既存のビルド設定では古い構文を残しておけない
Swiftには #if os(...) や #if arch(...) のようなビルド設定(build configuration)が以前から用意されていましたが、これらは非アクティブ側の分岐についてもパースは行われます。つまり、非アクティブ側に書かれたコードも「現在のSwiftコンパイラで構文として成立している」必要がありました。
そのため、Swiftの構文がバージョン間で変わった場合、古い書き方と新しい書き方を同じファイルに共存させることができません。たとえばSwift 2.2 で構文が廃止された機能を、Swift 2.1 向けには残したまま同じソースに含めようとすると、新しいコンパイラで構文エラーになってしまいます。結果として、パッケージ作者は次のような負担を強いられました。
- 旧Swiftと新Swiftそれぞれに対応するブランチを分けて並行メンテナンスする
- あるいは古い構文・新しい構文のどちらかを切り捨てて、対応バージョンを絞る
このままではSwiftの構文変更のたびにエコシステムの分断が起きやすく、言語のバージョンアップそのものの移行コストも高止まりしてしまう、という課題がありました。
02 どのように解決されるのか
Swiftコンパイラ自身が認識する言語バージョンを条件に使える、新しいビルド設定 #if swift(...) を追加します。これにより、Swiftのバージョンによって有効にするコードを切り替えられるようになります。
基本的な使い方
#if swift(>=2.2) のように、>= 演算子とバージョン番号を組み合わせて書きます。コンパイラに埋め込まれている言語バージョンが条件を満たせば、そのブランチが有効なコードとして採用されます。
#if swift(>=2.2)
print("Active!")
#else
this! code! will! not! parse! or! produce! diagnostics!
#endif
#if swift は他のビルド設定と同様に行単位ではなく、文や宣言といったまとまりを丸ごと囲みます。
非アクティブ側のブランチはパースされない
#if swift の最大の特徴は、非アクティブ側のブランチをコンパイラがパースしない という点です。従来の #if os(...) などと違い、字句解析時点の診断(lex diagnostics)も出しません。
上の例の #else 側のような、現在のSwiftコンパイラでは構文として成立しないコードであっても、#if swift(>=2.2) の非アクティブ側に置いておけばエラーになりません。これにより、旧バージョンのSwift向けの古い構文と、新バージョン向けの新しい構文を、同じソースファイルに共存させられるようになります。
たとえば、構文が変わった機能について、Swiftのバージョンごとに別々の書き方を一つのファイルにまとめることができます。
#if swift(>=2.2)
// 新しいSwift向けの書き方
#else
// 旧Swift向けの書き方(新コンパイラでは構文エラーになるようなコードでも置ける)
#endif
バージョンの書式と比較演算子
バージョン番号は当面、最大2つの成分(メジャー・マイナー)までを想定しています。構文変更がパッチリビジョン(+0.0.1 相当)で入ることは考えにくいためです。
比較演算子はシンプルさを重視して >= のみをサポートします。必要になれば将来的に他の演算子を追加することも想定されていますが、この提案のスコープでは >= に限定されています。
既存コードへの影響
#if swift は opt-in の仕組みで、明示的に書いたコードにしか影響しません。そのため、この提案の導入によって既存コードが壊れることはありません。