summary refs log tree commit diff stats
path: root/compiler
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2020-07-15 23:00:06 +0200
committerGitHub <noreply@github.com>2020-07-15 23:00:06 +0200
commitc5358b0d4b1d27db04b974a0183c9b7312bc7bdc (patch)
treeca3a71953358c8f9475188045ba61c5dfb5caf87 /compiler
parent813dd1b670b953b0ac8348b04079faadace46c29 (diff)
downloadNim-c5358b0d4b1d27db04b974a0183c9b7312bc7bdc.tar.gz
An optimizer for ARC (#14962)
* WIP: an optimizer for ARC
* do not optimize away destructors in 'finally' if unstructured control flow is involved
* optimized the optimizer
* minor code cleanup
* first steps to .cursor inference
* cursor inference: big steps to a working solution
* baby steps
* better .cursor inference
* new feature: expandArc for easy inspection of the AST after ARC transformations
* added topt_cursor test
* adapt tests
* cleanups, make tests green
* optimize common traversal patterns
* moved test case
* fixes .cursor inference so that npeg compiles once again
* cursor inference: more bugfixes

Co-authored-by: Clyybber <darkmine956@gmail.com>
Diffstat (limited to 'compiler')
-rw-r--r--compiler/commands.nim3
-rw-r--r--compiler/cursor_inference.nim295
-rw-r--r--compiler/injectdestructors.nim54
-rw-r--r--compiler/optimizer.nim285
-rw-r--r--compiler/options.nim2
-rw-r--r--compiler/renderer.nim21
-rw-r--r--compiler/transf.nim2
7 files changed, 632 insertions, 30 deletions
diff --git a/compiler/commands.nim b/compiler/commands.nim
index 1984c894a..d0936a0c6 100644
--- a/compiler/commands.nim
+++ b/compiler/commands.nim
@@ -866,6 +866,9 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
   of "expandmacro":
     expectArg(conf, switch, arg, pass, info)
     conf.macrosToExpand[arg] = "T"
+  of "expandarc":
+    expectArg(conf, switch, arg, pass, info)
+    conf.arcToExpand[arg] = "T"
   of "oldgensym":
     processOnOffSwitchG(conf, {optNimV019}, arg, pass, info)
   of "useversion":
diff --git a/compiler/cursor_inference.nim b/compiler/cursor_inference.nim
new file mode 100644
index 000000000..7806a535f
--- /dev/null
+++ b/compiler/cursor_inference.nim
@@ -0,0 +1,295 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2020 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## Cursor inference:
+## The basic idea was like this: Elide 'destroy(x)' calls if only
+## special literals are assigned to 'x' and 'x' is not mutated or
+## passed by 'var T' to something else. Special literals are string literals or
+## arrays / tuples of string literals etc.
+##
+## However, there is a much more general rule here: Compute which variables
+## can be annotated with `.cursor`. To see how and when we can do that,
+## think about this question: In `dest = src` when do we really have to
+## *materialize* the full copy? - Only if `dest` or `src` are mutated
+## afterwards. `dest` is the potential cursor variable, so that is
+## simple to analyse. And if `src` is a location derived from a
+## formal parameter, we also know it is not mutated! In other words, we
+## do a compile-time copy-on-write analysis.
+
+import
+  ast, types, renderer, idents, intsets, options, msgs
+
+type
+  Cursor = object
+    s: PSym
+    deps: IntSet
+  Con = object
+    cursors: seq[Cursor]
+    mayOwnData: IntSet
+    mutations: IntSet
+    reassigns: IntSet
+    config: ConfigRef
+
+proc locationRoot(e: PNode; followDotExpr = true): PSym =
+  var n = e
+  while true:
+    case n.kind
+    of nkSym:
+      if n.sym.kind in {skVar, skResult, skTemp, skLet, skForVar, skParam}:
+        return n.sym
+      else:
+        return nil
+    of nkDotExpr, nkDerefExpr, nkBracketExpr, nkHiddenDeref,
+        nkCheckedFieldExpr, nkAddr, nkHiddenAddr:
+      if followDotExpr:
+        n = n[0]
+      else:
+        return nil
+    of nkObjUpConv, nkObjDownConv:
+      n = n[0]
+    of nkHiddenStdConv, nkHiddenSubConv, nkConv, nkCast:
+      n = n[1]
+    of nkStmtList, nkStmtListExpr:
+      if n.len > 0:
+        n = n[^1]
+      else:
+        return nil
+    else:
+      return nil
+
+proc addDep(c: var Con; dest: var Cursor; dependsOn: PSym) =
+  if dest.s != dependsOn:
+    dest.deps.incl dependsOn.id
+
+proc cursorId(c: Con; x: PSym): int =
+  for i in 0..<c.cursors.len:
+    if c.cursors[i].s == x: return i
+  return -1
+
+proc getCursors(c: Con): IntSet =
+  #[
+  Question: if x depends on y and y depends on z then also y depends on z.
+
+    Or does it?
+
+  var y = x # y gets the copy already
+
+  var harmless = x
+
+  y.s = "mutate"
+
+  ]#
+  result = initIntSet()
+  for cur in c.cursors:
+    if not c.mayOwnData.contains(cur.s.id) and
+        cur.s.typ.skipTypes({tyGenericInst, tyAlias}).kind != tyOwned:
+      block doAdd:
+        for d in cur.deps:
+          # you cannot borrow from a location that is mutated or (owns data and is re-assigned)
+          if c.mutations.contains(d) or (c.mayOwnData.contains(d) and c.reassigns.contains(d)):
+            #echo "bah, not a cursor ", cur.s, " bad dependency ", d
+            break doAdd
+        when true:
+          result.incl cur.s.id
+        when true:
+          echo "computed as a cursor ", cur.s, " ", cur.deps, " ", c.config $ cur.s.info
+
+proc analyseAsgn(c: var Con; dest: var Cursor; n: PNode) =
+  case n.kind
+  of nkEmpty, nkCharLit..pred(nkNilLit):
+    # primitive literals including the empty are harmless:
+    discard
+  of nkNilLit:
+    when false:
+      # XXX think about this case. Is 'nil' an owned location?
+      if hasDestructor(n.typ):
+        # 'myref = nil' is destructive and so 'myref' should not be a cursor:
+        c.mayOwnData.incl dest.s.id
+
+  of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkCast, nkConv:
+    analyseAsgn(c, dest, n[1])
+
+  of nkIfStmt, nkIfExpr:
+    for i in 0..<n.len:
+      analyseAsgn(c, dest, n[i].lastSon)
+
+  of nkCaseStmt:
+    for i in 1..<n.len:
+      analyseAsgn(c, dest, n[i].lastSon)
+
+  of nkStmtList, nkStmtListExpr:
+    if n.len > 0:
+      analyseAsgn(c, dest, n[^1])
+
+  of nkClosure:
+    for i in 1..<n.len:
+      analyseAsgn(c, dest, n[i])
+    # you must destroy a closure:
+    c.mayOwnData.incl dest.s.id
+
+  of nkObjConstr:
+    for i in 1..<n.len:
+      analyseAsgn(c, dest, n[i])
+    if hasDestructor(n.typ):
+      # you must destroy a ref object:
+      c.mayOwnData.incl dest.s.id
+
+  of nkCurly, nkBracket, nkPar, nkTupleConstr:
+    for son in n:
+      analyseAsgn(c, dest, son)
+    if n.typ.skipTypes(abstractInst).kind == tySequence:
+      # you must destroy a sequence:
+      c.mayOwnData.incl dest.s.id
+
+  of nkSym:
+    if n.sym.kind in {skVar, skResult, skTemp, skLet, skForVar, skParam}:
+      if n.sym.flags * {sfThread, sfGlobal} != {}:
+        # aliasing a global is inherently dangerous:
+        c.mayOwnData.incl dest.s.id
+      else:
+        # otherwise it's just a dependency, nothing to worry about:
+        c.addDep(dest, n.sym)
+
+  of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr,
+      nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkAddr, nkHiddenAddr:
+    analyseAsgn(c, dest, n[0])
+
+  of nkCallKinds:
+    if hasDestructor(n.typ):
+      # calls do construct, what we construct must be destroyed,
+      # so dest cannot be a cursor:
+      c.mayOwnData.incl dest.s.id
+    elif n.typ.kind in {tyLent, tyVar}:
+      # we know the result is derived from the first argument:
+      let r = locationRoot(n[1])
+      if r != nil:
+        c.addDep(dest, r)
+    else:
+      let magic = if n[0].kind == nkSym: n[0].sym.magic else: mNone
+      # this list is subtle, we try to answer the question if after 'dest = f(src)'
+      # there is a connection betwen 'src' and 'dest' so that mutations to 'src'
+      # also reflect 'dest':
+      if magic in {mNone, mMove, mSlice, mAppendStrCh, mAppendStrStr, mAppendSeqElem, mArrToSeq}:
+        for i in 1..<n.len:
+          # we always have to assume a 'select(...)' like mechanism.
+          # But at least we do filter out simple POD types from the
+          # list of dependencies via the 'hasDestructor' check for
+          # the root's symbol.
+          if hasDestructor(n[i].typ.skipTypes({tyVar, tySink, tyLent, tyGenericInst, tyAlias})):
+            analyseAsgn(c, dest, n[i])
+
+  else:
+    # something we cannot handle:
+    c.mayOwnData.incl dest.s.id
+
+proc rhsIsSink(c: var Con, n: PNode) =
+  if n.kind == nkSym and n.typ.skipTypes(abstractInst-{tyOwned}).kind == tyRef:
+    discard "do no pessimize simple refs further, injectdestructors.nim will prevent moving from it"
+  else:
+    let r = locationRoot(n, followDotExpr = false)
+    if r != nil and r.kind != skParam:
+      # let x = cursor? --> treat it like a sink parameter
+      c.mayOwnData.incl r.id
+      c.mutations.incl r.id
+
+proc analyse(c: var Con; n: PNode) =
+  case n.kind
+  of nkCallKinds:
+    let parameters = n[0].typ
+    let L = if parameters != nil: parameters.len else: 0
+
+    analyse(c, n[0])
+    for i in 1..<n.len:
+      let it = n[i]
+      let r = locationRoot(it)
+      if r != nil and i < L:
+        let paramType = parameters[i].skipTypes({tyGenericInst, tyAlias})
+        if paramType.kind in {tyVar, tySink, tyOwned}:
+          # pass by var? mayOwnData the root
+          # Else we seek to take ownership of 'r'. This is only valid when 'r'
+          # actually owns its data. Thus 'r' cannot be a cursor:
+          c.mayOwnData.incl r.id
+          # and we assume it might get wasMoved(...) too:
+          c.mutations.incl r.id
+      analyse(c, it)
+
+  of nkAsgn, nkFastAsgn:
+    analyse(c, n[0])
+    analyse(c, n[1])
+
+    if n[0].kind == nkSym:
+      if hasDestructor(n[0].typ):
+        # re-assignment to the full object is fundamentally different:
+        let idx = cursorId(c, n[0].sym)
+        if idx >= 0:
+          analyseAsgn(c, c.cursors[idx], n[1])
+
+        c.reassigns.incl n[0].sym.id
+    else:
+      # assignments like 'x.field = value' mean that 'x' itself cannot
+      # be a cursor:
+      let r = locationRoot(n[0])
+      if r != nil and r.typ.skipTypes(abstractInst).kind notin {tyPtr, tyRef}:
+        # however, an assignment like 'it.field = x' does not influence r's
+        # cursorness property:
+        c.mayOwnData.incl r.id
+        c.mutations.incl r.id
+
+    if hasDestructor(n[1].typ):
+      rhsIsSink(c, n[1])
+
+  of nkAddr, nkHiddenAddr:
+    analyse(c, n[0])
+    let r = locationRoot(n[0])
+    if r != nil:
+      c.mayOwnData.incl r.id
+      c.mutations.incl r.id
+
+  of nkTupleConstr, nkBracket, nkObjConstr:
+    for i in ord(n.kind == nkObjConstr)..<n.len:
+      if n[i].kind == nkSym:
+        # we assume constructions with cursors are better without
+        # the cursors because it's likely we can move then, see
+        # test arc/topt_no_cursor.nim
+        let r = n[i].sym
+        c.mayOwnData.incl r.id
+        c.mutations.incl r.id
+
+  of nkVarSection, nkLetSection:
+    for it in n:
+      let value = it[^1]
+      analyse(c, value)
+      if hasDestructor(it[0].typ):
+        for i in 0..<it.len-2:
+          let v = it[i]
+          if v.kind == nkSym and v.sym.flags * {sfThread, sfGlobal} == {}:
+            # assume it's a cursor:
+            c.cursors.add Cursor(s: it[i].sym)
+            if it.kind == nkVarTuple and value.kind in {nkPar, nkTupleConstr} and
+                it.len-2 == value.len:
+              # this might mayOwnData it again:
+              analyseAsgn(c, c.cursors[^1], value[i])
+              rhsIsSink(c, value[i])
+            else:
+              # this might mayOwnData it again:
+              analyseAsgn(c, c.cursors[^1], value)
+              rhsIsSink(c, value)
+
+  of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
+      nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
+      nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
+      nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
+    discard
+  else:
+    for child in n: analyse(c, child)
+
+proc computeCursors*(n: PNode; config: ConfigRef): IntSet =
+  var c = Con(config: config)
+  analyse(c, n)
+  result = getCursors c
diff --git a/compiler/injectdestructors.nim b/compiler/injectdestructors.nim
index 98cfb4743..556d098f4 100644
--- a/compiler/injectdestructors.nim
+++ b/compiler/injectdestructors.nim
@@ -13,18 +13,11 @@
 
 ## See doc/destructors.rst for a spec of the implemented rewrite rules
 
