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 1 commit
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
Prev Previous commit
Next Next commit
Overhaul semantics discussion
We need to separate description of the dependency itself --
which places a bound on when particular objects can be
destroyed -- from the syntax.
In particular, the syntax specifies a relationship between
two objects, but that relationship is not always a
lifetime dependency itself (because of "copied" dependencies).
  • Loading branch information
tbkka committed Mar 22, 2024
commit eed1b0576d17f19db2f91e38df1e31bda2bb48e0
119 changes: 69 additions & 50 deletions proposals/NNNN-lifetime-dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ struct EscStruct {
borrowing func f2(...) -> /* @dependsOn(self) */ NonescapableType
mutating func f3(...) -> /* @dependsOn(self) */ NonescapableType

// Note: no lifetime dependency is inferred for a consuming method
// on an `Escapable` type, since there is no legal option
// 🛑 Error: there is no valid lifetime dependency for
// a consuming method on an `Escapable` type
consuming func f4(...) -> NonescapableType
}

Expand Down Expand Up @@ -275,46 +275,101 @@ We expect these implicit inferences to cover most cases, with the explicit form

## Detailed design

### Relation to ~Escapable

The lifetime dependencies described in this document can be applied only to `~Escapable` return values.
Further, any return value that is `~Escapable` must have a lifetime dependency.
In particular, this implies that the initializer for a non-escapable type must have at least one argument.

```swift
struct S: ~Escapable {
init() {} // 🛑 Error: ~Escapable return type must have lifetime dependency
}
```

### Basic Semantics

A lifetime dependency annotation creates a *lifetime dependency* between a *dependent value* and a *source value*.
This relationship obeys the following requirements:

* The dependent value must be nonescapable.

* The dependent value's lifetime must not be longer than that of the source value.

* The dependent value is treated as an ongoing access to the source value.
Following Swift's usual exclusivity rules, the source value may not be mutated during the lifetime of the dependent value;
if the access is a mutating access, the source value is further prohibited from being accessed at all during the lifetime of the dependent value.

The compiler must issue a diagnostic if any of the above cannot be satisfied.

### Grammar

This new syntax adds an optional lifetime modifier just before the return type.
This modifies *function-result* in the Swift grammar as follows:

>
> *function-signature* → *parameter-clause* **`async`***?* **`throws`***?* *function-result**?* \
> *function-signature* → *parameter-clause* **`async`***?* **`rethrows`** *function-result**?*
> *function-result* → **`->`** *attributes?* *lifetime-modifiers?* *type*
> *lifetime-modifiers* **`->`** *lifetime-modifier* *lifetime-modifiers?*
> *lifetime-modifier* **`->`** **`@dependsOn`** **`(`** *lifetime-dependency* **`)`**
> *lifetime-dependency* **`->`** **`self`** | *local-parameter-name* | **`scoped self`** | **`scoped`** *local-parameter-name*
> *function-signature* → *parameter-clause* **`async`***?* **`rethrows`** *function-result**?* \
> *function-result* → **`->`** *attributes?* *lifetime-modifiers?* *type* \
> *lifetime-modifiers* **`->`** *lifetime-modifier* *lifetime-modifiers?* \
> *lifetime-modifier* **`->`** **`@dependsOn`** **`(`** *lifetime-dependent* **`)`** \
> *lifetime-dependent* → **`->`** **`self`** | *local-parameter-name* | **`scoped self`** | **`scoped`** *local-parameter-name*
>

Here, the *lifetime-dependency* argument to the lifetime modifier must be one of the following:
The *lifetime-dependent* argument to the lifetime modifier is one of the following:

* *local-parameter-name*: the local name of one of the function parameters, or
* the token **`self`**, or
* either of the above preceded by the **`scoped`** keyword

This modifier creates a lifetime dependency with the return value used as the dependent value.
The return value must be nonescapable.

The source value of the resulting dependency can vary.
In some cases, the source value will be the named parameter or `self` directly.
However, if the corresponding named parameter or `self` is nonescapable, then that value will itself have an existing lifetime dependency and thus the new dependency might "copy" the source of that existing dependency.

The following table summarizes the possibilities, which depend on the type and mutation modifier of the argument or `self` and the existence of the `scoped` keyword.
Here, "scoped" indicates that the dependent gains a direct lifetime dependency on the named parameter or `self` and "copied" indicates that the dependent gains a lifetime dependency on the source of an existing dependency:

| mutation modifier | argument type | without `scoped` | with `scoped` |
| ------------------ | ------------- | ---------------- | ------------- |
| borrowed | escapable | scoped | scoped |
| inout or mutating | escapable | scoped | scoped |
| consuming | escapable | Illegal | Illegal |
| borrowed | nonescapable | copied | scoped |
| inout or mutating | nonescapable | copied | scoped |
| consuming | nonescapable | copied | Illegal |

Two observations may help in understanding the table above:
* An escapable argument cannot have a pre-existing lifetime dependency, so copying is never possible in those cases.
* A consumed argument cannot be the source of a lifetime dependency that will outlive the function call, so only copying is legal in that case.

