Skip to content

Instantly share code, notes, and snippets.

@eeckstein
Last active January 9, 2023 09:12
Show Gist options
  • Save eeckstein/29bcba7899d851ae95860c152ce83d50 to your computer and use it in GitHub Desktop.
Save eeckstein/29bcba7899d851ae95860c152ce83d50 to your computer and use it in GitHub Desktop.
Binding Properties

Binding Properties

Introduction

"Standalone" inout and borrow bindings are proposed here: https://gist.github.pie.apple.com/tkientzle/1819f59bef91ac85cac2859c153aae6e

On top of that, inout/borrow properties allow using inout and borrow bindings for properties in structs.

struct S {
  inout i: Int
  init(i: stored inout Int) {
    self.i = &i
  }
  func increment() {
    i += 1
  }
}

var x = 27
let s = S(i: &x)
s.increment()
// x == 28

Motivation

There are several use cases for this feature in the Swift compiler sources. For example:

  func collectAllUses(of value: Value, into list: inout InstructionList) {
    struct Walker : ValueDefUseWalker {
      inout result: InstructionList
      mutating func walkDown(value: Value) { result.append(value) }
    }
    var walker = Walker(result: &list)
    walker.walkDown(value)
  }

Proposed Solution

The proposed solution is similar to the standalone bindings, except that the bindings are embedded into a struct. A struct which has binding properties is not copyable and must not escape (e.g. via a function return).

Initializers

A binding property must be initialized with an appropriate binding, passed as argument to the initializer. Such a binding argument must be indicated with an additional keyword (I called it stored in the above example) to indicate that the lifetime of the binding extends to the lifetime of the initialized struct.

In the following example i is alive during the execution of the initializer while j is alive during the lifetime of the initialized struct.

  init(i: inout Int, j: stored inout Int) {
    i += 1
    self.j = &j
  }

Note that this implies that all initializers of a struct with binding properties must have at least one stored binding argument. Otherwise it wouldn't be possible to initialize the binding properties. In fact, the compiler knows that a struct has a binding property if its initializers have at least one stored argument.

Default initializers for structs with binding properties can be synthesized, as with regular structs.

struct ThreeFields {
  borrow a: Int
  inout b: Int
  let c: Int
  /// synthesized initializer:
  init (a: stored borrow Int, b: stored borrow Int, c: Int) {
    self.a = &a
    self.b = &b
    self.c = c
  }
}

Structs containing other structs with binding properties

For example:

struct T {
  let s: S   // from the first example
}

It's interesting to see how such a struct can be initialized. Specifically how the property s can be initialized. There are several possibilities:

  • The property S is created inside the initializer:
init(i: stored inout Int) {
  self.s = S(&i)
}
  • A value of S is passed to the initizlier:
  init(s: stored S) {
    self.s = s
  }

Note that the the initializer argument s is passed as "owned", which means it's not violating the non-copyable rule. Still, it needs the stored keyword to let the caller know that it's lifetime is bound the the whole created struct.

Lifetime Safety

As with standalone bindings, the compiler has to make sure that the lifetime of a struct with a binding does not exceed the lifetime of a bound value. In most cases this is ensured by the strict stack discipline of variable scopes.

{
  var x = 27
  let s = S(i: &x)
  ...
  // lifetime of s ends here
  // lifetime of x ends here
}

But this does not apply to classes. Therefore it's not possible to have binding properties in classes. The lifetime of a class object is not statically known in most cases.

An exception is introduced by the proposed consume operator: it's possible to end the lifetime of a variable earlier than at the end of its scope with the proposed consume operator. The compiler must check for possible bindings and issue an error if needed.

Non-escaping closures

With binding properties we can also support storing non-escaping closures in structs. For example:

struct WithClosure {
  let c: @nonescaping () -> ()
}

Such a non-escaping closure property needs to be annotated with @nonescaping because the default for closure properties is "escaping".

Library Evolution

Structs with binding properties can be exported by library evolution modules, with the following rules:

  • It's not possible to add a binding property to a struct which doesn't have a binding property yet. This follows from the fact that all initializers of a struct with binding properties must have a stored argument.
  • It's possible to change a stored binding property to a computed binding property or vice versa. Both variants are accessed via the _modify accessor.

Future directions

It's also possible to have inout/borrow bindings in enum payloads or in tuple elements. But it's unclear if that would be a useful thing.

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