Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lifetime Dependency Annotations for Non-escapable Types #2305

Open
wants to merge 62 commits into
base: main
Choose a base branch
from
Open
Changes from 4 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
7e767dc
First full draft.
tbkka Jan 25, 2024
243e33b
Whitespace cleanup
tbkka Jan 25, 2024
b30308d
Fix formatting
tbkka Jan 25, 2024
ccc54d9
Future direction: Lifetime dependencies between arguments
tbkka Jan 26, 2024
2992389
Link to pitch thread
tbkka Feb 6, 2024
ba3946c
New examples for Future Directions
tbkka Feb 7, 2024
0aab19d
Match the implementation: Use local parameter names for argument ref…
tbkka Feb 7, 2024
f9b06b7
Clarify some Alternatives and Future Directions
tbkka Feb 8, 2024
84521ca
methods are `mutating`, not `inout`
tbkka Feb 12, 2024
3055bec
Expand alternative syntaxes
tbkka Feb 22, 2024
78d07f2
Overhaul to adopt the new `@dependsOn()` syntax
tbkka Mar 21, 2024
6ebcdb4
Corrections from meg-gupta
tbkka Mar 21, 2024
eed1b05
Overhaul semantics discussion
tbkka Mar 22, 2024
2e82c6b
Use "nonescapable" for the concept, rather than `~Escapable`
tbkka Mar 26, 2024
f648b02
Use "copied lifetime dependency" more consistently
tbkka Mar 26, 2024
a502076
s/@dependsOn/dependsOn/ to match implementation
tbkka Apr 12, 2024
40a78d9
Changed `StorageView` and `BufferReference` to `Span` to match the si…
atrick May 3, 2024
bf846dd
Merge pull request #1 from atrick/rename-span
tbkka May 7, 2024
25e4355
Add a link to the forum discussion.
atrick May 4, 2024
af162ef
Additions to Proposed solutions
atrick May 28, 2024
adfe8a0
Dependency semantics by example
atrick May 28, 2024
3b6f859
Cleanup Future directions
atrick May 28, 2024
2fd52e1
Additions to Future directions
atrick May 28, 2024
1c5a02e
Grammar updates
atrick May 28, 2024
c6ede5d
Merge pull request #2 from atrick/update
tbkka May 28, 2024
def2c82
Update and cleanup the grammar.
atrick May 29, 2024
d9aa90b
Merge pull request #3 from atrick/lifetime-dependency
tbkka May 31, 2024
992b2fe
Add a change log entry.
atrick Jun 5, 2024
aed8750
Syntax update: dependsOn(self).
atrick Jun 7, 2024
1859b98
Remove Future Direction: only limited to results
atrick Jun 7, 2024
6c80aca
Fix OwnedSpan ~Escapable typo.
atrick Jun 7, 2024
43dbc15
Tweak wording in the `ref1.drop` example.
atrick Jun 7, 2024
d8f4cb3
New section: Immortal requirements.
atrick Jun 9, 2024
5555ee6
New alternatives considered: Initializer syntax.
atrick Jun 9, 2024
eb1d88e
Update Future component syntax.
atrick Jun 8, 2024
a9ca58d
New alternative considered: dependsOn(unchecked)
atrick Jun 10, 2024
181050a
Remove redundant future direction: containers and their elements
atrick Jun 11, 2024
d7c1ac7
Move "Parameter index" section to Alternatives considered.
atrick Jun 11, 2024
b01cd1e
Typo in example from "Depending on an escapable"
atrick Jun 11, 2024
02fc95a
Rename Array to ContiguousArray.
atrick Jun 12, 2024
de45422
New example: Escapable properties in a nonescapable type
atrick Jun 12, 2024
5b0fd84
Changelog entries
atrick Jun 9, 2024
226d635
Merge pull request #4 from atrick/lifetime-dependency
tbkka Jun 12, 2024
9acaba8
Fix a grammar typo.
atrick Jun 14, 2024
51832e8
Fix a typo in the dependent parameters example.
atrick Jun 14, 2024
0ed7906
Typo: fix two more cases of Array -> ContiguousArray renaming.
atrick Jun 14, 2024
b6c51d8
Merge pull request #5 from atrick/lifetime-dependency
tbkka Jun 17, 2024
a969447
Update the free-standing function inference rules.
atrick Jun 20, 2024
25f30b3
Merge pull request #6 from atrick/lifetime-dependency
tbkka Jun 21, 2024
e1d364e
Added future directions: Function type syntax and closures
atrick Jun 21, 2024
e814e62
Merge pull request #7 from atrick/lifetime-dependency
tbkka Jun 25, 2024
9583bd7
Add an alternative spelling: `lifetime` vs. `dependsOn`.
atrick Jul 10, 2024
699d547
Merge pull request #8 from atrick/lifetime-dependency
tbkka Jul 11, 2024
f337c2f
Remove the Alternative: Initializer syntax: result vs. inout syntax
atrick Jul 31, 2024
3b6439c
Add Alternative: @lifetime annotation and where clause.
atrick Jul 31, 2024
ac43daa
Simplified implicit lifetime dependencies
atrick Aug 1, 2024
4152b2e
Merge pull request #9 from atrick/lifetime-dependency
tbkka Aug 13, 2024
5cd2fd3
Fix inference rules to be sequenced.
atrick Aug 13, 2024
0a4392f
In the lifetime annotation example, add an argument label.
atrick Aug 15, 2024
f16293c
Merge pull request #10 from atrick/lifetime-dependency
tbkka Aug 16, 2024
b841b47
Update Future Direction: Lifetime dependence for closures
atrick Aug 19, 2024
9ba7a57
Merge pull request #11 from atrick/lifetime-dependency
tbkka Aug 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 103 additions & 48 deletions proposals/NNNN-lifetime-dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ This is a key requirement for the `Span` type (previously called `BufferView`) b
**Edited** (June 9, 2024):

