[Pitch] Extensible availability checking

Swift gives developers a way to limit the use of declarations in certain contexts using the @available attribute. For example, a library developer can specify that a new API is only available at runtime on macOS 15 or newer using @available:

@available(macOS, introduced: 15)
public struct Foo { /* ... */ }

The compiler only accepts references to the struct Foo in code that proves that it executes on macOS 15 or later, using either @available or if #available(...):

let _ = Foo() // error: 'Foo' is only available in macOS 15 or newer

if #available(macOS 15, *) {
  let _ = Foo() // OK
}

@available(macOS, introduced: 15)
struct Bar {
  var x = Foo() // OK
}

The @available attribute can also be used to mark declarations as deprecated, obsolete, or unavailable and restrict availability relative to the active Swift language mode, or even universally:

@available(*, deprecated, message: "Don't use this")
func notRecommendedAnywhere()

@available(swift, obsoleted: 6.0)
func proneToDataRaces()

@available(watchOS, unavailable)
func onlyForBigScreens()

The functionality of the @available attribute is powerful, but so far it has been limited to expressing constraints for domains that the compiler has a built-in understanding of. Over the years, new kinds availability domains have been added to the compiler in a piecemeal fashion. For example, the availability of package description APIs relative to the Package.swift manifest version can be described with the compiler-internal @available(_PackageDescription, ...) attribute. Additionally, for the experimental Embedded Swift feature a compiler-internal @_unavailableInEmbedded attribute was created in order to restrict the use of standard library APIs that are incompatible with the Embedded Swift.

@available could theoretically be applied to many domains, but hard-coding an understanding of each kind of domain into the compiler doesn’t scale. This pitch seeks to unlock new use cases for @available by allowing developers to declare their own availability domains using Swift source code.

Motivation

It’s easy to imagine new uses for versioned availability checking that aren’t supported by @available today. For example, APIs in an ABI stable library could be annotated with the version of that library that they were introduced in:

@available(MyLibrary, introduced: 2.1)
public func newFunctionality() // only available when dylib v2.1 or later is installed

This would allow prebuilt dynamic libraries to be distributed separately from their clients and remain compatible regardless of the version of the library the client was built against, just like how apps built with the latest SDK for an Apple operating system can be deployed to run on older runtimes. As Swift expands to more platforms and eventually becomes ABI stable on them, this capability is increasingly important. Swift’s own standard library may need something along these lines, since the Swift runtime is not a built-in component that evolves in lockstep with platforms like Windows or Linux.

Another, less obvious yet very useful extension of availability checking would be to use it to encode boolean requirements. One can imagine using @available as a substitute for conditional compilation based on the state of a feature flag:

@available(MyFeatureFlag)
func requiresMyFeature()

if #available(MyFeatureFlag) {
  requiresMyFeature()
} else {
  // ... fallback behavior
}

In the example above, if the boolean value corresponding to the availability of MyFeatureFlag were known to be false at compile time, then the compiler would be able to constant fold away each if #available(MyFeatureFlag) query, keeping only the else branches and eliminating the unreachable declarations protected by @available(MyFeatureFlag). Expressing this compile-time constraint using @available allows the compiler to provide better diagnostics to the developer than if conditional compilation were used:

#if MY_FEATURE_FLAG
func conditionallyCompiled() { }
#endif

@available(macOS 16, *)
func conditionallyAvailable() { }

conditionallyCompiled() // error: Cannot find 'conditionallyCompiled' in scope
conditionallyAvailable() // error: 'conditionallyAvailable()' is only available for MyFeatureFlag

With enough expressivity for availability domain declarations, boolean availability domains could be used for a wide-range of purposes:

  • The availability of feature flags would not necessarily have to be determined at compile time. They could instead alter program execution dynamically if their values were determined at runtime.
  • Declarations could indicate that their availability depends on whether an upcoming or optional language mode is enabled, e.g. @available(StrictMemorySafety) or @available(StrictMemorySafety, unavailable).
  • Experimental standard library APIs could indicate that they are only available when building against the development toolchain, e.g. @available(ExperimentalPreview).

What is an availability domain?

This pitch defines “availability domains” as Swift language entities that represent dimensions of restrictions on the use of declarations. For a given availability domain, there is a single value associated with it and @available attributes can be used describe invariants in terms of that value. If the compiler cannot verify that the invariant specified by an @available attribute holds in a given context, then the declaration protected by the attribute cannot be used in that context.

Developers ought to be able to define their own availability domain so long as the following criteria are met:

  1. The domain’s value is defined globally in the module or program.
  2. The value does not change over the course of program execution. If the value were allowed to change, the compiler’s checks that the invariant holds would be subject to time-of-check, time-of-use races.
  3. The type of the value is either a version tuple or boolean. This is just a syntactic restriction that could be lifted if more expressivity were warranted for new use cases.
  4. The compiler can resolve the domain’s value by the executing or interpreting code written in Swift.

