Skip to content

Instantly share code, notes, and snippets.

@atrick
Last active April 25, 2024 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atrick/a41ca32367bc4b9fb1d54f9aece5c749 to your computer and use it in GitHub Desktop.
Save atrick/a41ca32367bc4b9fb1d54f9aece5c749 to your computer and use it in GitHub Desktop.
Indefinite lifetimes

Indefinite lifetimes

Immortal lifetimes

In some cases, a nonescapable value must be constructed without any object that can stand in as the source of a dependence. Consider extending the standard library Optional or Result types to be conditionally escapable:

enum Optional<Wrapped: ~Escapable>: ~Escapable {
  case none, some(Wrapped)
}

extension Optional: Escapable where Wrapped: Escapable {}

enum Result<Success: ~Escapable, Failure: Error>: ~Escapable {
  case failure(Failure), success(Success)
}

extension Result: Escapable where Success: Escapable {}

When constructing an Optional<NotEscapable>.none or Result<NotEscapable>.failure(error) case, there's no lifetime to assign to the constructed value in isolation, and it wouldn't necessarily need one for safety purposes, because the given instance of the value doesn't store any state with a lifetime dependency. Instead, the initializer for cases like this can be annotated with dependsOn(immortal):

extension Optional {
  init(nilLiteral: ()) dependsOn(immortal) {
    self = .none
  }
}

Once the escapable instance is constructed, it is limited in scope to the caller's function body since the caller only sees the static nonescapable type. If a dynamically escapable value needs to be returned further up the stack, that can be done by chaining multiple dependsOn(immortal) functions.

Depending on immutable global variables

Another place where immortal lifetimes might come up is with dependencies on global variables. When a value has a scoped dependency on a global let constant, that constant lives for the duration of the process and is effectively perpetually borrowed, so one could say that values dependent on such a constant have an effectively infinite lifetime as well. This will allow returning a value that depends on a global by declaring the function's return type with dependsOn(immortal):

let staticBuffer = ...

func getStaticallyAllocated() -> dependsOn(immortal) BufferReference {
  staticBuffer.bufferReference()
}

Depending on a BitwiseCopyable value

Creating a safe reference from an UnsafePointer

Occasionally, library authors need to derive a nonescapable value from a BitwiseCopyable value. This most often happens when converting an UnsafePointer into a safe reference type. One way to do this is by passing both the object that owns the storage and the pointer into that storage into the safe reference's initializer:

struct BufferReference<T>: ~Escapable {
  private var base: UnsafePointer<T>
  private var count: Int

  init<StorageOwner>(unsafeBaseAddress: UnsafePointer<T>, count: Int, dependingOn storageOwner: borrowing StorageOwner)
    -> dependsOn(storageOwner) Self { ... }
}

Passing the storage owner along with the pointer makes the original dependence more explicit, but sometimes the owner is unavailable or inconvenient to pass. The library author may instead declare a dependence directly on the UnsafePointer:

public struct BufferReference<T>: ~Escapable {
  ...
 
  // The client must ensure that `unsafeBaseAddress` is valid within entire scope of the caller.
  internal init(unsafeBaseAddress: UnsafePointer<Element>, count: Int) dependsOn(unsafeBaseAddress) { ... }
}

A BitwiseCopyable value has no ownership or lifetime, so the compiler has no way to enforce this dependence. This puts responsibility on the programmer to ensure that the calling function keeps the pointer valid over all uses of the nonescapable return value. This is usually done by calling a closure-taking API like withUnsafePointer or withUnsafeBufferPointer:

func decode(_ bufferRef: BufferReference<Int>) { /*...*/ }

extension UnsafeBufferPointer {
  // The client must ensure the lifetime of the buffer across the invocation of `body`.
  // The client must ensure that no code modifies the buffer during the invocation of `body`.
  func withUnsafeBufferReference<Result>(_ body: (BufferReference<Element>) throws -> Result) rethrows -> Result {
    // Construct BufferReference using its internal, unsafe API.
    try body(BufferReference(unsafePointer: baseAddress!, count: count))
  }
}

func decodeArrayAsUBP(array: [Int]) {
  array.withUnsafeBufferPointer { buffer in
    buffer.withUnsafeBufferReference {
      decode($0)
    }
  }
}

Interfaces that depend on BitwiseCopyable do not make good public APIs because of the responsibility placed on the calling function. As an alternative, Array could have been extended to avoid the responsibility of managing an UnsafeBufferPointer. Here, the compiler will enforce the dependence between BufferReference and the exclusive access scope of the array, identified by self:

extension Array {
  func withBufferReference<Result>(_ body: (BufferReference<Element>) throws -> Result) rethrows -> Result {
    try withUnsafeBufferPointer {
      return try body(BufferReference(unsafePointer: $0.baseAddress!, count: $0.count, dependsOn: self))
    }
  }
}

func decodeArray(array: [Int]) {
  array.withBufferReference {
    decode($0)
  }
}

BitwiseCopyable values do not have a scoped lifetime

When a nonescapable return value depends on a BitwiseCopyable source, that dependence is not enforced in the caller. A BitwiseCopyable value has no ownership or lifetime--the compiler is free to create temporary copies as needed and keep those temporary copies around as long as it likes. It follows that there's no way for the compiler to enforce a dependence on a BitwiseCopyable value, and ignoring that dependence is always valid. This might surprise some programmers:

func decodeBuffer() {
  var bufferRef: BufferReference?
  do {
    let (baseAddress, count) = ... 
    bufferRef = BufferReference(baseAddress: baseAddress, count: count)
  }
  decode(bufferRef!)
}

In this example, the programmer creates a nonescapable BufferReference that depends on baseAddress, but uses the bufferRef outside of the syntactic scope of the baseAddress variable. This is valid code as long as the pointer value that baseAddress contained when bufferRef was created is still valid when bufferRef is used.

BitwiseCopyable substitution into generics

Dependencies on BitwiseCopyable values may be ignored regardless of their static type because of type substitution:

func f1<T>(arg: borrowing T) -> dependsOn(scoped arg) NonescapableType

func f2() {
    let i: Int = ...
    // Programmer must ensure that the value in `i` is valid over all uses of `ne`.
    let ne = f1(arg: i)
    //...
    use(ne)
}

It follows that 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:

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

Reference Notes

Joe's Forum post on indefinite lifetimes

Forum post on BitwiseCopyable

Notse on the @_unsafeNonescapableResult function attribute

The @_unsafeNonescapableResult function attribute can be applied to any function that returns a nonescapable type. This attribute selectively disables lifetime dependence diagnostics on that function, allowing it to return a nonescapable value without a dependence on one of the function's arguments:

func f1(arg: borrowing ArgType) -> dependsOn(scoped arg) NonescapableType

@_unsafeNonescapableResult
func f2() -> NonescapableType { f1(ArgType()) }

func f3() {
  let nonescapableValue = f2()
  // ...
}

The nonescapable return value can be used within the body of the calling function, but it cannot otherwise escape or be returned up the call stack. This is identical to a return value declared as dependsOn(immortal). Unlike dependsOn(immortal), however, diagnostics on the return value are suppressed within the function's implementation. For example, f2 can return the nonescapable result of a call to f1 because f2 is annotated with @_unsafeNonescapableResult.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment