`Borrow` and `Inout` types for safe, first-class references

Hi everyone. We're working on introducing new non-Escapable standard library types for referencing single values, Borrow and Inout. Analogous to how Span and MutableSpan provide safe access to groups of contiguous values in memory owned by someone else, Borrow and Inout provide safe access to a single value owned by someone else. This unblocks a number of capabilities that Swift developers have long asked for, such as the ability to bind a local variable or stored property to a reference to another value. I've put up the draft proposal here:


Borrow and Inout types for safe, first-class references

Summary of changes

Two new standard library types, Borrow and Inout, represent safe references to another value, with shared (immutable) or exclusive (mutable) access respectively.

Motivation

Swift can provide temporary access to a value as part of a function call:

  • An inout parameter receives temporary exclusive access to a value from by the caller. The callee can modify the parameter, and even consume its current value (so long as it gets replaced with a new value), and the caller reclaims ownership once the callee returns.
  • A borrowing parameter receives temporary shared access to a value from the caller. Since there may be other ongoing accesses to the same value, the callee can generally only read the value, but it can do so without needing an independent copy of the value.

However, it is useful to be able to form these sorts of references outside of the confines of a function call, as local variable bindings, as members of other types, in generic containers, and so on. Developers can use classes to box values and pass references to a common holder object around, but in doing so they introduce allocation, reference counting, and dynamic exclusivity checking overhead. UnsafePointer is, of course, unsafe, and interacts awkwardly with Swift's high-level semantics, requiring extreme care to use properly.

Proposed solution

We introduce two new generic types to the standard library to represent references as first-class, non-Escapable types. Borrow represents a shared borrow of another value, which can be used to read the target value but not consume or modify its contents:

public struct Borrow<Value: ~Copyable>: Copyable & ~Escapable {
  @_lifetime(borrow target)
  public init(_ target: borrowing Value)

  public var value: Value { borrow }
}

Inout<T> represents an exclusive access to another value, granting the owner of the Inout the ability to modify the target value and assume exclusive use of the target value for as long as the Inout value is active:

public struct Inout<Value: ~Copyable>: ~Copyable & ~Escapable {
  @_lifetime(&target)
  public init(_ target: inout Value)

  public var value: Value { borrow; mutate }
}

Note that these types' interfaces use lifetime dependencies to be able to construct ~Escapable values, and also use borrow and mutate accessors to provide efficient access to the target value without unnecessary limitations on the scope of the access.

References are formed by passing the target to one of the Borrow.init or Inout.init initializers. Once formed, the target value can be accessed through the reference's value property. Using these types, developers can bind a local reference to a nested value once for repeated operations:

func updateTotal(in dictionary: inout [String: Int], for key: String,
                 with values: [Int]) {
  // Project a key out of a dictionary once...
  var entry = Inout(&dictionary[key, default: 0])

  // ...and then repeatedly modify it, without repeatedly looking into the
  // hash table
  for value in values {
    entry.value += value
  }
}

Using the experimental lifetimes feature, developers can write functions that return references:

struct Vec3 {
  var x, y, z: Double

  @_lifetime(&self)
  mutating func at(index: Int) -> Inout<Double> {
    switch index {
    case 0: return Inout(&x)
    case 1: return Inout(&y)
    case 2: return Inout(&z)
    default:
      fatalError("out of bounds")
    }
  }
}

Borrow and Inout can also appear as fields of other non-Escapable types:

// A struct-of-arrays of people records.
struct People {
  var names: [String]
  var ages: [Int]

  subscript(i: Int) -> Person {
    @_lifetime(&self)
    mutating get {
      return Person(name: &names[i], age: &ages[i])
    }
  }
}

// A mutable reference to a single person.
struct Person: ~Copyable, ~Escapable {
  var name: Inout<String>
  var age: Inout<Int>
}

Detailed design

Lifetime dependence

Both Borrow and Inout are non-Escapable types. Once formed, they carry a lifetime dependency on their target, so can be used only as long as the target value can remain borrowed (in the case of Borrow) or exclusively accessed (in the case of Inout). Conversely, the target undergoes a borrow or exclusive access for the duration of the reference's lifetime, so the target may only undergo other borrowing accesses while a dependent Borrow is in use, and cannot be used directly at all while a dependent Inout has exclusive access to it.

var totals = [17, 38]

do {
  let apples = Borrow(totals[0])

  print(apples.value) // prints 17

  apples.value += 2 // ERROR, Borrow.value is read only

  totals[1] += 1 // ERROR, cannot mutate `totals` while borrowed

  print(totals[1]) // prints 38. we can still borrow `totals` again
  print(apples.value) // prints 17
}

do {
  var bananas = Inout(&totals[1])

  bananas.value += 2 // we can mutate the value through `Inout`

  print(bananas.value) // prints 40

  print(totals[1]) // ERROR, totals is exclusively accessed by `bananas`

  bananas.value += 2
  print(bananas.value) // prints 42
}

print(totals) // prints [17, 42]

This behavior is analogous to the interaction between an Array and dependent Span or MutableSpan values accessed through its span and mutableSpan properties. (Indeed, one could look at Borrow and Inout as being the single-value analogs to the multiple-value-referencing Span and MutableSpan, respectively.)

Interaction with nontrivial accesses

A Borrow can target any value, and Inout can target any mutable location, including properties or subscripted values produced as the result of get/set pairs, yielded by yielding coroutine accessors, guarded by dynamic exclusivity checks, or observed by didSet/willSet accessors. In these situations, the access will first be initiated to form the target value (invoking the getter, starting the yielding borrow or yielding mutate coroutine, etc.). The Borrow or Inout reference will then be formed targeting that value. When the reference's lifetime ends, the access will be ended (invoking the setter, resuming the yielding coroutine, invoking willSet and/or didSet observers).

struct NoisyCounter {
  private var _value: Int

  var value: Int {
    get {
      print("counted \(_value)")
      return _value
    }
    set {
      print("updating counter to \(newValue)")
      _value = newValue
    }
  }
}

var counter = NoisyCounter(67)
do {
  var counterRef = Inout(&counter.value) // begins access to `counter.value`, prints "counted 67"
  counterRef.value += 1
  counterRef.value += 1
  // access to `counter.value` ends, prints "updating counter to 69"
}

Note that Borrow and Inout are dependent on the access; they only reference the target value and do not capture any context in order to end the access themselves. Therefore, a Borrow or Inout derived from a nontrivial access generally cannot have its lifetime extended beyond its immediate caller, since the caller must execute the code to end the access at the end of the reference's lifetime.

@_lifetime(&target)
func noisyCounterRef(from target: inout NoisyCounter) -> Inout<Int> {
  // ERROR, would extend the lifetime of `Inout` outside of the formal access
  return Inout(&target.value)
}

Direct accesses to struct stored properties, direct accesses to immutable class stored properties, or accesses that go through borrow or mutate accessors do not require any code execution at the end of the access, so Borrow and Inout values targeting those are only limited by the parent access from which the property or subscript was projected.

Representation of Borrow

Reading the following section is not necessary to use Borrow, but is of interest to understand its type layout and implementation.

Depending on the properties of the Value type parameter, Borrow<Value> may either be represented as a pointer to the target value in memory, or as a bitwise copy of the target value's representation. The pointer representation is used if Value meets any of the following criteria:

  • MemoryLayout<Value>.size is greater than 4 * MemoryLayout<Int>.size; or
  • Value is not bitwise-borrowable; or
  • Value is addressable-for-dependencies.

The size threshold aligns with the Swift calling convention's threshold for passing and returning values in registers, to avoid wasteful bitwise-copying of very large values while ensuring that Borrows can be passed and returned across function boundaries without being dependent on temporary stack allocations.

The emphasized terms are defined below:

Bitwise borrowability

An Int value has the same meaning no matter where it appears in memory, so even if one is passed as a borrowing parameter, Swift will avoid indirection and pass an Int by value at the machine calling convention level. Similarly, an object reference's pointer value is equivalent anywhere in memory, so even though the act of copying a strong reference requires increasing the object's reference count, the underlying pointer can be passed by value. We refer to these types as bitwise-borrowable, since a borrow can be passed across functions by bitwise copy.

As such, immutable values of bitwise-borrowable type do not have a stable address. If Borrow always used a pointer-to-target representation, then forming a Borrow targeting a bitwise-borrowable value would require storing that value in memory, possibly in a temporary stack allocation. A temporary stack allocation would mean that functions would be unable to receive a borrowed parameter, form a Borrow of it, and return that value, since the Borrow would depend on the function's own stack frame:

@_lifetime(borrow target)
func refer(to target: AnyObject) -> Borrow<AnyObject> {
  // This ought to be allowed
  Borrow(target)
}

Therefore, a Borrow of a small bitwise-borrowable type takes on the representation of the value itself, unless the type is also addressable-for-dependencies (described below).

Addressability for dependencies

Some types are bitwise-borrowable, but also provide interfaces that produce lifetime-dependent values such as Spans that need to have pointers into their in-memory representation. InlineArray is one such example; it is bitwise-borrowable when its element type is, but its span property produces a Span with a pointer to the array's elements, which is expected to have a lifetime constrained by borrowing the InlineArray:

@_lifetime(borrow target)
func span(over array: [2 of Int8]) -> Span<Int8> {
  // This ought to be allowed
  return array.span
}

Swift classifies InlineArray, as well as any type containing an InlineArray within its inline storage, as addressable-for-dependencies. Values of such types are always passed indirectly as a parameter to a function call whose return value has a lifetime dependency on that parameter. In the example above, this ensures that in the call to span(over:), the array parameter exists in memory that outlives the call, allowing the Span to be safely formed and returned to the caller.

Borrow should not interfere with the lifetime of dependent values projected from the target through the Borrow, so when the Value type is addressable-for-dependencies, Borrow uses the pointer representation.

@_lifetime(copy borrow)
func span(over borrow: Borrow<[2 of Int8]>) -> Span<Int8> {
  // This also ought to be allowed
  return borrow.target.span
}

struct, union, and class types imported from C, Objective-C, and C++ are always considered to be addressable-for-dependencies. This is intended to make it easier for Borrow types to interact with data types in those languages that use pointers and/or C++ references to represent relationships between values.

Representation of Inout

inout parameters are always passed by address at the machine calling convention level, so Inout can use a pointer representation in all cases without limiting its ability to be passed across function call boundaries.

Source compatibility

This proposal adds two new top-level declarations to the standard library, Borrow and Inout. According to GitHub code search, existing code that declares a struct Borrow or struct Inout is rare, though not nonexistent. Swift's name lookup rules favor locally-defined and explicitly-imported names over standard library names, so existing code should continue to compile and behave as it used to.

ABI compatibility

This proposal is additive and does not affect the ABI of existing code.

Implications on adoption

Generic support for Borrow requires new runtime type layout functionality, which may limit the availability of these types when targeting older Swift runtimes.

Future directions

~Escapable target types

As proposed here, Borrow and Inout both require their target type to be Escapable. There are implementation limitations in the compiler that prevent implementing references to non-Escapable types. With the limitations of the current lifetime system, the non-Escapable value projected from a Borrow or Inout would also be artificially lifetime-constrained to the lifetime of the reference, since the current model lacks the ability to track multiple lifetimes per value.

A borrowing reference type that is always represented as a pointer

As discussed in the "Representation of Borrow" section, Borrow<Value> will use a value representation for some Value types, rather than a pointer. This should be transparent to most native Swift code, but in some situations, particularly when doing manual data layout or interoperating with other languages, it may be interesting to have a variant that is always represented as a pointer. That type would sometimes be forced to have a shorter maximum lifetime than Borrow can provide if the target needs to have a temporary memory location formed to point to.

Generalized single-yield coroutines

As discussed in the "Interaction with nontrivial accesses" section, when a Borrow or Inout targets a value that is referenced through a nontrivial access (such as a get/set pair, a yielding coroutine accessor, a stored property with dynamic exclusivity checking, etc.), the reference's lifetime is confined to the caller that initiated the access, since that same caller must end the access after the reference's lifetime ends. However, if we allowed arbitrary functions and methods to be defined as single-yield coroutines, not only accessors, that could provide a way to define functions that compute references depending on nontrivial accesses:

yielding func noisyCounterRef(from target: inout NoisyCounter) -> Inout<Int> {
  // this would be OK, since we're yielding `Inout` to the caller without
  // ending the current execution context
  yield Inout(&target.value)
}

Access-capturing reference types

Another possible tool for handling references into nontrivial accesses might be to introduce a "fat reference" that can capture the suspended execution contexts of any accesses to be passed along with the value reference itself. Such a type would naturally use more space and incur more execution overhead to use, but may be useful in some circumstances.

Primitive reference bindings in more places

Using Borrow and Inout, developers can form reference bindings anywhere a variable or property can be declared, reaching beyond the limited places in the language that references can be formed. However, as distinct types with their own interface, these types introduce indirection overhead in forming and dereferencing the references separate from the target values. It may still be valuable to introduce primitive reference binding syntax to the language (which could be viewed as sugar over forming an explicit Borrow or Inout):

// Explicitly-formed reference
let x = Borrow(y)
x.value.foo()

// Reference binding sugar
borrow x = y
x.foo()

Implicit dereferencing or member forwarding

Along similar lines, we might consider giving Borrow or Inout dynamic member lookup capabilities, or introducing something like Rust's Deref trait to automatically forward name lookups from Borrow or Inout to the target value. This would somewhat reduce the syntactic overhead of working with these types, though any such mechanism will be imperfect, since Borrow and Inout's own members will shadow any forwarding mechanism.

exclusive ownership or reborrowing for Inout.value

Inout shares an ergonomic problem with MutableSpan: for safety, mutation of the target value through Inout requires exclusive access to the Inout value. In Swift today, this exclusive access can only be exercised through mutable bindings, which will force Inout values to be assigned to var bindings even if the Inout value itself is never mutated, and prevent expressions returning Inout values from being used directly in mutation expressions:

var source = #"print("Hello World")"#

let ref = Inout(&source)
// ERROR, mutating `value` requires mutable access to `ref`
ref.value += #"; print("Goodbye Universe")"#

func getRef(from: inout String) -> Inout<String> { return Inout(&from) }

// ERROR, mutating `value` requires mutable access to temporary value
getRef(from: &source).value += #"; print("...except for that guy")"#

Any of the remedies we are considering for MutableSpan are also applicable to Inout:

  • A new exclusive ownership mode, which is applicable to values that are exclusively owned either because they offer mutable access or because they are owned immutable values, would allow for temporary and immutable Inout values to be projected safely.
  • Alternatively, a mechanism similar to Rust's "reborrowing" mechanism, whereby mutable references are consumed by projection operations but can be re-formed after those dependent projections are completed, could also be made to work with Inout values and derived projections.

Alternatives considered

Naming the Inout type

Our usual naming conventions might argue that the proper spelling of the inout-capturing reference type would be InOut, capitalizing both words. The authors subjectively find this odd-looking, and hard to type, and see inout in its special

Naming the value property

We propose giving Borrow and Inout a property named value to access the target value. This aligns with the interface of the proposed Unique type. Some other possibilities we considered include:

  • using a subscript with no parameters, so that reference[] dereferences the value
  • using a more reference-specific name such as target
  • introducing a dedicated dereference operator akin to C's *x
29 Likes

Thanks Joe! Here’s some early read feedback:


It’s not obvious to me why we have Borrow and Inout types instead of extending the borrow/inout syntax to work in more places, since we already have those. What is the difference between using an inout Foo parameter over an Inout<Foo> parameter? If there are no real differences, given that inout Foo doesn’t work in field or local positions today, why are we creating a different syntax to occupy the same role? I see the future direction where borrow x = y is sugar for let x = Borrow(y), but I’d like to understand why it’s not the direction today.


I didn’t exhaustively read the Box thread, but I did notice that it uses a box[] syntax to access the contents rather than a value property. I imagine that this should be unified, if it hasn’t already been?


When discussing bitwise-borrowable types, the proposal says that this might not work if we chose to use an address representation:

@_lifetime(borrow target)
func refer(to target: AnyObject) -> Borrow<AnyObject> {
  // This ought to be allowed
  Borrow(target)
}

Is the implication that this only works with bitwise borrowable types?


The proposal explains that some types, like InlineArray, can be addressable-for-dependencies. However, I don’t see a discussion of the ABI implications of that. How does the compiler infer that a structure is addressable-for-dependencies? If I add a small InlineArray to a resilient struct, am I potentially changing how it needs to be passed by borrow? There hasn’t been a formal proposal for @lifetime to ask those questions yet, so it’ll have to bleed here: does adding a @_lifetime annotation, or letting the compiler infer the necessity of one, have ABI implications?


The interface for Borrow and Inout only have a safe value property. Should they also have withUnsafePointer-family functions? (Or is the expectation that withUnsafePointer(borrow.value) is the way to go?)

11 Likes

The Box proposal, now proposed as Unique, has been changed to use a .value property instead.

5 Likes

Another question that I forgot: does Borrow<ReferenceType> work to ensure that there is no refcount overhead on the borrowed object?

2 Likes

No, Borrow can represent both the bitwise and address forms based on what T is. Atomic<T> is always passed by address, so Borrow<Atomic<T>> will just be a pointer to the atomic unlike Borrow<Int> which is just the Int itself.

1 Like

I think there's a precedent in Swift that storage representation (such as ownership and indirection) is denoted using modifiers instead of first-class types. Aside from the ownership modifiers of function parameters, some examples are unowned and indirect. I think that's usually beneficial, because it means that people only have to specify the representation of storage when they define it, not when they use it.

