Skip to content

[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

Closed
wants to merge 14 commits into from

Conversation

jrose-apple
Copy link
Contributor

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.

@jrose-apple
Copy link
Contributor Author

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)

@slavapestov
Copy link
Contributor

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.

@jrose-apple
Copy link
Contributor Author

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

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 |.

Copy link
Contributor

@CodaFi CodaFi Sep 16, 2017

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.

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 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.

Copy link
Contributor

@CodaFi CodaFi Sep 18, 2017

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.

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 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.

@CodaFi
Copy link
Contributor

CodaFi commented Sep 16, 2017

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 default clause then we don't need to go through the space subtraction and size checks and stuff. Just add the default and bail.

@jrose-apple
Copy link
Contributor Author

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.

@CodaFi
Copy link
Contributor

CodaFi commented Sep 21, 2017

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.

@CodaFi
Copy link
Contributor

CodaFi commented Sep 21, 2017

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.

@jrose-apple
Copy link
Contributor Author

@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.

@CodaFi
Copy link
Contributor

CodaFi commented Sep 22, 2017

Alright, so we currently have four diagnosable cases we're worried about here:

  1. (exhaustive) enum + switch covered by known values:

Easy. Everything is fine.

  1. (exhaustive) enum + switch not covered by known values:

Easy. Diagnose as an error in Swift 4 mode, warning in 3 in 3 mode under @ _downgrade_exhaustivity_check, and present missing cases.

  1. _nonexhaustive enum + switch covered by known values:

Easy. If we have a default, we're done. Otherwise offer to insert one.

  1. _nonexhaustive enum + switch not covered by known values:

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 Type space if you did this). Roll back this check so the space engine performs subtractions the way it is supposed to, then make sure that decomposition only occurs for the in-module cases - this should guarantee that switches over out-of-module cases should pass through the space subtraction routine without affecting coverage.

Now, when you go to diagnose missing cases, you examine the type of the subject expression to see if you need to emit a warning or an error. This should align with the four cases above.

@jrose-apple
Copy link
Contributor Author

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.

@CodaFi
Copy link
Contributor

CodaFi commented Sep 23, 2017

Ah, sorry, I think you got case 2 and case 3 mixed up

Yeah, I mixed my memory of implementing @_downgrade_exhaustivity_check into this (which adds a whole other dimension of pain).

Case 3 needs to be an error in Swift 5 and a warning in Swift 3/4.

Does this mean that we consider it a compatible change to mark a previously exhaustive (I assume this is the default) enum as nonexhaustive? If not, then this should always be an error.

@CodaFi
Copy link
Contributor

CodaFi commented Sep 23, 2017

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;
       }

@jrose-apple
Copy link
Contributor Author

Case 3 needs to be an error in Swift 5 and a warning in Swift 3/4.

Does this mean that we consider it a compatible change to mark a previously exhaustive (I assume this is the default) enum as nonexhaustive? If not, then this should always be an error.

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.

@jrose-apple
Copy link
Contributor Author

Still more work to do, but I think this is sufficient for proposal review!

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 2da03935b230f1187ab83bb7fb65750b52e5cc71

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - b8713c7264e9ab3c06a7811c3f06b76c7e063aba

@jrose-apple
Copy link
Contributor Author

@swift-ci please test

@jrose-apple
Copy link
Contributor Author

@swift-ci Please clean test Linux

@jrose-apple
Copy link
Contributor Author

@swift-ci Please test source compatibility

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - b8713c7264e9ab3c06a7811c3f06b76c7e063aba

@jrose-apple
Copy link
Contributor Author

@swift-ci Please test source compatibility

@jrose-apple
Copy link
Contributor Author

jrose-apple commented Sep 29, 2017

Still to do:

  • Incorporate Robert's patch above
  • Make the warning/error case I was talking about above work in more complicated scenarios
  • Use a fixed layout for enums with raw types (a trivial 32-bit value)
  • Vastly improve diagnostics; in particular they should mention that the enum is non-exhaustive
  • Consider synthesizing a validating init(rawValue:) for imported closed enums
  • Replace @_fixedLayout in the standard library's resilient build with _exhaustive

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.

@jrose-apple jrose-apple added the swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review label Sep 29, 2017
@jrose-apple
Copy link
Contributor Author

Picking this up again. Rebased, but still need to revisit the patches and see what's left.

@swift-ci Please test

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.
@jrose-apple
Copy link
Contributor Author

Rebased again! Follow-up work (will take place in other PRs, to keep this one from growing any more):

  • Add unknown case, or however we end up spelling it. This subsumes a good chunk of the diagnostic work I was planning earlier because it's now necessary up front
  • Improve run-time diagnostics; in particular they should mention that the enum is non-frozen
  • Consider synthesizing a validating init(rawValue:) for imported frozen enums

@jrose-apple
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

swift-ci commented Feb 1, 2018

Build failed
Swift Test OS X Platform
Git Sha - 4ad31959beed15ff4832bf63d7ea798ef4f18f3e

@jrose-apple
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

swift-ci commented Feb 1, 2018

Build failed
Swift Test Linux Platform
Git Sha - 4ad31959beed15ff4832bf63d7ea798ef4f18f3e

@swift-ci
Copy link
Contributor

swift-ci commented Feb 1, 2018

Build failed
Swift Test OS X Platform
Git Sha - 4ad31959beed15ff4832bf63d7ea798ef4f18f3e

@jrose-apple
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

swift-ci commented Feb 2, 2018

Build failed
Swift Test Linux Platform
Git Sha - 29a8890

@swift-ci
Copy link
Contributor

swift-ci commented Feb 2, 2018

Build failed
Swift Test OS X Platform
Git Sha - 29a8890

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.
@jrose-apple
Copy link
Contributor Author

The proposal took a new direction, implemented in #14945. Closing.

@jrose-apple jrose-apple deleted the non-exhaustive-enums branch March 21, 2018 18:07
@AnthonyLatsis AnthonyLatsis added compiler The Swift compiler itself feature A feature request or implementation library evolution Feature: library evolution (umbrella feature for features that facilitate resilient libraries) enum Feature → type declarations: Swift enumeration declarations switch exhaustivity Feature: enforcement of exhaustivity in 'switch' statements and removed swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review labels Mar 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler The Swift compiler itself enum Feature → type declarations: Swift enumeration declarations feature A feature request or implementation library evolution Feature: library evolution (umbrella feature for features that facilitate resilient libraries) switch exhaustivity Feature: enforcement of exhaustivity in 'switch' statements
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants