This document identifies several potential type constraints related to value ownership and lifetime in Swift and explains those constraints relate to each other. They have not yet been formally proposed. This is only a straw-man. The goal of giving these constraints a name and gathering them all in one place is at least twofold. First, we gain a clear understanding of how to structure the internals of the compiler. Second, as we propose language features, we can refer to dependent features and understand how the individual proposal will fit into cohesive programming model. The goal is not to surface each constraint identified here as separate knob in the language. We expressly want to avoid making Swift a language of annotations. But the only way we can minimize the impact on the programming model is by first seeing the landscape of features that we want to cover. Naturally, any new annotation will be justified before formally proposing an addition to the language.
Table of Contents
- Variations on type features
- Proposed constraints, behaviors, and wrapper types related to "move-only" types
- BitwiseMovable type constraint
- BitwiseCopyable type constraint (aka Trivial)
- NonCopyable type constraint (aka move-only)
- Immovable type constraint
- SharedValue type constraint
- BorrowedValue wrapper type
- @unique variable
@eagermove
type behavior- @noImplicitCopy variable
- @noImplicitCopy type behavior
- @dependsOn parameter
- @nonescaping variable
- NonEscaping (or @nonescaping) type constraint
- @nonescaping property
- @nonescaping enforcement
- Borrowed views
- C++ Interoperability Requirements
- Address-dependent types and native atomics (SharedValue)
- Bitwise borrowing requires a BitwiseMovable constraint
- Constraints that associate types with side effects
A type constraint places requirements on the type, usually via marker protocol. While a typical protocol conformance requires the implementation of methods or properties, a type constraint limits the structure or behavior of the type. For example, an AnyObject
constraint requires the type to be a single class reference. Similarly, a Sendable
capability prohibits references to shared objects. Type constraints and capabilities are transitive top-down. If an aggregate has a constraint, then so must its members, unless explicitly exempted.
Below, I use type-name convention (TypeName) for generic type constraints and capabilities. All other forms of type qualifiers are named as annotations (@typeQualifier).
A variable constraint places requirements on a variable's usage, usually by qualifying the variable's type. Constraining a variable means that it accepts more types. For example, a hypothetical "nonescaping" parameter would accept both escapable and nonescapable types.
public func constrainParameter<T>(argument: @nonescaping T)
Contrast this with adding a requirement to a generic type parameter, which restricts the API such that it accespts fewer types, but places no constraint on the usage of the argument:
public func constrainType<T : Escapable>(argument: T)
The type requirement is expressed as a capability (Escapable), while the variable's type qualifier is expressed as a limitation (@nonescaping).
Removing a default capability from a type requires new syntax for generic type constraints. We need to express that a generic type can be bound to either an Escapable or NonEscapable type. Strawman:
public func constrainType<T : ?Escapable>(argument: T)
We may want to enforce a type constraint without full-fledged type system support for a negative capability. Let's use this syntax to indicate that View
does not have the (default) Escapable
capability:
@nonescaping
struct MyView {...}
When MyView
is assigned to a variable, or bound to a parameter with a concrete type, @nonescaping
will be automatically inferred as a variable constraint:
public func cannotEscape(view: MyView) // '@nonescaping view' is automatically inferred
This strategy could be extended to concrete protocol constraints:
@nonescaping
protocol View {...}
Passing a View
to a generic API then requires an explicit variable constraint:
public func constrainParameter<T>(argument: @nonescaping T)
A View
can never be type erased such that it loses its nonescaping constraint. It cannot be passed to an unqualified generic API and cannot be assigned to an existential without the @nonescaping
constraint. (This severely limits usability of types with conrete constraints).
A type annotation may also be used to statically place constraints on values of that type without imposing any type constraints. This lets a programmer selectively bypass some default constraints for improved optimization. Type erasure conservatively drops the behavior, unless it can be inferred from a generic or protocol requirement. Specialization reintroduces the behavior.
protocol CanImplicitCopy {}
@eagerMove
struct PreferMove : CanImplicitCopy {...}
let preferMove: PreferMove = ...
let canImplicitCopy: CanImplicitCopy = preferMove
foo(canImplicitCopy) // could conceivably pass-by-copy
-
move and borrow may bypass the value witness
-
implies "bitwise-borrowable": a single instance may inhabit multiple addressable locations as long as exclusivity is enforced
-
constrains the type to be address-independent
-
transitively constrains all stored properties to be non-reference types
-
copy, move, and borrow may bypass the value witness (they all reduce to memcpy)
-
deinit may bypass the value witness (it reduces to a nop)
-
implies BitwiseMovable
Removes the copy capability from a type, grants it deinit capability, and grants it eager-move behavior
-
copy is prohibited
-
deinit is supported
-
preserves uniqueness (but does not constrain uniqueness)
-
grants @eagerMove behavior
-
is not BitwiseCopyable even if all members are BitwiseCopyable (deinit is witnessed)
-
transitively inferred on a struct from any of its properties
Removes both the copy and move capability
-
implies NonCopyable
-
cannot take ownership of values
-
transitively inferred on a struct from any of its properties
-
requires class property or global storage
-
implies NonCopyable
-
is not BitwiseMovable even of all its members are BitwiseMovable
That's it. There are no other special semantics. A SharedValue can be moved. But typically they would only be borrowed.
This is useful for address-dependent types. It guarantees a stable address across sequential inout calls and across multiple simultaneous borrows. A native atomic implementation would allow mutation via those simultaneous borrows.
SharedValue types can only passed as arguments by borrowing in-place. They can only be composed into borrowed values (e.g. passed to an Optional API) by wrapping them in a BorrowedValue type. This all natural falls out of the fact that they are not BitwiseMovable.
Alternatively, this can be expressed as a @pinned
variable declaration. SharedValue would be inferred for any struct containing a @pinned
property.
-
An intrinsic wrapper around a pointer to a value with guaranteed ownership
-
conforms to Copyable
-
conforms to BitwiseMovable
-
allows address-dependent types to be bitwise-borrowed or even copied
This is analogous to a C++ const &
type, but with safety guaranteed by exclusivity enforcement and scoped lifetimes.
-
guarantees that any reference contained in this value is unique
-
assignment from a non-unique value requires dynamic enforcement
A type behavior is used to control eager-move semantics. These types have relaxed lifetime semantics. They are moved or destroyed as soon as possible, making their lifetimes optimization dependent. Their deinitializers do not execute predictably relative to other side effects. (This is not the default behavior for lifetimes because, in the presence of weak references and custom deinitializers, it defies common programmer intuition and makes behavior difficult to reproduce and debug.)
Eager-move will automatically be implied for all copy-on-write (CoW) types, but can also be granted by certain type annotations.
See also Optimization rules governing the lifetime of Swift variables.
-
copy is prohibited, except via an explicit copy intrinsic
-
in the absence of a copy intrinsic, preserves but does not constrain uniqueness
-
grants eager-move behavior
- implies
@noImplicitCopy
for all variables of this type
This not a type constraint. Copies may "escape" through type erasure. Then subsequent copies of the type-erased value no longer need not be explicit.
-
indicates a lifetime dependence from the functions return value to a parameter (or self).
-
if the parameter is borrowed (on the caller side) or passed
inout
, then the dependence is on the exclusive access scope.
For BitwiseCopyable (trivial) return values, this serves as an "interior pointer" annotation.
-
values held by this variable cannot escape the variable scope. This includes explicit copies of the variable
-
escaping the scope by returning a value requires a
@dependsOn
parameter -
enforcement may be bypassed using
withoutActuallyEscaping
-
A borrowed NonCopyable variable implies
@nonescaping
-
As a concrete type constraint, this effectively implies
@nonescaping
on all variable declarations, including self and the initializer's return value -
As a generic type constraint, removes the type's Escapable capability. This is no safer than the concrete constraint but allows type erasure where the variable is not already either borrowed NonCopyable or explicitly qualified as
@nonescaping
.
@nonescaping
types are required to build APIs based on a BufferView
. See the BufferView
proposal.
-
requires read/modify accessors
-
implies
@depensOnSelf
for the accessor -
implies
@nonescaping
yielded value dependent on the access scope -
can't fullfill a protocol requirement unless the requirement is also
@nonescaping
[TBD] Refer to the borrowed view proposal.
@nonescaping
variables can never be type erased such that they lose their nonescaping constraint. They can neither be passed to an generic API nor assigned to an existential unless the destination has a @nonescaping
constraint. They can however, be borrowed as NonCopyable
types. Thus, @nonescaping
variables can be passed to move-only compatible APIs. The cannot, however, be passing to a __consuming
parameter (including self) since the argument is not borrowed.
Specifically, when an API refers to a polymorphic type, the compiler can enforce @nonescaping
arguments at the API boundary in any of these ways:
-
A
@nonescaping
parameter annotation -
A
borrow
parameter annotation combined with a NonCopyable requirement -
An
@_effects(nonescaping)
annotation, which is unsafe -
Automatic compiler analysis of always-emit-into-client code
[TBD]
See also:
Types without copy constructors requires:
- NonCopyable type constraint
Usability of non-copyable types requires:
- Generic type constraints for negative capabilities
- A NonCopyable constraint on all standard library types
- Copyable extensions for all standard library types
Safe sequence conformance for C++ container types requires the implementation of "safe iterators". Safe iterators requires "borrowed view" support:
@dependsOn
parameter annotations@nonescaping
concrete type constraints@nonescaping
properties (read/modify accessors)
Safe iterators requires have the same usability requirements
In rare cases, variables need to have a stable address. This will support native Swift atomics, potentially using special syntax. This example uses a @pinned
annotation to infer a "shared value" type:
struct Atomic<T> {
@pinned
var value: T
}
var x: Atomic<Int>
// Return an address for demonstration purposes--don't actually do this
let address = withUnafeBytes(of: &x) { return $0 }
withUnafeBytes(of: &x) { assert(address == $0) }
A shared value's representation can only be copied by reading it's internal state. Exclusivity allows simultaneous borrowing because both accesses appear to be reads. The atomic API allows mutation via a borrowed value. Atomic APIs would prevent the compiler from reusing or reordering the atomic value across this potential mutation.
// borrow 'x' twice in the same scope
updateTwoAtomics(x, x)
func updateTwoAtomics(a: Atomic<Int>, b: Atomic<Int>) {
let value = a.load(ordering: .acquire)
b.store(value + 1, ordering: .release)
// will load 'value + 1'
a.load()
}
Borrowing a shared value either needs to be in-place or it needs to be wrapped in a copyable "BorrowedValue" type that refers to the shared address.
Noncopyable (move-only) values have seemingly arbitrary usability limitations unless they can be assumed to be bitwise borrowable. The bitwise borrowable capability releases those limitations and is naturally implied by the BitwiseMovable constraint.
Borrowing arguments can be handled in the compiler by reusing the caller's storage whenever an argument is implicitly borrowed. This requires emitting an exclusivity check, but has no ABI impact. This does not, however, solve the other usability problems. Consider passing a borrowed value to an optional API:
func borrowOptional<T: ?Copyable>(t: T?)
func borrowValue<T: ?Copyable>(t: T) {
borrowOptional(t) // error: copying into an optional
}
If move-only values can be assumed BitwiseMovable, then all of these usability problems related to physical storage, such as building aggregates, vanish.
Unfortunately, ?Copyable
only removes a capability. We can't reasonably require the BitwiseMovable capability. So enforcing any assumption about BitwiseMovable requires adding type constraints to generic APIs:
func genericAPI<T: ?Copyable & BitwiseMovable>(canBeBorrowed: T)
Relying on bitwise borrowing would mean that the BitwiseMovable constrains needs to be plumbed through all generics API. It also means that values with Objective-C weak references can only be borrowed by boxing them inside a "by-reference" type.
Instead, we will likely need to speculatively box generic borrowed values just in case they might contain an Objective-C weak reference. We can do this either by adding a programmer-visible BorrowedValue type wrapper or by transparently introducing the wrapper as an implementation detail. The value witnesseses of BorrowedValue would be trivial pointer copies. By granting BitwiseMovable to a borrowed value, the BorrowedValue wrapper would allow that borrowed value to be passed to Optional APIs and generally composed with other borrowed values.
These constraints aren't needed for ownership control. But they are relevant for understanding how restrictions on types converge toward a programming model based on pure value semantics. These unlock advanced optimization capabilities.
-
constrains all user-defined deinits for all types transitively referenced by this type including their subclasses or conformances.
-
the user-defined deinits cannot produce side effects. Notably, it cannot access other objects unless they are uniquely referenced by the deinitialized object.
-
inferred when all types transitively referenced are final and have no user-defined deinit, within the usual boundaries of library evolution
- prohibits the implementation from accessing global variables, transitively
- repeated invocation cannot affect semantics
-
implies BitwiseMovable
-
implies Sendable
-
implies PureDeinit
-
grants
@eagerMove
behavior -
transitively requires all properties to be PureValue
-
constrains all references to be transitively unique (or CoW)
-
inferred for BitwiseCopyable and copy-on-write types whose elements are PureValues
-
implies @noglobal for all methods
-
implies @pure for any @noglobal function for which all arguments are PureValues
PureValue signals a broad intention that "mutation" cannot be observed by other values. This intention is not fully enforceable at the type level. The real usefulness of this constraint is twofold:
-
as a placeholder for related constraints that are important for safety and optimization (BitwiseMovable, PureDeinit, Sendable, eager-move).
-
to infer function purity without widespread function annotation
PureValue could mostly be subsumed by Sendable if we're willing to place most of the same constraints on Sendable.
Separately constraining types and variables presents a challenge for minimizing API annotations.
Imagine if, to achieve full optimization and flexibility, API declarations need to look something like this:
func properAPI<T: BitwiseMovable, PureDeinit, Sendable>(
argument: @moveonly @unique T
)
This argues for a single constraint name that captures all the defaults we want in a value-semantics programming model. We might even consider inferring generic type constraints from a parameter's variable constraints.