Skip to content

Instantly share code, notes, and snippets.

@atrick
Last active November 5, 2024 19:50
Show Gist options
  • Save atrick/9409356c89a5f67dd9f68f708f57262e to your computer and use it in GitHub Desktop.
Save atrick/9409356c89a5f67dd9f68f708f57262e to your computer and use it in GitHub Desktop.
Property lifetimes

Property lifetimes

With ~Copyable and ~Escapable types, Swift properties can, in theory, be accessed in different ways that affect where the property value can be used. Here we describe four kinds of property access that each have different lifetime semantics. These lifetime semantics generalize to function results and coroutine yields; in this respect, there's no need to distinguish between "properties" and functions. Each kind of lifetime does, however, describe a relationship between the result and some outer value that the result is derived from. For this reason, property access is a helpful conceptual framing. This document suggests possible ways that the semantics could be expressed in the language. Our goal is not for programmers to think in terms of these four kinds of access lifetimes; natural usage of ~Copyable and ~Escapable types should lead to common-sense behavior. Instead, this formalization is meant to guide future Swift evolution proposals, which may propose some useful subsets of the features described here for incremental adoption.

Each section below describes one of the four kinds of property lifetimes; each increasingly restricts the context in which the property's value can be used. The semantics are demonstrated in terms of self-explanatory ContiguousArray.span and Span.subscript properties. Examples show explicit lifetime annotation for clarity, but lifetimes would easily be inferred. Each case includes a corresponding Rust declaration as a way to restate the semantics in terms of an established formal model.

1. Copied or Moved Properties

Return an owned value with no lifetime restrictions. Conceptually: copy or move the underlying value.

Swift 6.0: The property semantics for Copyable accessors using get.

  • Copyable getters can always be implemented as a _read accessors; the getter is trivially generated by copying the yielded value.

  • ~Copyable properties require consuming get; they cannot be implemented with _read.

Example:

class Inner {}
struct Outer: ~Copyable {
  let inner: Inner
}
 
// the copy of outer.inner is used after consuming outer
takeOuterAndInner(outer.inner, consume outer)

Swift ~Escapable: N/A -- requires Escapable

Rust: N/A

2. Dependent Properties

Return an owned value with a lifetime dependency. Conceptually: copy any underlying reference.

Swift 6.0: N/A

Swift ~Escapable:

  • Requires a parameter (self for true properties) that has an abstract lifetime associated with its type: @lifetime(param.lifetime).

  • Both the parameter type (Self) and property type may be conditionally escapable. Enforcement of lifetime dependencies in the property's implementation ensures that the parameter is nonescapable whenever the returned property is nonescapable. This implies that an unconditionally escapable property can never depend on a conditionally escapable Self.

  • Copyable getters can always be implemented as a _read accessors; the getter is trivially generated by copying the yielded value.

  • ~Copyable properties require consuming get; they cannot be implemented with _read.

struct Span<T>: ~Escapable {
  lifetime storage: Self

  @lifetime(self.storage)
  subscript(range) -> Span<T> { get { ... } }
}

let span2: Span<T>
do {
  let contiguousArray = ...
  do {
    let span1 = contiguousArray.span
    span2 = span1[range]
  }
  _ = span2[0] // OK
}
_ = span2[0] // ERROR: outside of 'contiguousArray' access scope

Rust:

Same-lifetime dependency on a non-reference:

impl<'a, T> Span<'a, T> {
    fn subscript(self: Span<'a, T>, _:Range<i32>) -> Span<'a, T>
}

// No need to take a reference to 'span1' here.
let span2 = span1.subscript(range)

3. Projected Properties

Return a borrowed value that depends on the current access to self. Conceptually: create a reference.

Swift 6.0: N/A

Swift ~Escapable:

  • Requires a borrowed parameter: @borrow(param)

  • Requires borrow or unsafeAddress; _read is illegal.

  • ~Copyable requires a new feature: "borrowed returns".

  • Allows unconditionally nonescapable properties to depend on an access of an escapable or conditionally escapable Self.

struct ContiguousArray<T> {
  @borrow(self)
  var span: Span<T> { borrow { ... } }
}

let span: Span<T>
do {
  let contiguousArray = ...
  span = contiguousArray.span
  _ = span[0] // OK
}
_ = span[0] // ERROR: outside of 'contiguousArray' access scope

Rust:

Lifetime dependence on a reference:

impl<T> ContiguousArray<T> {
    fn span<'a>(&'a self) -> Span<'a, T>
}

// Need to take a reference to 'contiguousArray' here.
let span = &contiguousArray.span

4. Yielded Properties

Return a borrowed value that depends on a new access to self. Conceptually: Create a locally scoped lifetime.

Swift 6.0:

  • Current property semantics for ~Copyable implemented with _read.

Swift ~Escapable:

  • Requires _read for ~Escapable
struct DiscontiguousArray<T> {
  var span: Span<T> { _read { ... } }
}

let discontiguousArray = ...
let span: Span<T>
do {
  span = discontiguousArray.span
  _ = span[0] // OK
}
_ = span[0] // ERROR: outside of 'discontiguousArray.span' access scope

Rust:

Higher-ranked lifetime: for <'b> fn span(&'a Array) -> Span<'b> where 'a: 'b

Summary

Access kind: Copied Dependent Projected Yielded
property syntax : R { get } : R { get } : R { borrow } : R { _read }1
mutable syntax : R { set } : R { set }2 : R { mutate } : R { _modify }1
function syntax -> R @lifetime(param.lifetime)34 @borrow(param)5+-> borrow R6 -> yield R
conceptually copy/move value copy reference create reference create access
ownership owned owned borrowed borrowed
scope access of param.lifetime access of param new access
Copyable OK OK Exclusive access to self auto-getter/setter
~Copyable consuming consuming Exclusive access to self Exclusive access to self
Escapable OK copying or consuming Exclusive access to self auto-getter/setter
~Escapable Invalid OK Exclusive access to self Exclusive access to self
Escapable self OK Requires Escapable property OK OK

Footnotes

  1. The current _read/_modify syntax is temporary. I suggest replacing it with yield and mutatable yield. 2

  2. set reverses the dependency. self must have a lifetime associated with the property type. self becomes dependent on the new value's with respect to that lifetime.

  3. The @lifetime(param) annotation will typically be inferred as a dependency on self or on a parameter of the same type as the result.

  4. In the syntax @lifetime(param.lifetime), the .lifetime component will be inferred ~Escapable types that have a lifetime associated with Self.

  5. The @borrow(param) annotation will typically be inferred as a dependency on self.

  6. The borrow return type modifier is only needed for ~Copyable properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment