Compiler bug w.r.t. protocol inheritance and generics?

Consider this simple example (Swift 6):

protocol StorableObject { }

actor Storage<V: StorableObject> {
    let value: V
    
    init(value: V) {
        self.value = value
    }
}

protocol Labeled: StorableObject {
    var label: String { get }
}

struct PaperRecord: Labeled {
    var label: String
}

let storage: Storage<Labeled> // compiler error: "Type 'any Labeled' cannot conform to 'StorableObject'"

Consider this for reference:

let labeled: Labeled = PaperRecord(label: "paper 1") // This does work though.

// What also works:
actor AlternativeStorage<V> {
    let value: V
    
    init(value: V) {
        self.value = value
    }
}

let alternativeStorage: AlternativeStorage<Labeled> // No compiler error here!

What am I missing!? Why would the requirement of the generic parameter to conform to my protocol StorableObject break the possibility to use Labeled as a generic parameter for Storage?

swift-driver version: 1.115 Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)
Target: arm64-apple-macosx14.0

A boxed protocol type cannot satisfy a protocol conformance requirement in a generic signature. So you can only pass a boxed protocol type as a generic argument if the corresponding generic parameter is not subject to conformance requirements, like in your second example. (There are exceptions like Sendable and Copyable but that’s a special case.)

This is a frequently-asked question and you can try this search to find some past discussions: Search results for 'Any cannot conform to protocol' - Swift Forums

Here is another query for the formal term that turns up additional discussion: Search results for 'Self-conforming existential' - Swift Forums

5 Likes

Can you link a topic which in your opinion actually discusses the same issue? I'd like to understand where the logical contradiction would arise if we would let this compile. E.g. I found this Type 'any Protocol' cannot conform to 'Protocol' which makes a point about contravariance of the return type of the generic function which is used as an example, that makes a lot of sense to me, but I don't see the connection to my example. Thanks!

I don’t know if I’ve ever seen this explicitly mentioned, but experimentation shows that in the type expression Foo<Labeled>, Labeled is short for Foo<any Labeled>. This is the expected behavior in expressions like let foo: Labeled, but it can be surprising that this expansion also happens in angle brackets.

I don’t think there’s anything unsound about what you’re trying to do, but it would perhaps be a breaking change to stop inferring that a protocol in type argument position refers to its existential projection. Maybe this was discussed in the SE-0335 discussion?

1 Like

If the protocol has associated types, static methods or inits, there would be a soundness issue in allowing it to self-conform. If it doesn't have any requirements of that sort, the issue is that the runtime representation doesn't work out.

An analogy is this: imagine all concrete types that conform to a protocol P have a certain shape, and a requirement where T: P is a statement that T must have this shape. An any P is then a dynamic box for storing concrete values with this shape. However, the any P itself does not have the correct shape to satisfy the where T: P requirement.

The discussion there talks about the language feature where we can open the any P to get the concrete payload out. This was added in Swift 5.7 to address common situations where people run into this limitation.

For example if you have this:

func callee<T: P>(_: T) {}

func caller(p: any P) { callee(p) }

Conceptually, because p is passed as an argument, we can take the concrete type out of the box, and then pass this concrete type as the generic argument for T. So the above code compiles, and we don't encounter the restriction where any P does not conform to P.

However, if you have this, say:

func callee<T: P>(_: [T]) {}

func caller(_ p: [any P]) {
  callee(p)
}

In this case, there is no single concrete substitution for T that also conforms to P, so the above code is rejected with the same error you saw.

1 Like

A protocol without associated types, static methods, or inits can still have semantic requirements that make self-conformance unsound.

Consider a hypothetical marker protocol, HasTwoValues—all conforming types have two possible values. Thus, Bool can conform to HasTwoValues. So can, for example, a hypothetical type ChessPlayer. If both types conform, then the existential type any HasTwoValues now has (at least) four values—therefore, it does not meet the semantic requirement of HasTwoValues.

2 Likes

Right, and if you tried to encode this in the type system, you'd be back to where you started with a static requirement:

protocol HasTwoValues {
  static var twoValues: (Self, Self) { get }
}

Sure—my point was to emphasize that self-conformance can lead to unsoundness even when it's not ruled out by requirements encoded in the type system.

I think understand what you're saying, but I'll nitpick and say that unsoundness usually means you have a scenario where the type checker accepts a piece code, which then miscompiles because the runtime types of values get mixed up in some way. In your example, there might be higher-level invariants that are violated elsewhere in the program, but it's not unsoundness, per se.

4 Likes

I agree that this is an overly broad use of the term “unsound”. The language offers myriad ways to violate your program’s invariants. For example, you could just add another property to a HasTwoValues-conforming type that affects the interpretation of the “two” values. To label all such bad designs as “unsoundness” renders the term meaningless.

In that Swift would not actually miscompile my toy example, sure.

Logically speaking, however, any HasTwoValues is not a HasTwoValues-conforming type irrespective of how many of its semantic requirements are expressed in a way that the type checker can enforce—the takeaway being that Swift as a language shouldn't automatically make protocols self-conforming even if they don't have these requirements encoded in the type system.

I guess I'm using unsoundness with respect to the underlying logic being dealt with, rather than how it's enforced by Swift's type checker in practice, sort of like how we've talked about elsewhere UB-in-theory versus UB-in-practice.

3 Likes