-## XXX Optimization to implement: if a local variable is only assigned
-## string literals as in ``let x = conf: "foo" else: "bar"`` do not
-## produce a destructor call for ``x``. The address of ``x`` must also
-## not have been taken. ``x = "abc"; x.add(...)``
-
-# Todo:
-# - eliminate 'wasMoved(x); destroy(x)' pairs as a post processing step.
-
 import
-  intsets, ast, astalgo, msgs, renderer, magicsys, types, idents,
+  intsets, strtabs, ast, astalgo, msgs, renderer, magicsys, types, idents,
   strutils, options, dfa, lowerings, tables, modulegraphs, msgs,
-  lineinfos, parampatterns, sighashes, liftdestructors
+  lineinfos, parampatterns, sighashes, liftdestructors, optimizer,
+  cursor_inference
 
 from trees import exprStructuralEquivalent, getRoot
 
@@ -42,6 +35,7 @@ type
   Con = object
     owner: PSym
     g: ControlFlowGraph
+    cursors: IntSet
     graph: ModuleGraph
     otherRead: PNode
     inLoop, inSpawn: int
@@ -155,6 +149,17 @@ proc isLastRead(location: PNode; cfg: ControlFlowGraph; otherRead: var PNode; pc
       pc = min(variantA, variantB)
   return pc
 
+proc isCursor(n: PNode; c: Con): bool =
+  case n.kind
+  of nkSym:
+    result = sfCursor in n.sym.flags or c.cursors.contains(n.sym.id)
+  of nkDotExpr:
+    result = sfCursor in n[1].sym.flags
+  of nkCheckedFieldExpr:
+    result = isCursor(n[0], c)
+  else:
+    result = false
+
 proc isLastRead(n: PNode; c: var Con): bool =
   # first we need to search for the instruction that belongs to 'n':
   var instr = -1
@@ -486,17 +491,6 @@ proc ensureDestruction(arg, orig: PNode; c: var Con; s: var Scope): PNode =
   else:
     result = arg
 
-proc isCursor(n: PNode): bool =
-  case n.kind
-  of nkSym:
-    result = sfCursor in n.sym.flags
-  of nkDotExpr:
-    result = sfCursor in n[1].sym.flags
-  of nkCheckedFieldExpr:
-    result = isCursor(n[0])
-  else:
-    result = false
-
 proc cycleCheck(n: PNode; c: var Con) =
   if c.graph.config.selectedGC != gcArc: return
   var value = n[1]
@@ -727,7 +721,8 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode =
     elif n.kind in {nkBracket, nkObjConstr, nkTupleConstr, nkClosure, nkNilLit} +
          nkCallKinds + nkLiterals:
       result = p(n, c, s, consumed)
-    elif ((n.kind == nkSym and isSinkParam(n.sym)) or isAnalysableFieldAccess(n, c.owner)) and isLastRead(n, c):
+    elif ((n.kind == nkSym and isSinkParam(n.sym)) or isAnalysableFieldAccess(n, c.owner)) and
+        isLastRead(n, c) and not (n.kind == nkSym and isCursor(n, c)):
       # Sinked params can be consumed only once. We need to reset the memory
       # to disable the destructor which we have not elided
       result = destructiveMoveVar(n, c, s)
@@ -830,7 +825,7 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode =
         if it.kind == nkVarTuple and hasDestructor(ri.typ):
           let x = lowerTupleUnpacking(c.graph, it, c.owner)
           result.add p(x, c, s, consumed)
-        elif it.kind == nkIdentDefs and hasDestructor(it[0].typ) and not isCursor(it[0]):
+        elif it.kind == nkIdentDefs and hasDestructor(it[0].typ) and not isCursor(it[0], c):
           for j in 0..<it.len-2:
             let v = it[j]
             if v.kind == nkSym:
@@ -851,7 +846,7 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode =
           result.add v
     of nkAsgn, nkFastAsgn:
       if hasDestructor(n[0].typ) and n[1].kind notin {nkProcDef, nkDo, nkLambda} and
-          not isCursor(n[0]):
+          not isCursor(n[0], c):
         # rule (self-assignment-removal):
         if n[1].kind == nkSym and n[0].kind == nkSym and n[0].sym == n[1].sym:
           result = newNodeI(nkEmpty, n.info)
@@ -988,7 +983,7 @@ proc moveOrCopy(dest, ri: PNode; c: var Con; s: var Scope, isDecl = false): PNod
       let snk = c.genSink(dest, ri, isDecl)
       result = newTree(nkStmtList, snk, c.genWasMoved(ri))
     elif ri.sym.kind != skParam and ri.sym.owner == c.owner and
-        isLastRead(ri, c) and canBeMoved(c, dest.typ):
+        isLastRead(ri, c) and canBeMoved(c, dest.typ) and not isCursor(ri, c):
       # Rule 3: `=sink`(x, z); wasMoved(z)
       let snk = c.genSink(dest, ri, isDecl)
       result = newTree(nkStmtList, snk, c.genWasMoved(ri))
@@ -1048,6 +1043,8 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
     echoCfg(c.g)
     echo n
 
+  c.cursors = computeCursors(n, g.config)
+
   var scope: Scope
   let body = p(n, c, scope, normal)
 
@@ -1059,7 +1056,12 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
         scope.final.add c.genDestroy(params[i])
   #if optNimV2 in c.graph.config.globalOptions:
   #  injectDefaultCalls(n, c)
-  result = processScope(c, scope, body)
+  result = optimize processScope(c, scope, body)
   dbg:
     echo ">---------transformed-to--------->"
     echo renderTree(result, {renderIds})
+
+  if g.config.arcToExpand.hasKey(owner.name.s):
+    echo "--expandArc: ", owner.name.s
+    echo renderTree(result, {renderIr})
+    echo "-- end of expandArc ------------------------"
diff --git a/compiler/optimizer.nim b/compiler/optimizer.nim
new file mode 100644
index 000000000..5d1139bfd
--- /dev/null
+++ b/compiler/optimizer.nim
@@ -0,0 +1,285 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2020 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## Optimizer:
+## - elide 'wasMoved(x); destroy(x)' pairs
+## - recognize "all paths lead to 'wasMoved(x)'"
+
+import
+  ast, renderer, idents, intsets
+
+from trees import exprStructuralEquivalent
+
+const
+  nfMarkForDeletion = nfNone # faster than a lookup table
+
+type
+  BasicBlock = object
+    wasMovedLocs: seq[PNode]
+    kind: TNodeKind
+    hasReturn, hasBreak: bool
+    label: PSym # can be nil
+    parent: ptr BasicBlock
+
+  Con = object
+    somethingTodo: bool
+    inFinally: int
+
+proc nestedBlock(parent: var BasicBlock; kind: TNodeKind): BasicBlock =
+  BasicBlock(wasMovedLocs: @[], kind: kind, hasReturn: false, hasBreak: false,
+    label: nil, parent: addr(parent))
+
+proc breakStmt(b: var BasicBlock; n: PNode) =
+  var it = addr(b)
+  while it != nil:
+    it.wasMovedLocs.setLen 0
+    it.hasBreak = true
+
+    if n.kind == nkSym:
+      if it.label == n.sym: break
+    else:
+      # unnamed break leaves the block is nkWhileStmt or the like:
+      if it.kind in {nkWhileStmt, nkBlockStmt, nkBlockExpr}: break
+
+    it = it.parent
+
+proc returnStmt(b: var BasicBlock) =
+  b.hasReturn = true
+  var it = addr(b)
+  while it != nil:
+    it.wasMovedLocs.setLen 0
+    it = it.parent
+
+proc mergeBasicBlockInfo(parent: var BasicBlock; this: BasicBlock) {.inline.} =
+  if this.hasReturn:
+    parent.wasMovedLocs.setLen 0
+    parent.hasReturn = true
+
+proc wasMovedTarget(matches: var IntSet; branch: seq[PNode]; moveTarget: PNode): bool =
+  result = false
+  for i in 0..<branch.len:
+    if exprStructuralEquivalent(branch[i][1].skipAddr, moveTarget,
+                                strictSymEquality = true):
+      result = true
+      matches.incl i
+
+proc intersect(summary: var seq[PNode]; branch: seq[PNode]) =
+  # keep all 'wasMoved(x)' calls in summary that are also in 'branch':
+  var i = 0
+  var matches = initIntSet()
+  while i < summary.len:
+    if wasMovedTarget(matches, branch, summary[i][1].skipAddr):
+      inc i
+    else:
+      summary.del i
+  for m in matches:
+    summary.add branch[m]
+
+
+proc invalidateWasMoved(c: var BasicBlock; x: PNode) =
+  var i = 0
+  while i < c.wasMovedLocs.len:
+    if exprStructuralEquivalent(c.wasMovedLocs[i][1].skipAddr, x,
+                                strictSymEquality = true):
+      c.wasMovedLocs.del i
+    else:
+      inc i
+
+proc wasMovedDestroyPair(c: var Con; b: var BasicBlock; d: PNode) =
+  var i = 0
+  while i < b.wasMovedLocs.len:
+    if exprStructuralEquivalent(b.wasMovedLocs[i][1].skipAddr, d[1].skipAddr,
+                                strictSymEquality = true):
+      b.wasMovedLocs[i].flags.incl nfMarkForDeletion
+      c.somethingTodo = true
+      d.flags.incl nfMarkForDeletion
+      b.wasMovedLocs.del i
+    else:
+      inc i
+
+proc analyse(c: var Con; b: var BasicBlock; n: PNode) =
+  case n.kind
+  of nkCallKinds:
+    var special = false
+    var reverse = false
+    if n[0].kind == nkSym:
+      let s = n[0].sym
+      if s.magic == mWasMoved:
+        b.wasMovedLocs.add n
+        special = true
+      elif s.name.s == "=destroy":
+        if c.inFinally > 0 and (b.hasReturn or b.hasBreak):
+          discard "cannot optimize away the destructor"
+        else:
+          c.wasMovedDestroyPair b, n
+        special = true
+      elif s.name.s == "=sink":
+        reverse = true
+
+    if not special:
+      if not reverse:
+        for i in 0 ..< n.len:
+          analyse(c, b, n[i])
+      else:
+        #[ Test tmatrix.test3:
+        Prevent this from being elided. We should probably
+        find a better solution...
+
+            `=sink`(b, - (
+              let blitTmp = b;
+              wasMoved(b);
+              blitTmp + a)
+            `=destroy`(b)
+
+        ]#
+        for i in countdown(n.len-1, 0):
+          analyse(c, b, n[i])
+      if canRaise(n[0]): returnStmt(b)
+
+  of nkSym:
+    # any usage of the location before destruction implies we
+    # cannot elide the 'wasMoved(x)':
+    b.invalidateWasMoved n
+
+  of nkNone..pred(nkSym), succ(nkSym)..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
+      nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
+      nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
+      nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
+    discard "do not follow the construct"
+
+  of nkAsgn, nkFastAsgn:
+    # reverse order, see remark for `=sink`:
+    analyse(c, b, n[1])
+    analyse(c, b, n[0])
+
+  of nkIfStmt, nkIfExpr:
+    let isExhaustive = n[^1].kind in {nkElse, nkElseExpr}
+    var wasMovedSet: seq[PNode] = @[]
+
+    for i in 0 ..< n.len:
+      var branch = nestedBlock(b, n[i].kind)
+
+      analyse(c, branch, n[i])
+      mergeBasicBlockInfo(b, branch)
+      if isExhaustive:
+        if i == 0:
+          wasMovedSet = move(branch.wasMovedLocs)
+        else:
+          wasMovedSet.intersect(branch.wasMovedLocs)
+    for i in 0..<wasMovedSet.len:
+      b.wasMovedLocs.add wasMovedSet[i]
+
+  of nkCaseStmt:
+    let isExhaustive = skipTypes(n[0].typ,
+      abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString} or
+      n[^1].kind == nkElse
+
+    analyse(c, b, n[0])
+
+    var wasMovedSet: seq[PNode] = @[]
+
+    for i in 1 ..< n.len:
+      var branch = nestedBlock(b, n[i].kind)
+
+      analyse(c, branch, n[i])
+      mergeBasicBlockInfo(b, branch)
+      if isExhaustive:
+        if i == 1:
+          wasMovedSet = move(branch.wasMovedLocs)
+        else:
+          wasMovedSet.intersect(branch.wasMovedLocs)
+    for i in 0..<wasMovedSet.len:
+      b.wasMovedLocs.add wasMovedSet[i]
+
+  of nkTryStmt:
+    for i in 0 ..< n.len:
+      var tryBody = nestedBlock(b, nkTryStmt)
+
+      analyse(c, tryBody, n[i])
+      mergeBasicBlockInfo(b, tryBody)
+
+  of nkWhileStmt:
+    analyse(c, b, n[0])
+    var loopBody = nestedBlock(b, nkWhileStmt)
+    analyse(c, loopBody, n[1])
+    mergeBasicBlockInfo(b, loopBody)
+
+  of nkBlockStmt, nkBlockExpr:
+    var blockBody = nestedBlock(b, n.kind)
+    if n[0].kind == nkSym:
+      blockBody.label = n[0].sym
+    analyse(c, blockBody, n[1])
+    mergeBasicBlockInfo(b, blockBody)
+
+  of nkBreakStmt:
+    breakStmt(b, n[0])
+
+  of nkReturnStmt, nkRaiseStmt:
+    for child in n: analyse(c, b, child)
+    returnStmt(b)
+
+  of nkFinally:
+    inc c.inFinally
+    for child in n: analyse(c, b, child)
+    dec c.inFinally
+
+  else:
+    for child in n: analyse(c, b, child)
+
+proc opt(c: Con; n, parent: PNode; parentPos: int) =
+  template recurse() =
+    let x = shallowCopy(n)
+    for i in 0 ..< n.len:
+      opt(c, n[i], x, i)
+    parent[parentPos] = x
+
+  case n.kind
+  of nkCallKinds:
+    if nfMarkForDeletion in n.flags:
+      parent[parentPos] = newNodeI(nkEmpty, n.info)
+    else:
+      recurse()
+
+  of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
+      nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
+      nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
+      nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
+    parent[parentPos] = n
+
+  else:
+    recurse()
+
+
+proc optimize*(n: PNode): PNode =
+  # optimize away simple 'wasMoved(x); destroy(x)' pairs.
+  #[ Unfortunately this optimization is only really safe when no exceptions
+     are possible, see for example:
+
+  proc main(inp: string; cond: bool) =
+    if cond:
+      try:
+        var s = ["hi", inp & "more"]
+        for i in 0..4:
+          use s
+        consume(s)
+        wasMoved(s)
+      finally:
+        destroy(s)
+
+    Now assume 'use' raises, then we shouldn't do the 'wasMoved(s)'
+  ]#
+  var c: Con
+  var b: BasicBlock
+  analyse(c, b, n)
+  if c.somethingTodo:
+    result = shallowCopy(n)
+    for i in 0 ..< n.safeLen:
+      opt(c, n[i], result, i)
+  else:
+    result = n
diff --git a/compiler/options.nim b/compiler/options.nim
index 80881d903..8b73c07fc 100644
--- a/compiler/options.nim
+++ b/compiler/options.nim
@@ -235,6 +235,7 @@ type
     options*: TOptions    # (+)
     globalOptions*: TGlobalOptions # (+)
     macrosToExpand*: StringTableRef
+    arcToExpand*: StringTableRef
     m*: MsgConfig
     evalTemplateCounter*: int
     evalMacroCounter*: int
@@ -410,6 +411,7 @@ proc newConfigRef*(): ConfigRef =
     options: DefaultOptions,
     globalOptions: DefaultGlobalOptions,
     macrosToExpand: newStringTable(modeStyleInsensitive),
+    arcToExpand: newStringTable(modeStyleInsensitive),
     m: initMsgConfig(),
     evalExpr: "",
     cppDefines: initHashSet[string](),
diff --git a/compiler/renderer.nim b/compiler/renderer.nim
index 449ee4504..3cd532e15 100644
--- a/compiler/renderer.nim
+++ b/compiler/renderer.nim
@@ -20,7 +20,8 @@ import
 type
   TRenderFlag* = enum
     renderNone, renderNoBody, renderNoComments, renderDocComments,
-    renderNoPragmas, renderIds, renderNoProcDefs, renderSyms, renderRunnableExamples
+    renderNoPragmas, renderIds, renderNoProcDefs, renderSyms, renderRunnableExamples,
+    renderIr
   TRenderFlags* = set[TRenderFlag]
   TRenderTok* = object
     kind*: TTokType
@@ -47,11 +48,20 @@ type
       pendingNewlineCount: int
     fid*: FileIndex
     config*: ConfigRef
