Skip to content
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

Explicit Codable & CodingKey Derived Conformance #8125

Conversation

itaiferber
Copy link
Contributor

@itaiferber itaiferber commented Mar 15, 2017

What's in this pull request?
This PR integrates preliminary compiler work to support derived conformance for the newly introduced Codable and CodingKey types which are part of #8124.

This PR focuses on internal compiler features for improving the experience of using the features provided in #8124.

A swift-evolution proposal will be introduced momentarily and will link to this PR.

NOTE: This PR should not be merged until #8124 is.

This work is not complete. Remaining top-level features which will be iterated upon:

  • Need to not generate decode expressions for immutable vars with default initializers
  • Provide failure diagnostics
  • Add unit tests:
    • Imported types
    • Collections (when possible)
    • Generic types
    • Nested types
  • Synthesize CodingKeys type even w/o other derivation

Integrate synthesis of Codable and CodingKey requirements along with preliminary unit tests
@itaiferber itaiferber changed the title Explicitly Codable & CodingKey Derived Conformance Explicit Codable & CodingKey Derived Conformance Mar 15, 2017
Copy link
Contributor

@slavapestov slavapestov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work overall, I only have a few nitpicks so far.

Can you add some tests for generic types? I only see non generics here.


auto funcDC = cast<DeclContext>(encodeDecl);
ASTContext &C = funcDC->getASTContext();
auto &tc = *(TypeChecker *)C.getLazyResolver();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please just pass down a TypeChecker instead of doing an unsafe cast here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we can't. 😕 This is in the body synthesizer for the encode(to:) method, which can only accept the AbstractFunctionDecl whose body it's synthesizing.

The only way we can do this more safely that I can tell is to eagerly generate the body directly in deriveCodable_encode below, instead of using a body synthesizer. Is that better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think the conformance itself is lazily synthesized, so there should not be much of a perf win from lazily synthesizing the bodies. However @jrose-apple should comment on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I followed the exact model that RawRepresentable, Equatable, and Hashable use. If we eagerly synthesize the body here we should probably do it for those as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those don't need to cast the resolver to a TypeChecker though...

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 guess a refactor might be in order here. The TypeChecker is only used to be passed into getOrSynthesizeCodingKeysDecl, which at this point should only really be performing a lookup (because it would have already been derived here otherwise). Thinking about it, this is even strictly unnecessary; I should be able to just lookupDirect the CodingKeys decl since at this point it should be guaranteed to be there.

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 realized the type checker is actually used further down to tell if the type's superclass is Codable, so we can't get rid of it. Should we not synthesize the body lazily, then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're just doing a conformance check and not trying to record a dependency or emit a diagnostic, you can call ModuleDecl::lookupConformance() instead of TypeChecker::conformsToProtocol().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ModuleDecl::lookupConformance requires a lazy resolver: Optional<ProtocolConformanceRef> lookupConformance(Type type, ProtocolDecl *protocol, LazyResolver *resolver)
Should be safe to pass C.getLazyResolver() without a cast then, yeah?

Copy link
Contributor Author

@itaiferber itaiferber Mar 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this allows me to get rid of tc altogether:

static bool superclassIsCodable(ClassDecl *type) {
  assert(type && "Cannot get superclass of null type.");
  if (!type->hasSuperclass())
    return false;

  auto &C = type->getASTContext();
  auto *codableProto = C.getProtocol(KnownProtocolKind::Codable);

  auto *superclassDecl = type->getSuperclassDecl();
  auto *superclassModule = superclassDecl->getModuleContext();
  return (bool)superclassModule->lookupConformance(type->getSuperclass(),
                                                   codableProto,
                                                   C.getLazyResolver());
}

Unless this is unsafe for some reason, I'm switching to this

@@ -0,0 +1,25 @@
// REQUIRES: objc_interop
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these really require objc interop or does this all work on Linux with swift-corelibs-foundation too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These require the import of Foundation, which necessitates the inclusion of objc_interop, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Linux, objc_interop is off, but you might still have a Foundation module (swift-corelibs-foundation is pure Swift and doesn't use the ObjC runtime as you're no doubt aware).

