← All lessonsGuide · Ownership, in full
Guide

Ownership, in full

~Copyable, borrowing, inout, consuming, consume, copy, deinit and discard self — and what the compiler does underneath

Every value in Swift has an owner, and every time you pass a value somewhere the language decides what happens to ownership: is it copied, lent, or handed over? For most of Swift's history you never had to say — the compiler copied whatever it needed. Swift 5.9 let you say, and Swift 6 built on it: a type can refuse to be copied (~Copyable), a parameter can borrow or take its argument (borrowing, consuming), and a noncopyable struct can have a deinit. Lesson 40 meets all of it in one type, Swift 6.4's Continuation. This guide goes through each piece — what it means, what the compiler checks, and what the compiler makes of it in SIL — with every piece of code compiled and run on the iOS 27 simulator.

1 · from lesson 40

One owner: ~Copyable

Every type you have written so far is Copyable: the compiler adds that conformance to every struct, enum and class without being asked, and assigning or passing a value copies it (a class's reference, a struct's fields). Writing ~Copyable takes the conformance away. The value then has exactly one owner at a time:

A type that cannot be copied
struct Ticket: ~Copyable {
    let seat: String
}
Give it to a second variable, then use the first
let a = Ticket(seat: "12A")
let b = a
print(b.seat)
print(a.seat)

It does not compile

'a' used after consume
note: consumed here
note: used here

let b = a did not copy the ticket; it moved it, and b owns it now. The two notes say exactly that: let b = a is where a was consumed, and the error is the use after it. A move is always allowed; using what was moved from is not. Asking for a copy outright is refused too:

copy, on a noncopyable value
let a = Ticket(seat: "12A")
let b = copy a
print(b.seat)

It does not compile

'copy' cannot be applied to noncopyable types

Those two errors come from different parts of the compiler: the second is about a type, the first about what happens to a value over time. Inside the compiler, at the end of this guide, shows which part reports each — and how the second one works.

2 · from lesson 40

Three ways to hand a value to a function

With a copyable type the compiler picks for you. With a noncopyable one it makes you choose, because the choice decides whether you still have the value afterwards. A parameter without a choice is an error:

A noncopyable parameter, with no ownership said
func show(_ ticket: Ticket) {
    print(ticket.seat)
}

It does not compile

parameter of noncopyable type 'Ticket' must specify ownership
note: add 'borrowing' for an immutable reference
note: add 'inout' for a mutable reference
note: add 'consuming' to take the value from the caller

Its three notes are the three choices — the whole of ownership, in one table:

ModifierThe function…The caller afterwardsIn SIL (below)
borrowingreads the value for the length of the callstill has it, unchanged@guaranteed
inoutgets exclusive access, may change it, gives it backhas it, perhaps changed@inout
consumingtakes it for goodno longer has it@owned

All three on one ticket:

Borrow, change in place, and take
struct Ticket: ~Copyable {
    var seat: String
}

func look(at ticket: borrowing Ticket) {
    print("looked at", ticket.seat)
}

func upgrade(_ ticket: inout Ticket) {
    ticket.seat = "1A"
}

func redeem(_ ticket: consuming Ticket) {
    print("redeemed", ticket.seat)
}
Used in that order
var ticket = Ticket(seat: "12A")
look(at: ticket)
look(at: ticket)
upgrade(&ticket)
look(at: ticket)
redeem(ticket)

It prints

Console
looked at 12A
looked at 12A
looked at 1A
redeemed 1A

Borrowed twice, changed in place, borrowed again, then taken. After redeem(ticket) the variable is spent; a further look(at: ticket) would be "used after consume". Methods work the same way through self: a plain func borrows self, a mutating func takes it inout, and a consuming func takes it for good.

Borrowing is how a noncopyable type can still be Equatable. The standard library declares the protocol like this:

From the iOS 27 SDK's Swift interface — not code to type
public protocol Equatable : ~Copyable, ~Escapable {
  static func == (lhs: borrowing Self, rhs: borrowing Self) -> Bool
}

== borrows both sides, so comparing two values leaves both where they were:

A noncopyable type that is Equatable
struct Pass: ~Copyable, Equatable {
    let seat: String

    static func == (lhs: borrowing Pass, rhs: borrowing Pass) -> Bool {
        lhs.seat == rhs.seat
    }
}
Compared, then used again
let a = Pass(seat: "12A")
let b = Pass(seat: "12A")
print("a == b:", a == b)
print("still have both:", a.seat, b.seat)

It prints

Console
a == b: true
still have both: 12A 12A
3 · from lesson 40

What they are underneath: SIL's conventions

The three modifiers are not new ideas to the compiler — they are the conventions it has always used for passing values, now with names you can write. In SIL, the compiler's intermediate language, borrowing is @guaranteed, inout is @inout and consuming is @owned. And with an ordinary copyable type you can watch the compiler decide when a copy is needed. One struct holding a class reference, given to a borrowing function, to a consuming one while it is still needed, and to a consuming one with consume:

The same value, three ways
final class Box {}

struct Holder {
    var box = Box()
}

func keep(_ holder: borrowing Holder) {}
func take(_ holder: consuming Holder) {}

func caller() {
    let holder = Holder()
    keep(holder)
    take(holder)
    take(consume holder)
}
keep(holder) in SIL — a borrow: the callee is @guaranteed, and nothing is copied
// function_ref keep(_:)
%4 = function_ref @$s7program4keepyyAA6HolderVF : $@convention(thin) (@guaranteed Holder) -> ()
%5 = apply %4(%2) : $@convention(thin) (@guaranteed Holder) -> ()
take(holder) — the callee is @owned, and the caller still needs holder, so the compiler first makes a copy
retain_value %2
// function_ref take(_:)
%7 = function_ref @$s7program4takeyyAA6HolderVnF : $@convention(thin) (@owned Holder) -> ()
%8 = apply %7(%2) : $@convention(thin) (@owned Holder) -> ()
take(consume holder) — the same @owned call, and no copy: ownership is handed over
// function_ref take(_:)
%10 = function_ref @$s7program4takeyyAA6HolderVnF : $@convention(thin) (@owned Holder) -> ()
%11 = apply %10(%2) : $@convention(thin) (@owned Holder) -> ()

The copy is retain_value: Holder is a struct whose one field is a class reference, so copying it means adding one to the box's reference count — the copy that consuming costs when you still need the value, and that consume saves. And inout, on a var:

Changed in place
final class Box {}

struct Holder {
    var box = Box()
}

func tweak(_ holder: inout Holder) {}

func adjust() {
    var holder = Holder()
    tweak(&holder)
}
tweak(&holder) in SIL — exclusive access (begin_access [modify]), passed as @inout
%5 = begin_access [modify] [static] %0
// function_ref tweak(_:)
%6 = function_ref @$s7program5tweakyyAA6HolderVzF : $@convention(thin) (@inout Holder) -> ()
%7 = apply %6(%5) : $@convention(thin) (@inout Holder) -> ()
end_access %5
4 · from lesson 40

consume and copy: ownership you can write in an expression

consume x ends x's life right there and hands its value on. It works on ordinary copyable values too:

consume, on a String
let name = "Pip"
let moved = consume name
print(moved)
print(name)

It does not compile

'name' used after consume
note: consumed here
note: used here

copy x is the opposite: an explicit copy. Where you need it is a parameter you have marked borrowing — even of a copyable type. Writing the modifier tells the compiler you care where copies happen, so it stops making them silently:

A borrowed String, kept in a constant
func shout(_ name: borrowing String) -> String {
    let kept = name
    return kept.uppercased()
}

It does not compile

'name' is borrowed and cannot be consumed
note: consumed here
With the copy written out
func shout(_ name: borrowing String) -> String {
    let kept = copy name
    return kept.uppercased()
}

shout("pip") then prints

Console
PIP

That is the whole difference between leaving a parameter unmarked and writing borrowing: the calling convention is the same, but every copy inside the function becomes something you can see.

SE-0377, the proposal that added the modifiers, says the same of consuming — a copyable consuming parameter is not implicitly copyable either, and its example of the error is return (x, x). Swift 6.4 does not hold consuming to that:

A consuming String, returned twice
func both(_ name: consuming String) -> (String, String) {
    return (name, name)
}

It compiles, and print(both("pip")) prints

Console
("pip", "pip")

So in Swift 6.4 it is borrowing alone that makes the copies of a copyable parameter visible.

