* fixes #15361; better cursor inference
This commit is contained in:
Andreas Rumpf 2020-09-24 15:41:45 +02:00 • committed by GitHub
commit 4ec5cbac0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 45 deletions

View file

@ -13,6 +13,17 @@
## algorithm. ## algorithm.
## The used data structure is "union find" with path compression. ## 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.)
import ast, types, lineinfos, options, msgs, renderer import ast, types, lineinfos, options, msgs, renderer
from trees import getMagic, whichPragma from trees import getMagic, whichPragma
from wordrecg import wNoSideEffect from wordrecg import wNoSideEffect
@ -26,7 +37,8 @@ type
VarFlag = enum VarFlag = enum
ownsData, ownsData,
preventCursor preventCursor,
isReassigned
VarIndexKind = enum VarIndexKind = enum
isEmptyRoot, isEmptyRoot,
@ -266,36 +278,38 @@ proc allRoots(n: PNode; result: var seq[PSym]; followDotExpr = true) =
else: else:
discard "nothing to do" discard "nothing to do"
proc analyseAsgn(c: var Partitions; dest: var VarIndex; n: PNode) = 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 case n.kind
of nkEmpty, nkCharLit..nkNilLit: of nkEmpty, nkCharLit..nkNilLit:
# primitive literals including the empty are harmless: # primitive literals including the empty are harmless:
discard discard
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkCast, nkConv: of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkCast, nkConv:
analyseAsgn(c, dest, n[1]) destMightOwn(c, dest, n[1])
of nkIfStmt, nkIfExpr: of nkIfStmt, nkIfExpr:
for i in 0..<n.len: for i in 0..<n.len:
analyseAsgn(c, dest, n[i].lastSon) destMightOwn(c, dest, n[i].lastSon)
of nkCaseStmt: of nkCaseStmt:
for i in 1..<n.len: for i in 1..<n.len:
analyseAsgn(c, dest, n[i].lastSon) destMightOwn(c, dest, n[i].lastSon)
of nkStmtList, nkStmtListExpr: of nkStmtList, nkStmtListExpr:
if n.len > 0: if n.len > 0:
analyseAsgn(c, dest, n[^1]) destMightOwn(c, dest, n[^1])
of nkClosure: of nkClosure:
for i in 1..<n.len: for i in 1..<n.len:
analyseAsgn(c, dest, n[i]) destMightOwn(c, dest, n[i])
# you must destroy a closure: # you must destroy a closure:
dest.flags.incl ownsData dest.flags.incl ownsData
of nkObjConstr: of nkObjConstr:
for i in 1..<n.len: for i in 1..<n.len:
analyseAsgn(c, dest, n[i]) destMightOwn(c, dest, n[i])
if hasDestructor(n.typ): if hasDestructor(n.typ):
# you must destroy a ref object: # you must destroy a ref object:
dest.flags.incl ownsData dest.flags.incl ownsData
@ -303,7 +317,7 @@ proc analyseAsgn(c: var Partitions; dest: var VarIndex; n: PNode) =
of nkCurly, nkBracket, nkPar, nkTupleConstr: of nkCurly, nkBracket, nkPar, nkTupleConstr:
inc c.inConstructor inc c.inConstructor
for son in n: for son in n:
analyseAsgn(c, dest, son) destMightOwn(c, dest, son)
dec c.inConstructor dec c.inConstructor
if n.typ.skipTypes(abstractInst).kind == tySequence: if n.typ.skipTypes(abstractInst).kind == tySequence:
# you must destroy a sequence: # you must destroy a sequence:
@ -322,7 +336,7 @@ proc analyseAsgn(c: var Partitions; dest: var VarIndex; n: PNode) =
of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr, of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr,
nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkAddr, nkHiddenAddr: nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkAddr, nkHiddenAddr:
analyseAsgn(c, dest, n[0]) destMightOwn(c, dest, n[0])
of nkCallKinds: of nkCallKinds:
if hasDestructor(n.typ): if hasDestructor(n.typ):
@ -348,7 +362,7 @@ proc analyseAsgn(c: var Partitions; dest: var VarIndex; n: PNode) =
# list of dependencies via the 'hasDestructor' check for # list of dependencies via the 'hasDestructor' check for
# the root's symbol. # the root's symbol.
if hasDestructor(n[i].typ.skipTypes({tyVar, tySink, tyLent, tyGenericInst, tyAlias})): if hasDestructor(n[i].typ.skipTypes({tyVar, tySink, tyLent, tyGenericInst, tyAlias})):
analyseAsgn(c, dest, n[i]) destMightOwn(c, dest, n[i])
else: else:
# something we cannot handle: # something we cannot handle:
@ -389,24 +403,36 @@ proc deps(c: var Partitions; dest, src: PNode) =
if dest.kind == nkSym: if dest.kind == nkSym:
let vid = variableId(c, dest.sym) let vid = variableId(c, dest.sym)
if vid >= 0: if vid >= 0:
analyseAsgn(c, c.s[vid], src) destMightOwn(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.kind == nkSym:
if (src.sym.kind in {skVar, skResult, skTemp} or let s = src.sym
(src.sym.kind in {skLet, skParam, skForVar} and hasDisabledAsgn(src.sym.typ))): if {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 c.s[vid].flags.incl preventCursor
elif src.sym.kind in {skVar, skResult, skTemp, skLet, skForVar}: when false: echo "A not a cursor: ", dest.sym, " ", s
# XXX: we need to compute variable alive ranges before doing anything else: else:
let srcid = variableId(c, src.sym) let srcid = variableId(c, s)
if srcid >= 0 and preventCursor in c.s[srcid].flags: 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': # you cannot borrow from a local that lives shorter than 'vid':
if c.s[srcid].aliveStart > c.s[vid].aliveStart or when false: echo "B not a cursor ", dest.sym, " ", c.s[srcid].aliveEnd, " ", c.s[vid].aliveEnd
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 false: echo "C not a cursor ", dest.sym, " ", c.s[srcid].flags
c.s[vid].flags.incl preventCursor c.s[vid].flags.incl preventCursor
if src.kind == nkSym and hasDestructor(src.typ): #if src.kind == nkSym and hasDestructor(src.typ):
rhsIsSink(c, src) # rhsIsSink(c, src)
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 traverse(c: var Partitions; n: PNode) = proc traverse(c: var Partitions; n: PNode) =
inc c.abstractTime inc c.abstractTime
@ -418,11 +444,11 @@ proc traverse(c: var Partitions; n: PNode) =
if child.kind == nkVarTuple and last.kind in {nkPar, nkTupleConstr}: if child.kind == nkVarTuple and last.kind in {nkPar, nkTupleConstr}:
if child.len-2 != last.len: return if child.len-2 != last.len: return
for i in 0..<child.len-2: for i in 0..<child.len-2:
registerVariable(c, child[i]) #registerVariable(c, child[i])
deps(c, child[i], last[i]) deps(c, child[i], last[i])
else: else:
for i in 0..<child.len-2: for i in 0..<child.len-2:
registerVariable(c, child[i]) #registerVariable(c, child[i])
deps(c, child[i], last) deps(c, child[i], last)
of nkAsgn, nkFastAsgn: of nkAsgn, nkFastAsgn:
traverse(c, n[0]) traverse(c, n[0])
@ -432,15 +458,8 @@ proc traverse(c: var Partitions; n: PNode) =
deps(c, n[0], n[1]) deps(c, n[0], n[1])
of nkSym: of nkSym:
dec c.abstractTime 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)
of nkNone..pred(nkSym), succ(nkSym)..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef, of nodesToIgnoreSet:
nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
dec c.abstractTime dec c.abstractTime
discard "do not follow the construct" discard "do not follow the construct"
of nkCallKinds: of nkCallKinds:
@ -514,6 +533,79 @@ proc traverse(c: var Partitions; n: PNode) =
else: else:
for child in n: traverse(c, child) 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:
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)
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; cursorInference = false): Partitions = proc computeGraphPartitions*(s: PSym; n: PNode; cursorInference = false): Partitions =
result = Partitions(performCursorInference: cursorInference) result = Partitions(performCursorInference: cursorInference)
if s.kind notin {skModule, skMacro}: if s.kind notin {skModule, skMacro}:
@ -523,6 +615,9 @@ proc computeGraphPartitions*(s: PSym; n: PNode; cursorInference = false): Partit
if resultPos < s.ast.safeLen: if resultPos < s.ast.safeLen:
registerVariable(result, s.ast[resultPos]) registerVariable(result, s.ast[resultPos])
computeLiveRanges(result, n)
# resart the timer for the second pass:
result.abstractTime = 0
traverse(result, n) traverse(result, n)
proc dangerousMutation(g: MutationInfo; v: VarIndex): bool = proc dangerousMutation(g: MutationInfo; v: VarIndex): bool =
@ -560,7 +655,7 @@ proc computeCursors*(s: PSym; n: PNode; config: ConfigRef) =
var par = computeGraphPartitions(s, n, true) var par = computeGraphPartitions(s, n, true)
for i in 0 ..< par.s.len: for i in 0 ..< par.s.len:
let v = addr(par.s[i]) let v = addr(par.s[i])
if v.flags == {} and v.sym.kind notin {skParam, skResult} and 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.flags * {sfThread, sfGlobal} == {} and hasDestructor(v.sym.typ) and
v.sym.typ.skipTypes({tyGenericInst, tyAlias}).kind != tyOwned: v.sym.typ.skipTypes({tyGenericInst, tyAlias}).kind != tyOwned:
let rid = root(par, i) let rid = root(par, i)