I think tests should not pull in Foundation on Linux though, however we might still be able to test the conformance derivation by cooking up a 'mock' Foundation module that only provides the necessary protocols and nothing else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doug mentioned this too. Keep in mind though that these protocols use Data, which may pull in other things which really do require ObjC; this may prohibit us from being able to mock this in a truly objc_interop-free way...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to clarify -- this code does work on Linux with corelibs-foundation right? It's just that it's hard to test in this scenario?

Would adding some tests that rely on the conformance being derived to swift-corelibs-foundation be an acceptable solution then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe so. swift-corelibs-foundation will require some changes to bring it in line with the implementation of JSONEncoder and JSONDecoder specifically, but the primary issue in testing is not one of actual ObjC interop, but one of the testing framework.

All of my implementations should be able to land in swift-corelibs-foundation as well with no changes.

// Returns whether the type represented by the given ClassDecl inherits from a
// type which is Codable.
static bool classInheritsCodable(TypeChecker &tc, ClassDecl *type) {
if (!type || !type->hasSuperclass())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the class conforms to Codable but has no superclass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's handled by default below in getOrSynthesizeCodingKeysDecl, deriveCodable_encode, and deriveCodable_init. This is used to determine whether a class has a Codable superclass so we can emit a super key, and call try super.encode(to: container.superEncoder()) and try super.init(from: container.superDecoder()); if the class is Codable but its superclass is not, we don't need to emit those things.

Perhaps a better name for this function would just be superclassIsCodable or similar?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that sounds like a better name. Also, can you remove the '!type' check or replace it with an assertion? It's not clear to me that this can happen and we should not let it happen silently if it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can remove that; there's no real need for it.

//
// Validate the decl eagerly.
if (!varDecl->hasType())
tc.validateDecl(varDecl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasType/getType will assert if the VarDecl is in a different module. This can happen if you define the Codable conformance in an extension. Can you test this case?

You should use getInterfaceType() instead

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 caught this case in testing exactly that — asking the TypeChecker to validate the decl eagerly evaluates the type so that past the evaluateDecl line, hasType() == true. However, if getInterfaceType() allows us to avoid doing this eager evaluation, then we should use that. Looking into it now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not call validateDecl() on deserialized decks. They are already fully validated

Copy link
Contributor Author

@itaiferber itaiferber Mar 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateDecl() is the only way I see that we can do this. getInterfaceType() also asserts, since the decl has no interface type set either: Assertion failed: (hasInterfaceType() && "No interface type was set"), function getInterfaceType, file /Users/itai/Development/swift-oss/swift/lib/AST/Decl.cpp, line 1769.

Is there another way to force evaluation of the type without validating the decl?
FWIW, if the decl is already validated, why does it not have a type? Just curious — trying to learn about the process here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateDecl() is the right thing to call if the decl is in the same module and has not yet been type checked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the solution here, then? The var decl has no type, nor an interface type, but we need the type to be evaluated in order to continue.

// Bind Keyed*Container to Keyed*Container<KeyType>
Type boundType[1] = { keyType };
auto containerType = BoundGenericType::get(keyedContainerDecl, Type(),
C.AllocateCopy(boundType));
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 the AllocateCopy is necessary

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 think it is — BoundGenericType::get takes an ArrayRef referencing the bound types, which it does not copy. The ArrayRef will then be pointing to whatever it was created with, in this case, an array on the stack. We need to allocate a copy or else we'll have a dangling pointer. No?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It's hard to remember which ArrayRefs are copied and which ones are not. It's a bit of a mess...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry, I discovered this myself — lots of fun trying to diagnose the undefined behavior... ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general in these cases, the right thing to do is to use a SmallVector or a TinyPtrVector.

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 originally used SmallVectors all over the place and was seeing strange behavior, until @DougGregor corrected me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we are storing the ArrayRef here? Sorry I did a drive by. This brings up an interesting question though. If everyone is already copying here, it may make sense to just change this API to always copy. Internally if we run into overhead, we can optimize later. I am not saying that you should fix this in this PR. Just speaking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BoundGenericType::get takes an ArrayRef without copying it. Most decls do, in fact, because they likely assume that these things were already allocated earlier in the parsing process (which they were), so it's safe to take a reference without copying. Here, however, I'm synthesizing it all from scratch, so it does need to be allocated because these references are long-lived.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. What I was saying is that in the past this has been a source of bugs. Having a more conservative API here from a memory management perspective would be good since those bugs would be defined away. Like I said before, I do not think that this PR is the place to make such a change and I am not putting it on you to fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! We're not in disagreement here 😄 An audit of those would be a good idea (some are even taken as MutableArrayRefs...), but not part of this.

@itaiferber
Copy link
Contributor Author

@slavapestov Thanks for the review! As for the generic tests, absolutely — that's on the to-do list above.

@slavapestov
Copy link
Contributor

I think you can merge this whenever you're ready -- we can sort out the remaining issues later. I hope you get some time to work on the various TODO comments too :)

@itaiferber
Copy link
Contributor Author

itaiferber commented Mar 16, 2017

@slavapestov It seems I don't have write access for this repo — do you mind running a CI test please? And yes, the TODO comments are a top priority to eliminate and are covered in the TODO list at the top of this PR.

@slavapestov
Copy link
Contributor

@swift-ci Please smoke test

@slavapestov
Copy link
Contributor

Wait, don't we have to merge the other PR first?

@itaiferber
Copy link
Contributor Author

I don't want to merge this, though, until #8124 lands as well

@itaiferber
Copy link
Contributor Author

itaiferber commented Mar 16, 2017

Yeah, we definitely shouldn't merge this until the other one lands. Does the smoke test run the unit tests? Those will fail because the Codable and CodingKey protocols won't actually be in Foundation... If not, this should at least compile and pass.

@slavapestov
Copy link
Contributor

Yes, the smoke tests run the tests, that's the whole point of presubmit testing :)

@itaiferber
Copy link
Contributor Author

itaiferber commented Mar 16, 2017

Duh, yeah. Okay, then this will almost certainly fail ¯\(ツ)

Copy link
Contributor

@gottesmm gottesmm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itaiferber I am looking through this for swift style violations...

I am seeing a couple already. Have you run this through clang-format? It will lower the amount of style violations I need to comment on.

@@ -0,0 +1,950 @@
#include "TypeChecker.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a swift header.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean, like #include "swift/.../TypeChecker.h"? This is not a public header; it is in lib/Sema/TypeChecker.h, right alongside lib/Sema/DerivedConformanceCodable.cpp. This is how all other clients using TypeChecker import it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this

@@ -0,0 +1,615 @@
#include "TypeChecker.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a swift header.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this

@gottesmm
Copy link
Contributor

gottesmm commented Mar 17, 2017 via email

@itaiferber
Copy link
Contributor Author

Ah, got it — will do!

Copy link
Contributor

@gottesmm gottesmm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix these issues and run this through git-clang-format. Once that is done, I will look again.

default:
return false;
}
} else if (isa<StructDecl>(this) || isa<ClassDecl>(this)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This violates LLVM style:

http://llvm.org/docs/CodingStandards.html#don-t-use-else-after-a-return

All of the code paths before the else if return. The else is not necessary. In fact, you could even invert the if condition and do an early return false if you want to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you fix this, can you also add a * to the auto above on knownProtocol. (Or even change it to a reference). I know that this is something that is not in the code that you have touched yourself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing the else after return. knownProtocol is an Optional<KnownProtocolKind>, so leaving as auto

ASTContext &C = tc.Context;
auto parentDC = type->getDeclContext();
auto superType = type->getSuperclass();
return (bool)tc.conformsToProtocol(superType,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this bool conversion necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be. Removing

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 stand corrected; the return type is Optional<swift::ProtocolConformanceRef>, so the explicit cast is required.

// Asserts that the Foundation module is loaded, that the given name does not
// refer to more than one type, and that the entity with the given name refers
// to a type instead of a different type of declaration.
static TypeDecl* lookupFoundationTypeDecl(ASTContext &C, Identifier name) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This violates Swift style. We use 'TypeDecl *'. This is a case that would be caught by clang-format.

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 was following style I'd seen earlier, but fixed throughout the file

ConformanceCheckFlags::Used);
}

