Allow enum case matching without '?'

For context, read: Matching optionals in a switch statement

At the moment, if the enum is optional, you have to use the ? operator to switch over the cases, otherwise an error diagnostic was emitted telling the user that the case does not exist in the enum.

@Joe_Groff has said it's a bug and that it should be allowed to match cases without using ?, but I would like to get the opinion of others including the core team members if this should be considered a bug or not. I have a PR ready that fixes it if the core team agrees it's a bug. Otherwise, I am happy to create a proposal & run it through Swift Evolution when new proposals are being accepted.

cc @John_McCall @Douglas_Gregor @Joe_Groff

3 Likes

I would consider this a bug, since IIRC we do allow patterns to be optional-promoted in some other cases.

4 Likes

Tagging @codafi, who was the main person against this change the first time it came up.

2 Likes

We allow promotions across typed pattern bindings and in expression patterns. The only other construct TypeCheckPattern tries to desugar are optional evaluation expressions to optional some patterns. I think that latter construction is the model we oughta shoot for here.

We could offer a fixit for this that uses the same logic that OP uses to insert an implicit conversion and instead just insert a ?. I think the confusion in the original thread was mostly about the diagnostic being poor.

We already offer a fix-it:

enum Foo { case one, case two }
let foo: Foo? = .two

switch foo {
  case .one: print("one") // Insert '?'
  case Foo.two: print("two") // No fix-it here though
}

however optional promotion seems much better and works similar to other cases such as:

let bar: Int? = 0

switch bar {
  case 0: print("0") // Ok
  default: print("not zero")
}

We can special case the diagnostics though so we don't end up saying Enum case 'one' not found in type 'Foo?'. There's also a bug where if we switch over a struct instead of an enum then the diagnostic still mentions an enum case rather than a struct static property (unsure if that's expected or not). But as mentioned in the original thread, it should be promoted instead to have a consistent behaviour.

1 Like

I think we have diverging definitions of “consistent” when it comes to these semantics. I believe the expression pattern coercion is an emergent behavior of type check pattern not telling the type checker to disable coercions, and the model of pattern matching semantics today not allowing implicit conversions is ultimately correct. Patterns in case statements in particular reflect value constructions. The constructor for an optional pattern is either none or some, not an invisible injection.

It’s worth noting too that not all pattern bindings are created equal when it comes to the conversion we apply, and we do warn when spurious promotions arise

if let x = 0 {} // warns

It’s certainly more convenient to allow this. I don’t know if it leads to more consistent semantics.

1 Like

Patterns in case statements in particular reflect value constructions. The constructor for an optional pattern is either none or some , not an invisible injection.

Shouldn't the latter case in the my example above not be allowed then? Something like case 0: ... should trigger the same diagnostic if we're switching over an optional Int.

It seems strange that one example is valid and the other isn't. We're even going through the process of actually looking up the enum case in the base type in the type checker, so a promotion if the case exists seems reasonable or at least convenient to a user and consistent with the Int? pattern as I mentioned above.

I am not saying something like if let x = 0 {} should be promoted, however in context of an enum, a promotion would certainly be helpful for the user.

Well, yeah. That’s what I mean. TypeCheckPattern tries too hard to assume patterns can be shunted into the form of the type of the value being switched on. It’s kind of spaghettified itself because of that.

Notice that TypeCheckPattern itself never performs optional injections, it always relies on the expression type checker to do its best to come up with them. That’s why I’m opposed to this change: we’re making a part of the type system that hasn’t previously dealt with implicit conversions suddenly aware of them. If Future Swift decides to support more kinds of implicit conversions, the corresponding injections and their commutative closure needs to be reflected in TypeCheckPattern.

I think we do some implicit conversions in TypeCheckPattern - for example, nil is converted to .none implicitly.

That’s arguably a desugaring, not an implicit conversion like the kind we’re discussing above.

2 Likes

Optional-promotion should work everywhere. If the context expects a T? and a T is provided, the compiler should automatically promote it. That is, we should follow the Liskov substitution principle consistently, and treat T as a subtype of T?.

This should apply to pattern-matching just as well as the rest of the language, and I would consider it a bug that the compiler does not already work this way. Situations where a value is promoted to Optional then immediately unwrapped, eg. if let x = 0, can give a warning but should not be an error.

1 Like

I’m coming at this from a more nuanced perspective than behavioral subtyping. It’s not that the typing constraints don’t line up - it’s clear that they do - it’s that you are matching values of a particular constructed form against patterns for a different constructed form. In doing so, we’re muddying our semantics by confusing patterns with expressions. They are separate syntactic categories in our grammar, and thus far they have had separate semantics because - among other reasons - the typing rules for expressions admit quite a lot more conversions than just optional upcasts.

I’m sorry to use the vaguery of platonic ideals (“forms”) when I’m arguing. I’m trying to avoid spinning off into the weeds about patterns. A lot of my thinking on this comes from Fred McBride’s thesis on the subject for LISP.

2 Likes

Interesting, I'll give it a read. Due to my limited knowledge of type theory, I can't really argue any further in that respect, however, I still feel this is a bug which should be fixed and work like other enum patterns (like Int? as described a few posts above). The code for the implicit conversion can be moved outside of TypeCheckPattern if needed.

So what's the verdict on this - can the core team provide a definitive answer if this is considered a bug or does this need a SE proposal? cc @John_McCall @Joe_Groff @Douglas_Gregor I'd love to put forward a proposal if needed :smiley:

I'll make sure we talk about it soon.

2 Likes

We discussed this in today's core team meeting, and we decided that it's a bug fix. Swift's pattern syntax intentionally blurs the line between expressions and pattern forms, with the idea that the difference should not be something users normally have to think about. There's also already precedent for subtyping rules in the pattern grammar with existentials; for instance, this:

protocol P {}
enum A: P { case a, b, c }
enum B: P { case x, y, z }

let p: P = A.a

switch p {
case A.a: ...
case B.z: ...
default: ...
}

is accepted, and the A.a pattern behaves like .a as A, performing the type check and then the enum case check in succession. In order to maintain consistency of behavior between pure patterns and patterns interpreted as expressions, we agree that it makes sense for a subtyping rule to apply to patterns matched against Optional as well.

16 Likes

Thanks Joe! Could you take a look at my PR: [Typechecker] Allow matching an enum case against an optional enum without '?' by theblixguy · Pull Request #22486 · apple/swift · GitHub, which fixes this.