dfa.nim: track object/tuple field accesses more precisely; sink(o.x); sink(o.y) needs to compile; activate the tuple unpacking transf.nim bugfix

This commit is contained in:
Araq 2019-04-15 17:57:59 +02:00
commit 045e026d0e
8 changed files with 135 additions and 47 deletions

View file

@ -29,7 +29,10 @@
## "A Graph–Free Approach to Data–Flow Analysis" by Markus Mohnen. ## "A Graph–Free Approach to Data–Flow Analysis" by Markus Mohnen.
## https://link.springer.com/content/pdf/10.1007/3-540-45937-5_6.pdf ## https://link.springer.com/content/pdf/10.1007/3-540-45937-5_6.pdf
import ast, astalgo, types, intsets, tables, msgs, options, lineinfos import ast, astalgo, types, intsets, tables, msgs, options, lineinfos, renderer
from patterns import sameTrees
from aliases import isPartOf, TAnalysisResult
type type
InstrKind* = enum InstrKind* = enum
@ -37,7 +40,10 @@ type
Instr* = object Instr* = object
n*: PNode n*: PNode
case kind*: InstrKind case kind*: InstrKind
of def, use: sym*: PSym of def, use: sym*: PSym # 'sym' can also be 'nil' and
# then 'n' contains the def/use location.
# This is used so that we can track object
# and tuple field accesses precisely.
of goto, fork, join: dest*: int of goto, fork, join: dest*: int
ControlFlowGraph* = seq[Instr] ControlFlowGraph* = seq[Instr]
@ -47,9 +53,6 @@ type
label: PSym label: PSym
fixups: seq[TPosition] fixups: seq[TPosition]
ValueKind = enum
undef, value, valueOrUndef
Con = object Con = object
code: ControlFlowGraph code: ControlFlowGraph
inCall, inTryStmt: int inCall, inTryStmt: int
@ -77,7 +80,7 @@ proc codeListing(c: ControlFlowGraph, result: var string, start=0; last = -1) =
result.add "\t" result.add "\t"
case c[i].kind case c[i].kind
of def, use: of def, use:
result.add c[i].sym.name.s result.add renderTree(c[i].n)
of goto, fork, join: of goto, fork, join:
result.add "L" result.add "L"
result.add c[i].dest+i result.add c[i].dest+i
@ -564,16 +567,50 @@ proc genReturn(c: var Con; n: PNode) =
genNoReturn(c, n) genNoReturn(c, n)
const const
InterestingSyms = {skVar, skResult, skLet, skParam, skForVar} InterestingSyms = {skVar, skResult, skLet, skParam, skForVar, skTemp}
PathKinds* = {nkDotExpr, nkCheckedFieldExpr,
nkBracketExpr, nkDerefExpr, nkHiddenDeref,
nkAddr, nkHiddenAddr}
proc genUse(c: var Con; n: PNode) = proc genUse(c: var Con; orig: PNode) =
var n = n var n = orig
while n.kind in {nkDotExpr, nkCheckedFieldExpr, var iters = 0
nkBracketExpr, nkDerefExpr, nkHiddenDeref, while n.kind in PathKinds:
nkAddr, nkHiddenAddr}:
n = n[0] n = n[0]
inc iters
if n.kind == nkSym and n.sym.kind in InterestingSyms: if n.kind == nkSym and n.sym.kind in InterestingSyms:
c.code.add Instr(n: n, kind: use, sym: n.sym) c.code.add Instr(n: orig, kind: use, sym: if iters > 0: nil else: n.sym)
proc instrTargets*(ins: Instr; loc: PNode): bool =
assert ins.kind in {def, use}
if ins.sym != nil and loc.kind == nkSym:
result = ins.sym == loc.sym
else:
result = ins.n == loc or sameTrees(ins.n, loc)
if not result:
# We can come here if loc is 'x.f' and ins.n is 'x' or the other way round.
# def x.f; question: does it affect the full 'x'? No.
# def x; question: does it affect the 'x.f'? Yes.
# use x.f; question: does it affect the full 'x'? No.
# use x; question does it affect 'x.f'? Yes.
result = isPartOf(ins.n, loc) == arYes
proc isAnalysableFieldAccess*(n: PNode; owner: PSym): bool =
var n = n
while true:
if n.kind in {nkDotExpr, nkCheckedFieldExpr, nkHiddenSubConv, nkHiddenStdConv, nkObjDownConv, nkObjUpConv}:
n = n[0]
elif n.kind == nkBracketExpr:
let x = n[0]
if x.typ != nil and x.typ.skipTypes(abstractInst).kind == tyTuple:
n = x
else:
break
else:
break
# XXX Allow closure deref operations here if we know
# the owner controlled the closure allocation?
result = n.kind == nkSym and n.sym.owner == owner and owner.kind != skModule
proc genDef(c: var Con; n: PNode) = proc genDef(c: var Con; n: PNode) =
if n.kind == nkSym and n.sym.kind in InterestingSyms: if n.kind == nkSym and n.sym.kind in InterestingSyms:
@ -651,10 +688,12 @@ proc gen(c: var Con; n: PNode) =
of nkCharLit..nkNilLit: discard of nkCharLit..nkNilLit: discard
of nkAsgn, nkFastAsgn: of nkAsgn, nkFastAsgn:
gen(c, n[1]) gen(c, n[1])
# watch out: 'obj[i].f2 = value' sets 'f2' but
# "uses" 'i'. But we are only talking about builtin array indexing so
# it doesn't matter and 'x = 34' is NOT a usage of 'x'.
genDef(c, n[0]) genDef(c, n[0])
of nkDotExpr, nkCheckedFieldExpr, nkBracketExpr, of PathKinds:
nkDerefExpr, nkHiddenDeref, nkAddr, nkHiddenAddr: genUse(c, n)
gen(c, n[0])
of nkIfStmt, nkIfExpr: genIf(c, n) of nkIfStmt, nkIfExpr: genIf(c, n)
of nkWhenStmt: of nkWhenStmt:
# This is "when nimvm" node. Chose the first branch. # This is "when nimvm" node. Chose the first branch.

View file

@ -139,7 +139,7 @@ import
lineinfos, parampatterns lineinfos, parampatterns
const const
InterestingSyms = {skVar, skResult, skLet, skForVar} InterestingSyms = {skVar, skResult, skLet, skForVar, skTemp}
type type
Con = object Con = object
@ -153,17 +153,24 @@ type
uninit: IntSet # set of uninit'ed vars uninit: IntSet # set of uninit'ed vars
uninitComputed: bool uninitComputed: bool
proc isLastRead(s: PSym; c: var Con; pc, comesFrom: int): int = const toDebug = ""
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 var pc = pc
while pc < c.g.len: while pc < c.g.len:
case c.g[pc].kind case c.g[pc].kind
of def: of def:
if c.g[pc].sym == s: if instrTargets(c.g[pc], location):
# the path lead to a redefinition of 's' --> abandon it. # the path lead to a redefinition of 's' --> abandon it.
return high(int) return high(int)
inc pc inc pc
of use: of use:
if c.g[pc].sym == s: if instrTargets(c.g[pc], location):
c.otherRead = c.g[pc].n c.otherRead = c.g[pc].n
return -1 return -1
inc pc inc pc
@ -171,9 +178,9 @@ proc isLastRead(s: PSym; c: var Con; pc, comesFrom: int): int =
pc = pc + c.g[pc].dest pc = pc + c.g[pc].dest
of fork: of fork:
# every branch must lead to the last read of the location: # every branch must lead to the last read of the location:
var variantA = isLastRead(s, c, pc+1, pc) var variantA = isLastRead(location, c, pc+1, pc)
if variantA < 0: return -1 if variantA < 0: return -1
let variantB = isLastRead(s, c, pc + c.g[pc].dest, pc) let variantB = isLastRead(location, c, pc + c.g[pc].dest, pc)
if variantB < 0: return -1 if variantB < 0: return -1
elif variantA == high(int): elif variantA == high(int):
variantA = variantB variantA = variantB
@ -186,11 +193,11 @@ proc isLastRead(s: PSym; c: var Con; pc, comesFrom: int): int =
proc isLastRead(n: PNode; c: var Con): bool = proc isLastRead(n: PNode; c: var Con): bool =
# first we need to search for the instruction that belongs to 'n': # first we need to search for the instruction that belongs to 'n':
doAssert n.kind == nkSym
c.otherRead = nil c.otherRead = nil
var instr = -1 var instr = -1
for i in 0..<c.g.len: for i in 0..<c.g.len:
if c.g[i].n == n: # This comparison is correct and MUST not be ``instrTargets``:
if c.g[i].kind == use and c.g[i].n == n:
if instr < 0: if instr < 0:
instr = i instr = i
break break
@ -199,8 +206,11 @@ proc isLastRead(n: PNode; c: var Con): bool =
# we go through all paths beginning from 'instr+1' and need to # we go through all paths beginning from 'instr+1' and need to
# ensure that we don't find another 'use X' instruction. # ensure that we don't find another 'use X' instruction.
if instr+1 >= c.g.len: return true if instr+1 >= c.g.len: return true
dbg:
echo "beginning here ", instr
when true: when true:
result = isLastRead(n.sym, c, instr+1, -1) >= 0 result = isLastRead(n, c, instr+1, -1) >= 0
else: else:
let s = n.sym let s = n.sym
var pcs: seq[int] = @[instr+1] var pcs: seq[int] = @[instr+1]
@ -507,6 +517,8 @@ proc pArg(arg: PNode; c: var Con; isSink: bool): PNode =
branch = copyNode(arg[i]) branch = copyNode(arg[i])
branch.add pArgIfTyped(arg[i][0]) branch.add pArgIfTyped(arg[i][0])
result.add branch result.add branch
elif isAnalysableFieldAccess(arg, c.owner) and isLastRead(arg, c):
result = destructiveMoveVar(arg, c)
else: else:
# an object that is not temporary but passed to a 'sink' parameter # an object that is not temporary but passed to a 'sink' parameter
# results in a copy. # results in a copy.
@ -647,9 +659,15 @@ proc moveOrCopy(dest, ri: PNode; c: var Con): PNode =
result = genCopy(c, dest.typ, dest, ri) result = genCopy(c, dest.typ, dest, ri)
result.add p(ri, c) result.add p(ri, c)
else: else:
# XXX At least string literals can be moved? if isAnalysableFieldAccess(ri, c.owner) and isLastRead(ri, c):
result = genCopy(c, dest.typ, dest, ri) # Rule 3: `=sink`(x, z); wasMoved(z)
result.add p(ri, c) var snk = genSink(c, dest.typ, dest, ri)
snk.add ri
result = newTree(nkStmtList, snk, genWasMoved(ri, c))
else:
# XXX At least string literals can be moved?
result = genCopy(c, dest.typ, dest, ri)
result.add p(ri, c)
proc computeUninit(c: var Con) = proc computeUninit(c: var Con) =
if not c.uninitComputed: if not c.uninitComputed:
@ -785,10 +803,6 @@ proc p(n: PNode; c: var Con): PNode =
recurse(n, result) recurse(n, result)
proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode = proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
const toDebug = ""
when toDebug.len > 0:
if owner.name.s == toDebug or toDebug == "always":
echo "injecting into ", n
if sfGeneratedOp in owner.flags or isInlineIterator(owner): return n if sfGeneratedOp in owner.flags or isInlineIterator(owner): return n
var c: Con var c: Con
c.owner = owner c.owner = owner
@ -802,8 +816,9 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
for i in 0..<c.g.len: for i in 0..<c.g.len:
if c.g[i].kind in {goto, fork}: if c.g[i].kind in {goto, fork}:
c.jumpTargets.incl(i+c.g[i].dest) c.jumpTargets.incl(i+c.g[i].dest)
#if owner.name.s == "test0p1": dbg:
# echoCfg(c.g) echo "injecting into ", n
echoCfg(c.g)
if owner.kind in {skProc, skFunc, skMethod, skIterator, skConverter}: if owner.kind in {skProc, skFunc, skMethod, skIterator, skConverter}:
let params = owner.typ.n let params = owner.typ.n
for i in 1 ..< params.len: for i in 1 ..< params.len:
@ -822,8 +837,7 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
else: else:
result.add body result.add body
when toDebug.len > 0: dbg:
if owner.name.s == toDebug or toDebug == "always": echo "------------------------------------"
echo "------------------------------------" echo owner.name.s, " transformed to: "
echo owner.name.s, " transformed to: " echo result
echo result

View file

@ -48,7 +48,7 @@ proc canonKind(n: PNode): TNodeKind =
proc sameKinds(a, b: PNode): bool {.inline.} = proc sameKinds(a, b: PNode): bool {.inline.} =
result = a.kind == b.kind or a.canonKind == b.canonKind result = a.kind == b.kind or a.canonKind == b.canonKind
proc sameTrees(a, b: PNode): bool = proc sameTrees*(a, b: PNode): bool =
if sameKinds(a, b): if sameKinds(a, b):
case a.kind case a.kind
of nkSym: result = a.sym == b.sym of nkSym: result = a.sym == b.sym

View file

@ -843,7 +843,7 @@ proc gident(g: var TSrcGen, n: PNode) =
else: else:
t = tkOpr t = tkOpr
put(g, t, s, if n.kind == nkSym and renderSyms in g.flags: n.sym else: nil) put(g, t, s, if n.kind == nkSym and renderSyms in g.flags: n.sym else: nil)
if n.kind == nkSym and (renderIds in g.flags or sfGenSym in n.sym.flags): if n.kind == nkSym and (renderIds in g.flags or sfGenSym in n.sym.flags or n.sym.kind == skTemp):
when defined(debugMagics): when defined(debugMagics):
put(g, tkIntLit, $n.sym.id & $n.sym.magic) put(g, tkIntLit, $n.sym.id & $n.sym.magic)
else: else:

View file

@ -1009,8 +1009,8 @@ proc transform(c: PTransf, n: PNode): PTransNode =
result = transformYield(c, n) result = transformYield(c, n)
else: else:
result = transformSons(c, n) result = transformSons(c, n)
#of nkAsgn: of nkAsgn:
# result = transformAsgn(c, n) result = transformAsgn(c, n)
of nkIdentDefs, nkConstDef: of nkIdentDefs, nkConstDef:
result = PTransNode(n) result = PTransNode(n)
result[0] = transform(c, n[0]) result[0] = transform(c, n[0])

View file

@ -23,5 +23,5 @@ proc test(): auto =
var (a, b, _) = test() var (a, b, _) = test()
doAssert: assign_counter == 0 doAssert assign_counter == 0
doAssert: sink_counter == 9 doAssert sink_counter == 12 # + 3 because of the conservative tuple unpacking transformation

View file

@ -88,8 +88,8 @@ proc `-`*(a: sink Matrix; b: Matrix): Matrix =
doAssert(a.len == b.len) # non destructive use before sink is ok doAssert(a.len == b.len) # non destructive use before sink is ok
result = a result = a
for i in 0 ..< result.m: for i in 0 ..< result.m:
for j in 0 ..< result.n: for j in 0 ..< result.n:
result[i, j] = a[i, j] - b[i, j] result[i, j] = a[i, j] - b[i, j]
proc info = proc info =
echo "after ", allocCount, " ", deallocCount echo "after ", allocCount, " ", deallocCount

View file

@ -0,0 +1,35 @@
discard """
output: '''works'''
"""
type
MyVal = object
f: ptr float
proc `=destroy`(x: var MyVal) =
if x.f != nil:
dealloc(x.f)
proc `=sink`(x1: var MyVal, x2: Myval) =
if x1.f != x2.f:
`=destroy`(x1)
x1.f = x2.f
proc `=`(x1: var MyVal, x2: Myval) {.error.}
proc newVal(x: float): MyVal =
result.f = create(float)
result.f[] = x
proc sinkMe(x: sink MyVal) =
discard
proc main =
var y = (newVal(3.0), newVal(4.0))
sinkMe y[0]
sinkMe y[1]
echo "works"
main()