# # # 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. ## See doc/destructors.rst for a spec of the implemented rewrite rules import intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, strutils, options, dfa, lowerings, tables, modulegraphs, msgs, lineinfos, parampatterns, sighashes type Con = object owner: PSym g: ControlFlowGraph jumpTargets: IntSet destroys, topLevelVars: PNode graph: ModuleGraph emptyNode: PNode otherRead: PNode inLoop: int uninit: IntSet # set of uninit'ed vars uninitComputed: bool const toDebug = "" # "server" # "serverNimAsyncContinue" template dbg(body) = when toDebug.len > 0: if c.owner.name.s == toDebug or toDebug == "always": body proc isLastRead(location: PNode; c: var Con; pc, comesFrom: int): int = var pc = pc while pc < c.g.len: case c.g[pc].kind of def: if defInstrTargets(c.g[pc], location): # the path lead to a redefinition of 's' --> abandon it. return high(int) inc pc of use: if useInstrTargets(c.g[pc], location): 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: let variantA = isLastRead(location, c, pc+1, pc) if variantA < 0: return -1 var variantB = isLastRead(location, c, pc + c.g[pc].dest, pc) if variantB < 0: return -1 elif variantB == high(int): variantB = variantA pc = variantB 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': c.otherRead = nil var instr = -1 let m = dfa.skipConvDfa(n) for i in 0..= c.g.len: return true result = isLastRead(n, c, instr+1, -1) >= 0 dbg: echo "ugh ", c.otherRead.isNil, " ", result proc initialized(code: ControlFlowGraph; pc: int, init, uninit: var IntSet; comesFrom: int): int = ## Computes the set of definitely initialized variables across 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 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 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 not isSinkType(ri.sym.typ): m.add "; try to make " m.add renderTree(ri) m.add " a 'sink' parameter" m.add "; routine: " m.add c.owner.name.s localError(c.graph.config, ri.info, errGenerated, m) proc makePtrType(c: Con, baseType: PType): PType = result = newType(tyPtr, c.owner) addSonSkipIntLit(result, baseType) proc genOp(c: Con; t: PType; kind: TTypeAttachedOp; dest, ri: PNode): PNode = var op = t.attachedOps[kind] if op == nil or op.ast[genericParamsPos].kind != nkEmpty: # give up and find the canonical type instead: let h = sighashes.hashType(t, {CoType, CoConsiderOwned, CoDistinct}) let canon = c.graph.canonTypes.getOrDefault(h) if canon != nil: op = canon.attachedOps[kind] if op == nil: globalError(c.graph.config, dest.info, "internal error: '" & AttachedOpToStr[kind] & "' operator not found for type " & typeToString(t)) elif op.ast[genericParamsPos].kind != nkEmpty: globalError(c.graph.config, dest.info, "internal error: '" & AttachedOpToStr[kind] & "' operator is generic") if sfError in op.flags: checkForErrorPragma(c, t, ri, AttachedOpToStr[kind]) let addrExp = newNodeIT(nkHiddenAddr, dest.info, makePtrType(c, dest.typ)) addrExp.add(dest) result = newTree(nkCall, newSymNode(op), addrExp) when false: proc preventMoveRef(dest, ri: PNode): bool = let lhs = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) var ri = ri if ri.kind in nkCallKinds and ri[0].kind == nkSym and ri[0].sym.magic == mUnown: ri = ri[1] let rhs = ri.typ.skipTypes({tyGenericInst, tyAlias, tySink}) result = lhs.kind == tyRef and rhs.kind == tyOwned proc canBeMoved(t: PType): bool {.inline.} = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) result = t.kind != tyRef and t.attachedOps[attachedSink] != nil proc genSink(c: Con; dest, ri: PNode): PNode = let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) let k = if t.attachedOps[attachedSink] != nil: attachedSink else: attachedAsgn if t.attachedOps[k] != nil: result = genOp(c, t, k, dest, 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 genCopyNoCheck(c: Con; dest, ri: PNode): PNode = let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) result = genOp(c, t, attachedAsgn, dest, ri) proc genCopy(c: var Con; dest, ri: PNode): PNode = let t = dest.typ if tfHasOwned in t.flags: # try to improve the error message here: if c.otherRead == nil: discard isLastRead(ri, c) checkForErrorPragma(c, t, ri, "=") genCopyNoCheck(c, dest, ri) proc genDestroy(c: Con; dest: PNode): PNode = let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) result = genOp(c, t, attachedDestructor, dest, 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 genWasMoved(n: PNode; c: var Con): PNode = result = newNodeI(nkCall, n.info) result.add(newSymNode(createMagic(c.graph, "wasMoved", mWasMoved))) result.add n #mWasMoved does not take the address proc genDefaultCall(t: PType; c: Con; info: TLineInfo): PNode = result = newNodeI(nkCall, info) result.add(newSymNode(createMagic(c.graph, "default", mDefault))) result.typ = t proc destructiveMoveVar(n: PNode; c: var Con): PNode = # generate: (let tmp = v; reset(v); tmp) # XXX: Strictly speaking we can only move if there is a ``=sink`` defined # or if no ``=sink`` is defined and also no assignment. result = newNodeIT(nkStmtListExpr, n.info, n.typ) var temp = newSym(skLet, getIdent(c.graph.cache, "blitTmp"), c.owner, n.info) temp.typ = n.typ var v = newNodeI(nkLetSection, n.info) let tempAsNode = newSymNode(temp) var vpart = newNodeI(nkIdentDefs, tempAsNode.info, 3) vpart[0] = tempAsNode vpart[1] = c.emptyNode vpart[2] = n add(v, vpart) result.add v result.add genWasMoved(skipConv(n), c) result.add tempAsNode proc sinkParamIsLastReadCheck(c: var Con, s: PNode) = assert s.kind == nkSym and s.sym.kind == skParam if not isLastRead(s, c): localError(c.graph.config, c.otherRead.info, "sink parameter `" & $s.sym.name.s & "` is already consumed at " & toFileLineCol(c. graph.config, s.info)) proc p(n: PNode; c: var Con): PNode proc pArg(arg: PNode; c: var Con; isSink: bool): PNode proc moveOrCopy(dest, ri: PNode; c: var Con): PNode 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) if hasDestructor(n.typ): var m = genCopy(c, tmp, n) m.add p(n, c) result.add m if isLValue(n): message(c.graph.config, n.info, hintPerformance, ("passing '$1' to a sink parameter introduces an implicit copy; " & "use 'move($1)' to prevent it") % $n) else: result.add newTree(nkAsgn, tmp, p(n, c)) result.add tmp proc isDangerousSeq(t: PType): bool {.inline.} = let t = t.skipTypes(abstractInst) result = t.kind == tySequence and tfHasOwned notin t[0].flags proc containsConstSeq(n: PNode): bool = if n.kind == nkBracket and n.len > 0 and n.typ != nil and isDangerousSeq(n.typ): return true result = false case n.kind of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv: result = containsConstSeq(n[1]) of nkObjConstr, nkClosure: for i in 1.. 0: ri = genDefaultCall(v.typ, c, v.info) if ri.kind != nkEmpty: let r = moveOrCopy(v, ri, c) result.add r else: # keep the var but transform 'ri': var v = copyNode(n) var itCopy = copyNode(it) for j in 0.. 0 and isDangerousSeq(ri.typ): result = genCopy(c, dest, ri) else: result = genSink(c, dest, ri) result.add p(ri, c) of nkObjConstr, nkTupleConstr, nkClosure, nkCharLit..nkNilLit: result = genSink(c, dest, ri) result.add p(ri, c) of nkSym: if isSinkParam(ri.sym): # Rule 3: `=sink`(x, z); wasMoved(z) sinkParamIsLastReadCheck(c, ri) var snk = genSink(c, dest, ri) snk.add ri result = newTree(nkStmtList, snk, genWasMoved(ri, c)) elif ri.sym.kind != skParam and ri.sym.owner == c.owner and isLastRead(ri, c) and canBeMoved(dest.typ): # Rule 3: `=sink`(x, z); wasMoved(z) var snk = genSink(c, dest, ri) snk.add ri result = newTree(nkStmtList, snk, genWasMoved(ri, c)) else: result = genCopy(c, dest, ri) result.add p(ri, c) of nkHiddenSubConv, nkHiddenStdConv, nkConv: result = moveOrCopy(dest, ri[1], c) if not sameType(ri.typ, ri[1].typ): let copyRi = copyTree(ri) copyRi[1] = result[^1] result[^1] = copyRi of nkObjDownConv, nkObjUpConv: result = moveOrCopy(dest, ri[0], c) let copyRi = copyTree(ri) copyRi[0] = result[^1] result[^1] = copyRi of nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt: handleNested(ri): moveOrCopy(dest, node, c) else: if isAnalysableFieldAccess(ri, c.owner) and isLastRead(ri, c) and canBeMoved(dest.typ): # Rule 3: `=sink`(x, z); wasMoved(z) var snk = genSink(c, dest, ri) snk.add ri result = newTree(nkStmtList, snk, genWasMoved(ri, c)) else: result = genCopy(c, dest, ri) result.add p(ri, c) proc computeUninit(c: var Con) = if not c.uninitComputed: c.uninitComputed = true c.uninit = initIntSet() var init = initIntSet() discard initialized(c.g, pc = 0, init, c.uninit, comesFrom = -1) proc injectDefaultCalls(n: PNode, c: var Con) = case n.kind of nkVarSection, nkLetSection: for it in n: if it.kind == nkIdentDefs and it[^1].kind == nkEmpty: computeUninit(c) for j in 0.. 0: result.add c.topLevelVars if c.destroys.len > 0: c.destroys.sons = reverseDestroys(c.destroys.sons) if owner.kind == skModule: result.add newTryFinally(body, extractDestroysForTemporaries(c, c.destroys)) g.globalDestructors.add c.destroys else: result.add newTryFinally(body, c.destroys) else: result.add body dbg: echo ">---------transformed-to--------->" echo result