SE-0267 — `where` clauses on contextually generic declarations

But shouldn't we be speaking about runtime versions instead? So if an app built with Swift 6 (new mangling) depends on a framework built with Swift 5, it is forced to use the old mangling to be able to safely talk to that framework. Can the runtime handle this?

1 Like

Yes, I saw, this. But protocol requirements fall into this category and are specifically excluded. It wasn't clear whether you were only excluding requirements or excluding all declarations attached to a protocol.

Thanks! With this clarification I am very much supportive of the proposal. If the name mangling / ABI issue can be sorted out that's even better (one less thing for binary-stable library authors to worry about).

I would like to see a future direction to introduce support for where clauses on all declarations in a generic context. This proposal is a great step forward, but as it stands some declarations (such as properties) will still only be possible in a constrained extension. It would be great to lift that limitation as well and have complete flexibility in how we organize our declarations.

2 Likes

Yeah, in the world of binary compatibility, your deployment target can't affect your ABI, because people drop old versions after a few years (and Apple's frameworks always have the newest deployment target). We'd need yet another setting of "the oldest deployment target your clients support". This is implementable, but does need design.

(Also, for Anthony: "10.15 or older" is correct; the app may be targeting 10.15, or 10.14, or… But it's not even really about that; it's about the framework moving from targeting 10.15 to 10.16 without breaking binary compatibility.)

Ah, right, of course it's "or older". As far as I understand, ABIs talk to each other through the runtime. If we emit two declarations with different mangled names, older frameworks will pick the old one and new ones, obviously, - the new canonicalized one. But is it absolutely necessary to emit a second declaration, that is, would it be viable to have two mangled names for one thing, and have the runtime decide which one to look for in-place?

Sometimes a client talks to a library through the runtime and sometimes a client talks to a library directly. "ABI" ("application binary interface") covers both aspects of this, and unfortunately both need to be able to uniquely identify declarations (which is what mangled names do).

I see, thanks! Sorry if this is another dumb question: why can we not "point" the old mangled name to the new one, similar to how we use type aliases for obsoleted types in the Standard Library. The declaration won't have a unique name, but we can still uniquely identify it.

1 Like

Again, that would work for functions, initializers, and subscripts, but types appear inside other mangled names. Mangled names for types are also used to communicate with the runtime, and I'm pretty sure the old runtime wouldn't be able to find types under the new names even if you emitted symbols for them.

1 Like

Yes, I understand now that types are out of question, I was thinking if we can avoid emitting a second declaration just to have that old mangled name around. So it was "emitting a copy under the old mangling vs emitting the original mangled name and somehow pointing it to the canonical one".

The review period for this proposal ended last Thursday, October 31, 2019. The core team has decided to accept this proposal with one modification. The proposal addresses the issue of conditional protocol requirements by disallowing constraints involving Self from being applied to protocol requirements, but it should also do so for non-final class methods, to avoid the same problem with conditional dynamically-dispatched methods in classes.

Brent Royal-Gordon raised the valid concern that this change exposes more opportunities for surprising ABI breaks:

However, as Jordan Rose noted, this problem already exists in the mangling implementation today:

So this is not a new problem, and the Core Team does not think that SE-267 makes it substantially worse. The ABI concern should not prevent implementation of this proposal.

Thanks to everyone who participated in the review!

7 Likes

Are you referring to something like this (this actually compiles)?

class Class<U> {
  func foo<T>(arg: T) where U: Equatable {
    print("Class")
  }
}

class Sub<U>: Class<U> {
  override func foo<T>(arg: T) where U: Sequence {
    print("Sub")
  }
}

let bar: Class = Sub<Int>()
bar.foo(arg: 0) // "Sub"

Edit

Oops, it gets diagnosed correctly on master.

Yep, this won't compile on master anymore, I fixed a long standing bug with generic signatures being ignored while matching overrides.

/Users/suyashsrijan/Desktop/test.swift:2:8: error: unexpected note produced: overridden declaration is here
  func foo<T>(arg: T) where U: Equatable {
       ^
/Users/suyashsrijan/Desktop/test.swift:8:17: error: unexpected error produced: overridden method 'foo' has generic signature <U, T where U : Sequence> which is incompatible with base method's generic signature <U, T where U : Equatable>; expected generic signature to be <U, T where τ_0_0 : Equatable>

If that's accepted by the compiler, it's a bug, because the override wouldn't apply for all subclass instances, and as your example shows, it would get unsoundly invoked on objects that don't match the subclass method constraints.

1 Like

Nice! I will make sure it does so for non-generic members too.

1 Like

Does the implementation ban overloading methods that only differ in one generic constraint?

struct SomeWrapper<Wrapped> {
  let wrapped: Wrapped
}

protocol HasIdentity {
  static func ===(lhs: Self, rhs: Self) -> Bool
}

extension SomeWrapper: Equatable {
  static func ==(lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapped>) -> Bool where Wrapped: Equatable {
    return lhs.wrapped == rhs.wrapped
  }

  static func ==(lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapped>) -> Bool where Wrapped: HasIdentity {
    return lhs.wrapped === rhs.wrapped
  }
}

Nope, you can overload just like you would via constrained extensions.

But then this is a bug right? As via @Douglas_Gregor proposal this would create a workaround for banned overlapping implementation of protocol requirements, or am I wrong here?

Original example:

https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md#overlapping-conformances

1 Like

I will have to test how this compiles before jumping into further conclusions, but what I expect is a conformance error, since the conformance is not actually stated as conditional*, and the == signatures do not match the requirement signatures.

* conformance when <constraints> { potential witnesses }
vs conformance { pot. witnesses }

1 Like

I just tested this and luckily the conformance error is raised as expected. Conformances certainly deserve some test coverage though, so thanks a lot for calling this out!

One thing worth noting: the following simplified version will compile due to the synthesized conformance. The explicit == are just overloads, and they cannot be dispatched to dynamically.

struct SomeWrapper<Wrapped> {}

extension SomeWrapper: Equatable {
  static func ==(
    lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapped>
  ) -> Bool where Wrapped: Equatable {
    print("overload")
    return true
  }
}
let a = SomeWrapper<Int>()
let b = SomeWrapper<Int>()

func areEqual<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
   return lhs == rhs
}

print(b == a) // overload, true
print(areEqual(a, b)) // true
1 Like

We should probably then warn in this circumstance (to silence the warning, separate the extension declaring conformance from the extension implementing the non-conforming overload).

3 Likes

Since synthesized conformances can only be declared internally, we could suggest to declare the conformance directly on the type instead. Stating these conformances on extensions is generally a source of confusion anyway.

Terms of Service

Privacy Policy

Cookie Policy