# cython: auto_pickle=True


import cython
cython.declare(PyrexTypes=object, ExprNodes=object, Nodes=object, Builtin=object,
               Options=object, TreeVisitor=object, CythonTransform=object,
               InternalError=object, error=object, warning=object,
               fake_rhs_expr=object, TypedExprNode=object)

from . import Builtin
from . import ExprNodes
from . import Nodes
from . import Options
from . import PyrexTypes

from .Visitor import TreeVisitor, CythonTransform
from .Errors import error, warning, InternalError


class TypedExprNode(ExprNodes.ExprNode):
    # Used for declaring assignments of a specified type without a known entry.
    def __init__(self, type, may_be_none=None, pos=None):
        super().__init__(pos)
        self.type = type
        self._may_be_none = may_be_none

    def may_be_none(self):
        return self._may_be_none != False

# Fake rhs to silence "unused variable" warning
fake_rhs_expr = TypedExprNode(PyrexTypes.unspecified_type)


class ControlBlock:
    """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 = {Entry(a), Entry(c)}

    """

    def __init__(self):
        self.children = set()
        self.parents = set()
        self.positions = set()

        self.stats = []
        self.gen = {}
        self.bounded = set()

        self.i_input = 0
        self.i_output = 0
        self.i_gen = 0
        self.i_kill = 0
        self.i_state = 0

    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)

    def print(self, level=0, seen=None):
        if seen is None:
            seen = set()
        print(f"{' '*level}{self} {'*' if self in seen else ''}")
        if self in seen:
            return
        for stat in self.stats:
            print(f"{' '*(level+1)}-{stat}")
        seen.add(self)
        for child in self.children:
            child.print(level+1, seen)


class ExitBlock(ControlBlock):
    """Non-empty exit point block."""

    def empty(self):
        return False


class AssignmentList:
    def __init__(self):
        self.stats = []


