Hello everyone,
I'm trying to make sense the way existential containers are represented/handled at the SIL level. Unfortunately, I am struggling to understand a couple of things, and were hoping for your enlightenment.
Consider the following code in Swift:
protocol Pr {
func foo()
}
struct St: Pr {
func foo() {}
var bar = 123
}
func fn() {
let a: Pr = St()
let b = a as! St
}
The part of the SIL in which I am interested relates to the two statements of the function fn
.
The initialization of a
looks something like this:
%0 = alloc_stack $Pr, let, name "a"
// ...
%4 = init_existential_addr %0 : $*Pr, $St
store %3 to [trivial] %4 : $*St
My current understanding of init_existential_addr
(synthetized from the docs) is that it initializes an existential container prepared to store a value of type St
at the given address. This would be consistent with the store
instruction that follows.
Now, the initialization of b
looks something like this:
%6 = alloc_stack $Pr
copy_addr %0 to [initialization] %6 : $*Pr
%8 = alloc_stack $St
unconditional_checked_cast_addr Pr in %6 : $*Pr to St in %8 : $*St
My interpretation of this snippet is as follows.
-
%6
is a temporary "container" that will be use to copy the value ofa
. -
copy_addr
copies the whole container at%0
, i.e., the data, the witness and the protocol witness tables. -
%8
represents the storage ofb
, a stack allocation of typeSt
. -
unconditional_checked_cast_addr
casts the address of the container in%6
to that of value of typeSt
and moves? it in%8
, destroying the temporary in the process.
What I am not sure to get is how unconditional_checked_cast_addr
is able to determine that %6
(its first operand) is indeed the address of an St
value. This, in turn, challenges my understanding of almost all other instructions.
My first assumption is that alloc_stack
allocates enough memory to store an "instance" of its type argument. If the latter is a concrete type (e.g., St
), then this amounts the type's stride. But for an existential type (e.g., Pr
), then this amounts to the size of an existential container (3 words + pointer to witness + pointer to protocol witness tables).
In this particular example, I can imagine that the contents of a value of type St
"fits" within the 3 words of the container. Hence, it would make sense that %6
is the address of a block that can be reinterpreted as a value of type St
. But the same SIL will be generated if I bloat the size of St
(e.g., redeclaring bar
as a (Int, Int, Int, Int)
). In this case, my understanding is that the contents of the existential container will be allocated on the heap, and its 3 words of data will just contain a pointer to this memory. From this assumption, I would guess that init_existential_addr
would be responsible for allocating this heap memory (while destroy_addr
would be responsible to deallocate it) and returning its address, which would be %4
here. This is consistent with the first store
instruction, which initializes a
. But then the cast instruction no longer makes any sense, as%6
would be the pointer to a pointer of an St
value.
Could any one point to me which of my assumptions are incorrect?