// Looks up a type declaration with the given name in the Foundation module.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a doxygen comment, it should use 3 slashes ala LLVM style.

http://llvm.org/docs/CodingStandards.html#doxygen-use-in-documentation-comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These weren't meant to be Doxygen comments, but no harm in documenting everything here. Added

// refer to more than one type, and that the entity with the given name refers
// to a type instead of a different type of declaration.
static TypeDecl* lookupFoundationTypeDecl(ASTContext &C, Identifier name) {
auto foundationModule = C.getLoadedModule(C.Id_Foundation);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a pointer, please use 'auto *', rather than just auto.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

C.Id_KeyedEncodingContainer,
codingKeysType);

auto containerExpr = new (C) DeclRefExpr(ConcreteDeclRef(containerDecl),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More 'auto *'

AccessSemantics::DirectToStorage);

auto enumElements = codingKeysEnum->getAllElements();
if (!enumElements.empty()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be extracted into its own function.

// `let container` (containerExpr) is generated above.

// encoder
auto encoderParam = encodeDecl->getParameterList(1)->get(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More 'auto *' everywhere in the rest of this scope.


// Now need to generate `try container.encode(x, forKey: .x)` for all
// existing properties.
for (auto elt : enumElements) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a pointer? Or a reference? In these cases it is good to put in the '*' or the '&' so people down the line are able to understand the ownership semantics of the for each. We have had several cases of people assuming an auto was a & when it was really a *& resulting in miscompiles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointer; fixed

/*Implicit=*/true);

// container.encode(self.x, forKey: CodingKeys.x)
Expr* args[2] = { varExpr, keyExpr };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a SmallVector here.

@gottesmm
Copy link
Contributor

@itaiferber If you are unfamiliar with git-clang-format, it will take care of a ton of the issues I brought up, but it only does it on the git-diff of what you changed, so you don't have to worry about changing other pieces of code you didn't touch.

@itaiferber
Copy link
Contributor Author

@gottesmm Thanks for all of these — I will address them and re-run through git-clang-format.

@gottesmm
Copy link
Contributor

@itaiferber Btw, of the changes I mentioned, git-clang-format will handle the 'Type*' => 'Type *' transformations. So I would just let it handle those for you

* Add Swift headers
* Avoid else-after-return
* Convert documentation to Doxygen comments
* Follow correct pointer style
* Add missing pointer/reference annotations on auto variables
* Use SmallDenseSet instead of DenseSet
* Split up large functions (and factor out repeated code where possible)
* Avoid casting TypeChecker
@itaiferber
Copy link
Contributor Author

@gottesmm That last push should've address most if not all of your concerns, hopefully. Refactored a lot of code in DerivedConformanceCodingKeys.cpp that should shorten it up considerably. Unit tests pass as they did before, and clang-format has nothing to complain about but the indentation of function calls (most of which I find unreadable once it formats them)

@gottesmm
Copy link
Contributor

gottesmm commented Mar 20, 2017 via email

}

// Empty enums are allowed to conform as well.
return enumDecl->getAllElements().empty() ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads up: I'm changing hasOnlyCasesWithoutAssociatedValues so you no longer need the first part of this clause.

Copy link
Contributor Author

@itaiferber itaiferber Apr 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CodaFi Thanks for the heads up on this. (Sorry for the delay, got back from vacation yesterday.) Is this change in, or not yet? Any good way for me to track the change so I can update accordingly?

All referenced known types are now part of the Swift stdlib, rather than Foundation
@itaiferber
Copy link
Contributor Author

BTW, I'm currently working on shuffling PRs (this and #8124) so that all the Swift stdlib/derived conformance stuff will be part of one PR and can land together, and the Foundation bits can land separately. This should NOT be merged.

@itaiferber
Copy link
Contributor Author

The work put up here has been subsumed into #9004.

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.

5 participants