View file

@ -0,0 +1,53 @@
discard """
output: '''true
true
true
true
true'''
cmd: "nim c --gc:arc $file"
"""
# bug #15361
type
ErrorNodeKind = enum Branch, Leaf
Error = ref object
case kind: ErrorNodeKind
of Branch:
left: Error
right: Error
of Leaf:
leafError: string
input: string
proc ret(input: string, lefterr, righterr: Error): Error =
result = Error(kind: Branch, left: lefterr, right: righterr, input: input)
proc parser() =
var rerrors: Error
let lerrors = Error(
kind: Leaf,
leafError: "first error",
input: "123 ;"
)
# If you remove "block" - everything works
block:
let rresult = Error(
kind: Leaf,
leafError: "second error",
input: ";"
)
# this assignment is needed too
rerrors = rresult
# Returns Error(kind: Branch, left: lerrors, right: rerrors, input: "some val")
# needs to be a proc call for some reason, can't inline the result
var data = ret(input = "some val", lefterr = lerrors, righterr = rerrors)
echo data.left.leafError == "first error"
echo data.left.input == "123 ;"
# stacktrace shows this line
echo data.right.leafError == "second error"
echo data.right.input == ";"
echo data.input == "some val"
parser()

View file

@ -52,7 +52,7 @@ _ = (
blitTmp, ";") blitTmp, ";")
lvalue = _[0] lvalue = _[0]
lnext_cursor = _[1] lnext_cursor = _[1]
`=sink`(result.value, lvalue) `=sink`(result.value, move lvalue)
-- end of expandArc ------------------------ -- end of expandArc ------------------------
--expandArc: tt --expandArc: tt
@ -148,7 +148,7 @@ proc p1(): Maybe =
var lnext: string var lnext: string
(lvalue, lnext) = (lresult, ";") (lvalue, lnext) = (lresult, ";")
result.value = lvalue result.value = move lvalue
proc tissue15130 = proc tissue15130 =
doAssert p1().value == @[123] doAssert p1().value == @[123]

View file

@ -31,8 +31,9 @@ proc `=destroy`(v: var T) =
proc use(x: T) = discard proc use(x: T) = discard
proc usedToBeBlock = proc usedToBeBlock =
var v1 : T var v1 = T()
var v2: T = v1 var v2: T = v1
discard addr(v2) # prevent cursorfication
use v1 use v1
usedToBeBlock() usedToBeBlock()

View file

@ -96,14 +96,16 @@ proc info =
allocCount = 0 allocCount = 0
deallocCount = 0 deallocCount = 0
proc copy(a: Matrix): Matrix = a
proc test1 = proc test1 =
var a = matrix(5, 5, 1.0) var a = matrix(5, 5, 1.0)
var b = a var b = copy a
var c = a + b var c = a + b
proc test2 = proc test2 =
var a = matrix(5, 5, 1.0) var a = matrix(5, 5, 1.0)
var b = a var b = copy a
var c = -a var c = -a
proc test3 = proc test3 =