New "ping-pong" DFA (#14322)

* New ping-pong analysis

* Add testcase for #13456

* Remove debugging leftover

* Unquote "unstructured controlflow"

* Fix typo

* Fix exponential complexity in edge cases

* Add sanity testcase

* Fix
This commit is contained in:
Clyybber 2020-05-15 20:24:43 +02:00 • committed by GitHub
commit 9f78f116b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 163 additions and 73 deletions

View file

@ -35,11 +35,11 @@ from patterns import sameTrees
type type
InstrKind* = enum InstrKind* = enum
goto, fork, join, def, use goto, fork, def, use
Instr* = object Instr* = object
n*: PNode # contains the def/use location. n*: PNode # contains the def/use location.
case kind*: InstrKind case kind*: InstrKind
of goto, fork, join: dest*: int of goto, fork: dest*: int
else: discard else: discard
ControlFlowGraph* = seq[Instr] ControlFlowGraph* = seq[Instr]
@ -47,8 +47,6 @@ type
TPosition = distinct int TPosition = distinct int
TBlock = object TBlock = object
joins: seq[Instr]
forks: seq[TPosition]
case isTryBlock: bool case isTryBlock: bool
of false: of false:
label: PSym label: PSym
@ -72,7 +70,7 @@ proc codeListing(c: ControlFlowGraph, result: var string, start=0; last = -1) =
var jumpTargets = initIntSet() var jumpTargets = initIntSet()
let last = if last < 0: c.len-1 else: min(last, c.len-1) let last = if last < 0: c.len-1 else: min(last, c.len-1)
for i in start..last: for i in start..last:
if c[i].kind in {goto, fork, join}: if c[i].kind in {goto, fork}:
jumpTargets.incl(i+c[i].dest) jumpTargets.incl(i+c[i].dest)
var i = start var i = start
while i <= last: while i <= last:
@ -83,7 +81,7 @@ proc codeListing(c: ControlFlowGraph, result: var string, start=0; last = -1) =
case c[i].kind case c[i].kind
of def, use: of def, use:
result.add renderTree(c[i].n) result.add renderTree(c[i].n)
of goto, fork, join: of goto, fork:
result.add "L" result.add "L"
result.addInt c[i].dest+i result.addInt c[i].dest+i
result.add("\t#") result.add("\t#")
@ -102,7 +100,6 @@ proc echoCfg*(c: ControlFlowGraph; start=0; last = -1) {.deprecated.} =
proc forkI(c: var Con; n: PNode): TPosition = proc forkI(c: var Con; n: PNode): TPosition =
result = TPosition(c.code.len) result = TPosition(c.code.len)
c.code.add Instr(n: n, kind: fork, dest: 0) c.code.add Instr(n: n, kind: fork, dest: 0)
c.blocks[^1].forks.add result
proc gotoI(c: var Con; n: PNode): TPosition = proc gotoI(c: var Con; n: PNode): TPosition =
result = TPosition(c.code.len) result = TPosition(c.code.len)
@ -110,6 +107,24 @@ proc gotoI(c: var Con; n: PNode): TPosition =
#[ #[
Join is no more
===============
Instead of generating join instructions we adapt our traversal of the CFG.
When encountering a fork we split into two paths, we follow the path
starting at "pc + 1" until it encounters the joinpoint: "pc + forkInstr.dest".
If we encounter gotos that would jump further than the current joinpoint,
as can happen with gotos generated by unstructured controlflow such as break, raise or return,
we simply suspend following the current path, and follow the other path until the new joinpoint
which is simply the instruction pointer returned to us by the now suspended path.
If the path we are following now, also encounters a goto that exceeds the joinpoint
we repeat the process; suspending the current path and evaluating the other one with a new joinpoint.
If we eventually reach a common joinpoint we join the two paths.
This new "ping-pong" approach has the obvious advantage of not requiring join instructions, as such
cutting down on the CFG size but is also mandatory for correctly handling complicated cases
of unstructured controlflow.
Design of join Design of join
============== ==============
@ -258,10 +273,6 @@ duplicate the 'join' instructions on breaks and return exits!
]# ]#
proc joinI(c: var Con; fromFork: TPosition; n: PNode) =
let dist = fromFork.int - c.code.len
c.code.add Instr(n: n, kind: join, dest: dist)
proc genLabel(c: Con): TPosition = proc genLabel(c: Con): TPosition =
result = TPosition(c.code.len) result = TPosition(c.code.len)
@ -289,10 +300,6 @@ proc popBlock(c: var Con; oldLen: int) =
exits.add c.gotoI(newNode(nkEmpty)) exits.add c.gotoI(newNode(nkEmpty))
for e in exits: for e in exits:
c.patch e c.patch e
for j in c.blocks[oldLen].joins:
var patchedJ = j
patchedJ.dest -= c.code.len
c.code.add patchedJ
c.blocks.setLen(oldLen) c.blocks.setLen(oldLen)
template withBlock(labl: PSym; body: untyped) {.dirty.} = template withBlock(labl: PSym; body: untyped) {.dirty.} =
@ -353,7 +360,6 @@ when true:
c.gen(n[1]) c.gen(n[1])
else: else:
withBlock(nil): withBlock(nil):
let oldForksLen = c.blocks[^1].forks.len
var endings: array[3, TPosition] var endings: array[3, TPosition]
for i in 0..2: for i in 0..2:
c.gen(n[0]) c.gen(n[0])
@ -362,8 +368,6 @@ when true:
for i in countdown(endings.high, 0): for i in countdown(endings.high, 0):
let endPos = endings[i] let endPos = endings[i]
c.patch(endPos) c.patch(endPos)
c.joinI(c.blocks[^1].forks.pop(), n)
doAssert(c.blocks[^1].forks.len == oldForksLen)
else: else:
@ -374,7 +378,6 @@ else:
# body # body
# jmp lab1 # jmp lab1
# lab2: # lab2:
let oldForksLen = c.blocks[^1].forks.len
let lab1 = c.genLabel let lab1 = c.genLabel
withBlock(nil): withBlock(nil):
if isTrue(n[0]): if isTrue(n[0]):
@ -386,15 +389,11 @@ else:
c.gen(n[1]) c.gen(n[1])
c.jmpBack(n, lab1) c.jmpBack(n, lab1)
c.patch(lab2) c.patch(lab2)
setLen(c.blocks[^1].forks, oldForksLen)
template forkT(n, body) = template forkT(n, body) =
let oldLen = c.blocks[^1].forks.len
let lab1 = c.forkI(n) let lab1 = c.forkI(n)
body body
c.patch(lab1) c.patch(lab1)
c.joinI(lab1, n)
setLen(c.blocks[^1].forks, oldLen)
proc genIf(c: var Con, n: PNode) = proc genIf(c: var Con, n: PNode) =
#[ #[
@ -433,7 +432,6 @@ proc genIf(c: var Con, n: PNode) =
join F1 join F1
]# ]#
let oldLen = c.blocks[^1].forks.len
var endings: seq[TPosition] = @[] var endings: seq[TPosition] = @[]
for i in 0..<n.len: for i in 0..<n.len:
var it = n[i] var it = n[i]
@ -446,8 +444,6 @@ proc genIf(c: var Con, n: PNode) =
for i in countdown(endings.high, 0): for i in countdown(endings.high, 0):
let endPos = endings[i] let endPos = endings[i]
c.patch(endPos) c.patch(endPos)
c.joinI(c.blocks[^1].forks.pop(), n)
doAssert(c.blocks[^1].forks.len == oldLen)
proc genAndOr(c: var Con; n: PNode) = proc genAndOr(c: var Con; n: PNode) =
# asgn dest, a # asgn dest, a
@ -474,7 +470,6 @@ proc genCase(c: var Con; n: PNode) =
abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString} abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString}
var endings: seq[TPosition] = @[] var endings: seq[TPosition] = @[]
let oldLen = c.blocks[^1].forks.len
c.gen(n[0]) c.gen(n[0])
for i in 1..<n.len: for i in 1..<n.len:
let it = n[i] let it = n[i]
@ -491,8 +486,6 @@ proc genCase(c: var Con; n: PNode) =
for i in countdown(endings.high, 0): for i in countdown(endings.high, 0):
let endPos = endings[i] let endPos = endings[i]
c.patch(endPos) c.patch(endPos)
c.joinI(c.blocks[^1].forks.pop(), n)
doAssert(c.blocks[^1].forks.len == oldLen)
proc genBlock(c: var Con; n: PNode) = proc genBlock(c: var Con; n: PNode) =
withBlock(n[0].sym): withBlock(n[0].sym):
@ -511,10 +504,6 @@ proc genBreakOrRaiseAux(c: var Con, i: int, n: PNode) =
c.blocks[i].breakFixups.add (lab1, trailingFinales) c.blocks[i].breakFixups.add (lab1, trailingFinales)
for b in countdown(c.blocks.high, i):
for f in countdown(c.blocks[b].forks.high, 0):
c.blocks[i].joins.add Instr(n: n, kind: join, dest: c.blocks[b].forks[f].int)
proc genBreak(c: var Con; n: PNode) = proc genBreak(c: var Con; n: PNode) =
if n[0].kind == nkSym: if n[0].kind == nkSym:
#echo cast[int](n[0].sym) #echo cast[int](n[0].sym)
@ -530,7 +519,6 @@ proc genBreak(c: var Con; n: PNode) =
return return
proc genTry(c: var Con; n: PNode) = proc genTry(c: var Con; n: PNode) =
let oldForksLen = c.blocks[^1].forks.len
var endings: seq[TPosition] = @[] var endings: seq[TPosition] = @[]
let oldLen = c.blocks.len let oldLen = c.blocks.len
@ -543,10 +531,6 @@ proc genTry(c: var Con; n: PNode) =
for f in c.blocks[oldLen].raiseFixups: for f in c.blocks[oldLen].raiseFixups:
c.patch(f) c.patch(f)
for j in c.blocks[oldLen].joins:
var patchedJ = j
patchedJ.dest -= c.code.len
c.code.add patchedJ
c.blocks.setLen oldLen c.blocks.setLen oldLen
@ -561,7 +545,6 @@ proc genTry(c: var Con; n: PNode) =
for i in countdown(endings.high, 0): for i in countdown(endings.high, 0):
let endPos = endings[i] let endPos = endings[i]
c.patch(endPos) c.patch(endPos)
c.joinI(c.blocks[^1].forks.pop(), n)
# join the 'elsePos' forkI instruction: # join the 'elsePos' forkI instruction:
#c.joinI(c.blocks[^1].forks.pop(), n) #c.joinI(c.blocks[^1].forks.pop(), n)
@ -569,7 +552,6 @@ proc genTry(c: var Con; n: PNode) =
let fin = lastSon(n) let fin = lastSon(n)
if fin.kind == nkFinally: if fin.kind == nkFinally:
c.gen(fin[0]) c.gen(fin[0])
doAssert(c.blocks[^1].forks.len == oldForksLen)
template genNoReturn(c: var Con; n: PNode) = template genNoReturn(c: var Con; n: PNode) =
# leave the graph # leave the graph
@ -751,7 +733,6 @@ proc genCall(c: var Con; n: PNode) =
genBreakOrRaiseAux(c, i, n) genBreakOrRaiseAux(c, i, n)
break break
c.patch(endGoto) c.patch(endGoto)
c.joinI(c.blocks[^1].forks.pop(), n)
dec c.inCall dec c.inCall
proc genMagic(c: var Con; n: PNode; m: TMagic) = proc genMagic(c: var Con; n: PNode; m: TMagic) =

