blog

Swiftコンパイラ中間コード SIL型システム

この記事はHow to talk to your kids about SIL type useを個人的に翻訳したものです。 SILはオブジェクト型とアドレス型の2つに大別されます。オブジェクト型には...

Jan 17, 2021 · 15 min. read
シェア

SIL object and address types

enum class SILValueCategory : uint8_t {
 /// An object is a value of the type.
 Object,
 /// An address is a pointer to an allocated variable of the type
 /// (possibly uninitialized).
 Address,
};

オブジェクト・タイプには、整数、クラス・インスタンスへの参照、構造体値、関数などがあります。

class SILType {
public:
 /// The unsigned is a SILValueCategory.
 using ValueType = llvm::PointerIntPair<TypeBase *, 2, unsigned>;
private:
 ValueType value;
 SILType(CanType ty, SILValueCategory category)
 : value(ty.getPointer(), unsigned(category)) {
 }

SILTypeのコンストラクタからわかるように、SILTypeにはValueType型の値が含まれます。ValueType型はCanTypeと、オブジェクトかアドレスかを示すフラグから構成されます。

/// CanType - This is a Type that is statically known to be canonical. To get
/// one of these, use Type->getCanonicalType(). Since all CanType's can be used
/// as 'Type' (they just don't have sugar) we derive from Type.
class CanType : public Type {

CanType型の1つは正規型です。

SIL コードでは、SIL オブジェクト型の解析と印刷には、$ の後に正規の正式な型が続きます。以下に簡単な例を示します:

$Int // Swift Int型の値
$*String // SwiftのString型は取り組む価値がある
$(Int, Optional<(Int) -> ()>) // 整数とオプションの型メソッドを含むタプル

アドレス型は、inout パラメータと同様に、左の値をロードして格納する Swift 式によって生成された SIL コードに表示されます。SIL で値として表現することができず、アドレスを通して間接的に操作されなければならない形式的な型もいくつかあります; これらはアドレスのみの型と呼ばれます。

すべての形式型が正規のSIL型ではないことに注意してください。特に、関数型とメタ型は特殊であり、これらは後で分析されます。

以下は、SILTypeにおけるより重要なインターフェースの一部です:

SILValueCategory getCategory() const {
 return SILValueCategory(value.getInt());
}
SILType getAddressType() const {
 return SILType(getASTType(), SILValueCategory::Address);
}
SILType getObjectType() const {
 return SILType(getASTType(), SILValueCategory::Object);
}
CanType getASTType() const {
 return CanType(value.getPointer());
}
/// アドレス型であるかどうか
bool isAddress() const { return getCategory() == SILValueCategory::Address; }
/// 型がオブジェクト型であるかどうか
bool isObject() const { return getCategory() == SILValueCategory::Object; }

SIL type lowering

型低下 - 型低下、swift を書く際に通常使用されるシステムが提供する型は、形式的な型と呼ばれ、Swift の形式的な型システムは、所有権の移転の規約やパラメータの直接性のような多くの表現上の問題から意図的に抽象化されます。一方、SIL は、これらの種類の実装の詳細のほとんどを表現することを意図しており、これらの違いは SIL 型システムで表現されるべきであり、それゆえ、はるかに豊富な SIL 型です。形式的な型からSIL型に変換する操作は型ダウングレードと呼ばれ、SIL型は下げられた型としても知られています。

SILは中間言語であるため、SILの値は抽象マシンの無限レジスタにほぼ対応します。アドレス・オンリー型は基本的に、レジスタに格納するには複雑すぎる型です。非アドレスオンリー型はローダブル型と呼ばれ、レジスタにロードすることができます。

アドレス型を非アドレス専用型に向けることは合法ですが、オブジェクト型がアドレス専用型を含むことは合法ではありません。

include/swift/SIL/SILType.h、型下げのためのすべてのロジックがあります。

/// getLoweredType - Get the type used to represent values of the Swift type in SIL.
SILType getLoweredType() const {
	return LoweredType;
}
 const TypeLowering & getTypeLowering(SILType t, SILFunction &F);

すでにSILタイプを持っていて、それがトリビアルか、ローダブルか、アドレス・オンリーかをチェックする必要がある場合があります。これを行うために、SILTypeクラスは様々なメソッドを定義しています:

/// True if the underlying AST type is trivial, meaning it is loadable and can
/// be trivially copied, moved or detroyed. Returns false for address types
/// even though they are technically trivial.
bool isTrivial(const SILFunction &F) const;
 
/// True if the type, or the referenced type of an address type, is loadable.
/// This is the opposite of isAddressOnly.
bool isLoadable(const SILFunction &F) const {
return !isAddressOnly(F);
}
/// True if the type, or the referenced type of an address type, is
/// address-only. This is the opposite of isLoadable.
bool isAddressOnly(const SILFunction &F) const;

Trivial, loadable, and address-only types

タイプはアドレスのみであることを強制する2つの重要な属性があります:

