"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
There are several use cases for this feature in the Swift compiler sources. For example:
- We'd like to have the ability to store a "reference" to the pass context in other structs, e.g. in the
Builder
. TheBuilder
is constructed from the context and it would be nice to store an inout reference to the context in the Builder (see https://github.com/apple/swift/blob/ce4ec0842a4c7d0ebf71f7b6fe9b6feab4bc76ca/SwiftCompilerSources/Sources/SIL/Builder.swift#L26). The current "workaround" is that the context is basically a pointer to a C++ data structure. - We have several utilities which can be "configured" by implementing a protocol. For example, the walker utilities (https://github.com/apple/swift/blob/main/SwiftCompilerSources/Sources/Optimizer/Utilities/WalkUtils.swift). Currently it's not possible that such a utility implementation stores its result in something which is not a member property of the utility. E.g. it would be nice to write
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)
}
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).
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
}
}
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.
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.
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".
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.
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.