5 · from lesson 40

deinit and discard self

A struct gets a deinit only if it cannot be copied:

A copyable struct with a deinit
struct Handle {
    let fd: Int

    deinit {
        print("closing \(fd)")
    }
}

It does not compile

deinitializer cannot be declared in struct 'Handle' that conforms to 'Copyable'
note: consider adding '~Copyable' to struct 'Handle'

A copyable struct has no single moment when "it" ends — each copy ends on its own. A noncopyable one has one owner, so it has one end, and the deinit runs there: when the owner's scope ends, unless something consumed it first. A consuming method can end it without the deinit, with discard self:

A handle with a deinit, and a close() that skips it
struct Handle: ~Copyable {
    let fd: Int

    deinit {
        print("deinit: closing \(fd) for you")
    }

    consuming func close() {
        print("close(): closing \(fd)")
        discard self
    }
}
One closed by hand, one left to its deinit
let a = Handle(fd: 3)
a.close()
let b = Handle(fd: 4)
_ = b
print("end of main")

It prints

Console
close(): closing 3
end of main
deinit: closing 4 for you

a was closed by hand, so its deinit never ran. Nothing consumed b, so its deinit ran when main ended — after "end of main", because a noncopyable value lives to the end of its scope. That is deterministic clean-up for a value type, which until Swift 5.9 only classes could have.

6 · from lesson 40

A real one: Swift 6.4's Continuation

Lesson 40 meets every piece so far in one type. The iOS 27 SDK declares it like this:

From the iOS 27 SDK's _Concurrency interface (shortened) — not code to type
@frozen public struct Continuation<Success, Failure> : ~Copyable, @unchecked Sendable where Failure : Error, Success : ~Copyable {
  internal let context: Builtin.RawUnsafeContinuation
  …
  deinit
  …
  public consuming func resume(returning value: consuming sending Success) where Failure == Never {
    Builtin.resumeNonThrowingContinuationReturning(context, value)
    discard self
  }
  …
  public consuming func resume(throwing error: __owned Failure) {
    Builtin.resumeThrowingContinuationThrowing(context, error)
    discard self
  }
  …
}

~Copyable, so there is one continuation. consuming on each resume, so a resume uses it up. discard self at the end of each resume, so a resumed continuation ends quietly. And a deinit, which therefore runs only for a continuation that was never resumed. A second resume is the ticket's mistake again — a second consume this time, not a use:

Resumed twice
let answer = await withContinuation(of: String.self) { continuation in
    continuation.resume(returning: "first")
    continuation.resume(returning: "second")
}
print("got", answer)

It does not compile

'continuation' consumed more than once
note: consumed here
note: consumed again here

A forgotten resume reaches the deinit, and the deinit stops the program — the message names the line of the library's Continuation.swift it stops on:

Never resumed
let answer = await withContinuation(of: String.self) { continuation in
    print("never resumed")
}
print("got", answer)

It compiles, and stops the program

_Concurrency/Continuation.swift:56: Fatal error: Continuation was deinitialized without being resumed.
Child process terminated with signal 5: Trace/BPT trap

And where the compiler cannot see "once" — inside an escaping closure that might run any number of times — it refuses outright:

An old API, with a completion handler
func fetchCity(_ done: @escaping @Sendable (String) -> Void) {
    done("Seattle")
}
Resumed from that handler
let city = await withContinuation(of: String.self) { continuation in
    fetchCity { answer in
        continuation.resume(returning: answer)
    }
}
print(city)

It does not compile

noncopyable 'continuation' cannot be consumed when captured by an escaping closure or borrowed by a non-Escapable type
7 · from lesson 40

Generics: ~Copyable widens what is accepted

A generic parameter is Copyable unless it says otherwise — a hidden requirement, like the conformance on your own types. So an ordinary generic function rejects a noncopyable argument:

An ordinary generic function
func describe<T>(_ value: borrowing T) {
    print("a", T.self)
}

describe(Ticket(seat: "12A")) does not compile

global function 'describe' requires that 'Ticket' conform to 'Copyable'
note: 'where T: Copyable' is implicit here

