diff options
Diffstat (limited to 'compiler/varpartitions.nim')
-rw-r--r-- | compiler/varpartitions.nim | 1019 |
1 files changed, 1019 insertions, 0 deletions
diff --git a/compiler/varpartitions.nim b/compiler/varpartitions.nim new file mode 100644 index 000000000..1711fea46 --- /dev/null +++ b/compiler/varpartitions.nim @@ -0,0 +1,1019 @@ +# +# +# 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, borrow checking 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. + +## We perform two passes over the AST: +## - Pass one (``computeLiveRanges``): collect livetimes of local +## variables and whether they are potentially re-assigned. +## - Pass two (``traverse``): combine local variables to abstract "graphs". +## Strict func checking: Ensure that graphs that are connected to +## const parameters are not mutated. +## Cursor inference: Ensure that potential cursors are not +## borrowed from locations that are connected to a graph +## that is mutated during the liveness of the cursor. +## (We track all possible mutations of a graph.) +## +## See https://nim-lang.github.io/Nim/manual_experimental.html#view-types-algorithm +## for a high-level description of how borrow checking works. + +import ast, types, lineinfos, options, msgs, renderer, typeallowed, modulegraphs +from trees import getMagic, isNoSideEffectPragma, stupidStmtListExpr +from isolation_check import canAlias + +when defined(nimPreviewSlimSystem): + import std/assertions + +type + AbstractTime = distinct int + +const + MaxTime = AbstractTime high(int) + MinTime = AbstractTime(-1) + +proc `<=`(a, b: AbstractTime): bool {.borrow.} +proc `<`(a, b: AbstractTime): bool {.borrow.} + +proc inc(x: var AbstractTime; diff = 1) {.borrow.} +proc dec(x: var AbstractTime; diff = 1) {.borrow.} + +proc `$`(x: AbstractTime): string {.borrow.} + +type + SubgraphFlag = enum + isMutated, # graph might be mutated + isMutatedDirectly, # graph is mutated directly by a non-var parameter. + isMutatedByVarParam, # graph is mutated by a var parameter. + connectsConstParam # graph is connected to a non-var parameter. + + VarFlag = enum + ownsData, + preventCursor, + isReassigned, + isConditionallyReassigned, + viewDoesMutate, + viewBorrowsFromConst + + VarIndexKind = enum + isEmptyRoot, + dependsOn, + isRootOf + + Connection = object + case kind: VarIndexKind + of isEmptyRoot: discard + of dependsOn: parent: int + of isRootOf: graphIndex: int + + VarIndex = object + con: Connection + flags: set[VarFlag] + sym: PSym + reassignedTo: int + aliveStart, aliveEnd: AbstractTime # the range for which the variable is alive. + borrowsFrom: seq[int] # indexes into Partitions.s + + MutationInfo* = object + param: PSym + mutatedHere, connectedVia: TLineInfo + flags: set[SubgraphFlag] + maxMutation, minConnection: AbstractTime + mutations: seq[AbstractTime] + + Goal* = enum + constParameters, + borrowChecking, + cursorInference + + Partitions* = object + abstractTime: AbstractTime + defers: seq[PNode] + processDefer: bool + s: seq[VarIndex] + graphs: seq[MutationInfo] + goals: set[Goal] + unanalysableMutation: bool + inAsgnSource, inConstructor, inNoSideEffectSection: int + inConditional, inLoop: int + inConvHasDestructor: int + owner: PSym + g: ModuleGraph + +proc mutationAfterConnection(g: MutationInfo): bool {.inline.} = + #echo g.maxMutation.int, " ", g.minConnection.int, " ", g.param + g.maxMutation > g.minConnection + +proc `$`*(config: ConfigRef; g: MutationInfo): string = + result = "" + if g.flags * {isMutated, connectsConstParam} == {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} == {isMutated, connectsConstParam} and + (mutationAfterConnection(g) or isMutatedDirectly in g.flags): + info = g + return true + return false + +template isConstParam(a): bool = a.kind == skParam and a.typ.kind notin {tyVar, tySink} + +proc variableId(c: Partitions; x: PSym): int = + for i in 0 ..< c.s.len: + if c.s[i].sym == x: return i + return -1 + +proc registerResult(c: var Partitions; n: PNode) = + if n.kind == nkSym: + c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0, + aliveStart: MaxTime, aliveEnd: c.abstractTime) + +proc registerParam(c: var Partitions; n: PNode) = + assert n.kind == nkSym + if isConstParam(n.sym): + c.s.add VarIndex(con: Connection(kind: isRootOf, graphIndex: c.graphs.len), + sym: n.sym, reassignedTo: 0, + aliveStart: c.abstractTime, aliveEnd: c.abstractTime) + c.graphs.add MutationInfo(param: n.sym, mutatedHere: unknownLineInfo, + connectedVia: unknownLineInfo, flags: {connectsConstParam}, + maxMutation: MinTime, minConnection: MaxTime, + mutations: @[]) + else: + c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0, + aliveStart: c.abstractTime, aliveEnd: c.abstractTime) + +proc registerVariable(c: var Partitions; n: PNode) = + if n.kind == nkSym and variableId(c, n.sym) < 0: + c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0, + aliveStart: c.abstractTime, aliveEnd: c.abstractTime) + +proc root(v: var Partitions; start: int): int = + result = start + var depth = 0 + while v.s[result].con.kind == dependsOn: + result = v.s[result].con.parent + inc depth + if depth > 0: + # path compression: + var it = start + while v.s[it].con.kind == dependsOn: + let next = v.s[it].con.parent + v.s[it].con = Connection(kind: dependsOn, parent: result) + it = next + +proc potentialMutation(v: var Partitions; s: PSym; level: int; info: TLineInfo) = + let id = variableId(v, s) + if id >= 0: + let r = root(v, id) + let flags = if s.kind == skParam: + if isConstParam(s): + {isMutated, isMutatedDirectly} + elif s.typ.kind == tyVar and level <= 1: + # varParam[i] = v is different from varParam[i][] = v + {isMutatedByVarParam} + else: + {isMutated} + else: + {isMutated} + + case v.s[r].con.kind + of isEmptyRoot: + v.s[r].con = Connection(kind: isRootOf, graphIndex: v.graphs.len) + v.graphs.add MutationInfo(param: if isConstParam(s): s else: nil, mutatedHere: info, + connectedVia: unknownLineInfo, flags: flags, + maxMutation: v.abstractTime, minConnection: MaxTime, + mutations: @[v.abstractTime]) + of isRootOf: + let g = addr v.graphs[v.s[r].con.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 flags + 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 = AbstractTime 0 + var con = v.abstractTime + var gb: ptr MutationInfo = nil + if v.s[rb].con.kind == isRootOf: + gb = addr v.graphs[v.s[rb].con.graphIndex] + if param == nil: param = gb.param + mutatedHere = gb.mutatedHere + rbFlags = gb.flags + mut = gb.maxMutation + con = min(con, gb.minConnection) + + v.s[rb].con = Connection(kind: dependsOn, parent: ra) + case v.s[ra].con.kind + of isEmptyRoot: + v.s[ra].con = Connection(kind: isRootOf, graphIndex: v.graphs.len) + 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].con.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 borrowFromConstExpr(n: PNode): bool = + case n.kind + of nkCharLit..nkNilLit: + result = true + of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, + nkCast, nkObjUpConv, nkObjDownConv: + result = borrowFromConstExpr(n.lastSon) + of nkCurly, nkBracket, nkPar, nkTupleConstr, nkObjConstr, nkClosure, nkRange: + result = true + for i in ord(n.kind == nkObjConstr)..<n.len: + if not borrowFromConstExpr(n[i]): return false + of nkCallKinds: + if getMagic(n) == mArrToSeq: + result = true + for i in 1..<n.len: + if not borrowFromConstExpr(n[i]): return false + else: + result = false + else: result = false + +proc pathExpr(node: PNode; owner: PSym): PNode = + #[ From the spec: + + - ``source`` itself is a path expression. + - Container access like ``e[i]`` is a path expression. + - Tuple access ``e[0]`` is a path expression. + - Object field access ``e.field`` is a path expression. + - ``system.toOpenArray(e, ...)`` is a path expression. + - Pointer dereference ``e[]`` is a path expression. + - An address ``addr e``, ``unsafeAddr e`` is a path expression. + - A type conversion ``T(e)`` is a path expression. + - A cast expression ``cast[T](e)`` is a path expression. + - ``f(e, ...)`` is a path expression if ``f``'s return type is a view type. + Because the view can only have been borrowed from ``e``, we then know + that owner of ``f(e, ...)`` is ``e``. + + Returns the owner of the path expression. Returns ``nil`` + if it is not a valid path expression. + ]# + var n = node + result = nil + while true: + case n.kind + of nkSym: + case n.sym.kind + of skParam, skTemp, skResult, skForVar: + if n.sym.owner == owner: result = n + of skVar: + if n.sym.owner == owner or sfThread in n.sym.flags: result = n + of skLet, skConst: + if n.sym.owner == owner or {sfThread, sfGlobal} * n.sym.flags != {}: + result = n + else: + discard + break + of nkDotExpr, nkDerefExpr, nkBracketExpr, nkHiddenDeref, + nkCheckedFieldExpr, nkAddr, nkHiddenAddr: + n = n[0] + of nkHiddenStdConv, nkHiddenSubConv, nkConv, nkCast, + nkObjUpConv, nkObjDownConv: + n = n.lastSon + of nkStmtList, nkStmtListExpr: + if n.len > 0 and stupidStmtListExpr(n): + n = n.lastSon + else: + break + of nkCallKinds: + if n.len > 1: + if (n.typ != nil and classifyViewType(n.typ) != noView) or getMagic(n) == mSlice: + n = n[1] + else: + break + else: + break + else: + break + # borrowFromConstExpr(n) is correct here because we need 'node' + # stripped off the path suffixes: + if result == nil and borrowFromConstExpr(n): + result = n + +const + RootEscapes = 1000 # in 'p(r)' we don't know what p does to our poor root. + # so we assume a high level of indirections + +proc allRoots(n: PNode; result: var seq[(PSym, int)]; level: int) = + case n.kind + of nkSym: + if n.sym.kind in {skParam, skVar, skTemp, skLet, skResult, skForVar}: + result.add((n.sym, level)) + + of nkDerefExpr, nkHiddenDeref: + allRoots(n[0], result, level+1) + of nkBracketExpr, nkDotExpr, nkCheckedFieldExpr, nkAddr, nkHiddenAddr: + allRoots(n[0], result, level) + + of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkConv, + nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkCast, + nkObjUpConv, nkObjDownConv: + if n.len > 0: + allRoots(n.lastSon, result, level) + of nkCaseStmt, nkObjConstr: + for i in 1..<n.len: + allRoots(n[i].lastSon, result, level) + of nkIfStmt, nkIfExpr: + for i in 0..<n.len: + allRoots(n[i].lastSon, result, level) + of nkBracket, nkTupleConstr, nkPar: + for i in 0..<n.len: + allRoots(n[i], result, level-1) + + of nkCallKinds: + if n.typ != nil and n.typ.kind in {tyVar, tyLent}: + if n.len > 1: + # XXX We really need the unwritten RFC here and distinguish between + # proc `[]`(x: var Container): var T # resizes the container + # and + # proc `[]`(x: Container): var T # only allows for slot mutation + allRoots(n[1], result, RootEscapes) + 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 + + for i in 1 ..< n.len: + let it = n[i] + if typ != nil and i < typ.n.len: + assert(typ.n[i].kind == nkSym) + let paramType = typ.n[i].typ + if not paramType.isCompileTimeOnly and not typ.returnType.isEmptyType and + canAlias(paramType, typ.returnType): + allRoots(it, result, RootEscapes) + else: + allRoots(it, result, RootEscapes) + + of mSlice: + allRoots(n[1], result, level+1) + else: + discard "harmless operation" + else: + discard "nothing to do" + +proc destMightOwn(c: var Partitions; dest: var VarIndex; n: PNode) = + ## Analyse if 'n' is an expression that owns the data, if so mark 'dest' + ## with 'ownsData'. + case n.kind + of nkEmpty, nkCharLit..nkNilLit: + # primitive literals including the empty are harmless: + discard + + of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkCast: + destMightOwn(c, dest, n[1]) + + of nkConv: + if hasDestructor(n.typ): + inc c.inConvHasDestructor + destMightOwn(c, dest, n[1]) + dec c.inConvHasDestructor + else: + destMightOwn(c, dest, n[1]) + + of nkIfStmt, nkIfExpr: + for i in 0..<n.len: + inc c.inConditional + destMightOwn(c, dest, n[i].lastSon) + dec c.inConditional + + of nkCaseStmt: + for i in 1..<n.len: + inc c.inConditional + destMightOwn(c, dest, n[i].lastSon) + dec c.inConditional + + of nkStmtList, nkStmtListExpr: + if n.len > 0: + destMightOwn(c, dest, n[^1]) + + of nkClosure: + for i in 1..<n.len: + destMightOwn(c, dest, n[i]) + # you must destroy a closure: + dest.flags.incl ownsData + + of nkObjConstr: + for i in 1..<n.len: + destMightOwn(c, dest, n[i]) + if hasDestructor(n.typ): + # you must destroy a ref object: + dest.flags.incl ownsData + + of nkCurly, nkBracket, nkPar, nkTupleConstr: + inc c.inConstructor + for son in n: + destMightOwn(c, dest, son) + dec c.inConstructor + if n.typ.skipTypes(abstractInst).kind == tySequence: + # you must destroy a sequence: + dest.flags.incl ownsData + + of nkSym: + if n.sym.kind in {skVar, skResult, skTemp, skLet, skForVar, skParam}: + if n.sym.flags * {sfThread, sfGlobal} != {}: + # aliasing a global is inherently dangerous: + dest.flags.incl ownsData + else: + # otherwise it's just a dependency, nothing to worry about: + connect(c, dest.sym, n.sym, n.info) + # but a construct like ``[symbol]`` is dangerous: + if c.inConstructor > 0: dest.flags.incl ownsData + + of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr, + nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkAddr, nkHiddenAddr: + destMightOwn(c, dest, n[0]) + + of nkCallKinds: + if n.typ != nil: + if hasDestructor(n.typ) or c.inConvHasDestructor > 0: + # 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} and n.len > 1: + # we know the result is derived from the first argument: + var roots: seq[(PSym, int)] = @[] + allRoots(n[1], roots, RootEscapes) + if roots.len == 0 and c.inConditional > 0: + # when in a conditional expression, + # to ensure that the first argument isn't outlived + # by the lvalue, we need find the root, otherwise + # it is probably a local temporary + # (e.g. a return value from a call), + # we should prevent cursorfication + dest.flags.incl preventCursor + else: + for r in roots: + connect(c, dest.sym, r[0], 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, mOpenArrayToSeq}: + for i in 1..<n.len: + # we always have to assume a 'select(...)' like mechanism. + # But at least we do filter out simple POD types from the + # list of dependencies via the 'hasDestructor' check for + # the root's symbol. + if hasDestructor(n[i].typ.skipTypes({tyVar, tySink, tyLent, tyGenericInst, tyAlias})): + destMightOwn(c, dest, n[i]) + + else: + # something we cannot handle: + dest.flags.incl preventCursor + +proc noCursor(c: var Partitions, s: PSym) = + let vid = variableId(c, s) + if vid >= 0: + c.s[vid].flags.incl preventCursor + +proc pretendOwnsData(c: var Partitions, s: PSym) = + let vid = variableId(c, s) + if vid >= 0: + c.s[vid].flags.incl ownsData + +const + explainCursors = false + +proc isConstSym(s: PSym): bool = + result = s.kind in {skConst, skLet} or isConstParam(s) + +proc toString(n: PNode): string = + if n.kind == nkEmpty: result = "<empty>" + else: result = $n + +proc borrowFrom(c: var Partitions; dest: PSym; src: PNode) = + const + url = "see https://nim-lang.github.io/Nim/manual_experimental.html#view-types-algorithm-path-expressions for details" + + let s = pathExpr(src, c.owner) + if s == nil: + localError(c.g.config, src.info, "cannot borrow from " & src.toString & ", it is not a path expression; " & url) + elif s.kind == nkSym: + if dest.kind == skResult: + if s.sym.kind != skParam or s.sym.position != 0: + localError(c.g.config, src.info, "'result' must borrow from the first parameter") + + let vid = variableId(c, dest) + if vid >= 0: + var sourceIdx = variableId(c, s.sym) + if sourceIdx < 0: + sourceIdx = c.s.len + c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: s.sym, reassignedTo: 0, + aliveStart: MinTime, aliveEnd: MaxTime) + + c.s[vid].borrowsFrom.add sourceIdx + if isConstSym(s.sym): + c.s[vid].flags.incl viewBorrowsFromConst + else: + let vid = variableId(c, dest) + if vid >= 0: + c.s[vid].flags.incl viewBorrowsFromConst + #discard "a valid borrow location that is a deeply constant expression so we have nothing to track" + + +proc borrowingCall(c: var Partitions; destType: PType; n: PNode; i: int) = + let v = pathExpr(n[i], c.owner) + if v != nil and v.kind == nkSym: + when false: + let isView = directViewType(destType) == immutableView + if n[0].kind == nkSym and n[0].sym.name.s == "[]=": + localError(c.g.config, n[i].info, "attempt to mutate an immutable view") + + for j in i+1..<n.len: + if getMagic(n[j]) == mSlice: + borrowFrom(c, v.sym, n[j]) + else: + localError(c.g.config, n[i].info, "cannot determine the target of the borrow") + +proc borrowingAsgn(c: var Partitions; dest, src: PNode) = + proc mutableParameter(n: PNode): bool {.inline.} = + result = n.kind == nkSym and n.sym.kind == skParam and n.sym.typ.kind == tyVar + + if dest.kind == nkSym: + if directViewType(dest.typ) != noView: + borrowFrom(c, dest.sym, src) + else: + let viewOrigin = pathExpr(dest, c.owner) + if viewOrigin != nil and viewOrigin.kind == nkSym: + let viewSym = viewOrigin.sym + let directView = directViewType(dest[0].typ) # check something like result[first] = toOpenArray(s, first, last-1) + # so we don't need to iterate the original type + let originSymbolView = directViewType(viewSym.typ) # find the original symbol which preserves the view type + # var foo: var Object = a + # foo.id = 777 # the type of foo is no view, so we need + # to check the original symbol + let viewSets = {directView, originSymbolView} + + if viewSets * {mutableView, immutableView} != {}: + # we do not borrow, but we use the view to mutate the borrowed + # location: + let vid = variableId(c, viewSym) + if vid >= 0: + c.s[vid].flags.incl viewDoesMutate + #[of immutableView: + if dest.kind == nkBracketExpr and dest[0].kind == nkHiddenDeref and + mutableParameter(dest[0][0]): + discard "remains a mutable location anyhow" + else: + localError(c.g.config, dest.info, "attempt to mutate a borrowed location from an immutable view") + ]# + else: + discard "nothing to do" + +proc containsPointer(t: PType): bool = + proc wrap(t: PType): bool {.nimcall.} = t.kind in {tyRef, tyPtr} + result = types.searchTypeFor(t, wrap) + +proc deps(c: var Partitions; dest, src: PNode) = + if borrowChecking in c.goals: + borrowingAsgn(c, dest, src) + + var targets: seq[(PSym, int)] = @[] + var sources: seq[(PSym, int)] = @[] + allRoots(dest, targets, 0) + allRoots(src, sources, 0) + + let destIsComplex = containsPointer(dest.typ) + + for t in targets: + if dest.kind != nkSym and c.inNoSideEffectSection == 0: + potentialMutation(c, t[0], t[1], dest.info) + + if destIsComplex: + for s in sources: + connect(c, t[0], s[0], dest.info) + + if cursorInference in c.goals and src.kind != nkEmpty: + let d = pathExpr(dest, c.owner) + if d != nil and d.kind == nkSym: + let vid = variableId(c, d.sym) + if vid >= 0: + destMightOwn(c, c.s[vid], src) + for source in sources: + let s = source[0] + if s == d.sym: + discard "assignments like: it = it.next are fine" + elif {sfGlobal, sfThread} * s.flags != {} or hasDisabledAsgn(c.g, s.typ): + # do not borrow from a global variable or from something with a + # disabled assignment operator. + c.s[vid].flags.incl preventCursor + when explainCursors: echo "A not a cursor: ", d.sym, " ", s + else: + let srcid = variableId(c, s) + if srcid >= 0: + if s.kind notin {skResult, skParam} and ( + c.s[srcid].aliveEnd < c.s[vid].aliveEnd): + # you cannot borrow from a local that lives shorter than 'vid': + when explainCursors: echo "B not a cursor ", d.sym, " ", c.s[srcid].aliveEnd, " ", c.s[vid].aliveEnd + c.s[vid].flags.incl preventCursor + elif {isReassigned, preventCursor} * c.s[srcid].flags != {}: + # you cannot borrow from something that is re-assigned: + when explainCursors: echo "C not a cursor ", d.sym, " ", c.s[srcid].flags, " reassignedTo ", c.s[srcid].reassignedTo + c.s[vid].flags.incl preventCursor + elif c.s[srcid].reassignedTo != 0 and c.s[srcid].reassignedTo != d.sym.id: + when explainCursors: echo "D not a cursor ", d.sym, " reassignedTo ", c.s[srcid].reassignedTo + c.s[vid].flags.incl preventCursor + + +proc potentialMutationViaArg(c: var Partitions; n: PNode; callee: PType) = + if constParameters in c.goals and tfNoSideEffect in callee.flags: + discard "we know there are no hidden mutations through an immutable parameter" + elif c.inNoSideEffectSection == 0 and containsPointer(n.typ): + var roots: seq[(PSym, int)] = @[] + allRoots(n, roots, RootEscapes) + for r in roots: potentialMutation(c, r[0], r[1], n.info) + +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..<child.len-2: + #registerVariable(c, child[i]) + deps(c, child[i], last[i]) + else: + for i in 0..<child.len-2: + #registerVariable(c, child[i]) + deps(c, child[i], last) + of nkAsgn, nkFastAsgn, nkSinkAsgn: + traverse(c, n[0]) + inc c.inAsgnSource + traverse(c, n[1]) + dec c.inAsgnSource + deps(c, n[0], n[1]) + of nkSym: + dec c.abstractTime + + of nodesToIgnoreSet: + 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.signatureLen else: 0 + let m = getMagic(n) + + if m == mEnsureMove and n[1].kind == nkSym: + # we know that it must be moved so it cannot be a cursor + noCursor(c, n[1].sym) + + for i in 1..<n.len: + let it = n[i] + if i < L: + let paramType = parameters[i].skipTypes({tyGenericInst, tyAlias}) + if not paramType.isCompileTimeOnly and paramType.kind in {tyVar, tySink, tyOwned}: + var roots: seq[(PSym, int)] = @[] + allRoots(it, roots, RootEscapes) + if paramType.kind == tyVar: + if c.inNoSideEffectSection == 0: + for r in roots: potentialMutation(c, r[0], r[1], it.info) + for r in roots: noCursor(c, r[0]) + + if borrowChecking in c.goals: + # a call like 'result.add toOpenArray()' can also be a borrow + # operation. We know 'paramType' is a tyVar and we really care if + # 'paramType[0]' is still a view type, this is not a typo! + if directViewType(paramType[0]) == noView and classifyViewType(paramType[0]) != noView: + borrowingCall(c, paramType[0], n, i) + elif m == mNone: + potentialMutationViaArg(c, n[i], parameters) + + of nkAddr, nkHiddenAddr: + traverse(c, n[0]) + when false: + # XXX investigate if this is required, it doesn't look + # like it is! + var roots: seq[(PSym, int)] + allRoots(n[0], roots, RootEscapes) + for r in roots: + potentialMutation(c, r[0], r[1], it.info) + + of nkTupleConstr, nkBracket: + for child in n: traverse(c, child) + if c.inAsgnSource > 0: + for i in 0..<n.len: + if n[i].kind == nkSym: + # we assume constructions with cursors are better without + # the cursors because it's likely we can move then, see + # test arc/topt_no_cursor.nim + pretendOwnsData(c, n[i].sym) + + of nkObjConstr: + for child in n: traverse(c, child) + if c.inAsgnSource > 0: + for i in 1..<n.len: + let it = n[i].skipColon + if it.kind == nkSym: + # we assume constructions with cursors are better without + # the cursors because it's likely we can move then, see + # test arc/topt_no_cursor.nim + pretendOwnsData(c, it.sym) + + of nkPragmaBlock: + let pragmaList = n[0] + var enforceNoSideEffects = 0 + for i in 0..<pragmaList.len: + if isNoSideEffectPragma(pragmaList[i]): + enforceNoSideEffects = 1 + break + + inc c.inNoSideEffectSection, enforceNoSideEffects + traverse(c, n.lastSon) + dec c.inNoSideEffectSection, enforceNoSideEffects + of nkWhileStmt, nkForStmt, nkParForStmt: + for child in n: traverse(c, child) + # analyse loops twice so that 'abstractTime' suffices to detect cases + # like: + # while cond: + # mutate(graph) + # connect(graph, cursorVar) + for child in n: traverse(c, child) + + if n.kind == nkWhileStmt: + traverse(c, n[0]) + # variables in while condition has longer alive time than local variables + # in the while loop body + of nkDefer: + if c.processDefer: + for child in n: traverse(c, child) + else: + for child in n: traverse(c, child) + +proc markAsReassigned(c: var Partitions; vid: int) {.inline.} = + c.s[vid].flags.incl isReassigned + if c.inConditional > 0 and c.inLoop > 0: + # bug #17033: live ranges with loops and conditionals are too + # complex for our current analysis, so we prevent the cursorfication. + c.s[vid].flags.incl isConditionallyReassigned + +proc computeLiveRanges(c: var Partitions; n: PNode) = + # first pass: Compute live ranges for locals. + # **Watch out!** We must traverse the tree like 'traverse' does + # so that the 'c.abstractTime' is consistent. + inc c.abstractTime + case n.kind + of nkLetSection, nkVarSection: + for child in n: + let last = lastSon(child) + computeLiveRanges(c, last) + if child.kind == nkVarTuple and last.kind in {nkPar, nkTupleConstr}: + if child.len-2 != last.len: return + for i in 0..<child.len-2: + registerVariable(c, child[i]) + #deps(c, child[i], last[i]) + else: + for i in 0..<child.len-2: + registerVariable(c, child[i]) + #deps(c, child[i], last) + + if c.inLoop > 0 and child[0].kind == nkSym: # bug #22787 + let vid = variableId(c, child[0].sym) + if child[^1].kind != nkEmpty: + markAsReassigned(c, vid) + of nkAsgn, nkFastAsgn, nkSinkAsgn: + computeLiveRanges(c, n[0]) + computeLiveRanges(c, n[1]) + if n[0].kind == nkSym: + let vid = variableId(c, n[0].sym) + if vid >= 0: + if n[1].kind == nkSym and (c.s[vid].reassignedTo == 0 or c.s[vid].reassignedTo == n[1].sym.id): + c.s[vid].reassignedTo = n[1].sym.id + if c.inConditional > 0 and c.inLoop > 0: + # bug #22200: live ranges with loops and conditionals are too + # complex for our current analysis, so we prevent the cursorfication. + c.s[vid].flags.incl isConditionallyReassigned + else: + markAsReassigned(c, vid) + + of nkSym: + dec c.abstractTime + if n.sym.kind in {skVar, skResult, skTemp, skLet, skForVar, skParam}: + let id = variableId(c, n.sym) + if id >= 0: + c.s[id].aliveEnd = max(c.s[id].aliveEnd, c.abstractTime) + if n.sym.kind == skResult: + c.s[id].aliveStart = min(c.s[id].aliveStart, c.abstractTime) + + of nodesToIgnoreSet: + dec c.abstractTime + discard "do not follow the construct" + of nkCallKinds: + for child in n: computeLiveRanges(c, child) + + let parameters = n[0].typ + let L = if parameters != nil: parameters.signatureLen else: 0 + + for i in 1..<n.len: + let it = n[i] + if it.kind == nkSym and i < L: + let paramType = parameters[i].skipTypes({tyGenericInst, tyAlias}) + if not paramType.isCompileTimeOnly and paramType.kind == tyVar: + let vid = variableId(c, it.sym) + if vid >= 0: + markAsReassigned(c, vid) + + of nkAddr, nkHiddenAddr: + computeLiveRanges(c, n[0]) + if n[0].kind == nkSym: + let vid = variableId(c, n[0].sym) + if vid >= 0: + c.s[vid].flags.incl preventCursor + + of nkPragmaBlock: + computeLiveRanges(c, n.lastSon) + of nkWhileStmt, nkForStmt, nkParForStmt: + for child in n: computeLiveRanges(c, child) + # analyse loops twice so that 'abstractTime' suffices to detect cases + # like: + # while cond: + # mutate(graph) + # connect(graph, cursorVar) + inc c.inLoop + for child in n: computeLiveRanges(c, child) + dec c.inLoop + + if n.kind == nkWhileStmt: + computeLiveRanges(c, n[0]) + # variables in while condition has longer alive time than local variables + # in the while loop body + of nkElifBranch, nkElifExpr, nkElse, nkOfBranch: + inc c.inConditional + for child in n: computeLiveRanges(c, child) + dec c.inConditional + of nkDefer: + if c.processDefer: + for child in n: computeLiveRanges(c, child) + else: + c.defers.add n + else: + for child in n: computeLiveRanges(c, child) + +proc computeGraphPartitions*(s: PSym; n: PNode; g: ModuleGraph; goals: set[Goal]): Partitions = + result = Partitions(owner: s, g: g, goals: goals) + if s.kind notin {skModule, skMacro}: + let params = s.typ.n + for i in 1..<params.len: + registerParam(result, params[i]) + if resultPos < s.ast.safeLen: + registerResult(result, s.ast[resultPos]) + + computeLiveRanges(result, n) + result.processDefer = true + for i in countdown(len(result.defers)-1, 0): + computeLiveRanges(result, result.defers[i]) + result.processDefer = false + # restart the timer for the second pass: + result.abstractTime = AbstractTime 0 + traverse(result, n) + result.processDefer = true + for i in countdown(len(result.defers)-1, 0): + traverse(result, result.defers[i]) + result.processDefer = false + +proc dangerousMutation(g: MutationInfo; v: VarIndex): bool = + #echo "range ", v.aliveStart, " .. ", v.aliveEnd, " ", v.sym + if {isMutated, isMutatedByVarParam} * g.flags != {}: + for m in g.mutations: + #echo "mutation ", m + if m in v.aliveStart..v.aliveEnd: + return true + return false + +proc cannotBorrow(config: ConfigRef; s: PSym; g: MutationInfo) = + var m = "cannot borrow " & s.name.s & + "; what it borrows from is potentially mutated" + + if g.mutatedHere != unknownLineInfo: + m.add "\n" + m.add config $ g.mutatedHere + m.add " the mutation is here" + if g.connectedVia != unknownLineInfo: + m.add "\n" + m.add config $ g.connectedVia + m.add " is the statement that connected the mutation to the parameter" + localError(config, s.info, m) + +proc checkBorrowedLocations*(par: var Partitions; body: PNode; config: ConfigRef) = + for i in 0 ..< par.s.len: + let v = par.s[i].sym + if v.kind != skParam and classifyViewType(v.typ) != noView: + let rid = root(par, i) + if rid >= 0: + var constViolation = false + for b in par.s[rid].borrowsFrom: + let sid = root(par, b) + if sid >= 0: + if par.s[sid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[sid].con.graphIndex], par.s[i]): + cannotBorrow(config, v, par.graphs[par.s[sid].con.graphIndex]) + if par.s[sid].sym.kind != skParam and par.s[sid].aliveEnd < par.s[rid].aliveEnd: + localError(config, v.info, "'" & v.name.s & "' borrows from location '" & par.s[sid].sym.name.s & + "' which does not live long enough") + if viewDoesMutate in par.s[rid].flags and isConstSym(par.s[sid].sym): + localError(config, v.info, "'" & v.name.s & "' borrows from the immutable location '" & + par.s[sid].sym.name.s & "' and attempts to mutate it") + constViolation = true + if {viewDoesMutate, viewBorrowsFromConst} * par.s[rid].flags == {viewDoesMutate, viewBorrowsFromConst} and + not constViolation: + # we do not track the constant expressions we allow to borrow from so + # we can only produce a more generic error message: + localError(config, v.info, "'" & v.name.s & + "' borrows from an immutable location and attempts to mutate it") + + #if par.s[rid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[rid].con.graphIndex], par.s[i]): + # cannotBorrow(config, s, par.graphs[par.s[rid].con.graphIndex]) + +proc computeCursors*(s: PSym; n: PNode; g: ModuleGraph) = + var par = computeGraphPartitions(s, n, g, {cursorInference}) + for i in 0 ..< par.s.len: + let v = addr(par.s[i]) + if v.flags * {ownsData, preventCursor, isConditionallyReassigned} == {} and + v.sym.kind notin {skParam, skResult} and + v.sym.flags * {sfThread, sfGlobal} == {} and hasDestructor(v.sym.typ) and + v.sym.typ.skipTypes({tyGenericInst, tyAlias}).kind != tyOwned and + (getAttachedOp(g, v.sym.typ, attachedAsgn) == nil or + sfError notin getAttachedOp(g, v.sym.typ, attachedAsgn).flags): + let rid = root(par, i) + if par.s[rid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[rid].con.graphIndex], par.s[i]): + discard "cannot cursor into a graph that is mutated" + else: + v.sym.flags.incl sfCursor + when false: + echo "this is now a cursor ", v.sym, " ", par.s[rid].flags, " ", g.config $ v.sym.info |