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.