Support Negative Availability Literals

I don't understand how this feature would work without the wildcard. For example, what happens if I compile #unavailable(iOS 12) in watchOS? Shouldn't we throw a warning saying that we need to consider watchOS in the statement, which is exactly what * is meant to treat?

Even if does nothing in terms of functionality, it still serves the purpose of allowing the code to be shared to other platforms. If we remove that then we would have to re-think the entire statement -- should we allow multiple platforms in one statement for example? If yes, what do to if the platform being compiled is not in the statement?

I just don't know if it's worth it to bikeshed something that has no impact on the feature's functionality. The wildcard exists simply to prevent your code from not compiling in multiplat apps, and even in #available where it actually "does" something the huge majority of apps will never trigger it, and if they do the app will likely not compile anyway due to the platform specific code not being macroed out. Still, I do think it it's useful in terms of platform availability, and we can talk more about removing it if there's a clear solution to what happens in this case.

But we don't need to consider watchOS in this case, for the same reason that an if statement doesn't need to have an empty else clause: if we assert that some code executes based on a Boolean value, we don't need to assert that it doesn't execute on the opposite value.

It's fine for #unavailable(iOS 12) to be read as "on iOS 12+ only".

By contrast, the * solves a problem if this had been valid syntax:

if #available(iOS 12) {
    <some code>
}

This appears to say that <some code> executes only on iOS 12+ only, but we need it to mean "on iOS 12+ or any other platform". To avoid confusion, we need to add the * to tell the reader that.

In my view, the requirement for * makes sense for #unavailable because of the example with multiple conditions:

That clearly makes sense, because the first * tells me that the code will never be executed on watchOS, and the second * tells me that the code will never be executed on iOS. Therefore, I can see why the code won't ever be executed on any platform. The alternative spelling doesn't make so much sense to me:

if #unavailable(iOS 13), #unavailable(watchOS 3) {
  // Warning: code will never be executed
}

This will inevitably be read to say "if iOS 13 is unavailable and watchOS 3 is unavailable," and the warning will therefore be very puzzling: Surely, the code should be executed if either we're on iOS 13 12 or we're on watchOS 3 2? But that isn't what this code would do.

OK, that's what it means, I agree.

But your statement here confuses me. Comma-separated if clauses mean "and" not "or", so the way you say it reads is correct. It both reads and means the same thing that you said for the version with the *: it's an impossible-to-satisfy condition. Where would anyone get the "either…or" from?

FWIW, @rockbruno seems to be intending to make this an error, not a warning, with a fixit to consolidate the tests in a single #unavailable clause:

That's a bit of a typo on my part, which I will fix. But the point is that it is not an impossible condition to satisfy "if iOS 13 is unavailable and watchOS 3 is unavailable." If we're on either iOS 12 or watchOS 2, then the condition is satisfied.

Put another way, it would be legitimate to expect if #unavailable(iOS 13), #unavailable(watchOS 3) to be equivalent to if #unavailable(iOS 13, watchOS 3). That is not the proposed behavior, and the * is what tells the user of such.

Well, yes, and no, and yes, and no.

You are right, with the corrections you made, that my version would be misinterpretable as satisfying the condition on iOS 12.

However, according to the current proposal, every multi-clause unavailability check will evaluate to false, and this is going to be an error, not a mere warning about unreachable code. So, your multi-clause counter-example isn't something I was expecting to be valid anyway.

With a single #unavailable clause, it's a bit less clear cut, because I think it's natural to assume that listing the platforms to specific cases (and no "default" case) would imply that the code will execute only on those platforms. After all, the reason why we do have a default case in an #available check is that omitting it would tend to suggest that its code would execute only on the explicitly listed platforms.

However, on reflection I admit that it's a fairly weak argument, so I withdraw the proposed syntax amendment.

However, I'm going to go a bit further and say that it's pretty clear from (for example) this thread, that when trying to describe the '*' in an #unavailable condition, multiple people have got it wrong — even, I believe, you. (false was the correct answer.)

The only viable way, I am convinced, for a reasonable person to understand what an #unavailable condition means is to drop the 'un', work out what the #available condition means, then negate the answer.

For that reason, I think there are only two reasonable ways to proceed:

— Spell the unavailability check !#available. The question of whether it's stylistically worse is far outweighed by its clarity of meaning.

— Drop this proposal, and fall back to if #available(…) { } else { … }. It's a little ugly, but it's a small price to pay to avoid the confusion that #unavailable seems likely to produce.

1 Like