**Note**: In practice, the `scoped` modifier keyword is likely to be only rarely used. The rules above were designed to support the known use cases without requiring such a modifier.

#### Initializers

Initializers can have arguments, and there are cases where users will want to specify a lifetime dependency between one or more arguments and the constructed value.
We propose allowing initializers to write out an explicit return clause for this case.
Since nonescapable values cannot be returned without a lifetime dependency,
initializers for such types must specify a lifetime dependency on one or more arguments.
We propose allowing initializers to write out an explicit return clause for this case, which permits the use of the same syntax as functions or methods.
The return type must be exactly the token `Self` or the token sequence `Self?` in the case of a failable initializer:

```swift
struct S {
init(arg1: Type1) -> @dependsOn(arg1) Self
init?(arg2: Type2) -> @dependsOn(scoped arg2) Self?
init?(arg2: Type2) -> @dependsOn(arg2) Self?
}
```

> Grammar of an initializer declaration:
>
> *initializer-declaration* → *initializer-head* *generic-parameter-clause?* *parameter-clause* **`async`***?* **`throws`***?* *initializer-lifetime-modifier?* *generic-where-clause?* *initializer-body* \
> *initializer-declaration* → *initializer-head* *generic-parameter-clause?* *parameter-clause* **`async`***?* **`rethrows`** *initializer-lifetime-modifier?* *generic-where-clause?* *initializer-body*
> *initializer-declaration* → *initializer-head* *generic-parameter-clause?* *parameter-clause* **`async`***?* **`rethrows`** *initializer-lifetime-modifier?* *generic-where-clause?* *initializer-body* \
> *initializer-lifetime-modifier* → `**->**` *lifetime-modifiers* ** **`Self`** \
> *initializer-lifetime-modifier* → `**->**` *lifetime-modifiers* ** **`Self?`**

The implications of mutation modifiers and argument type on the resulting lifetime dependency exactly follow the rules above for functions and methods.

### Inference Rules

If there is no explicit lifetime dependency, we will automatically infer one according to the following rules:
Expand All @@ -326,55 +381,19 @@ Note that this is not affected by the presence, type, or modifier of any other a

* the return type is `~Escapable`,
* there is exactly one argument that satisfies any of the following:
** is either `~Copyable` or `~Escapable`
** is `Escapable` and `Copyable` and has an explicit `borrowing`, `consuming`, or `inout` convention specified
- is either `~Copyable` or `~Escapable`
- has an explicit `borrowing`, `consuming`, or `inout` convention specified

In this case, the compiler will infer a dependency on the unique argument identified by this last set of conditions.

**In no other case** will a function, method, or initializer implicitly gain a lifetime dependency.
If a function, method, or initializer has a `~Escapable` return type, 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.

### Semantics

The previous sections detail how lifetime dependency between the return value of a function or method and a function argument, method argument, or `self` can be explicitly declared or implicitly inferred by the compiler.

When the dependency involves a function, method, or initializer argument,
if the corresponding argument is `borrowing` or `inout` then we can refer to that argument as the *source* of the dependency, and the return value then has a *scoped lifetime dependency* on the source.
When this occurs, the compiler may shorten the lifetime of the return value or extend the lifetime of the source value within the existing language rules in order to satisfy the requirements below.
Further, the compiler will issue diagnostics if these requirements cannot be satisfied:

* The return value must be destroyed before the source value.
This can be obstructed if there are other factors (such as nested scopes, function returns, or closure captures) that contradict the lifetime dependency.
* For a borrowing argument, the source value cannot be mutated before the return value is destroyed.
* For an inout argument, the source value is accessed or mutated before the return value is destroyed.

The rules above apply with the obvious modifications for a method that explicitly or implicitly has a lifetime dependency between the return value and `self`.

If the `lifetime-kind` is `consume` or `copy`, then the return value from the function or method gains the same lifetime dependency as the function argument, method argument, or `self`.
In this case, we’ll refer to the argument or `self` as the *original* value.
In this case, the original value must itself must be `~Escapable`, and must in turn have a borrow or mutate lifetime dependency on some other source value.
The return value will then have a borrow or mutate lifetime dependency on that same source value that will be enforced by the compiler as above.

### Relation to ~Escapable

The lifetime dependencies described in this document can be applied only to `~Escapable` return values.
Further, any return value that is `~Escapable` must have a lifetime dependency.
In particular, this implies that the initializer for a non-escapable type must have at least one argument.

```swift
struct S: ~Escapable {
init() {} // 🛑 Error: ~Escapable return type must have lifetime dependency
}
```

## Source compatibility

Everything discussed here is additive to the existing Swift grammar and type system.
It has no effect on existing code.

The tokens `-> dependsOn` in a function declaration might indicate the beginning of a borrowing lifetime annotation or could indicate that the function returns an existing type called `dependsOn`.
This ambiguity can be fully resolved in the parser by looking for an open parenthesis `(` after the `dependsOn` token.

## Effect on ABI stability

Lifetime dependency annotations may affect how values are passed into functions, and thus adding or removing one of these annotations should generally be expected to affect the ABI.
Expand Down