# # # 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) 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); reset(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, rodread, tables, modulegraphs, configuration const InterestingSyms = {skVar, skResult, skLet} type Con = object owner: PSym g: ControlFlowGraph jumpTargets: IntSet tmpObj: PType tmp: PSym destroys, topLevelVars: PNode toDropBit: Table[int, PSym] graph: ModuleGraph proc getTemp(c: var Con; typ: PType; info: TLineInfo): PNode = # XXX why are temps fields in an object here? let f = newSym(skField, getIdent(":d" & $c.tmpObj.n.len), c.owner, info) f.typ = typ rawAddField c.tmpObj, f result = rawDirectAccess(c.tmp, f) 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" template interestingSym(s: PSym): bool = s.owner == c.owner and s.kind in InterestingSyms and hasDestructor(s.typ) 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) if n[1].typ.isNil: # XXX toptree crashes without this workaround. Figure out why. return 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]) template genOp(opr, opname) = 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 result = newTree(nkCall, newSymNode(op), newTree(nkHiddenAddr, dest)) proc genSink(c: Con; t: PType; dest: PNode): PNode = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) genOp(if t.sink != nil: t.sink else: t.assignment, "=sink") proc genCopy(c: Con; t: PType; dest: PNode): PNode = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) genOp(t.assignment, "=") proc genDestroy(c: Con; t: PType; dest: PNode): PNode = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) genOp(t.destructor, "=destroy") proc addTopVar(c: var Con; v: PNode) = c.topLevelVars.add newTree(nkIdentDefs, v, emptyNode, emptyNode) proc dropBit(c: var Con; s: PSym): PSym = result = c.toDropBit.getOrDefault(s.id) assert result != nil proc registerDropBit(c: var Con; s: PSym) = let result = newSym(skTemp, getIdent(s.name.s & "_AliveBit"), c.owner, s.info) result.typ = getSysType(c.graph, s.info, tyBool) let trueVal = newIntTypeNode(nkIntLit, 1, result.typ) c.topLevelVars.add newTree(nkIdentDefs, newSymNode result, emptyNode, trueVal) c.toDropBit[s.id] = result # generate: # if not sinkParam_AliveBit: `=destroy`(sinkParam) c.destroys.add newTree(nkIfStmt, newTree(nkElifBranch, newSymNode result, genDestroy(c, s.typ, newSymNode s))) proc p(n: PNode; c: var Con): PNode template recurse(n, dest) = for i in 0.. 0: c.addTopVar(newSymNode c.tmp) result = newNodeI(nkStmtList, n.info) if c.topLevelVars.len > 0: result.add c.topLevelVars if c.destroys.len > 0: result.add newTryFinally(body, c.destroys) else: result.add body when defined(nimDebugDestroys): if owner.name.s == "main" or true: echo "------------------------------------" echo owner.name.s, " transformed to: " echo result