Skip to content

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

Conversation

itaiferber
Copy link
Contributor

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.

Lifts the restriction that prevented Codable synthesis in extensions declared in the same source file as the original type declaration.
@itaiferber
Copy link
Contributor Author

@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);
Copy link
Contributor Author

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);
Copy link
Contributor Author

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).
Copy link
Contributor Author

@itaiferber itaiferber Sep 1, 2017

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:

  1. A first pass on X to validate access and function signatures
  2. A first pass on extension X to validate some access and inherited types
  3. A second pass on X to do conformance checking
  4. 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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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 enums, 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.)

Copy link
Contributor

@jrose-apple jrose-apple left a 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
Copy link
Contributor

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
Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@jrose-apple
Copy link
Contributor

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.

@swift-ci
Copy link
Contributor

swift-ci commented Sep 1, 2017

Build failed
Swift Test OS X Platform
Git Sha - f74d33c

@itaiferber
Copy link
Contributor Author

@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 init(from:) is being synthesized, it doesn't really have a source location anyway...

@jrose-apple
Copy link
Contributor

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()) {
Copy link
Contributor

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.

@itaiferber
Copy link
Contributor Author

@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 Equatable and Hashable, more people are going to hit this limitation.

@itaiferber itaiferber closed this Sep 8, 2017
@allevato
Copy link
Member

Is there a chance that this could be revisited soon? 😀 Now that synthesized Equatable/Hashable has landed, and it sounds like conditional conformances are on the horizon, same-file extension synthesis is an important part of the story that allows container types like Optional, Array, and Dictionary to be usable as fields in types with synthesized operations.

@jrose-apple
Copy link
Contributor

Why is that? Why wouldn't the existing code just work?

@itaiferber
Copy link
Contributor Author

@allevato I agree that this should be revisited, but this isn't preventing anything from happening. You just can't get implicit implementation of Codable in an extension at the moment...

@jrose-apple
Copy link
Contributor

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.

@allevato
Copy link
Member

Yeah, that's true—I was thinking a little too myopically about what needed to be synthesized. Array and Dictionary would likely still need to implement manual operators because they're complex containers.

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.

@itaiferber
Copy link
Contributor Author

@allevato Does that currently work with Equatable and Hashable synthesis, or is that also explicitly disabled? I haven't had the time to look at the implementation, unfortunately.

I agree, though, this would be nice to have for sure.

@allevato
Copy link
Member

No, Equatable/Hashable also forbids synthesis in an extension right now. When we were first discussing that long ago there were resilience concerns, but now same-file extensions should be safe because they have access to everything private.

I started working on a PR to add it to Equatable/Hashable, but then I pulled up this PR and saw that it wasn't as simple as I thought it might be at the outset.

@itaiferber
Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants