Compiler crash, conditional conformance and standard library protocols

The compiler crashes with a segmentation fault 11 when I try to use a generic function, on a generic type, with conditional conformance and standard library protocols:

struct TestType<T: Error> { }

extension Array: Error where Element: Error { }

struct GenericType<G> {
    func test<T>(_ value: TestType<[T]>) { }
}

If I change almost anything about this, like using a homemade protocol instead of Error, use unconditional conformance, put TestType<[T]>as the return type instead of a parameter, or use a non-generic outer type, it works.

Giving it more information by constraining T doesn't help.

Full investigation here: Generic segmentation fault · GitHub

Is this a known bug?

Not sure if it’s known, but the compiler shouldn’t crash with any code, so it’s reasonable to go ahead a file a bug in any case.

However, in general, it’s important to know that it’s not supported to conform a type you don’t own to a protocol you don’t own. That’s liable to stop compiling or exhibit different behavior at any time.

Alright, so then I suppose the compiler should really have complained on the second line! That conformance works fine and I can use it in lots of places, but not in this very specific case.

Or rather, the one I'm using is for RawRepresentable which is very useful on Arrays of them. I suppose I should make my own protocol then? The tricky part is that there's no way to say

All types that conform to RawRepresentable also conform to MyRawRepresentable

At least as far as I can tell. So I need to manually annotate all my enums with : MyRawRepresentable.

I’m not sure what you mean by “not supported”.

The ability to retroactively conform a type to a protocol is a well-defined core part of the Swift language.

The fact that libraries may evolve in such a way that client code breaks in the future, does not affect whether the language itself supports a particular construct.

In this case, conforming a type to a protocol is 100% “supported” by the language, regardless of where each piece comes from.

2 Likes

Retroactive conformance is a key feature of Swift. Anyone is able to conform a type they don’t own to a protocol that they own, just like they can conform a type they own to a protocol they don’t own. Breaking that in any way would be out of the question.

It is not supported, however, to conform a type that you don’t own to a protocol that you don’t own—for example, a standard library type to a standard library protocol. I mean “unsupported” in the same sense that an underscored feature such as @_specialize is unsupported. Any specific conformance today that falls under this category may compile today but may cease to compile tomorrow (for example, when the standard library adds the conformance itself) or may compile but exhibit different behavior, and it will not be considered a breaking change. To be clear, such scenarios are amply evidenced in past proposals already implemented.

If I recall, there is or was a draft PR in the Swift repository to add a compiler warning to help educate users; the status of that PR doesn’t come to mind.

2 Likes

Underscored attributes are documented as being unsupported. They are an implementation detail of the compiler or standard library.

Do you have a reference to similar documentation for the types of protocol conformance under discussion?

To the best of my knowledge there is no such documentation.

The ability for any programmer to conform any type to any protocol has been part of Swift since its initial release, before the language was open-sourced. And I do not recall any evolution proposal to change that being approved.

As far as I can tell, this type of conformance is and has been a core feature of Swift from the very beginning, and dropping support for it would be a severely breaking change to the language.

Not off the top of my head. To be clear, however, what I'm describing has always been the case (i.e., that any such retroactive conformance is liable to stop compiling or change behavior at any time); there is no support being dropped. If there is missing documentation about that, then it's fair to have that documentation added.

I think we're talking about subtly different things.

The ability for any programmer to conform any type to any protocol has always been there, just as there is the ability for any programmer to use underscored features. There is no move to change that.

You and I have both participated in past threads about how to actually design something that works in terms of actually supporting multiple conformances so that types not owned by the user can be reliably conformed to protocols not owned by the user, but nothing has borne fruit yet.

This is a conversation in the "Using Swift" section; I'm simply stating that, at present, such conformances are not in fact supported, in that they are liable to break at any time. I'm not saying at all that they shouldn't be supported (or even claiming that they couldn't be); simply, they aren't.

1 Like

