[Pitch] Non-Escapable Types and Lifetime Dependency

Another aspect of Compile-time Lifetime Dependency Annotations that adopters tend to encounter early on, but isn't directly confronted in the proposal, is their interaction with
BitwiseCopyable. BitwiseCopyable values have neither ownership nor lifetimes. The compiler is free to create temporary copies as needed and keep those temporary copies around as long as it likes--it has no observable effect on the program. It follows that a lifetime dependence on a BitwiseCopyable value has no meaning. The compiler has no way to enforce such a dependence, and ignoring the dependence is always valid. To avoid giving programers the wrong impression, the compiler issues an error in code that asks for lifetime dependence on a BitwiseCopyable type:

func f(arg: Int) -> dependsOn(scoped arg) NonEscapableType // ERROR: invalid lifetime dependence on BitwiseCopyable type

The programmer has asked to "borrow" an integer over the lifetime of NonEscapableType, which is meaningless because an integer is nothing more than its bitwise representation.

The interaction between BitwiseCopyable and conditionally escapable types leads to conditionally ignored dependence annotations. Conditionally escapable types are introduced in Non-Escapable Types:

struct Box<T: ~Escapable>: ~Escapable {
  var t: T
}
 
// Box gains the ability to escape whenever its
// generic argument is Escapable
extension Box: Escapable where T: Escapable { }

Returning an escapable or conditionally escapable type requires lifetime dependence:

func transfer<T: ~Escapable>(arg: Box<T>) -> dependsOn(arg) Box<T> // 'dependsOn' may be inferred

The compiler ignores this lifetime dependence when it applies to an escapable type like Int:

func escapingInt() -> Box<Int> {
  let box = Box(t: 3)
  return transfer(arg: box) // OK: Box<Int> is escapable
}

Adopters do often need to prevent BitwiseCopyable values from escaping. This requires wrapping the BitwiseCopyable value in unconditionally ~Escapable types. @Joe_Groff shows how to do that in Values of ~Escapable type with unlimited, indefinite, and/or unknown lifetime, where UnsafePointer is BitwiseCopyable.

public struct Ref<T: ~Copyable>: ~Escapable {
  private var address: UnsafePointer<T>
 
  private init<X>(unsafePointer: UnsafePointer<T>, dependingOn dummy: borrowing X) -> dependsOn(dummy) Ref<T> { ... }
 
  public init(borrowing value: borrowing T) -> dependsOn(value) Ref<T> {
    // This initializer call is only safe if the programmer passes an `unsafePointer` that correctly
    // depends on the lifetime of `value`.
    self.init(unsafePointer: _someWayToGetALimitedScopePointer(to: value), dependingOn: value)
  }
}

Creating a dependence on a BitwiseCopyable value is inherently unsafe. But this approach limits unsafety to a single point at which the programmer declares that the BitwiseCopyable value (unsfePointer) depends on value. We could consider formalizing this step with a standard "DependentValue" wrapper.

1 Like