SE-290 (second review): Unavailability Condition

The purpose of the wildcard is to allow code to be easily ported to different platforms. In this case, the minimum target will never be available, so it indicates that the expression should always return false.

I understand the first impression is to see it as "something that makes the expression return true", but this is not what the wildcard is -- just what it was originally meant to achieve in #available due to how platforms are branched. I think this could be cleared by changing the documentation to be clear about this. It was not necessary before because there was just one use-case for availability lists, but now that there's more than one it's clear that this is needed.

I don't have a lot of experience with tvOS, but both the app delegate and the scene delegate APIs are marked as available on tvOS, so I'm really not sure what you mean here.

I'm not sure what you mean by "complicated multiplatform scenario"—are you saying you can't see why someone would use this in a codebase that runs multiple platforms? (e.g. iOS & macOS). I think perhaps you're saying you can't see any reason why someone would write if #unavailable(iOS 14, macOS 11, *)? I can certainly imagine cases where someone would want to run code on either of those platforms if a certain version were unavailable. (e.g. Showing a "Sorry, widgets are not available on this version of OS" label.)

Can you help me understand what scenarios this feature is built for?

1 Like

I also don't, but let's assume that the same code can be used, without changes, between both platforms.

An user who makes their iOS app will add an unavailability check to support SceneDelegate:

if #unavailable(iOS 13, *) {
  loadMainWindow()
}

At some other part of the code, there will be an equivalent availability check to jump to the scene logic. This is the scenario this feature is built for, because this cannot be conveyed by a single statement.

Let's now assume that the developer adds support for tvOS. Here two things could happen:

    1. The minimum deployment target for tvOS is above the requirement to support scenes.
    1. The minimum deployment target for tvOS is below the requirement.

If 1) happens, then this code will continue to work regardless of what the developer assumed unavailability would do, because the unavailability will propertly return false (no backport needed) and the availability will return true (jump to scenes).

If 2) happens, first of all, the code will not compile, because the availability check now needs to include the correct tvOS version that implements scenes.

However, after fixing the availability check, if the user does not also modify the unavailability pair of the scenario to contain the version where to stop doing backwards compability, the app will fail to do so correctly and result in a bug in certain versions as the statement will always return false.

I think the point some readers are trying to prove is that the bug here is being directly caused by the assumption that the wildcard will always return true, but this is not true -- if the assumption is correct, you would face this bug regardless if this feature was called !#available, if it had no wildcard, if it had a special syntax or anything else we came up with during the pitch. The problem here comes from the statements not being one conjointed piece of code.

That line of reasoning assumes that an #unavailable check will be accompanied by a #available check somewhere else. That's not going to always be true (e.g. showing "Sorry, widgets aren't available here."), and it's not even necessarily true in the scene delegate case.

To use a scene delegate, you just declare a class that conforms to UISceneDelegate and note it in the app's Info.plist. The system frameworks take care of instantiating the object; none of your other code ever needs to reference it. Most scene delegates will never have an #available check in them; the system frameworks simply won't call that code on older versions of the OS.

So I don't think we can rely on the notion that an #unavailable check will almost always be paired with an #available check for reliability.

I made a mistake in my reasoning -- this issue would actually be solved if we didn't have a wildcard (and also no predeterminated behavior for other platforms), because it would cause the app to continue to fail to compile until the proper version was added. We would lose the ability to port code to other platforms though, so it's unsure if this alternative is viable for us.

I think this is a weird case. This cannot be done with #available {} else {}, so the only way someone would think that this would work for unavailability is if they think the wildcard means "always return true". I think this needs to be treated as documentation issue, because it's just not what it means and the only reason people think that is because the documentation is misleading.

Sure it can:

var widgetsAreAvailable: Bool {
  if #available(macOS 11, iOS 14, *) { return true }
  else { return false }
}

if !(widgetsAreAvailable) {
  widgetsUnavailableLabel.hidden = true
} 

But perhaps you mean that this can't accurately do the right thing for future platforms here, and that's true. But it's already the existing case. (Really, the right solution in this particular case is if #canImport(WidgetKit), but that only works because widgets have been defined in a separate module.)

If this is really meant to be the inverse of #available, then it seems like there should not be any *. If we could write out the "else" condition that #unavailable is mimicking, I think it would look like this:

if #available(iOS 14, macOS 11, *) {
  doNewStuff()
}
else { // #unavailable(iOS 14, macOS 11)
  doOldStuff()
}

Note that the * still lives on the top line; it doesn't need to appear on both conditions, because the wildcard has already caught everything else.

I'm honestly not sure what the right solution is here. I think #unavailable(*) is confusing, and I don't think we should accept the proposal as-is. The natural solution in my mind is to disallow the *, but as @xwu points out, users may be confused that this code doesn't run on linux:

if #unavailable(iOS 14) {
  // linux: iOS 14 is *clearly* unavailable, so why doesn't this run?
}

I think that situation is less confusing than the one where a * would explicitly suggest to me that it should run on any platform, but I won't argue that it's not at all confusing. We can add documentation all we want, but as you yourself demonstrated, users may not find that documentation even when explicitly looking for it.

Perhaps we could issue a warning if compiling on non-mentioned platforms, with an accompanying fixit?

