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 {...}
}