The note shows the requirement nobody wrote: where T: Copyable, implicit. Writing <T: ~Copyable> removes it. Read the ~ as "not necessarily": the function now accepts more types, copyable and noncopyable alike, and in return promises not to copy what it is given:

The same function, with T: ~Copyable
func describe<T: ~Copyable>(_ value: borrowing T) {
    print("a", T.self)
}

Given a Ticket, then a String, it prints

Console
a Ticket
a String

The standard library is full of these. Optional is declared for noncopyable payloads, and its take() shows ownership in four lines — consume self, then put nil back:

From the iOS 27 SDK's Swift interface (shortened) — not code to type
@frozen public enum Optional<Wrapped> : ~Copyable, ~Escapable where Wrapped : ~Copyable, Wrapped : ~Escapable {
  …
}

extension Optional where Wrapped : ~Copyable, Wrapped : ~Escapable {
  public mutating func take() -> Optional<Wrapped> {
    let result = consume self
    self = nil
    return result
  }
}
A ticket that can be redeemed
struct Ticket: ~Copyable {
    let seat: String

    consuming func redeem() -> String {
        seat
    }
}
Taken out of an optional
var maybe: Ticket? = Ticket(seat: "12A")
if let ticket = maybe.take() {
    print("redeemed", ticket.redeem())
}
print("left behind: nil?", maybe == nil)

It prints

Console
redeemed 12A
left behind: nil? true
8 · from lesson 40

Collections: Array refuses, InlineArray accepts

Array still requires Copyable elements:

An array of tickets
let tickets = [Ticket(seat: "1A"), Ticket(seat: "1B")]

It does not compile

generic struct 'Array' requires that 'Ticket' conform to 'Copyable'
note: 'where Element: Copyable' is implicit here

The same hidden requirement, on Array's own generic parameter. InlineArray (Swift 6.2), a fixed-size array stored inline, does not have it — it is noncopyable itself whenever its elements are:

From the iOS 27 SDK's Swift interface (shortened) — not code to type
@frozen public struct Array<Element> : _DestructorSafeContainer { … }

@frozen public struct InlineArray<let count : Int, Element> : ~Copyable where Element : ~Copyable { … }
Two tickets in an InlineArray
let row: InlineArray<2, Ticket> = [Ticket(seat: "1A"), Ticket(seat: "1B")]
print("seats:", row[0].seat, row[1].seat)

It prints

Console
seats: 1A 1B
9 · from lesson 40

switch over a noncopyable enum

A switch on a borrowed enum borrows too (Swift 6.0): it can look inside a case without taking the value apart, so the enum is still there afterwards:

A noncopyable enum, described
enum Pass: ~Copyable {
    case day(String)
    case season
}

func describe(_ pass: borrowing Pass) -> String {
    switch pass {
    case .day(let date):
        return "a day pass for \(date)"
    case .season:
        return "a season pass"
    }
}
Described twice
let pass = Pass.day("Oct 15")
print(describe(pass))
print(describe(pass))

It prints

Console
a day pass for Oct 15
a day pass for Oct 15
10 · from lesson 40

~Escapable: values that cannot outlive their scope

Copyable has a sibling, Escapable, and a type can drop it the same way. A ~Escapable value may not outlive what it was made from — unless it says otherwise, the scope it was made in: it cannot be returned, stored or captured beyond it. Span (Swift 6.2) is the standard example: a view of memory that belongs to someone else, such as an array's elements, which must not outlive the array:

A function that returns a Span
func numbersView(of numbers: [Int]) -> Span<Int> {
    let span = numbers.span
    return span
}

It does not compile

a function cannot return a ~Escapable result

Yet numbers.span returns one. Here is how — a Span is a pointer and a count, and the property that makes one says whose borrow it lives on:

From the iOS 27 SDK's Swift interface (shortened) — not code to type
@frozen public struct Span<Element> : ~Escapable, Copyable, BitwiseCopyable where Element : ~Copyable {
  internal let _pointer: UnsafeRawPointer?
  …
  internal let _count: Int
  …
}

extension Array {
  …
  public var span: Span<Element> {
    @lifetime(borrow self)
    borrowing get {
      …
      let pointer = _buffer.firstElementAddress
      let count = _buffer.immutableCount
      let span = Span(_unsafeStart: pointer, count: count)
      return _overrideLifetime(span, borrowing: self)
    }
  }
  …
}

