-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[AutoDiff] [Sema] Limit implicit @differentiable
attribute creation.
#33776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@swift-ci Please smoke test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Thank you for this fix!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on fixing the crasher, but IMO the semantics implied here could be improved. Why should a refining protocol declare an explicit @differentiable
attribute when it can be inherited?
In the example you gave:
protocol P1: Differentiable {
@differentiable(wrt: self)
func callAsFunction(_ input: Float) -> Float
}
protocol P2: P1 {}
extension P2 {
@differentiable(wrt: (self, input))
public func callAsFunction(_ input: Float) -> Float {
return input
}
}
I don't see any issues with generating the derivative for P2. callAsFunction(_:)
with respect to self
, because any call to the derivative would need to call through protocol dispatch.
The error message also seems non-standard: file1.swift:14:15: note: instance method 'callAsFunction' must have explicit '@differentiable(wrt: self)' attribute to satisfy requirement instance method 'callAsFunction' (in protocol 'P1') because it is declared in a different file than the conformance of 'S' to 'P1'
public func callAsFunction(_ input: Float) -> Float {
^
@differentiable(wrt: self) This seems to be suggesting that it's the other file's responsibility to fix, while providing no ways to actually fix it within the current file. What if this is imported from a different module? |
Ah, good question. Here's the original example again: // file1.swift
import _Differentiation
protocol P1: Differentiable {
@differentiable(wrt: self)
func callAsFunction(_ input: Float) -> Float
}
protocol P2: P1 {}
extension P2 {
@differentiable(wrt: (self, input))
public func callAsFunction(_ input: Float) -> Float {
return input
}
} // main.swift
import _Differentiation
struct S: P2 {} The issue is the trigger of implicit Currently, implicit It would be nice in theory to make Does this make sense? Do you have other ideas? |
That's a great point. How about this message instead:
The phrasing Edit: we discussed offline and agreed this message is better. Context: error messages like these are precedented for other protocol witness mismatch kinds. Here's an // file1.swift
protocol P1 {
func method()
}
protocol P2: P1 {}
extension P2 {
mutating func method() {}
} // main.swift
struct S: P2 {} $ swiftc file1.swift main.swift
main.swift:4:8: error: type 'S' does not conform to protocol 'P1'
struct S: P2 {}
^
file1.swift:8:17: note: candidate is marked 'mutating' but protocol does not allow it
mutating func method() {}
^
file1.swift:3:8: note: protocol requires function 'method()' with type '() -> ()'
func method()
^ |
@swift-ci Please smoke test |
We discussed a bit offline, but I feel we've missed an important issue in today's semantics as well as the semantics proposed in this PR. I think there are multiple aspects to the issue that this PR is trying to mitigate, but I think the root problem is the fact that conformances are implicitly changing declarations in protocol extensions. Let me dive into this with examples. Case 1: Candidates in protocol extensions that do not matchFirst, should declarations in a protocol extension that almost matches a protocol requirement be modified so that it can be treat as a requirement? Similar to what you posted above, such an example would be: protocol P1 {
func method()
}
protocol P2: P1 {}
extension P2 {
mutating func method() {}
}
struct S: P2 {} In this example, the protocol extension method does not (but almost!) matches the protocol requirement. Note that it has nothing to do with whether the conformance is declared in the same file or not. The example above results in compilation errors similar to the ones you've shown. Therefore, a similar AD example should fail as well, namely: protocol P1: Differentiable {
@differentiable(wrt: x)
func method(x: Float) -> Float
}
protocol P2: P1 {}
extension P2 {
@differentiable(wrt: (self, x))
func method(x: Float) -> Float { return 0 }
}
struct S: P2 {} However this succeeds to compile today, because Sema is adding an implicit // File 1
protocol P1: Differentiable {
@differentiable(wrt: x)
func method(x: Float) -> Float
}
protocol P2: P1 {}
extension P2 {
public func method(x: Float) -> Float { return 0 }
}
struct S1: P2 {} // Removing this will non-obviously cause file 2 to fail to compile // File 2
struct S2: P2 {} File 1 defines the Now, we modify file 1 by doing something that's seemingly completely harmless to file 2, that is, removing To sum up, IMO the root problem is not at all related to whether conformances are declared in the same file as the witness candidate in a protocol extension, it is the fact that protocol conformance checking is modifying arbitrarily other declarations implicitly, causing their interface to change in an entirely non-obvious way. To solve this problem, for this example: public protocol P1: Differentiable {
@differentiable(wrt: x)
func method(x: Float) -> Float
}
public protocol P2: P1 {}
extension P2 {
public func method(x: Float) -> Float { return 0 }
}
struct S1: P2 {} IMO the right behavior we should go with is to not emit any implicit Case 2: Candidates in protocol extensions that effectively matchNow, back to the original example in your PR description: import _Differentiation
protocol P1: Differentiable {
@differentiable(wrt: self)
func callAsFunction(_ input: Float) -> Float
}
protocol P2: P1 {}
extension P2 {
@differentiable(wrt: (self, input))
public func callAsFunction(_ input: Float) -> Float {
return input
}
} struct S: P2 {} Semantically, Case 2 is nontrivial to implement but we need to fix case 1 to avoid those semantic issues in cross-file and cross-module cases. To summarize, today's protocol conformance checking is adding implicit attributes to other declarations. IMO this attribute-adding behavior should be limited, in a way that is more limiting than what you've done for this patch, to only declarations declared in the conforming type's declaration context. Does that make sense? |
Thanks for the detailed write-up!
I think this makes sense, and I'll look into it. Edit: it actually matters whether the witness declaration is in the same file as the conformance. Here's a simple example: // main.swift
// Conformance declaration here.
extension S: P {} // other_file.swift
import _Differentiation
protocol P: Differentiable {
@differentiable(wrt: (self, x))
func method(_ x: Float) -> Float
}
struct S: Differentiable {
// Witness declaration here.
func method(_ x: Float) -> Float { x }
}
This is because compiling To get the desired semantics, I'll limit implicit New error message:
|
During protocol witness matching for a protocol requirement with `@differentiable` attributes, implicit `@differentiable` attributes may be created for the witness under specific conditions (when the witness has a `@differentiable` attribute with superset differentiability parameters, or when the witness has less-than-public visibility). Do not generate implicit `@differentiable` attributes for protocol witnesses when the protocol conformance is declared from a separate file from the witness. Otherwise, compilation of the file containing the conformance creates references to external symbols for the implicit `@differentiable` attributes, even though no such symbols exist. Resolves SR-13455.
Change phrasing to remove "blame" from the non-matching witness. Remove redundant DescriptiveDeclKind arguments.
…col witnesses. Require explicit `@differentiable` attribute on witnesses declared in a different type context or file than the protocol conformance.
@swift-ci Please test |
* 'master' of github.com:apple/swift: [AutoDiff] [Sema] Limit implicit `@differentiable` attribute creation. (swiftlang#33776)
swiftlang#33776) During protocol witness matching for a protocol requirement with `@differentiable` attributes, implicit `@differentiable` attributes may be created for the witness under specific conditions (when the witness has a `@differentiable` attribute with superset differentiability parameters, or when the witness has less-than-public visibility). Do not generate implicit `@differentiable` attributes for protocol witnesses when the protocol conformance is declared from a separate file or type context from the witness. Resolves SR-13455.
This has broken some code where there are multiple public protocol P: Differentiable {
@differentiable(wrt: self)
@differentiable(wrt: (self, x))
func foo(_ x: Float) -> Float
}
// In a different file:
public struct S: P {
// Should generate implicit `@differentiable(wrt: self)` here.
@differentiable(wrt: (self, x))
public func foo(_ x: Float) -> Float {
x
}
} I'll fix this. |
When checking protocol conformances with `@differentiable` requirements, the type checker is supposed to accept omissions of `@differentiable` attributes when there exsits an attribute that covers a superset of the differentiation configuration. This was accidentally regressed in swiftlang#33776 which made the following test case fail to compile. This is fixed by adjusting the witness matching conditions. ```swift // rdar://70348904 reproducer: public protocol P: Differentiable { @differentiable(wrt: self) @differentiable(wrt: (self, x)) func foo(_ x: Float) -> Float } public struct S: P {} extension S { // This had worked until swiftlang#33776. @differentiable(wrt: (self, x)) public func foo(_ x: Float) -> Float { x } } ``` Also fix some suboptimal diagnostics where more information could be shown. Resolves rdar://70348904.
…34533) When checking protocol conformances with `@differentiable` requirements, the type checker is supposed to accept omissions of `@differentiable` attributes when there exsits an attribute that covers a superset of the differentiation configuration. This was accidentally regressed in #33776 which made the following test case fail to compile. This is fixed by adjusting the witness matching conditions. ```swift // rdar://70348904 reproducer: public protocol P: Differentiable { @differentiable(wrt: self) @differentiable(wrt: (self, x)) func foo(_ x: Float) -> Float } public struct S: P {} extension S { // This had worked until #33776. @differentiable(wrt: (self, x)) public func foo(_ x: Float) -> Float { x } } ``` Also fix some suboptimal diagnostics where more information could be shown. Resolves rdar://70348904.
During protocol witness matching for a protocol requirement with
@differentiable
attributes, implicit@differentiable
attributes may becreated for the witness under specific conditions (when the witness has a
@differentiable
attribute with superset differentiability parameters, orwhen the witness has less-than-public visibility). This avoids requiring users
to explicitly write unnecessary
@differentiable
attributes.Do not generate implicit
@differentiable
attributes for protocol witnesseswhen the protocol conformance is declared from a separate file from the witness.
Otherwise, compilation of the file containing the conformance creates references
to external symbols for the implicit
@differentiable
attributes, even thoughno such symbols exist.
Resolves SR-13455.
Example:
Before:
After: