Handling unknown cases in enums [RE: SE-0192]

I like this thought. Do you have any ideas for how else this could be used?

Off the top of my head, I could imagine a debug constant could be made to warn when set, though that use case seems rather hacky.

edit: Thinking on it, any if-statement test can be inverted for an unreachable code warning…

It would be nice if this could build on the #warning proposal.

Unfortunately that’s what you get when you design by committee.

I actually really like fallback. It makes it clear what is going on, and it also makes it clear when it warns that “fallback will never be used” if the enum is exhaustive.

1 Like

+1, this being an attribute makes a LOT of sense to me.

-Chris

7 Likes

+1 for annotation

+1. Quote my old post:

Some ideas:

(silent | warn | ignore | ignorable) + (future | unknown) + [cases]

e.g.

  • @ignorableFutureCases
  • @ignorableUnknownCases
  • @warnFutureCases
  • @warnUnknownCases
  • @ignoreFuture
  • @ignoreUnknown
  • @silentUnknown

The thing I don't like about using an attribute modifier on default (like @unused default) is that it is more difficult to type then simply replacing default with another word. At least for me, I'll always use unknown instead of default for all non-frozen switches, so I worry it would become a bit annoying to add the attribute each time.

I would say the same thing about @escaping except that the compiler can insert it for you automatically via fix-its, so it's not an issue.

Having said that, my worries would probably go away if it will work with auto-complete once the '@' has been typed.

1 Like

Yes, seems like attribute is better alternative. And for now '@unused default' is better candidate IMO

But I want to point out that this Xiaodi's statement seems is not true, IIUC:

If @unused default is written switching over a frozen enum: “error: ‘@unused default’ unreachable because enum is
frozen” (fixit to remove default)

From Jordan Rose's replies I made a conclusion that '@unused default'(aka 'unknown case') will be allowed for
'standard'(frozen) enums.
Moreover, in this case there will be a nice symmetry:

// 'standard'(aka frozen) enum

enum MyEnum { case one, two }

// 1. Exhaustive switch with 'default'

switch myenum {
case .one : ...
case .two : ...
default : ... // Current behavior: Warning: Default will never be executed
// Will be addedd: .. Add @unused if expected
}

// 2. Exhaustive switch with '@unused default'

switch myenum {
case .one : ...
case .two : ...
@unused default : ... // No warnings, we expect this will not be called,
// until MyEnum added new case. And after this
// we can still compile the code(with warning).
// I.e. don't have to fix the problem right now - for example when
// MyEnum is declared in 3rd party source file, not our own file.
}

// 3. Non-exhaustive switch with 'default' - standard situation, no warnings

// 4. Non-exhaustive switch with '@unused default'

switch myenum {
case .one : ...
@unused default : ... // Warning: @unused default will be executed. Remove @unused attribute to silence the warning.
}

1 Like

As far as names go, "unknown" is the "least bad" term that's been suggested so far (separate from whether it's a modifier, or an attribute, or a standalone thing). I'll admit that part of that is because it's not particularly "content-ful", i.e. it doesn't have an immediate existing meaning and isn't a technical term related to anything else. But the problem is inherently one with two parts—non-public cases and future cases—and we haven't come up with a better term that encompasses both.