  • なぜなら、これらの型の値は、そのアドレスがグローバル・リストに登録されていなければならないからです。レジスタはグローバル・アドレスを持たないので、このような値をレジスタに渡す意味はありません。

  • このタイプの値は、コンパイル時に既知のサイズを持たない可能性があります。 SIL値は1つのレジスタの値よりも大きくすることができますが、IRGenはSIL値を浮動小数点数や整数などの0個以上のスカラーLLVM値に分割するため、コンパイル時にそのサイズを知っておく必要があります。

最初のタイプの典型的な例は、クラスのインスタンスへの弱い参照です。Swift では、弱い参照は、メモリセーフな方法で参照サイクルを中断するために使用されます。弱参照は、Swift ランタイムのグローバル構造内のすべての弱参照を登録することによって実装されます。クラスインスタンスへの最後の強い参照を破棄するとき、ランタイムは、破棄されていないそのインスタンスへの弱い参照があるかどうかをチェックし、それらを nil に設定します。

2番目のタイプの典型的な例は、ジェネリックパラメータ型の値です。C ++やClayとは異なり、Swiftはジェネリック関数と型を完全にインスタンス化できないことを思い出してください。コンパイル時にそれらのジェネリックパラメータにバインドされる可能性のあるすべての特定の型を知ることなく、ジェネリック型を使用するコードをコンパイルすることが可能です。これは、実行時に型のサイズとアライメント、およびそれらの値のメタデータを操作する方法を伝えるために間接的にジェネリック値を渡すことによって行われます。

ローダブル型とアドレスオンリー型の違いに加えて、ローダブル型にはさらに細かい違いがあります。ロード可能な型は、その型の値が他のロジックを実行することなく自由にコピーしたり破棄したりできる場合、トリビアルであると言われます。トリビアル型の例としては、整数、浮動小数点値、永続的な構造体へのポインタなどがあります。

ロード可能だが自明でない型の典型的な例は、クラス・インスタンスへの強い参照です。クラス参照は、この値を所有するだけでレジスタにロードできます。 単一割り当てセマンティクスにより、参照カウントのセマンティクスが保持されます。すべての強参照をグローバルに登録したり、他の理由でメモリに格納したりする必要はありません。 ただし、強参照をコピーする場合は、その参照カウントを増やす必要があります。強参照を含む値を破棄する場合は、参照カウントを減らす必要があります。

SILが集約タイプを下げるように要求されると、コードはまず集約の各メンバーの下げを調べます。すべてのメンバがトリビアルである場合、集約型はトリビアルであり、すべてのメンバがローダブルである場合、ジャンクション集約型はローダブルであり、少なくとも1つのメンバがアドレスオンリーである場合、集約型はアドレスオンリーです。

クラスの型を下げるには、クラスのフィールドを見る必要がないことに注意してください。

さらに、非クラス・バインディング・プロトコル型の値は、そのプロトコルに適合する任意の特定の型を含むことができるため、アドレス・オンリーでなければなりません。完全な適合型はコンパイル時にわからないため、少なくとも1つは弱い参照を含むと仮定しなければなりません。

実存型は実存型とも呼ばれ、型について抽象化する方法です。正確な型を知らなくても、その型が存在すると断言することができます。

前のセクションでメンバ参照型について説明しました。ジェネリック・パラメータを使用して集約型を下げる場合、SUBSTITUTIONSを適用する必要があるためです。 例えば、以下のコードを考えてみてください:

struct Box<T> {
 var value: T
}
$*Box<Any> // address-only -- 集約型には存在型
$Box<Int> // trivial -- 集約型は些細なフィールドしか含まない。
$Box<NSObject> // loadable -- 強力な参照型を含む
struct Transform<T> {
 var fn: (T) -> T
}
$Transform<Any> // 常にロード可能な型であるメソッド
struct Phantom<T> {
 var counter: Int
}
$Phantom<Any> // trivial
$Phantom<() -> ()> // 常にトリビアルである
$Phantom<NSObject> // ... また、些細な

最初の2つの型は、ジェネリック構造体の縮小がジェネリック引数に依存することを示しています。Boxがローダブルであるかアドレスのみであるかについて話すことは意味がなく、Fooのいくつかの型についてはBoxについてだけ話します

また、Transformの例を使ったのは、ジェネリック・パラメータの型がaddress-onlyの場合、集約がaddress-onlyになることを強制するものではないことを示すためです。もう1つのケースは上記のPhantom型で、その型パラメータはどのフィールドの型にも全く現れません。

SIL function types

下のコードを見てください:

struct Transform<T> {
 let fn: (T) -> T
}
func double(x: Int) -> Int {
 return x + x
}
let myTransform = Transform(fn: identity)

関数を格納するために使用される汎用型 Transform があります。関数の入力と出力はジェネリック・パラメータです。ジェネリックパラメータはアドレスのみの型なので、間接的に渡す必要があります。マシンレベルでは、Transform.fnは戻り値として戻り引数へのポインタを使用し、引数として引数のポインタを使用することが考えられます。

一方、double 関数は整数で動作し、それは問題ではありません; もちろん、入力値 x がレジスタに到着し、戻り値が関数から戻った後に別のレジスタに格納されることが期待されます。Int を代入した後、myTransform.fn の形式型は double の形式型と正確に一致します。なぜなら、myTransform.fnを整数値で呼び出す人は、関数自体が期待する整数値そのものではなく、整数値のアドレスを渡すことになるからです。

let myTransform = Transform(fn: double)
myTransform.fn(1) // 2

関数型の削減は、引数型と結果型の削減だけではないことは明らかです。パラメータ型が些細なものであっても、間接的に渡されなければならない場合には、より柔軟な表現が必要です。TypeLowering :: getLoweredType()さらに,形式型の下げは,何らかの方法で置換を考慮しなければならないことが分かっています.関数型、メタ型、タプルを下げる場合、この関数の長い形式を使用する必要があります。

抽象パターンは本質的に,置換された型から派生する元の未置換の型です.関数型を下げると、パラメータ渡しの規約は、置換された形式型ではなく、抽象パターンから派生します。関数型を下げた結果は、SILFunctionType クラスのインスタンスとなり、FunctionType に欠けていた詳細情報、つまり、パラメータと結果の渡し方に関する規約が追加されます。パラメータを値で渡すか、アドレスで返すか、ロード可能な型の所有権の移譲の有無などです。

最後に複雑なのは、関数自体に呼び出し方を記述する規約があることです。先ほどの例では、doubleはグローバル関数で、レキシカルコンテキストから値を取り込まないので、単一の関数ポインタとして渡すことができます。これはTHIN関数と呼ばれます。一方、クロージャ値が取り込まれ、myTransform.fnに格納される場合、取り込まれた値を参照するためにコンテキストを保持する必要があります。thick関数は2つの値で表現され、関数ポインタの後にコンテキストオブジェクトへの強い参照が続きます。

いくつか例を挙げましょう:

// Original type: (Any, Int) -> ()
// Substituted type: (Any, Int) -> ()
$@convention(thick) (@in Any, Int) -> ()
// Original type: (T) -> T
// Substituted type: (T) -> T
$@convention(thick) (@in T) -> @out T
// Original type: (Int) -> Int
// Substituted type: (Int) -> Int
$@convention(thick) (Int) -> Int
// Original type: (NSObject) -> NSObject
// Substituted type: (NSObject) -> NSObject
$@convention(thick) (@owned NSObject) -> @owned NSObject
// Original type: (T) -> T
// Substituted type: (Int) -> Int
$@convention(thick) (@in Int) -> @out Int

myTransform.fnの型は縮小されています:

$@convention(thick) (@in T) -> @out T

THICK関数規約は、ユーザーがクロージャを含むあらゆる関数値を格納することができるため、使用しなければなりません。また、総称引数はアドレスのみの型なので、Tは間接的に渡され、返されなければなりません。一方、doubleの型は低くなっています:

$@convention(thin) (Int) -> Int

この時点では、まだコードをコンパイルすることはできませんが、少なくとも SILFunctionTypes レベルでの型の不一致は検出できます。abstraction difference式の形式的な型は一致するが、縮小された型は一致しない場合を .抽象化の違いは、再抽象サンクで置換された関数値をラップすることによってSILGenによって処理されます。

再抽象thunkは引数を転送し、関数を呼び出し、結果を転送します。 代入された引数が些細なもので、元の引数が間接的に渡される場合、thunkはそのアドレスから値をロードし、代入された関数に渡します。同様に、代入された結果が些細なもので、元の結果が間接的に返される場合、thunkは代入された結果の値を取り、thunkに与えられた間接的な戻り値のアドレスに格納します。

もしあなたがデバッガーのバックトレースで再抽象サンクを見たことがあるなら、あなたはそれが何であるか知っています; ほとんどの場合、あなたはそれらを無視することができます。

再抽出サンクはlib/SILGen/SILGenPoly.cpp)に実装されていますSILGenFunction::emitOrigToSubstValue() SILGenFunction::emitSubstToOrigValue()主なエントリーポイントは .

概念的には、これらの操作はSUBSTITUTED型Sとプリミティブ型Oを使用します:

