-
Notifications
You must be signed in to change notification settings - Fork 10.6k
[Sema]: Add Codable synthesis for enums with associated values #34855
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
There are still some things to clean up and tests to add here. |
@swift-ci build toolchain |
Linux Toolchain (Ubuntu 16.04) Install command |
macOS Toolchain Install command |
6814196
to
20bc352
Compare
@swift-ci test |
@swift-ci build toolchain |
Linux Toolchain (Ubuntu 16.04) Install command |
20bc352
to
4269d71
Compare
@swift-ci 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.
I don't feel like I've caught everything in this PR that could be improved, but here's a starting point.
If you want to go the extra mile, I think there are pretty strong structural similarities between enums (and their elements), associated values (and their parameters), and structs (and their stored properties) which you could exploit to make this code more principled and easier to follow. But you can always leave that refactoring for someone else.
lib/Sema/DerivedConformances.cpp
Outdated
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 seems kinda bad, but I guess that's why we have a feature flag.
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 is copy pasted from the struct case and not specific to the additions in this PR. It just felt right to have the comment here as well.
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.
It's behind a feature flag, because the proposal has not yet been accepted.
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.
The original context is bogus as well. This deriving mechanism actively suppresses downstream error diagnostics in a bizarre attempt to restructure the diagnostics it emits. This code needs refactoring, but this PR is not the place to do it.
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 suspect now that TypeChecker
is just a namespace and we have more requests that we can perform real validation here.
@swift-ci smoke test |
@swift-ci smoke test |
@beccadax When you have a minute, could you please give this another read? |
6a92c2f
to
677a701
Compare
@swift-ci smoke test |
@swift-ci smoke test linux |
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.
The synthesis is looking good, as far as I had to work with similar things recently at least :-)
Very excited to see this polished up!
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.
Sorry about the delay. I think there's a bunch more you could do to clean this up, but you can do that in a followup PR if you need to get this in.
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.
Id_CodingKeys
is known to start with an uppercase letter, so you can just do a normal scratch +=
here.
auto *codingKeyCase = | ||
lookupEnumCase(C, codingKeysEnum, elementDecl->getBaseIdentifier()); | ||
if (!codingKeyCase) | ||
return nullptr; |
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.
You should do this early return before you allocate inherited
, and probably as early as possible to avoid unnecessary work.
|
||
// if the type conforms to {En,De}codable, add it to the enum. | ||
Identifier paramIdentifier = getVarNameForCoding(paramDecl); | ||
bool generatedName = false; |
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 variable doesn't seem to be used anywhere.
return dyn_cast<NominalTypeDecl>(decl); | ||
} | ||
|
||
static EnumDecl *addImplicitCodingKeys_enum(EnumDecl *target) { |
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.
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.
Looking further down, I also see some commonalities with existing code in addImplicitCodingKeys()
.
return false; | ||
} | ||
|
||
auto *codingKeysTypeDecl = _codingKeysTypeDecl; |
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's the purpose of this copy? (It looks like maybe something modifies codingKeysTypeDecl
? If so, you might want to explain that.)
for (auto entry : llvm::enumerate(*elt->getParameterList())) { | ||
auto *paramDecl = entry.value(); | ||
Identifier identifier = getVarNameForCoding(paramDecl); | ||
if (identifier.empty()) { | ||
identifier = C.getIdentifier("_" + std::to_string(entry.index())); | ||
} | ||
auto *caseCodingKey = lookupEnumCase(C, caseCodingKeys, identifier); | ||
|
||
params.push_back(getVarNameForCoding(paramDecl)); | ||
|
||
// If no key is defined for this parameter, use the default value | ||
if (!caseCodingKey) { | ||
// This should have been verified to have a default expr in the | ||
// CodingKey synthesis | ||
assert(paramDecl->hasDefaultExpr()); | ||
decodeCalls.push_back(paramDecl->getTypeCheckedDefaultExpr()); | ||
continue; | ||
} | ||
|
||
// Type.self | ||
auto *parameterTypeExpr = TypeExpr::createImplicit( | ||
funcDC->mapTypeIntoContext(paramDecl->getInterfaceType()), C); | ||
auto *parameterMetaTypeExpr = | ||
new (C) DotSelfExpr(parameterTypeExpr, SourceLoc(), SourceLoc()); | ||
// BarCodingKeys.x | ||
auto *metaTyRef = | ||
TypeExpr::createImplicit(caseCodingKeys->getDeclaredType(), C); | ||
auto *keyExpr = | ||
new (C) MemberRefExpr(metaTyRef, SourceLoc(), caseCodingKey, | ||
DeclNameLoc(), /*Implicit=*/true); | ||
|
||
auto *nestedContainerExpr = new (C) | ||
DeclRefExpr(ConcreteDeclRef(nestedContainerDecl), DeclNameLoc(), | ||
/*Implicit=*/true, AccessSemantics::DirectToStorage); | ||
// decode(_:, forKey:) | ||
auto *decodeCall = UnresolvedDotExpr::createImplicit( | ||
C, nestedContainerExpr, C.Id_decode, {Identifier(), C.Id_forKey}); | ||
|
||
// nestedContainer.decode(Type.self, forKey: BarCodingKeys.x) | ||
auto *callExpr = CallExpr::createImplicit( | ||
C, decodeCall, {parameterMetaTypeExpr, keyExpr}, | ||
{Identifier(), C.Id_forKey}); | ||
|
||
// try nestedContainer.decode(Type.self, forKey: BarCodingKeys.x) | ||
auto *tryExpr = new (C) TryExpr(SourceLoc(), callExpr, Type(), | ||
/*Implicit=*/true); | ||
|
||
decodeCalls.push_back(tryExpr); | ||
} |
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 didn't diff, but I assume that once again this loop body is very similar to deriveBodyDecodable_init()
's.
llvm::SmallString<128> buffer; | ||
buffer.append("Case '"); | ||
buffer.append(elt->getBaseIdentifier().str()); | ||
buffer.append( | ||
"' cannot be decoded because it is not defined in CodingKeys."); |
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.
You might want to look at llvm::Twine—it's really good at building strings like this.
auto *debugMessage = new (C) StringLiteralExpr( | ||
StringRef("Invalid number of keys found, expected one."), SourceRange(), | ||
/* Implicit */ true); | ||
auto *throwStmt = createThrowDecodingErrorTypeMismatchStmt( | ||
C, funcDC, targetEnum, containerExpr, debugMessage); |
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 noticing that you have two helper functions to create throw statements, but you use each of them once and in both cases you need to separately build a StringLiteralExpr
first. You might want to see if you can merge them together and reduce duplication by tweaking the parameters slightly.
// generate: case .<Case>: | ||
auto pat = new (C) EnumElementPattern( | ||
TypeExpr::createImplicit(enumDecl->getDeclaredType(), C), SourceLoc(), | ||
DeclNameLoc(), DeclNameRef(), elt, subpattern); | ||
pat->setImplicit(); | ||
|
||
auto labelItem = CaseLabelItem(pat); | ||
auto body = BraceStmt::create(C, SourceLoc(), caseStatements, SourceLoc()); | ||
cases.push_back(CaseStmt::create(C, CaseParentKind::Switch, SourceLoc(), | ||
labelItem, SourceLoc(), SourceLoc(), body, | ||
/*case body vardecls*/ caseBodyVarDecls)); | ||
} | ||
|
||
// generate: switch self { } | ||
auto enumRef = | ||
new (C) DeclRefExpr(ConcreteDeclRef(selfRef), DeclNameLoc(), | ||
/*implicit*/ true, AccessSemantics::Ordinary); | ||
|
||
auto switchStmt = SwitchStmt::create(LabeledStmtInfo(), SourceLoc(), enumRef, | ||
SourceLoc(), cases, SourceLoc(), C); | ||
statements.push_back(switchStmt); | ||
|
||
auto *body = BraceStmt::create(C, SourceLoc(), statements, SourceLoc(), | ||
/*implicit=*/true); | ||
return {body, /*isTypeChecked=*/false}; | ||
} |
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.
You know, this part, and probably a good chunk of the preamble, is fairly similar to the switch statement generation in deriveBodyDecodable_enum_init()
. I wonder if you could extract a helper that takes a lambda to call with each case from the enum it's switching over?
NOTE(codable_enum_duplicate_case_name_here,none, | ||
"cannot automatically synthesize %0 because %1 has duplicate " | ||
"case name %2", (Type, Type, Identifier)) | ||
NOTE(codable_enum_duplicate_parameter_name_here,none, | ||
"cannot automatically synthesize %0 for %1 because " | ||
"user defined parameter name %2 in %3 conflicts with " | ||
"automatically generated parameter name", (Type, Type, Identifier, Identifier)) |
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.
Nit: We usually put the argument types on a separate line, partially to avoid line limit violations.
@beccadax Thanks for the feedback. Follow up PR sounds great. I‘d love to get this in as soon as possible. |
This pull request adds
Codable
synthesis for enums with associated values.