Availability domain declarations

Custom availability domains that represent a versioned resource could be declared in source code using a global function, annotated with a special attribute, and returning a version tuple:

@availabilityDomain(MyLibrary)
public func myLibraryVersion() -> (Int, Int, Int) {
  return (2, 1, 0)
}

A domain could also be declared as a read-only global variable (either stored or computed):

@availabilityDomain(MyLibrary)
public let myLibraryVersion = (2, 1, 0)

In any source file where the domain declaration is visible, the compiler would allow the domain to be specified in @available to constrain use of other declarations. For if #available queries the compiler would generate code that retrieves the version value for the domain to compare with the version specified in the query.

For domains that represent boolean conditions, the compiler could allow domains to be declared with Bool values:

@availabilityDomain(MyFeatureFlag)
public var myFeatureEnabled = true

And for availability domains where the value is known at compile time, the declarations could be declared as compile time values to guarantee that the compiler is able to propagate the value:

@availabilityDomain(MyFeatureFlag)
@const public var myFeatureEnabled = true

@available attributes and custom domains

The name of a versioned custom availability domain can be accepted as the first, unnamed argument to @available and any additional fields that would be accepted in an @available(swift, ...) attribute should also be accepted:

@available(MyLibrary, introduced: 1.0, deprecated: 2.0, obsoleted: 3.0)
public func fullCircle()

For custom availability domains with boolean values, the domain name can be accepted by itself, with deprecated, or with unavailable:

@available(MyFeature)
public func requiresMyFeature()

@available(MyFeature, deprecated)
public func requiresMyFeatureButDeprecated()

@available(MyFeature, unavailable)
public func onlyWhenMyFeatureIsDisabled()

It should be possible to module-qualify an availability domain name, in case it would otherwise be ambiguous:

import SomeLibrary
import OtherLibrary

@available(SomeLibrary.Feature)
public func requiresFeatureFromSomeLibrary()

@available(OtherLibrary.Feature)
public func requiresFeatureFromOtherLibrary()

Multiple simultaneous @available attributes for different availability domains should be accepted and form a conjunction of availability constraints:

@available(MyFeature)
@available(MyLibrary, introduced: 1.0)
@available(macOS, introduced: 15)
public func requiresMyFeatureAndMyLibraryV1AndMacOS15()

if #available statements and custom domains

References to custom availability domains should be accepted in #available(...) in order to allow developers to constrain availability in executable code:

// Versioned
if #available(MyLibrary 3.0) { /* ... */ }

// Boolean
if #available(MyFeatureFlag) { /* ... */ }

Since the soundness of availability checking relies on a domain’s value remaining constant over the lifetime of a process, the compiler should generate code for if #available that only retrieves the domain’s value once and then stores it to reuse for any subsequent comparisons.

Control over diagnostics

When a program is built for macOS, a target triple such as arm64-apple-macos14 is passed to the compiler and indicates the minimum version of the macOS runtime that the program requires. Diagnostics that would be emitted by the availability checker for declarations that were introduced in versions of macOS prior to the deployment target are suppressed. This is an important tool for developer ergonomics, since many programs are only designed to support a trailing window of platform runtime versions and incompatibility with earlier ones is irrelevant. A similar “deployment target” value could be specified for versioned custom availability domains as a command line argument, like -minimum-availability MyLibrary-2.1.3.

Sometimes, it also makes sense to ignore availability constraints from certain domains entirely. For example, when building a program using a macOS target triple, availability of APIs on iOS is not diagnosed:

// SDK
@available(macOS 15, iOS 18.0, *)
public func releasedFall2024()

// App built with -target arm64-apple-macos14
@available(macOS 15, *)
func justMacOS() {
  releasedFall2024() // OK, no diagnostics about iOS
}

Diagnosing iOS availability by default in this configuration would be an overreach by the compiler, since it cannot assume that the developer also plans to build the code for iOS. The developer should not be asked to satisfy iOS availability constraints if those constraints might never be relevant.

This platform availability example raises a question for custom availability domains: should there be a way to declare an availability domain but indicate that the availability checker should ignore it by default? For example, imagine the libraries in the Swift toolchain adopted a new versioned availability domain called SwiftToolchain and adopted it to describe the availability of new APIs in terms of the Swift toolchain version they were introduced in:

