Skip to content
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

[SR-11828] case nil compiles for any type even if it will never match, due to optional hoisting #54240

Open
lilyball mannequin opened this issue Nov 22, 2019 · 7 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. standard library Area: Standard library umbrella

Comments

@lilyball
Copy link
Mannequin

lilyball mannequin commented Nov 22, 2019

Previous ID SR-11828
Radar rdar://problem/57418653
Original Reporter @lilyball
Type Bug
Environment

Apple Swift version 5.1.2 (swiftlang-1100.0.278 clang-1100.0.33.9)
Target: x86_64-apple-darwin19.0.0

Additional Detail from JIRA
Votes 1
Component/s Standard Library
Labels Bug
Assignee None
Priority Medium

md5: 1b964c512d445f610c1522c297d47045

Issue Description:

It appears that in any switch you can write case nil (or use nil as any sub-field of the case) and it will compile. It seems the nil here is becoming an _OptionalNilComparisonType, using ~= to compare, and the value it's comparing against is hoisted up to an optional.

This can also be seen with the simple expression nil ~= 3.

The net result here is I just wrote a case that can't possibly ever match and I got no warnings.

I believe we need to take the ~=(lhs: _OptionalNilComparisonType, rhs: Wrapped?) operator and do whatever magic you've done in other cases to produce a warning if this optionally-hoists the right-hand side.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Nov 22, 2019

Reproduction code:

func foo(_ x: Int) {
    switch x {
    case nil: fatalError("unreachable")
    default: break
    }
}

@beccadax
Copy link
Contributor

@swift-ci create

@typesanitizer
Copy link

Interesting! For the following code, we do emit a warning:

func foo(_ x: Int) {
  switch Optional.some(x) {
  case .none: print("Unreachable")
  default: ()
  }
}

but we suppress the warning if print is replaced by fatalError. For consistency, in the implicit conversion case, I think we should not warn in the fatalError case, but we should warn if there is some non-aborting code.

Right now, the constant evaluation machinery is relying on @_transparent and that particular overload is already marked @_transparent. Wonder why it's not getting triggered. Hmm. cc ravikandhadai (JIRA User).

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Nov 23, 2019

The warning in that case is different than in the original. Replacing fatalError("unreachable") with print in my original sample still doesn't print a warning.

I think the problem is in your sample code, you're stuffing a constant value into the switch so the compiler knows that it will never match nil, but in my case the switch condition isn't an optional and instead we're relying on optional hoisting during the evaluation of ~=.

@typesanitizer
Copy link

> Replacing fatalError("unreachable") with print in my original sample still doesn't print a warning.

Agreed. All I was saying was I would not expect your fatalError example to produce a warning (which it does not, correctly), but it ought to produce a warning if we replaced fatalError with print (but it does not, and that is certainly a bug). So I think we are in agreement there.

> I think the problem is in your sample code, you're stuffing a constant value into the switch so the compiler knows that it will never match nil, but in my case the switch condition isn't an optional and instead we're relying on optional hoisting during the evaluation of ~=.

I don't quite get what you mean by "optional hoisting" but I realize I was mistaken in understanding what it was "desugaring" to.

func foo(_ x: Int) {
    switch x {
    case nil: print("unreachable")
    default: break
    }
}

// I initially thought it got translated to this [incorrect]

func foo(_ x: Int) {
    switch Optional.some(x) {
    case nil: print("unreachable")
    default: break
    }
}

// actually it seems to be getting treated like this [also incorrect?]

func foo(_ x: Int) {
    switch x {
    case (let y) where (nil ~= Optional.some(y)): print("unreachable")
    default: break
    }
}

If it were getting treated as the second case, because that overload is marked @_transparent , it should give a warning. For example, the following code gives a warning as long as nope() is marked @_transparent.

@_transparent
func nope() -> Bool { return false }
func foo(_ x: Int) {
    switch x {
    case (let y) where nope(): print("unreachable")
    default: print(x)
    }
} 

So my second interpretation is also wrong. I wonder what it is actually doing.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Nov 24, 2019

Agreed. All I was saying was I would not expect your fatalError example to produce a warning (which it does not, correctly), but it ought to produce a warning if we replaced fatalError with print (but it does not, and that is certainly a bug). So I think we are in agreement there.

I do in fact expect it to print a warning. The warning in your case is "this code isn't reachable", and it makes sense to suppress that with a fatalError because otherwise you'd get a warning on a fatalError("unreachable") for code the type system demands you write but the compiler can determine will never execute.

But in my case, I expect a warning even with fatalError because the warning should not be "this code is unreachable" but rather "you're invoking optional hoisting when you don't want to", which is a warning we already have elsewhere, most notably on the lhs of the ?? operator.

I don't quite get what you mean by "optional hoisting"

When you use a non-optional value in a place where the type system expects an optional, so the non-optional value is implicitly wrapped up in Optional.some.

If it were getting treated as the second case

I believe it's being treated as

func foo(_ x: Int) {
    switch x {
    case (let y) where (nil ~= y): print("unreachable")
    default: break
    }
}

This does not produce a warning.

@typesanitizer
Copy link

But in my case, I expect a warning even with fatalError because the warning should not be "this code is unreachable" but rather "you're invoking optional hoisting when you don't want to"

Makes sense. Thanks for clarifying.

I think I realized the issue: @_transparent functions seem to not work for diagnostics when you have non-trivial computations in them, like if/switch, it only works for simple integer addition and the like. So marking that overload @_transparent seems to not have any effect.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. standard library Area: Standard library umbrella
Projects
None yet
Development

No branches or pull requests

2 participants