  • emitOrigToSubstValue() 抽象度OのS型の値を、抽象度SのS型の値に変換します。
  • emitSubstToOrigValue() 抽象度SのS型の値を、抽象度OのS型の値に変換。

実際には、これらの関数は型と抽象パターンのペアを使用します。なぜなら、同じマシンは様々な型の関数変換を実行するためにサンクを生成するためにも使用されるからです。例えば、Int は Any の正式なサブタイプであり、したがって -> Any の正式なサブタイプです。その関数の thunk が呼び出され、その結果が実存オブジェクトにラップされ、呼び出し元に返されます。

Lowered metatypes

もし上記のどれかが理解できなくても、気にしないでください。metatypesにも似たようなものがありますが、説明するのはもっと簡単です。

したがって、NSObject型の値には、何千もあるNSObjectのサブクラスのいずれかを含めることができます。クラス・メタタイプは、実行時にメタタイプ型のオブジェクトへのポインタになります。

しかし、Int.Type型の値は、Intのサブタイプとして一意に識別される必要があるだけであり、そのサブタイプInt自体は1つしかありません。その結果、Int.Typeを格納する必要はなく、実際にはNULL値になります。

しかし、繰り返しになりますが、代入前と代入後では下げ方が異なります。汎用型パラメータTがある場合、T.Typeは実行時の値へのポインタに下げられなければなりません。T:=Intを置き換えるコンテキストでこの値を使用する場合、Int.Typeの値がNULLのときにInt.Typeをどのように格納すればよいのでしょうか?

以前と同様に、その値の最も一般的なシナリオをSILに伝える抽象スキーマに従って、メタタイプを下げなければならないというのが答えです。生成されたメタタイプはCONVENTIONでアノテーションされます。

// Original type: NSObject
// Substituted type: NSObject
$@convention(thick) NSObject
// Original type: Int
// Substituted type: Int
$@convention(thin) Int
// Original type: T
// Substituted type: T
$@convention(thick) Int

再抽出サンクは必要なく、メタタイプは太さの変換だけが必要です。thinからthickになるとき、コンパイル時にユニークなメタタイプに対応するランタイム値がロードされます。微調整を行う場合、コンパイル時に一意でなければならないことが分かっているため、実行時の値は単に破棄されます。

One last thing: SIL box types

SILコードのSILBoxTypeが以下のような形になっているのを見たことがあるかもしれません:

$@box Int

ボックス型は、ヒープに割り当てられた値のコンテナです。ボックスは、変更可能なキャプチャー変数の型や、間接的な列挙型インスタンスのペイロードとして現れます。前者の場合、ペイロードは共有され、変更可能な参照セマンティクスを持ちます。後者の場合、ペイロードは不変で、値として振る舞います。残念ながら、この2つを区別することはできません。

Substitutions with SIL types

完全に具体的なSIL型を生成するために、従属型を含むSIL型に対して置換を実行する必要がある場合があります。

一方、タプル型の構成要素は縮小SIL型です。したがって、置換T: = Int.Typeを適用する場合、SIL型置換の期待される動作は以下のようになります:

// 汎用型パラメータは "unlowered position "に表示される。
$Array<T> => $Array<Int.Type>
// 汎用型パラメータは "下がった位置 "に表示される。
$(T, T) => $(@convention(thick) Int.Type, @convention(thick) Int.Type)

SIL型置換を実行するロジックはSILType :: subst()メソッドにあり、置換の右辺を下げるケースは、抽象スキーマ・コールバック型下げとして元のジェネリック・パラメータを使用することで正しく処理できます。

Optional payloads are maximally abstract

今までに、あなたは現在の実装の興味深い制限を理解するのに十分知っています。 SwiftのOptional型の定義を思い出してください:

enum Optional<T> {
 case some(T)
 case none
}

したがって、オプショナル・オブジェクトに格納される関数は、最大限に抽象的でなければなりません。

Optional<(Int) -> Int>Optional<(T) -> Tの変換時に実行時に何もしないことも可能ですが、その場合、両方の値が常に後者として保存されることになります。

オプションのペイロードを再選択できるように、どこかの時点で変更を加えるのがベストでしょう。これにはコンパイラの追加サポートが必要ですが、いずれにせよセマンティック解析にはすでに特別な要件があります。

しかし、現時点では、SILでOptionalsを使用する場合、ペイロード型は決して下げられないことを覚えておくことが重要です。 SILType :: getAnyOptionalObjectType()AbstractionPattern :: getOpaque()例えば、オプショナルの SILType を呼び出す場合、その後、最大に不透明な抽象化を使用して結果を下げなければなりません。間違ってペイロード型を下げることを忘れたり、抽象モードそのものを使用してエラーを下げたりすることは簡単で、SILバリデータの失敗やコンパイルエラーにつながります。

Next steps

SIL型は、関数呼び出しの規約や、値がどのようにメモリに格納され、どのように渡されるかについて、より詳細な情報をIRGenに提供します。値がどのようにサイズ調整され、アラインメントされ、どのようにマシンレジスタにマッピングされるかはまだわかっていません。これはIRGenの仕事であり、次回の投稿で説明しようと思います。

Conclusion

SIL型システムは形式的な型システムの上にアドレスの概念を導入しています。SIL型はType loweringによって形式的な型から構築され、Type loweringは型を些細な受け渡しが可能な型、レジスタにロードできるが特別なコピーと破棄の動作が必要な型、そして常に間接的に受け渡しをしなければならないアドレスのみの型に分類するタスクを持ちます。常に間接的に渡されなければならないアドレスのみの型。 この分類は関数型削減の引数と結果にも反映されます。この場合、SILGenは抽象度の違いを埋めるためにさまざまな変換を実行します。削減されたメタ型とSILボックスによってSIL型システムが完成します。



SIL.rstにSILオーナーシップモデルを説明するセクションを追加 #13546

Swift 中間言語

所有権SSAと安全な内部ポインタ

SIL 所有権モデル

Read next

今日のリートコード(5):最長の公開接頭辞

前の記事\n毎日のLeetCode前例集\nコードリポジトリ\nGitHub: ...\nGitee: ...\nタイトル:最も長い共通接頭辞\nタイトルソース\n書く

Jan 17, 2021 · 5 min read