diff --git a/doc/destructors.rst b/doc/destructors.rst new file mode 100644 index 0000000000..a9e25f6ec1 --- /dev/null +++ b/doc/destructors.rst @@ -0,0 +1,563 @@ +================================== +Nim Destructors and Move Semantics +================================== + +:Authors: Andreas Rumpf +:Version: |nimversion| + +.. contents:: + + +About this document +=================== + +This document describes the upcoming Nim runtime which does +not use classical GC algorithms anymore but is based on destructors and +move semantics. The new runtime's advantages are that Nim programs become +oblivious to the involved heap sizes and programs are easier to write to make +effective use of multi-core machines. As a nice bonus, files and sockets and +the like will not require manual ``close`` calls anymore. + +This document aims to be a precise specification about how +move semantics and destructors work in Nim. + + +Motivating example +================== + +With the language mechanisms described here a custom seq could be +written as: + +.. code-block:: nim + + type + myseq*[T] = object + len, cap: int + data: ptr UncheckedArray[T] + + proc `=destroy`*[T](x: var myseq[T]) = + if x.data != nil: + for i in 0..= x.cap: resize(x) + x.data[x.len] = y + inc x.len + + proc `[]`*[T](x: myseq[T]; i: Natural): lent T = + assert i < x.len + x.data[i] + + proc `[]=`*[T](x: myseq[T]; i: Natural; y: sink T) = + assert i < x.len + x.data[i] = y + + proc createSeq*[T](elems: varargs[T]): myseq[T] = + result.cap = elems.len + result.len = elems.len + result.data = cast[type(result.data)](alloc(result.cap * sizeof(T))) + for i in 0.. 0: + yield it + + +Lent type +========= + +``proc p(x: sink T)`` means that the proc ``p`` takes ownership of ``x``. +To eliminate even more creation/copy <-> destruction pairs, a proc's return +type can be annotated as ``lent T``. This is useful for "getter" accessors +that seek to allow an immutable view into a container. + +The ``sink`` and ``lent`` annotations allow us to remove most (if not all) +superfluous copies and destructions. + +``lent T`` is like ``var T`` a hidden pointer. It is proven by the compiler +that the pointer does not outlive its origin. No destructor call is injected +for expressions of type ``lent T`` or of type ``var T``. + + +.. code-block:: nim + + type + Tree = object + kids: seq[Tree] + + proc construct(kids: sink seq[Tree]): Tree = + result = Tree(kids: kids) + # converted into: + `=move`(result.kids, kids) + + proc `[]`*(x: Tree; i: int): lent Tree = + result = x.kids[i] + # borrows from 'x', this is transformed into: + result = addr x.kids[i] + # This means 'lent' is like 'var T' a hidden pointer. + # Unlike 'var' this cannot be used to mutate the object. + + iterator children*(t: Tree): lent Tree = + for x in t.kids: yield x + + proc main = + # everything turned into moves: + let t = construct(@[construct(@[]), construct(@[])]) + echo t[0] # accessor does not copy the element! + + + +Owned refs +========== + +Let ``W`` be an ``owned ref`` type. Conceptually its hooks look like: + +.. code-block:: nim + + proc `=destroy`(x: var W) = + if x != nil: + assert x.refcount == 0, "dangling unowned pointers exist!" + `=destroy`(x[]) + x = nil + + proc `=`(x: var W; y: W) {.error: "owned refs can only be moved".} + + proc `=move`(x, y: var W) = + if x != y: + `=destroy`(x) + bitwiseCopy x, y # raw pointer copy + y = nil + +Let ``U`` be an unowned ``ref`` type. Conceptually its hooks look like: + +.. code-block:: nim + + proc `=destroy`(x: var U) = + if x != nil: + dec x.refcount + + proc `=`(x: var U; y: U) = + # Note: No need to check for self-assignments here. + if y != nil: inc y.refcount + if x != nil: dec x.refcount + bitwiseCopy x, y # raw pointer copy + + proc `=move`(x, y: var U) = + # Note: Moves are the same as assignments. + `=`(x, y) + + +Hook lifting +============ + +The hooks of a tuple type ``(A, B, ...)`` are generated by lifting the +hooks of the involved types ``A``, ``B``, ... to the tuple type. In +other words, a copy ``x = y`` is implemented +as ``x[0] = y[0]; x[1] = y[1]; ...``, likewise for ``=move`` and ``=destroy``. + +Other value-based compound types like ``object`` and ``array`` are handled +correspondingly. For ``object`` however, the compiler generated hooks +can be overridden. This can also be important to use an alternative traversal +of the involved datastructure that is more efficient or in order to avoid +deep recursions. + + + +Hook generation +=============== + +The ability to override a hook leads to a phase ordering problem: + +.. code-block:: nim + + type + Foo[T] = object + + proc main = + var f: Foo[int] + # error: destructor for 'f' called here before + # it was seen in this module. + + proc `=destroy`[T](f: var Foo[T]) = + discard + + +The solution is to define ``proc `=destroy`[T](f: var Foo[T])`` before +it is used. The compiler generates implicit +hooks for all types in *strategic places* so that an explicitly provided +hook that comes too "late" can be detected reliably. These *strategic places* +have been derived from the rewrite rules and are as follows: + +- In the construct ``let/var x = ...`` (var/let binding) + hooks are generated for ``typeof(x)``. +- In ``x = ...`` (assignment) hooks are generated for ``typeof(x)``. +- In ``f(...)`` (function call) hooks are generated for ``typeof(f(...))``. + + +nodestroy pragma +================ + +The experimental `nodestroy`:idx: pragma inhibits hook injections. This can be +used to specialize the object traversal in order to avoid deep recursions: + + +.. code-block:: nim + + type Node = ref object + x, y: int32 + left, right: owned Node + + type Tree = object + root: owned Node + + proc `=destroy`(t: var Tree) {.nodestroy.} = + # use an explicit stack so that we do not get stack overflows: + var s: seq[owned Node] = @[t.root] + while s.len > 0: + let x = s.pop + if x.left != nil: s.add(x.left) + if x.right != nil: s.add(x.right) + # free the memory explicit: + dispose(x) + # notice how even the destructor for 's' is not called implicitly + # anymore thanks to .nodestroy, so we have to call it on our own: + `=destroy`(s) + + +As can be seen from the example, this solution is hardly sufficient and +should eventually be replaced by a better solution.