Rework DFA traversal (#18016)

* enable using dbg: without a context

* Optimally joining first write/last read analysis

* Add test for #18002

* potLastReads -> potentialLastReads
This commit is contained in:
Clyybber 2021-05-16 00:15:53 +02:00 • committed by GitHub
commit 3614523664
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 133 additions and 109 deletions

View file

@ -46,6 +46,13 @@ type
sinkArg sinkArg
const toDebug {.strdefine.} = "" const toDebug {.strdefine.} = ""
when toDebug.len > 0:
var shouldDebug = false
template dbg(body) =
when toDebug.len > 0:
if shouldDebug:
body
proc hasDestructor(c: Con; t: PType): bool {.inline.} = proc hasDestructor(c: Con; t: PType): bool {.inline.} =
result = ast.hasDestructor(t) result = ast.hasDestructor(t)
@ -54,11 +61,6 @@ proc hasDestructor(c: Con; t: PType): bool {.inline.} =
if not result and c.graph.config.selectedGC in {gcArc, gcOrc}: if not result and c.graph.config.selectedGC in {gcArc, gcOrc}:
assert(not containsGarbageCollectedRef(t)) assert(not containsGarbageCollectedRef(t))
template dbg(body) =
when toDebug.len > 0:
if c.owner.name.s == toDebug or toDebug == "always":
body
proc getTemp(c: var Con; s: var Scope; typ: PType; info: TLineInfo): PNode = proc getTemp(c: var Con; s: var Scope; typ: PType; info: TLineInfo): PNode =
let sym = newSym(skTemp, getIdent(c.graph.cache, ":tmpD"), nextSymId c.idgen, c.owner, info) let sym = newSym(skTemp, getIdent(c.graph.cache, ":tmpD"), nextSymId c.idgen, c.owner, info)
sym.typ = typ sym.typ = typ
@ -75,106 +77,111 @@ import sets, hashes, tables
proc hash(n: PNode): Hash = hash(cast[pointer](n)) proc hash(n: PNode): Hash = hash(cast[pointer](n))
type AliasCache = Table[(PNode, PNode), AliasKind] proc aliasesCached(cache: var Table[(PNode, PNode), AliasKind], obj, field: PNode): AliasKind =
proc aliasesCached(cache: var AliasCache, obj, field: PNode): AliasKind =
let key = (obj, field) let key = (obj, field)
if not cache.hasKey(key): if not cache.hasKey(key):
cache[key] = aliases(obj, field) cache[key] = aliases(obj, field)
cache[key] cache[key]
proc collectLastReads(cfg: ControlFlowGraph; cache: var AliasCache, lastReads, potLastReads: var IntSet; pc: var int, until: int) = type
template aliasesCached(obj, field: PNode): untyped = State = ref object
lastReads: IntSet
potentialLastReads: IntSet
notLastReads: IntSet
alreadySeen: HashSet[PNode]
proc preprocessCfg(cfg: var ControlFlowGraph) =
for i in 0..<cfg.len:
if cfg[i].kind in {goto, fork} and i + cfg[i].dest > cfg.len:
cfg[i].dest = cfg.len - i
proc mergeStates(a: var State, b: sink State) =
# Inplace for performance:
# lastReads = a.lastReads + b.lastReads
# potentialLastReads = (a.potentialLastReads + b.potentialLastReads) - (a.notLastReads + b.notLastReads)
# notLastReads = a.notLastReads + b.notLastReads
# alreadySeen = a.alreadySeen + b.alreadySeen
# b is never nil
if a == nil:
a = b
else:
a.lastReads.incl b.lastReads
a.potentialLastReads.incl b.potentialLastReads
a.potentialLastReads.excl a.notLastReads
a.potentialLastReads.excl b.notLastReads
a.notLastReads.incl b.notLastReads
a.alreadySeen.incl b.alreadySeen
proc computeLastReadsAndFirstWrites(cfg: ControlFlowGraph) =
var cache = initTable[(PNode, PNode), AliasKind]()
template aliasesCached(obj, field: PNode): AliasKind =
aliasesCached(cache, obj, field) aliasesCached(cache, obj, field)
while pc < until:
case cfg[pc].kind
of def:
let potLastReadsCopy = potLastReads
for r in potLastReadsCopy:
if cfg[pc].n.aliasesCached(cfg[r].n) == yes:
# the path leads to a redefinition of 's' --> sink 's'.
lastReads.incl r
potLastReads.excl r
elif cfg[r].n.aliasesCached(cfg[pc].n) != no:
# only partially writes to 's' --> can't sink 's', so this def reads 's'
# or maybe writes to 's' --> can't sink 's'
cfg[r].n.comment = '\n' & $pc
potLastReads.excl r
inc pc var cfg = cfg
of use: preprocessCfg(cfg)
let potLastReadsCopy = potLastReads
for r in potLastReadsCopy:
if cfg[pc].n.aliasesCached(cfg[r].n) != no or cfg[r].n.aliasesCached(cfg[pc].n) != no:
cfg[r].n.comment = '\n' & $pc
potLastReads.excl r
potLastReads.incl pc var states = newSeq[State](cfg.len + 1)
states[0] = State()
inc pc for pc in 0..<cfg.len:
of goto: template state: State = states[pc]
pc += cfg[pc].dest if state != nil:
of fork: case cfg[pc].kind
var variantA = pc + 1 of def:
var variantB = pc + cfg[pc].dest var potentialLastReadsCopy = state.potentialLastReads
var potLastReadsA, potLastReadsB = potLastReads for r in potentialLastReadsCopy:
var lastReadsA, lastReadsB: IntSet if cfg[pc].n.aliasesCached(cfg[r].n) == yes:
while variantA != variantB and max(variantA, variantB) < cfg.len and min(variantA, variantB) < until: # the path leads to a redefinition of 's' --> sink 's'.
if variantA < variantB: state.lastReads.incl r
collectLastReads(cfg, cache, lastReadsA, potLastReadsA, variantA, min(variantB, until)) state.potentialLastReads.excl r
else: elif cfg[r].n.aliasesCached(cfg[pc].n) != no:
collectLastReads(cfg, cache, lastReadsB, potLastReadsB, variantB, min(variantA, until)) # only partially writes to 's' --> can't sink 's', so this def reads 's'
# or maybe writes to 's' --> can't sink 's'
cfg[r].n.comment = '\n' & $pc
state.potentialLastReads.excl r
state.notLastReads.incl r
# Add those last reads that were turned into last reads on both branches var alreadySeenThisNode = false
lastReads.incl lastReadsA * lastReadsB for s in state.alreadySeen:
# Add those last reads that were turned into last reads on only one branch, if cfg[pc].n.aliasesCached(s) != no or s.aliasesCached(cfg[pc].n) != no:
# but where the read operation itself also belongs to only that branch alreadySeenThisNode = true; break
lastReads.incl (lastReadsA + lastReadsB) - potLastReads if alreadySeenThisNode: cfg[pc].n.flags.excl nfFirstWrite
else: cfg[pc].n.flags.incl nfFirstWrite
let oldPotLastReads = potLastReads state.alreadySeen.incl cfg[pc].n
potLastReads = initIntSet()
potLastReads.incl potLastReadsA + potLastReadsB mergeStates(states[pc + 1], move(states[pc]))
of use:
var potentialLastReadsCopy = state.potentialLastReads
for r in potentialLastReadsCopy:
if cfg[pc].n.aliasesCached(cfg[r].n) != no or cfg[r].n.aliasesCached(cfg[pc].n) != no:
cfg[r].n.comment = '\n' & $pc
state.potentialLastReads.excl r
state.notLastReads.incl r
# Remove potential last reads that were invalidated in a branch, state.potentialLastReads.incl pc
# but don't remove those which were turned into last reads on that branch
potLastReads.excl ((oldPotLastReads - potLastReadsA) - lastReadsA)
potLastReads.excl ((oldPotLastReads - potLastReadsB) - lastReadsB)
pc = min(variantA, variantB) state.alreadySeen.incl cfg[pc].n
proc collectFirstWrites(cfg: ControlFlowGraph; alreadySeen: var HashSet[PNode]; pc: var int, until: int) = mergeStates(states[pc + 1], move(states[pc]))
while pc < until: of goto:
case cfg[pc].kind mergeStates(states[pc + cfg[pc].dest], move(states[pc]))
of def: of fork:
var alreadySeenThisNode = false var copy = State()
for s in alreadySeen: copy[] = states[pc][]
if cfg[pc].n.aliases(s) != no or s.aliases(cfg[pc].n) != no: mergeStates(states[pc + cfg[pc].dest], copy)
alreadySeenThisNode = true; break mergeStates(states[pc + 1], move(states[pc]))
if alreadySeenThisNode: cfg[pc].n.flags.excl nfFirstWrite
else: cfg[pc].n.flags.incl nfFirstWrite
alreadySeen.incl cfg[pc].n let lastReads = (states[^1].lastReads + states[^1].potentialLastReads) - states[^1].notLastReads
var lastReadTable: Table[PNode, seq[int]]
inc pc for position, node in cfg:
of use: if node.kind == use:
alreadySeen.incl cfg[pc].n lastReadTable.mgetOrPut(node.n, @[]).add position
for node, positions in lastReadTable:
inc pc block checkIfAllPosLastRead:
of goto: for p in positions:
pc += cfg[pc].dest if p notin lastReads: break checkIfAllPosLastRead
of fork: node.flags.incl nfLastRead
var variantA = pc + 1
var variantB = pc + cfg[pc].dest
var alreadySeenA, alreadySeenB = alreadySeen
while variantA != variantB and max(variantA, variantB) < cfg.len and min(variantA, variantB) < until:
if variantA < variantB:
collectFirstWrites(cfg, alreadySeenA, variantA, min(variantB, until))
else:
collectFirstWrites(cfg, alreadySeenB, variantB, min(variantA, until))
alreadySeen.incl alreadySeenA + alreadySeenB
pc = min(variantA, variantB)
proc isLastRead(n: PNode; c: var Con): bool = proc isLastRead(n: PNode; c: var Con): bool =
let m = dfa.skipConvDfa(n) let m = dfa.skipConvDfa(n)
@ -1096,6 +1103,8 @@ proc injectDefaultCalls(n: PNode, c: var Con) =
injectDefaultCalls(n[i], c) injectDefaultCalls(n[i], c)
proc injectDestructorCalls*(g: ModuleGraph; idgen: IdGenerator; owner: PSym; n: PNode): PNode = proc injectDestructorCalls*(g: ModuleGraph; idgen: IdGenerator; owner: PSym; n: PNode): PNode =
when toDebug.len > 0:
shouldDebug = toDebug == owner.name.s or toDebug == "always"
if sfGeneratedOp in owner.flags or (owner.kind == skIterator and isInlineIterator(owner.typ)): if sfGeneratedOp in owner.flags or (owner.kind == skIterator and isInlineIterator(owner.typ)):
return n return n
var c = Con(owner: owner, graph: g, g: constructCfg(owner, n), idgen: idgen) var c = Con(owner: owner, graph: g, g: constructCfg(owner, n), idgen: idgen)
@ -1107,26 +1116,7 @@ proc injectDestructorCalls*(g: ModuleGraph; idgen: IdGenerator; owner: PSym; n:
if optCursorInference in g.config.options: if optCursorInference in g.config.options:
computeCursors(owner, n, g) computeCursors(owner, n, g)
block: computeLastReadsAndFirstWrites(c.g)
var cache = initTable[(PNode, PNode), AliasKind]()
var lastReads, potLastReads: IntSet
var pc = 0
collectLastReads(c.g, cache, lastReads, potLastReads, pc, c.g.len)
lastReads.incl potLastReads
var lastReadTable: Table[PNode, seq[int]]
for position, node in c.g:
if node.kind == use:
lastReadTable.mgetOrPut(node.n, @[]).add position
for node, positions in lastReadTable:
var allPositionsLastRead = true
for p in positions:
if p notin lastReads: allPositionsLastRead = false; break
if allPositionsLastRead:
node.flags.incl nfLastRead
var alreadySeen: HashSet[PNode]
pc = 0
collectFirstWrites(c.g, alreadySeen, pc, c.g.len)
var scope: Scope var scope: Scope
let body = p(n, c, scope, normal) let body = p(n, c, scope, normal)

View file

@ -784,3 +784,37 @@ proc main3 =
main3() main3()
# misc
proc smoltest(x: bool): bool =
while true:
if true: return x
discard smoltest(true)
# bug #18002
type
TTypeAttachedOp = enum
attachedAsgn
attachedSink
attachedTrace
PNode = ref object
discard
proc genAddrOf(n: PNode) =
assert n != nil, "moved?!"
proc atomicClosureOp =
let x = PNode()
genAddrOf:
block:
x
case attachedTrace
of attachedSink: discard
of attachedAsgn: discard
of attachedTrace: genAddrOf(x)
atomicClosureOp()