Matching optionals in a switch statement

I have two questions about matching optionals in a switch statement. Apparently an Int? can be matched against non-optional integer literals or non-optional integer expressions:

let x: Int? = 1

switch x {
case 1:  print("one")
case 1?: print("one?")
case 1+2: print("one+two")
default: print("default")
}

Question 1: case 1? is never executed. Should the compiler detect that and emit a warning?

An optional enum can not be matched against a non-optional value:

enum MyEnum { case a, b }

let e: MyEnum? = .a

switch e {
case .a: print("a") // error: Enum case 'a' not found in type 'MyEnum?'
case .a?: print("some a") // OK
default: print("default")
}

Question 2: Is that fundamentally different from the integer case, or should it compile as well?

(Tested with Xcode 9.3.1, with both the default toolchain and the Swift 4.2 snapshot from May 20, 2018.)

Regards, Martin

1 Like

For question 1, there could be a warning when a case in a switch statement is unreachable, sure.

As for question 2, I believe the “dot syntax”/“implicit member expressions” are intentionally limited so that type inference doesn't have to do anything heroic. In this case, it's limited to looking up type methods and variables on MyEnum?, so you can use case .none, but not implicit member lookup on MyEnum which would be subsequently implicitly promoted to Optional. I don't know where these limitations are documented, though, and it's possible that some of them could be lifted.

I was going to write that this isn't that important because it's just a small convenience/syntactic sugar for MyEnum.a, but when I actually tried that in your example, using case MyEnum.a, I get

error: enum case 'a' is not a member of type 'MyEnum?'
case MyEnum.a: print("a") // error: Enum case 'a' not found in type 'MyEnum?'

which makes no sense to me, since I'm being explicit about the lookup and the result should be implicitly promoted. Trying another enum in that context shows that something seems broken with lookup in switch cases, even without the Optional.

enum MyEnum { case a, b }
enum E { case one }

let e: MyEnum = .a

switch e {
  case E.one: print("one") // error: enum case 'one' is not a member of type 'MyEnum'
  default: print("default")
}

And if you play around with it in a playground you can get truly nonsensical error messages like

error: NSCountedSet.playground:154:8: error: type 'E' has no member 'a'
case E.a: print("one") // error: enum case 'one' is not a member of type 'MyEnum'

which seem to be the result of some bad caching somewhere.

enum A { case one }
enum B { case one }

let e = A.one

switch e {
  case A.one: print("one")
  case B.one: print("one") // ERR: Enum case 'one' is not a member of type 'A'
}

But, but ... one is a member of type A!

@jawbroken: Do you mean this should be reported as being something more/worse than bad diagnostics?

Yeah, the whole thing seems broken. Thanks for resurrecting this thread, because I had forgotten about it. Your example is definitely bad diagnostics, but this one seems like a clear bug:

enum A { case one }

let e: A? = A.one

switch e {
  case A.one: print("one") // error: Enum case 'one' is not a member of type 'A?'
}

or, similarly,

struct S {
  var i: Int
  static var ten: S { return S(i: 10)}
}

let s: S? = .ten

switch s {
  case .ten: print("ten") // error: enum case 'ten' not found in type 'S?'
  default: print("default")
}

And they both seem to stem from the same underlying issue with name lookups in switch statements. I filed a bug report.

1 Like

Yeah, the whole thing seems broken. Thanks for resurrecting this thread, because I had forgotten about it. Your example is definitely bad diagnostics, but this one seems like a clear bug:

That's not a bug. The correct pattern for this is one of the following

switch e {
case A.one?: print("one")
}

switch e {
case .some(A.one): print("one")
}

We do not allow implicit conversions outside of expression patterns - which is arguably an accidental effect of TypeCheckPattern always trying to coerce to the scrutinee's type.

The diagnostics could be better here, but it's gonna take some serious special-casing.

Question 1: case 1? is never executed. Should the compiler detect that and emit a warning?

We should, but doing so will be tricky. We currently detect duplicate literals that have plain integral and floating type. Extending that check to optionals of literals requires checking for applies of ~= to an inject_into_optional

Question 2: Is that fundamentally different from the integer case, or should it compile as well?

The difference is that expression patterns allow conversions where normal patterns don't. This should not compile because the pattern in the first case does not reflect the construction of values of type A?. If it behaved this way, it would also be subject to the surprising behavior of your first example but we would at least be able to catch and warn about it. Checking arbitrary expression patterns, however, is subject to concerns isomorphic to solving the halting problem.

2 Likes

I looked up what an expression pattern is, and found it's the expression in a switch statement case label. So should this say “inside” not “outside”? I'm struggling to understand this sentence, perhaps because it's fairly heavy on internal compiler details.

If this isn't a bug, it's going to be hard to explain to people (including me) why those examples shouldn't work but these do:

let optionalInt: Int? = 1

switch optionalInt {
  case 1: print("one")
  default: print("default")
}

switch optionalInt {
  case (1 as Int): print("one")
  default: print("default")
}

let one: Int = 1

switch optionalInt {
  case one: print("one")
  default: print("default")
}

Can you elaborate on the mental model that I should have here?

That's a good idea.

It ought to comple as well. That's a bug.

Thanks for raising these issues! If you have a moment, both of them are worth filing on bugs.swift.org, if you haven't already.

case X.y is treated as case .y as X, in other words, as if there's a type cast pattern to the named enum inserted in between. This is what allows catch MyError.code to work to pattern-match a concrete type out of an Error value, for instance. We ought to emit a diagnostic that A and B are unrelated, so the cast will always fail, though.

That's an accident of the current implementation rather than intentional design. The pattern grammar in general attempts to follow the expression grammar, so that a pattern ...let x... looks like an expr ...123... that matches the pattern with x = 123.

1 Like

Pardon me for abusing this thread, but what does

Swift Sync System create

in [SR-7799] Matching optional enums against non-optional values · Issue #50338 · apple/swift · GitHub mean? (And what would be the proper forum to ask questions about the bug reporting system?)

"Swift Sync System" is a bot we use to synchronize bug reports from our public bug tracker with Apple's internal bug reporter system.

1 Like