Strictly speaking, the logical behaviour is for multiple unavailability checks to evaluate to true if the intersection of their requirements applies. For example, #unavailable(iOS 13, watchOS 3, *), #unavailable(iOS 12, tvOS 12, *) should evaluate to true on iOS 12 and earlier, only.

This isn’t an important use case to support in today’s Swift, but it might come up in practice if we had macros or availability aliases.

It's true, but to be fair there was no documentation on what the wildcard was supposed to represent. Xcode has no symbol lookup for literals, and even the official documentation page wasn't describing it fully. The answer was buried in a doc comment inside the compiler, which was a research failure on my part as I didn't realize the * semantics were important until the proposal was already in review. Now, with all cards on table I think this makes sense, we need to consider how it will compile in alternate platforms regardless if it's going to resolve in a true or false statement.

By itself, (iOS 12) would fail to compile in watchOS because it doesn't know what "iOS" is. Surely deep down in the compiler we could make it work, but remember that * also has the readability purpose of saying that this is also considering whatever else you're compiling. It feels weird at first that it resolves to false, but even for #available I had no idea what #available(*) would do at first. I think this is an issue with the literal's documentation more than it's an issue with if our approach is right or not.

Recall that this question has already been settled by the core team, and the spelling is to be #unavailable. The proposal is returned for clarification on the wildcard.

The essence of the problem is that '*' in #available is informally understood to mean that the if code will always run on other platforms (other than those explicitly called out, I mean).

When someone with that informal understanding sees '*' in #unavailable, there will be a predisposition to think it also means that the if code will always run on other platforms — or at least to wonder whether or not the code will run on other platforms.

In fact, it means that the if code will not run. A lot of people are going to think that's bizarre. They are not in fact going to reason about the OS version relative to a minimum deployment version for a non-specific platform — even if, pace @xwu, they should.

To move this forward, what changes are you going to make to the proposal text to address the core team's question about the semantics of '*'? Is there a way we can introduce a "negative availability" feature without also introducing confusion?

I'm trying to find a situation where this could cause confusion to see if we should be advocating for * to return true like in the availability or to search for a different solution. What makes this confusing in my head is that if you do have a condition triggering the wildcard it means the condition has nothing to do with your platform, so whether it returns true or false should, in practice, be irrelevant. As mentioned before it really has no purpose besides allowing other platforms to compile.

I guess the only situation where the wildcard wouldn't be completely useless is if you're doing something like this:

if #available(iOS 13, *) {
    #if iOS
       // some iOS 13 code
    #elseif macOS
       // some macOS code
    #endif
}

This will work as designed, and in the case of unavailability it could cause some confusion indeed. But is this right? Why is the macOS code inside the expression if the statement doesn't explicitly define a version for it? I think someone who really needed this would write it like this:

if #available(iOS 13, *) {
    #if iOS
       // some iOS 13 code
    #endif
}
#if macOS
// some macOS code
#endif

This then goes back to the original point: The expression has now nothing to do with macOS, so we can't get confused because what it does just doesn't matter. Can you think of any legitimate reason to use the wildcard?

I think I might have an alternative (but not wildly different) approach. Let me try to write it up, and (if successful) I'll post it later.

Motivation

The semantics of * in the current unavailability proposal are hard to express informally, not least because the actual proposed meaning is that something is not going to happen, rather than that it is. We haven’t yet seen a straightforward statement of the meaning that doesn’t provoke a “wait … what?” response, even if that’s followed by “oh … OK” some time later.

I’ve come to think that we can’t come to agreement on * for unavailability because there’s an underlying problem with #available, and we must solve that problem before settling the design of #unavailable.

The problem with #available is that it’s too easy read as a complex Boolean expression. That introduces confusion because combining the sub-parts is not as easy at it seems, especially when we look at negated (unavailability) conditions with similar syntax.

For example, an availability condition that mentions a specific OS version, such as #available(iOS 14, …) is too easy to read as asking whether the following code should run if iOS 14+ is available. If it is not, what does that mean in the context of the whole expression? That iOS is not available? That iOS is available but it’s not version 14+?

Those questions have answers in the current design, but the answers are not as accessible as might be desired.

Proposed Solution: Availability

We start by fixing #available with two small changes. One is a syntax change to suggest improved semantics, and the other is a semantic change to the troublesome “other platforms” part of an availability check.

(1) We change the syntax to suggest that #available is more like a switch over possible platforms than a Boolean expression combined by ||. The suggested change is subtle. Instead of this:

    if #available(iOS 14, watchOS 7, *) { … }

