Pitch: Warning for Retroactive Conformances of External Types in Resilient Libraries

Previous discussion thread here: Warning for retroactive conformances if library evolution is enabled

Hi folks! I'd like to revive discussion on this warning to identify problematic retroactive conformances before they're locked into an ABI. I've attached a pitch below, and I would like to bring it up on Evolution soon.

Normally, warnings don't need to go through evolution, but when I last spoke to core team members about this, they felt it was consequential enough to warrant evolution discussion. I've updated the implementation at apple/swift#36068 and I think it's ready to go.

Warning for Retroactive Conformances of External Types in Resilient Libraries

Introduction

Many Swift libraries vend currency protocols, like Equatable, Hashable, Codable,
among others, that unlock worlds of common functionality for types that conform
to them. Sometimes, if a type from another module does not conform to a common
currency protocols, developers will declare a conformance of that type to that
protocol within their module. However, protocol conformances are globally unique
within a process in the Swift runtime, and if multiple modules declare the same
conformance, it can cause major problems for library clients and hinder the
ability to evolve libraries over time.

Motivation

Consider a library that, for one of its core APIs, declares a conformance of
Date to Identifiable, in order to use it with an API that diffs elements
of a collection by their identity.

// Not a great implementation, but I suppose it could be useful.
extension Date: Identifiable {
    public var id: TimeInterval { timeIntervalSince1970 }
}

Now that this client has declared this conformance, if Foundation decides to
add this conformance in a later revision, this client will fail to build.
If this is an app client, that might be okay --- the breakage will be confined
to their process, and it's their responsibility to remove their conformance,
rebuild, and resubmit their app or redeploy their service.
However, if this is a library target, this conformance propagates down to every
client that imports the library. This is especially bad for frameworks that
are built with library evolution enabled, as their clients link against
binary frameworks and usually are not aware these conformances don't come from
the actual owning module.

Proposed solution

This proposal adds a warning when library evolution is enabled that explicitly
calls out this pattern as problematic and unsupported.

/tmp/retro.swift:3:1: warning: extension declares a conformance of imported type 'Date' to imported protocol 'Identifiable'; this is not supported when library evolution is enabled
extension Date: Identifiable {
^

If absolutely necessary, clients can silence this warning by explicitly
module-qualifying both of the types in question, to explicitly state that they
are intentionally declaring this conformance:


extension Foundation.Date: Swift.Identifiable {
    // ...
}

Detailed design

This warning is intentionally scoped to attempt to prevent a common mistake that
has bad consequences for ABI-stable libraries.
This warning does not trigger for conformances of external types to protocols
defined within the current module, as those conformances are safe.

Source compatibility

This proposal is just a warning addition, and doesn't affect source
compatibility.

Effect on ABI stability

This proposal has no effect on ABI stability.

Effect on API resilience

This proposal has no effect on API resilience.

Alternatives considered

Enabling this warning always

This pattern is technically never a good idea, since it subjects your code to
runtime breakage in the future. However, I believe the risk to individual apps
is much lower than the risk of shipping one of these retroactive conformances in
an ABI-stable framework.

Putting it behind a flag

This warning could very well be enabled by a flag, but there's not much
precedent in Swift for flags to disable individual warnings.

21 Likes

I love it! I’d probably be on the side of “warn always” rather than just for resilient libraries. This comes up enough on the forums as something you Shouldn’t Do that it feels like we should just bake that into the language (and we have a silencing method for those that really think they need it).

7 Likes

I too agree it’s perfectly reasonable to ask users to “module-qualify” the types and protocols involved whether in library evolution mode or not.

That said, I think the major idea being pitched here is that such retroactive conformances are to be made explicitly unsupported in library evolution mode, which is something a step beyond. It would dilute the seriousness of the warning and its silencing mechanism to have them equally in scenarios where the conformance is explicitly supported in Swift even if often unwise.

As I understand it, the reason that the pitch is not for disallowing this usage altogether in library evolution mode is to permit resilient libraries vended in lock-step to continue to use retroactive conformances. This could apply equally to third-party package conglomerates as it does already to Apple’s. If we had some formal way of designating these relationships (which to my mind are in the same vein as @_spi and cross-import overlays) then the warning could be made an outright error in library evolution mode.

4 Likes

I would add this in the warning, finding this details online is never easy.

Yep, the warning includes a note with this information and a fix-it that inserts them. I am considering removing the fix-it, to discourage folks from just silencing the warning and not considering the ramifications.

6 Likes