# # # The Nim Compiler # (c) Copyright 2020 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## Partition variables into different graphs. Used for ## Nim's write tracking and also for the cursor inference. ## The algorithm is a reinvention / variation of Steensgaard's ## algorithm. ## The used data structure is "union find" with path compression. import ast, types, lineinfos, options, msgs, renderer from trees import getMagic, whichPragma from wordrecg import wNoSideEffect from isolation_check import canAlias from typeallowed import isViewType type SubgraphFlag = enum isMutated, # graph might be mutated connectsConstParam, # graph is connected to a non-var parameter. VarFlag = enum ownsData, preventCursor VarIndexKind = enum isEmptyRoot, dependsOn, isRootOf VarIndex = object flags: set[VarFlag] case kind: VarIndexKind of isEmptyRoot: discard of dependsOn: parent: int of isRootOf: graphIndex: int sym: PSym aliveStart, aliveEnd: int # the range for which the variable is alive. MutationInfo* = object param: PSym mutatedHere, connectedVia: TLineInfo flags: set[SubgraphFlag] maxMutation, minConnection: int mutations: seq[int] Partitions* = object abstractTime: int s: seq[VarIndex] graphs: seq[MutationInfo] unanalysableMutation, performCursorInference: bool inAsgnSource, inConstructor, inNoSideEffectSection: int proc mutationAfterConnection(g: MutationInfo): bool {.inline.} = #echo g.maxMutation, " ", g.minConnection, " ", g.param g.maxMutation > g.minConnection proc `$`*(config: ConfigRef; g: MutationInfo): string = result = "" if g.flags == {isMutated, connectsConstParam}: result.add "\nan object reachable from '" result.add g.param.name.s result.add "' is potentially mutated" if g.mutatedHere != unknownLineInfo: result.add "\n" result.add config $ g.mutatedHere result.add " the mutation is here" if g.connectedVia != unknownLineInfo: result.add "\n" result.add config $ g.connectedVia result.add " is the statement that connected the mutation to the parameter" proc hasSideEffect*(c: var Partitions; info: var MutationInfo): bool = for g in mitems c.graphs: if g.flags == {isMutated, connectsConstParam} and mutationAfterConnection(g): info = g return true return false template isConstParam(a): bool = a.kind == skParam and a.typ.kind != tyVar proc variableId(c: Partitions; x: PSym): int {.inline.} = for i in 0 ..< c.s.len: if c.s[i].sym == x: return i return -1 proc registerVariable(c: var Partitions; n: PNode) = if n.kind == nkSym and variableId(c, n.sym) < 0: if isConstParam(n.sym): c.s.add VarIndex(kind: isRootOf, graphIndex: c.graphs.len, sym: n.sym, aliveStart: c.abstractTime, aliveEnd: c.abstractTime) c.graphs.add MutationInfo(param: n.sym, mutatedHere: unknownLineInfo, connectedVia: unknownLineInfo, flags: {connectsConstParam}, maxMutation: -1, minConnection: high(int), mutations: @[]) else: c.s.add VarIndex(kind: isEmptyRoot, sym: n.sym, aliveStart: c.abstractTime, aliveEnd: c.abstractTime) proc root(v: var Partitions; start: int): int = result = start var depth = 0 while v.s[result].kind == dependsOn: result = v.s[result].parent inc depth if depth > 0: # path compression: var it = start while v.s[it].kind == dependsOn: let next = v.s[it].parent v.s[it] = VarIndex(kind: dependsOn, parent: result, sym: v.s[it].sym, flags: v.s[it].flags, aliveStart: v.s[it].aliveStart, aliveEnd: v.s[it].aliveEnd) it = next proc potentialMutation(v: var Partitions; s: PSym; info: TLineInfo) = let id = variableId(v, s) if id >= 0: let r = root(v, id) case v.s[r].kind of isEmptyRoot: v.s[r] = VarIndex(kind: isRootOf, graphIndex: v.graphs.len, sym: v.s[r].sym, flags: v.s[r].flags, aliveStart: v.s[r].aliveStart, aliveEnd: v.s[r].aliveEnd) v.graphs.add MutationInfo(param: if isConstParam(s): s else: nil, mutatedHere: info, connectedVia: unknownLineInfo, flags: {isMutated}, maxMutation: v.abstractTime, minConnection: high(int), mutations: @[v.abstractTime]) of isRootOf: let g = addr v.graphs[v.s[r].graphIndex] if g.param == nil and isConstParam(s): g.param = s if v.abstractTime > g.maxMutation: g.mutatedHere = info g.maxMutation = v.abstractTime g.flags.incl isMutated g.mutations.add v.abstractTime else: assert false, "cannot happen" else: v.unanalysableMutation = true proc connect(v: var Partitions; a, b: PSym; info: TLineInfo) = let aid = variableId(v, a) if aid < 0: return let bid = variableId(v, b) if bid < 0: return let ra = root(v, aid) let rb = root(v, bid) if ra != rb: var param = PSym(nil) if isConstParam(a): param = a elif isConstParam(b): param = b let paramFlags = if param != nil: {connectsConstParam} else: {} # for now we always make 'rb' the slave and 'ra' the master: var rbFlags: set[SubgraphFlag] = {} var mutatedHere = unknownLineInfo var mut = 0 var con = v.abstractTime var gb: ptr MutationInfo = nil if v.s[rb].kind == isRootOf: gb = addr v.graphs[v.s[rb].graphIndex] if param == nil: param = gb.param mutatedHere = gb.mutatedHere rbFlags = gb.flags mut = gb.maxMutation con = min(con, gb.minConnection) v.s[rb] = VarIndex(kind: dependsOn, parent: ra, sym: v.s[rb].sym, flags: v.s[rb].flags, aliveStart: v.s[rb].aliveStart, aliveEnd: v.s[rb].aliveEnd) case v.s[ra].kind of isEmptyRoot: v.s[ra] = VarIndex(kind: isRootOf, graphIndex: v.graphs.len, sym: v.s[ra].sym, flags: v.s[ra].flags, aliveStart: v.s[ra].aliveStart, aliveEnd: v.s[ra].aliveEnd) v.graphs.add MutationInfo(param: param, mutatedHere: mutatedHere, connectedVia: info, flags: paramFlags + rbFlags, maxMutation: mut, minConnection: con, mutations: if gb != nil: gb.mutations else: @[]) of isRootOf: var g = addr v.graphs[v.s[ra].graphIndex] if g.param == nil: g.param = param if g.mutatedHere == unknownLineInfo: g.mutatedHere = mutatedHere g.minConnection = min(g.minConnection, con) g.connectedVia = info g.flags.incl paramFlags + rbFlags if gb != nil: g.mutations.add gb.mutations else: assert false, "cannot happen" proc allRoots(n: PNode; result: var seq[PSym]; followDotExpr = true) = case n.kind of nkSym: if n.sym.kind in {skParam, skVar, skTemp, skLet, skResult, skForVar}: result.add(n.sym) of nkDotExpr, nkDerefExpr, nkBracketExpr, nkHiddenDeref, nkCheckedFieldExpr, nkAddr, nkHiddenAddr: if followDotExpr: allRoots(n[0], result, followDotExpr) of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkConv, nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkCast, nkObjUpConv, nkObjDownConv: if n.len > 0: allRoots(n.lastSon, result, followDotExpr) of nkCaseStmt, nkObjConstr: for i in 1.. 1: allRoots(n[1], result, followDotExpr) else: let m = getMagic(n) case m of mNone: if n[0].typ.isNil: return var typ = n[0].typ if typ != nil: typ = skipTypes(typ, abstractInst) if typ.kind != tyProc: typ = nil else: assert(typ.len == typ.n.len) for i in 1 ..< n.len: let it = n[i] if typ != nil and i < typ.len: assert(typ.n[i].kind == nkSym) let paramType = typ.n[i].typ if not paramType.isCompileTimeOnly and not typ.sons[0].isEmptyType and canAlias(paramType, typ.sons[0]): allRoots(it, result, followDotExpr) else: allRoots(it, result, followDotExpr) of mSlice: allRoots(n[1], result, followDotExpr) else: discard "harmless operation" else: discard "nothing to do" proc analyseAsgn(c: var Partitions; dest: var VarIndex; n: PNode) = case n.kind of nkEmpty, nkCharLit..nkNilLit: # primitive literals including the empty are harmless: discard of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkCast, nkConv: analyseAsgn(c, dest, n[1]) of nkIfStmt, nkIfExpr: for i in 0.. 0: analyseAsgn(c, dest, n[^1]) of nkClosure: for i in 1.. 0: dest.flags.incl ownsData of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr, nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkAddr, nkHiddenAddr: analyseAsgn(c, dest, n[0]) of nkCallKinds: if hasDestructor(n.typ): # calls do construct, what we construct must be destroyed, # so dest cannot be a cursor: dest.flags.incl ownsData elif n.typ.kind in {tyLent, tyVar}: # we know the result is derived from the first argument: var roots: seq[PSym] allRoots(n[1], roots) for r in roots: connect(c, dest.sym, r, n[1].info) else: let magic = if n[0].kind == nkSym: n[0].sym.magic else: mNone # this list is subtle, we try to answer the question if after 'dest = f(src)' # there is a connection betwen 'src' and 'dest' so that mutations to 'src' # also reflect 'dest': if magic in {mNone, mMove, mSlice, mAppendStrCh, mAppendStrStr, mAppendSeqElem, mArrToSeq}: for i in 1..= 0: c.s[vid].flags.incl preventCursor proc rhsIsSink(c: var Partitions, n: PNode) = if n.kind == nkSym and n.typ.skipTypes(abstractInst-{tyOwned}).kind == tyRef: discard "do no pessimize simple refs further, injectdestructors.nim will prevent moving from it" else: var roots: seq[PSym] allRoots(n, roots, followDotExpr = false) # let x = cursor? --> treat it like a sink parameter for r in roots: noCursor(c, r) proc deps(c: var Partitions; dest, src: PNode) = var targets, sources: seq[PSym] allRoots(dest, targets) allRoots(src, sources) proc wrap(t: PType): bool {.nimcall.} = t.kind in {tyRef, tyPtr} let destIsComplex = types.searchTypeFor(dest.typ, wrap) or isViewType(dest.typ) for t in targets: if dest.kind != nkSym and c.inNoSideEffectSection == 0: potentialMutation(c, t, dest.info) if destIsComplex: for s in sources: connect(c, t, s, dest.info) if c.performCursorInference and src.kind != nkEmpty: if dest.kind == nkSym: let vid = variableId(c, dest.sym) if vid >= 0: analyseAsgn(c, c.s[vid], src) # do not borrow from a different local variable, this is easier # than tracking reassignments, consider 'var cursor = local; local = newNode()' if src.kind == nkSym: if (src.sym.kind in {skVar, skResult, skTemp} or (src.sym.kind in {skLet, skParam, skForVar} and hasDisabledAsgn(src.sym.typ))): c.s[vid].flags.incl preventCursor elif src.sym.kind in {skVar, skResult, skTemp, skLet, skForVar}: # XXX: we need to compute variable alive ranges before doing anything else: let srcid = variableId(c, src.sym) if srcid >= 0 and preventCursor in c.s[srcid].flags: # you cannot borrow from a local that lives shorter than 'vid': if c.s[srcid].aliveStart > c.s[vid].aliveStart or c.s[srcid].aliveEnd < c.s[vid].aliveEnd: c.s[vid].flags.incl preventCursor if src.kind == nkSym and hasDestructor(src.typ): rhsIsSink(c, src) proc traverse(c: var Partitions; n: PNode) = inc c.abstractTime case n.kind of nkLetSection, nkVarSection: for child in n: let last = lastSon(child) traverse(c, last) if child.kind == nkVarTuple and last.kind in {nkPar, nkTupleConstr}: if child.len-2 != last.len: return for i in 0..= 0: c.s[id].aliveEnd = max(c.s[id].aliveEnd, c.abstractTime) of nkNone..pred(nkSym), succ(nkSym)..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef, nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo, nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt, nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr: dec c.abstractTime discard "do not follow the construct" of nkCallKinds: for child in n: traverse(c, child) let parameters = n[0].typ let L = if parameters != nil: parameters.len else: 0 for i in 1.. 0: for i in 0.. 0: for i in 1..