summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2020-03-18 16:57:34 +0100
committerGitHub <noreply@github.com>2020-03-18 16:57:34 +0100
commitfb641483f0e2ed974b89d629ea5ec28e5e6145ce (patch)
tree8f74544adb04d46a7946554e2fd73e6bbc417ff3
parenta96842aaeb46ddf3990f9632259a26cb451c5b80 (diff)
downloadNim-fb641483f0e2ed974b89d629ea5ec28e5e6145ce.tar.gz
arc optimizations (#13325)
* scope based destructors
* handle 'or' and 'and' expressions properly, see the new test arc/tcontrolflow.nim
* make this branch mergable, logic is disabled for now
-rw-r--r--compiler/ccgexprs.nim4
-rw-r--r--compiler/cgmeth.nim2
-rw-r--r--compiler/closureiters.nim4
-rw-r--r--compiler/injectdestructors.nim321
-rw-r--r--doc/destructors.rst1
-rw-r--r--lib/system.nim9
-rw-r--r--tests/arc/tcontrolflow.nim55
-rw-r--r--tests/destructor/tasync_prototype.nim5
-rw-r--r--tests/destructor/tdestructor.nim2
-rw-r--r--tests/destructor/tmisc_destructors.nim2
-rw-r--r--tests/destructor/tv2_raise.nim2
11 files changed, 312 insertions, 95 deletions
diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim
index 4aad1a4f8..4cf6f36db 100644
--- a/compiler/ccgexprs.nim
+++ b/compiler/ccgexprs.nim
@@ -2573,6 +2573,10 @@ proc expr(p: BProc, n: PNode, d: var TLoc) =
       else:
         putLocIntoDest(p, d, sym.loc)
     of skTemp:
+      if sym.loc.r == nil:
+        # we now support undeclared 'skTemp' variables for easier
+        # transformations in other parts of the compiler:
+        assignLocalVar(p, n)
       if sym.loc.r == nil or sym.loc.t == nil:
         #echo "FAILED FOR PRCO ", p.prc.name.s
         #echo renderTree(p.prc.ast, {renderIds})
diff --git a/compiler/cgmeth.nim b/compiler/cgmeth.nim
index a71ac6ea7..edf9db383 100644
--- a/compiler/cgmeth.nim
+++ b/compiler/cgmeth.nim
@@ -7,7 +7,7 @@
 #    distribution, for details about the copyright.
 #
 
-## This module implements code generation for multi methods.
+## This module implements code generation for methods.
 
 import
   intsets, options, ast, msgs, idents, renderer, types, magicsys,
diff --git a/compiler/closureiters.nim b/compiler/closureiters.nim
index ef08a7bae..3422a8a46 100644
--- a/compiler/closureiters.nim
+++ b/compiler/closureiters.nim
@@ -154,8 +154,8 @@ type
                     # is their finally. For finally it is parent finally. Otherwise -1
 
 const
-  nkSkip = { nkEmpty..nkNilLit, nkTemplateDef, nkTypeSection, nkStaticStmt,
-            nkCommentStmt } + procDefs
+  nkSkip = {nkEmpty..nkNilLit, nkTemplateDef, nkTypeSection, nkStaticStmt,
+            nkCommentStmt} + procDefs
 
 proc newStateAccess(ctx: var Ctx): PNode =
   if ctx.stateVarSym.isNil:
diff --git a/compiler/injectdestructors.nim b/compiler/injectdestructors.nim
index 5e51aeb9d..55886540c 100644
--- a/compiler/injectdestructors.nim
+++ b/compiler/injectdestructors.nim
@@ -13,6 +13,13 @@
 
 ## 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, msgs, renderer, magicsys, types, idents,
@@ -20,6 +27,10 @@ import
   lineinfos, parampatterns, sighashes
 
 from trees import exprStructuralEquivalent
+from algorithm import reverse
+
+const
+  scopeBasedDestruction = false
 
 type
   Con = object
@@ -27,10 +38,13 @@ type
     g: ControlFlowGraph
     jumpTargets: IntSet
     destroys, topLevelVars: PNode
+    scopeDestroys: seq[PNode] # used as a stack that pop from
+                              # at strategic places which try to
+                              # mimic the natural scope.
     graph: ModuleGraph
     emptyNode: PNode
     otherRead: PNode
-    inLoop: int
+    inLoop, hasUnstructuredCf, inDangerousBranch: int
     declaredVars: IntSet # variables we already moved to the top level
     uninit: IntSet # set of uninit'ed vars
     uninitComputed: bool
@@ -285,7 +299,6 @@ proc getTemp(c: var Con; typ: PType; info: TLineInfo): PNode =
   let sym = newSym(skTemp, getIdent(c.graph.cache, ":tmpD"), c.owner, info)
   sym.typ = typ
   result = newSymNode(sym)
-  c.addTopVar(result)
 
 proc genWasMoved(n: PNode; c: var Con): PNode =
   result = newNodeI(nkCall, n.info)
@@ -339,10 +352,10 @@ proc isClosureEnv(n: PNode): bool = n.kind == nkSym and n.sym.name.s[0] == ':'
 proc passCopyToSink(n: PNode; c: var Con): PNode =
   result = newNodeIT(nkStmtListExpr, n.info, n.typ)
   let tmp = getTemp(c, n.typ, n.info)
-  # XXX This is only required if we are in a loop. Since we move temporaries
-  # out of loops we need to mark it as 'wasMoved'.
-  result.add genWasMoved(tmp, c)
+  when not scopeBasedDestruction:
+    c.addTopVar(tmp)
   if hasDestructor(n.typ):
+    result.add genWasMoved(tmp, c)
     var m = genCopy(c, tmp, n)
     m.add p(n, c, normal)
     result.add m
@@ -354,6 +367,8 @@ proc passCopyToSink(n: PNode; c: var Con): PNode =
     if c.graph.config.selectedGC in {gcArc, gcOrc}:
       assert(not containsGarbageCollectedRef(n.typ))
     result.add newTree(nkAsgn, tmp, p(n, c, normal))
+  # Since we know somebody will take over the produced copy, there is
+  # no need to destroy it.
   result.add tmp
 
 proc isDangerousSeq(t: PType): bool {.inline.} =
@@ -375,60 +390,137 @@ proc containsConstSeq(n: PNode): bool =
       if containsConstSeq(son): return true
   else: discard
 
-template handleNested(n: untyped, processCall: untyped) =
+proc handleTmpDestroys(c: var Con; body: PNode; t: PType;
+                       oldHasUnstructuredCf, oldTmpDestroysLen: int) =
+  if c.hasUnstructuredCf == oldHasUnstructuredCf:
+    # no need for a try-finally statement:
+    if body.kind == nkStmtList:
+      for i in countdown(c.scopeDestroys.high, oldTmpDestroysLen):
+        body.add c.scopeDestroys[i]
+    elif isEmptyType(t):
+      var n = newNodeI(nkStmtList, body.info)
+      n.add body[^1]
+      for i in countdown(c.scopeDestroys.high, oldTmpDestroysLen):
+        n.add c.scopeDestroys[i]
+      body[^1] = n
+    elif body.kind == nkStmtListExpr and body.len > 0 and body[^1].kind == nkSym:
+      # special case: Do not translate (x; y; sym) into
+      # (x; y; tmp = sym; destroy(x); destroy(y); tmp )
+      # but into
+      # (x; y; destroy(x); destroy(y); sym )
+      let sym = body[^1]
+      body[^1] = c.scopeDestroys[^1]
+      for i in countdown(c.scopeDestroys.high - 1, oldTmpDestroysLen):
+        body.add c.scopeDestroys[i]
+      body.add sym
+    else:
+      # fun ahead: We have to transform (x; y; E()) into
+      # (x; y; tmp = E(); destroy(x); destroy(y); tmp )
+      let t2 = body[^1].typ
+      let tmp = getTemp(c, t2, body.info)
+      when not scopeBasedDestruction:
+        c.addTopVar(tmp)
+      # the tmp does not have to be initialized
+      var n = newNodeIT(nkStmtListExpr, body.info, t2)
+      n.add newTree(nkFastAsgn, tmp, body[^1])
+      for i in countdown(c.scopeDestroys.high, oldTmpDestroysLen):
+        n.add c.scopeDestroys[i]
+      n.add tmp
+      body[^1] = n
+      #c.scopeDestroys.add genDestroy(c, tmp)
+  else:
+    # unstructured control flow was used, use a 'try finally' to ensure
+    # destruction:
+    if isEmptyType(t):
+      var n = newNodeI(nkStmtList, body.info)
+      for i in countdown(c.scopeDestroys.high, oldTmpDestroysLen):
+        n.add c.scopeDestroys[i]
+      body[^1] = newTryFinally(body[^1], n)
+    else:
+      # fun ahead: We have to transform (x; y; E()) into
+      # ((try: tmp = (x; y; E()); finally: destroy(x); destroy(y)); tmp )
+      let t2 = body[^1].typ
+      let tmp = getTemp(c, t2, body.info)
+      when not scopeBasedDestruction:
+        c.addTopVar(tmp)
+      # the tmp does not have to be initialized
+      var fin = newNodeI(nkStmtList, body.info)
+      for i in countdown(c.scopeDestroys.high, oldTmpDestroysLen):
+        fin.add c.scopeDestroys[i]
+      var n = newNodeIT(nkStmtListExpr, body.info, t2)
+      n.add newTryFinally(newTree(nkFastAsgn, tmp, body[^1]), fin)
+      n.add tmp
+      body[^1] = n
+      #c.scopeDestroys.add genDestroy(c, tmp)
+
+  c.scopeDestroys.setLen oldTmpDestroysLen
+
+proc handleNested(n, dest: PNode; c: var Con; mode: ProcessMode): PNode =
+  template processCall(node: PNode): PNode =
+    if node.typ == nil or dest == nil:
+      p(node, c, mode)
+    else:
+      moveOrCopy(dest, node, c)
+
+  proc handleScope(n, dest: PNode; t: PType;
+                   takeOver: Natural; c: var Con; mode: ProcessMode): PNode =
+    let oldHasUnstructuredCf = c.hasUnstructuredCf
+    let oldTmpDestroysLen = c.scopeDestroys.len
+    result = shallowCopy(n)
+    for i in 0..<takeOver:
+      result[i] = n[i]
+    let last = n.len - 1
+    for i in takeOver..<last:
+      result[i] = p(n[i], c, normal)
+
+    # if we have an expression producing a temporary, we must
+    # not destroy it too early:
+    if isEmptyType(t):
+      result[last] = processCall(n[last])
+      if c.scopeDestroys.len > oldTmpDestroysLen:
+        handleTmpDestroys(c, result, t, oldHasUnstructuredCf, oldTmpDestroysLen)
+    else:
+      setLen(result.sons, last)
+      if c.scopeDestroys.len > oldTmpDestroysLen:
+        handleTmpDestroys(c, result, t, oldHasUnstructuredCf, oldTmpDestroysLen)
+      if result.kind != nkFinally:
+        result.add processCall(n[last])
+      else:
+        result = newTree(nkStmtListExpr, result, processCall(n[last]))
+        result.typ = t
+
   case n.kind
   of nkStmtList, nkStmtListExpr:
     if n.len == 0: return n
-    result = copyNode(n)
-    for i in 0..<n.len-1:
-      result.add p(n[i], c, normal)
-    template node: untyped = n[^1]
-    result.add processCall
+    result = shallowCopy(n)
+    let last = n.len - 1
+    for i in 0..<last:
+      result[i] = p(n[i], c, normal)
+    result[last] = processCall(n[last])
+    # A statement list does not introduce a scope, the AST can
+    # contain silly nested statement lists.
+    #result = handleScope(n, dest, n.typ, 0, c, mode)
   of nkBlockStmt, nkBlockExpr:
-    result = copyNode(n)
-    result.add n[0]
-    template node: untyped = n[1]
-    result.add processCall
+    result = handleScope(n, dest, n.typ, 1, c, mode)
   of nkIfStmt, nkIfExpr:
     result = copyNode(n)
     for son in n:
-      var branch = copyNode(son)
-      if son.kind in {nkElifBranch, nkElifExpr}:
-        template node: untyped = son[1]
-        branch.add p(son[0], c, normal) #The condition
-        branch.add if node.typ == nil: p(node, c, normal) #noreturn
-                   else: processCall
-      else:
-        template node: untyped = son[0]
-        branch.add if node.typ == nil: p(node, c, normal) #noreturn
-                   else: processCall
-      result.add branch
+      result.add handleScope(son, dest, son[^1].typ, 0, c, mode)
   of nkCaseStmt:
     result = copyNode(n)
     result.add p(n[0], c, normal)
     for i in 1..<n.len:
-      var branch: PNode
-      if n[i].kind == nkOfBranch:
-        branch = n[i] # of branch conditions are constants
-        template node: untyped = n[i][^1]
-        branch[^1] = if node.typ == nil: p(node, c, normal) #noreturn
-                     else: processCall
-      elif n[i].kind in {nkElifBranch, nkElifExpr}:
-        branch = copyNode(n[i])
-        branch.add p(n[i][0], c, normal) #The condition
-        template node: untyped = n[i][1]
-        branch.add if node.typ == nil: p(node, c, normal) #noreturn
-                   else: processCall
-      else:
-        branch = copyNode(n[i])
-        template node: untyped = n[i][0]
-        branch.add if node.typ == nil: p(node, c, normal) #noreturn
-                   else: processCall
-      result.add branch
+      result.add handleScope(n[i], dest, n[i][^1].typ, n[i].len - 1, c, mode)
   of nkWhen: # This should be a "when nimvm" node.
     result = copyTree(n)
-    template node: untyped = n[1][0]
-    result[1][0] = processCall
+    result[1][0] = handleScope(n[1][0], dest, n[1][0][^1].typ, 0, c, mode)
+  of nkWhileStmt:
+    #result = copyNode(n)
+    inc c.inLoop
+    result = handleScope(n, dest, nil, 0, c, mode)
+    #result.add p(n[0], c, normal)
+    #result.add p(n[1], c, normal)
+    dec c.inLoop
   else: assert(false)
 
 proc ensureDestruction(arg: PNode; c: var Con): PNode =
@@ -439,9 +531,23 @@ proc ensureDestruction(arg: PNode; c: var Con): PNode =
     # This was already done in the sink parameter handling logic.
     result = newNodeIT(nkStmtListExpr, arg.info, arg.typ)
     let tmp = getTemp(c, arg.typ, arg.info)
-    result.add genSink(c, tmp, arg)
-    result.add tmp
-    c.destroys.add genDestroy(c, tmp)
+    when not scopeBasedDestruction:
+      c.addTopVar(tmp)
+      result.add genSink(c, tmp, arg)
+      result.add tmp
+      c.destroys.add genDestroy(c, tmp)
+    else:
+      # if we're inside a dangerous 'or' or 'and' expression, we
+      # do need to initialize it. 'elif' is not among this problem
+      # as we have a separate scope for 'elif' to attach the destructors to.
+      if c.inDangerousBranch == 0 and c.hasUnstructuredCf == 0:
+        tmp.sym.flags.incl sfNoInit
+      c.addTopVar(tmp)
+      # since we do not initialize these temporaries anymore, we
+      # use raw assignments instead of =sink:
+      result.add newTree(nkFastAsgn, tmp, arg)
+      result.add tmp
+      c.scopeDestroys.add genDestroy(c, tmp)
   else:
     result = arg
 
@@ -486,10 +592,49 @@ proc cycleCheck(n: PNode; c: var Con) =
       message(c.graph.config, n.info, warnCycleCreated, msg)
       break
 
+proc pVarTopLevel(v: PNode; c: var Con; ri, res: PNode) =
+  # move the variable declaration to the top of the frame:
+  if not containsOrIncl(c.declaredVars, v.sym.id):
+    c.addTopVar v
+  if isUnpackedTuple(v):
+    if c.inLoop > 0:
+      # unpacked tuple needs reset at every loop iteration
+      res.add newTree(nkFastAsgn, v, genDefaultCall(v.typ, c, v.info))
+  elif sfThread notin v.sym.flags:
+    # do not destroy thread vars for now at all for consistency.
+    c.destroys.add genDestroy(c, v)
+  if ri.kind == nkEmpty and c.inLoop > 0:
+    res.add moveOrCopy(v, genDefaultCall(v.typ, c, v.info), c)
+  elif ri.kind != nkEmpty:
+    res.add moveOrCopy(v, ri, c)
+
+proc pVarScoped(v: PNode; c: var Con; ri, res: PNode) =
+  if not containsOrIncl(c.declaredVars, v.sym.id):
+    c.addTopVar(v)
+  if isUnpackedTuple(v):
+    if c.inLoop > 0:
+      # unpacked tuple needs reset at every loop iteration
+      res.add newTree(nkFastAsgn, v, genDefaultCall(v.typ, c, v.info))
+  elif {sfGlobal, sfThread} * v.sym.flags == {sfGlobal}:
+    c.destroys.add genDestroy(c, v)
+  else:
+    # We always translate 'var v = f()' into bitcopies. If 'v' is in a loop,
+    # the destruction at the loop end will free the resources. Other assignments
+    # will destroy the old value inside 'v'. If we have 'var v' without an initial
+    # default value we translate it into 'var v = default()'. We translate
+    # 'var x = someGlobal' into 'var v = default(); `=`(v, someGlobal). The
+    # lack of copy constructors is really beginning to hurt us. :-(
+    #if c.inDangerousBranch == 0: v.sym.flags.incl sfNoInit
+    c.scopeDestroys.add genDestroy(c, v)
+  if ri.kind == nkEmpty and c.inLoop > 0:
+    res.add moveOrCopy(v, genDefaultCall(v.typ, c, v.info), c)
+  elif ri.kind != nkEmpty:
+    res.add moveOrCopy(v, ri, c)
+
 proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
   if n.kind in {nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkIfStmt,
-                nkIfExpr, nkCaseStmt, nkWhen}:
-    handleNested(n): p(node, c, mode)
+                nkIfExpr, nkCaseStmt, nkWhen, nkWhileStmt}:
+    result = handleNested(n, nil, c, mode)
   elif mode == sinkArg:
     if n.containsConstSeq:
       # const sequences are not mutable and so we need to pass a copy to the
@@ -522,6 +667,9 @@ proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
     elif n.kind in {nkObjDownConv, nkObjUpConv}:
       result = copyTree(n)
       result[0] = p(n[0], c, sinkArg)
+    elif n.typ == nil:
+      # 'raise X' can be part of a 'case' expression. Deal with it here:
+      result = p(n, c, normal)
     else:
       # copy objects that are not temporary but passed to a 'sink' parameter
       result = passCopyToSink(n, c)
@@ -554,12 +702,22 @@ proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
     of nkCallKinds:
       let parameters = n[0].typ
       let L = if parameters != nil: parameters.len else: 0
+
+      var isDangerous = false
+      if n[0].kind == nkSym and n[0].sym.magic in {mOr, mAnd}:
+        inc c.inDangerousBranch
+        isDangerous = true
+
       result = shallowCopy(n)
       for i in 1..<n.len:
         if i < L and isSinkTypeForParam(parameters[i]):
           result[i] = p(n[i], c, sinkArg)
         else:
           result[i] = p(n[i], c, normal)
+
+      if isDangerous:
+        dec c.inDangerousBranch
+
       if n[0].kind == nkSym and n[0].sym.magic in {mNew, mNewFinalize}:
         result[0] = copyTree(n[0])
         if c.graph.config.selectedGC in {gcHooks, gcArc, gcOrc}:
@@ -567,7 +725,8 @@ proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
           result = newTree(nkStmtList, destroyOld, result)
       else:
         result[0] = p(n[0], c, normal)
-
+      when scopeBasedDestruction:
+        if canRaise(n[0]): inc c.hasUnstructuredCf
       if mode == normal:
         result = ensureDestruction(result, c)
     of nkDiscardStmt: # Small optimization
@@ -589,20 +748,15 @@ proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
             let v = it[j]
             if v.kind == nkSym:
               if sfCompileTime in v.sym.flags: continue
-              # move the variable declaration to the top of the frame:
-              if not containsOrIncl(c.declaredVars, v.sym.id):
-                c.addTopVar v
-              # make sure it's destroyed at the end of the proc:
-              if not isUnpackedTuple(v) and sfThread notin v.sym.flags:
-                # do not destroy thread vars for now at all for consistency.
-                c.destroys.add genDestroy(c, v)
-              elif c.inLoop > 0:
-                # unpacked tuple needs reset at every loop iteration
-                result.add newTree(nkFastAsgn, v, genDefaultCall(v.typ, c, v.info))
-            if ri.kind == nkEmpty and c.inLoop > 0:
-              ri = genDefaultCall(v.typ, c, v.info)
-            if ri.kind != nkEmpty:
-              result.add moveOrCopy(v, ri, c)
+              when not scopeBasedDestruction:
+                pVarTopLevel(v, c, ri, result)
+              else:
+                pVarScoped(v, c, ri, result)
+            else:
+              if ri.kind == nkEmpty and c.inLoop > 0:
+                ri = genDefaultCall(v.typ, c, v.info)
+              if ri.kind != nkEmpty:
+                result.add moveOrCopy(v, ri, c)
         else: # keep the var but transform 'ri':
           var v = copyNode(n)
           var itCopy = copyNode(it)
@@ -634,6 +788,7 @@ proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
           result.add call
         else:
           let tmp = getTemp(c, n[0].typ, n.info)
+          c.addTopVar(tmp)
           var m = genCopyNoCheck(c, tmp, n[0])
           m.add p(n[0], c, normal)
           result = newTree(nkStmtList, genWasMoved(tmp, c), m)
@@ -648,17 +803,22 @@ proc p(n: PNode; c: var Con; mode: ProcessMode): PNode =
           result.add p(n[0], c, sinkArg)
         else:
           result.add copyNode(n[0])
+      inc c.hasUnstructuredCf
     of nkWhileStmt:
-      result = copyNode(n)
-      inc c.inLoop
-      result.add p(n[0], c, normal)
-      result.add p(n[1], c, normal)
-      dec c.inLoop
+      result = handleNested(n, nil, c, mode)
     of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
        nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
        nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
-       nkExportStmt, nkPragma, nkCommentStmt, nkBreakStmt, nkBreakState:
+       nkExportStmt, nkPragma, nkCommentStmt, nkBreakState:
+      result = n
+    of nkBreakStmt:
+      inc c.hasUnstructuredCf
       result = n
+    of nkReturnStmt:
+      result = shallowCopy(n)
+      for i in 0..<n.len:
+        result[i] = p(n[i], c, mode)
+      inc c.hasUnstructuredCf
     else:
       result = shallowCopy(n)
       for i in 0..<n.len:
