Proposed modification to SE-0377: should explicit parameter ownership modifiers suppress implicit copying?

I quite like the idea of making @noImplicitCopy unnecessary, and I like the general idea of tagging the binding with that semantic based on how it is spelled. At first I was concerned about more than doubling the set of assignment keywords, but after playing with it I think the concision is a serious advantage.

One lingering concern is how this will impact readability of structs and classes. Hunting for variable declarations isn’t as common as scanning a type for property declarations, and this change would increase the set of watch words to let, var, inout, borrow, consume, and presumably const, eventually.

Some other remaining questions:

  • If I declare inout x = &y, is y still in scope? I would assume not, because x now has exclusive access.

  • Since the same keywords are still being proposed as operators, would let x = consume y and consume x = y both mean the same thing?

  • Does this pitch also cover parameters? If so, does that mean that func foo(_ arg: inout T) now means arg is no-implicit-copy within the body of foo?

  • Can property declarations use borrow and consume?

  • I also feel like this doesn’t address @John_McCall’s point about the inconsistency of &. I’ll again plug my previous suggestion of making & mandatory for all borrowings or consumptions of bindings:

Mutability Local Variable Local Borrow Local Consume Value Parameter Borrowed Parameter Consuming Parameter
Immutable let x: T inout let x: T in let x: T func f(x: T) (preferred)
or func f(x: let T)
func f(x: inout let T) func f(x: in T) (preferred)
or func f(x: in let T)
Mutable var x: T inout x: T (preferred)
or inout var x: T
in var x: T func f(x: var T) func f(x: inout T) (preferred)
or func f(x: inout var T)
func f(x: in var T) (mutability is not visible to caller)
Assignment Rule
let x = y Implicit immutable copy of y.
var x = y Implicit mutable copy of y.
inout let x = &y
or in x = &y
Immutable borrow/consume of y. y can be mutable or immutable.
inout x = &y
or in var x = &y
Mutable borrow /consume of y. y must be mutable.
inout let x: T
var y = copy(x)
Values must be explicitly copied out of in let, in var, inout let, or inout var. Mutability of copy is independent.
var x: T
func f(_: inout let T) { }
f(&x)
All bindings can be passed to inout let parameters. (Callee immutably borrows the binding for the duration of the call.)
inout var x: T
func f(_: inout x: T) { }
f(&x)
var, in var, and inout var bindings can be passed to inout var parameters. (Existing Swift semantics.)
let x: T
func f(_: in T) { }
f(&x)
let, var, in let, and in var bindings can be passed to in parameters. Consumes the caller’s binding; callee loses mutability. (Can’t pass inout because access is transferred to the callee, but the callee wouldn’t transfer ownership back.)
let x: T
func f(_: in var T) { }
f(&x)
let, var, in let, and in var bindings can be passed to in var parameters. Caller doesn’t see any difference from in parameters, but value is mutable within callee.
// To end the lifetime of a binding early, assign the binding to _.
// Ending the lifetime of an inout binding writes its value back.
func addFortyTwo(to x: inout Int) {
    x += 42
    _ = &x
    print("Added!")
}

// To end the lifetime of a binding while passing it as a parameter, use the standard library’s drop() helper function.
// (note this won’t work with inout bindings; drop those explicitly with assignment to _):
func drop<T>(_ binding: in T) -> in T {
    return &binding
}

var heavyWeight = MyStruct()
doStuff(with: drop(heavyWeight))

// To read the value of a binding, use the standard library’s copy() helper function.
@_semantics("binding.copy")
func copy<T>(_ binding: inout let T) -> T {
    // Unused implementation; the compiler actually never emits a call to this function.
    // It just exists to make the type system happy.
    return _Builtin.copy(&binding)
}

inout var x: T
var copyOfX = copy(&x)
2 Likes