diff options
Diffstat (limited to 'doc/destructors.md')
-rw-r--r-- | doc/destructors.md | 152 |
1 files changed, 117 insertions, 35 deletions
diff --git a/doc/destructors.md b/doc/destructors.md index a96ac35ef..e192fd362 100644 --- a/doc/destructors.md +++ b/doc/destructors.md @@ -13,12 +13,12 @@ Nim Destructors and Move Semantics About this document =================== -This document describes the upcoming Nim runtime which does +This document describes the ARC/ORC 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 +move semantics. The 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. +the like can be written not to require manual `close` calls anymore. This document aims to be a precise specification about how move semantics and destructors work in Nim. @@ -30,17 +30,20 @@ Motivating example With the language mechanisms described here, a custom seq could be written as: - ```nim + ```nim test type myseq*[T] = object len, cap: int data: ptr UncheckedArray[T] - proc `=destroy`*[T](x: var myseq[T]) = + proc `=destroy`*[T](x: myseq[T]) = if x.data != nil: for i in 0..<x.len: `=destroy`(x.data[i]) dealloc(x.data) + proc `=wasMoved`*[T](x: var myseq[T]) = + x.data = nil + proc `=trace`[T](x: var myseq[T]; env: pointer) = # `=trace` allows the cycle collector `--mm:orc` # to understand how to trace the object graph. @@ -51,7 +54,7 @@ written as: # do nothing for self-assignments: if a.data == b.data: return `=destroy`(a) - wasMoved(a) + `=wasMoved`(a) a.len = b.len a.cap = b.cap if b.data != nil: @@ -59,11 +62,19 @@ written as: for i in 0..<a.len: a.data[i] = b.data[i] + proc `=dup`*[T](a: myseq[T]): myseq[T] {.nodestroy.} = + # an optimized version of `=wasMoved(tmp); `=copy(tmp, src)` + # usually present if a custom `=copy` hook is overridden + result = myseq[T](len: a.len, cap: a.cap, data: nil) + if a.data != nil: + result.data = cast[typeof(result.data)](alloc(result.cap * sizeof(T))) + for i in 0..<result.len: + result.data[i] = `=dup`(a.data[i]) + proc `=sink`*[T](a: var myseq[T]; b: myseq[T]) = # move assignment, optional. # Compiler is using `=destroy` and `copyMem` when not provided `=destroy`(a) - wasMoved(a) a.len = b.len a.cap = b.cap a.data = b.data @@ -84,9 +95,10 @@ written as: x.data[i] = y proc createSeq*[T](elems: varargs[T]): myseq[T] = - result.cap = elems.len - result.len = elems.len - result.data = cast[typeof(result.data)](alloc(result.cap * sizeof(T))) + result = myseq[T]( + len: elems.len, + cap: elems.len, + data: cast[typeof(result.data)](alloc(result.cap * sizeof(T)))) for i in 0..<result.len: result.data[i] = elems[i] proc len*[T](x: myseq[T]): int {.inline.} = x.len @@ -101,7 +113,7 @@ well as other standard collections is performed via so-called "Lifetime-tracking hooks", which are particular [type bound operators]( manual.html#procedures-type-bound-operators). -There are 4 different hooks for each (generic or concrete) object type `T` (`T` can also be a +There are 6 different hooks for each (generic or concrete) object type `T` (`T` can also be a `distinct` type) that are called implicitly by the compiler. (Note: The word "hook" here does not imply any kind of dynamic binding @@ -117,21 +129,48 @@ other associated resources. Variables are destroyed via this hook when they go out of scope or when the routine they were declared in is about to return. -The prototype of this hook for a type `T` needs to be: +A `=destroy` hook is allowed to have a parameter of a `var T` or `T` type. Taking a `var T` type is deprecated. The prototype of this hook for a type `T` needs to be: ```nim - proc `=destroy`(x: var T) + proc `=destroy`(x: T) ``` The general pattern in `=destroy` looks like: ```nim - proc `=destroy`(x: var T) = + proc `=destroy`(x: T) = # first check if 'x' was moved to somewhere else: if x.field != nil: freeResource(x.field) ``` +A `=destroy` is implicitly annotated with `.raises: []`; a destructor +should not raise exceptions. For backwards compatibility the compiler +produces a warning for a `=destroy` that does raise. + +A `=destroy` can explicitly list the exceptions it can raise, if any, +but this of little utility as a raising destructor is implementation defined +behavior. Later versions of the language specification might cover this case precisely. + + +`=wasMoved` hook +---------------- + +A `=wasMoved` hook sets the object to a state that signifies to the destructor there is nothing to destroy. + +The prototype of this hook for a type `T` needs to be: + + ```nim + proc `=wasMoved`(x: var T) + ``` + +Usually some pointer field inside the object is set to `nil`: + + ```nim + proc `=wasMoved`(x: var T) = + x.field = nil + ``` + `=sink` hook ------------ @@ -238,10 +277,10 @@ The general pattern in using `=destroy` with `=trace` looks like: Test[T](size: size, arr: cast[ptr UncheckedArray[T]](alloc0(sizeof(T) * size))) - proc `=destroy`[T](dest: var Test[T]) = + proc `=destroy`[T](dest: Test[T]) = if dest.arr != nil: for i in 0 ..< dest.size: dest.arr[i].`=destroy` - dest.arr.dealloc + dealloc dest.arr proc `=trace`[T](dest: var Test[T]; env: pointer) = if dest.arr != nil: @@ -255,6 +294,31 @@ The general pattern in using `=destroy` with `=trace` looks like: than the other hooks. +`=dup` hook +----------- + +A `=dup` hook duplicates an object. `=dup(x)` can be regarded as an optimization replacing a `wasMoved(dest); =copy(dest, x)` operation. + +The prototype of this hook for a type `T` needs to be: + + ```nim + proc `=dup`(x: T): T + ``` + +The general pattern in implementing `=dup` looks like: + + ```nim + type + Ref[T] = object + data: ptr T + rc: ptr int + + proc `=dup`[T](x: Ref[T]): Ref[T] = + result = x + if x.rc != nil: + inc x.rc[] + ``` + Move semantics ============== @@ -264,6 +328,24 @@ document uses the notation `lastReadOf(x)` to describe that `x` is not used afterward. This property is computed by a static control flow analysis but can also be enforced by using `system.move` explicitly. +One can query if the analysis is able to perform a move with `system.ensureMove`. +`move` enforces a move operation and calls `=wasMoved` whereas `ensureMove` is +an annotation that implies no runtime operation. An `ensureMove` annotation leads to a static error +if the compiler cannot prove that a move would be safe. + +For example: + + ```nim + proc main(normalParam: string; sinkParam: sink string) = + var x = "abc" + # valid: + let valid = ensureMove x + # invalid: + let invalid = ensureMove normalParam + # valid: + let alsoValid = ensureMove sinkParam + ``` + Swap ==== @@ -381,7 +463,7 @@ destroyed at the scope exit. x = lastReadOf z ------------------ (move-optimization) `=sink`(x, z) - wasMoved(z) + `=wasMoved`(z) v = v @@ -401,14 +483,14 @@ destroyed at the scope exit. f_sink(notLastReadOf y) -------------------------- (copy-to-sink) - (let tmp; `=copy`(tmp, y); + (let tmp = `=dup`(y); f_sink(tmp)) f_sink(lastReadOf y) ----------------------- (move-to-sink) f_sink(y) - wasMoved(y) + `=wasMoved`(y) Object and array construction @@ -421,7 +503,7 @@ function has `sink` parameters. Destructor removal ================== -`wasMoved(x);` followed by a `=destroy(x)` operation cancel each other +`=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. The current implementation does perform this optimization. @@ -430,11 +512,11 @@ optimization. Self assignments ================ -`=sink` in combination with `wasMoved` can handle self-assignments but +`=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. +into `=sink(x, x); =wasMoved(x)` because that would lose `x`'s value. The solution is that simple self-assignments that consist of - Symbols: `x = x` @@ -469,10 +551,10 @@ Is transformed into: try: if cond: `=sink`(result, a) - wasMoved(a) + `=wasMoved`(a) else: `=sink`(result, b) - wasMoved(b) + `=wasMoved`(b) finally: `=destroy`(b) `=destroy`(a) @@ -486,10 +568,10 @@ Is transformed into: `=sink`(y, "xyz") `=sink`(x, select(true, let blitTmp = x - wasMoved(x) + `=wasMoved`(x) blitTmp, let blitTmp = y - wasMoved(y) + `=wasMoved`(y) blitTmp)) echo [x] finally: @@ -517,7 +599,7 @@ that the pointer does not outlive its origin. No destructor call is injected for expressions of type `lent T` or of type `var T`. - ```nim + ```nim test type Tree = object kids: seq[Tree] @@ -525,13 +607,13 @@ for expressions of type `lent T` or of type `var T`. proc construct(kids: sink seq[Tree]): Tree = result = Tree(kids: kids) # converted into: - `=sink`(result.kids, kids); wasMoved(kids) + `=sink`(result.kids, kids); `=wasMoved`(kids) `=destroy`(kids) proc `[]`*(x: Tree; i: int): lent Tree = result = x.kids[i] # borrows from 'x', this is transformed into: - result = addr x.kids[i] + # result = addr x.kids[i] # This means 'lent' is like 'var T' a hidden pointer. # Unlike 'var' this hidden pointer cannot be used to mutate the object. @@ -637,11 +719,11 @@ The ability to override a hook leads to a phase ordering problem: # error: destructor for 'f' called here before # it was seen in this module. - proc `=destroy`[T](f: var Foo[T]) = + proc `=destroy`[T](f: Foo[T]) = discard ``` -The solution is to define ``proc `=destroy`[T](f: var Foo[T])`` before +The solution is to define ``proc `=destroy`[T](f: 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* @@ -662,7 +744,7 @@ The experimental `nodestroy`:idx: pragma inhibits hook injections. This can be used to specialize the object traversal in order to avoid deep recursions: - ```nim + ```nim test type Node = ref object x, y: int32 left, right: Node @@ -670,15 +752,15 @@ used to specialize the object traversal in order to avoid deep recursions: type Tree = object root: Node - proc `=destroy`(t: var Tree) {.nodestroy.} = + proc `=destroy`(t: Tree) {.nodestroy.} = # use an explicit stack so that we do not get stack overflows: var s: seq[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) + # free the memory explicitly: + `=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) |