@lifetime(borrow self) is a lifetime dependence: the span may live only as long as the borrow of the array it came from, and the compiler holds every use of it to that. _overrideLifetime(span, borrowing: self) makes the pointer-and-count value into one that depends on that borrow. In your own code the attribute is still experimental in Swift 6.4:

The same function, saying whose borrow the result lives on
@lifetime(borrow numbers)
func numbersView(of numbers: [Int]) -> Span<Int> {
    let span = numbers.span
    return span
}

It does not compile

'@lifetime' attribute is only valid when experimental feature LifetimeDependence is enabled
a function cannot return a ~Escapable result

Borrowed for the length of a call it is fine — which is all a Span is for: reading someone else's storage without copying it:

A function that borrows a Span
func total(_ numbers: Span<Int>) -> Int {
    var sum = 0
    for i in numbers.indices {
        sum += numbers[i]
    }
    return sum
}
Given an array's span
let numbers = [1, 2, 3, 4]
print("total:", total(numbers.span))

It prints

Console
total: 10

That is the ~Escapable you saw in Optional's and Equatable's declarations: those types now work for non-escaping values as well.

11 · from lesson 40

Inside the compiler: the move checker

The errors on this page come from two places. Some are about types, and the type checker reports them as it reads your code: copy applied to a noncopyable value, a noncopyable parameter that does not say how it is passed, a noncopyable type given to a generic that needs Copyable or put in an Array, a deinit in a copyable struct. The others are about time — a value used after it moved, consumed twice, consumed while only borrowed — and the type checker cannot see those, because they depend on the order things happen in. They come later, from a pass that works on SIL: the move checker.

How it works is the surprising part. SILGen — the stage that turns your type-checked code into SIL — does not avoid copying a noncopyable value. Where an ordinary value would be copied, it copies this one too, and it marks the variable with an instruction, mark_unresolved_non_copyable_value, which leaves the rest to the checker. One function, as SILGen left it — the compiler printed it just before the move checker ran:

A borrow, then a consume
struct Ticket: ~Copyable {
    let seat: String
}

func look(at ticket: borrowing Ticket) {}
func redeem(_ ticket: consuming Ticket) {}

func use() {
    let ticket = Ticket(seat: "12A")
    look(at: ticket)
    redeem(ticket)
}
use() in SIL, just before the move checker
// use()
// Isolation: global_actor. type: MainActor
sil [ossa] @$s7program3useyyF : $@convention(thin) () -> () {
bb0:
  %0 = alloc_stack [lexical] [var_decl] $Ticket, let, name "ticket", type $Ticket
  %1 = mark_unresolved_non_copyable_value [consumable_and_assignable] %0
  %2 = metatype $@thin Ticket.Type
  %3 = string_literal utf8 "12A"
  %4 = integer_literal $Builtin.Word, 3
  %5 = integer_literal $Builtin.Int1, -1
  %6 = metatype $@thin String.Type
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %7 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
  %8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
  // function_ref Ticket.init(seat:)
  %9 = function_ref @$s7program6TicketV4seatACSS_tcfC : $@convention(method) (@owned String, @thin Ticket.Type) -> @owned Ticket
  %10 = apply %9(%8, %2) : $@convention(method) (@owned String, @thin Ticket.Type) -> @owned Ticket
  store %10 to [init] %1
  %12 = load_borrow %1
  // function_ref look(at:)
  %13 = function_ref @$s7program4look2atyAA6TicketV_tF : $@convention(thin) (@guaranteed Ticket) -> ()
  %14 = apply %13(%12) : $@convention(thin) (@guaranteed Ticket) -> ()
  end_borrow %12
  %16 = load [copy] %1
  // function_ref redeem(_:)
  %17 = function_ref @$s7program6redeemyyAA6TicketVnF : $@convention(thin) (@owned Ticket) -> ()
  %18 = apply %17(%16) : $@convention(thin) (@owned Ticket) -> ()
  destroy_addr %1
  dealloc_stack %0
  %21 = tuple ()
  return %21
} // end sil function '$s7program3useyyF'

