diff --git a/Destructors.rest b/Destructors.rest new file mode 100644 index 0000000..924eff6 --- /dev/null +++ b/Destructors.rest @@ -0,0 +1,129 @@ +This page is a follow-up of https://nim-lang.org/araq/destructors.html and further outlines of where Nim is heading in the future. (Did I hear anyone say "Nim v2"?) + +Nim's strings and sequences should become "GC-free" implementations and are exemplary for how Nim's core should work. Strings and sequences are value-based that means ``=`` performs a copy (conceptually). In practice many copies can be optimized away (see my blog post). The "optimized" copy is called a "move" and is supported via the type bound operator ``=sink``. + +Rewrite rules (simplified) +========================== + +-------- -------------------- ----------------------------------------------------------- +Rule Pattern Meaning +-------- -------------------- ----------------------------------------------------------- +1 var x; stmts var x; try stmts finally: `=destroy`(x) +2 x = f() `=sink`(x, f()) +3 x = lastReadOf z `=sink`(x, z) +4 x = y `=`(x, y) # a copy +5 f(g()) var tmp; `=sink`(tmp, g()); f(tmp); `=destroy`(tmp) +-------- -------------------- ----------------------------------------------------------- + +Rule (5) can be optimized further to ``var tmp = bitwiseCopy(g()); f(tmp); =destroy(tmp)``. + + +Sink parameters +=============== + +A ``sink`` parameter conveys a transfer of ownership. The parameter will be *consumed*. + +A ``sink`` parameter is internally **not** mapped to ``var``, instead the +usual "pass-by-copy" / "optimize to by-ref if more efficient" implementation +is used. However, similar rules apply -- you cannot pass a ``const`` to +a ``sink`` parameter. + +A ``sink`` parameter **must** be **consumed** exactly once within the +proc's body. The compiler will use a dataflow analysis to prove this fact. +For a ``sink`` parameter called ``sp`` a **consume** looks like: + +.. code-block:: nim + proc consume(c: var Container; sp: sink T) = + locationDerivedFrom(c) = sp + +This assignment is mapped to the ``=sink`` operator. + +A consume can also be forwarded, "pass sp to a different proc as a sink parameter": + +.. code-block:: nim + proc consume(c: var Container; sp: sink T) = + c.takeAsSink(sp) + + +Use after consume +----------------- + +Locations passed to a ``sink`` parameter are invalidated after the call +and the compiler tries to prove that it is not used again afterwards. For +local variables this is quite easy to prove: + +.. code-block:: nim + proc consume(c: var Container; element: sink T) = + c[i] = element + + proc main() = + var x = initT() + for i in 0..3: + container.consume(x) # Error: attempt to re-use already moved value 'x' + +For arbitrary locations involving array accesses etc it is too hard to prove +it is not used afterwards. The compiler transforms ``takeAsSink(sp)`` into +``takeAsSink(sp); reset(sp)``. ``reset`` sets the value back into its default +value. For locals the ``reset`` can be optimized away (stores to a dead object), +for function calls there is no location to reset at all. + +For a location that has had its value moved into a sink parameter no +destructor call needs to be injected. This is an important optimization +to keep the produced code small. + + +Sink for locals +--------------- + +``sink T`` is also a valid type for locals. For a variable ``v`` of +type ``sink T`` no destructor call is injected and it is statically +ensured that every code path leads to its consumption. + + +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. + +Like ``sink T`` ``lent T`` is a valid annotation for local variables too. +For a variable ``v`` of type ``lent T`` it is statically ensured that no code +path leads to its consumption, in other words that it must not escape its +local stack frame (either directly or indirectly via passing to a ``sink`` +parameter). For ``v`` no destructor call is injected since it doesn't own +the object. + +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 that the compiler needs to prove that +it doesn't outlive its origin. + + +.. code-block:: nim + + type + Tree = object + kids: seq[Tree] + + proc construct(kids: sink seq[Tree]): Tree = + result = Tree(kids: kids) + # converted into: + `=sink`(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!