# # # 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) 2 x = f() `=sink`(x, f()) 3 x = lastReadOf z `=sink`(x, z); wasMoved(z) 3.2 x = path z; body ``x = bitwiseCopy(path z);`` do not emit `=destroy(x)`. Note: body must not mutate ``z`` nor ``x``. All assignments to ``x`` must be of the form ``path z`` but the ``z`` can differ. Neither ``z`` nor ``x`` can have the flag ``sfAddrTaken`` to ensure no other aliasing is going on. 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) Rule 3.2 describes a "cursor" variable, a variable that is only used as a view into some data structure. See ``compiler/cursors.nim`` for details. Note: In order to avoid the very common combination ``reset(x); =sink(x, y)`` for variable definitions we must turn "the first sink/assignment" operation into a copyMem. This is harder than it looks: while true: try: if cond: break # problem if we run destroy(x) here :-/ var x = f() finally: destroy(x) And the C++ optimizers don't sweat to optimize it for us, so we don't have to do it. ]# 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 uninit: IntSet # set of uninit'ed vars uninitComputed: bool proc isLastRead(s: PSym; c: var Con; pc, comesFrom: int): int = var pc = pc 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. return high(int) inc pc of use: if c.g[pc].sym == s: c.otherRead = c.g[pc].n return -1 inc pc of goto: pc = pc + c.g[pc].dest of fork: # every branch must lead to the last read of the location: var variantA = isLastRead(s, c, pc+1, pc) if variantA < 0: return -1 let variantB = isLastRead(s, c, pc + c.g[pc].dest, pc) if variantB < 0: return -1 elif variantA == high(int): variantA = variantB pc = variantA of InstrKind.join: let dest = pc + c.g[pc].dest if dest == comesFrom: return pc + 1 inc pc return pc 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 when true: result = isLastRead(n.sym, c, instr+1, -1) >= 0 else: 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. 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 of InstrKind.join: inc pc #echo c.graph.config $ n.info, " last read here!" return true proc initialized(code: ControlFlowGraph; pc: int, init, uninit: var IntSet; comesFrom: int): int = ## Computes the set of definitely initialized variables accross all code paths ## as an IntSet of IDs. var pc = pc while pc < code.len: case code[pc].kind of goto: pc = pc + code[pc].dest of fork: let target = pc + code[pc].dest var initA = initIntSet() var initB = initIntSet() let pcA = initialized(code, pc+1, initA, uninit, pc) discard initialized(code, target, initB, uninit, pc) # we add vars if they are in both branches: for v in initA: if v in initB: init.incl v pc = pcA+1 of InstrKind.join: let target = pc + code[pc].dest if comesFrom == target: return pc inc pc of use: let v = code[pc].sym if v.kind != skParam and v.id notin init: # attempt to read an uninit'ed variable uninit.incl v.id inc pc of def: let v = code[pc].sym init.incl v.id inc pc return pc 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: # do not patch the builtin type bound operators for seqs: let dest = s.typ.sons[1].skipTypes(abstractVar) if dest.kind != tySequence: 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 elif ri.kind == nkSym and ri.sym.kind == skParam and ri.sym.typ.kind != tySink: m.add "; try to make " m.add renderTree(ri) m.add " a 'sink' parameter" 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 = when false: if t.kind != tyString: echo "this one ", c.graph.config$dest.info, " for ", typeToString(t, preferDesc) debug t.sink.typ.sons[2] echo t.sink.id, " owner ", t.id quit 1 let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) let op = if t.sink != nil: t.sink else: t.assignment if op != nil: genOp(op, "=sink", ri) else: # in rare cases only =destroy exists but no sink or assignment # (see Pony object in tmove_objconstr.nim) # we generate a fast assignment in this case: result = newTree(nkFastAsgn, dest) proc genCopy(c: Con; t: PType; dest, ri: PNode): PNode = if tfHasOwned in t.flags: checkForErrorPragma(c, t, ri, "=") 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