Recently we've encountered a really strange crash that was only reproducible in the release builds of our product. After further investigation I was able to deduce the minimal example.
Consider this program, compiled with Swift 5.9:
class A {}
class B: A {}
func printType<T>(of object: T) {
print(type(of: object))
}
let a: A.Type = B.self
printType(of: a)
Here we are passing metatype value B.self
, casted to A.Type
to generic function printType
. In unoptimized build (swiftc test.swift
) it prints B.Type
which is an expected output.
But if we include -O
flag in compiler invocation, suddenly the program prints A.Type
. So it looks like some optimizations are preventing the type(of:)
from deducing the true type of the object
.
Issue is not reproducible, if printType
is annotated with @_optimize(none)
.
Also, if I rewrite the function without generic usage, it prints B.Type
, as expected:
func printType(of object: Any) {
print(type(of: object))
}
After digging a bit into the disassembly I uncovered that optimized version of the program calls generic specialization <output.A.Type> of output.printType<A>(of: A) -> ()
that is not calling swift_getDynamicType
under the hood, but instead just instantiates Metatype A.Type
and prints its name instead (I also attached @inline(never)
attribute to be sure that the issue is not in inlining):
generic specialization <output.A.Type> of output.printType<A>(of: A) -> ():
pushq %r14
pushq %rbx
pushq %rax
leaq (demangling cache variable for type metadata for Swift._ContiguousArrayStorage<Any>)(%rip), %rdi
callq __swift_instantiateConcreteTypeFromMangledName
movl $64, %esi
movl $7, %edx
movq %rax, %rdi
callq swift_allocObject@PLT
movq %rax, %rbx
movq $1, 16(%rax)
movq $2, 24(%rax)
leaq (demangling cache variable for type metadata for output.A.Type)(%rip), %rdi
callq __swift_instantiateConcreteTypeFromMangledName
movq %rax, %r14
leaq (demangling cache variable for type metadata for output.A.Type.Type)(%rip), %rdi
callq __swift_instantiateConcreteTypeFromMangledName
movq %rax, 56(%rbx)
movq %r14, 32(%rbx)
movabsq $-2233785415175766016, %rdx
movl $32, %esi
movl $10, %ecx
movq %rbx, %rdi
movq %rdx, %r8
callq ($ss5print_9separator10terminatoryypd_S2StF)@PLT
movq %rbx, %rdi
addq $8, %rsp
popq %rbx
popq %r14
jmp swift_release@PLT
For comparison, this is printType<A>(of: A) -> ()
function, emitted just under specialization:
output.printType<A>(of: A) -> ():
pushq %r15
pushq %r14
pushq %rbx
movq %rsi, %rbx
movq %rdi, %r14
leaq (demangling cache variable for type metadata for Swift._ContiguousArrayStorage<Any>)(%rip), %rdi
callq __swift_instantiateConcreteTypeFromMangledName
movl $64, %esi
movl $7, %edx
movq %rax, %rdi
callq swift_allocObject@PLT
movq %rax, %r15
movq $1, 16(%rax)
movq $2, 24(%rax)
movq %r14, %rdi
movq %rbx, %rsi
xorl %edx, %edx
callq swift_getDynamicType@PLT
movq %rax, %r14
movq %rbx, %rdi
callq swift_getMetatypeMetadata@PLT
movq %rax, 56(%r15)
movq %r14, 32(%r15)
movabsq $-2233785415175766016, %rdx
movl $32, %esi
movl $10, %ecx
movq %r15, %rdi
movq %rdx, %r8
callq ($ss5print_9separator10terminatoryypd_S2StF)@PLT
movq %r15, %rdi
popq %rbx
popq %r14
popq %r15
jmp swift_release@PLT
It calls swift_getDynamicType
followed by swift_getMetatypeMetadata
that gives expected result.
This looks like an optimization bug to me, but maybe I am misunderstanding something about generics in Swift?
Hope someone from Swift team could comment on this.