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

(cherry picked from commit 9f78f116b2)
This commit is contained in:
Clyybber
2020-05-15 20:24:43 +02:00
committed by narimiran
parent a7ebbad8cf
commit c3ecd38da1
3 changed files with 61 additions and 74 deletions

View File

@@ -35,11 +35,11 @@ from patterns import sameTrees
type
InstrKind* = enum
goto, fork, join, def, use
goto, fork, def, use
Instr* = object
n*: PNode # contains the def/use location.
case kind*: InstrKind
of goto, fork, join: dest*: int
of goto, fork: dest*: int
else: discard
ControlFlowGraph* = seq[Instr]
@@ -47,8 +47,6 @@ type
TPosition = distinct int
TBlock = object
joins: seq[Instr]
forks: seq[TPosition]
case isTryBlock: bool
of false:
label: PSym
@@ -72,7 +70,7 @@ proc codeListing(c: ControlFlowGraph, result: var string, start=0; last = -1) =
var jumpTargets = initIntSet()
let last = if last < 0: c.len-1 else: min(last, c.len-1)
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)
var i = start
while i <= last:
@@ -83,7 +81,7 @@ proc codeListing(c: ControlFlowGraph, result: var string, start=0; last = -1) =
case c[i].kind
of def, use:
result.add renderTree(c[i].n)
of goto, fork, join:
of goto, fork:
result.add "L"
result.addInt c[i].dest+i
result.add("\t#")
@@ -102,7 +100,6 @@ proc echoCfg*(c: ControlFlowGraph; start=0; last = -1) {.deprecated.} =
proc forkI(c: var Con; n: PNode): TPosition =
result = TPosition(c.code.len)
c.code.add Instr(n: n, kind: fork, dest: 0)
c.blocks[^1].forks.add result
proc gotoI(c: var Con; n: PNode): TPosition =
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
==============
@@ -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 =
result = TPosition(c.code.len)
@@ -289,10 +300,6 @@ proc popBlock(c: var Con; oldLen: int) =
exits.add c.gotoI(newNode(nkEmpty))
for e in exits:
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)
template withBlock(labl: PSym; body: untyped) {.dirty.} =
@@ -353,7 +360,6 @@ when true:
c.gen(n[1])
else:
withBlock(nil):
let oldForksLen = c.blocks[^1].forks.len
var endings: array[3, TPosition]
for i in 0..2:
c.gen(n[0])
@@ -362,8 +368,6 @@ when true:
for i in countdown(endings.high, 0):
let endPos = endings[i]
c.patch(endPos)
c.joinI(c.blocks[^1].forks.pop(), n)
doAssert(c.blocks[^1].forks.len == oldForksLen)
else:
@@ -374,7 +378,6 @@ else:
# body
# jmp lab1
# lab2:
let oldForksLen = c.blocks[^1].forks.len
let lab1 = c.genLabel
withBlock(nil):
if isTrue(n[0]):
@@ -386,15 +389,11 @@ else:
c.gen(n[1])
c.jmpBack(n, lab1)
c.patch(lab2)
setLen(c.blocks[^1].forks, oldForksLen)
template forkT(n, body) =
let oldLen = c.blocks[^1].forks.len
let lab1 = c.forkI(n)
body
c.patch(lab1)
c.joinI(lab1, n)
setLen(c.blocks[^1].forks, oldLen)
proc genIf(c: var Con, n: PNode) =
#[
@@ -433,7 +432,6 @@ proc genIf(c: var Con, n: PNode) =
join F1
]#
let oldLen = c.blocks[^1].forks.len
var endings: seq[TPosition] = @[]
for i in 0..<n.len:
var it = n[i]
@@ -446,8 +444,6 @@ proc genIf(c: var Con, n: PNode) =
for i in countdown(endings.high, 0):
let endPos = endings[i]
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) =
# asgn dest, a
@@ -474,7 +470,6 @@ proc genCase(c: var Con; n: PNode) =
abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString}
var endings: seq[TPosition] = @[]
let oldLen = c.blocks[^1].forks.len
c.gen(n[0])
for i in 1..<n.len:
let it = n[i]
@@ -491,8 +486,6 @@ proc genCase(c: var Con; n: PNode) =
for i in countdown(endings.high, 0):
let endPos = endings[i]
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) =
withBlock(n[0].sym):
@@ -511,10 +504,6 @@ proc genBreakOrRaiseAux(c: var Con, i: int, n: PNode) =
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) =
if n[0].kind == nkSym:
#echo cast[int](n[0].sym)
@@ -530,7 +519,6 @@ proc genBreak(c: var Con; n: PNode) =
return
proc genTry(c: var Con; n: PNode) =
let oldForksLen = c.blocks[^1].forks.len
var endings: seq[TPosition] = @[]
let oldLen = c.blocks.len
@@ -543,10 +531,6 @@ proc genTry(c: var Con; n: PNode) =
for f in c.blocks[oldLen].raiseFixups:
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
@@ -561,7 +545,6 @@ proc genTry(c: var Con; n: PNode) =
for i in countdown(endings.high, 0):
let endPos = endings[i]
c.patch(endPos)
c.joinI(c.blocks[^1].forks.pop(), n)
# join the 'elsePos' forkI instruction:
#c.joinI(c.blocks[^1].forks.pop(), n)
@@ -569,7 +552,6 @@ proc genTry(c: var Con; n: PNode) =
let fin = lastSon(n)
if fin.kind == nkFinally:
c.gen(fin[0])
doAssert(c.blocks[^1].forks.len == oldForksLen)
template genNoReturn(c: var Con; n: PNode) =
# leave the graph
@@ -750,7 +732,6 @@ proc genCall(c: var Con; n: PNode) =
genBreakOrRaiseAux(c, i, n)
break
c.patch(endGoto)
c.joinI(c.blocks[^1].forks.pop(), n)
dec c.inCall
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 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
while pc < c.g.len:
while pc < c.g.len and pc < until:
case c.g[pc].kind
of def:
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
of fork:
# every branch must lead to the last read of the location:
let variantA = isLastRead(location, c, pc+1, pc)
if variantA < 0: return -1
var variantB = isLastRead(location, c, pc + c.g[pc].dest, pc)
if variantB < 0: return -1
var variantA = pc + 1
var variantB = pc + c.g[pc].dest
while variantA != variantB:
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)
of InstrKind.join:
let dest = pc + c.g[pc].dest
if dest == comesFrom: return pc + 1
inc pc
return pc
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.
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
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
while pc < instr:
while pc < until:
case c.g[pc].kind
of def:
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
of fork:
# every branch must not contain a def/use of our location:
let variantA = isFirstWrite(location, c, pc+1, pc, instr)
if variantA < 0: return -1
var variantB = isFirstWrite(location, c, pc + c.g[pc].dest, pc, instr + c.g[pc].dest)
if variantB < 0: return -1
var variantA = pc + 1
var variantB = pc + c.g[pc].dest
while variantA != variantB:
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)
of InstrKind.join:
let dest = pc + c.g[pc].dest
if dest == comesFrom: return pc + 1
inc pc
return pc
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.
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,
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
## as an IntSet of IDs.
var pc = pc
@@ -173,20 +177,22 @@ proc initialized(code: ControlFlowGraph; pc: int,
of goto:
pc = pc + code[pc].dest
of fork:
let target = pc + code[pc].dest
var initA = initIntSet()
var initB = initIntSet()
let pcA = initialized(code, pc+1, initA, uninit, pc)
discard initialized(code, target, initB, uninit, pc)
var variantA = pc + 1
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:
for v in initA:
if v in initB:
init.incl v
pc = pcA+1
of InstrKind.join:
let target = pc + code[pc].dest
if comesFrom == target: return pc
inc pc
of use:
let v = code[pc].n.sym
if v.kind != skParam and v.id notin init:
@@ -1033,7 +1039,7 @@ proc computeUninit(c: var Con) =
c.uninitComputed = true
c.uninit = 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) =
case n.kind

View File

@@ -96,4 +96,4 @@ proc procStat() =
let cols = line.splitWhitespace(maxSplit=1)
let x = cols[0]
let (nm, rest) = (cols[0], cols[1])
procStat()
procStat()