`@available(*, unavailable)` enum cases and switch exhaustiveness

I stumbled across something unexpected with @available(*, unavailable) on enum cases and wanted to check whether this is intentional or not.

Given this somewhat philosophical enum:

enum Availability {
    @available(*, unavailable)
    case unavailable

    case available
}

Both of these compile:

// Version A - just the available case
switch availability {
case .available: print("here")
}

// Version B - both cases
switch availability {
case .available: print("here")
case .unavailable: print("how did we get here?")
}

Meanwhile, construction is correctly blocked:

let nope = Availability.unavailable // error: 'unavailable' is unavailable

So it seems like @available(*, unavailable) on an enum case both prevents construction and excludes the case from exhaustiveness checking, while still allowing you to pattern match on it.

Is this intended behaviour? And if so, what's the motivation?

$ xcrun swift --version
swift-driver version: 1.127.14.1 Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
Target: arm64-apple-macosx15.0

Yes, it's intentional that enum elements that are unavailable in the current compilation context do not need to be covered to make a switch exhaustive. As you explored, stating that the element is unavailable means that it should be impossible to construct it and therefore the case for the unavailable element would be unreachable (any code that does somehow construct unavailable elements is exercising undefined behavior).

You might be wondering why, then, switches that cover the unavailable element are also accepted. This tweak to your example illustrates why:

enum Availability {
  @available(macOS, unavailable)
  case unavailable

  case available
}

func test(availability: Availability) {
  switch availability {
  case .available: print("here")
  case .unavailable: print("how did we get here?")
  }
}

The .unavailable element is unavailable when compiling for macOS, but when compiling for other targets case .unavailable: is reachable. If the compiler rejected case .unavailable: you'd have to wrap it in something like #if !os(macOS) in order to make the code portable, which would be a hassle.

It's possible the compiler could make an exception for @available(*, unavailable) elements since they should always be unavailable, but even that might create ergonomic issues since sometimes @available(*, unavailable) is used to make a declaration unavailable under certain compile time conditions that just aren't expressible in the language.

5 Likes

That framing makes some sense. Thank you for the insight.

But that leads me to my next question:

enum Availability {
  @available(macOS, unavailable)
  case unavailable

  case available
}

func test(availability: Availability) {
  switch availability {
  case .available:
    print("here")
  case .unavailable:
    executeUnavailable() // 'executeUnavailable()' is unavailable in macOS
  }
}

@available(macOS, unavailable)
func executeUnavailable() {
    print("how did we get here?")
}

It seems like any moderately complex cross-platform code would need to use other directives anyway?

Yes, you'd need to conditionalize the code if it uses other @available(macOS, unavailable) declarations in the case body. To avoid that, the compiler would need to treat the case body as if it had all of the availability constraints implied by the enum element's availability, which is a reasonable enhancement request but a bit tricky to implement. There's plenty of potential code that doesn't need to do that, though, and still benefits from allowing the switch to cover unavailable cases.

1 Like