Introduction
mutating methods require the caller to ensure that the location passed as self can only be accessed through self. This guarantee of exclusive access is a very strong property that can be used by carefully written types like MutableSpan to achieve higher-level safety guarantees. However, operations that want exclusive access are not always semantically mutations, and tying these concepts together creates some real usability problems for these types. This proposal adds a new exclusive parameter ownership modifier which has the same exclusivity properties as mutating but does not permit the value to be changed. (This is only narrowly useful, and most programmers will never need it, but it is very important in those cases.) It also adopts the modifier on some existing APIs.
Motivation
Swift currently supports three parameter/method ownership modifiers: borrowing, inout/mutating, and consuming.
These modifiers can be understood concretely as statements about the basic capabilities of the parameter. For example, an inout parameter can be modified, but a borrowing parameter cannot.
These modifiers can also be understood more abstractly as propositions in Swift's formal logic of ownership. Each modifier represents a different assertion about the exclusivity that the value or location is known to have within the function. For example, an inout parameter is known to be the only way to access that memory location as long as the function is running, while a borrowing parameter makes no such guarantee.
These two levels are naturally related. The abstract propositions about ownership create conditions that make the concrete capabilities safe and reasonable. Swift permits multiple borrowing parameters to be bound to the same value because they can only be used to read it. inout requires exclusive ownership because that allows mutation to still be subject to static local reasoning.
However, the abstract propositions enable a lot more than just that. The exclusive ownership of inout states that this function is the only code that can access the given memory location. A carefully-designed type can make use of that to get Swift to enforce its own safety properties. For example, an UnsafeMutablePointer essentially has reference semantics: two different copies of the same pointer can be used to access the same memory. This can lead to memory safety bugs. A safe mutable pointer type could use non-copyability to maintain an invariant that there's only one pointer value that referenes a particular piece of memory. It could then use exclusivity to enforce that only one operation can be done to that memory at a time.
That is exactly what's already being done by MutableSpan. The construction of a MutableSpan has a precondition that the memory is uniquely referenced and fully initialized. The operations on MutableSpan conspire to maintain those properties as invariants: the type is non-Copyable, and it has no methods that can be used as effective copies. The operations that merely read the underlying memory are borrowing because it is safe that have multiple such operations occuring at once. The operations that mutate the underlying memory are mutating, not because they actually mutate the MutableSpan value (which is just the pointer and its bound), but because mutating ownership prevents any other operations from happening on that MutableSpan value at the same time, statically ruling out several classes of memory safety bug.
Hidden in that last point is a very real usability problem. mutating operations on value types are usually mutations of the value. Swift therefore, quite reasonably, only allows mutating operations to be performed on mutable memory locations. But if an operation is only mutating because it wants exclusive ownership of the value, this is too strong! It disallows useful patterns like calling the operation on a function result (e.g. array.mutableSpan.operation()) or a local let (e.g. mySpan.operation()). What's needed is a weaker form of ownership that still asserts exclusive access to the value but does not claim to mutate it. That is what we are proposing here.
Proposed solution
A parameter can be declared to have exclusive ownership:
func partition(values: exclusive MutableSpan<Int>, by pivot: Int) {
// ...
}
Within the function, the parameter cannot be mutated or consumed, but it can be borrowed or used exclusively.
func testUses(span: exclusive MutableSpan<Int>) {
// valid: can call an exclusive operation on MutableSpan
span[0] = 10
// invalid: cannot mutate an exclusive value
span = span.extracting(last: 10)
}
The argument passed to the parameter must be an expression that can be used exclusively. This includes most expressions except references to storage declarations that can only be borrowed; the exact rules are given in the detailed design.
func testLocalVariable() {
var array = [10, 0, 20, 5]
// valid: array.mutableSpan produces an r-value that can be used
// exclusively
partition(values: array.mutableSpan, by: 7)
// valid: a local variable (even an immutable one) can be used
// exclusively
let span = array.mutableSpan
partition(values: span, by: 12)
}
func testInoutParameter(span: inout MutableSpan<Int>) {
// valid: an inout or exclusive parameter can be used exclusively
partition(values: span)
}
func testBorrowingParameter(span: MutableSpan<Int>) {
// invalid: a borrowing parameter cannot be used exclusively
partition(values: span)
}
A non-static method of a struct, enum, or protocol, including an accessor of a non-static property or subscript, can be declared to have exclusive ownership:
extension MutableSpan {
exclusive func partition(by pivot: Element) {
// ...
}
subscript(index: Int) -> Element {
borrow { /* ... */ }
exclusive mutate { /* ... */ }
}
}
This behaves exactly as if the implicit self parameter was declared to have exclusive ownership. No other function can be declared exclusive.
Detailed design
exclusive is an ordinary parameter/method ownership modifier. It participates in the function type system like other existing modifiers:
-
exclusivecannot be combined with any other parameter/method modifiers:mutating,nonmutating,inout,borrowing, orconsuming. -
Two functions cannot be overloaded solely by the ownership of a parameter.
-
Whether a parameter is
exclusiveis part of the type of a function. Two function types that differ only by parameter ownership are different function types. -
Changing the ownership of a parameter changes the ABI of the function.
-
For the purposes of function subtype conversions,
consuming Tis a subtype ofexclusive T, which is a subtype ofborrowing T. Since function subtyping is contravariant over parameter types, this means that e.g.(exclusive T) -> Intconverts to(consuming T) -> Int. -
A
consumingprotocol requirement can be implemented by anexclusivemethod. Anexclusiveprotocol requirement can be implemented by aborrowingmethod.
exclusive parameters of copyable type are not implicitly copyable, in the same way that consuming and borrowing parameters are not implicitly copyable.
Exclusive uses
An expression can be used exclusively if:
- it is an expression that produces a temporary value, such as a function call;
- it is an expression that can be implicitly copied to produce a temporary value; or
- it is a storage reference expression and both:
- the storage declaration is one of
- a stored variable, constant, or property,
- a variable, constant, property, or subscript with a getter, or
- a variable, constant, property, or subscript with some kind of modify accessor; and
- the base expression, if any, can satisfy the ownership requirement of the accessor selected (treating a stored property as if it had an
exclusiveaccessor).
- the storage declaration is one of
Using a stored variable or property exclusively may cause exclusivity conflicts, essentially as if it were mutated.
A global let constant, static let property, or class instance let property cannot be used exclusively. These storage locations would otherwise have to used dynamic exclusivity enforcement, even on simple reads. This would be a binary compatibility problem for existing code, because they currently do not require enforcement. More fundamentally, it would be a poor trade-off to force dynamic exclusivity checking on all uses of lets just for a vanishingly rare case where are used exclusively.
An escaping local let constant may require dynamic exclusivity enforcement if it is ever used exclusively.
Changes to MutableSpan
All existing mutating methods and accessors on MutableSpan are revised to be exclusive. It is meaningful to have a mutating operation on MutableSpan, e.g. if it changed the bounds of the span, but none of the existing operations happen to look like that, so they should all be weakened to only require exclusive access.