The current design of lifetimes relies on "value lifetimes" and "conditionally non-escapable types". Values of Escapable
type have no lifetime scope. Values of ~Escapable
type have a single lifetime scope. A nonescapable value cannot live beyond the current scope unless the scope's function interface provides value-based lifetime propagation via @dependsOn
annotations.
E ≡ some T
NE ≡ some T: ~Escapable
Without a lifetime annotation, functions cannot return nonescapable values:
func foo(a: NE) -> NE // ERROR: interface violates nonescapable type checking
Adding value dependence to the interface allows nonescapable values to propagate out of their initial scope:
func foo(a: NE) -> @dependsOn(a) NE // OK: interface passes nonescapable type checking
Conditionally nonescapable types can contain nonescapable elements:
struct Container<NE>: ~Escapable {
var element: @dependsOn(self) NE
init(element: NE) -> @dependsOn(element) Self {...}
}
extension Container<E> {} // OK: conforms to Escapable.
Here, Container
becomes nonescapable only when its element type is nonescapable. This nonescapable Container
inherits the lifetime of its single element value from the initializer and propagates that lifetime to all uses of its element
property.
When a type conforms to Escapable
, any @dependsOn
annotation that modifies that type is ignored. So, when Container's element conforms to Escapable
, the @dependsOn(self) NE
annotation in Container's interface is ignored. And when Container
conforms to Escapable
, the @dependsOn(element) Self
in Container's interface is ignored.
Value lifetimes allow nonescapable values to pass through aggregates, retaining their original lifetime scope:
var c1: Container<NE>
{
let c2 = Container<NE>(element: c1.element)
c1.element = c2.element // OK: c2.element can outlive c2
}
Borrowing a value creates a new lifetime scope. The @dependsOn(borrow <name>)
annotation forces the named variable to be borrowed over the lifetime of the dependent value. Values that depends on a borrow are constrained by that borrow scope independent from their original lifetime:
struct Container<NE>: ~Escapable {
var storage: Storage
init(element: NE) -> @dependsOn(element) Self {...}
var view: @dependsOn(borrow self) View // New lifetime scope
}
struct View<NE>: ~Escapable, ~Copyable {
var ne: @dependsOn(self) NE { get {...} }
}
c1,c2 : Container<NE>
v1,v2 : View<NE>
var v1 = c1.view
{
let v2 = c2.view
v1.ne = v2.ne // ERROR: lifetime violation
}
Here, each view instance depends on a borrow of its container. Retrieving an element from one of the view's inherits the container's borrow scope. Hence, assigning an element from one view into another is a lifetime violation unless the destination view has a narrower lifetime than the source view.
This design can be extended through a natural progression of three additional features:
-
Value component lifetimes
-
Abstract lifetime components
-
Protocol lifetime requirements
In the current design, aggregating multiple values merges their scopes.
struct Container<NE>: ~Escapable {
var a: /*@dependsOn(self)*/ NE
var b: /*@dependsOn(self)*/ NE
init(a: NE, b: NE) -> @dependsOn(a, b) Self {...}
}
This can have the effect of narrowing the lifetime scope of some components:
var a = ...
{
let b = ...
let c = Container<NE>(a: a, b: b)
a = c.a // ERROR: `a` outlives `c.a`, which is constrained by the lifetime of `b`
}
In the future, the lifetimes of multiple values can be represented independently by attaching a @lifetime
attribute to a stored property and referring to that property's name inside @dependsOn
annotations:
struct Container<NE>: ~Escapable {
@lifetime
var a: /*@dependsOn(self.a)*/ NE
@lifetime
var b: /*@dependsOn(self.b)*/ NE
init(a: NE, b: NE) -> @dependsOn(a -> \.a, b -> \.b) Self {...}
}
The nesting level of a component is the inverse of the nesting level of its lifetime. a
and b
are nested components of Container
, but the lifetime of a Container
instance is nested within both lifetimes of a
and b
.
Lifetime dependence is not always neatly tied to stored properties. Say that our Container
now holds multiple elements within its own storage. We can use a top-level @lifetime
annotation to name an abstract lifetime for all the elements:
@lifetime(elements)
struct Container<NE>: ~Escapable {
var storage: UnsafeMutablePointer<NE>
init(element: NE) -> @dependsOn(element -> \.elements) Self {...}
subscript(position: Int) -> @dependsOn(self.elements) NE
}
Note that a subscript setter reverses the dependence: @dependsOn(newValue -> \.elements)
.
As before, when Container
held a single element, it can temporarily take ownership of an element without narrowing its lifetime:
var c1: Container<NE>
{
let c2 = Container<NE>(element: c1[i])
c1[i] = c2[i] // OK: c2[i] can outlive c2
}
Let's return to the example in which a view provides access to a borrowed container's elements. The lifetime of the view depends on the container's storage. Therefore, the view depends on a borrow of the container. The container's elements, however, no longer depend on the container's storage once they have been copied. This can be expressed by giving the view an abstract lifetime for its elements, separate from the view's own lifetime:
@lifetime(elements)
struct View<NE>: ~Escapable {
var storage: UnsafePointer<NE>
init(container: Container)
-> @dependsOn(container.elements -> \.elements) // Copy the lifetime assoicate with container.elements
Self {...}
subscript(position: Int) -> @dependsOn(self.elements) NE
}
@lifetime(elements)
struct MutableView<NE>: ~Escapable, ~Copyable {
var storage: UnsafeMutablePointer<NE>
//...
}
extension Container {
// Require a borrow scope in the caller that borrows the container
var view: @dependsOn(borrow self) View<NE> { get {...} }
var mutableView: @dependsOn(borrow self) MutableView<NE> { mutating get {...} }
}
Now an element can be copied out of a view v2
and assigned to another view v1
whose lifetime exceeds the borrow scope that constrains the lifetime of v2
.
var c1: Container<NE>
let v1 = c1.mutableView
{
let v2 = c1.view // borrow scope for `v2`
v1[i] = v2[i] // OK: v2[i] can outlive v2
}
To see this more abstractly, rather than directly assigning, v1[i] = v2[i]
, we can use a generic interface:
func transfer(from: NE, to: @dependsOn(from) inout NE) {
to = from
}
var c1: Container<NE>
let v1 = c1.mutableView
{
let v2 = c1.view // borrow scope for `v2`
transfer(from: v2[i], to: &v1[i]) // OK: v2[i] can outlive v2
}
Value lifetimes are limited because they provide no way to refer to a lifetime without refering to a concrete type that the lifetime is associated with. To support generic interfaces, protocols need to refer to any lifetime requirements that can appear in interface.
Imagine that we want to access view through a protocol. To support returning elements that outlive the view, we need to require an elements
lifetime requirement:
@lifetime(elements)
protocol ViewProtocol {
subscript(position: Int) -> @dependsOn(self.elements) NE
}
Let's return to View's initializer;
@lifetime(elements)
struct View<NE>: ~Escapable {
init(container: borrowing Container) ->
// Copy the lifetime assoicate with container.elements
@dependsOn(container.elements -> \.elements)
Self {...}
}
This is not a useful initializer, because View
should not be specific to a concrete Container
type. Instead, we want View
to be generic over any container that provides elements
that can be copied out of the container's storage:
@lifetime(elements)
protocol ElementStorage: ~Escapable {}
@lifetime(elements)
struct View<NE>: ~Escapable {
init(storage: ElementStorage) ->
// Copy the lifetime assoicate with storage.elements
@dependsOn(storage.elements -> \.elements)
Self {...}
}