we write this:

    if #available(iOS: 14, watchOS: 7, *: true) { … }

The idea is to emphasize that only one of the listed checks is made, depending on the compilation/run-time target: all the others are skipped. There is no sense in which the different parameters are combined as a Boolean result. (“The commas don’t mean OR”.)

This allows us to reason independently about each platform. Instead of reading this as “if available on iOS 14 or watchOS 7 or any version of any other platform”, we can read this as “here is what happens on iOS; here is what happens on watchOS; here is what happens on other platforms”. The different is slight, but important when we try to introduce syntax for unavailability checks.

(2) As shown above, the syntax of the catch-all case changes from * to *: true. It becomes a catch-all platform case, along with a value that says what to do in that case.

There is also a semantic change here. By specifying value true, we indicate that there is no version check here (avoiding a wordy statement about how the check works, involving the deployment target version, along with a version comparison that cannot actually fail). We are simply specifying that for other platforms, the code associated with the if statement will execute.

Note that a platform case for * is always required for #available, similar to the current rule.

Those are the only two necessary changes. However, with these changes comes the opportunity to round out the possibilities, and to introduce some conveniences.

— We allow *: false as an alternative to *: true, indicating that the body of the if is not executed for unlisted platforms. This is not possible at present, and may be an unlikely case, but I see no reason to disallow it.

— We allow (well, re-allow) * as shorthand for *: true. Since it’s a very common case, the shorter form is useful.

— We allow specific platform names to have values true and false instead of version numbers, if desired. true is roughly equivalent to testing against version 0; false is roughly equivalent to testing against version maxInt.

All of this means we can have availability expressions that look like this, with a mixture of true and false cases:

    if #available(iOS: 14, tvOS: true, watchOS: false, *: true) { … }

This allows the programmer to easily customize how the conditional code is used according to platform (and version, of course). Note that the tvOS case isn’t actually necessary here, but there’s no harm in allowing it to be specified explicitly, and it may be useful documentation sometimes.

Proposed Solution: Unavailability

The proposal for unavailability becomes almost identical to the current proposal, apart from being modified to match the above syntax.

The one important difference is that the * platform case is no longer required, though it may optionally be specified as true or false as desired. The reverse of this availability check:

    if #available(iOS: 14, watchOS: 7, *: true) { … }

now looks like this:

    if #unavailable(iOS: 14, watchOS: 7) { … }

In other words, the body of the if is executed on the listed platforms only , if the relevant platform’s version check fails. This example is equivalent to:

    if #unavailable(iOS: 14, watchOS: 7, *: false) { … }

The following example is meaningful but means something different:

    if #unavailable(iOS: 14, watchOS: 7, *: true) { … }

Note that * as shorthand for *: true (or *: false — which would it be?) is not allowed for #unavailable, because of the possibility of confusion.

As with #available, a true condition indicates that the body of the if will be executed on that platform, and false indicates that it will not . There is no confusing reference to the deployment target version, with a test that always fails.

Benefits

The above proposal removes any uncertainty or ambiguity about when a given platform test will be applied, and about what happens when a version test evaluates to false.

There is still a duality between #available and #unavailable. Here it is in abbreviated and full forms:

— The reverse of #available(iOS: 14, watchOS: 7, *) is #unavailable(iOS: 14, watchOS: 7).

— The reverse of #available(iOS: 14, watchOS: 7, *: true) is #unavailable(iOS: 14, watchOS: 7, *: false).

And another duality:

— The reverse of #available(iOS: 14, watchOS: 7, *: false) is #unavailable(iOS: 14, watchOS: 7, *: true).

Source Compatibility

If accepted, the above syntax would become the only syntax for #unavailable, and the preferred syntax for #available. The old syntax for #available would be taken to mean the same thing as the new syntax without the new spellings, and would have the same effect if not exactly the same semantics (in terms of deployment target versions).

Later, the old syntax could be deprecated with a warning and an easy fixit, and eventually the old syntax could be removed from the language.

In general this sounds good, I'm just not sure with omitting the wildcard completely because a call like #if unavailable(iOS x) would semantically make no sense in a different platform. I think we still need something to represent other platforms, even if it's the verbose *: false.

In any way I think we will keep running into circles with this, so before proceeding I thought about laying out everything we know it's true:

    1. The wildcard officially represents the minimum deployment target of a platform. It exists to make sure all availability statements are properly considering (and behaving correctly in) future platforms.
    1. The official docs for the attribute don't mention 1), only that it includes all platforms. This doc for the literal doesn't mention it at all.
    1. Because of 2), we had lengthy discussions about what the wildcard was supposed to do in unavailability until we pinpointed 1) in the compiler's code.
    1. Because of 2) again, any multiplatform availability/unavailability condition that is intentionally relying on the wildcard to be executed correctly will be confusing until you learn 1).

