Swift编译器中间码SIL类型系统
这篇文章为个人对How to talk to your kids about SIL type use的翻译,其中夹带私活,阅读需谨慎。
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,
};
SIL可以分为两大类型,object(对象)类型 和 address(地址)类型。object类型包括整数,对class实例的引用,struct值或函数。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类型的value,而ValueType是由一个CanType以及一个标志组成,标志指示它是object还是address。
/// 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类型就是canonical类型。
在SIL代码中,解析和打印SIL对象类型都是以$
作为前缀,后跟canonical formal type(如果忘记了这意味着什么,参见上一篇文章中canonical类型的定义)。SIL地址类型则带有$ *
前缀。以下是一些简单的示例:
$Int // 一个Swift Int类型的值
$*String // 一个Swift String类型值得地址
$(Int, Optional<(Int) -> ()>) // 一个包含integer和可选类型方法的元组
address类型出现在由Swift表达式生成的SIL代码中,该表达式加载并存储左值(可分配的位置)以及inout参数。还有一些formal types不能在SIL中表示为值,必须通过地址间接进行操作;这些称为address-only(仅地址)类型。
请注意,并非所有formal types都是合法的SIL类型。特别是function(函数)类型和metatypes(元类型)是特殊的,这些稍后分析。
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
type lowering - 类型降级,我们平常在写swift中用到的系统提供的类型称为formal type,也就是正式类型。Swift的formal type系统有意抽象了许多代表性的问题,例如所有权转移约定和参数的直接性。而SIL旨在代表大多数此类实现细节,这些差异应在SIL类型系统中得到体现,因此SIL type要丰富的多。从formal type到SIL type的转换的操作称为类型降级,SIL type又称为lowered types
,降低的类型。
由于SIL是一种中间语言,SIL值大致对应于抽象机的无限寄存器。address-only(纯地址)类型本质上是那些too complex
而无法存储在寄存器中的类型。非address-only(纯地址)类型称为loadable(可加载)类型,这意味着它们可以被加载到寄存器中。
将一个地址类型指向一个非address-only(纯地址)类型是合法的,但是对象类型包含address-only(纯地址)类型是不合法的。
master/lib/SIL/IR/TypeLowering.cpp中有类型降级的全部逻辑。
其中定义了一个类TypeLowering,这个类包含了一些SIL type的扩展信息,来供SIL使用。
getLoweredType
从一个formal type返回一个SIL type。
/// getLoweredType - Get the type used to represent values of the Swift type in SIL.
SILType getLoweredType() const {
return LoweredType;
}
其中定义了一个helper类TypeConverter,TypeConverter创建和管理TypeLowerings,并提供了
getTypeLowering
返回一个TypeLowering类的对象。
const TypeLowering & getTypeLowering(SILType t, SILFunction &F);
有时,你已经有了SIL类型,并且需要检查它是否为trivial,loadable或address-only 类型。为此,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
有两个关键属性可将类型强制为address-only:
-
该类型的值必须始终存在于内存中,因为这些类型的值必须在某种全局列表中注册其地址。在寄存器中传递这样的值没有意义,因为寄存器没有全局地址。
-
该类型的值在编译时可能没有已知的大小。 尽管SIL值可以大于单个寄存器中的值,但我们必须在编译时知道它们的大小,因为IRGen会将SIL值分成零个或多个
scalar
(标量) LLVM值,例如浮点数和整数。
第一种类型的典型示例是对class实例的弱引用。在Swift中,弱引用用于以内存安全的方式中断引用周期。弱引用是通过在Swift运行时的全局结构中注册所有弱引用来实现的(可以在stdlib/public/runtime/SwiftObject.mm中找到代码)。销毁对class实例的最后一个强引用时,运行时将检查是否有该实例未销毁的弱引用,并将它们设置为nil。
第二类的canonical示例是一个类型为泛型参数的值。回想一下,与C ++或Clay不同,Swift不能完全实例化泛型函数和类型。可以编译使用泛型的代码,而无需了解在编译时可能绑定到这些泛型参数的所有具体类型;这是通过间接传递泛型值来告诉运行时该类型的大小和对齐方式以及如何操纵这些值的metadata(元数据)来实现的。
除了loadable类型和address-only类型之间的区别外,loadable类型之间还存在进一步的改进。如果可以自由复制和销毁该类型的值而无需执行其他逻辑,则我们说loadable类型是trivial的。Trivial类型的示例包括整数,浮点值以及指向permanent
(常驻)结构(如metatypes)的指针。
一个loadable但非trivial类型的canonical示例是,一个class实例的强引用。只要我们own
这个值,就可以将class引用加载到寄存器中。 single-assignment (单赋值)语义可确保保留引用计数语义。无需全局注册所有强引用或出于任何其他原因将它们存储在内存中。 但是,如果我们想复制一个强引用,则必须增加其引用计数。如果销毁包含强引用的值,则必须减少引用计数。
当我们要求SIL降低聚合类型(例如struct,enum或tuple类型)时,代码首先查看聚合中每个成员的降低。如果所有成员都是trivial的,则聚合类型是trivial的; 如果所有成员都loadable,则结聚合类型是loadable;并且如果至少一个成员address-only,则聚合类型是address-only。
注意,降低class的类型永远不需要查看class的字段。class实例始终是单个引用计数的指针,该指针是loadable。
此外,non-class绑定协议类型的值(即所谓opaque existentials
(不透明的存在))必须是 address-only,因为它们可以包含符合该协议的任何具体类型。由于我们在编译时不知道完整的符合类型,因此我们必须假设其中至少有一个包含弱引用(或某些其他opaque existential)。
存在类型也叫existential type,是对类型做抽象的一种方法。可以在你不知道具体类型的情况下,就断言该类型存在。
回顾上一节中有关成员引用类型的材料。这在这里起作用,因为在使用泛型参数降低聚合类型时,我们必须应用substitutions。 例如,思考以下代码:
struct Box<T> {
var value: T
}
$*Box<Any> // address-only -- 聚合类型包含一个存在类型
$Box<Int> // trivial -- 聚合类型仅仅包含trivial字段
$Box<NSObject> // loadable -- 包含一个强引用类型
struct Transform<T> {
var fn: (T) -> T
}
$Transform<Any> // 一个总是loadable类型的方法
struct Phantom<T> {
var counter: Int
}
$Phantom<Any> // trivial
$Phantom<() -> ()> // 总是trivial
$Phantom<NSObject> // ... 也是trivial
前两种类型表明,泛型struct的降低取决于泛型参数。谈论Box
是loadable的或address-only是没有意义的,对于某些类型的Foo
,仅谈论Box <Foo>
。
另外,我使用Transform
示例,来表明如果泛型参数的类型是address-only, 并不会将聚合强制为address-only,因为此处泛型参数不直接显示为一个字段类型,而是作为fn的参数类型。另一种情况是上面的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
,用于存储一个函数。函数的输入和输出是泛型参数。泛型参数是address-only类型,因此必须间接传递。可以想象在机器级别,Transform.fn
使用返回参数的指针作为返回值,而使用参数中的指针作为参数。
另一方面,double
函数可操作整数,这不重要;当然,我们希望输入值x
到达寄存器中,并且从函数返回后,返回值将存储在另一个寄存器中。Swift的formal type系统允许将identity
存储在myTransform.fn
中,因为在substitution T:= Int
之后,myTransform.fn
的formal type与double
的formal type完全匹配。但是,如果像目前为止所描述的那样天真地进行编译,则代码将在运行时做错事情,因为任何使用整数调用myTransform.fn
的人都将传递整数值的地址,而不是函数本身所期望的整数值本身。
勘误
感觉作者没写清楚,identity可能是double,这样按照描述编译是不会失败的,如下能正确输出2。猜测可能因为文章写的比较早,当时Swift还不支持这种语法。
let myTransform = Transform(fn: double)
myTransform.fn(1) // 2
显然,function类型的降低不仅仅是其参数和结果类型的降低。我们需要一个更灵活的表示形式来表示参数类型可能是trivial但仍必须间接传递的情况。此外,我们意识到formal type的降低必须以某种方式考虑substitution。实际上,要将完全substituted formal type传递给TypeLowering :: getLoweredType()
。降低function类型,metatypes和tuples时,必须使用此函数的较长形式,该形式带有两个参数;substituted formal type和抽象模式。
抽象模式本质上是原始unsubstituted类型,substituted就是由它派生的。降低function类型时,参数传递约定源自抽象模式,而不是substituted formal type。降低函数类型的结果是SILFunctionType类的实例,该实例增加了FunctionType缺少的一些详细信息,即有关如何传递参数和结果的conventions
(约定)。是按值传递或返回它们,还是按地址传递或返回它们,以及是否有loadable类型的所有权转移(trivial类型不需要所有权转移)。
最后一个复杂之处是函数本身具有一个convention
,描述了如何调用它。在我们前面的示例中,double
是一个全局函数,它不会从词法上下文中捕获任何值,因此我们可以将其作为单个函数指针进行传递。这称为thin
函数。另一方面,如果我们采用闭包值并将其存储在myTransform.fn
中,则必须保留上下文以引用捕获的值;这称为thick
函数。一个thick函数表示为两个值,函数指针后跟对上下文对象的强引用。
下面是一些示例:
// 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,因为泛型参数是address-only类型。
另一方面,我们将double
的类型降低了:
$@convention(thin) (Int) -> Int
此时,我们仍然无法编译代码,但是至少我们可以检测到SILFunctionTypes级别的类型不匹配,而不仅仅是对不正确的代码进行错误编译。表达式的形式类型匹配,但降低的类型不匹配的情况称为abstraction difference
(抽象差异)。通过SILGen将substituted函数值包装在一个re-abstraction thunk中来处理抽象差异。
re-abstraction thunk 可转发参数,调用函数并转发结果,并小心处理参数和结果中的任何抽象差异。 如果substituted参数是trivial,但是原始参数是间接传递的,thunk将从其地址加载值并将其传递给substituted函数。类似地,如果substituted结果是trivial,但原始结果是间接返回的,则thunk将获取substituted结果值,并将其存储在提供给thunk的间接返回地址中。
如果你曾在调试器回溯中看见re-abstraction thunks,那就知道它们是什么;大多数情况下,你可以忽略它们,这仅意味着你正在使用泛型做事,而Swift在幕后进行了一些魔术。
re-abstraction thunks 实现在这里lib/SILGen/SILGenPoly.cpp)。主要入口是SILGenFunction::emitOrigToSubstValue()
和 SILGenFunction::emitSubstToOrigValue()
。
从概念上讲,这些操作采用substituted类型S和原始类型O:
-
emitOrigToSubstValue()
将抽象级别为O的一个S类型的值转换为抽象级别为S的一个S类型的值 -
emitSubstToOrigValue()
将抽象级别为S的一个S类型的值转换为抽象级别为O的一个S类型的值
在实践中,这些函数实际上采用一对类型和抽象模式,因为同一台机器也被用来发出thunk来执行各种类型的函数转换。例如,Int
是Any
的formal sub-type(子类型),因此() -> Int
是() -> Any
的formal sub-type;传递() -> Int
作为类型() -> Any
的值时,需要将substituted函数包装在thunk中。调用该函数的thunk,然后将结果包装在一个存在的对象中,然后返回给调用者。
Lowered metatypes
如果以上都不合理,请不要流汗。我花了一些时间来了解function类型的降低以及为什么需要re-abstraction thunks,而我可能没有很好的解释它。metatypes(元类型)也有类似的东西,但更容易描述。
一个metatype类型的值必须在运行时唯一地标识formal type。因此,NSObject.Type
类型的值可以包含NSObject的任何子类,其中有数千个。Class metatypes降低为指向运行时类型metatypes对象的指针。
但是,类型为Int.Type
的值仅需要唯一地标识为Int
的子类型,而其中只有一个子类型Int
本身。因此,Int.Type
根本不需要存储,实际上会降低为空值。
但是,同样,在替换前后,降低的情况有所不同。如果我具有泛型类型参数T
,则T.Type
必须降低为指向运行时值的指针。如果我在替换为T:= Int
的上下文中使用此值,那么当Int.Type
值为空时,如何在其中存储Int.Type
?
和以前一样,答案是必须根据抽象模式降低metatypes,这种抽象模式告诉SIL该值的most general
场景将是什么。生成的metatype用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
一个thick
metatype具有运行时表示形式,而thin
metatype则没有。请注意,class metatypes总是thick(尽管也许我们可以说最终class的class metatypes是thin,但我们现在不这样做)。只有值类型可以具有thin metatype。
无需re-abstraction thunks,metatypes仅需要thickness
conversions。当我们从thin到thick时,我们正在加载与唯一的编译时metatype相对应的运行时值。我们精打细算时,只是丢弃运行时值,因为我们知道它在编译时必须是唯一的。
One last thing: SIL box types
你可能在SIL代码中看到过SILBoxType,它们看起来像下面的SIL代码形式:
$@box Int
一个box类型是为值heap-allocated(分配堆)的容器。Boxes作为可变捕获变量的类型以及间接enum实例的payload而出现。在前一种情况下,payload是共享的,并且具有可变的引用语义;在后者中,它是不可变的并且表现为值。不幸的是,我们无法区分这两者,这可能是问题,也可能不是问题。
Substitutions with SIL types
有时,有必要对包含从属类型的SIL类型执行substitution以产生fully-concrete(完全具体)的SIL类型。
回想一下,substitution将GenericTypeParamTypes映射为formal types。此外,当SILType包含BoundGenericType时,BoundGenericType的参数是formal types,而不是降低的SIL类型。
另一方面,tuple类型的元件是降低的SIL类型。因此,当我们应用substitution T:= Int.Type
时,我们期望SIL类型substitution的行为如下:
// 泛型类型参数出现在 "unlowered position"
$Array<T> => $Array<Int.Type>
// 泛型类型参数出现在 "lowered position"
$(T, T) => $(@convention(thick) Int.Type, @convention(thick) Int.Type)
执行SIL类型substitution的逻辑可以在SILType :: subst() 方法中找到,并且可以通过使用原始泛型参数作为抽象模式回调类型降低来正确处理降低替换右侧的情况。
Optional payloads are maximally abstract
现在,你已经足够了解当前实现的有趣限制。 回忆一下Swift中Optional
类型的定义:
enum Optional<T> {
case some(T)
case none
}
如果我在可选的payload中存储类型为(Int) -> Int
的函数值,则其他人可能会对它进行操作,就好像它是(T) -> Int
,(Int) -> T
或(T) -> T
。因此,存储在可选对象中的函数必须maximally abstract
(最大程度抽象)。
这是一个权衡;在将Optional<(Int) -> Int>
转换为Optional<(T) -> T
时可以在运行时不做任何工作,但这是以始终将两个值都存储为后者为代价的。
最好在某个时候进行更改,以便可以重新选择可选的payload。这将需要对编译器的附加编译器支持,但是无论如何,它们在语义分析中已经具有特殊的要求。
但就目前而言,重要的是要记住,在SIL中使用Optionals时,始终不会降低payload类型。例如,如果你在可选的SILType上调用SILType :: getAnyOptionalObjectType()
,则必须随后使用maximally opaque
抽象AbstractionPattern :: getOpaque()
降低结果。容易错误的忘记降低payload类型,或将其自身用作抽象模式来降低错误,这很容易导致SIL验证程序失败或编译错误。
Next steps
SIL类型向IRGen提供了有关函数调用约定以及值如何存储在内存中以及如何传递等更多详细信息。目前,我们仍然不知道值的大小和对齐方式,也不知道它们如何映射到机器寄存器。这是IRGen的工作,我将在下一篇文章中尝试解释。
Conclusion
SIL类型系统在formal type系统之上引入了地址的概念。SIL类型是通过Type lowering
从formal type构造的,该类型的任务是将类型分类为可以trivially传递的类型,可以加载到寄存器中但需要特殊拷贝和销毁行为的类型,以及必须总是被间接传递的address-only类型。 这种分类也反映在函数类型降低的参数和结果中。两个表达式可能具有相同的formal type,但可能具有不同的SIL类型,在这种情况下,SILGen知道会发出各种转换来桥接抽象差异。降低的metatypes和SIL boxes完善了SIL类型系统。
How to talk to your kids about SIL type use
Add a section to SIL.rst describing the SIL Ownership Model. #13546
Swift Intermediate Language (SIL)
Ownership SSA and Safe Interior Pointers
SIL Ownership Model