class ControlFlow:
    """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
       in_try_block  int  track if we're in a try...except or try...finally block
    """

    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
        self.in_try_block = 0

    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 or
                entry.from_closure or entry.in_closure or
                entry.error_on_uninitialized)

    def is_statically_assigned(self, entry):
        if (entry.is_local and entry.is_variable and
                (entry.type.is_struct_or_union or
                 entry.type.is_complex or
                 entry.type.is_array or
                 entry.type.is_cython_lock_type or
                 (entry.type.is_cpp_class and not entry.is_cpp_optional))):
            # stack allocated structured variable => never uninitialised
            return True
        return False

    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, rhs_scope=None):
        if self.block and self.is_tracked(entry):
            assignment = NameAssignment(lhs, rhs, entry, rhs_scope=rhs_scope)
            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 = NameDeletion(node, 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))
            ## XXX: We don't track expression evaluation order so we can't use
            ## XXX: successful reference as initialization sign.
            ## # Local variable is definitely bound after this reference
            ## if not node.allow_null:
            ##     self.block.bounded.add(entry)
            self.entries.add(entry)

    def normalize(self):
        """Delete unreachable and orphan blocks."""
        queue = {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: set = self.blocks - visited
        block: ControlBlock
        for block in unreachable:
            block.detach()

        visited.remove(self.entry_point)

        parent: ControlBlock
        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

    def initialize(self):
        """Set initial state, map assignments to bits."""
        self.assmts = {}
        assmts: AssignmentList
        block: ControlBlock

        bit: int = 1
        for entry in self.entries:
            assmts = AssignmentList()
            assmts.mask = assmts.bit = bit
            self.assmts[entry] = assmts
            bit <<= 1

        for block in self.blocks:
            for stat in block.stats:
                if isinstance(stat, NameAssignment):
                    stat.bit = bit
                    assmts = self.assmts[stat.entry]
                    assmts.stats.append(stat)
                    assmts.mask |= bit
                    bit <<= 1

        for block in self.blocks:
            for entry, stat in block.gen.items():
                assmts = self.assmts[entry]
                if stat is Uninitialized:
                    block.i_gen |= assmts.bit
                else:
                    block.i_gen |= stat.bit
                block.i_kill |= assmts.mask
            block.i_output = block.i_gen
            for entry in block.bounded:
                block.i_kill |= self.assmts[entry].bit

        for assmts in self.assmts.values():
            self.entry_point.i_gen |= assmts.bit
        self.entry_point.i_output = self.entry_point.i_gen

    def map_one(self, istate, entry):
        ret = set()
        assmts: AssignmentList = self.assmts[entry]
        if istate & assmts.bit:
            if self.is_statically_assigned(entry):
                ret.add(StaticAssignment(entry))
            elif entry.from_closure:
                ret.add(Unknown)
            else:
                ret.add(Uninitialized)

        assmt: NameAssignment
        for assmt in assmts.stats:
            if istate & assmt.bit:
                ret.add(assmt)
        return ret

    def reaching_definitions(self):
        """Per-block reaching definitions analysis."""
        block: ControlBlock
        parent: ControlBlock

        dirty = True
        while dirty:
            dirty = False
            for block in self.blocks:
                i_input = 0
                for parent in block.parents:
                    i_input |= parent.i_output
                i_output = (i_input & ~block.i_kill) | block.i_gen
                if i_output != block.i_output:
                    dirty = True
                block.i_input = i_input
                block.i_output = i_output


class LoopDescr:
    def __init__(self, next_block, loop_block):
        self.next_block = next_block
        self.loop_block = loop_block
        self.exceptions = []


class ExceptionDescr:
    """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:
    def __init__(self, lhs, rhs, entry, rhs_scope=None):
        if lhs.cf_state is None:
            lhs.cf_state = set()
        self.lhs = lhs
        self.rhs = rhs
        self.entry = entry
        self.pos = lhs.pos
        self.refs = set()
        self.is_arg = False
        self.is_deletion = False
        self.inferred_type = None
        # For generator expression targets in comprehensions (and possibly other things),
        # the rhs can have a different scope than the lhs.
        self.rhs_scope = rhs_scope

    def __repr__(self):
        return '%s(entry=%r)' % (self.__class__.__name__, self.entry)

    def infer_type(self):
        self.inferred_type = self.rhs.infer_type(self.rhs_scope or self.entry.scope)
        return self.inferred_type

    def type_dependencies(self):
        return self.rhs.type_dependencies(self.rhs_scope or self.entry.scope)

    @property
    def type(self):
        if not self.entry.type.is_unspecified:
            return self.entry.type
        return self.inferred_type


class StaticAssignment(NameAssignment):
    """Initialised at declaration time, e.g. stack allocation."""
    def __init__(self, entry):
        if not entry.type.is_pyobject:
            may_be_none = False
        else:
            may_be_none = None  # unknown
        lhs = TypedExprNode(
            entry.type, may_be_none=may_be_none, pos=entry.pos)
        super().__init__(lhs, lhs, entry)

    def infer_type(self):
        return self.entry.type

    def type_dependencies(self):
        return ()


class Argument(NameAssignment):
    def __init__(self, lhs, rhs, entry):
        NameAssignment.__init__(self, lhs, rhs, entry)
        self.is_arg = True


class NameDeletion(NameAssignment):
    def __init__(self, lhs, entry):
        NameAssignment.__init__(self, lhs, lhs, entry)
        self.is_deletion = True

    def infer_type(self):
        inferred_type = self.rhs.infer_type(self.entry.scope)
        if (not inferred_type.is_pyobject
                and inferred_type.can_coerce_to_pyobject(self.entry.scope)):
            return PyrexTypes.py_object_type
        self.inferred_type = inferred_type
        return inferred_type


class Uninitialized:
    """Definitely not initialised yet."""


class Unknown:
    """Coming from outer closure, might be initialised or not."""


class NameReference:
    def __init__(self, node, entry):
        if node.cf_state is None:
            node.cf_state = set()
        self.node = node
        self.entry = entry
        self.pos = node.pos

    def __repr__(self):
        return '%s(entry=%r)' % (self.__class__.__name__, self.entry)


class ControlFlowState(list):
    # Keeps track of Node's entry assignments
    #
    # cf_is_null        [boolean] It is uninitialized
    # cf_maybe_null     [boolean] May be uninitialized
    # is_single         [boolean] Has only one assignment at this point

    cf_maybe_null = False
    cf_is_null = False
    is_single = False

    def __init__(self, state):
        if Uninitialized in state:
            state.discard(Uninitialized)
            self.cf_maybe_null = True
            if not state:
                self.cf_is_null = True
        elif Unknown in state:
            state.discard(Unknown)
            self.cf_maybe_null = True
        else:
            if len(state) == 1:
                self.is_single = True
        # XXX: Remove fake_rhs_expr
        super().__init__(
            [i for i in state if i.rhs is not fake_rhs_expr])

    def one(self):
        return self[0]


class GVContext:
    """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 srcdescr not 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:
    """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 [%s %s]' % (
                            stat.entry.name, 'deletion' if stat.is_deletion else 'definition', stat.pos[1])
                    elif isinstance(stat, NameReference):
                        if stat.entry:
                            label += '\n %s [reference %s]' % (stat.entry.name, stat.pos[1])
            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:
    """Collect error/warnings messages first then sort"""
    def __init__(self):
        self.messages = set()

    def error(self, pos, message):
        self.messages.add((pos, True, message))

    def warning(self, pos, message):
        self.messages.add((pos, False, message))

    def report(self):
        for pos, is_error, message in sorted(self.messages):
            if is_error:
                error(pos, message)
            else:
                warning(pos, message, 2)


@cython.cfunc
def check_definitions(flow: ControlFlow, compiler_directives: dict):
    flow.initialize()
    flow.reaching_definitions()

    # Track down state
    assignments = set()
    # Node to entry map
    references = {}
    assmt_nodes = set()

    block: ControlBlock
    assmt: NameAssignment
    for block in flow.blocks:
        i_state = block.i_input
        for stat in block.stats:
            i_assmts = flow.assmts[stat.entry]
            state = flow.map_one(i_state, stat.entry)
            if isinstance(stat, NameAssignment):
                stat.lhs.cf_state.update(state)
                assmt_nodes.add(stat.lhs)
                i_state = i_state & ~i_assmts.mask
                if stat.is_deletion:
                    i_state |= i_assmts.bit
                else:
                    i_state |= stat.bit
                assignments.add(stat)
                if stat.rhs is not fake_rhs_expr:
                    stat.entry.cf_assignments.append(stat)
            elif isinstance(stat, NameReference):
                references[stat.node] = stat.entry
                stat.entry.cf_references.append(stat)
                stat.node.cf_state.update(state)
                ## if not stat.node.allow_null:
                ##     i_state &= ~i_assmts.bit
                ## # after successful read, the state is known to be initialised
                state.discard(Uninitialized)
                state.discard(Unknown)
                for assmt in state:
                    assmt.refs.add(stat)

    # Check variable usage
    warn_maybe_uninitialized = compiler_directives['warn.maybe_uninitialized']
    warn_unused_result = compiler_directives['warn.unused_result']
    warn_unused = compiler_directives['warn.unused']
    warn_unused_arg = compiler_directives['warn.unused_arg']

    messages = MessageCollection()

    # assignment hints
    for node in assmt_nodes:
        if Uninitialized in node.cf_state:
            node.cf_maybe_null = True
            if len(node.cf_state) == 1:
                node.cf_is_null = True
            else:
                node.cf_is_null = False
        elif Unknown in node.cf_state:
            node.cf_maybe_null = True
        else:
            node.cf_is_null = False
            node.cf_maybe_null = False

    # Find uninitialized references and cf-hints
    for node, entry in references.items():
        if Uninitialized in node.cf_state:
            node.cf_maybe_null = True
            if (not entry.from_closure and len(node.cf_state) == 1
                    and entry.name not in entry.scope.scope_predefined_names):
                node.cf_is_null = True
            if (node.allow_null or entry.from_closure
                    or entry.is_pyclass_attr or entry.type.is_error):
                pass  # Can be uninitialized here
            elif node.cf_is_null and not entry.in_closure:
                if entry.error_on_uninitialized or (
                        Options.error_on_uninitialized and (
                        entry.type.is_pyobject or entry.type.is_unspecified)):
                    messages.error(
                        node.pos,
                        "local variable '%s' referenced before assignment"
                        % entry.name)
                else:
                    messages.warning(
                        node.pos,
                        "local variable '%s' referenced before assignment"
                        % entry.name)
            elif warn_maybe_uninitialized:
                msg = "local variable '%s' might be referenced before assignment" % entry.name
                if entry.in_closure:
                    msg += " (maybe initialized inside a closure)"
                messages.warning(
                    node.pos,
                    msg)
        elif Unknown in node.cf_state:
            # TODO: better cross-closure analysis to know when inner functions
            #       are being called before a variable is being set, and when
            #       a variable is known to be set before even defining the
            #       inner function, etc.
            node.cf_maybe_null = True
        else:
            node.cf_is_null = False
            node.cf_maybe_null = False

    # Unused result
    for assmt in assignments:
        if (not assmt.refs and not assmt.entry.is_pyclass_attr
                and not assmt.entry.in_closure):
            if assmt.entry.cf_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.cf_used = False

    # Unused entries
    for entry in flow.entries:
        if (not entry.cf_references
                and not entry.is_pyclass_attr):
            if entry.name != '_' and not entry.name.startswith('unused'):
                # '_' is often used for unused variables, e.g. in loops
                if entry.is_arg:
                    if warn_unused_arg:
                        messages.warning(entry.pos, "Unused argument '%s'" %
                                         entry.name)
                else:
                    if warn_unused:
                        messages.warning(entry.pos, "Unused entry '%s'" %
                                         entry.name)
            entry.cf_used = False

    messages.report()

    for node in assmt_nodes:
        node.cf_state = ControlFlowState(node.cf_state)
    for node in references:
        node.cf_state = ControlFlowState(node.cf_state)


class AssignmentCollector(TreeVisitor):
    def __init__(self):
        super().__init__()
        self.assignments = []

    def visit_Node(self):
        self._visitchildren(self, None, None)

    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 ControlFlowAnalysis(CythonTransform):

    def find_in_stack(self, env):
        if env == self.env:
            return self.flow
        for e, flow in reversed(self.stack):
            if e is env:
                return flow
        assert False

    def visit_ModuleNode(self, node):
        dot_output = self.current_directives['control_flow.dot_output']
        self.gv_ctx = GVContext() if dot_output else None

        from .Optimize import ConstantFolding
        self.constant_folder = ConstantFolding()

        # Set of NameNode reductions
        self.reductions = set()

        self.in_inplace_assignment = False
        self.env = node.scope
        self.flow = ControlFlow()
        self.stack = []  # a stack of (env, flow) tuples
        self.object_expr = TypedExprNode(PyrexTypes.py_object_type, may_be_none=True)
        self.visitchildren(node)

        check_definitions(self.flow, self.current_directives)

        if dot_output:
            annotate_defs = self.current_directives['control_flow.dot_annotate_defs']
            with open(dot_output, 'w') as fp:
                self.gv_ctx.render(fp, 'module', annotate_defs=annotate_defs)
        return node

    def visit_FuncDefNode(self, node):
        for arg in node.args:
            if arg.default:
                self.visitchildren(arg)
        self.visitchildren(node, ('decorators',))
        self.stack.append((self.env, self.flow))
        self.env = node.local_scope
        self.flow = ControlFlow()

        # Collect all entries
        for entry in node.local_scope.entries.values():
            if self.flow.is_tracked(entry):
                self.flow.entries.add(entry)

        self.mark_position(node)
        # Function body block
        self.flow.nextblock()

        for arg in node.args:
            self._visit(arg)
        if node.star_arg:
            self.flow.mark_argument(node.star_arg,
                                    TypedExprNode(Builtin.tuple_type,
                                                  may_be_none=False),
                                    node.star_arg.entry)
        if node.starstar_arg:
            self.flow.mark_argument(node.starstar_arg,
                                    TypedExprNode(Builtin.dict_type,
                                                  may_be_none=False),
                                    node.starstar_arg.entry)
        self._visit(node.body)
        # Workaround for generators
        if node.is_generator:
            self._visit(node.gbody.body)

        # 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)

        if self.gv_ctx is not None:
            self.gv_ctx.add(GV(node.local_scope.name, self.flow))

        self.env, self.flow = self.stack.pop()
        return node

    def visit_DefNode(self, node):
        node.used = True
        return self.visit_FuncDefNode(node)

    def visit_GeneratorBodyDefNode(self, node):
        return node

    def visit_CTypeDefNode(self, node):
        return node

    def mark_assignment(self, lhs, rhs=None, rhs_scope=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 not rhs:
            rhs = self.object_expr
        if lhs.is_name:
            if lhs.entry is not None:
                entry = lhs.entry
            else:
                entry = self.env.lookup(lhs.name)
            if entry is None:  # TODO: This shouldn't happen...
                return
            self.flow.mark_assignment(lhs, rhs, entry, rhs_scope=(rhs_scope or self.env))
        elif lhs.is_sequence_constructor:
            for i, arg in enumerate(lhs.args):
                if arg.is_starred:
                    # "a, *b = x" assigns a list to "b"
                    item_node = TypedExprNode(Builtin.list_type, may_be_none=False, pos=arg.pos)
                elif rhs is self.object_expr:
                    item_node = rhs
                else:
                    item_node = rhs.inferable_item_node(i)
                self.mark_assignment(arg, item_node, rhs_scope=rhs_scope)
        else:
            self._visit(lhs)

        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_AssignmentNode(self, node):
        raise InternalError("Unhandled assignment node %s" % type(node))

    def visit_SingleAssignmentNode(self, node):
        self._visit(node.rhs)
        self.mark_assignment(node.lhs, node.rhs)
        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.in_inplace_assignment = True
        self.visitchildren(node)
        self.in_inplace_assignment = False
        self.mark_assignment(node.lhs, self.constant_folder(node.create_binop_node()))
        return node

    def visit_DelStatNode(self, node):
        for arg in node.args:
            if arg.is_name:
                entry = arg.entry or self.env.lookup(arg.name)
                if entry.in_closure or entry.from_closure:
                    error(arg.pos,
                          "can not delete variable '%s' "
                          "referenced in nested scope" % entry.name)
                if not node.ignore_nonexisting:
                    self._visit(arg)  # mark reference
                self.flow.mark_deletion(arg, entry)
            else:
                self._visit(arg)
        return node

    def visit_CArgDeclNode(self, node):
        entry = self.env.lookup(node.name)
        if entry:
            may_be_none = not node.not_none
            self.flow.mark_argument(
                node, TypedExprNode(entry.type, may_be_none), 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)

                if entry in self.reductions and not self.in_inplace_assignment:
                    error(node.pos,
                          "Cannot read reduction variable in loop body")

        return node

    def visit_StatListNode(self, node):
        if self.flow.block:
            for stat in node.stats:
                self._visit(stat)
                if not self.flow.block:
                    stat.is_terminator = True
                    break
        return node

    def visit_Node(self, node):
        self.visitchildren(node)
        self.mark_position(node)
        return node

    def visit_SizeofVarNode(self, node):
        return node

    def visit_TypeidNode(self, 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:
            self.flow.nextblock(parent)
            self._visit(clause.condition)
            parent = self.flow.block
            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_AssertStatNode(self, node):
        """Essentially an if-condition that wraps a RaiseStatNode.
        """
        self.mark_position(node)
        next_block = self.flow.newblock()
        parent = self.flow.block
        # failure case
        parent = self.flow.nextblock(parent)
        self._visit(node.condition)
        self.flow.nextblock()
        self._visit(node.exception)
        if self.flow.block:
            self.flow.block.add_child(next_block)
        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))
        if node.condition:
            self._visit(node.condition)
        condition_block_end = self.flow.block
        # Body block
        self.flow.nextblock()
        self._visit(node.body)
        self.flow.loops.pop()
        # 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_end)
            self._visit(node.else_clause)
            if self.flow.block:
                self.flow.block.add_child(next_block)
        else:
            condition_block_end.add_child(next_block)

        if next_block.parents:
            self.flow.block = next_block
        else:
            self.flow.block = None
        return node

    def mark_forloop_target(self, node):
        # TODO: Remove redundancy with range optimization...
        is_special = False
        sequence = node.iterator.sequence
        target = node.target
        env = node.iterator.expr_scope or self.env
        if isinstance(sequence, ExprNodes.SimpleCallNode):
            function = sequence.function
            if sequence.self is None and function.is_name:
                entry = env.lookup(function.name)
                if not entry or entry.is_builtin:
                    if function.name == 'reversed' and len(sequence.args) == 1:
                        sequence = sequence.args[0]
                    elif function.name == 'enumerate' and len(sequence.args) == 1:
                        if target.is_sequence_constructor and len(target.args) == 2:
                            iterator = sequence.args[0]
                            if iterator.is_name:
                                iterator_type = iterator.infer_type(env)
                                if iterator_type.is_builtin_type:
                                    # assume that builtin types have a length within Py_ssize_t
                                    self.mark_assignment(
                                        target.args[0],
                                        ExprNodes.IntNode(target.pos, value='PY_SSIZE_T_MAX',
                                                          type=PyrexTypes.c_py_ssize_t_type),
                                        rhs_scope=node.iterator.expr_scope)
                                    target = target.args[1]
                                    sequence = sequence.args[0]
        if isinstance(sequence, ExprNodes.SimpleCallNode):
            function = sequence.function
            if sequence.self is None and function.is_name and function.name in ('range', 'xrange'):
                entry = env.lookup(function.name)
                if entry and entry.is_type and entry.type is Builtin.range_type:
                    is_special = True
                    for arg in sequence.args[:2]:
                        self.mark_assignment(target, arg, rhs_scope=node.iterator.expr_scope)
                    if len(sequence.args) > 2:
                        self.mark_assignment(target, self.constant_folder(
                            ExprNodes.binop_node(node.pos,
                                                    '+',
                                                    sequence.args[0],
                                                    sequence.args[2])),
                                            rhs_scope=node.iterator.expr_scope)

        if not is_special:
            # A for-loop basically translates to subsequent calls to
            # __getitem__(), so using an IndexNode here allows us to
            # naturally infer the base type of pointers, C arrays,
            # Python strings, etc., while correctly falling back to an
            # object type when the base type cannot be handled.

            self.mark_assignment(target, node.item, rhs_scope=node.iterator.expr_scope)

    def mark_parallel_forloop_assignment(self, node):
        target = node.target
        for arg in node.args[:2]:
            self.mark_assignment(target, arg)
        if len(node.args) > 2:
            self.mark_assignment(target, self.constant_folder(
                ExprNodes.binop_node(node.pos,
                                        '+',
                                        node.args[0],
                                        node.args[2])))
        if not node.args:
            # Almost certainly an error
            self.mark_assignment(target)

    def visit_AsyncForStatNode(self, node):
        return self.visit_ForInStatNode(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)
        condition_block_end = self.flow.block
        # Target assignment
        self.flow.nextblock()

        if isinstance(node, Nodes.ForInStatNode):
            self.mark_forloop_target(node)
        elif isinstance(node, Nodes.AsyncForStatNode):
            # not entirely correct, but good enough for now
            self.mark_assignment(node.target, node.item)
        elif isinstance(node, Nodes.ParallelRangeNode):  # Parallel
            self.mark_parallel_forloop_assignment(node)
        else:
            assert False, type(node)

        # Body block

        self.flow.nextblock()
        self._visit(node.body)
        self.flow.loops.pop()

        # 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_end)
            self._visit(node.else_clause)
            if self.flow.block:
                self.flow.block.add_child(next_block)
        else:
            condition_block_end.add_child(next_block)

        if next_block.parents:
            self.flow.block = next_block
        else:
            self.flow.block = None
        return node

    def _delete_privates(self, node, exclude=None):
        for private_node in node.assigned_nodes:
            if not exclude or private_node.entry is not exclude:
                self.flow.mark_deletion(private_node, private_node.entry)

    def visit_ParallelRangeNode(self, node):
        reductions = self.reductions

        # if node.target is None or not a NameNode, an error will have
        # been previously issued
        if hasattr(node.target, 'entry'):
            self.reductions = set(reductions)

            for private_node in node.assigned_nodes:
                private_node.entry.error_on_uninitialized = True
                pos, reduction = node.assignments[private_node.entry]
                if reduction:
                    self.reductions.add(private_node.entry)

            node = self.visit_ForInStatNode(node)

        self.reductions = reductions
        return node

    def visit_ParallelWithBlockNode(self, node):
        for private_node in node.assigned_nodes:
            private_node.entry.error_on_uninitialized = True

        self.visitchildren(node)
        # lastprivate isn't allowed/doesn't make sense for a parallel (non-for) block
        self._delete_privates(node)

        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 is not None:
            self._visit(node.step)
        condition_block_end = self.flow.block
        # Target assignment
        self.flow.nextblock()
        self.mark_assignment(node.target, node.bound1)
        if node.step is not None:
            self.mark_assignment(node.target, self.constant_folder(
                ExprNodes.binop_node(node.pos, '+', node.bound1, node.step)))
        # Body block
        self.flow.nextblock()
        self._visit(node.body)
        self.flow.loops.pop()
        # 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_end)
            self._visit(node.else_clause)
            if self.flow.block:
                self.flow.block.add_child(next_block)
        else:
            condition_block_end.add_child(next_block)

        if next_block.parents:
            self.flow.block = next_block
        else:
            self.flow.block = None
        return node

    def visit_LoopNode(self, node):
        raise InternalError("Generic loops are not supported")

    def visit_WithTargetAssignmentStatNode(self, node):
        self.mark_assignment(node.lhs, node.with_node.enter_call)
        return node

    def visit_WithStatNode(self, node):
        self._visit(node.manager)
        self._visit(node.enter_call)
        self._visit(node.body)
        return node

    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.flow.nextblock()
        self.flow.in_try_block += 1
        self._visit(node.body)
        self.flow.in_try_block -= 1
        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
            entry_point = self.flow.newblock(parent=self.flow.block)
            self.flow.nextblock()
            if clause.target:
                self.mark_assignment(clause.target, clause.exc_value)
            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_except_clause)

        if self.flow.block and self.flow.exceptions:
            self.flow.block.add_child(self.flow.exceptions[-1].entry_point)

        # Normal execution
        finally_enter = self.flow.newblock()
        self.flow.block = finally_enter
        self._visit(node.finally_clause)
        finally_exit = self.flow.block

        descr = ExceptionDescr(entry_point, finally_enter, finally_exit)
        self.flow.exceptions.append(descr)
        if self.flow.loops:
            self.flow.loops[-1].exceptions.append(descr)
        self.flow.block = body_block
        body_block.add_child(entry_point)
        self.flow.nextblock()
        self.flow.in_try_block += 1
        self._visit(node.body)
        self.flow.in_try_block -= 1
        self.flow.exceptions.pop()
        if self.flow.loops:
            self.flow.loops[-1].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)
        self.visitchildren(node)
        if self.flow.exceptions:
            self.flow.block.add_child(self.flow.exceptions[-1].entry_point)
        self.flow.block = None
        if self.flow.in_try_block:
            node.in_try_block = True
        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)

        outer_exception_handlers = iter(self.flow.exceptions[::-1])
        for handler in outer_exception_handlers:
            if handler.finally_enter:
                self.flow.block.add_child(handler.finally_enter)
                if handler.finally_exit:
                    # 'return' goes to function exit, or to the next outer 'finally' clause
                    exit_point = self.flow.exit_point
                    for next_handler in outer_exception_handlers:
                        if next_handler.finally_enter:
                            exit_point = next_handler.finally_enter
                            break
                    handler.finally_exit.add_child(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 loop.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 loop.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.stack.append((self.env, self.flow))
            self.env = node.expr_scope
        # Skip append node here
        self._visit(node.loop)
        if node.expr_scope:
            self.env, _ = self.stack.pop()
        return node

    def visit_ScopedExprNode(self, node):
        # currently this is written to deal with these two types
        # (with comprehensions covered in their own function)
        assert isinstance(node, (ExprNodes.IteratorNode, ExprNodes.AsyncIteratorNode)), node
        if node.expr_scope:
            self.stack.append((self.env, self.flow))
            self.flow = self.find_in_stack(node.expr_scope)
            self.env = node.expr_scope
        self.visitchildren(node)
        if node.expr_scope:
            self.env, self.flow = self.stack.pop()
        return node

    def visit_PyClassDefNode(self, node):
        self.visitchildren(node, ('dict', 'metaclass',
                                  'mkw', 'bases', 'class_result'))
        self.flow.mark_assignment(node.target, node.classobj,
                                  self.env.lookup(node.target.name))
        self.stack.append((self.env, self.flow))
        self.env = node.scope
        self.flow.nextblock()
        if node.doc_node:
            self.flow.mark_assignment(node.doc_node, fake_rhs_expr, node.doc_node.entry)
        self.visitchildren(node, ('body',))
        self.flow.nextblock()
        self.env, _ = self.stack.pop()
        return node

    def visit_CClassDefNode(self, node):
        # just make sure the nodes scope is findable in-case there is a list comprehension in it
        self.stack.append((node.scope, self.flow))
        self.visitchildren(node)
        self.stack.pop()
        return node

    def visit_AmpersandNode(self, node):
        if node.operand.is_name:
            # Fake assignment to silence warning
            self.mark_assignment(node.operand, fake_rhs_expr)
        self.visitchildren(node)
        return node

    def visit_BoolBinopNode(self, node):
        # Note - I don't believe BoolBinopResultNode needs special handling beyond this
        assert len(node.subexprs) == 2  # operand1 and operand2 only

        next_block = self.flow.newblock()
        parent = self.flow.block

        self._visit(node.operand1)

        self.flow.nextblock()
        self._visit(node.operand2)
        if self.flow.block:
            self.flow.block.add_child(next_block)

        parent.add_child(next_block)

        if next_block.parents:
            self.flow.block = next_block
        else:
            self.flow.block = None
        return node

    def visit_CondExprNode(self, node):
        assert len(node.subexprs) == 3
        self._visit(node.test)
        parent = self.flow.block
        next_block = self.flow.newblock()
        self.flow.nextblock()
        self._visit(node.true_val)
        if self.flow.block:
            self.flow.block.add_child(next_block)
        self.flow.nextblock(parent=parent)
        self._visit(node.false_val)
        if self.flow.block:
            self.flow.block.add_child(next_block)

        if next_block.parents:
            self.flow.block = next_block
        else:
            self.flow.block = None
        return node