- New section: Immortal requirements
- New alternative considered: Initializer syntax
- New alternative considered: dependsOn(unchecked) to disable lifetime dependence checking
- Updated future direction: component lifetime syntax
- New example: Escapable properties in a nonescapable type

**Edited** (July 31, 2024)

- New alternative considered: @lifetime annotation
- New alternative considered: where clause
- Simplified implicit lifetime dependencies and added same-type rule

#### See Also

* [Forum discussion of Non-Escapable Types and Lifetime Dependency](https://forums.swift.org/t/pitch-non-escapable-types-and-lifetime-dependency)
Expand Down Expand Up @@ -253,12 +258,11 @@ init(arg: <parameter-convention> ArgType) -> dependsOn(arg) Self

### Implicit Lifetime Dependencies

The syntax above allows developers to explicitly annotate lifetime dependencies in their code.
But because the possibilities are limited, we can usually allow the compiler to infer a suitable dependency.
The detailed rules are below, but generally we require that the return type be nonescapable and that there be one “obvious” source for the dependency.
The syntax above allows developers to explicitly annotate lifetime dependencies in their code. But because the possibilities are limited, we can usually allow the compiler to infer a suitable dependency. The detailed rules are below, but generally we require that the return type be nonescapable and that there be an “obvious” source for the dependency.

In particular, we can infer a lifetime dependency on `self` for any method that returns a nonescapable value.
As above, the details vary depending on whether `self` is escapable or nonescapable:
#### Self dependence

We can infer a lifetime dependency on `self` for any method that returns a nonescapable value. As above, the details vary depending on whether `self` is escapable or nonescapable:

```swift
struct NonescapableType: ~Escapable { ... }
Expand All @@ -282,25 +286,21 @@ struct NEStruct: ~Escapable {
}
```

For free or static functions or initializers, we can infer a lifetime dependency when the return value is nonescapable and there is only one obvious argument that can serve as the source of the dependency.
For example:
#### Same-type dependence

```swift
struct NEType: ~Escapable { ... }
For any function or method that returns a nonescapable type, we infer a copied lifetime dependency on all parameters of the same type.

// If there is only one argument with an explicit parameter convention:
func f(..., arg1: borrowing Type1, ...) -> /* dependsOn(arg1) */ NEType
`func foo<T: ~Escapable, U: ~Escapable, R: ~Escapable>(x: T, y: U) -> R { ... }`

// Or there is only one argument that is `~Escapable`:
func g(..., arg2: NEType, ...) -> /* dependsOn(arg2) */ NEType
implies:

// If there are multiple possible arguments that we might depend
// on, we require an explicit dependency:
// 🛑 Cannot infer lifetime dependency since `arg1` and `arg2` are both candidates
func g(... arg1: borrowing Type1, arg2: NEType, ...) -> NEType
```
-> dependsOn(x) where R == T
-> dependsOn(y) where R == U
-> dependsOn(x, y) where R == T == U
```

We expect these implicit inferences to cover most cases, with the explicit form only occasionally being necessary in practice.
This is particularly helpful for Generic APIs. With this rule, indicating that a generic parameter is `~Escapable` should usually be sufficient to infer the correct lifetime dependence.

### Dependent parameters

Expand Down Expand Up @@ -668,20 +668,35 @@ The implications of mutation modifiers and argument type on the resulting lifeti

### Inference Rules

If there is no explicit lifetime dependency, we will automatically infer one according to the following rules:
If there is no explicit lifetime dependency on the nonescapable result of a method or function, we will attempt to infer dependencies automatically according the following rules:

**For methods where the return value is nonescapable**, we will infer a dependency against self, depending on the mutation type of the function.
Note that this is not affected by the presence, type, or modifier of any other arguments to the method.
1. For methods where the return value is nonescapable, we will infer a dependency against `self`. If `self` is nonescapable, then we infer a copying dependency. If `self` is escapable, and the method is `borrowing` or `mutating`, then we infer a scoped dependency.

**For a free or static functions or initializers with at least one argument,** we will infer a lifetime dependency when the return value is nonescapable and exactly one argument that satisfies any of the following:
- is nonescapable, or
- is non-BitwiseCopyable and has an explicit `borrowing`, or `inout` convention
2. For methods, functions, and initializers where the return value is nonescapable, we infer a copied lifetime dependency on all parameters of the same (nonescapable) type, including the implicit `self` parameter.

In this case, the compiler will infer a dependency on the unique argument identified by these conditions.
3. For functions and initializers that have a nonescapable return value and a single parameter, we infer dependence on that parameter. If the parameter is nonescapable, then we infer a copying dependency; otherwise, we infer a scoped dependency.

For all inference rules, the type of dependence is the same as an explicit `dependsOn(argument)` on the same argument without any `scoped` qualifier based on the argument's type.

**In no other case** will a function, method, or initializer implicitly gain a lifetime dependency.
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.

We infer dependencies according to all applicable rules. Here, both rule #1 and #2 apply:

```
struct NE: ~Escapable { ... }
struct E {
func foo(ne: NE) -> /* dependsOn(self, ne) */ NE
}
```

Here, both rule #2 and #3 apply:

```
struct NE {
init(ne: NE) -> /* dependsOn(ne) */ Self
}
```

### Dependency semantics by example

Expand Down Expand Up @@ -851,27 +866,6 @@ Removing a lifetime dependency constraint only affects existing source code in t

## Alternatives considered

### Initializer syntax: result vs. inout syntax

The programming model for initializers is that they return `self` (with an implicit return statement):

`init(arg: ArgType) -> dependsOn(arg) Self`

But some people have criticized this syntax. They prefer to think of an initializer as mutating `self`, which would be
spelled:

`dependsOn(self: arg) init(arg: ArgType)`

We could adopt either or both of these options.

In a future with component lifetimes the syntax would look like either:

`init(arg1: Element, arg2: Element) -> dependsOn(a: arg1, b: arg2) Self {...}`

or

`dependsOn(self.a: arg1, self.b: arg2) init(arg1: Element, arg2: Element) -> Self {...}`

### Different Position

We propose above putting the annotation on the return value, which we believe matches the intuition that the method or property is producing this lifetime dependence alongside the returned value.
Expand Down Expand Up @@ -900,14 +894,75 @@ The currently proposed `dependsOn` spelling was chosen to convey the direction o

func foo(a: A, b: B) -> dependsOn(a) R

This does, however, introduce compound keyword. Alternatively, we could use a simpler `lifetime` keyword, which better matches the feature description. The general syntax would then be:
This does, however, introduce a keyword with a compound name. Alternatively, we could use a simpler `lifetime` keyword, which better matches the feature description. The general syntax would then be:

> **lifetime**(*target*: [scoped] *source*)

APIs with ambiguous depenencies would then typically be spelled:

func foo(a: A, b: B) -> lifetime(a) R

### @lifetime annotation

Instead of committing to a final, lightweight syntax, we can start with a single `@lifetime` annotation. It would take this form:

```
@lifetime(target1.component: [copy|mutate|borrow] source1.component)
@lifetime(target2.component: [copy|mutate|borrow] source2.component)
func foo(...)
```

`target` can be `self`, any parameter name, or, most commonly an empty string which implies the function result. `source` can be `self` or any parameter name. The most common usage would be:

```
@lifetime(copy arg)
func foo(arg: Arg1) -> R {}
```

The `.component` qualifier is only relevant once we have component lifetimes. See the "Component lifetime" section below.

An annotation has some advantages over a lighter-weight type modifier sytax:

The `@` sigil is helpful to distinguish lifetime dependence information from regular function syntax.

A position-independent annotation has an advantage that the fully expressive syntax is more self-evident. This makes it easier to educate reviewers about what is possible with the syntax.

The type modifier can occur in any type position within a function signature, in including before the `func` keyword for the 'self' type. This has potential readability problems when it comes to more complicated cases. Nested parentheses (`dependsOn(...)`) that can occur anywhere in the signature are visually confusing.

In the future, the single `@lifetime` annotation could be a useful modifier for other kinds declarations such as types and properties:

```
// Allow two components to have distinct lifetimes...
struct Pair<T: ~Escapable> {
@lifetime
var x: T

@lifetime
var y: T
}

// Allow two components to have dependent lifetimes...
struct Node: ~Escapable {
@lifetime
var parent: Node

@lifetime(parent)
var child: Node
}

// Declare an abstract lifetime and alias it with another lifetime.
@lifetime(elements: storage.elements)
struct Container {
var storage: Storage
}
```

### `where` clause

Some have advocated for a `where` clause on the function declaration. The function name could stand-in for its result, and directionality could be indicated with a comparison operator:

`func foo(arg: Arg) -> R where lifetime(foo) < lifetime([copy|borrow|mutate] arg)`

### dependsOn(unchecked) to disable lifetime dependence checking

A `dependsOn(unchecked)` annotation could allow programmers to disable lifetime dependence checking for a function result or argument. For example, the programmer may want to compose a nonescapable result from an immortal value that isn't visible to the compiler:
Expand Down