Commit e8d1cca4 authored by Vitja Makarov's avatar Vitja Makarov

Add control flow stuff from vitek/unitialized

parent 4dbd1e03
import cython
cython.declare(PyrexTypes=object, Naming=object, ExprNodes=object, Nodes=object,
Options=object, UtilNodes=object, ModuleNode=object,
LetNode=object, LetRefNode=object, TreeFragment=object,
TemplateTransform=object, EncodedString=object,
error=object, warning=object, copy=object)
import Builtin
import ExprNodes
import Nodes
from PyrexTypes import py_object_type, unspecified_type
from Visitor import TreeVisitor, CythonTransform
from Errors import error, warning, CompileError, InternalError
from cython import set
class TypedExprNode(ExprNodes.ExprNode):
# Used for declaring assignments of a specified type whithout a known entry.
def __init__(self, type):
self.type = type
object_expr = TypedExprNode(py_object_type)
class ControlBlock(object):
"""Control flow graph node. Sequence of assignments and name references.
children set of children nodes
parents set of parent nodes
positions set of position markers
stats list of block statements
gen dict of assignments generated by this block
bounded set of entries that are definitely bounded in this block
Example:
a = 1
b = a + c # 'c' is already bounded or exception here
stats = [Assignment(a), NameReference(a), NameReference(c),
Assignment(b)]
gen = {Entry(a): Assignment(a), Entry(b): Assignment(b)}
bounded = set([Entry(a), Entry(c)])
"""
def __init__(self):
self.children = set()
self.parents = set()
self.positions = set()
self.stats = []
self.gen = {}
self.bounded = set()
def empty(self):
return (not self.stats and not self.positions)
def detach(self):
"""Detach block from parents and children."""
for child in self.children:
child.parents.remove(self)
for parent in self.parents:
parent.children.remove(self)
self.parents.clear()
self.children.clear()
def add_child(self, block):
self.children.add(block)
block.parents.add(self)
class ExitBlock(ControlBlock):
"""Non-empty exit point block."""
def empty(self):
return False
class ControlFlow(object):
"""Control-flow graph.
entry_point ControlBlock entry point for this graph
exit_point ControlBlock normal exit point
block ControlBlock current block
blocks set children nodes
entries set tracked entries
loops list stack for loop descriptors
exceptions list stack for exception descriptors
"""
def __init__(self):
self.blocks = set()
self.entries = set()
self.loops = []
self.exceptions = []
self.entry_point = ControlBlock()
self.exit_point = ExitBlock()
self.blocks.add(self.exit_point)
self.block = self.entry_point
def newblock(self, parent=None):
"""Create floating block linked to `parent` if given.
NOTE: Block is NOT added to self.blocks
"""
block = ControlBlock()
self.blocks.add(block)
if parent:
parent.add_child(block)
return block
def nextblock(self, parent=None):
"""Create block children block linked to current or `parent` if given.
NOTE: Block is added to self.blocks
"""
block = ControlBlock()
self.blocks.add(block)
if parent:
parent.add_child(block)
elif self.block:
self.block.add_child(block)
self.block = block
return self.block
def is_tracked(self, entry):
if entry.is_anonymous:
return False
return entry.is_local or entry.is_pyclass_attr or entry.is_arg
def mark_position(self, node):
"""Mark position, will be used to draw graph nodes."""
if self.block:
self.block.positions.add(node.pos[:2])
def mark_assignment(self, lhs, rhs, entry=None):
if self.block:
if entry is None:
entry = lhs.entry
if not self.is_tracked(entry):
return
assignment = NameAssignment(lhs, rhs, entry)
self.block.stats.append(assignment)
self.block.gen[entry] = assignment
self.entries.add(entry)
def mark_argument(self, lhs, rhs, entry):
if self.block and self.is_tracked(entry):
assignment = Argument(lhs, rhs, entry)
self.block.stats.append(assignment)
self.block.gen[entry] = assignment
self.entries.add(entry)
def mark_deletion(self, node, entry):
if self.block and self.is_tracked(entry):
assignment = NameAssignment(node, None, entry)
self.block.stats.append(assignment)
self.block.gen[entry] = Uninitialized
self.entries.add(entry)
def mark_reference(self, node, entry):
if self.block and self.is_tracked(entry):
self.block.stats.append(NameReference(node, entry))
# Local variable is definitely bound after this reference
self.block.bounded.add(entry)
self.entries.add(entry)
def normalize(self):
"""Delete unreachable and orphan blocks."""
queue = set([self.entry_point])
visited = set()
while queue:
root = queue.pop()
visited.add(root)
for child in root.children:
if child not in visited:
queue.add(child)
unreachable = self.blocks - visited
for block in unreachable:
block.detach()
visited.remove(self.entry_point)
for block in visited:
if block.empty():
for parent in block.parents: # Re-parent
for child in block.children:
parent.add_child(child)
block.detach()
unreachable.add(block)
self.blocks -= unreachable
class LoopDescr(object):
def __init__(self, next_block, loop_block):
self.next_block = next_block
self.loop_block = loop_block
class ExceptionDescr(object):
"""Exception handling helper.
entry_point ControlBlock Exception handling entry point
finally_enter ControlBlock Normal finally clause entry point
finally_exit ControlBlock Normal finally clause exit point
"""
def __init__(self, entry_point, finally_enter=None, finally_exit=None):
self.entry_point = entry_point
self.finally_enter = finally_enter
self.finally_exit = finally_exit
class NameAssignment(object):
is_arg = False
def __init__(self, lhs, rhs, entry):
self.lhs = lhs
self.rhs = rhs
self.entry = entry
self.pos = lhs.pos
self.refs = set()
def __repr__(self):
return '%s(entry=%r)' % (self.__class__.__name__, self.entry)
class Argument(NameAssignment):
is_arg = True
class Uninitialized(object):
pass
class NameReference(object):
def __init__(self, node, entry):
self.node = node
self.entry = entry
self.pos = node.pos
def __repr__(self):
return '%s(entry=%r)' % (self.__class__.__name__, self.entry)
class GVContext(object):
"""Graphviz subgraph object."""
def __init__(self):
self.blockids = {}
self.nextid = 0
self.children = []
self.sources = {}
def add(self, child):
self.children.append(child)
def nodeid(self, block):
if block not in self.blockids:
self.blockids[block] = 'block%d' % self.nextid
self.nextid += 1
return self.blockids[block]
def extract_sources(self, block):
if not block.positions:
return ''
start = min(block.positions)
stop = max(block.positions)
srcdescr = start[0]
if not srcdescr in self.sources:
self.sources[srcdescr] = list(srcdescr.get_lines())
lines = self.sources[srcdescr]
return '\\n'.join([l.strip() for l in lines[start[1] - 1:stop[1]]])
def render(self, fp, name, annotate_defs=False):
"""Render graphviz dot graph"""
fp.write('digraph %s {\n' % name)
fp.write(' node [shape=box];\n')
for child in self.children:
child.render(fp, self, annotate_defs)
fp.write('}\n')
def escape(self, text):
return text.replace('"', '\\"').replace('\n', '\\n')
class GV(object):
"""Graphviz DOT renderer."""
def __init__(self, name, flow):
self.name = name
self.flow = flow
def render(self, fp, ctx, annotate_defs=False):
fp.write(' subgraph %s {\n' % self.name)
for block in self.flow.blocks:
label = ctx.extract_sources(block)
if annotate_defs:
for stat in block.stats:
if isinstance(stat, NameAssignment):
label += '\n %s [definition]' % stat.entry.name
elif isinstance(stat, NameReference):
if stat.entry:
label += '\n %s [reference]' % stat.entry.name
if not label:
label = 'empty'
pid = ctx.nodeid(block)
fp.write(' %s [label="%s"];\n' % (pid, ctx.escape(label)))
for block in self.flow.blocks:
pid = ctx.nodeid(block)
for child in block.children:
fp.write(' %s -> %s;\n' % (pid, ctx.nodeid(child)))
fp.write(' }\n')
class MessageCollection(list):
"""Collect error/warnings messages first then sort"""
def error(self, pos, message):
self.append((pos, True, message))
def warning(self, pos, message):
self.append((pos, False, message))
def _key(self, item):
return item[0]
def sort(self):
list.sort(self, key=self._key)
def check_definitions(flow, compiler_directives):
"""Based on algo 9.11 from Dragon Book."""
# Initialize
for block in flow.blocks:
block.input = {}
block.output = {}
for entry, item in block.gen.items():
block.output[entry] = set([item])
entry_point = flow.entry_point
entry_point.input = {}
entry_point.output = {}
for entry in flow.entries:
entry_point.gen[entry] = Uninitialized
entry_point.output[entry] = set([Uninitialized])
# Per-block reaching definitons
dirty = True
while dirty:
dirty = False
for block in flow.blocks:
input = {}
for parent in block.parents:
for entry, items in parent.output.iteritems():
if entry in input:
input[entry].update(items)
else:
input[entry] = set(items)
output = {}
for entry, items in input.iteritems():
if entry in block.gen:
continue
output[entry] = set(items)
if entry in block.bounded:
output[entry].discard(Uninitialized)
for entry, item in block.gen.iteritems():
output[entry] = set([item])
if not dirty:
if output != block.output:
dirty = True
block.input = input
block.output = output
# Track down state
messages = MessageCollection()
assignments = set()
for block in flow.blocks:
state = {}
for entry, items in block.input.iteritems():
state[entry] = items.copy()
for stat in block.stats:
if isinstance(stat, NameAssignment):
if stat.rhs:
state[stat.entry] = set([stat])
else:
state[stat.entry] = set([Uninitialized])
assignments.add(stat)
stat.entry._assignments.append(stat)
elif isinstance(stat, NameReference):
stat.entry.references.append(stat)
if Uninitialized in state[stat.entry]:
if stat.entry.from_closure:
pass # Can be uninitialized here
elif len(state[stat.entry]) == 1:
messages.error(stat.pos, "local variable '%s' referenced before assignment" % stat.entry.name)
else:
if compiler_directives['warn.maybe_uninitialized']:
messages.warning(stat.pos, "local variable '%s' might be referenced before assignment" % stat.entry.name)
state[stat.entry] -= set([Uninitialized])
for assmt in state[stat.entry]:
assmt.refs.add(stat)
# Check variable usage
warn_unused_result = compiler_directives['warn.unused_result']
warn_unused = compiler_directives['warn.unused']
warn_unused_arg = compiler_directives['warn.unused_arg']
for assmt in assignments:
if not assmt.refs and not assmt.entry.is_pyclass_attr \
and not assmt.entry.in_closure:
if assmt.entry.references and warn_unused_result:
if assmt.is_arg:
messages.warning(assmt.pos, "Unused argument value '%s'" % assmt.entry.name)
else:
messages.warning(assmt.pos, "Unused result in '%s'" % assmt.entry.name)
assmt.lhs.used = False
for entry in flow.entries:
if not entry.references and not entry.is_pyclass_attr and not entry.in_closure:
# TODO: handle unused buffers
if entry.type.is_buffer:
entry.used = True
continue
# TODO: starred args entries are not marked with is_arg flag
for assmt in entry._assignments:
if assmt.is_arg:
is_arg = True
break
else:
is_arg = False
if is_arg:
if warn_unused_arg:
messages.warning(entry.pos, "Unused argument '%s'" % entry.name)
# TODO: handle unused arguments
entry.used = True
else:
if warn_unused:
messages.warning(entry.pos, "Unused entry '%s'" % entry.name)
entry.used = False
# Sort warnings by position
messages.sort()
for pos, is_error, message in messages:
if is_error:
error(pos, message)
else:
warning(pos, message, 2)
class AssignmentCollector(TreeVisitor):
def __init__(self):
super(AssignmentCollector, self).__init__()
self.assignments = []
def visit_Node(self):
self.visitchildren(self)
def visit_SingleAssignmentNode(self, node):
self.assignments.append((node.lhs, node.rhs))
def visit_CascadedAssignmentNode(self, node):
for lhs in node.lhs_list:
self.assignments.append((lhs, node.rhs))
class CreateControlFlowGraph(CythonTransform):
"""Create NameNode use and assignment graph."""
def visit_ModuleNode(self, node):
self.gv_ctx = GVContext()
self.env_stack = []
self.env = node.scope
self.stack = []
self.flow = ControlFlow()
self.visitchildren(node)
dot_output = self.current_directives['control_flow.dot_output']
if dot_output:
annotate_defs = self.current_directives['control_flow.dot_annotate_defs']
fp = open(dot_output, 'wt')
try:
self.gv_ctx.render(fp, 'module', annotate_defs=annotate_defs)
finally:
fp.close()
return node
def visit_FuncDefNode(self, node):
self.env_stack.append(self.env)
self.env = node.local_scope
self.stack.append(self.flow)
self.flow = ControlFlow()
self.mark_position(node)
# Function body block
self.flow.nextblock()
if node.star_arg:
self.flow.mark_argument(node.star_arg,
TypedExprNode(Builtin.tuple_type),
node.star_arg.entry)
if node.starstar_arg:
self.flow.mark_argument(node.starstar_arg,
TypedExprNode(Builtin.dict_type),
node.starstar_arg.entry)
self.visitchildren(node)
# Exit point
if self.flow.block:
self.flow.block.add_child(self.flow.exit_point)
# Cleanup graph
self.flow.normalize()
check_definitions(self.flow, self.current_directives)
self.flow.blocks.add(self.flow.entry_point)
self.gv_ctx.add(GV(node.local_scope.name, self.flow))
self.flow = self.stack.pop()
self.env = self.env_stack.pop()
return node
def visit_DefNode(self, node):
## XXX: no target name node here
node.used = True
self.flow.mark_assignment(node, object_expr, self.env.lookup(node.name))
return self.visit_FuncDefNode(node)
def visit_CTypeDefNode(self, node):
return node
def mark_assignment(self, lhs, rhs=None):
if not self.flow.block:
return
if self.flow.exceptions:
exc_descr = self.flow.exceptions[-1]
self.flow.block.add_child(exc_descr.entry_point)
self.flow.nextblock()
if isinstance(lhs, (ExprNodes.AttributeNode, ExprNodes.IndexNode)):
self.visit(lhs)
return
if not rhs:
rhs = object_expr
if lhs.is_name:
if lhs.entry is None:
# TODO: This shouldn't happen...
return
self.flow.mark_assignment(lhs, rhs)
elif isinstance(lhs, ExprNodes.SequenceNode):
for arg in lhs.args:
self.mark_assignment(arg)
else:
# Could use this info to infer cdef class attributes...
pass
if self.flow.exceptions:
exc_descr = self.flow.exceptions[-1]
self.flow.block.add_child(exc_descr.entry_point)
self.flow.nextblock()
def mark_position(self, node):
"""Mark position if DOT output is enabled."""
if self.current_directives['control_flow.dot_output']:
self.flow.mark_position(node)
def visit_FromImportStatNode(self, node):
for name, target in node.items:
if name != "*":
self.mark_assignment(target)
self.visitchildren(node)
return node
def visit_SingleAssignmentNode(self, node):
self.visit(node.rhs)
self.mark_assignment(node.lhs)
return node
def visit_CascadedAssignmentNode(self, node):
self.visit(node.rhs)
for lhs in node.lhs_list:
self.mark_assignment(lhs, node.rhs)
return node
def visit_ParallelAssignmentNode(self, node):
collector = AssignmentCollector()
collector.visitchildren(node)
for lhs, rhs in collector.assignments:
self.visit(rhs)
for lhs, rhs in collector.assignments:
self.mark_assignment(lhs, rhs)
return node
def visit_InPlaceAssignmentNode(self, node):
self.visitchildren(node)
self.mark_assignment(node.lhs, node.create_binop_node())
return node
def _delete_name_node(self, node):
entry = node.entry or self.env.lookup(node.name)
if entry.in_closure or entry.from_closure:
error(node.pos, "can not delete variable '%s' referenced in nested scope" % entry.name)
# Mark reference
self.visit(node)
self.flow.mark_deletion(node, entry)
def visit_DelStatNode(self, node):
for arg in node.args:
if arg.is_name:
self._delete_name_node(arg)
elif arg.is_sequence_constructor:
self.visit_DelStatNode(arg)
else:
self.visit(arg)
return node
def visit_CArgDeclNode(self, node):
entry = self.env.lookup(node.name)
self.flow.mark_argument(node, TypedExprNode(entry.type), entry)
return node
def visit_NameNode(self, node):
if self.flow.block:
entry = node.entry or self.env.lookup(node.name)
if entry:
self.flow.mark_reference(node, entry)
return node
def visit_StatListNode(self, node):
for stat in node.stats:
if not self.flow.block:
break
self.visit(stat)
return node
def visit_Node(self, node):
self.visitchildren(node)
self.mark_position(node)
return node
def visit_IfStatNode(self, node):
next_block = self.flow.newblock()
parent = self.flow.block
# If clauses
for clause in node.if_clauses:
parent = self.flow.nextblock(parent)
self.visit(clause.condition)
self.flow.nextblock()
self.visit(clause.body)
if self.flow.block:
self.flow.block.add_child(next_block)
# Else clause
if node.else_clause:
self.flow.nextblock(parent=parent)
self.visit(node.else_clause)
if self.flow.block:
self.flow.block.add_child(next_block)
else:
parent.add_child(next_block)
if next_block.parents:
self.flow.block = next_block
else:
self.flow.block = None
return node
def visit_WhileStatNode(self, node):
condition_block = self.flow.nextblock()
next_block = self.flow.newblock()
# Condition block
self.flow.loops.append(LoopDescr(next_block, condition_block))
self.visit(node.condition)
# Body block
self.flow.nextblock()
self.visit(node.body)
# Loop it
if self.flow.block:
self.flow.block.add_child(condition_block)
self.flow.block.add_child(next_block)
# Else clause
if node.else_clause:
self.flow.nextblock(parent=condition_block)
self.visit(node.else_clause)
if self.flow.block:
self.flow.block.add_child(next_block)
else:
condition_block.add_child(next_block)
self.flow.loops.pop()
self.flow.block = next_block
return node
def visit_ForInStatNode(self, node):
condition_block = self.flow.nextblock()
next_block = self.flow.newblock()
# Condition with iterator
self.flow.loops.append(LoopDescr(next_block, condition_block))
self.visit(node.iterator)
# Target assignment
self.flow.nextblock()
self.mark_assignment(node.target)
# Body block
self.flow.nextblock()
self.visit(node.body)
# Loop it
if self.flow.block:
self.flow.block.add_child(condition_block)
# Else clause
if node.else_clause:
self.flow.nextblock(parent=condition_block)
self.visit(node.else_clause)
if self.flow.block:
self.flow.block.add_child(next_block)
else:
condition_block.add_child(next_block)
self.flow.loops.pop()
self.flow.block = next_block
return node
def visit_ForFromStatNode(self, node):
condition_block = self.flow.nextblock()
next_block = self.flow.newblock()
# Condition with iterator
self.flow.loops.append(LoopDescr(next_block, condition_block))
self.visit(node.bound1)
self.visit(node.bound2)
if node.step:
self.visit(node.step)
# Target assignment
self.flow.nextblock()
self.mark_assignment(node.target)
# TODO: force target use, should ForFromStatNode should allocate temp var instead
self.visit(node.target)
# Body block
self.flow.nextblock()
self.visit(node.body)
# Loop it
if self.flow.block:
self.flow.block.add_child(condition_block)
# Else clause
if node.else_clause:
self.flow.nextblock(parent=condition_block)
self.visit(node.else_clause)
if self.flow.block:
self.flow.block.add_child(next_block)
else:
condition_block.add_child(next_block)
self.flow.loops.pop()
self.flow.block = next_block
return node
def visit_LoopNode(self, node):
raise InternalError, "Generic loops are not supported"
def visit_WithStatNode(self, node):
# never be here: WithStatNode is replaced with try except finally
raise InternalError, "with statement is not supported"
def visit_TryExceptStatNode(self, node):
# After exception handling
next_block = self.flow.newblock()
# Body block
self.flow.newblock()
# Exception entry point
entry_point = self.flow.newblock()
self.flow.exceptions.append(ExceptionDescr(entry_point))
self.flow.nextblock()
## XXX: links to exception handling point should be added by
## XXX: children nodes
self.flow.block.add_child(entry_point)
self.visit(node.body)
self.flow.exceptions.pop()
# After exception
if self.flow.block:
if node.else_clause:
self.flow.nextblock()
self.visit(node.else_clause)
if self.flow.block:
self.flow.block.add_child(next_block)
for clause in node.except_clauses:
self.flow.block = entry_point
if clause.pattern:
for pattern in clause.pattern:
self.visit(pattern)
else:
# TODO: handle * pattern
pass
if clause.target:
self.mark_assignment(clause.target)
entry_point = self.flow.newblock(parent=self.flow.block)
self.flow.nextblock()
self.visit(clause.body)
if self.flow.block:
self.flow.block.add_child(next_block)
if self.flow.exceptions:
entry_point.add_child(self.flow.exceptions[-1].entry_point)
if next_block.parents:
self.flow.block = next_block
else:
self.flow.block = None
return node
def visit_TryFinallyStatNode(self, node):
body_block = self.flow.nextblock()
# Exception entry point
entry_point = self.flow.newblock()
self.flow.block = entry_point
self.visit(node.finally_clause)
# Normal execution
finally_enter = self.flow.newblock()
self.flow.block = finally_enter
self.visit(node.finally_clause)
finally_exit = self.flow.block
self.flow.exceptions.append(ExceptionDescr(entry_point, finally_enter, finally_exit))
self.flow.block = body_block
## XXX: Is it still required
body_block.add_child(entry_point)
self.visit(node.body)
self.flow.exceptions.pop()
if self.flow.block:
self.flow.block.add_child(finally_enter)
if finally_exit:
self.flow.block = self.flow.nextblock(parent=finally_exit)
else:
self.flow.block = None
return node
def visit_RaiseStatNode(self, node):
self.mark_position(node)
if self.flow.exceptions:
self.flow.block.add_child(self.flow.exceptions[-1].entry_point)
self.flow.block = None
return node
def visit_ReraiseStatNode(self, node):
self.mark_position(node)
if self.flow.exceptions:
self.flow.block.add_child(self.flow.exceptions[-1].entry_point)
self.flow.block = None
return node
def visit_ReturnStatNode(self, node):
self.mark_position(node)
self.visitchildren(node)
for exception in self.flow.exceptions[::-1]:
if exception.finally_enter:
self.flow.block.add_child(exception.finally_enter)
if exception.finally_exit:
exception.finally_exit.add_child(self.flow.exit_point)
break
else:
if self.flow.block:
self.flow.block.add_child(self.flow.exit_point)
self.flow.block = None
return node
def visit_BreakStatNode(self, node):
if not self.flow.loops:
#error(node.pos, "break statement not inside loop")
return node
loop = self.flow.loops[-1]
self.mark_position(node)
for exception in self.flow.exceptions[::-1]:
if exception.finally_enter:
self.flow.block.add_child(exception.finally_enter)
if exception.finally_exit:
exception.finally_exit.add_child(loop.next_block)
break
else:
self.flow.block.add_child(loop.next_block)
self.flow.block = None
return node
def visit_ContinueStatNode(self, node):
if not self.flow.loops:
#error(node.pos, "continue statement not inside loop")
return node
loop = self.flow.loops[-1]
self.mark_position(node)
for exception in self.flow.exceptions[::-1]:
if exception.finally_enter:
self.flow.block.add_child(exception.finally_enter)
if exception.finally_exit:
exception.finally_exit.add_child(loop.loop_block)
break
else:
self.flow.block.add_child(loop.loop_block)
self.flow.block = None
return node
def visit_ComprehensionNode(self, node):
if node.expr_scope:
self.env_stack.append(self.env)
self.env = node.expr_scope
# Skip append node here
self.visit(node.target)
self.visit(node.loop)
if node.expr_scope:
self.env = self.env_stack.pop()
return node
def visit_ScopedExprNode(self, node):
if node.expr_scope:
self.env_stack.append(self.env)
self.env = node.expr_scope
self.visitchildren(node)
if node.expr_scope:
self.env = self.env_stack.pop()
return node
def visit_PyClassDefNode(self, node):
self.flow.mark_assignment(node.target,
object_expr, self.env.lookup(node.name))
# TODO: add negative attribute list to "visitchildren"?
self.visitchildren(node, attrs=['dict', 'metaclass', 'mkw', 'bases', 'classobj'])
self.env_stack.append(self.env)
self.env = node.scope
self.flow.nextblock()
self.visitchildren(node, attrs=['body'])
self.flow.nextblock()
self.env = self.env_stack.pop()
return node
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment