summary refs log tree commit diff stats
path: root/doc/destructors.rst
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2019-08-11 21:55:47 +0200
committerAndreas Rumpf <rumpf_a@web.de>2019-08-11 21:55:47 +0200
commitda64c8762fd4af44866879061c10f707b0dcd310 (patch)
tree89711b4f775605b510785580ea9b8ddc94fc5a58 /doc/destructors.rst
parent212ae2f1257628bd5d1760593ce0a1bad768831a (diff)
downloadNim-da64c8762fd4af44866879061c10f707b0dcd310.tar.gz
destructors: spec reflects reality, =sink is here to stay
Diffstat (limited to 'doc/destructors.rst')
-rw-r--r--doc/destructors.rst266
1 files changed, 135 insertions, 131 deletions
diff --git a/doc/destructors.rst b/doc/destructors.rst
index f17bdadf7..a48799427 100644
--- a/doc/destructors.rst
+++ b/doc/destructors.rst
@@ -52,17 +52,12 @@ written as:
       for i in 0..<a.len:
         a.data[i] = b.data[i]
 
-  proc `=move`*[T](a, b: var myseq[T]) =
-    # do nothing for self-assignments:
-    if a.data == b.data: return
+  proc `=sink`*[T](a: var myseq[T]; b: myseq[T]) =
+    # move assignment
     `=destroy`(a)
     a.len = b.len
     a.cap = b.cap
     a.data = b.data
-    # b's elements have been stolen so ensure that the
-    # destructor for b does nothing:
-    b.data = nil
-    b.len = 0
 
   proc add*[T](x: var myseq[T]; y: sink T) =
     if x.len >= x.cap: resize(x)
@@ -128,38 +123,41 @@ The general pattern in ``=destroy`` looks like:
 
 
 
-`=move` hook
+`=sink` hook
 ------------
 
