[Pitch] Borrowing Accessors

I just put up the draft proposal for Borrowing Accessors, which together with Yielding Accessors completes the full suite of accessor support described in the Prospective Vision for Accessors. More complete details are in the draft proposal. The following summarizes the goal of this ongoing effort:

Conceptually, borrowing accessors return a “borrow” of a value, providing either mutable or immutable access to some stored value. Swift typically implements “borrow” similarly to a C++ reference or C pointer.

As detailed in the Prospective Vision, this gives you a suite of approaches for implementing property access. For example, considering just immutable (read-only) access, you will now have these options:

Get (since Swift 1.0)

struct S {
  var foo : Foo {
    get {
      // ... return a `Foo` instance
    }
}

A plain get allows you to return an instance of the declared type. This instance can be a copy of a stored value or can be freshly constructed. In particular, this accessor can construct and return a non-copyable value, but cannot provide access to a non-copyable value stored in memory.

Borrow (this pitch)

struct S {
  var foo : Foo {
    borrow {
      // ... return an existing `Foo` instance
    }
  }
}

A borrow accessor returns a “borrow” of a value that must outlive the execution of the accessor. Borrows can efficiently provide access to existing values, even if they are large or non-copyable. The compiler will verify that callers of this accessor do not extend their use of the borrow inappropriately. (For example, the compiler will not allow the caller to modify the containing object while they are using the borrowed property value.)

Yielding Borrow (SE-0474)

struct S {
  var foo : Foo {
    yielding borrow {
      // ... yield access to a value
      // ... after the caller is done, resume here
    }
  }
}

A yielding borrow uses coroutine semantics to “yield” the value to the caller, suspending the execution of the accessor until it is continued by the caller. This allows the provider to not only do work before yielding the value, but to also do work after the caller is finished using the value. Swift’s standard library uses an early version of this for its Dictionary subscript and will likely adopt the standard form as soon as the implementation is complete. This can also be used to set and clear markers for dynamically identifying access conflicts.

For callers, the distinction between different accessor types is mostly invisible. The caller just reads or writes the value. The only time this might impact the caller is if they try to simultaneously access multiple properties on a single value at the same time.

For providers, these three options provide a trade-off that can impact how you design your types and protocols. At the risk of over-simplifying:

  • get is the simplest,
  • borrow is the most efficient, and
  • yielding borrow is the most flexible.

Of course, there are also mutating counterparts to each of the above: set, mutate, and yielding mutate.

23 Likes

The examples show that at least some stored properties can be returned from borrow accessors, implying that those stored properties are guaranteed to be directly stored in memory. But it currently isn't guaranteed that all stored properties are directly stored in memory. The documentation of MemoryLayout.offset(of:) gives more specific details:

I always thought of those specific details as being implementation details instead of language guarantees. In particular, it isn't explained when a stored property is stored in a packed bitfield, and closure reabstraction is only explained in compiler-internal and ABI documentation and doesn't otherwise affect the user-facing semantics of the language.

Personally, I think it would be better to not expose those details in user-facing semantics, such as what stored properties may be returned from borrow accessors. For example, the details of closure reabstraction would probably be confusing or seem unpredictable. Instead, maybe there could be an attribute or property wrapper to guarantee that a particular stored property is directly stored in memory.

2 Likes

Borrows in Swift are not guaranteed to point at memory either unlike other languages. There’s a notion of “bitwise borrowable” in Swift that says a borrow can be the value itself rather than a reference thereof, so in most cases returning a stored property from a borrow accessor will most likely just return the value itself.

2 Likes

I’ve tried to clarify my summary above to refer to “stored values” rather than “values in memory.” The point here is that the value returned from a borrowing accessor must outlive the execution of the accessor itself. That doesn’t necessarily mean it is physically in memory.

2 Likes

The borrowing get accessor (used by span and bytes properties) isn't mentioned in the prospective vision document. Please could someone explain if borrowing get is another kind of "borrowing accessor", and which proposal added it to the language?

borrowing get is just a get accessor where the self value is borrowed for the duration of the getter function call. After the call completes, the returned value is effectively a copy of the property, just as with any other get access.

A borrow accessor returns a borrow of the accessed value, which is not a copy and requires that the provider guarantee the value will remain valid after the accessor function has completed.

Yes, this does mean you can have a borrowing borrow which has both of these behaviors: The providing value itself is borrowed (appropriate when the containing value is non-copyable) and the property value is returned via borrow (which is appropriate when the property value is non-copyable).

I believe the use of borrowing as a modifier for methods, accessors, and function parameters was added in SE-0377.

Edit: I suppose the above might be a good reason to change the title of this proposal to “Borrow Accessors” rather than “Borrowing Accessors”

2 Likes

@meg-gupta Is a borrow operator always borrowing? I presume it’s borrowing by default and I’m pretty sure it cannot be consuming, but haven’t yet convinced myself that no other ownership modifier is feasible.

One thing I am sometimes confused about with the existing borrowing/inout SomeNonCopyableType parameter bindings, which is now pushed further by these accessors: How are we intended to think about the “types” returned by a borrow/mutate accessor (I guess same question for the type yielded by the coroutine ones). I think this question is heavily related to the pitch from here Pitch: `borrow` and `inout` declaration keywords .

Right now in the language working with accessors returning borrowed values can require either painful ergonomics or inefficient code that relies on repeated expression elimination which wouldn’t be possible across resiliency boundaries. The common case is if I want to do more than one thing with a borrowed value without re-creating the borrow.

For example:

struct Node: ~Copyable {
    ...
}
struct Tree: ~Copyable {