For example, when pattern matching on an indirect enum case, I think it's nice that there's no need for a separate Indirect(let value) pattern just to acknowledge indirection. (For enums, it's also beneficial because there's currently no way to pattern match through a struct, and checking exhaustivity for computed properties would be complicated.)

Computed properties, especially with the new accessors like yielding borrow, and property wrappers also help with abstracting over storage representation at the point of use.

Class types also don't require indirection to be acknowledged at the point of use. For class types (and weak properties), the indirection is semantically important, because it introduces shared mutable state ("reference semantics"). In contrast, borrowing and inout references would preserve value semantics at the point of use, which I think makes it less important to acknowledge indirection at the point of use.


In Rust, implicit dereferencing with Deref is helpful because it usually hides indirection at the point of use, but it's unreliable: there are still situations where indirection suddenly has to be acknowledged. The proposal mentions one example: method shadowing.

Another example is assignment, which suddenly makes an explicit dereference necessary (e.g. *mut_ref = value or *mut_ref1 = *mut_ref2). Personally, I've at times forgotten to write an explicit dereference, because I'm accustomed to indirection being hidden. (But at least it's always just led to a compiler error instead of a bug; probably because variables in Rust are usually immutable. But using an immutable variable to store a mutable reference is only possible because of "reborrowing", which Swift doesn't have at least for now.)


The main benefit of first-class types, of course, is that they can basically make generic code also generic over ownership. I'm not sure how common that'll be in practice, though. I'd prefer if primitive reference bindings were introduced first; then first-class Borrow or Inout types could be introduced later if there's a clear need for them.

I don't think there's been strong demand for a first-class Unowned type, probably because an unowned reference is usually contained in some concrete type, instead of a generic container like an array. Maybe that could end up being the case for borrowing and inout references as well.

It is inconvenient that operators like ?? and methods like Optional.map or Result.get can't be generic over ownership. But using first-class types for those could have its own difficulties, such as the need for explicit wrapping. Computed properties and subscripts avoid these issues because, with the different kinds of accessors, they're basically generic over ownership. Maybe operators and methods could also have accessors, or a feature similar to accessors.

References in C++ also hide indirection at the point of use. They're commonly used (over pointers) despite their problems, and I think most of those problems wouldn't apply to Swift. That is, in C++, indirection is not memory safe, making it dangerous to hide indirection; references can behave unexpectedly in generic code; and structs containing references become non-copyable and non-movable.[1]


  1. Edit: To be fair, the reason structs with references become non-copyable and non-movable in C++ is because of the ambiguity between "rebind" semantics and "assign-through" semantics. Maybe that would also apply to Swift. ↩︎

8 Likes

I read the paragraph preceding the code example as a justification for bitwise borrows being necessary in some form:

As such, immutable values of bitwise-borrowable type do not have a stable address. If Borrow always used a pointer-to-target representation, then forming a Borrow targeting a bitwise-borrowable value would require storing that value in memory, possibly in a temporary stack allocation. A temporary stack allocation would mean that functions would be unable to receive a borrowed parameter, form a Borrow of it, and return that value, since the Borrow would depend on the function's own stack frame:

But if it works for non-bitwise-borrowed types, then that would just work anyways?

The issue with this is that bindings cannot be returned from function calls or be wrapped generically. Specifically, if we wanted to introduce a dictionary that was capable of storing noncopyable keys and values, the API to get the value of a particular key would have to return something like Optional<Inout<Value>>. This can’t simply be a binding.

1 Like

The compiler does not infer addressable-for-dependencies at all; it is another underscored attribute we introduced specifically for InlineArray (InlineArray.span) and String (String.utf8Span).

No, resilient structs are always passed opaquely by address already, so adding an InlineArray or String does not affect how it gets passed.

2 Likes

Borrow<Value> and borrowing Value - different types. Is it possible to unify the ownership system like this?

func f1() → inout Value
func f2() → borrowing Value

struct Foo: ~Escapable {
    borrowing v1: Value
    inout v2: Value
}

Borrow and Mutate Accessors proposal contains this example in Future Directions.

struct S<Value> {
  subscript(_ index: Int) -> Value {
     borrow { ... }
  }
  func indirect(_ parameter: Foo) -> borrowing Value {
     let index = ... compute index from parameter ...
     return self[index]
  }
}
4 Likes

To confirm, there are no circumstances in which moving from one to the other causes issues?

