Ownership Annotations

Hello Swift Evolution,

Over the course of summer of 2017, I was tasked with exploring the implementation of ownership annotations for the Swift language. The goal at the time was to release this proposal at the end of my time and un-underscore the attributes after we had completed discussion of the proposal. But, plans changed - this time very much for the better. @Michael_Gottesman and @huon have been pushing an experimental mode for changing the ownership semantics of the default calling conventions of the entire language. I have revised this proposal to adopt a more neutral stance with respect to that work and am releasing it now.

As usual, a gist is available with the draft as it stands: ownership-annotations.md ¡ GitHub

And that draft is available inline and online:

Ownership Annotations

During the review process, add the following fields as needed:

Introduction

This proposal adds the borrow† type annotation. With it, Swift users will be able to opt in to
value-type and ARC optimizations safely, easily, and reliably. To complement borrow, we are also proposing the
consume† type annotation and the consuming† member function attribute that signal a parameter will be
passed with the existing parameter-passing convention.

As it stands, Swift has one user-facing parameter passing convention: owned. Under the rules of this
convention, a function's arguments must be retained or copied before being passed. It is then the
responsibility of that function to balance the +1 reference count or destroy the copied value. Naturally,
if this is done for the arguments of every function, a large amount of ARC traffic and copies are generated
for values that, more often than not, have lifetimes that extend through the function call anyway.

Though the optimizer is capable of detecting and eliding some of this unnecessary ownership balancing,
it cannot catch every case, or worse, may make the decision not to perform the optimization at all after
a change has occured. Providing these ownership annotations gives Swift users deterministic performance
guarantees that are safe and easy to adopt wherever they see fit.

This proposal is a core part of the Ownership feature described in the
ownership manifesto.

Motivation

Shared

The semantics of borrow parameters first appeared in languages like Mezzo
and Rust where they are used in conjunction with
type system extensions and ownership systems to check that an aliasable reference
is read-only. By providing this kind of control over ownership, a class of data races
on these values is provably impossible and an avenue for performance wins immediately opens up.

Swift already provides a number of optimizations for function parameters involving value
types. Among these, the compiler is capable of eliding a copy by taking an immutable
reference to the memory associated with a value - the convention currently in use for
the self parameter of non-mutating functions. Providing a borrow type annotation
allows users a way to explicitly request copy elision for particular parameters. In many
cases, providing a marked speedup for function calls and a strong hint to the optimizer
that helps it streamline the body of functions themselves.

For reference types, borrow provides a guarantee that a reference will survive for the
duration of a call. Additionally, because the reference is immutable for that period,
and with help of the Law of Exclusivity, we have a guarantee that the reference will not be mutated out from under us. Using this,
we can elide a retain-release pair for the argument that would otherwise be necessary to
guarantee its lifetime, providing an overall reduction in ARC traffic.

Owned

For the times when borrow access to a value would be inappropriate, or when ownership of a value
should be an explicit part of the interface, we provide the consume annotation on parameter types
and the consuming attribute on function declarations. consume function parameters are
"consumed" by that function; the existing default for function parameters. For value types, this means
the function must recieve a copy and is responsible for destroying that copy when it exits. For reference
types, the function must recieve the value +1 and is responsible for decrementing the reference count at
exit.

The consuming attribute on member functions indicates that the function accepts the self parameter using this
consume convention. In the future, for moveonly types, this can be used to provide behaviors like guaranteed
resource destruction for value types.

Proposed solution

The compiler shall be modified to accept the borrow and consume parameter type annotations and the
consuming member function attribute.

Detailed design

Grammatical Changes

The Swift language will be amended to support the addition of the borrow and consume type annotations.

GRAMMAR OF A TYPE ANNOTATION

type-annotation → : attributes(opt) inout(opt) type
+type-annotation → : attributes(opt) borrow(opt) type
+type-annotation → : attributes(opt) consume(opt) type

In addition, the grammar of function declarations will be amended to support the consuming attribute.

-mutation-modifier → mutating | nonmutating
+self-ownership-convention → mutating | nonmutating | consuming

Semantic Changes

From a semantic perspective, parameters marked borrow behave similarly to existing consume
parameters and are mostly transparent to type checking. For this reason, and to encourage
users to easily experiment with the addition and removal of borrow from parameter types,
we do not allow overloading on the presence or absence of the borrow attribute. This also
means that protocol requirements cannot be satisfied by functions that mix borrow and consume
versions of parameters that would otherwise have the same type.

Similarly, functions marked consuming behave like existing nonmutating functions but
may not be mixed in protocol requirements. For the same reason it is also not a vector
for overloading.