  var leaf: Node {
    borrow {
      // Do some sort of traversal to find first leaf and return a borrow of it.
    }
  }
}

func leafUseOne(_ node: borrowing Node) { ... }
func leafUseTwo(_ node: borrowing Node) { ... }

func main() {
  var tree: Tree = ...

  // I want to feed the first leaf to `leafUseOne`, then to `leafUseTwo`
  // Today I have to re-write:
  leafUseOne(tree.leaf)
  leafUseTwo(tree.leaf)

  // Or I could do this which is more efficient but a bit ergonomically painful
  { (leaf: borrowing Node) -> Void in
    leafUseOne(leaf)
    leafUseTwo(leaf)
  }(tree.leaf)
}

Maybe the above example is contrived, and you could argue something that has to do a traversal shouldn’t be a property in the first place, but anyway… Since there is no way to store a borrow in a local variable, I either have to invoke the accessor twice, or to guarantee it doesn’t redo the work, I’d have to define a separate closure to do it (I’ve been working with noncopyable types a good amount recently and am having to do quite a bit of this ‘closure strategy’ which hurts readability).

Is there any potential for including a future direction where there’s some unification around what a `borrowing T` as parameter, and borrow accessor return value, represent as a type? Will these things be merged into some concrete type in the future that can be assigned to a local variable? Or is the answer that borrowing T itself is a type, and that is the type returned by a borrow accessor, but just not yet supported to bind to a local variable? For example will I eventually be able to write let leaf: borrowing Node = tree.leaf. I think this question is important especially as we evaluate the usability of these accessors across module boundaries.

Also further question thinking along the same lines, if there is an eventual type borrowing T, then would var x: T { borrow } actually just be a shorthand for var x: borrowing T { get }? Regardless of being able to assign to a local variable, the future directions already mentions being able to return a borrowing T from a function, so there’s a difference where for a function you change it to return a borrowing T but for properties you don’t change the return type, but rather the accessor type.

3 Likes

It is not. borrowing ownership modifier imposes additional semantics when self is Copyable and is not included by default.

FWIW, Swift doesn’t currently use bitfields as part of its struct layout algorithm.

It certainly does, perhaps surprisingly so, because it can change the space complexity of algorithms. See Value wrapped in a thunk recursively(?) for example.

1 Like

Would it make sense to make borrowing the default? Accessors returning non copyable types already require borrowing. The difference would be that accessors returning non copyable types would use borrowing by default. Then, to retain current semantics, the compiler would insert a copy if needed. Essentially, we’d change when the copying occurs: instead of happening inside the accessor code, it would occur at the call site (if required).

The main benefit of this approach is that it lessens the divide between copyable and non-copyable types. That’s because “copying get” would remain the default choice for copyable types, but then non-copyable types would have to use the new “borrowing” modifier. In my opinion, there isn’t a big enough semantic difference to justify this syntactic difference this proposal creates. Granted, it’s mostly advanced programmers who will write their own efficient non-copyable types. Regardless, the confusion remains for the users of said efficient API who might not be as advanced.

The biggest issue I foresee with implicit borrows is that ABI-compatible libraries would need to generate copying get thunks. This brings me to my final point: performance.

My naive guess is that code across optimization boundaries (e.g. a dynamic library and its client) might slightly benefit from eliminating a copy where a borrow would suffice. On the other hand, generating thunks for dynamic libraries might add to code size. Also, if the accessor body already generates an owned value, exposing that as a borrow, and then copying again at the call site actually results in an extra copy. Ultimately, whether implicit borrows generally eliminate or add copies depends on if most accessors expose a property owned by the underlying type (borrow eliminates a copy) or compute/create the return value when called (borrow adds a copy).

To be clear, I think “copying get” should still be allowed; just not remain the default. Please let me know if this makes sense or was already discussed!

Best, Filip

No, I don’t think we can change the default behavior for accessors. There are two problems:

ABI. Swift is ABI-stable (at least on Apple platforms) which places limits on our ability to change how existing source code compiles. Sometimes, we can work around this by generating both old-style and new-style code, but not always.

Exclusivity. Borrowing and non-borrowing accessors have different exclusivity behaviors. For example:

var container = ...

func f(_ value: SomeType) {
  container.modify()
}

f(container.value)

If container.value copies the property, then the mutation of container inside of f is legal. If container.value borrows the property, then container cannot be modified for the duration of that borrow (which is the duration of the function call in this example).

Because exclusivity can sometimes only be verified at run time, changing the default for .value from copying (get) to borrowing (borrow) can cause previously working code to now crash at runtime.

1 Like

I would like to see a version of the borrow accessor where its contents are guaranteed to be in memory. Currently, there is no proper way to re-vend the result of the unsafeAddress and unsafeMutableAddress accessors used by Unsafe[Mutable][Raw][Buffer]Pointer[_:], InlineArray[_:], MutableSpan[_:], and OutputSpan[_:].

@scanon may be able to comment here. I believe the standard library developers are planning to adopt borrow accessors as a replacement for unsafeAddress and unsafeMutableAddress, gated of course by API/ABI compatibility concerns that we’ll have to work through.