Lifetime definitions are an indisputably complicated feature. At least three aspects of the Swift language make the semantics challenging to express syntactically:
-
Swift lacks first-class references. This leads to a dichotomy between concrete vs. generic lifetimes. Concrete lifetimes imply a required lifetime dependence. Generic lifetimes represent potential lifetime dependence.
-
Unconditionally nonescapable types have fundamentally different lifetime constraints than conditionally escapable types, but the difference between these two flavors of lifetime dependent types is not syntactically apparent.
-
Swift stored property definitions are not, by default, part of a type's public interface, forcing some decoupling between a type's properties and the lifetime constraints imposed by those properties.
As a result, much of the rules described in the sections below depend on context. Although the semantics aren't readily apparent from syntax, the rules are actually self-explanatory and reasonably obvious once put into practice.
Nonescapable standard library types, such as Span
, are currently usable in Swift without adding lifetime definitions to the language. Lifetime definitions will allow Swift programmers to define their own nonescapable types.
Each ~Esacapable
type has at least one lifetime definition. Here we'll often refer them simply as lifetimes. For simple aggregates, lifetimes could be inferred from a type's stored properties. Whether inferred or not, the lifetime definitions need to be part of a type's public interface. This proposal avoids inferring public interface features from internal definitions, instead relying discrepancies between them to be diagnosed.
A lifetime definition is associated with a type in the generic context of its parent type. A concrete lifetime is associated with an unconditionally nonescapable type, typically Self
. Every unconditionally nonescapable type defines at least one concrete lifetime. Here, RawSpan
declares a storage
lifetime and associates it with Self
:
@lifetime(storage: Self) // concrete lifetime
struct RawSpan: ~Escapable {
let _pointer: UnsafeRawPointer
//...
}
Conceptually, storage
is associated with RawSpan
's _pointer
, but the explicit lifetime declaration provides a more meaningful name, and associating the lifetime with Self
allows useful dependency inferrence.
An initializer of an unconditionally nonescapable type must satisfy a lifetime dependency for each concrete lifetime. The dependency can be satisfied by inferred or explicit lifetime annotation on the initializer:
@lifetime(storage: Self) // concrete lifetime
struct RawSpan: ~Escapable {
// @lifetime(rawSpan) - inferred from 'Self'
init(_ rawSpan: RawSpan) { ... }
@lifetime(unsafePointer) // explicit borrow scope
init(unsafePointer: UnsafeRawPointer, count: Int) { ... }
}
A concrete lifetime cannot be associated with a conditionally escapable type. The following definition is illegal:
@lifetime(self: Self)
struct A<T: ~Escapable>: ~Escapable {}
// π `A` cannot be conditionally escapable because it's `self` lifetime
// is associated with concrete type: `Self`.
extension A: Escapable where T: Escapable {}
This means that unconditionally nonescapable public types can never be extended to be conditionally escapable.
A single concrete lifetime on Self
could be inferred as the default lifetime for an unconditionally nonescapable type:
// @lifetime(_: Self) - unnamed, concrete lifetime inferred
struct RawSpan: ~Escapable {
let _pointer: UnsafeRawPointer
//...
}
Instead, we choose to require explicit lifetime annotation on all unconditionally nonescapabe types:
-
A lifetime annotation clearly distinguishes unconditionally nonescapable types from conditionally escapable types: an important distinction that is otherwise absent from the type definition.
-
Forcing a lifetime annotation makes the irreversible library evolution requirement explicit. The author of
SpanPair
, for example, should deliberately decide whether each stored property should have a separate concrete lifetime before shipping the interface:
@lifetime(first: Span)
@lifetime(second: Span)
public struct SpanPair: ~Escapable {
let first: Span
let second: Span
}
- Each concrete lifetime creates a requirement that must be satisfied by all the type's intializers. An explicit annotation communicates that requirement to maintainers of the type.
A generic lifetime is associated with an a conditionally escapable type. Here, Optional
associates an unnamed lifetime with Wrapped
:
// @lifetime(_: Wrapped) - implicit generic lifetime
public enum Optional<Wrapped: ~Copyable & ~Escapable>: ~Copyable & ~Escapable {
case none
case some(Wrapped)
}
Generic lifetimes do not require an explicit lifetime declaration; its name serves no purpose, and a separate generic lifetime can be inferred for each generic parameter.
// @lifetime(_: T) - implicit generic lifetime
// @lifetime(_: U) - implicit generic lifetime
public struct A<T: ~Escapable, U: ~Escapable>: ~Escapable {
let t: T
}
Here, the generic lifetime associated with U
is unnecessary, but it places no additional restriction on the usage of its parent type, A
. Unlike concrete lifetimes, generic lifetimes are not required for type initializtion; they are only relevant when their associated type is used in a method signature.
Span
is unconditionally nonescapable, and, therefore declares a concrete lifetime. Its Element
type, however, is conditionally nonescapable; it will be associated with a separate, generic lifetime.
@lifetime(storage: Self) // concrete lifetime
// @lifetime(_: Element) // generic lifetime
struct Span<Element: ~Copyable & ~Escapable>: ~Escapable {
let _pointer: UnsafeRawPointer
//...
}
Nested concrete lifetimes can be exposed simply by associating the outer lifetime with the inner type:
@lifetime(storage: RawSpan) // concrete nested lifetime
struct TrivialSpan<Element: BitwiseCopyable> {
let rawSpan: RawSpan<Element>
}
The inner type may not be in the public interface, preventing the outer lifetime from being associated with the inner lifetime:
@lifetime(storage: Self)
public struct TrivialSpan<Element: BitwiseCopyable> {
let rawSpan: InternalRawSpan<Element>
}
As a convenience--to avoid specifying lifetime dependencies on all the internal interfaces--lifetimes can be aliased. Lifetime aliases can be but need not be in the public interface:
@lifetime(storage: Self)
public struct TrivialSpan<Element: BitwiseCopyable> {
lifetimealias storage = InternalRawSpan.storage
let rawSpan: InternalRawSpan<Element>
}
Lifetime aliases may also be useful in associating a lifetime with multiple types.
A protocol may define both concrete and generic lifetimes. Each generic ~Escapable
type in the protocol declaration, including Self
will have a generic lifetime, just like other generic types:
// @lifetime(_: Self) - inferred
// @lifetime(_: T) - inferred
protocol P: ~Escapable {
associatedtype T: ~Escapable
}
Additionally, a protocol can require concrete lifetimes via explicit lifetime annotation:
@lifetime(storage: Self)
// @lifetime(_: Element) - inferred
protocol NEContainer: ~Escapable {
associatedtype Element: ~Escapable
}
Nonescapable stored properties impose constraints on their parent type. If the stored property is generic over a conditionally escapable type, then that lifetime naturally propagates to the parent's generic context:
// '@lifetime(_: T)' - generic lifetime inferred
public struct A<T: ~Escapable>: ~Escapable {
var t: T
}
// '@lifetime(_: T)' - generic lifetime inferred
public struct B<T: ~Escapable>: ~Escapable {
var t: A<T>
}
If the stored property has a concrete lifetime, or concrete lifetime requirement, then the parent must have a concrete lifetime:
@lifetime(self: Self) // concrete lifetime
public struct A: ~Escapable { ... }
@lifetime(self: Self)
public struct B: ~Escapable {
var a: A // π ERROR: lifetime A.self is not exposed in B's interface
}
@lifetime(a: A)
public struct C: ~Escapable {
var a: A // β
OK: A.self is associated with the same type as `A`
}
@lifetime(self: Self)
public struct D: ~Escapable {
lifetimealias self = A.self
var a: A // β
OK: A.self is aliased with `self`
}
Question: this constraint is not required for correctness. It only adds Rust-like constraints. Do we really want it?
captures & non-recursive exclusivity