I think it makes sense, but I wonder why the compiler doesn't at least warn me when I extend an array of RawRepresentable to also be RawRepresentable for example.

One way to achieve the same thing would be if I make a shadow protocol with the same requirements, but the problem again is that I cant say

extension Self: MyRawRepresentable where Self: RawRepresentable { }

I think we’re using the word “supported” in different ways.

I am saying that the language itself does support these retroactive conformances, always has, is documented to do so, and the behavior is exactly as one would expect.

If a library is updated to provide a conformance which it did not previously provide, then of course that will break clients which provide the conformance themselves: Swift prohibits duplicate conformances of the same type to the same protocol in different ways.

And I am saying that they are supported, and always have been.

I’m not claiming such a conformance is resilient to changes in dependencies, nor that it should be. I am saying that in the situation where none of the dependencies provide the conformance in question, that it is completely fine for a programmer to provide the conformance themself.

The language supports that usage, and there is nothing in the language documentation which would suggest otherwise.

1 Like

Come on, it might work to conform stdlib types to stdlib protocols, but you have to accept that it's not supported, meaning Apple does not guarantee your code that's doing that will still compile tomorrow, or run properly.

That's what "not supported" means, and I think we should take Xiaodi's word for it. It also makes sense, they could decide to add those conformances at any time, and that could break your code, so it's clearly not a good idea to do it.

They do support conforming your own types to any protocol, so that will not stop working.

1 Like

There is a difference between a library supporting something, and the language supporting something.

The Swift language supports conforming types to protocols. This is documented in the documentation for the Swift language.

That documentation has no restriction on where either the type or the protocol come from.

The language supports this type of retroactive protocol conformance, by virtue of the fact that the language supports all protocol conformances, without except.

1 Like

There are a few issues worth clarifying here:

