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!