This document proposes a "requires-escapable" rule. This is a short way of saying that a dependency source can only be Escapable by requiring the dependency target to be Escapable under the same conditions. This isn't a new idea. It was informally proposed alongside the initial lifetime implementation, but the implementation was deferred because of engineering bandwidth. LifetimeAnnotation.md currently refers to it as the "same-type" rule until the implementation is complete. We want to include this in the SE proposal that we pitch in Jan 2026 month for source compatibility reasons.
The requires-escapable rule only affects the legality of function types. It does not change lifetime enforcement in the function body. It simply extends the existing lifetime rules to support conditional conformances. The lifetime rules have always prohibited copying a lifetime from an unconditionally Escapable type. Here's an example where we prohibit dependency from an unconditionally Escapable type:
struct S: Escapable {}
struct NE: ~Escapable {}
@_lifetime(copy s) // ERROR: dependency on Escapable type `S`
func foo(s: S) -> NE
The require-escapable rules extends this rule to handle conditional conformances.
Given
@_lifetime(copy a)
func foo<...>(..., a: A, ...) -> R
The lifetime annotation is valid iff:
A: Escapable requires R: Escapable
Without this rule, the compiler cannot statically know whether R can depend on A inside a generic context. The following examples show how theis rule affects diagnostics:
- ERROR: unrelated type parameter
@_lifetime(copy t) // ERROR
func foo<T: ~Escapable, U: ~Escapable>(t: T) -> U
- OK: same type parameter
@_lifetime(copy t) // OK
func foo<T: ~Escapable>(t: T) -> T
- OK: conditionally non-escapable nominal types
struct NE1<T: ~Escapable>: ~Escapable {
var t: T
}
extension NE1: Escapable where T: Escapable {}
@_lifetime(copy ne) // OK
func foo<T: ~Escapable>(ne: NE1<T>) -> T
- OK: conditionally non-escapable nominal types with related type parameters
struct NE2<T: ~Escapable>: ~Escapable {
var t: T
}
extension NE2: Escapable where T: Escapable {}
@_lifetime(copy ne) // OK
func foo<T: ~Escapable>(ne: NE1<NE1<T>>) -> NE2<T>
We need to avoid accidentally immortal values of non-Escapable type, such as:
protocol HasPointer: ~Escapable {
var p: UnsafeMutableRawPointer { get }
}
class Storage: HasPointer /* Escapable */ {
let p: UnsafeMutableRawPointer
init() {
p = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 0)
}
deinit {
p.deallocate()
}
}
struct AccidentalImmortal<T: ~Escapable & HasPointer>: ~Escapable {
var p: UnsafeMutableRawPointer
@_lifetime(copy t)
init(t: T) {
self.p = t.p
}
}
func foo() {
let storage = Storage()
// @lifetime(copy t) is ignored because 't' is Escapable...
let view = AccidentalImmortal(t: storage)
_ = consume storage
view.p[0].storeBytes(...) // Oops
}
People usually see this and say: "just implicitly borrow the argument in the caller". That doesn't make sense when the caller passes a generic type or when the result is conditionally Escapable. For example:
struct NEPair<T: ~Escapable, U: ~Escapable>: ~Escapable {
var t: T
var u: U
}
extension NEPair: Escapable where T: Escapable, U: Escapable {}
@_lifetime(copy t)
func foo<T: ~Escapable, U: Escapable>(t: T, u: U) -> NEPair<T, U> {
NEPair(t: t, u: u) // ERROR if we borrow `u` here
}
Similarly, the requires-Escapable rule above prevents simple composition of non-Escapable values.
Trest an unconditionally Escapable type as if it has an implicit lifetime variable.
// Currently implicit lifetime variable: @_lifetime(self: Self)
// Ideally (once lifetime variables are supported): @_lifetime(storage: T)
struct AlwaysNonescapable<T: ~Escapable & HasPointer>: ~Escapable {
var p: UnsafeMutableRawPointer
@_lifetime(copy t)
init(t: T) {
self.p = t.p
}
}
func foo<T: ~Escapable>(storage: T) {
_ = AlwaysNonescapable(t: storage) // Error: 't' has no known lifetime variable
}
As opposed to:
// No implicit @lifetime
struct ConditionallyEscapable<T: ~Escapable & HasPointer>: ~Escapable {
var p: UnsafeMutableRawPointer
@_lifetime(copy t)
init(t: T) {
self.p = t.p
}
}
extension ConditionallyEscapable: Escapable where T: Escapable {}
func foo<T: ~Escapable>(storage: T) {
_ = ConditionallyEscapable(t: storage) // OK: immortal when 'T' is Escapable
}
Consequently, certain generic programming patterns will be impossible until we add lifetime requirements to protocols.
See (GIST: Andy; Lifetime Requirements)[https://gist.github.com/atrick/7cab4821872d40fdbb32352d47b105a0a]