-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Allow Codable synthesis in same-file extensions #11735
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
Allow Codable synthesis in same-file extensions #11735
Conversation
Lifts the restriction that prevented Codable synthesis in extensions declared in the same source file as the original type declaration.
@swift-ci Please test |
// type itself or an extension, in which case we will emit the declaration | ||
// normally. | ||
if (target->hasClangNode()) | ||
tc.Context.addExternalDecl(encodeDecl); |
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.
We're never going to synthesize conformance for imported types; might as well remove this.
// type itself or an extension, in which case we will emit the declaration | ||
// normally. | ||
if (target->hasClangNode()) | ||
tc.Context.addExternalDecl(initDecl); |
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.
Same here
// If we just extended a nominal type with a protocol conformance (which | ||
// may have just gotten derived), members which are synthesized might need | ||
// to be visited for validation (since the extended type might have | ||
// otherwise finished being validated). |
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.
This addition is necessary to perform additional passes on types getting synthesized members in extensions after they've already been validated. For instance, the following code:
struct X {
let value: Int
}
extension X : Codable {}
performs four (very simplified) passes:
- A first pass on
X
to validate access and function signatures - A first pass on
extension X
to validate some access and inherited types - A second pass on
X
to do conformance checking - A second pass on
extension X
to do conformance checking
It's pass 4 here which actually synthesizes Codable
methods on X
. The method bodies, however, are lazily synthesized, so if the function decls are never visited, the bodies are never filled in. This leads to compiling code which references methods which are never compiled in, leading to linker errors.
This forces a pass on synthesized decls in case they haven't been visited before.
extension X : Codable {}
struct X {
let value: Int
}
would otherwise still work because pass 4 above would happen before pass 3, allowing pass 3 to synthesize the method bodies.
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.
This doesn't feel right, but I can't quite put my finger on what the right answer is. @DougGregor would probably know.
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.
@jrose-apple I agree; if there's a better way to do this, I'm more than happy to rip this out.
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.
What is different about Codable that necessitates this logic? Other derived conformances don't need this it seems.
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.
@slavapestov Codable
is so far the only protocol for which we synthesize conformance in a non-constrained manner — Equatable
, Hashable
, and RawRepresentable
are currently only synthesized for enum
s, and at that, they are automatically synthesized even if you don't request conformance. This case never comes up because we don't support arbitrary Equatable
/Hashable
derived conformance, so extending a struct
/class
doesn't work. (Yet — when Equatable
/Hashable
synthesis lands, it will have the same problem.)
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.
I'd like Doug to take a look at this, but I do have a few comments about testing:
- Can you make sure this is rejected across module boundaries, and that we get a reasonable error message in that case?
- Can you add an execution test for the successful case?
@@ -0,0 +1,17 @@ | |||
// RUN: %target-typecheck-verify-swift |
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.
Files under Inputs/ don't get tested, so no need for this RUN line.
@@ -1,29 +1,33 @@ | |||
// RUN: %target-typecheck-verify-swift | |||
// RUN: %target-typecheck-verify-swift -verify-ignore-unknown |
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.
Where is there an unknown location? (as opposed to a cross-file location)
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.
I'm currently re-building Swift now so I can't at the moment get at the exact unknown locations, but when Codable
derived conformance fails, the unit tests pick up a few <unknown>:0:0:
messages that don't actually appear during compilation. This has been an issue across all of these tests (hence why other files have -verify-ignore-unknown
); I've only added it where necessary to get tests to run correctly.
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.
We should figure out where those locations are coming from, because they're bad UX.
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.
Sure. These never appear to anyone who's doing real compilation (they only appear if you do just type checking, which is really only relevant to unit tests), but I'll investigate a bit further.
This is all suspicious for classes too, which are supposed to have all of their designated initializers in the original class declaration. I think you should just restrict it to structs and enums for now. |
Build failed |
@jrose-apple I can add the tests you asked for. Looks like some other tests have failed (either because of this or unrelated) so I'll look into that as well. As for not doing this for classes — how much does that restriction matter? Since |
It's about the AST structure. We walk the ClassDecl to find all the initializers when we check if a subclass has overridden them all. |
auto layout = canType->getExistentialLayout(); | ||
for (auto protocolType : layout.getProtocols()) | ||
inheritedProtocols.push_back(protocolType->getDecl()); | ||
} else if (auto *inheritedNominal = canType->getAnyNominal()) { |
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.
I don't think this 'else if' is reachable. isExistentialType() will already return true for ProtocolType.
@jrose-apple @slavapestov Thanks for the comments. I'm going to close this PR for now to rethink this a bit. If you can think of a better approach, let me know. For now, I don't think this is a big deal, but as we allow synthesis of |
Is there a chance that this could be revisited soon? 😀 Now that synthesized |
Why is that? Why wouldn't the existing code just work? |
@allevato I agree that this should be revisited, but this isn't preventing anything from happening. You just can't get implicit implementation of |
Ah, I see. If your type conditionally conforms to Codable, the conformance won't be autosynthesized. But the most common case will be using types that conditionally conform to Codable, and that will work fine as it does today. |
Yeah, that's true—I was thinking a little too myopically about what needed to be synthesized. I was mainly thinking of being able to write this: public enum Optional<Wrapped> {
// same as before
}
// These get synthesized
extension Optional: Equatable where Wrapped: Equatable {}
extension Optional: Hashable where Wrapped: Hashable {} But those are simple enough to implement that it may not be compelling on its own, and maybe it doesn't affect user code that much after all. |
@allevato Does that currently work with I agree, though, this would be nice to have for sure. |
No, I started working on a PR to add it to |
Yeah, this will likely require some deeper investigation and potential rearchitecting; it's on my list of things to get done for Swift 4.1 though, so this isn't getting lost in the meantime. |
What's in this pull request?
Resolves SR-4920 by lifting the restriction that prevented
Codable
synthesis in same-file extensions.Arbitrary synthesis of
Codable
in extensions is still disallowed, though.