#
#
# 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
from trees import getMagic, whichPragma, stupidStmtListExpr
from wordrecg import wNoSideEffect
from isolation_check import canAlias
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.}
type
SubgraphFlag = enum
isMutated, # graph might be mutated
connectsConstParam, # graph is connected to a non-var parameter.
VarFlag = enum
ownsData,
preventCursor,
isReassigned,
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
s: seq[VarIndex]
graphs: seq[MutationInfo]
goals: set[Goal]
unanalysableMutation: bool
inAsgnSource, inConstructor, inNoSideEffectSection: int
owner: PSym
config: ConfigRef
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 =
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; info: TLineInfo) =
let id = variableId(v, s)
if id >= 0:
let r = root(v, id)
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: {isMutated},
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 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 = 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: discard
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
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..<n.len:
allRoots(n[i].lastSon, result, followDotExpr)
of nkIfStmt, nkIfExpr:
for i in 0..<n.len:
allRoots(n[i].lastSon, result, followDotExpr)
of nkBracket, nkTupleConstr, nkPar:
for i in 0..<n.len:
allRoots(n[i], result, followDotExpr)
of nkCallKinds:
if n.typ != nil and n.typ.kind in {tyVar, tyLent}:
if n.len > 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 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, nkConv:
destMightOwn(c, dest, n[1])
of nkIfStmt, nkIfExpr:
for i in 0..<n.len:
destMightOwn(c, dest, n[i].lastSon)
of nkCaseStmt:
for i in 1..<n.len:
destMightOwn(c, dest, n[i].lastSon)
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 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..<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 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.config, src.info, "cannot borrow from " & $src & ", 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.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.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.config, n[i].info, "cannot determine the target of the borrow")
proc borrowingAsgn(c: var Partitions; dest, src: PNode) =
if dest.kind == nkSym:
if directViewType(dest.typ) != noView:
borrowFrom(c, dest.sym, src)
elif dest.kind in {nkHiddenDeref, nkDerefExpr, nkBracketExpr}:
case directViewType(dest[0].typ)
of mutableView:
# we do not borrow, but we use the view to mutate the borrowed
# location:
let viewOrigin = pathExpr(dest, c.owner)
if viewOrigin.kind == nkSym:
let vid = variableId(c, viewOrigin.sym)
if vid >= 0:
c.s[vid].flags.incl viewDoesMutate
of immutableView:
localError(c.config, dest.info, "attempt to mutate a borrowed location from an immutable view")
of noView: 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, sources: seq[PSym]
allRoots(dest, targets)
allRoots(src, sources)
let destIsComplex = containsPointer(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 cursorInference in c.goals and src.kind != nkEmpty:
if dest.kind == nkSym:
let vid = variableId(c, dest.sym)
if vid >= 0:
destMightOwn(c, c.s[vid], src)
for s in sources:
if s == dest.sym:
discard "assignments like: it = it.next are fine"
elif {sfGlobal, sfThread} * s.flags != {} or hasDisabledAsgn(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: ", dest.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 ", dest.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 ", dest.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 != dest.sym.id:
when explainCursors: echo "D not a cursor ", dest.sym, " reassignedTo ", c.s[srcid].reassignedTo
c.s[vid].flags.incl preventCursor
const
nodesToIgnoreSet = {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}
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]
allRoots(n, roots)
for r in roots: potentialMutation(c, r, 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:
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.len else: 0
let m = getMagic(n)
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]
allRoots(it, roots)
if paramType.kind == tyVar:
if c.inNoSideEffectSection == 0:
for r in roots: potentialMutation(c, r, it.info)
for r in roots: noCursor(c, r)
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 borrowChecking in c.goals and 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]
allRoots(n[0], roots)
for r in roots:
potentialMutation(c, r, 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 whichPragma(pragmaList[i]) == wNoSideEffect:
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)
else:
for child in n: traverse(c, child)
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)
of nkAsgn, nkFastAsgn:
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
else:
c.s[vid].flags.incl isReassigned
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.len 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:
c.s[vid].flags.incl isReassigned
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)
for child in n: computeLiveRanges(c, child)
else:
for child in n: computeLiveRanges(c, child)
proc computeGraphPartitions*(s: PSym; n: PNode; config: ConfigRef; goals: set[Goal]): Partitions =
result = Partitions(owner: s, config: config, 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)
# restart the timer for the second pass:
result.abstractTime = AbstractTime 0
traverse(result, n)
proc dangerousMutation(g: MutationInfo; v: VarIndex): bool =
#echo "range ", v.aliveStart, " .. ", v.aliveEnd, " ", v.sym
if isMutated in 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; config: ConfigRef) =
var par = computeGraphPartitions(s, n, config, {cursorInference})
for i in 0 ..< par.s.len:
let v = addr(par.s[i])
if v.flags * {ownsData, preventCursor} == {} 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:
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
#echo "this is now a cursor ", v.sym, " ", par.s[rid].flags, " ", config $ v.sym.info