SE-0519: Borrow and Inout types for safe, first-class references

Hi everyone,

The review of SE-0519 "Borrow and Inout types for safe, first-class references" begins now and runs through March 17, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager via the forum messaging feature. When contacting the review manager directly, please keep the proposal link at the top of the message.

Try it out

Development toolchains built from main have these APIs. They need to be used along with the experimental feature named BorrowAndMutateAccessors.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at https://github.com/swiftlang/swift-evolution/blob/main/process.md .

Thanks for contributing to Swift!

Doug Gregor
Review Manager

13 Likes

After the lengthy and varied discussion in the pitch thread, I'm disappointed to see the proposal almost unchanged, and almost none of the talking points of the pitch thread addressed under "alternatives considered".

  • -1 for the non-interaction with the evolution process
  • -1 for failure to address the overlap between this proposal and the borrowing accessors proposal. The two features aren't even close to orthogonal, so we should certainly not have both in the language. One of the two should expand to cover the use-cases for the other.
  • -1 for these are the wrong names, and/or the borrow accessors proposal is using the wrong names; the exact same concept shouldn't be Inout in one and mutate in the other.
  • -1 for these are the wrong names, they should line up with Span/MutableSpan, on which front the ship has already sailed.
10 Likes

I very briefly mentioned this in the pitch thread here: `Borrow` and `Inout` types for safe, first-class references - #24 by Alejandro

Subscripts are weird and can only return a single type. For things like Array, you could theoretically have a single subscript return both a Borrow<Element> and Inout<Element> but like I mentioned in the linked post, you create an ambiguity problem:

struct Array<Element: ~Copyable>: ~Copyable {
  subscript(i: Int) -> Borrow<Element> {
    get { ... }
  }

  subscript(I: Int) -> Inout<Element> {
    mutating get { ... }
  }
}

let a = Array<Atomic<Int>>(...)
let b = a[0] // error: ambiguous use of subscript what type is 'B'?

While the borrow/mutate accessors don’t make it completely obvious from their return type that what’s being returned isn’t an Element, but rather technically Borrow<Element/Inout<Element>, it does resolve this ambiguity issue that Swift has.

This is fair, should the mutate accessor be called inout? Is that confusing? Mutate<T>?

I think lining up with the Span names would be hard unless you meant X and MutableX. Ref and MutableRef? SpanOfOne/MutableSpanOfOne?

1 Like

The types in question depend on borrow and mutate accessors existing as a prerequisite (and the proposal notes this), in order to be able to return the target reference back, but the features are orthogonal otherwise. You can make a Borrow or Inout referring to a value projected through a borrow or mutate accessor, or any other supported set of accessors.

2 Likes

I continue to believe that these types should be named Borrowed<T> and Mutable<T>, as I noted in the pitch thread.

24 Likes

I see this as proof of the non-orthogonality, in that any use of either feature ping-pongs through both.

The accessor issue seems to be the root of the problem; what if we delete the "borrowing accessors" proposal and add to this one something like:

var x: T {
    inout {
        // expected to return an Inout<T>
        // the compiler uses the same implementation but automatically degrades
        // the result to a Borrow<T> when `self` is immutable
    }
}

Then (like Rust's Deref) the compiler could "look through" an Inout<T> or Borrow<T> when referring to members, and implicitly copy out of it if T is copyable, and implicitly convert between Borrow<T> and borrowing T as appropriate, and implicitly decay Inout<T> to Borrow<T> as necessary (Borrow<T> becomes a subtype of Inout<T>, and potentially you could also deal with covariance in the generic, which is missing from the current proposal). And in general you could form an Inout<T> or Borrow<T> as appropriate with the & operator.

(I've used Inout/Borrow in this paragraph for consistency with the proposal, but I think Reference/MutableReference by analogy with Span would be better, or we could consider keywords or sigils at the language level, eg. borrowing T and mutating T would seem to match up with function parameters (we can't use inout T because that means copy-in-copy-out)

That is, why aren't we considering the idea that we should have true, first-class references with language support, instead of a limited simulacrum in a dubious mishmash of language and library, that's clearly missing edge-cases such as casting and subtyping?

6 Likes

For all the same reasons that Int, UnsafePointer, Span, Bool, etc. are all standard library types instead of first-class builtin language types. Span very clearly could’ve been a builtin language type like how it is in Rust &[T].

There are several ways in which the Swift language is tied to specifics of its standard library:

  • Implicit conversion of & expressions to Swift.UnsafeMutablePointer when calling C functions
  • Integer generic parameters must be declared to be of type Swift.Int
  • forin expressions must be used with an expression of type Swift.Sequence
  • for awaitin expressions must be used with an expression of type Swift.AsyncSequence
  • Syntactic sugar for Swift.Optional and Swift.ImplicitlyUnwrappedOptional, including parsing the ?. operator and the implicit default initialization of stored properties to nil

Keith is proposing following this established pattern: inout accessors that must be declared to return an Inout<T>, which the compiler takes care of bridging to borrowed and inout expressions.

2 Likes

Yeah, sorry, I'm not trying to split hairs about language/library, I know they're both blurry lines. And I doubt that the proposed "sometimes Borrow<T> is by-value if T is trivially copyable" behavior can be solely implemented in the library, so these types probably actually live in the language anyway.

I mean more like, why are we limiting the interface and ergonomics of these types to what we can specify as as Swift(+borrowing accessors) API? Why not extend the magical "looking through" the different kinds of property wrappers, dynamicLookup keypaths and accessors that the compiler already does, to looking through these types? Why not use the power that these types convey, to simplify the borrowing accessor proposal? Why are we going straight for a solution tied to details that one person wrote in a vision document 15 months ago, rather than spending time exploring the solution space that is actually available?

@dabrahams and the Hylo team made a radical counterproposal that I won't claim to understand, but I'd like to see it thoroughly debated and rebutted before settling on something that appears to offer a worse developer experience.

This proposal as it stands is a concretion of the "are we Rust yet" meme that's been circulating for the last couple of years, except that it's worse in all ways than Rust's solution to the same problem. Some of those are thorny issues in the differences between Swift and Rust, but still, why settle for second best? And why do I find myself thinking that Rust is a simpler language than Swift?

This is effectively a "forever" choice for Swift. Swift 7 is "who-knows-when", and nobody's signing up for even the degree of backward-incompatibility that Swift 6 brought anyway. And Swift-evolution has a long history of rushing through pragmatic band-aids for specific needs, things that can be implemented quickly in the moment, without sufficient thought (or with explicit disregard for) the long term future of the language. I don't think anyone is well-served by this attitude (except perhaps Apple's WWDC demos). Rust evolution moves much more slowly and much more carefully, and at times that can be frustrating, but I think they have a far lower regret rate than Swift does.

If there's a specific Apple-internal need for this to exist already, underscored features and types can be made without an evolution proposal. But I don't think we're anywhere near having explored the possibility space for references to be betting Swift's future on such an underwhelming pair of proposals right here, right now.

10 Likes

Requiring mutate accessors to return/yield an Inout is just shoehorning Inout into that proposal for no reason. The compiler would have to have special knowledge of Inout in order to drill down to the actual value; it doesn’t actually just magically compose.

7 Likes

I don't buy the "for no reason" though; currently in that proposal the reference is created by compiler magic, where I'd rather have less magic there, and more deeply integrate these types into the language. Also currently the magic dereference behavior is limited to the "accessors" proposal, thereby requiring explicit use of the .value property on these types, whereas I think it should be extended to all values of these types.

I do not buy that these proposals are orthogonal to or in any way independent of each other. There's an exact 1-1 correspondence between borrowing access and Borrow<T>, and between mutating access and Inout<T>. Even without considering major changes to either proposal, you can shuffle the line between them back and forth for subtly different tradeoffs. The act of separating them has introduced an artificial boundary into the solution spaces we're able to explore.

And I think we should be considering major changes to both proposals, either to obtain greater power, greater consistency or lower complexity.

5 Likes

Memory access is inherently compiler magic. Swift has had built-in rules for what it means to access different kinds of storage declaration for 10+ years.

2 Likes

Overall, I think this is a welcome improvement and I’m glad this is being proposed for the language.


From the proposal:

I think it should be noted that the existential inline-storage maximum is 3 words. That means if you create an existential containing a Borrow of a 4-word type, then it will be stored in an out-of-line heap allocation, even though an existential containing a borrow of a 3-word or 5-word type would store it inline. (Assuming, of course, that the 4-word type is bitwise-borrowable and not addressable-for-dependencies.) I think this is important to call out in the proposal (and maybe even the documentation) since it's a narrow edge case that can cause unexpected performance and memory usage problems, especially if a programmer thinks of it like a pointer type.

I also think we should have other versions of Borrow. It would be nice to have a version where you can specify the maximum bitwise-copy size so that people can create types that are friendly with existentials and other size constraints.

It would also be nice to have a version of Borrow where the pointer representation is always used. This would be especially nice in generic cases where the borrowed type will almost always be larger than 4 words and allowing the bitwise copy representation would cause unnecessary branching and code size bloat.

Even if they're not added with this proposal, I think it's worth mentioning other versions of Borrow as a future direction.


I think it's odd that Swift has these pairings:

  • borrowing and mutating (borrowing/mutating func doThing())
  • borrowing and inout (func doThing(_: borrowing/inout Type))
  • borrow and mutate (var property: Type { yielding borrow yielding mutate })

And now this proposal pairs Borrow with Inout. Having all of these similar-but-not-the-same pairings makes the language feel inconsistent and convoluted. I imagine this will be a big source of frustration to people who are trying to learn Swift. Perhaps, instead, we could call them Borrow and MutableBorrow? That would match Span and MutableSpan.


I also don't like the naming of the value property.

  • "Value" is not a very descriptive term. Every instance in Swift has a value. A name like dereferenced, wrapped, or projection would do a much better job of describing what the accessor actually does.

  • It's easy to mistakenly assume the term "value" implies value semantics. Personally, I try to use the term "instance" instead of "value" for things that don't (or might not) have value semantics to avoid confusion.

  • If Borrow and Inout are single-instance analogues to Span and MutableSpan, I think using an empty subscript emphasizes this better (borrow[] vs span[0]).

  • Because "value" is a non-descriptive term, adding it to Unique, Borrow, and Inout would create a language convention that must be learned by programmers. Programmers would have to learn just as much as they would if [] were used for this convention.

value is the worst of both worlds. It's not as short as [], and it's not descriptive like dereferenced, wrapped, or projection.

If we think that dereferencing/unwrapping wrapped objects will be common enough that it's worth making people learn a new thing in order to declutter code, we should use the empty subscript. This could benefit not just types like UnsafePointer, Borrow, Inout, and Unique, but also other types worth adding to Swift in the future, such as an equivalent to C++'s shared_ptr, a copy-on-write wrapper type, and a wrapper type that behaves like indirect. I think it's worth noting that the empty subscript operator has been reserved for this purpose since 2018 at least.

If it's not common enough, then we should use a descriptive name for the operation. This risks the property being repeated a lot throughout some codebases. But, with borrowing and mutating accessors being the preferred way to offer borrowing/mutating access, it could end up not being a big deal.

8 Likes

I'm not sure that nitpicking individual details of my posts without acknowledging the broader strokes of them is helpful. I'm not the language lawyer, I'm the advanced user. I do not expect 100% technical perfection of myself in an off-the-cuff "what-about", "could-we-instead" style forum post. I didn't come to this thread with a full-formed proposal-worthy alternative in mind. But equally, I am consistently immersed in the details of this language, and if you can't explain it to me, I can't explain it to my team, or expect them to understand it independently. I think it's the Swift language team's job to thread this needle, to do the due diligence of exploring the whole solution space, and to explain in a way that a majority of users can understand, why the shape of a particular decision is constrained.

But if we want to discuss "magic", Inout.init(_: borrowing T) seems pretty magical too, in that no user-written Swift code with that signature could replicate what Inout is doing, AFAIK?. Which to me points to Inout itself being the fundamental unit of magic, and therefore there's no need to have both the magic of Inout.init and the magic body of mutate.

(It also suggests to me that spelling Borrow/Inout creation in this way is misleading, and that an explicit operator such as &x or borrow x would more accurately reflect what's going on)

To put my objection to these proposals (and in particular, the idea that this one layers on the accessors) another way, why are we considering proposals that result in these being two different things:

var x1: T { mutate }
var x2: Inout<T> { mutating get }

when conceptually those could be identical? Or at least, one could be sugar for the other? Why can I then write x1.whatever = something but I have to write x2.value.whatever = something? Why do I have to explicitly switch between these representations using Inout(&x1) and x2.value? I understand that "the accessor problem" is real, and unfortunately I don't see a way out without a new accessor, but I think that new accessor could unify these two as much as possible, such that both the bodies and the usages of these two different variables are identical.

12 Likes

Keith's opinion is overall understandable, and I can respect his position.

I believe the view that Rust feels simpler is related to the fundamental differences in the design philosophy of Rust and Swift.

In Rust, the default is move (non-copyable), and all operations are added generically on top of that foundation.

On the other hand, Swift's defaults are quite rich. Not only are values copyable by default, but if you create a class, you get reference counting for free. In Rust, you would have to manually add things like Arc in many cases.

The approach to properties and accessors also differs. In Rust, I feel that memory operations are expressed straightforwardly. Swift, however, expresses higher-level semantic operations.

For example, in Swift, you can pass a computed property as an inout argument to a function. This feels like a magical feature, but it's possible because the compiler automatically creates and inserts a temporary value.

Furthermore, in Swift, function effects are separated from types.

"throws" and the "Result" type are distinct entities. It's not a case of it being "represented as a Result type under the hood." This is because features like throws involve flow control enforcement, providing capabilities that go beyond what a type alone can do.

The same applies to "async." In some languages, async/await is tied to a Promise or Future type, but in Swift, there is no corresponding monad type.

When looking at Swift as a whole, the relationship between the new "borrowing" accessors and the "Borrow" type seems like a very "Swift-like" style that is consistent with its previous design patterns.

Swift's specifications are not simple; they are complex. However, in exchange, they are designed to be easy to use and to enhance productivity.

2 Likes

The Inout initializer is actually Inout.init(_: inout T) which at the machine level just passes a pointer to T that Inout grabs and stores in itself. It’s really not super magical at all.

Speaking personally, I view the need to write .value to dereference Borrow/Inout a temporary limitation. I would much rather prefer magic here that implicitly dereferences these types to access the underlying type’s members. In such a world you do get x2.whatever = something which would be identical.

Simply put these preexisting features are not good enough and have a ridiculous amount of overhead if not optimized. Dynamic member lookup cannot find function members of types, only property and subscript accesses. None of those also support operators involving the underlying type. My personal view is that the compiler will eventually get the magic to automatically look for .value (or some special name) on Borrow/Inout to implicitly dereference them or perhaps they themselves can conform to a Deref esque protocol which would allow types like UniqueBox to also participate in.

2 Likes

It's an unfortunate mismatch. The behavior of Borrow's layout is driven by the existing calling convention ABI, which passes values smaller than four Ints in registers. Since Borrow uses the value representation in exactly the situations where its target type is passed in registers across function calls, this ensures that a function can receive a parameter and return a Borrow referencing that parameter without needing to allocate a local temporary, since that temporary allocation would then artificially limit the Borrow's lifetime.

The proposal does mention this possibility, or at least the possibility of an always-pointer Ref type. Any alternative type that doesn't follow the calling convention will however have problems with temporary lifetimes in some situations.

We chose this name in an attempt to align the API shape with UniqueBox, which is also currently undergoing review. I agree that it's not the greatest name, though I do think there is some value in keeping a common shape to these wrapper-like types that primarily represent a single value (particularly if we do pursue a Deref-like feature in the future, as Alex noted above).

3 Likes

I think we can explore doing this in limited ways, like when accessing a member or calling a method, but I wouldn’t want to go much further than that. I think it’s very good that you have to be more explicit about touching the underlying memory. I certainly wouldn’t want a world in which

aVarOfTypeInout = foo()

could either overwrite the reference or the referenced value depending on how it type-checked, or similarly where

foo = aParamOfTypeBorrow

could either copy the reference or the referenced value.

3 Likes

From experience with Rust, you need to explicitly dereference both of those use cases:

let x: &mut isize = ...;

*x = foo();

foo = *x;

But yeah like you mentioned finding the balance of where this implicit dereferencing can be used and where requiring explicitness from the user clearly expresses intent.

3 Likes