Support Negative Availability Literals

Given that I now have a fully functional solution, I've submitted a draft proposal and implementation PR: https://github.com/apple/swift-evolution/pull/1184

1 Like

One thing I was wondering is that #available and #unavailable are very similar in the sense of reading through the code base, so it may be hard sometimes to spot that is an unavailable instead of an available if we are reading through the code looking for a problem. So one suggestion could be in code coloring use a slightly different color for #unavailable ... not sure if this was already thought or discussed but the point is to make it easier to look at a piece of code and easily spot which is an #available and #unavailable condition.

Maybe that's a stupid idea, but I'll throw it in the room nonetheless.

For this to work, we would need to basically have a (in lack of a better word) generic type, that has the availability requirements as a generic parameter.
Something like the following:

let hasiOS13: AvailabilityBoolean<iOS 13.0> = #available(iOS 13.0) // type could of course be inferred

This could enable an app having a single global variable, which can be used in if or guard statements or in ternary expressions and which could still be statically enforced.
It would probably mean a bit more implementation work, but could also pay off quite well IMHO.

It's a fair point. I wish we could go for the basic #available(...) == false but I don't know if the core team would accept it given that the implementation would be a huge hack. It's still in the table though if they think otherwise.

It would’ve been nice if there was a method you could call, like available(.iOS(14)), and get a Bool back which can used in various places like if or just storing it in some variable. So basically #available is modelled as a regular method call and we can deprecate existing uses of it. It might be a bit of a hack too but it seems more natural to me.

1 Like

Or a Struct/Class that provides this method and perhaps other methods to query the runtime environment. That seems more Swifty to me and better than adding free functions.

Using a hashmark to indicate something that doesn't resemble the C pre-processor doesn't seem that sensible, although I guess we're all used to it by now.

The Struct/Class would resemble UIKit's classes like UIDevice, UIScreen etc. that are Singletons and model aspects of the environment.

1 Like

Folks, while I would love to have #available as an expression I don't think it's really technically feasible to do so. This would likely require refactoring the entire symbol availability system, has many edge cases and doesn't really solve many problems. I would rather not diverge from the original proposal which just includes the ability to negate the current check, which is clearly an oversight in the compiler given that you can do so just fine in Obj-C.

5 Likes

Reviving this thread so we can work on the revision!

When I first wrote the proposal I was unaware of what the wildcard was precisely supposed to represent, but the compiler code has several pieces of documentation saying that it represents the minimum deployment target of the platform, as Ted mentioned. This supports the idea of having #unavailable(*) return false which was shown to be important during the review. Here's an updated draft to start with:

Semantics of *

The compiler uses the platform wildcard * to ease porting to new platforms. Because new platforms typically branch from existing platforms, the wildcard allows availability checks to execute the guarded branch on the new platform without requiring a modification to every availability guard in the program.

To achieve this in practice, the wildcard represents the minimum deployment target of the unspecified platform being compiled.

if #available(*) {
  // ...
} else {
  // Will never be executed
}

When multiple platforms are present in the statement, the wildcard represents only the platforms that were not specified. A check like #available(iOS 13, *) means "if compiling for iOS, iOS 13, otherwise, the minimum deployment target" and not "if compiling for iOS, iOS 13 and iOS's deployment target, otherwise, just the minimum deployment target". The wildcard doesn't include platforms that were explictly added to the statement, which can be visualized by how it's not possible to specify a platform multiple times.

if #available(iOS 12, *)
if #available(iOS 12, iOS 13, *) // Error: Version for 'iOS' already specified

For unavailability, this means that #unavailable(*) and #unavailable(notWhatImCompiling X, *) should return false. Since the minimum deployment target will always be present, the statement can never be true. This behavior also matches how a theoretical !#available(*) would behave if building expressions with #available was possible.

if #unavailable(*) {
  // Will never be executed
} else {
  // ...
}

The wildcard only represents platforms that were unspecified in the statement. This means that #unavailable(iOS 13, *) doesn't mean "iOS 13 and iOS's minimum deployment target", but "if iOS, iOS 13, otherwise the minimum deployment target"

As an interesting side effect, this means that having multiple unavailability checks in the same statement (#unavailable(iOS 13, *), #unavailable(watchOS 3, *) as opposed to #unavailable(iOS 13, watchOS 3, *)) would cause the statement to always be false.

In these cases, since wildcard checks are eventually optimized to boolean literals, the compiler will already emit a warning indicating that the code will never be executed. Still, we can provide a more descriptive diagnostic that suggests using a single check that considers all platforms.

if #unavailable(iOS 13, *), #unavailable(watchOS 3, *) {
  // ... 
  // Warning: code will never be executed
  // Error: unavailability checks are canceling each other, use a single check that treats all platforms 
  // fix-it: #unavailable(iOS 13, watchOS 3, *)
}
5 Likes

Thank you for this explanation of the * :slight_smile:

So, if I'm understanding this correctly, in the unavailability case, the * does not actually do anything (because anything it could do is already gated by the minimum deployment target), right?

So if we choose to keep it in the unavailability case, that would only be for syntactic consistency with the availability case, right?

Correct, I think it's fair to conclude that it's there just for consistency given that they use the same parsing/typechecking logic deep down. I think removing * would make the feature confusing, especially when providing fix-its. !#available(*), for example, wouldn't be fixable because the fix would be to remove the expression entirely. Allowing #unavailable(*) on the other hand makes all fix-its and warnings trivial to implement, plus the consistency.

That seems to me to be exactly backwards. We shouldn't be designing new language features based on whatever existing implementations of another feature happen to be lying around. Whatever it looks like internally, #unavailable is a different feature from #available. They're related, sure, but they're different. (If you absolutely wanted to insist they're "really" the same thing, just Boolean-ly negated, then you should — as I've said in the past — spell the new one as !#available and be done with it.)

If it makes no sense for #unavailable to specify *, then it should not do that. Simply raising the question in developers' minds as to why it's there makes this new feature complicated in a way that it's not supposed to be.

If we can't write an implementation that provides the feature we want, then I'd argue we shouldn't provide #unavailable at all.

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.

Terms of Service

Privacy Policy

Cookie Policy