-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[WIP] Implementation of non-exhaustive enums #11961
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
I haven't done the importer parts yet, and there are still more tests to write, but I thought I should throw the basic thing up ahead of that. @CodaFi, mind taking a look at the switch statement part? (the last commit) |
Hi @jrose-apple, I already had an exhaustiveness check in SILGenPattern.cpp that's driven by hasFixedLayout(). It should probably be removed now? Also I think we should redo hasFixedLayout() to check the exhaustiveness attribute if the given decl is an EnumDecl. Right now it looks for @_fixed_layout, which should probably be banned on enums (and all tests updated). I do want to decouple "use resilient access" from "non-exhaustive" though, so it's up to you how much you want to redo the hasFixedLayout() stuff. I would prefer that hasFixedLayout() returns true if the module is non-resilient or the enum is exhaustive, since this is what IRGen uses to decide how to project and inject enum cases. |
Thanks for listing out all these tasks; that's way easier then trying to track them down myself. I think it's okay to go to review without these, but they're definitely things I consider part of the work. |
@@ -74,11 +74,11 @@ namespace { | |||
/// condition is empty. | |||
struct SpaceEngine { | |||
enum class SpaceKind : uint8_t { | |||
Empty = 1 << 0, |
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.
These aren't just tags, they're powering the constexpr
bit-mangling/pattern-matching gadget in the rest of this file. With these values you get easy uniqueness of bit sequences closed under |
.
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 works either way, I guess but I would prefer the tag values become explicit then.
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.
That doesn't make sense to me. Your pairwise case works for any two uint8_ts, and that's already encoded in the enum's underlying type. And indeed, it would not work for arbitrary flags, and so making the constants powers of two is misleading.
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.
That doesn't make sense to me.
Let me be clearer: By "easy" I mean I could dump the binary value of a pair of space kinds and easily see from the bits which ones I had. Either way works for me, but if it's misleading to treat it as an option set I would still like the tag values to be explicitly stated in the source.
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 don't do that for any other enums except where the raw value is significant, so again it seems weird to include the values (especially when there's only 5 of them and the debugger can print them for you). I'm also not sure how this is really any different with regards to dumping. Before you'd get values like 0x0408
, 0x0210
, etc; now you get 0x0203
and 0x0104
.
WRT Sema making the space atomic based on the attribute is the right approach. There is a special case to optimize here: If we have an open enum or the product of all open enums with no |
A problem I found with this approach in the Space Engine: in Swift 4 mode, we should still distinguish between "all cases covered but the enum is non-exhaustive" (warning) and "known cases are uncovered" (error). I think I'm going to have to make a new space kind, or at least a special kind of Constructor space that's not resolvable. |
Can't that be determined from the type of the switch subject? Because spaces for open enums are atomic, in the general case you can walk into a tuple/constructor space and check for that atomic type space that resolves to an open enum. |
Alternatively, you could not make a distinction between open and closed enum spaces and then make the determination about exhaustiveness by doing the aforementioned type check. |
@slavapestov SILGen work is up. @CodaFi I don't understand what you're getting at, which I'm pretty sure is just a sign of my unfamiliarity with the problem space. Just to be sure we're on the same page, this is the behavior I'm hoping to get in Swift 4 mode: // _nonexhaustive public enum X {
// case a, b
// }
switch getX() {
case .a: break
case .b: break
} // warning
switch getX() {
case .a: break
} // error My current patch downgrades these both to warnings, because the non-exhaustive enum is treated as an opaque space, and then the constructors don't subtract from it. |
Alright, so we currently have four diagnosable cases we're worried about here:
Easy. Everything is fine.
Easy. Diagnose as an error in Swift 4 mode, warning
Easy. If we have a
Hard. Need to simultaneously offer missing cases as an error + a default. The last case is the one we care about here, and it can be detected without defining a new kind of space (you would be duplicating the Now, when you go to diagnose missing |
Ah, sorry, I think you got case 2 and case 3 mixed up. Case 2 is always an error. Case 3 needs to be an error in Swift 5 and a warning in Swift 3/4. Case 4 needs to be an error, always, with some fix-it. |
Yeah, I mixed my memory of implementing
Does this mean that we consider it a compatible change to mark a previously |
This was the diff I needed to get that fixit you wanted wired up. We can definitely offer a more specific diagnostic in this case diff --git a/lib/Sema/TypeCheckSwitchStmt.cpp b/lib/Sema/TypeCheckSwitchStmt.cpp
index 4d10c824f9..dcf2edb091 100644
--- a/lib/Sema/TypeCheckSwitchStmt.cpp
+++ b/lib/Sema/TypeCheckSwitchStmt.cpp
@@ -972,11 +972,7 @@ namespace {
}
static bool canDecompose(Type tp, const DeclContext *DC) {
- if (tp->is<TupleType>() || tp->isBool())
- return true;
- if (const EnumDecl *enumDecl = tp->getEnumOrBoundGenericEnum())
- return enumDecl->isExhaustive(DC);
- return false;
+ return tp->is<TupleType>() || tp->isBool() || tp->getEnumOrBoundGenericEnum();
}
// HACK: Search the space for any remaining cases that were labelled
@@ -1119,12 +1115,16 @@ namespace {
spaces.push_back(projection);
}
}
-
+
+ bool isNonExhaustiveSubject = false;
+ if (auto *enumDecl = subjectType->getEnumOrBoundGenericEnum())
+ isNonExhaustiveSubject = enumDecl->isExhaustive(DC);
+
Space totalSpace(subjectType, Identifier(),
Space::isSwift4NonExhaustiveEnum(TC, DC, subjectType));
Space coveredSpace(spaces);
size_t totalSpaceSize = totalSpace.getSize(TC, DC);
- if (totalSpaceSize > Space::getMaximumSize()) {
+ if (!isNonExhaustiveSubject && totalSpaceSize > Space::getMaximumSize()) {
// Because the space is large, we have to extend the size
// heuristic to compensate for actually exhaustively pattern matching
// over enormous spaces. In this case, if the covered space covers
@@ -1142,6 +1142,11 @@ namespace {
auto uncovered = totalSpace.minus(coveredSpace, TC, DC).simplify(TC, DC);
if (uncovered.isEmpty()) {
+ // If we've got here with a non-exhaustive subject then all we need is
+ // a `default`.
+ if (isNonExhaustiveSubject) {
+ diagnoseMissingCases(/*justNeedsDefault*/ true, Space());
+ }
return;
} |
This is the reality of introducing nonexhaustive enums for the first time: existing code is written without default cases. So for people transitioning to Swift 5 for the first time (or being diligent in Swift 4.1), yes, enums will go from exhaustive to non-exhaustive. (See the proposal for more details on the default behavior.) Thanks for the patch! I'll incorporate that into the series for now. It's probably about good enough to kick off the review, anyway. |
39dd814
to
b8713c7
Compare
Still more work to do, but I think this is sufficient for proposal review! @swift-ci Please test |
Build failed |
Build failed |
b8713c7
to
d3990e1
Compare
@swift-ci please test |
@swift-ci Please clean test Linux |
@swift-ci Please test source compatibility |
Build failed |
@swift-ci Please test source compatibility |
Still to do:
What's not included here is marking any enums non-exhaustive in the standard library or overlays; we want to hold off on that until we're ready to take Swift 5 changes. |
d3990e1
to
38929a6
Compare
Picking this up again. Rebased, but still need to revisit the patches and see what's left. @swift-ci Please test |
68f90dd
to
2a035cd
Compare
Since NS_ENUM has an enum_extensibility(open) in it already, we want to allow people to undo that by sticking enum_extensibility(closed) on the end of their enum. Furthermore, we actually want to ignore the enum_extensibility(open) that's in there now, so as not to introduce new warnings in Swift 4.1.
Previously (a03c40c) we assumed all Swift enums were non-exhaustive in ObjC, a weird choice in retrospect. Now that we actually distinguish exhaustive and non-exhaustive enums in Swift, we can use the 'enum_extensibility' attribute to mark them as open or closed in ObjC.
Per direction from the core team. The attributes will remain underscored until the proposal and names are reviewed and and approved, though.
That means a few of the stopgaps to avoid rocking the boat in Swift 4.1 have been removed, which means more test updates.
9dbb9ca
to
4ad3195
Compare
Rebased again! Follow-up work (will take place in other PRs, to keep this one from growing any more):
|
@swift-ci Please test |
Build failed |
4ad3195
to
29a8890
Compare
@swift-ci Please test |
Build failed |
Build failed |
@swift-ci Please test |
Build failed |
Build failed |
In theory there could be a "fixed-layout" enum that's not exhaustive but promises not to add any more cases with payloads, but we don't need that distinction today. (Note that @objc enums are still "fixed-layout" in the actual sense of "having a compile-time known layout". There's just no special way to spell that.)
(both C enums and Swift enums declared @objc), even if declared "exhaustive", because of the "feature" in C of treating a value not declared as a case as a valid value of an enum. No more undefined behavior here! This also adds more tests for trapping on unexpected cases for non-exhaustive non-@objc enums.
Per discussions on the first round of review for SE-0192. This was definitely the most-liked name. Additionally, @_nonexhaustive is now @_nonfrozen -- an attribute that only shows up when writing resilient libraries that still need to use Swift 4 mode and when printing enums imported from C in Swift 4 mode.
29a8890
to
53125e9
Compare
The proposal took a new direction, implemented in #14945. Closing. |
Initial implementation of https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md, which I plan to submit for swift-evolution review soon.