@available(SwiftToolchain 6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@frozen public struct Int128: Sendable { /*...*/ }

This would potentially be useful for versioning a binary distribution of the Swift toolchain on non-Apple platforms. However, use of any API with this additional availability constraint in code compiled for Apple platforms would be required to satisfy the SwiftToolchain constraint in addition to the existing platform availability constraints for the API. These extra availability annotations and conditionals would be redundant, but the compiler would have no way of knowing that. One way to solve this would be to make the SwiftToolchain availability domain unchecked by default in Apple SDKs, with the @unchecked attribute or something similar, for example:

@availabilityDomain(SwiftToolchain)
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
@unchecked // Ignore on Apple platforms 
#endif
public func toolchainVersion() -> (Int, Int, Int) { (6, 0, 0) }

Cross-platform libraries might still want to opt-in to diagnostics for SwiftToolchain availability, which could be done via a compiler flag.


If custom availability domains were offered in the Swift language, what problems would you want to see solved with them? Are there use cases for them in your own packages that require special consideration? I think there’s a large design space for domain declarations and how the compiler evaluates them, so I’m interested to hear alternative visions for the syntax and what can be expressed.

22 Likes

For maximum flexibility, the compiler could expand this to Bool-returning functions that take a version tuple as the argument:

@availabilityDomain(MyLibrary)
public func myFeatureEnabled(
  _ version: (major: Int, minor: Int, patch: Int)
) -> Bool {
  return version >= (3, 11, 23)
}

Hmm, seems to me like the logic could be encapsulated in the custom availability domain function itself? Consider some availability domain SwiftInt128Available:

@availabilityDomain(SwiftInt128Available)
public let isSwiftInt128Available: Bool = {
  if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
    return true
    // Ugh, I've garbled the logic;
    // imagine that the conditions are what you'd want them to be
  } else if #available(SwiftToolchain 6.0) {
    return true
  }
  return false
}()

This would limit the number of bespoke language features needed.

4 Likes

I'm not sure if this is the right place to ask, so apologies if this side-tracks the pitch too much, but…

One related need I've had from time to time is to find out if my code is using any "soft-deprecated" or "future-deprecated" API. Many Apple libraries like SwiftUI but also some OSS ones use availability annotations like:

@available(iOS, introduced: 13.0, deprecated: 100000.0, renamed: "foregroundStyle(_:)")
public func foregroundColor(_ color: Color?) -> some View

to kindly suggest in documentation that another API should be preferred. But if you used the API anyway, the compiler prints no deprecation warning.

I'm aware of no way to display a warning for these. The compiler doesn't seem to allow setting one's deployment target to OS versions that high (and even if it did, that'd cause all kinds of other problems with obsoleted API and such).

So would it be possible to use some form of incantation of -minimum-availability to turn uses of soft-deprecated API into proper compiler warnings?

It would be also nice if enabling soft-deprecation warning didn't trigger all deprecation warnings, as in the below SwiftUI example:

// Assume deployment target set to iOS 15.0:
Image(systemName: "globe")
  .animation(.default)          // ⚠️ warning: deprecated in iOS 15.0
  .onChange(of: state) { _ in } // âś… no warning yet; deprecated in iOS 17.0
  .foregroundColor(nil)         // ⚠️ warning: deprecated in a future iOS version
1 Like

Could you start another thread for this topic? It's a reasonable request, but I think it's not directly related to the pitch and should be discussed independently.

1 Like

@tshortli Also, it would be great to have such rendering in DocC for specific frameworks. I shared some thoughts about this here: Availability annotations for third party libraries when using Library evolution/ resilience · Issue #60458 · swiftlang/swift · GitHub.

For instance, in the documentation for Date, we can clearly see that it is available starting from iOS 8:
Date | Apple Developer Documentation.

However, for something like CommandGroup in the ArgumentParser framework, we can't see that it’s available starting from ArgumentParser 1.5.0. This makes it harder to know about such details, especially if you can’t easily migrate to a specific version of a framework and only have access to the latest version of the documentation.

I hope I managed to clearly express my idea! By the way, thank you for your pitch—it’s much appreciated!

I've posted before about the need for some sort of compile-time check on the availability of particular SDKs in the developer toolchain. See this thread for details… but in short, I want to be able to do something like this:

if #available(macOS 11, *) {
    // This is only available in the macOS 11 WidgetKit SDK;
    WidgetCenter.shared.reloadAllTimelines()
}

However, if I'm using a developer environment that doesn't have the macOS 11 SDK, this fails to compile, because the compiler doesn't even know what WidgetCenter is. (This could happen during an Xcode beta period, for example, when some team members might be using a beta SDK, and others are not.)

If I'm reading this pitch correctly, this use case would be addressed something like this:

if #available(WidgetKit 11, *) {
    // This is only available in the macOS 11 WidgetKit SDK
    WidgetCenter.shared.reloadAllTimelines()
}

If the WidgetKit SDK in the build environment wasn't version 11, then the compiler would just skip over that code entirely, ignoring the unrecognized symbols. Is that correct? I'm sure we can't make any promises about whether Apple would adopt such availability features in their SDK, but does this pitch provide a system that system SDKs (like Apple's) could adopt it? If so, I'm extremely excited about this pitch.

In the case of an Apple framework like WidgetKit, the version number might be tricky. For example, a developer might be tempted to write if #available(WidgetKit 11, *).

But the same SDKs became available on both macOS 11 and iOS 14. If we try to use the OS version number in the library, it's unclear whether it should be WidgetKit 11 or WidgetKit 14. Or if #available(WidgetKit, macOS 11, iOS 14). That all seems a mess. So it seems like system frameworks would need to be able to declare their own versioning, independent of that of the system framework. It looks to me like this pitch would support that kind of versioning. Am I reading that right?

2 Likes

This is an interesting idea. I've also wanted to find a way to allow a custom availability domain to map their versions onto the versions of other availability domains for convenience and compatibility. I don't think the approach in this example will work, though, because the type-checker needs to understand the mapping in order diagnose whether an availability requirement is met. Even if isSwiftInt128Available were marked @const, that still wouldn't be sufficient to allow the type-checker to understand it since evaluation of compile time values does not happen until the code optimization phase of compilation. This is discussed more in the compile-time values pitch, but it's a relatively fundamental limitation of @const as it has been designed thus far. If we wanted to make this mapping legible to the type checker, we'll need a syntax that is more structured and declarative in order for the type-checker to interpret the mapping.

No, this is an important thing to clarify: code that is "unavailable" or "potentially unavailable" always gets type-checked. Not only does it have to be syntactically valid, it has to be able to compile down to executable code. Removal of code that is unreachable due to availability is an optimization during or after code generation (a guaranteed optimization, but an optimization nonetheless). This pitch is not changing anything about whether unavailable code needs to be valid. In fact, one of the value propositions of this pitch is that it can be used as an alternative to conditional compilation, allowing you to get feedback from the type-checker for branches that will be eliminated at compile time, in contrast to conditional compilation where you have to build in multiple configurations to get that feedback.

The problem you want to solve is important, but I think it requires a different approach. It inherently requires some kind of extension of conditional compilation, not availability checking. Another way to put it is that the # needs to be before the if, not before the available to solve the problem you're describing.

1 Like

This part is especially interesting to me. Some of our teams have a massive number of #if flags in their codebases to guard prerelease or experimental features, and there's often no single build configuration that can successfully type-check a source file in its entirety. This has become a problem for semantic analysis tooling (anything built on top of indexstore or, soon, the JSON AST dump) because the inactive blocks simply don't exist in the outputs.

If they switched to custom availability domains, even if inactive blocks are stripped away during codegen, they would still be type-checked and available in index data and so forth, correct?

Is there any possibility that import declarations could be guarded by availability domains as well? I suspect that the answer here is "no" because it would make imports order-sensitive if you were importing a domain from another module and then guarding a second import on that domain, but I'm curious what your thoughts are here.

3 Likes

Correct! The sort of problem you're describing is exactly the sort of the situation I have in mind for boolean availability domains.

I think the answer is "no". In order to successfully type-check the unavailable branches, the compiler needs to load all of the dependencies. An import declaration doesn't have any generated code associated with it, so there's nothing to eliminate from the binary for it and it's therefore not clear what guarding an import with an availability check would mean.

1 Like

I'd definitely make use of this feature in Swift Testing. The namespace for availability domains is the same as other symbols, right? That could be confusing if e.g. two modules declare an Apple or POSIX domain and they differ slightly.

Swift Testing symbols have bespoke Swift and Xcode availability, but for syntax reasons we can't just write @available(swift, 6.1) or what-have-you. We resort to documentation attributes on symbols instead. This approach may not play nicely with availability domains, and I would want to see some DocC attribute we could apply to a domain declaration to give it a "pretty" name (e.g. SwiftStdlib -> "Swift Standard Library").

1 Like

Are you suggesting—and if not, I would suggest—that these availability domains should be qualifiable by module just like other symbols are? So in case disambiguation is needed, or just more clarity, one could write NIO.SomeFeatureFlag, etc.

1 Like

I believe that's already the case from the pitch write-up?

2 Likes

Ah, I'd missed that. Excellent.

The namespaces for availability domains and declarations are separate in this pitch:

The identifier for the domain is MyLibrary, whereas the name of the declaration representing the domain is myLibraryVersion. I thought it best to separate them because I think the conventions for declaration and domain names may need to be different. I can see an also see an argument for just using the declaration name for simplicity. Either way, though, module qualification of domain names should be supported for disambiguation.

The pitch generally feels like a great direction to me. In particular, it directly solves the conditionalizing of decls/code for Embedded Swift.

(Currently, we use #if hasFeature(Embedded) and @_unavailableInEmbedded, both of which are likely not desirable if Embedded Swift were to become stable (not an experimental feature anymore). The #if hasFeature is tied to being an experimental feature, and the ad-hoc @_unavailableInEmbedded annotation is not general enough.)

1 Like