// warning: on 'linux' platform, this condition will be false.
//   fixit: change to `#unavailable(iOS 14, linux *)` to silence this warning
if #unavailable(iOS 14) { }

(edit: Yes, I invented the linux * syntax here, and I'm not convinced it's right either. But some callout of linux would be appropriate. Maybe linux 0 instead?)

The confusion we had in that sense was because we were attributing a default behavior even though there was no wildcard. We can solve this by avoiding doing that at all, because then Linux wouldn't compile until you added the version in which you are checking unavailability for. This would also solve the example I made above with the disjointed statements, though we would probably have to come up with a way to represent "never unavailable for this platform because I don't care about it", just like the wildcard achieves that in the case of availability.

I think at the end it boils down to what the core team prefers. If we don't want to mess with the misleading view developers have of availability specs, then we can give up platform migration capabilities and get rid of the wildcard completely. Otherwise, I think boosting the documentations could treat the confusion in the end.

For getting rid of the wildcard, maybe one way of representing "I don't care about this statement" would be to have a versionless entry:

if #unavailable(iOS 13, macOS) { ... }

This returns true for iOS 13, always false for macOS (because it cannot possibly be unavailable if you can compile for it), while other platforms would throw a compilation error until they are explicitly added to the statement.

This should get rid of all confusion, at the cost of losing the platform migration benefits of *.

I like that spelling!

How can this be accepted? You'd be proposing to have code that straight-up doesn't compile (as in, causes a compiler error and doesn't result in a binary at all, not as in doesn't get compiled into the final binary) on Linux. This cannot possibly be preferred over the status quo.

1 Like

Well, in a way these pieces of code wouldn't compile anyway due to symbols that don't belong to X platform needing to be macroed out, but I agree that platform migration is important. This is just one alternative since the confusion topic always comes up.

It doesn't have to be a compiler error; it could also be a warning, with the default behavior called out in the warning message.

That said, it's hardly a new thing to have code that doesn't compile when you migrate to a new platform; any time you use platform-specific APIs, you're exposing yourself to the same thing.

This is a great addition to the language and will for sure help lots of folks simplifying their code.

It seems clear that there's no way to further expand this conversation without knowing the core team's opinion about the misinformation around the wildcard.

If they deem that this is not a big problem / that the views should be corrected, then we can likely just improve the documentation and roll with it. If they deem that it's an important issue we can eliminate the confusion by removing the wildcard, but if having code that doesn't compile is a problem, then there would be no perfect way to introduce this feature without either deprecating the current syntax or going with one of the "expressiony" alternatives like !#available.

Yep, we have all options on the table, it's up to the core team to make a reasonable and balanced decision which way to go.

I agree that positions have been laid out and it’s up to the core team to decide. I’m not entirely clear on what the “misinformation” around * is, though. Could you clarify?

I mean how the wildcard technically refers to the minimum target of the compiled platform, but since this isn't documented outside of the compiler a lot of people's first instinct ended up to think something in the lines of “all versions” or “always true”, which implies that this feature's wildcard would also return true.

I don't think that's an accurate representation of what the star means. Here's what the documentation says:

The * argument is required and specifies that on any other platform, the body of the code block guarded by the availability condition executes on the minimum deployment target specified by your target.

(Emphasis mine.) On any other unmentioned platform, the code runs. Yes, it runs with the availability of the symbols in that block is equal to the minimum deployment target of that platform, but that's not particularly remarkable, because that's true of all the code outside the availability condition as well. It's just saying "within this block, there's nothing special about symbol availability for unmentioned platforms".

The compiler implementation may substitute in the minimum deployment target of the current platform, but that's an implementation detail that is not necessary for a user to understand. Critically, that implementation could change so long as the overall behavior does not change; the simple guarantee is that on any unlisted platform, the code runs. We are not required to preserve the idea that * means "minimum deployment target" in other usages of the * symbol.

3 Likes

This is not compiler implementation. It is the publicly stated meaning of the * symbol by the core team:

If The Swift Programming Language does not adequately explain those semantics, then it is a defect in the book for which a bug should be filed. @rockbruno has described the meaning of * absolutely accurately.

2 Likes

Well, no actually, we have not enumerated all the possible design options. In fact, there is clearly the most intuitive option--albeit one that requires additional implementation work--which has not yet been put forward in this thread.

The core team's prompt for revision was specifically this:

The proposal needs to define the semantics for #unavailable for the platforms not specified and clearly explain how those semantics compose with the use of #available and other #unavailable annotations that may decorate a declaration.

The option that hasn't yet been mentioned here--although I sure hope that it was brought up during the revision pitch, but if so not by me--is to have no wildcard and to make if #unavailable(iOS 13) mean exactly what it says (in other words, on unstated platforms, iOS 13 is unavailable).

Composing would be intuitive: if #unavailable(iOS 13), #unavailable(macOS 11) would work exactly like if #unavailable(iOS 13, macOS 11), as users expect, and would not require further explanation or any warning; meanwhile, if #unavailable(iOS 13), #available(iOS 12, *) would also work exactly as expected and match if #available(iOS 12, *), #unavailable(iOS 13).

The implication, however, is that #unavailable(...) would not be merely the negation of #available(...). Syntactically that would be made manifest in that the former would forbid the use of wildcard * while the latter requires it.