look(at:) gets a load_borrow: a borrow, nothing copied. But for redeem, which takes ownership (@owned), SILGen wrote load [copy] — a copy of the ticket — and at the end of the function a destroy_addr, to destroy the original, exactly as it would for a String. The same function just after the checker:

The same function, just after the move checker
// use()
// Isolation: global_actor. type: MainActor
sil [ossa] @$s7program3useyyF : $@convention(thin) () -> () {
bb0:
  %0 = alloc_stack [lexical] [var_decl] $Ticket, let, name "ticket", type $Ticket
  %1 = metatype $@thin Ticket.Type
  %2 = string_literal utf8 "12A"
  %3 = integer_literal $Builtin.Word, 3
  %4 = integer_literal $Builtin.Int1, -1
  %5 = metatype $@thin String.Type
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %6 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
  %7 = apply %6(%2, %3, %4, %5) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
  // function_ref Ticket.init(seat:)
  %8 = function_ref @$s7program6TicketV4seatACSS_tcfC : $@convention(method) (@owned String, @thin Ticket.Type) -> @owned Ticket
  %9 = apply %8(%7, %1) : $@convention(method) (@owned String, @thin Ticket.Type) -> @owned Ticket
  store %9 to [init] %0
  %11 = load_borrow %0
  // function_ref look(at:)
  %12 = function_ref @$s7program4look2atyAA6TicketV_tF : $@convention(thin) (@guaranteed Ticket) -> ()
  %13 = apply %12(%11) : $@convention(thin) (@guaranteed Ticket) -> ()
  end_borrow %11
  %15 = load [take] %0
  debug_value undef : $*Ticket, let, name "ticket", expr op_deref
  // function_ref redeem(_:)
  %17 = function_ref @$s7program6redeemyyAA6TicketVnF : $@convention(thin) (@owned Ticket) -> ()
  %18 = apply %17(%15) : $@convention(thin) (@owned Ticket) -> ()
  dealloc_stack %0
  %20 = tuple ()
  return %20
} // end sil function '$s7program3useyyF'

The marker is gone, and load [copy] has become load [take]: the ticket is moved into redeem, not copied. The destroy_addr is gone too, because use() no longer owns anything to destroy, and the debugger is told that ticket has no value from there on (debug_value undef). The checker could do that because redeem(ticket) is the last use of ticket. Put a look(at: ticket) after it, and no move can replace that copy: the copy is what you see as "used after consume". The comment at the top of the checker's half for values in memory says it in so many words:

From the Swift compiler's source, lib/SILOptimizer/Mandatory/MoveOnlyAddressCheckerUtils.cpp — not code to type
/// Move Only Checking of Addresses
/// -------------------------------
///
/// In this file, we implement move checking of addresses. This allows for the
/// compiler to perform move checking of address only lets, vars, inout args,
/// and mutating self.
///
/// Move Address Checking in Swift
/// ------------------------------
///
/// In order to not have to rewrite all of SILGen to avoid copies, Swift has
/// taken an approach where SILGen marks moveonly addresses with a special
/// marker instruction and emits copies when it attempts to access move only
/// addresses. Then this algorithm fixed up SILGen's output by analyzing the
/// memory uses of a marked memory root location recursively using AccessPath
/// based analyses and then attempting to transform those uses based off of the
/// marked kind into one of a few variants of "simple move only address form"
/// (see below for more information). If the pass is unable to reason that it
/// can safely transform the uses into said form, we emit a diagnostic stating
/// the error to the user. If we emit said diagnostic, we then bail early. If we
/// do not emit a diagnostic, we then transform the IR so that the move only
/// address uses are in said form. This then guarantees that despite SILGen
/// emitting move only types with copies, in the end, our move only types are
/// never copied. As an additional check, once the pass has run we emit an extra
/// diagnostic if we find any copies of move only types so that the user can be
/// sure that any accepted program does not copy move only types.

The pass itself is short. It checks the values held in registers, then the ones held in memory — the two halves — and if neither found anything to report, any copy still left is the compiler's own mistake, reported as such:

