[Pitch] Non-Escapable Types and Lifetime Dependency

As review manager for these proposals, I've been reading through the discussion and review revision history to understand the discussion that has happened so far. I wanted to raise a few points for community discussion while we get this ready for review:

Syntax revision

It appears that most of the discussion in this thread discussed a syntax model for lifetime dependencies using borrow(x), mutate(x), copy(y), consume(y), with the former two indicating a scoped dependency on an access of x, and the latter two indicating a dependency copied from an existing nonescaping value y. The current proposal has been revised to use a simpler-looking model where one syntax dependsOn(x) "does what you mean", forming a scoped dependency on an access of x when x is escapable or a borrowing/inout parameter, or copying the dependency from x if it's a nonescaping consuming or copyable value. dependsOn(scoped x) can be used to explicitly form a scoped dependency. Speaking personally, this appears to be an improvement, but it doesn't appear that this revision was discussed yet in the pitch thread, so I wanted to prompt discussion about it.

Values of ~Escapable type with unlimited, indefinite, and/or unknown lifetime

The proposal only tangentially discusses values of ~Escapable type that don't have a lifetime dependency, mentioning in passing an @_unsafeNonescapableResult attribute without going into depth. I think this capability deserves a little more consideration, since from talking to some early adopters of this feature, it sounds like the need (or at least desire) for it comes up in a number of common scenarios. For one example, an enum may have some cases which are ~Escapable but also some cases that have Escapable-payload or no-payload case. If we extend the standard library Optional and Result types to allow for ~Escapable values, they would likely look like:

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

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

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.

Another place where indefinite lifetimes might come up arose in the pitch discussion thus far, where the question of what happens with dependencies on global variables arose. 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 would be analogous to the 'static lifetime in Rust.

Anecdotally, a few developers I've spoken to who've tried to early-adopt ~Escapable types have also wanted to be able to construct a value without a lifetime constraint as a layering strategy, where a primitive private unsafe initializer takes raw unsafe pointers as a building block for the safe public API, like:

public struct Ref<T: ~Copyable>: ~Escapable {
  private var address: UnsafePointer<T>

  private init(unsafePointer: UnsafePointer<T>) -> /*what lifetime?*/ Ref<T>

  public init(borrowing value: borrowing T) -> dependsOn(value) Ref<T> {
    self.init(unsafePointer: _someWayToGetALimitedScopePointer(to: value))
  }
}

This use case is perhaps less essential, since an arguably safer way to layer this is to have it so that even the primitive initializer takes a phantom parameter to specify the lifetime dependency, so that it's harder to create leaky unconstrained unsafe values:

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> {
    self.init(unsafePointer: _someWayToGetALimitedScopePointer(to: value), dependingOn: value)
  }
}

The proposal might encourage this approach more explicitly as a safer way of building up nonescaping reference types from primitives. However, the cases of enum payloads and global dependencies are perfectly safe and not uncommon situations where indefinite lifetimes would be useful.

Thank you to everyone who has contributed to the discussion thus far!

9 Likes