Below is my code for building a call-flow graph from a Python abstract syntax tree. I'm not satisfied with it because the algorithm is very complicated. Perhaps much more complicated than it needs to be. So I want suggestions on how to simplify the algorithm and also on how to make the code more readable.
class BasicBlock:
def __init__(self, insns):
self.insns = insns
def type(self):
return type(self.insns[-1]) if self.insns else None
class CFGBuilder:
def __init__(self):
self.succ = defaultdict(list)
self.bbs = []
def build_tree(self, nodes):
buf = []
for node in nodes:
tp = type(node)
if tp in (For, While):
if buf:
yield BasicBlock(buf), []
yield BasicBlock([node]), [self.build_tree(node.body)]
buf = []
elif tp == If:
buf.append(node)
branches = [self.build_tree(node.body),
self.build_tree(node.orelse)]
yield BasicBlock(buf), branches
buf = []
elif tp in (Break, Continue, Pass, Return):
buf.append(node)
yield BasicBlock(buf), []
return
elif tp in (Assign, Expr):
buf.append(node)
else:
assert False
if buf:
yield BasicBlock(buf), []
def connect(self, bb_tree, parent_bb, loop_bb):
tails = []
breaks = []
if parent_bb and bb_tree:
tails = [parent_bb]
for bb, branches in bb_tree:
self.bbs.append(bb)
for tail in tails:
self.succ[tail].append(bb)
tp = bb.type()
if tp == If:
true_tails, true_breaks = \
self.connect(branches[0], bb, loop_bb)
false_tails, false_breaks = \
self.connect(branches[1], bb, loop_bb)
breaks.extend(true_breaks + false_breaks)
if not true_tails and not false_tails:
return [], breaks
tails = true_tails + false_tails
if not branches[1]:
tails.append(bb)
elif tp in (For, While):
tails, loop_breaks = \
self.connect(branches[0], bb, bb)
for tail in tails:
self.succ[tail].append(bb)
tails = [bb] + loop_breaks
elif tp == Break:
if loop_bb:
return [], breaks + [bb]
return [bb], []
elif tp == Continue:
if loop_bb:
self.succ[bb].append(loop_bb)
return [], breaks
return [bb], []
elif tp == Return:
return [], []
elif tp in (Assign, Expr, Pass, None):
tails = [bb]
else:
assert False
return tails, breaks
def build(self, nodes):
# SSA construction requires an entry block.
bb_tree = [(BasicBlock([]), [])]
bb_tree.extend(self.build_tree(nodes))
# If the last block is a block statement, close the cfg with
# an empty block.
if bb_tree[-1][1]:
dummy = BasicBlock([])
bb_tree.append((dummy, []))
self.connect(bb_tree, None, None)
return self.bbs, self.succ
You can run it like this:
PROG = '''
for ba in range(10):
if -(a + 10):
break
for y in range(10):
if not x:
print('cont from x_99')
continue
x = x + 1
if x:
if y:
return x
pass
break
print(x)
print('blah')
while 1:
if x == 0:
while x < 10:
x = x + 1
y = y + 1
if x == 5:
break
if y == 5:
continue
else:
if y == 2:
b = b + 1
continue
a = a + 1
continue
print('prutt')
break
if b:
return
x = 99
if m:
while 2:
if 1:
break
else:
continue
x = 9
print('dead code')
print('this reaches')
print('but ok')
if 2:
break
else:
pass
break
print(10)
'''
root = parse(PROG)
builder = CFGBuilder()
bbs, succ = builder.build(root.body)
It produces this cfg:
Here is the gist for plotting.
ast.Import
. So like... I'll only verify it with your example source and not realistic source, I guess, because realistic source won't work. \$\endgroup\$dot
graphing code? It would be useful to include that. \$\endgroup\$