From the Swift compiler's source, lib/SILOptimizer/Mandatory/MoveOnlyChecker.cpp (shortened) — not code to type
class MoveOnlyCheckerPass : public SILFunctionTransform {
  void run() override {
    auto *fn = getFunction();
    …
    MoveOnlyChecker checker(fn, getAnalysis<DominanceAnalysis>()->get(fn),
                            getAnalysis<PostOrderAnalysis>(),
                            getAnalysis<DeadEndBlocksAnalysis>());

    checker.checkObjects();
    checker.checkAddresses();

    // If we did not emit any diagnostics, emit an error on any copies that
    // remain. If we emitted a diagnostic, we just want to rewrite all of the
    // non-copyable copies into explicit variants below and let the user
    // recompile.
    if (!checker.diagnosticEmitter.emittedDiagnostic()) {
      emitCheckerMissedCopyOfNonCopyableTypeErrors(fn,
                                                   checker.diagnosticEmitter);
    }
    …

Its messages to you are declared with the compiler's other SIL diagnostics:

From the Swift compiler's source, include/swift/AST/DiagnosticsSIL.def — not code to type
ERROR(sil_movechecking_owned_value_consumed_more_than_once, none,
      "'%0' consumed more than once", (StringRef))
ERROR(sil_movechecking_owned_value_consumed_and_used_at_same_time, none,
      "'%0' consumed and used at the same time", (StringRef))
ERROR(sil_movechecking_value_used_after_consume, none,
      "'%0' used after consume", (StringRef))
ERROR(sil_movechecking_guaranteed_value_consumed, none,
      "'%0' is borrowed and cannot be consumed", (StringRef))
ERROR(sil_movechecking_borrowed_parameter_captured_by_closure, none,
      "'%0' cannot be captured by an escaping closure since it is a borrowed "
      "parameter",
      (StringRef))
ERROR(sil_movechecking_capture_consumed, none,
      "non-'Copyable' '%0' cannot be consumed when captured by an escaping closure or borrowed by a non-'Escapable' type",
      (StringRef))

Four of these are on this page. "Consumed more than once": the second resume. "Used after consume": the moved ticket — and the consumed String, whose own checker, below, reports it in the same words. "Is borrowed and cannot be consumed": the borrowed name. And the last: the continuation resumed from an escaping completion handler. (On main the compiler has started to write non-'Copyable' where Xcode 27's says noncopyable.)

The move checker is one pass of the mandatory pipeline: the passes every build runs, optimized or not, many of them there to find errors. It comes after definite initialization (which reports a variable used before it has a value) and after the check for values sent across isolation boundaries — and right after the compiler has settled how long closures live:

From the Swift compiler's source, lib/SILOptimizer/PassManager/PassPipeline.cpp (shortened) — not code to type
static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P) {
  P.startPipeline("Mandatory Diagnostic Passes + Enabling Optimization Passes");
  …
  P.addDefiniteInitialization();
  …
  P.addSendNonSendable();
  …
  P.addClosureLifetimeFixup();

  //===---
  // Begin Ownership Optimizations
  //
  // These happen after ClosureLifetimeFixup because they depend on the
  // resolution of nonescaping closure lifetimes to correctly check the use
  // of move-only values as captures in nonescaping closures as borrows.

  // Check noImplicitCopy and move only types for objects and addresses.
  P.addMoveOnlyChecker();
  …
  // Check no uses after consume operator of a value in an address.
  P.addConsumeOperatorCopyableAddressesChecker();
  // No uses after consume operator of copyable value.
  P.addConsumeOperatorCopyableValuesChecker();

  // Check ~Escapable.
  if (P.getOptions().EnableLifetimeDependenceDiagnostics) {
    P.addLifetimeDependenceDiagnostics();
  }

  // As a temporary measure, we also eliminate move only for non-trivial types
  // until we can audit the later part of the pipeline. Eventually, this should
  // occur before IRGen.
  P.addMoveOnlyTypeEliminator();
  …

Three more ownership passes follow it. consume on an ordinary copyable value — the String in consume and copy — has two checkers of its own, one for values in memory and one for values in registers. ~Escapable has the lifetime-dependence diagnostics. And then, as a temporary measure the comment explains, move-only is eliminated from the code, so that the rest of the pipeline, not yet audited for it, does not have to handle it. Nothing of ~Copyable is checked when your program runs: it is a rule the compiler proves before there is a program, and by the time there is one, there is nothing left to check.

Flash cards

11 cards, for this guide only: they are not in lesson 40's deck. Answer in your head first, then tap the card.

Trick question · OwnershipTicket is ~Copyable. After let b = a, can you still use a?
let a = Ticket(seat: "12A")
let b = a
print(b.seat)
print(a.seat)

No. 'a' used after consume let b = a moved the ticket: b owns it now. A move is always allowed; using what was moved from is not.

Trick question · OwnershipCan a noncopyable type be Equatable — and do both values survive a comparison?

Yes, and yes. Equatable itself is declared ~Copyable, and its == takes borrowing Self on both sides: it looks without taking. It printed: a == b: true still have both: 12A 12A

Syntax · OwnershipHand a noncopyable value to a function three ways: to look at, to change in place, and to keep.
struct Ticket: ~Copyable {
    var seat: String
}

func look(at ticket: borrowing Ticket) {
    print("looked at", ticket.seat)
}

func upgrade(_ ticket: inout Ticket) {
    ticket.seat = "1A"
}

func redeem(_ ticket: consuming Ticket) {
    print("redeemed", ticket.seat)
}

borrowing (looked at; the caller keeps it), inout (changed and handed back) and consuming (taken for good). It printed: looked at 12A looked at 12A looked at 1A redeemed 1A

Trick question · OwnershipA copyable struct goes to a consuming parameter while the caller still needs it. What does the compiler add?

A copy. In SIL the call is @owned — the callee takes ownership — and since the caller still needs the value, the compiler first copies it: a retain_value on the struct, one more reference to its box. take(consume holder) makes the same call with no copy, and a borrowing parameter (@guaranteed) never needed one.

Trick question · OwnershipDoes consume work on an ordinary String?
let name = "Pip"
let moved = consume name
print(moved)
print(name)

Yes. 'name' used after consume consume ends a variable's life wherever it appears, copyable or not — and the compiler holds you to it.

Trick question · OwnershipA function marks its String parameter borrowing, then assigns it to a constant. Does that compile?
func shout(_ name: borrowing String) -> String {
    let kept = name
    return kept.uppercased()
}

No. 'name' is borrowed and cannot be consumed Writing borrowing turns off silent copies inside the function, even for a copyable type. Write let kept = copy name and it compiles — the copy is still made, but now you can see it.

Trick question · Genericsfunc describe<T>(_ value: borrowing T) — can you pass it a noncopyable Ticket?
func describe<T>(_ value: borrowing T) {
    print("a", T.self)
}

describe(Ticket(seat: "12A"))

No. global function 'describe' requires that 'Ticket' conform to 'Copyable' A generic parameter is Copyable unless it says otherwise. Write <T: ~Copyable> and it accepts both kinds — read the ~ as "not necessarily". It then printed: a Ticket a String

Syntax · GenericsTake a noncopyable value out of an optional, leaving nil behind.
var maybe: Ticket? = Ticket(seat: "12A")
if let ticket = maybe.take() {
    print("redeemed", ticket.redeem())
}
print("left behind: nil?", maybe == nil)

maybe.take() — a mutating method of Optional whose body is let result = consume self; self = nil; return result. It printed: redeemed 12A left behind: nil? true

Trick question · GenericsCan an Array hold noncopyable values?
let tickets = [Ticket(seat: "1A"), Ticket(seat: "1B")]

No. generic struct 'Array' requires that 'Ticket' conform to 'Copyable' InlineArray (Swift 6.2) can: it is declared ~Copyable where Element : ~Copyable. It printed: seats: 1A 1B

Trick question · OwnershipCan a function return a Span of an array's elements?
func numbersView(of numbers: [Int]) -> Span<Int> {
    let span = numbers.span
    return span
}

No. a function cannot return a ~Escapable result Span is ~Escapable: a view of someone else's memory may not outlive its scope. Borrowed as a parameter it is fine — total: 10

Trick question · OwnershipBefore the move checker runs, the SIL for redeem(ticket) copies the noncopyable ticket: load [copy]. What does the checker make of that copy?

A move. load [copy] becomes load [take], the marker mark_unresolved_non_copyable_value goes, and so does the destroy_addr at the end — redeem owns the ticket now. SILGen writes copies of noncopyable values like any others; the checker turns each one into a move, and a copy it cannot turn into a move — because the value is used again — is your "used after consume".

Further reading