SR-14064: Accessing Static Member on Downcast Type not Calling Overridden Implementation

I posted this issue in JIRA already, but wanted to ask here, if this is the expected behavior or a legit bug.

What I'm trying to do is have a static member (required by a protocol) that would return an array of elements of the associated type implemented by a struct using generics. Here's the snippet that demonstrates the implementation of the protocol requirement1:

// Protocol definitions

protocol PreviewProvider {
    static var previewConfigurations: [ViewBuilder] { get }
}

protocol ConfigurableView: AnyView, PreviewProvider { ... }

And the implementation:

class Label: ConfigurableView { ... }
class CarouselView<Component: ConfigurableView>: ConfigurableView { ... }

extension Label {
    static var previewConfigurations: [ViewBuilder] { [
        Label.Configuration(text: "Regular size label", fontSize: 1),
        Label.Configuration(text: "Bigger label", fontSize: 2),
        Label.Configuration(text: "Even bigger label", fontSize: 3),
    ] }
}

extension CarouselView {
    static var previewConfigurations: [ViewBuilder] { [] }
}

extension CarouselView where Component == Label {
    static var previewConfigurations: [ViewBuilder] { [
        CarouselView.Configuration(
            items: [
                Label.Configuration(text: "Hello", fontSize: 1),
                Label.Configuration(text: "World", fontSize: 1),
            ])
    ] }
}

What I wanted to achieve was a way to enumerate an array of these types:

[Label.self, CarouselView<Label>.self]
  .map { $0.previewConfigurations }

Which outputs:

[Label.Configuration, Label.Configuration, Label.Configuration],
[]

instead of:

[Label.Configuration, Label.Configuration, Label.Configuration],
[CarouselView<Label>.Configuration]

The previewConfigurations static member I'm calling should've called the implementation found in the extension with the conditional conformance, but instead calls the generic one (which yields an empty array).

But if I call the static member directly, it will call the right one.

CarouselView<Label>.self.previewConfigurations

which will output an array of one element (as it should):

[CarouselView<Label>.Configuration]

But if I downcast it to the protocol type, it will call the member in the generic implementation:

(CarouselView<Label> as PreviewProvider.type).self.previewConfigurations

and output an empty array:

[]

What's the expected behavior?

1. The whole code example and a Swift Package where you can run the test is included in the JIRA ticket SR-14064.

It's an expected behaviour. In particular, the protocol conformance for a generic (CarouselView) is the same regardless of the generic parameter. No matter what Component is, CarouselView.previewConfigurations will always use the same declaration. In this case, the most specific declaration that is always available to all conforming Carousel is the "empty array" one.

2 Likes

CarouselView can conform to ConfigurableView in only one way, meaning that it must have an implementation of previewConfigurations that works for any CarouselView, and that is the implementation to which a call to ConfigurableView.previewConfigurations will be dispatched dynamically. You have provided such an implementation: { [] }.

You can create as many implementations as you want named previewConfigurations, but there is no magic here. They shadow the implementation above because they share the same name, but they do not override it. There is still one and only one implementation for the protocol requirement: { [] }.

3 Likes

Thanks @Lantua & @xwu for such a prompt response! (I love how engaged this community is).

I see. The thing that puzzles me is, why does the dynamic dispatch work correctly if called it directly on the concrete type then?

CarouselView<Label>.previewConfigurations -> [CarouselView<Label>.Configuration]

Or to make a better example:

let componentType = CarouselView<Label>.self

let resultA = componentType
XCTAssertEqual(resultA.previewConfigurations.count, 1) // passes

let resultB: PreviewProvider = componentType
XCTAssertEqual(resultB.previewConfigurations.count, 1) // fails

But I think I get it, the PreviewProvider.previewConfigurations and CarouselView<Label>.previewConfigurations are two different types with two different virtual tables — is that why?


Edit: here's demonstrating this shortcoming using a much simpler example:

// Definition
protocol P {
    static func foo()
}
class A<T>: P { }

// Implementation
extension A {
    static func foo() { print("foo") }
}
extension A where T == Int {
    static func foo() { print("bar") }
}

where:

let classA_String = A<String>.self
let classA_Int = A<Int>.self
let classA_Int2: P.Type = A<Int>.self // downcast
classA_String.foo()
classA_Int.foo()
classA_Int2.foo()
// > foo
// > bar
// > foo?

Here's a silly drawing demonstrating the execution:
invariance
If I understand this correctly, you can't do conditional conformance on types with generics due to the lack of covariance. I think they're talking about the same case in the Generics manifesto.

So, this is definitely not expected behavior — it might not a bug, but more of a feature gap.

It's not dynamic dispatch. It's a much simpler* shadowing. Let's jump onto your new code. P has only one definition of foo, the protocol requirement, while A has three:

  • The unconstrained one (foo),
  • The one constrained to T == Int (bar), and
  • The one used for protocol conformance.

When you do A<Int>.foo, the compiler chooses the most specific one among the three, which is (bar). In fact, if the compiler doesn't know that T == Int, it can only choose among the first and the third ones, both of which are foo.

func x<T>(_: A<T>.Type) {
  A<T>.foo()
}

x(A<Int>.self) // foo

Swift needs to be able to generate unspecialized function correctly. It needs to be able to generate a binary for my x(_:) function with unknown T, i.e. unspecialized x, which is usable by any x<T>, including x<Int>. That's why, inside x, the compiler can't use the second definition of foo.

As a rule of thumb, if you didn't write override, they are different, unrelated functions.

Covariance plays very little role here (A<String> and A<Int> are unrelated regardless of variance support). There are a lot of constraints going into the design around this area (not to mention there are actual bugs riddled around). Unfortunately, it leads to a somewhat complex behaviour.

That's a different one.


* Well, I said simpler, but the precise shadowing rule is elusive to even the most veteran. We did explore a lot of corners a while back, though I think we ended up running around in circle.

1 Like

You can very much conditionally conform a generic type to a protocol. Again: you cannot, however, conform any type to any protocol in more than one way. In your example, your generic type unconditionally conforms to the stated protocol, and that is the one and only conformance. This is the expected behavior and will not change regardless of any future generics features.

3 Likes