~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.
~Copyable · lesson 40consume and copy: ownership you can write in an expression · lesson 40deinit and discard self · lesson 40Continuation · lesson 40~Copyable widens what is accepted · lesson 40Array refuses, InlineArray accepts · lesson 40switch over a noncopyable enum · lesson 40~Escapable: values that cannot outlive their scope · lesson 40~CopyableEvery 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:
struct Ticket: ~Copyable {
let seat: String
}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:
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.
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:
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:
| Modifier | The function… | The caller afterwards | In SIL (below) |
|---|---|---|---|
borrowing | reads the value for the length of the call | still has it, unchanged | @guaranteed |
inout | gets exclusive access, may change it, gives it back | has it, perhaps changed | @inout |
consuming | takes it for good | no longer has it | @owned |
All three on one ticket:
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)
}var ticket = Ticket(seat: "12A")
look(at: ticket)
look(at: ticket)
upgrade(&ticket)
look(at: ticket)
redeem(ticket)It prints
looked at 12A
looked at 12A
looked at 1A
redeemed 1ABorrowed 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:
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:
struct Pass: ~Copyable, Equatable {
let seat: String
static func == (lhs: borrowing Pass, rhs: borrowing Pass) -> Bool {
lhs.seat == rhs.seat
}
}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
a == b: true
still have both: 12A 12AThe 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:
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)
}// function_ref keep(_:)
%4 = function_ref @$s7program4keepyyAA6HolderVF : $@convention(thin) (@guaranteed Holder) -> ()
%5 = apply %4(%2) : $@convention(thin) (@guaranteed Holder) -> ()retain_value %2
// function_ref take(_:)
%7 = function_ref @$s7program4takeyyAA6HolderVnF : $@convention(thin) (@owned Holder) -> ()
%8 = apply %7(%2) : $@convention(thin) (@owned Holder) -> ()// 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:
final class Box {}
struct Holder {
var box = Box()
}
func tweak(_ holder: inout Holder) {}
func adjust() {
var holder = Holder()
tweak(&holder)
}%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 %5consume and copy: ownership you can write in an expressionconsume x ends x's life right there and hands its value on. It works on ordinary copyable values too:
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:
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
func shout(_ name: borrowing String) -> String {
let kept = copy name
return kept.uppercased()
}shout("pip") then prints
PIPThat 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:
func both(_ name: consuming String) -> (String, String) {
return (name, name)
}It compiles, and print(both("pip")) prints
("pip", "pip")So in Swift 6.4 it is borrowing alone that makes the copies of a copyable parameter visible.
deinit and discard selfA struct gets a deinit only if it cannot be copied:
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:
struct Handle: ~Copyable {
let fd: Int
deinit {
print("deinit: closing \(fd) for you")
}
consuming func close() {
print("close(): closing \(fd)")
discard self
}
}let a = Handle(fd: 3)
a.close()
let b = Handle(fd: 4)
_ = b
print("end of main")It prints
close(): closing 3
end of main
deinit: closing 4 for youa 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.
ContinuationLesson 40 meets every piece so far in one type. The iOS 27 SDK declares it like this:
@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:
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:
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:
func fetchCity(_ done: @escaping @Sendable (String) -> Void) {
done("Seattle")
}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
~Copyable widens what is acceptedA 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:
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:
func describe<T: ~Copyable>(_ value: borrowing T) {
print("a", T.self)
}Given a Ticket, then a String, it prints
a Ticket
a StringThe 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:
@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
}
}struct Ticket: ~Copyable {
let seat: String
consuming func redeem() -> String {
seat
}
}var maybe: Ticket? = Ticket(seat: "12A")
if let ticket = maybe.take() {
print("redeemed", ticket.redeem())
}
print("left behind: nil?", maybe == nil)It prints
redeemed 12A
left behind: nil? trueArray refuses, InlineArray acceptsArray still requires Copyable elements:
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:
@frozen public struct Array<Element> : _DestructorSafeContainer { … }
@frozen public struct InlineArray<let count : Int, Element> : ~Copyable where Element : ~Copyable { … }let row: InlineArray<2, Ticket> = [Ticket(seat: "1A"), Ticket(seat: "1B")]
print("seats:", row[0].seat, row[1].seat)It prints
seats: 1A 1Bswitch over a noncopyable enumA 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:
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"
}
}let pass = Pass.day("Oct 15")
print(describe(pass))
print(describe(pass))It prints
a day pass for Oct 15
a day pass for Oct 15~Escapable: values that cannot outlive their scopeCopyable 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:
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:
@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:
@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:
func total(_ numbers: Span<Int>) -> Int {
var sum = 0
for i in numbers.indices {
sum += numbers[i]
}
return sum
}let numbers = [1, 2, 3, 4]
print("total:", total(numbers.span))It prints
total: 10That is the ~Escapable you saw in Optional's and Equatable's declarations: those types now work for non-escaping values as well.
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:
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()
// 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:
// 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:
/// 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:
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:
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:
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.
11 cards, for this guide only: they are not in lesson 40's deck. Answer in your head first, then tap the card.
Ticket is ~Copyable. After let b = a, can you still use a?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.
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
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
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.
consume work on an ordinary String?Yes. 'name' used after consume consume ends a variable's life wherever it appears, copyable or not — and the compiler holds you to it.
String parameter borrowing, then assigns it to a constant. Does that compile?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.
func describe<T>(_ value: borrowing T) — can you pass it a noncopyable Ticket?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
nil behind.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
Array hold noncopyable values?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
Span of an array's elements?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
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".