Skip to content

Instantly share code, notes, and snippets.

@Gankra
Last active April 3, 2019 22:44
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Gankra/1f79fbf2a9776302a9d4c8c0097cc40e to your computer and use it in GitHub Desktop.
Save Gankra/1f79fbf2a9776302a9d4c8c0097cc40e to your computer and use it in GitHub Desktop.
Swift Ownership Manifesto TL;DR

Swift Ownership Manifesto TL;DR

Most of the manifesto is background and detailed definitions -- if you're confused or want details, read the manifesto!

https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170213/032155.html

Note also that manifestos aren't complete proposals -- syntax and details may change!

One piece of background: inout is kinda complicated because it can be used on computed properties -- foo(&val.x) might be sugar for

let temp = val.getX(); 
foo(&temp); 
val.setX(temp);

The setX part is the hardest, because it means inouts cannot just be dropped on the floor; they must be finalized with arbitrary code that isn't connected to the type of the inout. Today this is easy because inouts are only passed as function arguments, so the caller, which has full context, can destroy it.

TL;DR

  • Make inout truly exclusive ("Law Of Exclusivity")

    • Static enforcement: foo(&x, withClosure: { x += 1 }) is now a compilation error.
    • But if the compiler determines this can't be managed statically, there's dynamic management (sort of like a thread-unsafe Reader-Writer Lock that crashes if you can't acquire the lock -- very similar to RefCell for Rust programmers, but more language-controlled and optimizable)
    • Examples where dynamic tracking is necessary: globals, class properties, and the captures of an escaping closure
    • Memory accessed through any kind of UnsafePointer foo(&ptr.memory) has no safety guards -- it's Undefined Behaviour to alias inouts produced from UnsafePointer's (really read the actual Manifesto for details on this)
  • Add a shared keyword, which is like inout, but immutable and implicit (no & required).

    • Doesn't require copies (ARC retains) at call site
    • Body doesn't need to destroy the value (ARC release)
    • Downside: if the caller doesn't need the value, they can't transfer ownership to callee (bad if callee needs to do a copy)
    • Shared already existed -- self on methods has always been passed "shared" (this is +0 vs +1 for people who've seen that)
    • Shared doesn't imply pointers -- foo(x: shared Int) and foo(x: owned Int) are exactly the same, because Int is trivial.
    • Shared and Owned don't change the signature of a method for the purpose of conforming to protocols or overload resolution (although in the former case, a thunk may be needed to bridge the ABI)
  • Add owned (used like inout) and consuming (used like mutating) keywords to request non-shared passing where shared is implied (or just to be explicit).

  • Allow inout and shared on local variables -- inout root = &tree.root; shared elements = self.queue;

    • For now, these are lexically scoped -- the inout is destroyed when the variable goes out of scope.
    • An endScope(myVar) function will be added to explicitly terminate a variable early.
  • Returning shared or inout values will be done with coroutines (see the Appendix at the end of this for an example):

    • Finalizing inouts and shares can be done in the coroutine's continue/finish subroutines
    • New read and modify accessors that work like get and set, but are shared and inout instead.
    • Ability to iterate collections with shared or inout access to elements
  • Non-Copyable Values and Move Semantics

    • Intended to be something users can miss (potentially just for power users)
    • New Copyable protocol, stating you can be copied
    • Everything implicitly conforms to Copyable (unless it contains a non-Copyable value)
    • Generic code implies <T: Copyable> for ergonomics/back-compat
    • moveonly keyword to remove the implicit Copyable bounds and implementations
    • moveonly types can have deinits
    • move<T>(T) -> T function that can be used to request an explicit move of a copyable type (moves are often implicitly inserted by the optimizer)
    • shared vs owned actually matters for moveonly types.
  • Maybes:

    • The ability to use properties as first-class functions?
    • Explicitly copyable types that require you to call copy<T>(T) -> T -- used for things like "a big struct that can be copied but probably shouldn't be"?
  • Notable Omissions:

    • Rust-style lifetimes. These don't seem to really fit with the rest of Swift. Too much dynamicism!
    • Concurrency. Out of scope. This stuff doesn't protect you from data races or race conditions.

Implementation Priorities

Stolen directly from the document because this section is great. Decreasing order (sorry move semantics!).

  • Enforcing the Law of Exclusivity:

    • Static enforcement
    • Dynamic enforcement
    • Optimization of dynamic enforcement
  • New annotations and declarations:

    • shared parameters
    • consuming methods
    • Local shared and inout declarations
  • New intrinsics affecting DI:

    • The move function and its DI implications
    • The endScope function and its DI implications
  • Co-routine features:

    • Generalized accessors
    • Generators
  • Non-copyable types

    • Further design work
    • DI enforcement
    • moveonly contexts

Appendix: Coroutines

This is the biggest "innovation" of Swift's model, so I felt it deserved some fleshing out.

Here's a possible example of how coroutines would work:

mutating generator iterateMutable() -> inout Element {
  var i = startIndex, e = endIndex
  while i != e {
    yield &self[i]
    self.formIndex(after: &i)
  }
}

for inout x in myThing {
  x += 1
}

Potentially desugars to something like:

mutating func iterateMutable_start() -> (UnsafeMutablePointer<Element>?, State) { ... }
mutating func iterateMutable_continue(with state: inout State) -> UnsafeMutablePointer<Element>? { ... }
mutating func iterateMutable_finish(with state: State) { ... }

var (xOrStop, state) = myThing.iterateMutable_start()
while let x = xOrStop {
    x.memory += 1
    xOrStop = myThing.iterateMutable_continue(with: &state)
}
myThing.iterateMutable_finish(with: state)

Note that the user of iterateMutable has no code to handle inout finalizing -- all of that is handled by the coroutine methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment