|
| 1 | +# SIL Initializer Conventions |
| 2 | + |
| 3 | +A nominal type can define a number of initializers, some of which may |
| 4 | +delegate initialization to another initializer. There are specific calling |
| 5 | +conventions for these initializers within SIL that make up a part of the ABI |
| 6 | +for a type. This document aims to summarize the key calling conventions for |
| 7 | +these initializers. |
| 8 | + |
| 9 | + |
| 10 | +# Structs and Enums |
| 11 | + |
| 12 | +The delegation status for the initializer of a struct or enum is not encoded |
| 13 | +in the definitions of these initializers. Thus, all of these initializers |
| 14 | +have an implicit `metatype` argument for the instance to be passed in as the last |
| 15 | +argument to the initializer. Using `<...>` as a stand-in for other arguments |
| 16 | +that are part of the usual function calling convention, consider this example: |
| 17 | + |
| 18 | +``` |
| 19 | +// the non-delegating init MyStruct.init(final:) |
| 20 | +sil hidden [ossa] @$s4test8MyStructV5finalACSi_tcfC : $@convention(method) (<...>, @thin MyStruct.Type) -> MyStruct { |
| 21 | +bb0(<...>, %meta : $@thin MyStruct.Type): |
| 22 | + %a = alloc_box ${ var MyStruct }, var, name "self" |
| 23 | + %b = mark_uninitialized [rootself] %a : ${ var MyStruct } |
| 24 | + %c = begin_borrow [lexical] %b : ${ var MyStruct } |
| 25 | + %d = project_box %c : ${ var MyStruct }, 0 |
| 26 | + |
| 27 | + // ... initialize properties, etc ... |
| 28 | + |
| 29 | + %end = load [trivial] %d : $*MyStruct |
| 30 | + end_borrow %c : ${ var MyStruct } |
| 31 | + destroy_value %b : ${ var MyStruct } |
| 32 | + return %end : $MyStruct |
| 33 | +} |
| 34 | +
|
| 35 | +
|
| 36 | +// the delegating init MyStruct.init(delegates:) |
| 37 | +sil hidden [ossa] @$s4test8MyStructV9delegatesACyt_tcfC : $@convention(method) (<...>, @thin MyStruct.Type) -> MyStruct { |
| 38 | +bb0(<...>, %meta : $@thin MyStruct.Type): |
| 39 | + // Same allocation as the non-delegating: |
| 40 | + %a = alloc_box ${ var MyStruct }, var, name "self" |
| 41 | + %b = mark_uninitialized [rootself] %a : ${ var MyStruct } |
| 42 | + %c = begin_borrow [lexical] %b : ${ var MyStruct } |
| 43 | + %d = project_box %c : ${ var MyStruct }, 0 |
| 44 | + |
| 45 | + // ... delegate to MyStruct.init(final:) ... |
| 46 | + |
| 47 | + %ctor = function_ref @$s4test8MyStructV5finalACSi_tcfC : $@convention(method) (Int, @thin MyStruct.Type) -> MyStruct |
| 48 | + %ret = apply %ctor(<...>, %meta) : $@convention(method) (Int, @thin MyStruct.Type) -> MyStruct |
| 49 | + |
| 50 | + assign %ret to %d : $*MyStruct |
| 51 | + %end = load [trivial] %d : $*MyStruct |
| 52 | + end_borrow %c : ${ var MyStruct } |
| 53 | + destroy_value %b : ${ var MyStruct } |
| 54 | + return %end : $MyStruct |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +It's important to note that all initializers take a metadata argument, regardless of whether it is a delegating initializer. There is also no separation between allocating and non-allocating initializer entrypoints. All initializers perform allocation. |
| 59 | + |
| 60 | +# Classes |
| 61 | + |
| 62 | +Every designated initializer has two entry-points. One performs allocation (i.e., the "allocating" entry) before continuing at the second entrypoint which does the initialization (i.e., the "initializing" entrypoint). Here's an example of `MyClass.init(final:)`, which is a designated initializer, with its two entry-points: |
| 63 | + |
| 64 | +```swift |
| 65 | +// MyClass.__allocating_init(final:) |
| 66 | +sil hidden [exact_self_class] [ossa] @$s4test7MyClassC5finalACSi_tcfC : $@convention(method) (<...>, @thick MyClass.Type) -> @owned MyClass { |
| 67 | +bb0(%0 : $Int, %1 : $@thick MyClass.Type): |
| 68 | + %2 = alloc_ref $MyClass |
| 69 | + // function_ref MyClass.init(final:) |
| 70 | + %3 = function_ref @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass |
| 71 | + %4 = apply %3(%0, %2) : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %5 |
| 72 | + return %4 : $MyClass |
| 73 | +} |
| 74 | + |
| 75 | +// MyClass.init(final:) |
| 76 | +sil hidden [ossa] @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass { |
| 77 | +bb0(<...>, %1 : @owned $MyClass): |
| 78 | + %4 = mark_uninitialized [rootself] %1 : $MyClass |
| 79 | + |
| 80 | + // ... initialize MyClass ... |
| 81 | + |
| 82 | + %11 = copy_value %4 : $MyClass |
| 83 | + destroy_value %4 : $MyClass |
| 84 | + return %11 : $MyClass |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +In the mangling of these entrypoint labels, the uppercase `C` suffix indicates that it's the allocating entrypoint, whereas the lowercase `c` is the initializing entrypoint. Only the allocating entrypoint is published in the type's vtable: |
| 89 | + |
| 90 | +``` |
| 91 | +sil_vtable MyClass { |
| 92 | + // ... |
| 93 | + #MyClass.init!allocator: (MyClass.Type) -> (<...>) -> MyClass : @$s4test7MyClassC5finalACSi_tcfC // MyClass.__allocating_init(final:) |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +The initializing entrypoint is only referenced by either it's corresponding allocating entrypoint, or by a sub-class that is delegating up in a `super.init` call. For example, if we had: |
| 98 | + |
| 99 | +```swift |
| 100 | +class MyClass { |
| 101 | + var x: Int |
| 102 | + init(final x: Int) { |
| 103 | + self.x = x |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +class MyDerivedClass: MyClass { |
| 108 | + var y: Int |
| 109 | + init(subFinal y: Int) { |
| 110 | + self.y = y |
| 111 | + super.init(final: y) |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +Then the `super.init(final: y)` call directly invokes `MyClass.init(final:)`'s initializing entrypoint, bypassing its allocating init. Here's what that looks like in SIL: |
| 117 | + |
| 118 | +``` |
| 119 | +// MyDerivedClass.__allocating_init(final:) |
| 120 | +sil hidden [exact_self_class] [ossa] @$s4test14MyDerivedClassC5finalACSi_tcfC : $@convention(method) (Int, @thick MyDerivedClass.Type) -> @owned MyDerivedClass { |
| 121 | + // ... calls $s4test14MyDerivedClassC5finalACSi_tcfc in the usual way ... |
| 122 | +} |
| 123 | +
|
| 124 | +// MyDerivedClass.init(final:) |
| 125 | +sil hidden [ossa] @$s4test14MyDerivedClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyDerivedClass) -> @owned MyDerivedClass { |
| 126 | +bb0(%0 : $Int, %1 : @owned $MyDerivedClass): |
| 127 | + %2 = alloc_box ${ var MyDerivedClass }, let, name "self" |
| 128 | + %3 = mark_uninitialized [derivedself] %2 : ${ var MyDerivedClass } |
| 129 | + %4 = begin_borrow [lexical] %3 : ${ var MyDerivedClass } |
| 130 | + %5 = project_box %4 : ${ var MyDerivedClass }, 0 |
| 131 | + debug_value %0 : $Int, let, name "y", argno 1 |
| 132 | + store %1 to [init] %5 : $*MyDerivedClass |
| 133 | + |
| 134 | + // ... initialize self.y ... |
| 135 | + |
| 136 | + // perform the super call. notice the ownership transfer to the super.init. |
| 137 | + %14 = load [take] %5 : $*MyDerivedClass |
| 138 | + %15 = upcast %14 : $MyDerivedClass to $MyClass |
| 139 | + // function_ref MyClass.init(final:) |
| 140 | + %16 = function_ref @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %17 |
| 141 | + %17 = apply %16(%0, %15) : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %18 |
| 142 | + %18 = unchecked_ref_cast %17 : $MyClass to $MyDerivedClass |
| 143 | + store %18 to [init] %5 : $*MyDerivedClass // id: %19 |
| 144 | + |
| 145 | + // return as usual |
| 146 | + %20 = load [copy] %5 : $*MyDerivedClass |
| 147 | + end_borrow %4 : ${ var MyDerivedClass } |
| 148 | + destroy_value %3 : ${ var MyDerivedClass } |
| 149 | + return %20 : $MyDerivedClass |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +# Actors |
| 154 | + |
| 155 | +TODO: do we want to leverage the fact that the initializing entrypoint is only ever used by its allocating init, to merge them together? This effectively closes the door on actor inheritance, and really could just be an optimization done in the future. |
| 156 | + |
| 157 | +TODO: Add a pointer or quick primer on the `mark_uninitialized` kinds. Here's a quick list from the SIL.rst: |
| 158 | + |
| 159 | +``` |
| 160 | +The kind of mark_uninitialized instruction specifies the type of data the mark_uninitialized instruction refers to: |
| 161 | +• var: designates the start of a normal variable live range |
| 162 | +• rootself: designates self in a struct, enum, or root class |
| 163 | +• crossmodulerootself: same as rootself, but in a case where it’s not really safe to treat self as a root because the original module might add more stored properties. This is only used for Swift 4 compatibility. |
| 164 | +• derivedself: designates self in a derived (non-root) class |
| 165 | +79 |
| 166 | +• derivedselfonly: designates self in a derived (non-root) class whose stored properties have already been initialized |
| 167 | +• delegatingself: designates self on a struct, enum, or class in a delegating constructor (one that calls self.init) |
| 168 | +• delegatingselfallocated: designates self on a class convenience initializer’s initializing entry point |
| 169 | +``` |
| 170 | + |
| 171 | +TODO: I think we need to work on extending this in the compiler for actors so that, for example, `delegatingself` can correctly appear in a designated initializer, as it can for a struct. |
| 172 | + |
| 173 | + |
| 174 | + |
0 commit comments