@@ -721,7 +881,7 @@ proc moveOrCopy(dest, ri: PNode; c: var Con): PNode =
     else:
       result = genSink(c, dest, p(ri, c, sinkArg))
   of nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt:
-    handleNested(ri): moveOrCopy(dest, node, c)
+    result = handleNested(ri, dest, c, normal)
   else:
     if isAnalysableFieldAccess(ri, c.owner) and isLastRead(ri, c) and
         canBeMoved(c, dest.typ):
@@ -765,10 +925,6 @@ proc extractDestroysForTemporaries(c: Con, destroys: PNode): PNode =
       result.add destroys[i]
       destroys[i] = c.emptyNode
 
-proc reverseDestroys(destroys: seq[PNode]): seq[PNode] =
-  for i in countdown(destroys.len - 1, 0):
-    result.add destroys[i]
-
 proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
   if sfGeneratedOp in owner.flags or (owner.kind == skIterator and isInlineIterator(owner.typ)):
     return n
@@ -801,13 +957,16 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
   result = newNodeI(nkStmtList, n.info)
   if c.topLevelVars.len > 0:
     result.add c.topLevelVars
-  if c.destroys.len > 0:
-    c.destroys.sons = reverseDestroys(c.destroys.sons)
+  if c.destroys.len > 0 or c.scopeDestroys.len > 0:
+    reverse c.destroys.sons
+    var fin: PNode
     if owner.kind == skModule:
-      result.add newTryFinally(body, extractDestroysForTemporaries(c, c.destroys))
+      fin = newTryFinally(body, extractDestroysForTemporaries(c, c.destroys))
       g.globalDestructors.add c.destroys
     else:
-      result.add newTryFinally(body, c.destroys)
+      fin = newTryFinally(body, c.destroys)
+    for i in countdown(c.scopeDestroys.high, 0): fin[1][0].add c.scopeDestroys[i]
+    result.add fin
   else:
     result.add body
   dbg:
diff --git a/doc/destructors.rst b/doc/destructors.rst
index 6d3ed00dd..0d4a6c985 100644
--- a/doc/destructors.rst
+++ b/doc/destructors.rst
@@ -451,6 +451,7 @@ for expressions of type ``lent T`` or of type ``var T``.
     result = Tree(kids: kids)
     # converted into:
     `=sink`(result.kids, kids); wasMoved(kids)
+    `=destroy`(kids)
 
   proc `[]`*(x: Tree; i: int): lent Tree =
     result = x.kids[i]
diff --git a/lib/system.nim b/lib/system.nim
index 602fbc1f1..cd941b19f 100644
--- a/lib/system.nim
+++ b/lib/system.nim
@@ -1954,14 +1954,7 @@ template newException*(exceptn: typedesc, message: string;
                        parentException: ref Exception = nil): untyped =
   ## Creates an exception object of type ``exceptn`` and sets its ``msg`` field
   ## to `message`. Returns the new exception object.