From here, I can make two conclusions:

    1. The proposal needs to include updating the official documentation to better explain what the wildcard means. I personally find it confusing at first even for the original #available.
    1. If there is a legitimate use-case for 4), then we need to go back and rethink the syntax of #unavailable. If there's not, we could conclude that the use-cases are confusing simply because they are not right.

I think we'll know what to do if we focus on answering 6. In general, it's the same question I added here.

It's semantically consistent with the way if works in general: the body is executed only for Boolean conditions that are (a) checked and (b) true.

In general, we do not need to explicitly say, "For all other possible conditions, the ones we're not actually checking here, don't execute the body of the if". It's implicit.

Keep in mind that I'm suggesting an actual semantic change to #available in order to ease the way to #unavailable. The argument against changing the semantics cannot be that it … changes the semantics. :slight_smile:

However, if it's thought necessary to exhaustively list platforms — and, after all, I'm modeling this after the switch concept, for which Swift requires exhaustive enumeration — then go ahead and require a catch-all case for #unavailable too.

But (I'm suggesting) do not spell the #unavailable catch-all the same way as the #available catch-all, and do not give it the semantics of an unnecessary version check that requires a decoder ring to discover that it's always false. Just say false.

[Actually, I'm flexible on the spellings. For example, we could say others or even default instead of *, and we could say always/never instead of true/false.]

The availability is a little different from a normal if. A call like #available(iOS X, watchOS X, *) in tvOS doesn't mean "iOS, watchOS, or the minimum target of tvOS", it's just "the minimum target of tvOS". The compiler picks the relevant specification from the list and discards the rest, so a call without the wildcard would assume that tvOS "understands" what iOS and watchOS are, which is not how it works, so I don't know if that's what we should go for.

But this is a little tricky now because we're now approaching it from a readability point of view, so anything could be deemed correct if we think it could work.

One of the reasons that design tasks are hard is that they have to operate under given constraints. It is not within scope here to redesign #available. The challenge from the core team is specifically: with Swift as it is currently designed and a preference for #unavailable as the spelling for this proposed feature, how do we define the semantics of wildcard *?

In my view, @rockbruno gives a reasonable answer to the question. Are there superior alternatives? If not, the re-review should proceed with that clarifying revision.

Rather than start from the syntax, maybe we should start from the semantics?

As in, what sorts of conditions are people going to want to write? Once we know that, then we can decide how to spell them.

Here is a list of the possibilities for two specific operating systems, conjoined by “and” or by “or”:

“iOS 14 is not available”

“iOS 14 is not available but some other version of iOS is”

“iOS 14 is not available, and macOS 11 is not available”

“iOS 14 is not available but some other version of iOS is, and macOS 11 is not available”

“iOS 14 is not available, and macOS 11 is not available but some other version of macOS is”

“iOS 14 is not available but some other version of iOS is, and macOS 11 is not available but some other version of macOS is”

“iOS 14 is not available, or macOS 11 is not available”

“iOS 14 is not available but some other version of iOS is, or macOS 11 is not available”

“iOS 14 is not available, or macOS 11 is not available but some other version of macOS is”

“iOS 14 is not available but some other version of iOS is, or macOS 11 is not available but some other version of macOS is”

There are of course other possibilities such as “xor”, “nand”, and so forth.

The question becomes, which of these conditions are sensible, which are expected to be common, and which should we support?

Once we know what we want to support, then we can decide on the spellings.

This seems like an incorrect conceptualization. It's not "tvOS" doing the understanding here. Only the Swift compiler sees the full set of availability parameters. By the time it comes to execute on tvOS (for example), there's nothing referring to iOS or watchOS left. All that remains at run-time is the tvOS version check.

Sorry, I didn't reply to this the first time you made the same point.

Formally, I'm asking that the current proposal be withdrawn, and a new proposal (encompassing both #available and #unavailable) be pitched in its place. It would of course have to be re-reviewed in its entirety.

My argument for doing this is something along the lines of: "We thought we could implement #unavailable additively to the language, but that turns out to be too confusing. Let's use what we learned from trying this, and start over."

I see no update to the SE-0290 proposal that even mentions '*' in the text, let alone describes its semantics.

You and @rockbruno both seem to be saying you think the proposal is fine as-is, but it was returned for revision. What is the revision?