Compiler Version Directive
01 何が問題だったのか
Swift では #if swift(>=x.y) ディレクティブで言語バージョンに応じた条件付きコンパイルを書けます。Swift 3 までは「コンパイラのバージョン」と「言語のバージョン」が一致していたため、これで十分でした。
しかし Swift 4 以降は、--swift-version オプションによって新しいコンパイラで古い言語バージョンをコンパイルする 互換モード(compatibility mode) が導入され、「コンパイラのバージョン」と「言語のバージョン」という2つの軸を区別する必要が出てきました。
その場しのぎの対応として、互換モード用の「架空の言語バージョン」が追加されてきました。たとえば Swift 4 コンパイラを --swift-version 3 で動かしたときの言語バージョンは 3.2 とされ、Swift 4.1 コンパイラでは 3.3、Swift 4.2 コンパイラでは 3.4 / 4.1.50 というように、コンパイラのバージョンが上がるたびに既存の言語バージョンが際限なく増えていきます。
この仕組みにはいくつかの問題があります。
- 新しい互換バージョンを追加するたび、既存のバージョンがすべて付随して増殖するため、言語バージョンの数が二次関数的に膨れ上がります。
- 互換モードに関係なく「このコンパイラ以降で動かしたい」という条件を書くのが非常に面倒で、ミスを招きやすいです。たとえば「Swift 4.2 以降のコンパイラで有効にしたいコード」を
#if swiftだけで正確に表現しようとすると次のようになります。
#if swift(>=4.1.50) || (swift(>=3.4) && !swift(>=4.0))
// Swift 4.2 以降のコンパイラ向けのコード
#endif
同様に、Swift 5.0 以降のコンパイラを対象にしたいだけでも、互換モードを全て列挙する必要があります。
#if swift(>=5.0) || (swift(>=4.1.50) && !swift(>=4.2)) || (swift(>=3.5) && !swift(>=4.0))
// Swift 5.0 以降のコンパイラ向けのコード
#endif
このような条件は一目で意味を読み取るのが難しく、ライブラリ作者がコンパイラバージョンに応じてコードを出し分けたい、というだけのユースケースに対して複雑すぎます。
02 どのように解決されるのか
新たに #if compiler(...) ディレクティブを導入します。構文は既存の #if swift(...) と同じで、比較演算子とバージョン番号を取りますが、チェック対象が「言語バージョン」ではなく「コンパイラ自身のバージョン」 である点が異なります。互換モードで動いているかどうかに関係なく、コンパイラのバージョンそのものを判定できます。
使い方
コンパイラバージョンだけを条件にしたい場合は、compiler をそのまま使います。
#if compiler(>=4.2)
// Swift 4.2 以降のコンパイラ向けのコード
#endif
#if compiler(>=5.0)
// Swift 5.0 以降のコンパイラ向けのコード
#endif
これまで #if swift を入れ子や否定で組み合わせて表現していた「コンパイラバージョン以上」の条件が、1行で済むようになります。
swift と compiler は組み合わせることもでき、「この言語バージョンかつこのコンパイラ以降」という条件を自然に書けます。
#if swift(>=4.1) && compiler(>=5.0)
// --swift-version 4 以上のモードで動く Swift 5.0 以降のコンパイラ向けのコード
#endif
言語バージョンの運用の変更
この提案にはもう1つ、運用上の変更が含まれます。今後は 新しいコンパイラが出るたびに古い言語バージョンを増やす(バンプする)ことをやめる という方針です。
以前は Swift 4.1 コンパイラを --swift-version 3 で動かしたときの言語バージョンは 3.3、Swift 4.2 コンパイラでは 3.4、といった具合にコンパイラのリリースごとに増えていました。これからは、互換モードの言語バージョンはその互換モードの成立時点で固定され、コンパイラのバージョンに追従しなくなります。コンパイラのバージョンに応じた条件分岐は compiler ディレクティブで表現すべき、という整理です。
結果として、言語バージョンはある互換モードを表す安定した指標となり、コンパイラバージョンは compiler ディレクティブで直接扱えるようになります。両者が swift ディレクティブの中で混ざって膨れ上がる状況が解消されます。
既存コードへの影響
#if compiler を追加するだけの純粋に加算的な変更で、既存のコードの挙動は変わりません。既存の #if swift による条件分岐はそのまま動作し続けます。古いコンパイラでも機能するコードを書きたい場合は、従来どおり #if swift による入れ子の条件が必要です(古いコンパイラは compiler ディレクティブを理解しないため)。新しいコンパイラだけを対象にできるケースで、compiler がシンプルな代替手段として使えるようになります。