In the interest of progressive disclosure, if no parameter convention is specified, a
compiler-provided default will be selected - currently the owned convention, corresponding to
the consume annotation. Unannotated witnesses to a protocol requirement that specifies ownership
shall inherit the ownership annotations of the requirement. In short, a protocol vendor is free
to change ownership annotations without fear of an API-breaking change for clients that did not
explicitly opt-in to working with ownership annotations.

SIL

Parameters marked borrow will be lowered to the @guaranteed calling convention. The
mangling will be updated to account for the borrow attribute in function types.

Source compatibility

No change. This proposal is purely additive.

Effect on ABI stability

The addition of the borrow type annotation to function type signatures
necesarily means it needs a place in the function type mangling. The consume
attribute, being the existing default, has no effect.

Effect on API resilience

Adding and removing borrow on a parameter type, like adding and removing
inout on a parameter type, is not an ABI-compatible change. Unlike
adding and removing inout, in most cases it is an API-compatible change.

Replacing a nonmutating function by an equivalent consuming function is similarly
an API compatible change unless it changes a protocol requirement.

Future directions

Ownership annotations are a critical part of the future goals of the Ownership Manifesto. In particular, the
need for a non-owning ownership annotation is required to correctly model non-consuming
interactions with moveonly types and contexts. They also form a crucial part of a
potentially more efficient semantics for local variables, coroutines, and iterators. See
the manifesto for further details.

Alternatives considered

Continuing to let the optimizer transparently make this decision for users instead
of giving them tools to control ownership.

Naming†

To concretize discussion of names, some alternatives are provided below. The end goal is
a name the accurately conveys the semantics of each of the annotations, while also maintaining
consistency with the existing modifiers (mutating and nonmutating) and type annotations (inout).

borrow consuming consume
share[d] owning own[ed]
ref taking take
byref in transfer

Mixing Ownership In Protocol Witnesses

The compiler is capable of automatically forming the thunks necessary to allow mixing of ownership conventions between
protocol requirements and their witnesses. This means that the restriction on ownership-annotation mismatches
could be lifted. However, we believe that this would be detrimental in a number of ways:

  • Though the user's annotation specifies their expectation of the ownership-convention of a given parameter, a
    protocol with explicit ownership annotations indicates the API vendor expects a certain kind of usage to follow
    naturally from its definition. Allowing user overrides dilutes that expectation.
  • Mixing ownership annotations introduces thunking - thunking is not free. If a requirement is annotated borrow and
    its witness annotated consume the compiler will emit a thunk between these conventions that copies anyways. In addition
    to the overhead of the copy, if you are in a non-inlineable context, you also incur the cost of the thunk.
  • Mixing ownership annotations will cause headaches in a future world where moveonly types play a role. If a witness
    that mixes ownership annotations comes under the scope of a moveonly context, an API and ABI-breaking change is
    required to correct it. If we allow for this transparently, it would enable users to void the consumption contract
    with moveonly types accidentally.
24 Likes

Thanks for updating your proposal!

I'm concerned that this naming leads to some confusion surrounding the difference between the ownership parameter convention and a borrowed variable (in the future).

The ownership convention applies to the callee-side argument handling. If the argument also happens to be a move-only type, then that forces the caller to either move the argument (owned) or borrow the argument (guaranteed), and that can be implicit.

However, the current implementation of the owned/guaranteed neither moves nor borrows the argument, and I don't expect that to change for non-move-only types. So, there needs to be an explicit way do this:

func consumesArg(x: Any) {
  MyClass.staticProperty = x
}

func usesArg(y: Any) -> Any {
  yield y
}

var z: Any

consumesArg(move(z))

usesArg(borrow(z))

So, what you're calling a "borrowed" argument convention is required for borrowing an argument, but does not imply that the argument is borrowed. People need to understand the difference, so we shouldn't use the same name for both concepts.

I also do not think we should surface the "guaranteed" name to the language, that's even more confusing. We should simply have an owned/unowned convention. Since unowned is typically the default, you would almost never use it.

1 Like

Some thoughts on naming:

I think the ephemerals , as the ownership manifesto described inout-like type modifiers -- They are not themselves type-annotations, they are optional components of a type-annotation expression -- should be adjectives, because an ephemeral of a type is still almost a type, and thus should remain a noun: "_ is an inout Int", "_ is a borrowed T" or "_ is a shared String" and "_ is a consumed instance of Self" or "_ is an owned T" or "_ is a transferred File"

Also, I think the self-ownership-conventions should be gerunds, but most of the name suggestions for those are gerunds already.

Finally, I think "ref", "byref", "in" - in the context of self-ownership-conventions, and, depending on interpretation, even "take" are too terse and implementation specific to be swifty.

If I have the following:

// Library
public protocol P {
    func foo()
}

// Client
struct S: P {
    public func foo() {}
    public func bar(foobar: Int) {}
}

And later the library adds a default implemented protocol requirement for bar(), which already exists in struct S of the client:

// Library
public protocol P {
    func foo()
    func bar(foobar: borrow Int) // New requirement with default implementation
}

extension P {
    public func bar(foobar: borrow Int) {}
}

If I understand correctly this means that the ABI of bar() in the client will now be silently changed from foobar being consumed to being borrowed (assuming for now that the default is 'consume').

The proposal could use some example code with a small working example, not just interfaces.

9 Likes

Do you mean a first-class borrowed type more like a Rust shared reference?

What do you mean by this? Presumably even for non-moveonly types it also changes the caller's pattern of retain/release too. Also, I assume by implicit you mean implicit at the source level, not as a SILGen/IRGen implicit thunk-ing thing, because it has a semantic effect?

In any case, I think the link to moveonly types suggests the naming isn't incorrect per se: a borrow Moveonly has to act the same as a full shared reference, and a borrow Copyable is semantically indistinguishable from a owned Copyable, if Swift is continues to be aggressive about being able to materialize things, and about keeping memory locations (of non-class things) unstable and out of the language model. If Swift was to get first-class reference types (i.e. equivalents to Rust's &mut and &), then writing them as inout T and borrow T, e.g. let x: borrow T = arrayOfT[0] doesn't seem like the worst idea (there's various issues I can foresee with this, but it also seems like the natural way to generalise ownership from function boundaries to everywhere).

Do you mean a first-class borrowed type more like a Rust shared reference?

Well, yes, but that isn't central to my argument. Let's just focus on function boundaries for now.

The ownership convention applies to the callee-side argument handling. If the argument also happens to be a move-only type, then that forces the caller to either move the argument (owned) or borrow the argument (guaranteed), and that can be implicit.

What do you mean by this? Presumably even for non-moveonly types it also changes the caller’s pattern of retain/release too.

Sure, as an implementation detail.

Also, I assume by implicit you mean implicit at the source level, not as a SILGen/IRGen implicit thunk-ing thing, because it has a semantic effect?

I mean implicit at the source level. You probably don't want to spell out "move" or "borrow" if you already constrained to be MoveOnly.

It should be possible to write code that avoids semantic copies, regardless of whether the type is Copyable.

func consumesArg(x: owned Any)
func usesArg(y: unowned Any)

I should be able to move a Copyable value:

consumesArg(move(copyableThing))

Or I should be able to pass an immutable reference to the Copyable value:

usesArg(borrow(copyableThing))

There is a semantic difference, and, yes, I think the difference should be surfaced in the language model.

func doSomethingWith<T>(_ arg: unowned T, f: () ->()) {
  f()
  print(arg)
}

func foo<T>(t: T, u: T) {
  var z: T = t
  doSomethingWith(z) { z = u }
}

Prints 't' of course, regardless of whether arg's convention is "owned" or "unowned". This will continue to be legal because the "unowned" convention does not change the semantics of argument passing. i.e. it's not actually a "borrow" for any reasonable interpretation of the term. For that reason, I think calling the "unowned" convention a "borrow" is misleading.

Furthermore, in generic code where the semantic copy is not intended, I would like to be able to borrow. Mistakes like this will be caught by the compiler:

func foo<T>(t: T, u: T) {
  var z: T = t
  doSomethingWith(borrow(z)) { z = u }
}

I don't this restriction was actually mentioned above? But I'll assume it's something like

Ownership annotations on witnesses satisfying protocol requirements must match the annotations on the protocol declaration, e.g.

protocol Foo {
    associatedtype T
    consuming func f() 
    func g(x: consume T, y: borrow T)
}
struct Bar: Foo {
    func f() {} // error: missing 'consuming'
    func g(x: borrow T, y: consume T) {} // error: 'x' should be 'consume', 'y' should be 'borrow'
}

Sound reasonable?

The proposal doesn't actually say if a witness can be annotated. In fact, I don't actually think there's a way to get to make this API-compatible with the annotations-must-match restriction:

  • if they can be annotated, then the protocol cannot have its annotations changed in a API-compatible way, as type that conforms may have annotations matching the old declaration, but these won't match (and will violate the restriction)
  • if they can't be annotated, adding even a defaulted requirement may break code along the lines of @orobio's example, where there's a (previously) unrelated method that has annotations: adding the requirement makes this method a witness, but it has annotations!

This, along with not wanting to silently change behaviour as @orobio's example demonstrates, pushes me more towards no restriction. It seems feasible to have tooling (or a compiler flag/Xcode setting) that will flag mismatches between witnesses and protocol requirements on an opt-in basis.

The abstract idea of a protocol may not match all concrete implementations of it, because they may be more specialised or have "surprising" trade-offs (e.g. optimise one function to be O(1) at the cost of another becoming O(n2)). For instance, imagine a Bag protocol (ala Collection):

protocol Bag {
    associatedtype Element
    // presumably 'add' will usually store the value somewhere
    func add(_ value: consume Element)
    ...
}

extension Set: Bag {
    // 'add' does store the value into memory somewhere, and so 
    // benefits from 'consume'
    func add(_ value: consume Element) { ... }
    ...
}
extension Array: Bag {
    // Same as 'Set'
    func add(_ value: consume Element) { ... }
    ...
}

struct StringTrie: Bag {
    typealias Element = String
    // this is a trie, and so never actually stores 'value': it is
    // broken into parts  so there's no point in consuming the value
    func add(_ value: borrow String) { ... }
    ...
}

Concrete users of StringTrie avoid retains and releases, and generic users pay "only" the cost of the extra function call of the thunk, since consume->borrow is cheap. I think the protocol has the correct annotation here, as it is optimising for the most common case (and, in a moveonly world, maintaining maximum generality of what types can conform to it), but the concrete type has extra constraints that allow it to relax the requirements.

In a world where distinctions between inout, consume and borrow are enforced strictly there's a hierarchy: an owned value (consume) can always have a mutable pointer (inout) created to it for free and a mutable pointer can always be downgraded to an immutable one (borrow) for free . This implies that concerns about the performance of copying don't apply to satisfying a consume requirement with an inout witness (definitely a bit weird, but it's theoretically okay), an inout requirement with a borrow witness (i.e. protocol is giving the option for mutation, but the concrete implementation doesn't need it) and, transitively, a consume requirement with a borrow one.

Of course, the cost of the thunk call itself could be problematic enough (although these very small thunks seem like they'd be pretty much just the cost of a call to a function in another dylib?).

I don't think this is a particular concern: it will always be statically known if a type could be a moveonly type (due to source compatibility, there can't be a way to retroactively make a type moveonly, or instantiate a generic parameter with a moveonly type unless the parameter is marked as "possibly moveonly" in some form), and so statically known when ownership annotations must match. I don't think there's much value in maintaining consistency with behaviour with copyable types, because this doesn't seem particularly different to, say, func f(_: consume T) {} func g(x: borrow T) { f(x) } (that's also a mismatch in borrow/consume: that works with copyable T and can't if it's moveonly), and there's numerous other ways in which moveonly types different in behaviour (e.g. let t = (x, x)).

(This changed to borrow very recently: Plan to change the convention of passed "Normal Parameters" from +1 to +0 - #9 by Michael_Gottesman)

2 Likes

Isn't what you said about the callee just the matching side of this implementation detail? I.e. borrow and consume are not semantically relevant at all, and the only framing in which they're currently useful is the implementation details of where copies/destroys occur, both callee-side and caller-side? But, this isn't the main point...

Aha, that explains it nicely. Thanks.

I'm mostly convinced, the only remaining quibble I have about not calling it a borrow is that it will be a borrow for move-only types. However, having mulled on it all weekend, it's probably right to choose "unowned arguments are implicit borrows for move-only types" over "borrow arguments aren't a semantic borrow for copyable types", since the latter is likely to be (far) more common and the former is fairly obvious when you call the function.

I think that's well put.

I haven't had time to catch up on the discussion, sorry. Some things that stand out about the proposal.

  • You use shared as the section header where you describe borrow and owned as the section header where you describe consume.
  • I'm not sure I'm happy about the active/imperative borrow and consume instead of the more declarative/descriptive borrowed and consumed.
  • I'm not sure I'm happy about consume(d) instead of owned. It is more descriptive of the convention, I suppose.
  • For the purposes of optimization and/or move-only types, we will need some way to declare that reading a property produces an owned value instead of a borrowed one. (Essentially, that the generic access pattern for reading should be a getter instead of a read coroutine.) That doesn't need to be part of this proposal, I think, but it might be something we should keep in mind for naming the conventions.
  • The default convention is no longer the owned convention.
  • Pedantically, the revised grammar of type annotations is ambiguous. You should change inout(opt) to parameter-ownership-convention(opt) and then make parameter-ownership-convention ::= inout | borrow | consume.
  • Replacing a nonmutating method with a consuming method is an ABI-incompatible change, which follows from the general rule but probably ought to be restated.
  • I don't think I agree with anything in the mixing-ownership-in-protocol-witnesses section. We want libraries to be able to mark things with attributes without breaking source compatibility (or just generally making things more difficult) for clients adopting the protocol for copyable types. You could argue that we shouldn't allow explicit mis-matches, but that's it.
2 Likes

These are all good notes. I think the broad strokes of the replies I've gotten indicate that I should refocus the proposal to look towards a future with moveonly types and away from muddling about with the semantics of copyable types. The "stable performance" angle is looking frail these days with +0-all-args in master.

1 Like