Skip to content

Commit adfe8a0

Browse files
committed
Dependency semantics by example
1 parent af162ef commit adfe8a0

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed

proposals/NNNN-lifetime-dependency.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,136 @@ In this case, the compiler will infer a dependency on the unique argument identi
589589
**In no other case** will a function, method, or initializer implicitly gain a lifetime dependency.
590590
If a function, method, or initializer has a nonescapable return value, does not have an explicit lifetime dependency annotation, and does not fall into one of the cases above, then that will be a compile-time error.
591591

592+
593+
### Dependency semantics by example
594+
595+
This section illustrates the semantics of lifetime dependence one example at a time for each interesting variation. The following helper functions will be useful: `Array.span()` creates a scoped dependence to a nonescapable `Span` result, `copySpan()` creates a copied dependence to a `Span` result, and `parse` uses a `Span`.
596+
597+
```swift
598+
extension Array {
599+
// The returned span depends on the scope of Self.
600+
borrowing func span() -> /* dependsOn(scoped self) */ Span<Element> { ... }
601+
}
602+
603+
// The returned span copies dependencies from 'arg'.
604+
func copySpan<T>(_ arg: Span<T>) -> /* dependsOn(arg) */ Span<T> { arg }
605+
606+
func parse(_ span: Span<Int>) { ... }
607+
```
608+
609+
#### Scoped dependence on an immutable variable
610+
611+
```swift
612+
let a: Array<Int> = ...
613+
let span: Span<Int>
614+
do {
615+
let a2 = a
616+
span = a2.span()
617+
}
618+
parse(span) // 🛑 Error: 'span' escapes the scope of 'a2'
619+
```
620+
621+
The call to `span()` creates a scoped dependence on `a2`. A scoped dependence is determined by the lifetime of the variable, not the lifetime of the value assigned to that variable. So the lifetime of `span` cannot extend into the larger lifetime of `a`.
622+
623+
#### Copied dependence on an immutable variable
624+
625+
Let's contrast scoped dependence shown above with copied dependence on a variable. In this case, the value may outlive the variable it is copied from, as long as it is destroyed before the root of its inherited dependence goes out of scope. A chain of copied dependencies is always rooted in a scoped dependence.
626+
627+
An assignment that copies or moves a nonescapable value from one variable into another **copies** any lifetime dependence from the source value to the destination value. Thus, variable assignment has the same lifetime copy semantics as passing an argument using a `dependsOn()` annotation *without* a `scoped` keyword. So, the statement `let temp = span` has identical semantics to `let temp = copySpan(span)`.
628+
629+
```swift
630+
let a: Array<Int> = arg
631+
let final: Span<Int>
632+
do {
633+
let span = a.span()
634+
let temp = span
635+
final = copySpan(temp)
636+
}
637+
parse(final) // ✅ Safe: still within lifetime of 'a'
638+
```
639+
640+
Although the result of `copySpan` depends on `temp`, the result of the copy may be used outside of the `temp`'s lexical scope. Following the source of each copied dependence, up through the call chain if needed, eventually leads to the scoped dependence root. Here, `final` is the end of a lifetime dependence chain rooted at a scoped dependence on `a`:
641+
`a -> span -> temp -> {copySpan argument} -> final`. `final` is therefore valid within the scope of `a` even if the intermediate copies have been destroyed.
642+
643+
#### Copied dependence on a mutable value
644+
645+
First, let's add a mutable method to `Span`:
646+
647+
```swift
648+
extension Span {
649+
mutating func droppingPrefix(length: Int) -> /* dependsOn(self) */ Span<T> {
650+
let result = Span(base: base, count: length)
651+
self.base += length
652+
self.count -= length
653+
return result
654+
}
655+
}
656+
```
657+
658+
A dependence may be copied from a mutable ('inout') variable. In that case, the dependence is inherited from whatever value the mutable variable holds when it is accessed.
659+
660+
```swift
661+
let a: Array<Int> = ...
662+
var prefix: Span<Int>
663+
do {
664+
var temp = a.span()
665+
prefix = temp.droppingPrefix(length: 1) // access 'temp' as 'inout'
666+
// 'prefix' depends on 'a', not 'temp'
667+
}
668+
parse(prefix) // ✅ Safe: still within lifetime of 'a'
669+
```
670+
671+
#### Scoped dependence on 'inout' access
672+
673+
Now, let's return to scoped dependence, this time on a mutable variable. This is where exclusivity guarantees come into play. A scoped depenendence extends an access of the mutable variable across all uses of the dependent value. If the variable mutates again before the last use of the dependent, then it is an exclusivity violation.
674+
675+
```swift
676+
let a: Array<Int> = ...
677+
a[i] = ...
678+
let span = a1.span()
679+
parse(span) // ✅ Safe: still within 'span's access on 'a'
680+
a[i] = ...
681+
parse(span) // 🛑 Error: simultaneous access of 'a'
682+
```
683+
684+
Here, `a1.span()` initiates a 'read' access on `a1`. The first call to `parse(span)` safely extends that read access. The read cannot extend to the second call because a mutation of `a1` occurs before it.
685+
686+
#### Dependence reassignment
687+
688+
We've described how a mutable variable can be the source of a lifetime dependence. Now let's look at nonescapable mutable variables. Being nonescapable means they depend on another lifetime. Being mutable means that dependence may change during reassignment. Reassigning a nonescapable 'inout' sets its lifetime dependence from that point on, up to either the end of the variable's lifetime or its next subsequent reassignment.
689+
690+
```swift
691+
func reassign(_ span: inout Span<Int>) {
692+
let a: Array<Int> = ...
693+
span = a.span() // 🛑 Error: 'span' escapes the scope of 'a'
694+
}
695+
```
696+
697+
#### Reassignment with argument dependence
698+
699+
If a function takes a nonescapable 'inout' argument, it may only reassign that argument if it is marked dependent on another function argument that provies the source of the dependence.
700+
701+
```swift
702+
func reassignWithArgDependence(_ span: dependsOn(arg) inout [Int], _ arg: [Int]) {
703+
span = arg.span() // ✅ OK: 'span' already depends on 'arg' in the caller's scope.
704+
}
705+
```
706+
707+
#### Conditional reassignment creates conjoined dependence
708+
709+
'inout' argument dependence behaves like a conditional reassignment. After the call, the variable passed to the 'inout' argument has both its original dependence along with a new dependence on the argument that is the source of the argument dependence.
710+
711+
```swift
712+
let a1: Array<Int> = arg
713+
do {
714+
let a2: Array<Int> = arg
715+
var span = a1.span()
716+
testReassignArgDependence(&span, a2) // creates a conjoined dependence
717+
parse(span) // ✅ OK: within the lifetime of 'a1' & 'a2'
718+
}
719+
parse(span) // 🛑 Error: 'span' escapes the scope of 'a2'
720+
```
721+
592722
## Source compatibility
593723

594724
Everything discussed here is additive to the existing Swift grammar and type system.

0 commit comments

Comments
 (0)