When it comes to resilient libraries (such as the standard library, which is what we're talking about here), it's not "completely fine" to provide a conformance yourself for one of its types simply because there isn't one already. This is because, where a future version of the resilient library later adds a conformance, which of the implementations is then called at runtime is undefined (since Swift is designed for types to conform to protocols in only one way), meaning that the behavior of existing binaries can change in unexpected ways. I surmise that, if this happens to your app, you'd describe the situation as something other than "completely fine." That the documentation does not make note of this issue explicitly doesn't mean that it doesn't exist: Swift does not support a type conforming to a protocol in more than one way (this is documented); when you conform a resilient type you don't own to a resilient protocol you don't own, then you are setting up for a scenario in which a type has multiple conformances to the same protocol--which is not supported.

While we're talking about the standard library, let's be cognizant that it is not a dependency that's separable from the language. Indeed, one of the criteria for what goes into the standard library rather than a core library is that the item in question requires language support. In the scenario provided here, we're discussing the conformance of Array to Error: both of these types have baked-in "magic." For this reason, it's not accurate to say that "the language" supports a usage but "the dependency" does not. Indeed, I suspect the reason the compiler is crashing here has to do with some magical feature of these types not expressible in Swift which isn't playing well with retroactive conformance, whether it be the covariant Element type of Array or the self-conformance of Error to itself.

Ultimately, I think clearly we mean different things by "support." Swift has reached a state of maturity where users rightly expect a certain degree of stability. From my perspective, it would be inaccurate to tell a user that code they're writing is supported by the language when the language (of which the standard library is an integral part) explicitly reserves the right to make the code stop compiling or behave differently at any time and without warning. This is not the level of "support" I think users expect from Swift.

4 Likes

For what it’s worth, it’s not because of Error’s self-conformance, it also happens for other protocols like RawRepresentable. Are there any built in types and protocols without any magic that I could test on?

I also realised that while I’ve tried with my own protocols, I haven’t tried replacing Array with my own generic type.

Result doesn't have any magic, I think. Most protocols should be free of magic too.

There are many situations where an additive change to a library (such as the standard library) can cause clients of that library to stop compiling.

For example, if the standard library introduces a new operator, then any program which had already declared that operator for itself will fail to compile.

Similarly, if the standard library introduces a new generic function, then any program which had already declared a similarly-named function such that neither one is a refinement of the other with respect to generic constraints, will fail to compile.

(More precisely, call-sites will become ambiguous and thus fail to compile, if the types involved could satisfy both sets of constraints.)

Additive changes like these have been official declared “non-breaking” and “source-compatible”, despite the fact that they can directly break client code.

• • •

I see protocol conformances in the same exact light. A library adding a conformance is an additive change.

Many different additive changes can break client code. That is standard and normal and expected.

The fact that an additive change to a library may break client code is not relevant.

• • •

I don’t know enough about ABI stability to comment on that aspect, but the fact remains:

The Swift language is documented to allow anyone to conform any type to any protocol.

There are no documented exceptions to that, and any attempt to create a new exception would be massively source-breaking.

As a point of fact, neither of these is true, and the compiler has specific rules to ensure it. For instance, the following code which deliberately introduces conflicting definitions compiles just fine, and its behavior shadows the standard library's behavior (and not merely as a happy accident but because the compiler bends over backwards to make sure of it):

infix operator * : AdditionPrecedence // Evil. Do not use.
func max<T: Comparable>(_ x: T, _ y: T) -> T {
    return Swift.min(x, y) // Evil. Do not use.
}
print(max(2, 4) * 2 / 4)
// Uses custom definitions
// Equivalent to the following in "standard" Swift: min(2, 4) * (2 / 4)

Just because something isn't documented to be unsupported doesn't mean that it is supported. If you have a source that explicitly claims that anyone can conform any type to any protocol, I'd love to see it. We should probably then edit that text to better reflect the reality of the situation.

It has always been the case that any conformance of a type you don't own to a protocol you don't own can break, as you acknowledge, so it's hard to reckon how any change in this area would be more massively source breaking than the status quo. Particularly since the purpose of any change on the language side would be for the purpose of preventing unintentionally brittle code (i.e., source breakage).

2 Likes

In my testing, the following fails to compile with an ambiguity error:

func + (lhs: Int, rhs: Int) -> Int {
  return 0
}

let x = 1 + 2   // error: Ambiguous use of operator '+'

As does this:

func max<T: Numeric>(_ x: T, _ y: T) -> T {
  return 0
}

let a = max(1, 2) // error: Ambiguous use of 'max'

• • •

Look in the documentation where it says that “extension Foo: SomeProtocol” is valid Swift.

That is my evidence that Swift programmers can extend types to conform to protocols.

Does the documentation list any exceptions based on where Foo and SomeProtocol come from?

The burden of proof falls on the party making a positive claim of existence. In this case, you are the one making the positive claim that such an exception exists.

Please provide a citation for the existence of such an exception.

I want to point out that this is not what the Swift team has called retroactive conformance. Retroactive conformance is when you own neither the type nor the protocol. Other languages (Haskell, Rust) have rules against this; Swift does not.

7 Likes

That's a very good point. The compiler's rules of overload resolution specifically prioritize user declarations that exactly shadow standard library functions, but it doesn't do anything for overloads such as these that don't have exactly matching generic constraints. Worth filing a bug about this!

We don't have any disagreement on the facts, as far as I can tell. Swift programmers can extend types to conform to protocols, and there are no exceptions to that; it's certainly "valid Swift." Some of those extensions can also be broken at any time--namely, those where the extension author doesn't also control either the type, the protocol, or both. I call this "unsupported" and you call this "supported"; I think the latter terminology is misleading to the user, because a feature that leaves the user no way of ensuring their code will compile tomorrow or work correctly is not what I would call "supported." The Swift compiler doesn't support all valid Swift. What is there to prove or disprove?

Thanks; I've been sloppy in my use of terminology.

2 Likes

It seems like there wasn’t a formal rule against this, at least in 2018, since Jordan then proposed this as a new rule:

1 Like