+    mangler: seq[PSym]
 
 # We render the source code in a two phases: The first
 # determines how long the subtree will likely be, the second
 # phase appends to a buffer that will be the output.
 
+proc disamb(g: var TSrcGen; s: PSym): int =
+  # we group by 's.name.s' to compute the stable name ID.
+  result = 0
+  for i in 0 ..< g.mangler.len:
+    if s == g.mangler[i]: return result
+    if s.name.s == g.mangler[i].name.s: inc result
+  g.mangler.add s
+
 proc isKeyword*(i: PIdent): bool =
   if (i.id >= ord(tokKeywordLow) - ord(tkSymbol)) and
       (i.id <= ord(tokKeywordHigh) - ord(tkSymbol)):
@@ -850,7 +860,12 @@ proc gident(g: var TSrcGen, n: PNode) =
       t = tkSymbol
   else:
     t = tkOpr
-  if n.kind == nkSym and (renderIds in g.flags or sfGenSym in n.sym.flags or n.sym.kind == skTemp):
+  if renderIr in g.flags and n.kind == nkSym:
+    let localId = disamb(g, n.sym)
+    if localId != 0 and n.sym.magic == mNone:
+      s.add '_'
+      s.addInt localId
+  elif n.kind == nkSym and (renderIds in g.flags or sfGenSym in n.sym.flags or n.sym.kind == skTemp):
     s.add '_'
     s.addInt n.sym.id
     when defined(debugMagics):
@@ -1022,7 +1037,7 @@ proc gsub(g: var TSrcGen, n: PNode, c: TContext) =
     else:
       put(g, tkSymbol, "(wrong conv)")
   of nkHiddenCallConv:
-    if renderIds in g.flags:
+    if {renderIds, renderIr} * g.flags != {}:
       accentedName(g, n[0])
       put(g, tkParLe, "(")
       gcomma(g, n, 1)
diff --git a/compiler/transf.nim b/compiler/transf.nim
index 10a2680ae..be559abb8 100644
--- a/compiler/transf.nim
+++ b/compiler/transf.nim
@@ -233,7 +233,7 @@ proc hasContinue(n: PNode): bool =
 
 proc newLabel(c: PTransf, n: PNode): PSym =
   result = newSym(skLabel, nil, getCurrOwner(c), n.info)
-  result.name = getIdent(c.graph.cache, genPrefix & $result.id)
+  result.name = getIdent(c.graph.cache, genPrefix)
 
 proc transformBlock(c: PTransf, n: PNode): PNode =
   var labl: PSym