"Immortal dependence" seems more like a "non-dependence", i.e. dependsOn(nothing)
reads closer to the programming model than dependsOn(immortal)
.
let a: Array<Int>
let ref1 = a.span() // ref1 cannot outlive a
let ref2 = ref1.drop(4) // ref2 also cannot outlive a
After ref1.drop(4), the lifetime of ref2 does not depend on ref1. Rather, ref2 has inherited or copied ref1โs dependency on the lifetime of a.
I think it would be helpful to mention that ref1
is killed by the call to drop. That helps to explain why copied dependency is necessary and why a scoped dependency on a consuming
argument is illegal. (While I am familiar with the consuming
modifier, I did have to puzzle through this reasoning a little bit myself.)
I think it would also be helpful to mention why one would ever want scoped instead of copied lifetime.
init(arg: <parameter-convention> ArgType) -> dependsOn(arg) Self {
...
}
This syntax seems odd, as we syntactically assign to self
and never syntactically return self
(even if that's semantically equivalent). Would this work instead?
init(arg: <parameter-convention> ArgType) dependsOn(arg) {
...
}
func mayReassign(span: dependsOn(a) inout [Int], to a: [Int]) {
span = a.span()
}
Should the type of span
be [Int]
or Span<Int>
?
The new function argument dependence is additive, because the call does not guarantee reassignment. Instead, passing the 'inout' argument is like a conditional reassignment. After the function call, the dependent argument carries both lifetime dependencies.
It would be helpful to explain a little more what it means to carry both lifetime dependencies. I think the meaning is that the dependent cannot outlive either dependee, and from this perspective its lifetime is the intersection of the dependee's lifetimes. However, I think what would happen is that each dependee's lifetime would be extended if necessary (or possible) and thus the dependent's lifetime would be more like the union of each dependee's.
struct Container<Element>: ~Escapable {
Do you need to say Element: ~Escapable
, that is Element
may or may not be escapable?
extension Storage {
public func withUnsafeBufferPointer<R>(
_ body: (UnsafeBufferPointer<Element>) throws -> R
) rethrows -> R {
withExtendedLifetime (self) { ... }
}
}
let storage = Storage(...)
storage.withUnsafeBufferPointer { buffer in
let span = Span(unsafeBaseAddress: buffer.baseAddress!, count: buffer.count)
decode(span!) // โ
Safe: 'buffer' is always valid within the closure.
It is a little unfortunate that "unsafe" needs to appear in the argument label in this particular use, since this is safe for any code that follows Swift's strong precedent of ensuring closure pointer argument validity until the end of scope. That is, the Span
safely depends on the closure scope in which it is constructed (alternatively: the syntactic scope of the value passed in).
However, since this API precedent is not statically enforced, and this is specifically for adapter code between the previous unsafe world and the new safe world, I think the proposal is acceptable.
MemoryLayout
will suppress the escapable constraint on its generic parameter.
What about Custom(Debug)StringConvertible
, so that non-escapable types can pretty-print themselves to the console?
struct OwnedSpan<T>: ~Copyable & ~Escapable{
let owner: any ~Copyable
let span: dependsOn(scope owner) Span<T>
init(owner: consuming any ~Copyable, span: dependsOn(scope owner) Span<T>) -> dependsOn(scoped owner) Self {
self.owner = owner
self.span = span
}
}
func arrayToOwnedSpan<T>(a: consuming [T]) -> OwnedSpan<T> {
OwnedSpan(owner: a, span: a.span())
}
This is an interesting future direction. Wouldn't OwnedSpan
be escapable but non-copyable, because its dependent-lifetime member is coupled with the dependee and thus they can be moved around together?