Skip to content

Commit af162ef

Browse files
committed
Additions to Proposed solutions
- Dependent parameters - Dependent properties - Conditional dependencies - Immortal lifetimes - Depending on immutable global variables - Depending on an escapable `BitwiseCopyable` value - Depending on an escapable `BitwiseCopyable` value
1 parent 25e4355 commit af162ef

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed

proposals/NNNN-lifetime-dependency.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,204 @@ func g(... arg1: borrowing Type1, arg2: NEType, ...) -> NEType
275275

276276
We expect these implicit inferences to cover most cases, with the explicit form only occasionally being necessary in practice.
277277

278+
### Dependent parameters
279+
280+
Normally, lifetime dependence is required when a nonescapable function result depends on an argument to that function. In some rare cases, however, a nonescapable function parameter may depend on another argument to that function. Consider a function with an `inout` parameter. The function body may reassign that parameter to a value that depends on another parameter. This is similar in principle to a result dependence.
281+
282+
```swift
283+
func mayReassign(span: dependsOn(a) inout [Int], to a: [Int]) {
284+
span = a.span()
285+
}
286+
```
287+
288+
A `selfDependsOn` keyword is required to indicate that a method's implicit `self` depends on another parameter.
289+
290+
```swift
291+
extension Span {
292+
mutating selfDependsOn(other) func reassign(other: Span<T>) {
293+
self = other // ✅ OK: 'self' depends on 'other'
294+
}
295+
}
296+
```
297+
298+
We've discussed how a nonescapable result must be destroyed before the source of its lifetime dependence. Similarly, a dependent argument must be destroyed before an argument that it depends on. The difference is that the dependent argument may already have a lifetime dependence when it enters the function. 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.
299+
300+
```swift
301+
let a1: Array<Int> = ...
302+
var span = a1.span()
303+
let a2: Array<Int> = ...
304+
mayReassign(span: &span, to: a2)
305+
// 'span' now depends on both 'a1' and 'a2'.
306+
```
307+
308+
### Dependent properties
309+
310+
Structural composition is an important use case for nonescapable types. Getting or setting a nonescapable property requires lifetime dependence, just like a function result or an 'inout' parameter. There's no need for explicit annotation in these cases, because only one dependence is possible. A getter returns a value that depends on `self`. A setter replaces the current dependence from `self` with a dependence on `newValue`.
311+
312+
```swift
313+
struct Container<Element>: ~Escapable {
314+
var element: Element {
315+
/* dependsOn(self) */ get { ... }
316+
/* selfDependsOn(newValue) */ set { ... }
317+
}
318+
319+
init(element: Element) /* -> dependsOn(element) Self */ {...}
320+
}
321+
```
322+
323+
### Conditional dependencies
324+
325+
Conditionally nonescapable types can contain nonescapable elements:
326+
327+
```swift
328+
struct Container<Element>: ~Escapable {
329+
var element: /* dependsOn(self) */ Element
330+
331+
init(element: Element) -> dependsOn(element) Self {...}
332+
333+
func getElement() -> dependsOn(self) Element { element }
334+
}
335+
336+
extension Container<E> { // OK: conforms to Escapable.
337+
// Escapable context...
338+
}
339+
```
340+
341+
Here, `Container` becomes nonescapable only when its element type is nonescapable. When `Container` is nonescapable, it inherits the lifetime of its single element value from the initializer and propagates that lifetime to all uses of its `element` property or the `getElement()` function.
342+
343+
In some contexts, however, `Container` and `Element` both conform to `Escapable`. In those contexts, any `dependsOn` in `Container`'s interface is ignored, whether explicitly annotated or implied. So, when `Container`'s element conforms to `Escapable`, the `-> dependsOn(element) Self` annotation in its initializer is ignored, and the `-> dependsOn(self) Element` in `getElement()` is ignored.
344+
345+
### Immortal lifetimes
346+
347+
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:
348+
349+
```swift
350+
enum Optional<Wrapped: ~Escapable>: ~Escapable {
351+
case none, some(Wrapped)
352+
}
353+
354+
extension Optional: Escapable where Wrapped: Escapable {}
355+
356+
enum Result<Success: ~Escapable, Failure: Error>: ~Escapable {
357+
case failure(Failure), success(Success)
358+
}
359+
360+
extension Result: Escapable where Success: Escapable {}
361+
```
362+
363+
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)`:
364+
365+
```swift
366+
extension Optional {
367+
init(nilLiteral: ()) dependsOn(immortal) {
368+
self = .none
369+
}
370+
}
371+
```
372+
373+
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.
374+
375+
#### Depending on immutable global variables
376+
377+
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)`:
378+
379+
```swift
380+
let staticBuffer = ...
381+
382+
func getStaticallyAllocated() -> dependsOn(immortal) BufferReference {
383+
staticBuffer.bufferReference()
384+
}
385+
```
386+
387+
### Depending on an escapable `BitwiseCopyable` value
388+
389+
The source of a lifetime depenence may be an escapable `BitwiseCopyable` value. This is useful in the implementation of data types that internally use `UnsafePointer`:
390+
391+
```swift
392+
struct Span<T>: ~Escapable {
393+
...
394+
// The caller must ensure that `unsafeBaseAddress` is valid over all uses of the result.
395+
init(unsafeBaseAddress: UnsafePointer<T>, count: Int) dependsOn(unsafeBaseAddress) { ... }
396+
...
397+
}
398+
```
399+
400+
By convention, when the source of a dependence is escapable and `BitwiseCopyable`, it should have an "unsafe" label, such as `unsafeBaseAddress` above. This communicates to anyone who calls the function, that they are reponsibile for ensuring that the value that the result depends on is valid over all uses of the result. The compiler can't guarantee safety because `BitwiseCopyable` types do not have a formal point at which the value is destroyed. Specifically, for `UnsafePointer`, the compiler does not know which object owns the pointed-to storage.
401+
402+
```swift
403+
var span: Span<T>?
404+
let buffer: UnsafeBufferPointer<T>
405+
do {
406+
let storage = Storage(...)
407+
buffer = storage.buffer
408+
span = Span(unsafeBaseAddress: buffer.baseAddress!, count: buffer.count)
409+
// 🔥 'storage' may be destroyed
410+
}
411+
decode(span!) // 👿 Undefined behavior: dangling pointer
412+
```
413+
414+
Normally, `UnsafePointer` lifetime guarantees naturally fall out of closure-taking APIs that use `withExtendedLifetime`:
415+
416+
```swift
417+
extension Storage {
418+
public func withUnsafeBufferPointer<R>(
419+
_ body: (UnsafeBufferPointer<Element>) throws -> R
420+
) rethrows -> R {
421+
withExtendedLifetime (self) { ... }
422+
}
423+
}
424+
425+
let storage = Storage(...)
426+
storage.withUnsafeBufferPointer { buffer in
427+
let span = Span(unsafeBaseAddress: buffer.baseAddress!, count: buffer.count)
428+
decode(span!) // ✅ Safe: 'buffer' is always valid within the closure.
429+
}
430+
```
431+
432+
### Standard library extensions
433+
434+
#### Conditionally nonescapable types
435+
436+
The following standard library types will become conditionally nonescapable: `Optional`, `ExpressibleByNilLiteral`, and `Result`.
437+
438+
`MemoryLayout` will suppress the escapable constraint on its generic parameter.
439+
440+
#### `unsafeLifetime` helper functions
441+
442+
The following two helper functions will be added for implementing low-level data types:
443+
444+
```swift
445+
/// Replace the current lifetime dependency of `dependent` with a new copied lifetime dependency on `source`.
446+
///
447+
/// Precondition: `dependent` has an independent copy of the dependent state captured by `source`.
448+
func unsafeLifetime<T: ~Copyable & ~Escapable, U: ~Copyable & ~Escapable>(
449+
dependent: consuming T, dependsOn source: borrowing U)
450+
-> dependsOn(source) T { ... }
451+
452+
/// Replace the current lifetime dependency of `dependent` with a new scoped lifetime dependency on `source`.
453+
///
454+
/// Precondition: `dependent` depends on state that remains valid until either:
455+
/// (a) `source` is either destroyed if it is immutable,
456+
/// or (b) exclusive to `source` access ends if it is a mutable variable.
457+
func unsafeLifetime<T: ~Copyable & ~Escapable, U: ~Copyable & ~Escapable>(
458+
dependent: consuming T, scoped source: borrowing U)
459+
-> dependsOn(scoped source) T {...}
460+
```
461+
462+
These are useful for nonescapable data types that are internally represented using escapable types such as `UnsafePointer`. For example, some methods on `Span` will need to derive a new `Span` object that copies the lifetime dependence of `self`:
463+
464+
```swift
465+
extension Span {
466+
consuming func dropFirst() -> Span<T> {
467+
let local = Span(base: self.base + 1, count: self.count - 1)
468+
// 'local' can persist after 'self' is destroyed.
469+
return unsafeLifetime(dependent: local, dependsOn: self)
470+
}
471+
}
472+
```
473+
474+
Since `self.base` is an escapable value, it does not propagate the lifetime dependence of its container. Without the call to `unsafeLifetime`, `local` would be limited to the local scope of the value retrieved from `self.base`, and could not be returned from the method. In this example, `unsafeLifetime` communicates that all of the dependent state from `self` has been *copied* into `local`, and, therefore, `local` can persist after `self` is destroyed.
475+
278476
## Detailed design
279477

280478
### Relation to ~Escapable

0 commit comments

Comments
 (0)