View file

@ -65,9 +65,9 @@ template dbg(body) =
proc p(n: PNode; c: var Con; mode: ProcessMode): PNode proc p(n: PNode; c: var Con; mode: ProcessMode): PNode
proc moveOrCopy(dest, ri: PNode; c: var Con): PNode proc moveOrCopy(dest, ri: PNode; c: var Con): PNode
proc isLastRead(location: PNode; c: var Con; pc, comesFrom: int): int = proc isLastRead(location: PNode; c: var Con; pc, until: int): int =
var pc = pc var pc = pc
while pc < c.g.len: while pc < c.g.len and pc < until:
case c.g[pc].kind case c.g[pc].kind
of def: of def:
if defInstrTargets(c.g[pc], location): if defInstrTargets(c.g[pc], location):
@ -83,15 +83,17 @@ proc isLastRead(location: PNode; 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:
let variantA = isLastRead(location, c, pc+1, pc) var variantA = pc + 1
if variantA < 0: return -1 var variantB = pc + c.g[pc].dest
var variantB = isLastRead(location, c, pc + c.g[pc].dest, pc) while variantA != variantB:
if variantB < 0: return -1 if min(variantA, variantB) < 0: return -1
if max(variantA, variantB) >= c.g.len or min(variantA, variantB) >= until:
break
if variantA < variantB:
variantA = isLastRead(location, c, variantA, min(variantB, until))
else:
variantB = isLastRead(location, c, variantB, min(variantA, until))
pc = min(variantA, variantB) pc = min(variantA, variantB)
of InstrKind.join:
let dest = pc + c.g[pc].dest
if dest == comesFrom: return pc + 1
inc pc
return pc return pc
proc isLastRead(n: PNode; c: var Con): bool = proc isLastRead(n: PNode; c: var Con): bool =
@ -114,12 +116,12 @@ proc isLastRead(n: PNode; c: var Con): bool =
# 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
result = isLastRead(n, c, instr+1, -1) >= 0 result = isLastRead(n, c, instr+1, int.high) >= 0
dbg: echo "ugh ", c.otherRead.isNil, " ", result dbg: echo "ugh ", c.otherRead.isNil, " ", result
proc isFirstWrite(location: PNode; c: var Con; pc, comesFrom: int; instr: int): int = proc isFirstWrite(location: PNode; c: var Con; pc, until: int): int =
var pc = pc var pc = pc
while pc < instr: while pc < until:
case c.g[pc].kind case c.g[pc].kind
of def: of def:
if defInstrTargets(c.g[pc], location): if defInstrTargets(c.g[pc], location):
@ -134,15 +136,17 @@ proc isFirstWrite(location: PNode; c: var Con; pc, comesFrom: int; instr: int):
pc = pc + c.g[pc].dest pc = pc + c.g[pc].dest
of fork: of fork:
# every branch must not contain a def/use of our location: # every branch must not contain a def/use of our location:
let variantA = isFirstWrite(location, c, pc+1, pc, instr) var variantA = pc + 1
if variantA < 0: return -1 var variantB = pc + c.g[pc].dest
var variantB = isFirstWrite(location, c, pc + c.g[pc].dest, pc, instr + c.g[pc].dest) while variantA != variantB:
if variantB < 0: return -1 if min(variantA, variantB) < 0: return -1
if max(variantA, variantB) > until:
break
if variantA < variantB:
variantA = isFirstWrite(location, c, variantA, min(variantB, until))
else:
variantB = isFirstWrite(location, c, variantB, min(variantA, until))
pc = min(variantA, variantB) pc = min(variantA, variantB)
of InstrKind.join:
let dest = pc + c.g[pc].dest
if dest == comesFrom: return pc + 1
inc pc
return pc return pc
proc isFirstWrite(n: PNode; c: var Con): bool = proc isFirstWrite(n: PNode; c: var Con): bool =
@ -161,10 +165,10 @@ proc isFirstWrite(n: PNode; c: var Con): bool =
# ensure that we don't find another 'def/use X' instruction. # ensure that we don't find another 'def/use X' instruction.
if instr == 0: return true if instr == 0: return true
result = isFirstWrite(n, c, 0, -1, instr) >= 0 result = isFirstWrite(n, c, 0, instr) >= 0
proc initialized(code: ControlFlowGraph; pc: int, proc initialized(code: ControlFlowGraph; pc: int,
init, uninit: var IntSet; comesFrom: int): int = init, uninit: var IntSet; until: int): int =
## Computes the set of definitely initialized variables across all code paths ## Computes the set of definitely initialized variables across all code paths
## as an IntSet of IDs. ## as an IntSet of IDs.
var pc = pc var pc = pc
@ -173,20 +177,22 @@ proc initialized(code: ControlFlowGraph; pc: int,
of goto: of goto:
pc = pc + code[pc].dest pc = pc + code[pc].dest
of fork: of fork:
let target = pc + code[pc].dest
var initA = initIntSet() var initA = initIntSet()
var initB = initIntSet() var initB = initIntSet()
let pcA = initialized(code, pc+1, initA, uninit, pc) var variantA = pc + 1
discard initialized(code, target, initB, uninit, pc) var variantB = pc + code[pc].dest
while variantA != variantB:
if max(variantA, variantB) > until:
break
if variantA < variantB:
variantA = initialized(code, variantA, initA, uninit, min(variantB, until))
else:
variantB = initialized(code, variantB, initB, uninit, min(variantA, until))
pc = min(variantA, variantB)
# we add vars if they are in both branches: # we add vars if they are in both branches:
for v in initA: for v in initA:
if v in initB: if v in initB:
init.incl v init.incl v
pc = pcA+1
of InstrKind.join:
let target = pc + code[pc].dest
if comesFrom == target: return pc
inc pc
of use: of use:
let v = code[pc].n.sym let v = code[pc].n.sym
if v.kind != skParam and v.id notin init: if v.kind != skParam and v.id notin init:
@ -1040,7 +1046,7 @@ proc computeUninit(c: var Con) =
c.uninitComputed = true c.uninitComputed = true
c.uninit = initIntSet() c.uninit = initIntSet()
var init = initIntSet() var init = initIntSet()
discard initialized(c.g, pc = 0, init, c.uninit, comesFrom = -1) discard initialized(c.g, pc = 0, init, c.uninit, int.high)
proc injectDefaultCalls(n: PNode, c: var Con) = proc injectDefaultCalls(n: PNode, c: var Con) =
case n.kind case n.kind

View file

@ -19,6 +19,23 @@ c.text = hello
c.text = hello c.text = hello
pOD.text = hello pOD.text = hello
pOD.toks = @["hello"] pOD.toks = @["hello"]
fff
fff
2
fff
fff
2
fff
fff
2
mmm
fff
fff
fff
3
mmm
sink me (sink)
assign me (not sink)
''' '''
""" """
@ -199,3 +216,89 @@ when false:
let pOHD = parseOHD() let pOHD = parseOHD()
dump pOHD.text dump pOHD.text
dump pOHD.toks dump pOHD.toks
# bug #13456
iterator combinations[T](s: openarray[T], k: int): seq[T] =
let n = len(s)
assert k >= 0 and k <= n
var pos = newSeq[int](k)
var current = newSeq[T](k)
for i in 0..k-1:
pos[k-i-1] = i
var done = false
while not done:
for i in 0..k-1:
current[i] = s[pos[k-i-1]]
yield current
var i = 0
while i < k:
pos[i] += 1
if pos[i] < n-i:
for j in 0..i-1:
pos[j] = pos[i] + i - j
break
i += 1
if i >= k:
break
type
UndefEx = object of Exception
proc main2 =
var delayedSyms = @[1, 2, 3]
var unp: seq[int]
block myb:
for a in 1 .. 2:
if delayedSyms.len > a:
unp = delayedSyms
for t in unp.combinations(a + 1):
try:
var h = false
for k in t:
echo "fff"
if h: continue
if true:
raise newException(UndefEx, "forward declaration")
break myb
except UndefEx:
echo t.len
echo "mmm"
main2()
type ME = object
who: string
proc `=`(x: var ME, y: ME) =
if x.who.len > 0: echo "assign ",x.who
proc `=sink`(x: var ME, y: ME) =
if x.who.len > 0: echo "sink ",x.who
var dump: ME
template use(x) = dump = x
template def(x) = x = dump
var c = true
proc shouldSink() =
var x = ME(who: "me (sink)")
use(x) # we analyse this
if c: def(x)
else: def(x)
use(x) # ok, with the [else] part.
shouldSink()
dump = ME()
proc shouldNotSink() =
var x = ME(who: "me (not sink)")
use(x) # we analyse this
if c: def(x)
use(x) # Not ok without the '[else]'
shouldNotSink()