Support Negative Availability Literals

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?

Precisely, but that's my point for that spelling. That would make #unavailable(iOS X) on tvOS be semantically equal to an empty statement, so I don't know if it's the best way to go.

I believe Xiaodi is referring to my bump post here. That is my original draft for the revision, and the PR for it is available here (but it's the same as the post for now)

The new draft has some thoughts on this, which I re-described here. For the regular availability the semantic is that the compiler fetches the relevant specification from the list and discards the rest, so there's no real boolean logic like in an if. I think this makes sense for unavailability as well, although we agree that reversing the functionality of * might be confusing at first glance. On the other hand, I'm yet to find a use-case where one would need to write an availability statement that intentionally relies on the wildcard to work correctly to justify the confusion as being a legitimate problem, and not just bad coding on the confused person's end.

That's...not the topic of this thread, in that it isn't helping to move the revision of SE-0290 forward.

This is precisely the core team's decision:

The core team believes this is a merited addition to the language but that specific details of the proposal (as pointed out in the review thread) need to be addressed. [...]

The proposal needs revision to address the semantics of the #unavailable annotation for the platforms that are not specified. The wildcard * for #available has precise semantics, where * indicates the minimum deployment target for the unspecified platforms. 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 point of the discussion here is precisely how to address the review comments in a new revision.

Aside from some legalistic nonsense about whether I'm allowed to widen the scope of the discussion to talk more about supporting negative availability literals, in a non-review thread called "Support Negative Availability Literals"…

In the core team decision you quoted, they already have the semantics of *:

* indicates the minimum deployment target for the unspecified platforms

which is the same as Bruno's definition in the proposed revision:

the wildcard represents the minimum deployment target of the unspecified platform being compiled

If your argument is that the core team didn't realize (or thought that Swift programmers generally wouldn't realize) that this would be the meaning for #unavailable just as well as for #available, then you're sorta making my case for me: that it's not obvious how to "apply" that meaning to #unavailable in an easily graspable way.

In practice, no one — I swear, no one — reads #available(…, *) as something about deployment target versions on unspecified platforms. They teach themselves or learn that in practice it means "just do it" on unspecified platforms, and that leads to interminable confusion for #unavailable(…, *).

I believe that's what the core team is asking to be clarified, and that's what seems impossible.

Upon reflection, I have to agree with you that I was wrong to propose a different solution in response to the returned-for-revision review comments. That's not a viable way of proceeding in the evolution process.

So, I have to fall back to my already-stated fallback position: using * as proposed on #unavailable is too confusing, and this proposal should not be implemented.

My reading of the review notes is that during the review we were asked what would happen if you wrote #unavailable(*), of which my answer was "I don't know" as I had overlooked that detail. Then during the review we described the possibilities, and eventually found that the wildcard officially represents the minimum deployment target, concluding that the expression should return false. What I understood from the notes is that I think they just mean for us to formally describe this thought process in the proposal as it was a discussion point during the review that wasn't initially considered.

1 Like

This seems unproductive now, since you gave exactly this answer in the review thread, 14 days before the core team considered the proposal. I don't think they were uninformed about this definition, yet they asked for clarification.

I already said what I think they're asking for, 3 posts upthread.