-A `=move` hook moves an object around, the resources are stolen from the source
-and passed to the destination. It must be ensured that source's destructor does
-not free the resources afterwards.
+A `=sink` hook moves an object around, the resources are stolen from the source
+and passed to the destination. It is ensured that source's destructor does
+not free the resources afterwards by setting the object to its default value
+(the value the object's state started in). Setting an object ``x`` back to its
+default value is written as ``wasMoved(x)``.
 
 The prototype of this hook for a type ``T`` needs to be:
 
 .. code-block:: nim
 
-  proc `=move`(dest, source: var T)
+  proc `=sink`(dest: var T; source: T)
 
 
-The general pattern in ``=move`` looks like:
+The general pattern in ``=sink`` looks like:
 
 .. code-block:: nim
 
-  proc `=move`(dest, source: var T) =
-    # protect against self-assignments:
-    if dest.field != source.field:
-      `=destroy`(dest)
-      dest.field = source.field
-      source.field = nil
+  proc `=sink`(dest: var T; source: T) =
+    `=destroy`(dest)
+    dest.field = source.field
 
 
+**Note**: ``=sink`` does not need to check for self-assignments.
+How self-assignments are handled is explained later in this document.
+
 
 `=` (copy) hook
 ---------------
 
 The ordinary assignment in Nim conceptually copies the values. The ``=`` hook
-is called for assignments that couldn't be transformed into moves.
+is called for assignments that couldn't be transformed into ``=sink``
+operations.
 
 The prototype of this hook for a type ``T`` needs to be:
 
@@ -197,11 +195,11 @@ Swap
 ====
 
 The need to check for self-assignments and also the need to destroy previous
-objects inside ``=`` and ``=move`` is a strong indicator to treat ``system.swap``
-as a builtin primitive of its own that simply swaps every field in the involved
-objects via ``copyMem`` or a comparable mechanism.
+objects inside ``=`` and ``=sink`` is a strong indicator to treat
+``system.swap`` as a builtin primitive of its own that simply swaps every
+field in the involved objects via ``copyMem`` or a comparable mechanism.
 In other words, ``swap(a, b)`` is **not** implemented
-as ``let tmp = move(a); b = move(a); a = move(tmp)``!
+as ``let tmp = move(a); b = move(a); a = move(tmp)``.
 
 This has further consequences:
 
@@ -214,8 +212,12 @@ Sink parameters
 ===============
 
 To move a variable into a collection usually ``sink`` parameters are involved.
-A location that is passed to a ``sink`` parameters should not be used afterwards.
-This is ensured by a static analysis over a control flow graph. A sink parameter
+A location that is passed to a ``sink`` parameter should not be used afterwards.
+This is ensured by a static analysis over a control flow graph. If it cannot be
+proven to be the last usage of the location, a copy is done instead and this
+copy is then passed to the sink parameter.
+
+A sink parameter
 *may* be consumed once in the proc's body but doesn't have to be consumed at all.
 The reason for this is that signatures
 like ``proc put(t: var Table; k: sink Key, v: sink Value)`` should be possible
@@ -250,63 +252,10 @@ An implementation is allowed, but not required to implement even more move
 optimizations (and the current implementation does not).
 
 
-Self assignments
-================
-
-Unfortunately this document departs significantly from
-the older design as specified here, https://github.com/nim-lang/Nim/wiki/Destructors.
-The reason is that under the old design so called "self assignments" could not work.
-
-
-.. code-block:: nim
-
-  proc select(cond: bool; a, b: sink string): string =
-    if cond:
-      result = a # moves a into result
-    else:
-      result = b # moves b into result
-
-  proc main =
-    var x = "abc"
-    var y = "xyz"
-
-    # possible self-assignment:
-    x = select(rand() < 0.5, x, y)
-    # 'select' must communicate what parameter has been
-    # consumed. We cannot simply generate:
-    # (select(...); wasMoved(x); wasMoved(y))
-
-Consequence: ``sink`` parameters for objects that have a non-trivial destructor
-must be passed as by-pointer under the hood. A further advantage is that parameters
-are never destroyed, only variables are. The caller's location passed to
-a ``sink`` parameter has to be destroyed by the caller and does not burden
-the callee.
-
-
-Const temporaries
-=================
-
-Constant literals like ``nil`` cannot be easily be ``=moved``'d. The solution
-is to pass a temporary location that contains ``nil`` to the sink location.
-In other words, ``var T`` can only bind to locations, but ``sink T`` can bind
-to values.
-
-For example:
-
-.. code-block:: nim
-
-  var x: owned ref T = nil
-  # gets turned into by the compiler:
-  var tmp = nil
-  move(x, tmp)
-
 
 Rewrite rules
 =============
 
-**Note**: A function call ``f()`` is always the "last read" of the involved
-temporary location and so covered under the more general rewrite rules.
-
 **Note**: There are two different allowed implementation strategies:
 
 1. The produced ``finally`` section can be a single section that is wrapped
@@ -324,17 +273,28 @@ not destroyed at the scope exit, but at the proc exit.
   finally: `=destroy`(x)
 
 
-  f(...)
-  ------------------------    (function-call)
-  (let tmp;
+  g(f(...))
+  ------------------------    (nested-function-call)
+  g(let tmp;
   bitwiseCopy tmp, f(...);
   tmp)
   finally: `=destroy`(tmp)
 
 
+  x = f(...)
+  ------------------------    (function-sink)
+  `=sink`(x, f(...))
+
+
   x = lastReadOf z
   ------------------          (move-optimization)
-  `=move`(x, z)
+  `=sink`(x, z)
+  wasMoved(z)
+
+
+  v = v
+  ------------------   (self-assignment-removal)
+  discard "nop"
 
 
   x = y
@@ -342,62 +302,106 @@ not destroyed at the scope exit, but at the proc exit.
   `=`(x, y)
 
 
-  x = move y
-  ------------------          (enforced-move)
-  `=move`(x, y)
+  f_sink(g())
+  -----------------------     (call-to-sink)
+  f_sink(g())
 
 
   f_sink(notLastReadOf y)
-  -----------------------     (copy-to-sink)
-  (let tmp; `=`(tmp, y); f_sink(tmp))
-  finally: `=destroy`(tmp)
+  --------------------------     (copy-to-sink)
+  (let tmp; `=`(tmp, y);
+  f_sink(tmp))
 
 
-  f_sink(move y)
-  -----------------------     (enforced-move-to-sink)
-  (let tmp; `=move`(tmp, y); f_sink(tmp))
-  finally: `=destroy`(tmp)
+  f_sink(lastReadOf y)
+  -----------------------     (move-to-sink)
+  f_sink(y)
+  wasMoved(y)
+
+
+Object and array construction
+=============================
+
+Object and array construction is treated as a function call where the
+function has ``sink`` parameters.
 
 
+Destructor removal
+==================
+
+``wasMoved(x);`` followed by a `=destroy(x)` operation cancel each other
+out. An implementation is encouraged to exploit this in order to improve
+efficiency and code sizes.
+
 
-Cursor variables
+Self assignments
 ================
 
-There is an additional rewrite rule for so called "cursor" variables.
-A cursor variable is a variable that is only used for navigation inside
-a data structure. The otherwise implied copies (or moves) and destructions
-can be avoided altogether for cursor variables:
+``=sink`` in combination with ``wasMoved`` can handle self-assignments but
+it's subtle.
 
-::
+The simple case of ``x = x`` cannot be turned
+into ``=sink(x, x); wasMoved(x)`` because that would lose ``x``'s value.
+The solution is that simple self-assignments are simply transformed into
+an empty statement that does nothing.
 
-  var x {.cursor.}: T
-  x = path(z)
-  stmts
-  --------------------------  (cursor-var)
-  x = bitwiseCopy(path z)
-  stmts
-  # x is not destroyed.
+The complex case looks like a variant of ``x = f(x)``, we consider
+``x = select(rand() < 0.5, x, y)`` here:
 
 
-``stmts`` must not mutate ``z`` nor ``x``. All assignments to ``x`` must be
-of the form ``path(z)`` but the ``z`` can differ. Neither ``z`` nor ``x``
-can be aliased; this implies the addresses of these locations must not be
-used explicitly.
+.. code-block:: nim
 
-The current implementation does not compute cursor variables but supports
-the ``.cursor`` pragma annotation. Cursor variables are respected and
-simply trusted: No checking is performed that no mutations or aliasing
-occurs.
+  proc select(cond: bool; a, b: sink string): string =
+    if cond:
+      result = a # moves a into result
+    else:
+      result = b # moves b into result
+
+  proc main =
+    var x = "abc"
+    var y = "xyz"
+    # possible self-assignment:
+    x = select(true, x, y)
+
+
+Is transformed into:
 
-Cursor variables are commonly used in ``iterator`` implementations:
 
 .. code-block:: nim
 
-  iterator nonEmptyItems(x: seq[string]): string =
-    for i in 0..high(x):
-      let it {.cursor.} = x[i] # no string copies, no destruction of 'it'
-      if it.len > 0:
-        yield it
+  proc select(cond: bool; a, b: sink string): string =
+    try:
+      if cond:
+        `=sink`(result, a)
+        wasMoved(a)
+      else:
+        `=sink`(result, b)
+        wasMoved(b)
+    finally:
+      `=destroy`(b)
+      `=destroy`(a)
+
+  proc main =
+    var
+      x: string
+      y: string
+    try:
+      `=sink`(x, "abc")
+      `=sink`(y, "xyz")
+      `=sink`(x, select(true,
+        let blitTmp = x
+        wasMoved(x)
+        blitTmp,
+        let blitTmp = y
+        wasMoved(y)
+        blitTmp))
+      echo [x]
+    finally:
+      `=destroy`(y)
+      `=destroy`(x)
+
+As can be manually verified, this transformation is correct for
+self-assignments.
 
 
 Lent type
@@ -425,14 +429,14 @@ for expressions of type ``lent T`` or of type ``var T``.
   proc construct(kids: sink seq[Tree]): Tree =
     result = Tree(kids: kids)
     # converted into:
-    `=move`(result.kids, kids)
+    `=sink`(result.kids, kids); wasMoved(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.
+    # Unlike 'var' this hidden pointer cannot be used to mutate the object.
 
   iterator children*(t: Tree): lent Tree =
     for x in t.kids: yield x
@@ -459,11 +463,10 @@ Let ``W`` be an ``owned ref`` type. Conceptually its hooks look like:
 
   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
+  proc `=sink`(x: var W; y: W) =
+    `=destroy`(x)
+    bitwiseCopy x, y # raw pointer copy
+
 
 Let ``U`` be an unowned ``ref`` type. Conceptually its hooks look like:
 
@@ -479,9 +482,8 @@ Let ``U`` be an unowned ``ref`` type. Conceptually its hooks look like:
     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)
+  proc `=sink`(x: var U, y: U) {.error.}
+  # Note: Moves are not available.
 
 
 Hook lifting
@@ -490,7 +492,7 @@ 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``.
+as ``x[0] = y[0]; x[1] = y[1]; ...``, likewise for ``=sink`` and ``=destroy``.
 
 Other value-based compound types like ``object`` and ``array`` are handled
 correspondingly. For ``object`` however, the compiler generated hooks
@@ -529,6 +531,8 @@ have been derived from the rewrite rules and are as follows:
   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(...))``.
+- For every sink parameter ``x: sink T`` the hooks are generated
+  for ``typeof(x)``.
 
 
 nodestroy pragma