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:
- The domain’s value is defined globally in the module or program.
- 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.
- 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.
- 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.