-  when declared(owned):
-    var e: owned(ref exceptn)
-  else:
-    var e: ref exceptn
-  new(e)
-  e.msg = message
-  e.parent = parentException
-  e
+  (ref exceptn)(msg: message, parent: parentException)
 
 when hostOS == "standalone" and defined(nogc):
   proc nimToCStringConv(s: NimString): cstring {.compilerproc, inline.} =
diff --git a/tests/arc/tcontrolflow.nim b/tests/arc/tcontrolflow.nim
new file mode 100644
index 000000000..8fc9bf61d
--- /dev/null
+++ b/tests/arc/tcontrolflow.nim
@@ -0,0 +1,55 @@
+discard """
+  output: '''begin A
+elif
+destroyed
+end A
+begin false
+if
+destroyed
+end false
+begin true
+if
+end true
+'''
+  cmd: "nim c --gc:arc -d:danger $file"
+  disabled: "true"
+"""
+# we use the -d:danger switch to detect uninitialized stack
+# slots more reliably (there shouldn't be any, of course).
+
+# XXX Enable once scope based destruction works!
+
+type
+  Foo = object
+    id: int
+
+proc `=destroy`(x: var Foo) =
+  if x.id != 0:
+    echo "destroyed"
+    x.id = 0
+
+proc construct(): Foo = Foo(id: 3)
+
+proc elifIsEasy(cond: bool) =
+  echo "begin A"
+  if cond:
+    echo "if"
+  elif construct().id == 3:
+    echo "elif"
+  else:
+    echo "else"
+  echo "end A"
+
+elifIsEasy(false)
+
+
+proc orIsHard(cond: bool) =
+  echo "begin ", cond
+  if cond or construct().id == 3:
+    echo "if"
+  else:
+    echo "else"
+  echo "end ", cond
+
+orIsHard(false)
+orIsHard(true)
diff --git a/tests/destructor/tasync_prototype.nim b/tests/destructor/tasync_prototype.nim
index bd80adf0c..81fd824e9 100644
--- a/tests/destructor/tasync_prototype.nim
+++ b/tests/destructor/tasync_prototype.nim
@@ -32,7 +32,12 @@ proc serve(server: PAsyncHttpServer): PFutureBase =
       yield acceptAddrFut
       var fut = acceptAddrFut.value
 
+      # with the new scope based destruction, this cannot
+      # possibly work:
       var f {.cursor.} = processClient()
+      # It also seems to be the wrong way how to avoid the
+      # cycle. The cycle is caused by capturing the 'env'
+      # part from 'env.f'.
       when true:
         f.callback =
           proc () =
diff --git a/tests/destructor/tdestructor.nim b/tests/destructor/tdestructor.nim
index 5cfecea4e..b6d60323c 100644
--- a/tests/destructor/tdestructor.nim
+++ b/tests/destructor/tdestructor.nim
@@ -33,7 +33,7 @@ type
     p: pointer
 
 proc `=destroy`(o: var TMyObj) =
-  if o.p != nil: 
+  if o.p != nil:
     dealloc o.p
     o.p = nil
     echo "myobj destroyed"
diff --git a/tests/destructor/tmisc_destructors.nim b/tests/destructor/tmisc_destructors.nim
index 53c67e34b..73c54eab3 100644
--- a/tests/destructor/tmisc_destructors.nim
+++ b/tests/destructor/tmisc_destructors.nim
@@ -22,7 +22,7 @@ proc `=`(dest: var Foo, src: Foo) =
   assign_counter.inc
 
 proc test(): auto =
-  var a,b : Foo
+  var a, b: Foo
   return (a, b, Foo(boo: 5))
 
 var (a, b, _) = test()
diff --git a/tests/destructor/tv2_raise.nim b/tests/destructor/tv2_raise.nim
index 828d0a396..4ea94a864 100644
--- a/tests/destructor/tv2_raise.nim
+++ b/tests/destructor/tv2_raise.nim
@@ -2,7 +2,7 @@ discard """
   valgrind: true
   cmd: '''nim c -d:nimAllocStats --newruntime $file'''
   output: '''OK 3
-(allocCount: 8, deallocCount: 3)'''
+(allocCount: 8, deallocCount: 5)'''
 """
 
 import strutils, math