struct Foo {
	private var tup: (Bar, Bar, Bar, Bar)
	
	func bar() -> Borrow<Bar> {
		Borrow(tup.1)
	}
}

struct Foo {
	private var array: InlineArray<4, Bar>
	
	func bar() -> Borrow<Bar> {
		Borrow(tup[1])
	}
}

Last month, @John_McCall was still considering whether Inout<T> was needed:

As someone trying to come up to speed with exclusive ownership, the fact that the language would admit something like inout Inout<Borrow<T>> makes me anxious. It seems to add so many dimensions to the ownership space that I will never feel confident that I have chosen the right mix of modifiers and wrapper types.

7 Likes

Can this be expressed using keywords instead, keeping Borrow and Inout as implementation details rather than exposed types? Using Borrow and Inout as types feels somewhat un-Swifty, especially given that Swift already has ownership keywords.

This is unfortunate. Today we have Mutex<T> , which must always be declared as let while still allowing mutation of the protected value. That model works well. Introducing Inout , however, creates a reference that is mutable in two dimensions: the binding itself can be reassigned, and the underlying value can be mutated. In most cases, a let Inout seems to be the desired and sufficient form.

An immutable binding to a mutable reference (let Inout ) is clearly useful. A mutable binding (var Inout ) seems useful only in very narrow scenarios. In other words: are there concrete use cases that truly require Inout itself to be mutable? What kinds of problems cannot be expressed without allowing var Inout ?

Finally, there is the question of exclusive borrowing, which has been discussed recently. That seems to imply a third concept, something along the lines of BorrowExclusive type. Are there any current plans or design considerations around this, or thoughts on how exclusive borrowing would fit into this model?

3 Likes

At first sight, looks like these types could be accompanied by a property wrapper or a macro, same for Unique.

1 Like

Me too. I would rather prefer such pattern restricted than having them. This restriction can be relaxed over time, if ever.

No matter to say that strange things can be done, like this:

func foo<T>(value: inout Inout<Borrow<Inout<Borrow<T>>>>) {
}

Such things can not be expressed with keywords and rejected at compile time:

func foo<T>(value: inout inout borrowing inout borrowing: T) { // Error
}

The design should prioritize clarity and safety by avoiding these convoluted, deeply nested types.

1 Like

I'm finding the meaning of inout a bit confusing now, especially with the introduction of ~Copyable types and the ownership modifier. Previously, my mental model of inout was similar to a get-set property, where a copy of the value would be passed in, modified, and then the changed copy would be set back (out).

However, with ~Copyable instances, no copies are made. Instead, the inout parameter behaves as a mutable reference: there is no copy passed in, and no modified copy is set back. The same concept applies to the Inout type – it essentially represents a mutable reference.

This leads to the question: when dealing with Inout, what exactly does "in" and "out" represent in this context? (Semantically, it represents a unique non-escapable reference with exclusive access to a mutable value, IIUC)

1 Like

I think the metaphor for inout parameters when applied to non-copyable types is that the value is moved out of the binding when passed in, then moved back into the binding on return. With the exclusive access requirement allowing the language to treat this is a mutable reference at all optimization levels.

2 Likes

This is ABI breaking. However, for ABI stable modules a struct can be @frozen or resilient. A @frozen struct can’t change member types anyway and resilient structs are always pass by address like I mentioned. In non-ABI stable modules, this changes how Foo gets passed to bar.

No, I think you misunderstand John’s post here. John is referring to not needing the exclusive ownership access if we just special case MutableSpan (and Inout).

I agree this is confusing. The nice thing about the reborrowing approach discussed in the exclusive ownership proposal that you mentioned is that the most natural spelling is just always consuming Inout<T> (which was also suggested in Difficulty using MutableSpan ) (a great article on what that really is in Rust is here: haibane_tenshi's blog - Obscure Rust: reborrowing is a half-baked feature note &mut u32 is just Inout<UInt32>). Personally speaking, if we had reborrowing support for Inout and MutableSpan, passing these as consuming would become commonplace and could potentially (big if) become the default specifier if you didn’t supply one. E.g.

func foo(with x: Inout<Int>) { // consuming Inout<Int>
  x.value += 1 // ok
}

Rephrasing your answer, I think you’re saying:

  • This is not allowed on an ABI-visible @frozen struct
  • This is allowed on an ABI-visible resilient struct
  • This is allowed/doesn’t matter on structs that are module-internal or less visible
  • There are no other cases to consider

Is that right?

3 Likes