Strange behaviour of type(of:) on metatypes in generic context with optimized build

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.

12 Likes

Strange. I'd also say this is a bug because optimized and unoptimized builds should have the same observable behavior.

Probably unsurprisingly, this not only affects type(of:), but also dynamic type checks with is or as?. If you write the function like this:

func printType<T>(of object: T) {
    print(type(of: object))
    if object is B {
        print("object is B")
    } else {
        print("object is not B")
    }
}

The output is this under -O (i.e. also wrong):

A.Type
object is not B

@shivatinker Did you file a bug at github.com/apple/swift?

4 Likes