Swift 5.1 separates binary interfaces from compiler versions
Swift 5.1 delivers module stability and library evolution support. Opaque result types and property wrappers address related questions about what an API exposes to its clients.
A framework distributed as a binary has two audiences. The application compiler must understand its declarations, and the running application must agree with its compiled implementation. Swift 5 established the runtime contract on Apple platforms. The September 20 Swift 5.1 release adds module stability, with a textual interface that future compilers can consume. That removes a compiler-version coupling that a stable ABI alone could not address.
The release also includes support for library evolution. Together, these changes let library authors consider independent updates at more than one level. A client can change its compiler without requiring the original compiler's opaque module archive. A library can preserve appropriate flexibility to change its implementation without forcing every compiled client to change with it.
I like Swift's attention to explicit contracts. Binary distribution makes those contracts especially consequential because an application author cannot rebuild code that a vendor has supplied only as a compiled framework. The relevant question is which assumptions become part of that vendor's compatibility obligation.
A stable import format has a different job from a runtime
The earlier ABI Stability and More explanation distinguishes runtime compatibility from module compatibility. A swiftmodule archive describes declarations for the compiler, but its format is tied to the compiler version. An application can therefore fail to import a library even when their compiled code could otherwise interoperate.
The new textual interface addresses the import side of that problem. Its purpose is to communicate the binary framework's API in a form that different compiler versions can understand. It does not make the framework's private implementation source available. Nor does it turn platform-specific compiled code into a binary for another operating system or processor.
That last distinction matters when evaluating a distribution plan. Changing the compiler used to build a client is one axis of compatibility. Changing the platform on which the client executes is another. A stable declaration format cannot supply a missing implementation for the destination, just as a compatible implementation cannot supply an unreadable declaration format to the compiler.
Apple's Binary Frameworks in Swift session places the module interface in the larger framework-distribution workflow. The practical benefit is independence between vendor and consumer toolchain updates. That independence still requires the framework to describe the intended public contract and supply compiled code appropriate to its destinations.
Library evolution reserves implementation freedom
SE-0260 introduces library evolution mode and the @frozen annotation. The mode is opt-in through -enable-library-evolution. It changes the assumptions clients may make about the layout of public structs and the cases of public enums.
Without such a mechanism, a client compiler can embed knowledge of a type's storage layout. Adding a field later can invalidate that knowledge even when existing source-level operations retain the same spelling. A source-compatible edit is therefore not sufficient evidence of binary compatibility. Previously compiled clients do not get another chance to calculate the new layout.
Resilient types reserve the ability to change appropriate implementation details. Clients obtain information such as layout through mechanisms that allow the library to vary it. This has a cost because a client cannot always replace that interaction with a compile-time constant or a direct field access. The cost buys a specific form of independence between library and client releases.
A library distributed as source and rebuilt with its application has a different requirement. The compiler can optimize using the exact implementation included in that build, because the library does not change underneath the resulting executable. Enabling evolution mode everywhere would impose a distribution policy on code that does not need independent binary replacement.
The policy should follow the artifact being delivered. A private source dependency and a public binary framework may contain similar Swift declarations while making different promises about future versions. Build settings are part of that promise because they determine which assumptions the compiler gives clients permission to make.
An independently updated framework also needs to preserve the meaning of existing operations. A stable layout mechanism cannot make a changed interpretation of an argument harmless. Binary compatibility permits code to continue interacting, while the API documentation still determines whether that interaction remains useful to its callers.
Freezing a type is a long-term decision
Within an evolution-enabled library, @frozen gives up some of the reserved flexibility. A frozen struct promises that its stored fields will not change in the prohibited ways. A frozen enum similarly commits to its cases. That knowledge permits stronger client optimizations and, for enums, exhaustive handling of the fixed case set.
An annotation with that meaning belongs in API design, not merely in a list of performance hints. Once clients rely on the fixed representation, removing the annotation cannot erase the assumptions already present in their binaries. A library author needs a durable reason to believe that the relevant structure is complete.
For example, an enum that describes an open-ended set of service outcomes has different evolution pressures from one representing a mathematically fixed choice. The former may need additional cases as the service changes. The annotation cannot determine which model is appropriate, but it makes the author's choice consequential to compiled consumers.
I prefer reserving flexibility unless a type has a clear reason to expose a fixed structure. The optimization benefit is useful, but the distribution commitment can outlast the implementation that first motivated it. A declaration's current simplicity does not establish that its future requirements are equally simple.
Freezing also does not promise C layout. SE-0260 explicitly distinguishes its guarantees from those needed for a matching C struct. A library requiring a C-compatible representation should use an appropriate C declaration imported into Swift. Binary stability within Swift and interoperability with a foreign layout remain separate contracts.
Opaque results hide an implementation type
SE-0244 addresses another form of accidental exposure. A generic implementation can produce a complicated concrete type that clients do not need to name. Returning some Protocol lets the implementation choose a concrete type while clients work with the declared constraints.
A small example makes the direction of choice visible.
protocol Labelled {
var label: String { get }
}
struct FixedLabel: Labelled {
let label: String
}
func makeLabel() -> some Labelled {
return FixedLabel(label: "Ready")
}
let item = makeLabel()
print(item.label)
The caller can use label, but the declaration does not promise that the implementation type is FixedLabel. This differs from a generic function whose caller chooses a type parameter. Here the function chooses, and its public signature constrains what the caller can assume.
Opacity does not mean the function can return unrelated concrete types on different branches. The implementation must have one underlying result type for the relevant generic arguments. The hidden identity remains consistent, which gives the compiler more information than a container that can hold arbitrarily different conforming values at runtime.
This makes the choice useful for compositional APIs. An implementation can assemble generic components without exposing their entire nested type in its signature. A client depends on the operations the declaration promises. That can keep an internal restructuring from becoming an unnecessary source-level change for callers.
Wrappers give property behavior an explicit implementation
The release also includes SE-0258 property wrappers. A wrapper type implements a property's access behavior through wrappedValue, with synthesized backing storage for each wrapped property. An optional projectedValue supplies a related API through the dollar-prefixed projection.
That design separates the apparent property value from the mechanism used to access it. Repeated getter and setter logic can live in one named type, while declarations identify where it applies. The benefit depends on the wrapper communicating a coherent policy. An annotation that conceals unrelated work would make the property harder to understand even if it reduces repeated code.
The same question about exposure applies here as in binary libraries. A caller needs to know the behavior it can rely on, while implementation details need an appropriate place to remain private. A wrapper can make a recurring storage policy reusable, but the policy still has to define what reads and writes mean.
These additions do not remove the need to distinguish a source update from a binary update. Replacing a concrete return type with an opaque result in an existing public API can itself break compatibility, and changing a frozen layout can invalidate old clients. Swift 5.1 supplies tools for making these contracts deliberate. Their value depends on choosing the contract before independently distributed consumers begin relying on it.