# # # The Nim Compiler # (c) Copyright 2017 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## Injects destructor calls into Nim code as well as ## an optimizer that optimizes copies to moves. This is implemented as an ## AST to AST transformation so that every backend benefits from it. ## Rules for destructor injections: ## ## foo(bar(X(), Y())) ## X and Y get destroyed after bar completes: ## ## foo( (tmpX = X(); tmpY = Y(); tmpBar = bar(tmpX, tmpY); ## destroy(tmpX); destroy(tmpY); ## tmpBar)) ## destroy(tmpBar) ## ## var x = f() ## body ## ## is the same as: ## ## var x; ## try: ## move(x, f()) ## finally: ## destroy(x) ## ## But this really just an optimization that tries to avoid to ## introduce too many temporaries, the 'destroy' is caused by ## the 'f()' call. No! That is not true for 'result = f()'! ## ## x = y where y is read only once ## is the same as: move(x, y) ## ## Actually the more general rule is: The *last* read of ``y`` ## can become a move if ``y`` is the result of a construction. ## ## We also need to keep in mind here that the number of reads is ## control flow dependent: ## let x = foo() ## while true: ## y = x # only one read, but the 2nd iteration will fail! ## This also affects recursions! Only usages that do not cross ## a loop boundary (scope) and are not used in function calls ## are safe. ## ## ## x = f() is the same as: move(x, f()) ## ## x = y ## is the same as: copy(x, y) ## ## Reassignment works under this scheme: ## var x = f() ## x = y ## ## is the same as: ## ## var x; ## try: ## move(x, f()) ## copy(x, y) ## finally: ## destroy(x) ## ## result = f() must not destroy 'result'! ## ## The produced temporaries clutter up the code and might lead to ## inefficiencies. A better strategy is to collect all the temporaries ## in a single object that we put into a single try-finally that ## surrounds the proc body. This means the code stays quite efficient ## when compiled to C. In fact, we do the same for variables, so ## destructors are called when the proc returns, not at scope exit! ## This makes certains idioms easier to support. (Taking the slice ## of a temporary object.) ## ## foo(bar(X(), Y())) ## X and Y get destroyed after bar completes: ## ## var tmp: object ## foo( (move tmp.x, X(); move tmp.y, Y(); tmp.bar = bar(tmpX, tmpY); ## tmp.bar)) ## destroy(tmp.bar) ## destroy(tmp.x); destroy(tmp.y) ## ##[ From https://github.com/nim-lang/Nim/wiki/Destructors Rule Pattern Transformed into ---- ------- ---------------- 1.1 var x: T; stmts var x: T; try stmts finally: `=destroy`(x) 1.2 var x: sink T; stmts var x: sink T; stmts; ensureEmpty(x) 2 x = f() `=sink`(x, f()) 3 x = lastReadOf z `=sink`(x, z); wasMoved(z) 4.1 y = sinkParam `=sink`(y, sinkParam) 4.2 x = y `=`(x, y) # a copy 5.1 f_sink(g()) f_sink(g()) 5.2 f_sink(y) f_sink(copy y); # copy unless we can see it's the last read 5.3 f_sink(move y) f_sink(y); wasMoved(y) # explicit moves empties 'y' 5.4 f_noSink(g()) var tmp = bitwiseCopy(g()); f(tmp); `=destroy`(tmp) Remarks: Rule 1.2 is not yet implemented because ``sink`` is currently not allowed as a local variable. ``move`` builtin needs to be implemented. ]## import intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees, strutils, options, dfa, lowerings, tables, modulegraphs, msgs, lineinfos, parampatterns const InterestingSyms = {skVar, skResult, skLet} type Con = object owner: PSym g: ControlFlowGraph jumpTargets: IntSet destroys, topLevelVars: PNode graph: ModuleGraph emptyNode: PNode otherRead: PNode proc isHarmlessVar*(s: PSym; c: Con): bool = # 's' is harmless if it used only once and its # definition/usage are not split by any labels: # # let s = foo() # while true: # a[i] = s # # produces: # # def s # L1: # use s # goto L1 # # let s = foo() # if cond: # a[i] = s # else: # a[j] = s # # produces: # # def s # fork L2 # use s # goto L3 # L2: # use s # L3 # # So this analysis is for now overly conservative, but correct. var defsite = -1 var usages = 0 for i in 0.. 1: return false inc usages #of useWithinCall: # if c.g[i].sym == s: return false of goto, fork: discard "we do not perform an abstract interpretation yet" result = usages <= 1 proc isLastRead(n: PNode; c: var Con): bool = # first we need to search for the instruction that belongs to 'n': doAssert n.kind == nkSym c.otherRead = nil var instr = -1 for i in 0..= c.g.len: return true let s = n.sym var pcs: seq[int] = @[instr+1] var takenGotos: IntSet var takenForks = initIntSet() while pcs.len > 0: var pc = pcs.pop takenGotos = initIntSet() while pc < c.g.len: case c.g[pc].kind of def: if c.g[pc].sym == s: # the path lead to a redefinition of 's' --> abandon it. when false: # Too complex thinking ahead: In reality it is enough to find # the 'def x' here on the current path to make the 'use x' valid. # but for this the definition needs to dominate the usage: var dominates = true for j in pc+1 .. instr: # not within the same basic block? if c.g[j].kind in {goto, fork} and (j + c.g[j].dest) in (pc+1 .. instr): #if j in c.jumpTargets: dominates = false if dominates: break break inc pc of use: if c.g[pc].sym == s: c.otherRead = c.g[pc].n return false inc pc of goto: # we must leave endless loops eventually: if not takenGotos.containsOrIncl(pc): pc = pc + c.g[pc].dest else: inc pc of fork: # we follow the next instruction but push the dest onto our "work" stack: if not takenForks.containsOrIncl(pc): pcs.add pc + c.g[pc].dest inc pc #echo c.graph.config $ n.info, " last read here!" return true template interestingSym(s: PSym): bool = s.owner == c.owner and s.kind in InterestingSyms and hasDestructor(s.typ) template isUnpackedTuple(s: PSym): bool = ## we move out all elements of unpacked tuples, ## hence unpacked tuples themselves don't need to be destroyed s.kind == skTemp and s.typ.kind == tyTuple proc patchHead(n: PNode) = if n.kind in nkCallKinds and n[0].kind == nkSym and n.len > 1: let s = n[0].sym if s.name.s[0] == '=' and s.name.s in ["=sink", "=", "=destroy"]: if sfFromGeneric in s.flags: excl(s.flags, sfFromGeneric) patchHead(s.getBody) let t = n[1].typ.skipTypes({tyVar, tyLent, tyGenericInst, tyAlias, tySink, tyInferred}) template patch(op, field) = if s.name.s == op and field != nil and field != s: n.sons[0].sym = field patch "=sink", t.sink patch "=", t.assignment patch "=destroy", t.destructor for x in n: patchHead(x) proc patchHead(s: PSym) = if sfFromGeneric in s.flags: patchHead(s.ast[bodyPos]) proc checkForErrorPragma(c: Con; t: PType; ri: PNode; opname: string) = var m = "'" & opname & "' is not available for type <" & typeToString(t) & ">" if opname == "=" and ri != nil: m.add "; requires a copy because it's not the last read of '" m.add renderTree(ri) m.add '\'' if c.otherRead != nil: m.add "; another read is done here: " m.add c.graph.config $ c.otherRead.info localError(c.graph.config, ri.info, errGenerated, m) proc makePtrType(c: Con, baseType: PType): PType = result = newType(tyPtr, c.owner) addSonSkipIntLit(result, baseType) template genOp(opr, opname, ri) = let op = opr if op == nil: globalError(c.graph.config, dest.info, "internal error: '" & opname & "' operator not found for type " & typeToString(t)) elif op.ast[genericParamsPos].kind != nkEmpty: globalError(c.graph.config, dest.info, "internal error: '" & opname & "' operator is generic") patchHead op if sfError in op.flags: checkForErrorPragma(c, t, ri, opname) let addrExp = newNodeIT(nkHiddenAddr, dest.info, makePtrType(c, dest.typ)) addrExp.add(dest) result = newTree(nkCall, newSymNode(op), addrExp) proc genSink(c: Con; t: PType; dest, ri: PNode): PNode = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) genOp(if t.sink != nil: t.sink else: t.assignment, "=sink", ri) proc genCopy(c: Con; t: PType; dest, ri: PNode): PNode = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) genOp(t.assignment, "=", ri) proc genDestroy(c: Con; t: PType; dest: PNode): PNode = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) genOp(t.destructor, "=destroy", nil) proc addTopVar(c: var Con; v: PNode) = c.topLevelVars.add newTree(nkIdentDefs, v, c.emptyNode, c.emptyNode) 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 p(n: PNode; c: var Con): PNode template recurse(n, dest) = for i in 0.. 0: result.add c.topLevelVars if c.destroys.len > 0: result.add newTryFinally(body, c.destroys) else: result.add body when defined(nimDebugDestroys): if true: echo "------------------------------------" echo owner.name.s, " transformed to: " echo result