Odd/inconsistent compile warning for optional of enum

In one case ordering, get warning. In a different ordering, no warning. Even though in both certain case will never be reached:

enum Thing {
    case one, two
}


func printThing000(_ thing: Thing?) {
    // case's in this order, get warnings
    switch thing {
    case .none:
        print("000 .none")
    case .some(let me):
        print("000 .some of \(me)")
    case .one:      // Warning: Case is already handled by previous patterns; consider removing it
        print("000 .one")
    case .two:      // Warning: Case is already handled by previous patterns; consider removing it
        print("000 .two")
    }
}


func printThing111(_ thing: Thing?) {
    // case's in this order, no warning even though case .some() will never be reached
    switch thing {
    case .none:
        print("111 .none")
    case .one:          // no warning
        print("111 .one")
    case .two:          // no warning
        print("111 .two")
    case .some(let me): // will never be matched to here
        print("111 .some of \(me)")
    }
}
1 Like

It apparently has to do with optional flattening: you may sometimes (as in this case) consider Optional<Thing> as having a flat structure of .none, .one or .two, as these are all the values of that type. Because matches are evaluated in order, the second example exhausts all possible options before the switch gets to case .some(let me).

On the other hand, .some(let me) is a wildcard for "everything but .none", so in the first example, if it gets matched, then all the possibilities are already exhausted, and you have to switch again over the non-optional Thing bound to the me constant.

Note that this all still makes sense, as it's possible in the second example that there are more cases in the Thing enum than just .one or .two, so it's your "default" case for all the unhandled patterns (think of switching over strings: you'll have to have a default because it's impossible to spell out all the infinitely many strings).

1 Like

Your enum isn’t frozen to have only two cases forever (which would be the case if it were explicitly declared as such in a separate, ABI-stable library). If you add a new case three to your enum, the last two cases in the first example will still never be reached, but the last case in the second example will become reachable.

2 Likes

After adding @fronzen to the enum doesn't change anything. The second example still no warning. Is this correct behavior?

Declaring the enum this way is not "frozen"?

@frozen
enum Thing {
    case one, two
}

@frozen is an opt-out from certain restrictions for libraries compiled in library evolution mode. It was first available for the C overlay, standard library, and libraries shipped by Apple as of SE-0192, and then was extended to user-defined types in libraries compiled in library evolution mode in SE-0260. The attribute has no effect when library evolution mode is off. See the Swift Evolution proposals for more details.

3 Likes

Interesting, though, I think you're onto something in terms of a discrepancy:

var sign: FloatingPointSign? = .plus

switch sign {
case .none: fallthrough
case .minus: fallthrough
case .plus: print("Hi")
default: fatalError() // warning: default will never be executed
}

switch sign {
case .none: fallthrough
case .minus: fallthrough
case .plus: print("Hi")
case .some(_): fatalError() // no warning, hmm...
}
2 Likes

Filed https://bugs.swift.org/browse/SR-14993 to track fixing this.

1 Like

Not sure if this is exactly identical, but here's an earlier similar bug. You'll notice a lot more weirdness in this area, like you're not even allowed explicitly mention the enum name and you have to use implicit member expressions:

var sign: FloatingPointSign? = .plus

switch sign {
  case .none: fallthrough
  case FloatingPointSign.minus: fallthrough // error: enum case 'minus' is not a member of type 'FloatingPointSign?'
  case .plus: print("Hi")
}

I think they're two different issues. One is related to name lookup being weird and the other is about exhaustivity checking not working correctly.

1 Like

This crash:

switch sign {
case _: fallthrough
//case .minus: fallthrough
//case .plus: print("Hi")
    // does warning: Case is already handled by previous patterns; consider removing it
case .some(_): fatalError() // !!! crash here: error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
}

What do you expect it to do?

:man_facepalming:Sorry

Not really. If you remove the optionality, the compiler will treat your two cases .one and .two as exhaustive, so there is clearly a bug or limitation here, where it can't figure it out correctly when stashed inside an optional.

Terms of Service

Privacy Policy

Cookie Policy