(The obvious thing to try is to not encompass both, but unfortunately I can't think of how the compiler could possibly differentiate them, especially when dealing with C enums.)

On some specific other names I've seen coming up in this thread:

  • unused - The case is not unused. It will be used if the library developer adds a new enum case.
  • fallback - Not bad, but I'm concerned that as a word it's not really different from default.

To the idea of using #warning or #warnIfReachable rather than a dedicated annotation, please remember that this code is reachable. The assumption is that future cases will be added—it has to be to produce a correct program (and to not accidentally optimize away the catch-all case).


An attribute on the switch isn't inherently bad, but I don't see how it's better than an annotation on the case (with whatever spelling).


To Vladimir's last point, the proposal I'm writing up draws a distinction between "implicitly frozen" enums (the ones users define) and "explicitly frozen" enums (the ones in libraries). Using a catch-all case in an otherwise exhaustive switch will produce a warning on explicitly frozen enums, but unknown will be allowed for implicitly frozen enums. I suspect this will not affect anyone in practice; it just happens to do the right thing in the right places.

@default:

fallback is unfortunate because it's very close in appearance to fallthrough, and both will be mixed together in switch statements.

3 Likes

To the idea of using |#warning| or |#warnIfReachable| rather than a dedicated annotation, please remember that this code
/is reachable.

Well... isn't something like @unreachableAtCompiletime or @runtimeReachableOnly or even @runtimeOnly is the right
direction?

I don't think it is, but I'll admit to have been very close to this problem for a long time, so it may be that that's a more useful spelling for explaining it to people.

It seems like this discussion has moved away from the original post's suggestion to build a more general-purpose pattern-matching operator like #unknown. I liked this a lot because would allow us to be so much more specific. I feel like needing to future-proof your code would demand a lot of specificity.

Current changes in the revision PR say "it would be surprising to have such a pattern but keep the restrictions described in this proposal," but I'll admit I'm not sure what that means.

Have we decided against this suggestion? All the other models for this feature seem like an undesirable compromise to me, but I may be missing some crucial details. :blush: This thread has gotten quite long itself :sweat_smile:

If we haven't decided against the concept altogether but didn't like the original spelling, I humbly submit #invisible for consideration. I noticed that the original post says "This whole discussion is happening because the ABI stability work is introducing a new concept to enums - invisible members." It struck me that "invisible" captures the idea as well as "unknown" does.

Cheers
Hope I'm not adding to the noise

I believe we have.

I cleaned up the section on patterns a little in the version I have locally (not ready to push yet). Here's what it's going to look like:

However, this produces potentially surprising results when followed by a case that could also match a particular input. Because unknown acts as a catch-all, the input (.thoughtItWasDueNextWeek, true) would result in case 2 being chosen rather than case 3.

switch (excuse, notifiedTeacherBeforeDeadline) {
case (.eatenByPet, true): // 1
  // …
case (#unknown, true): // 2
  // …
case (.thoughtItWasDueNextWeek, _): // 3
  // …
case (_, false): // 4
  // …
}

The compiler would warn about this, at least, since there is a known value that can reach the unknown pattern.

As a top-level case, unknown must go last to avoid this issue. However, it's not possible to enforce the same thing for arbitrary patterns because there may be multiple enums in the pattern whose unknown cases need to be treated differently.

So the arguments against are a little weaker than they used to be, but it's still not the direction I want to go with this proposal, which has to get something that works and works simply in the common case.

My own problem with #invisible is that future cases aren't exactly "invisible". They're not visible, sure, but neither are the "invisible" because they're just…not there yet. Chris's characterization of this as "invisible" is therefore not something I'd suggest baking into the language. But I'll add it to the list of "other suggested names".

Returning to this after some time for reflection, I wonder if the warning behavior is really as critical as we have been assuming.

Perhaps we should simply introduce the ability for frameworks to declare their enums as (non)frozen, and later if there is a hue and/or cry for the warning then we can determine the best shape and spelling with actual experience under our collective belt.

Oh yeah I see how that could get pretty messy :blush:

I think I just figured this out (after getting it wrong several times)…

In the original pitch (the very first post of this topic), the subpattern #unknown was intended to match only unknown cases. That would have been fine as far as it goes, but here’s the point: There'd still need to be a final catch-all that matched everything that wasn't matched already (which is not the meaning of case #unknown:, in that definition of #unknown).

The "match-everything-else" catch-all is needed because otherwise you can't get the diagnostic messages right (for a non-frozen enum).

I think that was the point Jordan got, and has been insisting we get too:

  1. If the switch was previously (in Swift 4) exhaustive, then adding [hypothetically] just:
case #unknown: // matches only statically unknown cases
    …

would, at some future compilation, after more cases had been added to the enum, produce an error that the switch wasn't exhaustive. Most people don't want that to be an error.

  1. Adding:
case _: // aka 'default:', matches anything else
    …

would suppress all warnings about unhandled known cases, now and in the future. Nobody wants no warnings at all.

  1. Adding [hypothetically] both:
case #unknown: // matches only statically unknown cases
    …
case _: // aka 'default:', matches anything else
    …

would produce a warning that case _: cannot be reached, if the switch listed currently-known cases exhaustively. The only way to get rid of the warning would be to remove the one of the cases, leading to one of the other unwanted scenarios.

  1. Changing the semantics of the #unknown subpattern to match "everything unmatched so far" leads to ambiguities between #unknown and _ in compound match cases (as explained in the most recent posts).

The only correct approach combines those two cases of #3 into a single [hypothetical] case:

case #unknown, _:

which warns if and only the switch fails to enumerate some (presumably “new”) cases known at the time of compilation. This is the desired warning.

Since this catch-call is required regardless of other uses of a [hypothetical] #unknown subpattern, it may as well have a unique spelling:

unknown default:

which is where the proposal currently stands.

Does that sound right?

1 Like

I'm not sure what you mean by this. Could you give an example?

If the issue in question is just ensuring a warning is emitted on incorrect usage, we could just make the behaviour clearer by using a different name:

#warningDefault
#exhaustiveDefault
#fallback
#other
#unknown _
unknown _
#exhaustive _
exhaustive _

Admittedly, it's quite a difficult concept to express succinctly, but we shouldn't write the feature off just because using a (repurposed) spelling makes the semantics unclear.