Files
Nim/compiler/dfa.nim
2023-05-08 16:25:47 +02:00

502 lines
13 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
#
# The Nim Compiler
# (c) Copyright 2017 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Data flow analysis for Nim.
## We transform the AST into a linear list of instructions first to
## make this easier to handle: There are only 3 different branching
## instructions: 'goto X' is an unconditional goto, 'fork X'
## is a conditional goto (either the next instruction or 'X' can be
## taken), 'loop X' is the only jump that jumps back.
##
## Exhaustive case statements are translated
## so that the last branch is transformed into an 'else' branch.
## ``return`` and ``break`` are all covered by 'goto'.
##
## The data structures and algorithms used here are inspired by
## "A GraphFree Approach to DataFlow Analysis" by Markus Mohnen.
## https://link.springer.com/content/pdf/10.1007/3-540-45937-5_6.pdf
import ast, intsets, lineinfos, renderer, aliasanalysis
import std/private/asciitables
when defined(nimPreviewSlimSystem):
import std/assertions
type
InstrKind* = enum
goto, loop, fork, def, use
Instr* = object
case kind*: InstrKind
of goto, fork, loop: dest*: int
of def, use:
n*: PNode # contains the def/use location.
ControlFlowGraph* = seq[Instr]
TPosition = distinct int
TBlock = object
case isTryBlock: bool
of false:
label: PSym
breakFixups: seq[(TPosition, seq[PNode])] #Contains the gotos for the breaks along with their pending finales
of true:
finale: PNode
raiseFixups: seq[TPosition] #Contains the gotos for the raises
Con = object
code: ControlFlowGraph
inTryStmt, interestingInstructions: int
blocks: seq[TBlock]
owner: PSym
root: PSym
proc codeListing(c: ControlFlowGraph, start = 0; last = -1): string =
# for debugging purposes
# first iteration: compute all necessary labels:
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, loop}:
jumpTargets.incl(i+c[i].dest)
var i = start
while i <= last:
if i in jumpTargets: result.add("L" & $i & ":\n")
result.add "\t"
result.add ($i & " " & $c[i].kind)
result.add "\t"
case c[i].kind
of def, use:
result.add renderTree(c[i].n)
result.add("\t#")
result.add($c[i].n.info.line)
result.add("\n")
of goto, fork, loop:
result.add "L"
result.addInt c[i].dest+i
inc i
if i in jumpTargets: result.add("L" & $i & ": End\n")
proc echoCfg*(c: ControlFlowGraph; start = 0; last = -1) {.deprecated.} =
## echos the ControlFlowGraph for debugging purposes.
echo codeListing(c, start, last).alignTable
proc forkI(c: var Con): TPosition =
result = TPosition(c.code.len)
c.code.add Instr(kind: fork, dest: 0)
proc gotoI(c: var Con): TPosition =
result = TPosition(c.code.len)
c.code.add Instr(kind: goto, dest: 0)
proc genLabel(c: Con): TPosition = TPosition(c.code.len)
template checkedDistance(dist): int =
doAssert low(int) div 2 + 1 < dist and dist < high(int) div 2
dist
proc jmpBack(c: var Con, p = TPosition(0)) =
c.code.add Instr(kind: loop, dest: checkedDistance(p.int - c.code.len))
proc patch(c: var Con, p: TPosition) =
# patch with current index
c.code[p.int].dest = checkedDistance(c.code.len - p.int)
proc gen(c: var Con; n: PNode)
proc popBlock(c: var Con; oldLen: int) =
var exits: seq[TPosition]
exits.add c.gotoI()
for f in c.blocks[oldLen].breakFixups:
c.patch(f[0])
for finale in f[1]:
c.gen(finale)
exits.add c.gotoI()
for e in exits:
c.patch e
c.blocks.setLen(oldLen)
template withBlock(labl: PSym; body: untyped) =
let oldLen = c.blocks.len
c.blocks.add TBlock(isTryBlock: false, label: labl)
body
popBlock(c, oldLen)
proc isTrue(n: PNode): bool =
n.kind == nkSym and n.sym.kind == skEnumField and n.sym.position != 0 or
n.kind == nkIntLit and n.intVal != 0
template forkT(body) =
let lab1 = c.forkI()
body
c.patch(lab1)
proc genWhile(c: var Con; n: PNode) =
# lab1:
# cond, tmp
# fork tmp, lab2
# body
# jmp lab1
# lab2:
let lab1 = c.genLabel
withBlock(nil):
if isTrue(n[0]):
c.gen(n[1])
c.jmpBack(lab1)
else:
c.gen(n[0])
forkT:
c.gen(n[1])
c.jmpBack(lab1)
proc genIf(c: var Con, n: PNode) =
#[
if cond:
A
elif condB:
B
elif condC:
C
else:
D
cond
fork lab1
A
goto Lend
lab1:
condB
fork lab2
B
goto Lend2
lab2:
condC
fork L3
C
goto Lend3
L3:
D
goto Lend3 # not eliminated to simplify the join generation
Lend3:
join F3
Lend2:
join F2
Lend:
join F1
]#
var endings: seq[TPosition] = @[]
let oldInteresting = c.interestingInstructions
let oldLen = c.code.len
for i in 0..<n.len:
let it = n[i]
c.gen(it[0])
if it.len == 2:
forkT:
c.gen(it.lastSon)
endings.add c.gotoI()
if oldInteresting == c.interestingInstructions:
setLen c.code, oldLen
else:
for i in countdown(endings.high, 0):
c.patch(endings[i])
proc genAndOr(c: var Con; n: PNode) =
# asgn dest, a
# fork lab1
# asgn dest, b
# lab1:
# join F1
c.gen(n[1])
forkT:
c.gen(n[2])
proc genCase(c: var Con; n: PNode) =
# if (!expr1) goto lab1;
# thenPart
# goto LEnd
# lab1:
# if (!expr2) goto lab2;
# thenPart2
# goto LEnd
# lab2:
# elsePart
# Lend:
let isExhaustive = skipTypes(n[0].typ,
abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString, tyCstring}
var endings: seq[TPosition] = @[]
c.gen(n[0])
let oldInteresting = c.interestingInstructions
let oldLen = c.code.len
for i in 1..<n.len:
let it = n[i]
if it.len == 1 or (i == n.len-1 and isExhaustive):
# treat the last branch as 'else' if this is an exhaustive case statement.
c.gen(it.lastSon)
else:
forkT:
c.gen(it.lastSon)
endings.add c.gotoI()
if oldInteresting == c.interestingInstructions:
setLen c.code, oldLen
else:
for i in countdown(endings.high, 0):
c.patch(endings[i])
proc genBlock(c: var Con; n: PNode) =
withBlock(n[0].sym):
c.gen(n[1])
proc genBreakOrRaiseAux(c: var Con, i: int, n: PNode) =
let lab1 = c.gotoI()
if c.blocks[i].isTryBlock:
c.blocks[i].raiseFixups.add lab1
else:
var trailingFinales: seq[PNode]
if c.inTryStmt > 0: #Ok, we are in a try, lets see which (if any) try's we break out from:
for b in countdown(c.blocks.high, i):
if c.blocks[b].isTryBlock:
trailingFinales.add c.blocks[b].finale
c.blocks[i].breakFixups.add (lab1, trailingFinales)
proc genBreak(c: var Con; n: PNode) =
inc c.interestingInstructions
if n[0].kind == nkSym:
for i in countdown(c.blocks.high, 0):
if not c.blocks[i].isTryBlock and c.blocks[i].label == n[0].sym:
genBreakOrRaiseAux(c, i, n)
return
#globalError(n.info, "VM problem: cannot find 'break' target")
else:
for i in countdown(c.blocks.high, 0):
if not c.blocks[i].isTryBlock:
genBreakOrRaiseAux(c, i, n)
return
proc genTry(c: var Con; n: PNode) =
var endings: seq[TPosition] = @[]
let oldLen = c.blocks.len
c.blocks.add TBlock(isTryBlock: true, finale: if n[^1].kind == nkFinally: n[^1] else: newNode(nkEmpty))
inc c.inTryStmt
c.gen(n[0])
dec c.inTryStmt
for f in c.blocks[oldLen].raiseFixups:
c.patch(f)
c.blocks.setLen oldLen
for i in 1..<n.len:
let it = n[i]
if it.kind != nkFinally:
forkT:
c.gen(it.lastSon)
endings.add c.gotoI()
for i in countdown(endings.high, 0):
c.patch(endings[i])
let fin = lastSon(n)
if fin.kind == nkFinally:
c.gen(fin[0])
template genNoReturn(c: var Con) =
# leave the graph
c.code.add Instr(kind: goto, dest: high(int) - c.code.len)
proc genRaise(c: var Con; n: PNode) =
inc c.interestingInstructions
gen(c, n[0])
if c.inTryStmt > 0:
for i in countdown(c.blocks.high, 0):
if c.blocks[i].isTryBlock:
genBreakOrRaiseAux(c, i, n)
return
assert false #Unreachable
else:
genNoReturn(c)
proc genImplicitReturn(c: var Con) =
if c.owner.kind in {skProc, skFunc, skMethod, skIterator, skConverter} and resultPos < c.owner.ast.len:
gen(c, c.owner.ast[resultPos])
proc genReturn(c: var Con; n: PNode) =
inc c.interestingInstructions
if n[0].kind != nkEmpty:
gen(c, n[0])
else:
genImplicitReturn(c)
genBreakOrRaiseAux(c, 0, n)
const
InterestingSyms = {skVar, skResult, skLet, skParam, skForVar, skTemp}
proc skipTrivials(c: var Con, n: PNode): PNode =
result = n
while true:
case result.kind
of PathKinds0 - {nkBracketExpr}:
result = result[0]
of nkBracketExpr:
gen(c, result[1])
result = result[0]
of PathKinds1:
result = result[1]
else: break
proc genUse(c: var Con; orig: PNode) =
let n = c.skipTrivials(orig)
if n.kind == nkSym:
if n.sym.kind in InterestingSyms and n.sym == c.root:
c.code.add Instr(kind: use, n: orig)
inc c.interestingInstructions
else:
gen(c, n)
proc genDef(c: var Con; orig: PNode) =
let n = c.skipTrivials(orig)
if n.kind == nkSym and n.sym.kind in InterestingSyms:
if n.sym == c.root:
c.code.add Instr(kind: def, n: orig)
inc c.interestingInstructions
proc genCall(c: var Con; n: PNode) =
gen(c, n[0])
var t = n[0].typ
if t != nil: t = t.skipTypes(abstractInst)
for i in 1..<n.len:
gen(c, n[i])
if t != nil and i < t.len and isOutParam(t[i]):
# Pass by 'out' is a 'must def'. Good enough for a move optimizer.
genDef(c, n[i])
# every call can potentially raise:
if false: # c.inTryStmt > 0 and canRaiseConservative(n[0]):
# we generate the instruction sequence:
# fork lab1
# goto exceptionHandler (except or finally)
# lab1:
# join F1
forkT:
for i in countdown(c.blocks.high, 0):
if c.blocks[i].isTryBlock:
genBreakOrRaiseAux(c, i, n)
break
proc genMagic(c: var Con; n: PNode; m: TMagic) =
case m
of mAnd, mOr: c.genAndOr(n)
of mNew, mNewFinalize:
genDef(c, n[1])
for i in 2..<n.len: gen(c, n[i])
else:
genCall(c, n)
proc genVarSection(c: var Con; n: PNode) =
for a in n:
if a.kind == nkCommentStmt:
discard
elif a.kind == nkVarTuple:
gen(c, a.lastSon)
for i in 0..<a.len-2: genDef(c, a[i])
else:
gen(c, a.lastSon)
if a.lastSon.kind != nkEmpty:
genDef(c, a[0])
proc gen(c: var Con; n: PNode) =
case n.kind
of nkSym: genUse(c, n)
of nkCallKinds:
if n[0].kind == nkSym:
let s = n[0].sym
if s.magic != mNone:
genMagic(c, n, s.magic)
else:
genCall(c, n)
if sfNoReturn in n[0].sym.flags:
genNoReturn(c)
else:
genCall(c, n)
of nkCharLit..nkNilLit: discard
of nkAsgn, nkFastAsgn, nkSinkAsgn:
gen(c, n[1])
if n[0].kind in PathKinds0:
let a = c.skipTrivials(n[0])
if a.kind in nkCallKinds:
gen(c, a)
# 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])
of PathKinds0 - {nkObjDownConv, nkObjUpConv}:
genUse(c, n)
of nkIfStmt, nkIfExpr: genIf(c, n)
of nkWhenStmt:
# This is "when nimvm" node. Chose the first branch.
gen(c, n[0][1])
of nkCaseStmt: genCase(c, n)
of nkWhileStmt: genWhile(c, n)
of nkBlockExpr, nkBlockStmt: genBlock(c, n)
of nkReturnStmt: genReturn(c, n)
of nkRaiseStmt: genRaise(c, n)
of nkBreakStmt: genBreak(c, n)
of nkTryStmt, nkHiddenTryStmt: genTry(c, n)
of nkStmtList, nkStmtListExpr, nkChckRangeF, nkChckRange64, nkChckRange,
nkBracket, nkCurly, nkPar, nkTupleConstr, nkClosure, nkObjConstr, nkYieldStmt:
for x in n: gen(c, x)
of nkPragmaBlock: gen(c, n.lastSon)
of nkDiscardStmt, nkObjDownConv, nkObjUpConv, nkStringToCString, nkCStringToString:
gen(c, n[0])
of nkConv, nkExprColonExpr, nkExprEqExpr, nkCast, PathKinds1:
gen(c, n[1])
of nkVarSection, nkLetSection: genVarSection(c, n)
of nkDefer: doAssert false, "dfa construction pass requires the elimination of 'defer'"
else: discard
when false:
proc optimizeJumps(c: var ControlFlowGraph) =
for i in 0..<c.len:
case c[i].kind
of goto, fork:
var pc = i + c[i].dest
if pc < c.len and c[pc].kind == goto:
while pc < c.len and c[pc].kind == goto:
let newPc = pc + c[pc].dest
if newPc > pc:
pc = newPc
else:
break
c[i].dest = pc - i
of loop, def, use: discard
proc constructCfg*(s: PSym; body: PNode; root: PSym): ControlFlowGraph =
## constructs a control flow graph for ``body``.
var c = Con(code: @[], blocks: @[], owner: s, root: root)
withBlock(s):
gen(c, body)
if root.kind == skResult:
genImplicitReturn(c)
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
result = c.code # will move
else:
shallowCopy(result, c.code)
when false:
optimizeJumps result