1# cython: infer_types=True
2# cython: language_level=3
3# cython: auto_pickle=False
4
5#
6#   Tree visitor and transform framework
7#
8
9from __future__ import absolute_import, print_function
10
11import sys
12import inspect
13
14from . import TypeSlots
15from . import Builtin
16from . import Nodes
17from . import ExprNodes
18from . import Errors
19from . import DebugFlags
20from . import Future
21
22import cython
23
24
25cython.declare(_PRINTABLE=tuple)
26
27if sys.version_info[0] >= 3:
28    _PRINTABLE = (bytes, str, int, float)
29else:
30    _PRINTABLE = (str, unicode, long, int, float)
31
32
33class TreeVisitor(object):
34    """
35    Base class for writing visitors for a Cython tree, contains utilities for
36    recursing such trees using visitors. Each node is
37    expected to have a child_attrs iterable containing the names of attributes
38    containing child nodes or lists of child nodes. Lists are not considered
39    part of the tree structure (i.e. contained nodes are considered direct
40    children of the parent node).
41
42    visit_children visits each of the children of a given node (see the visit_children
43    documentation). When recursing the tree using visit_children, an attribute
44    access_path is maintained which gives information about the current location
45    in the tree as a stack of tuples: (parent_node, attrname, index), representing
46    the node, attribute and optional list index that was taken in each step in the path to
47    the current node.
48
49    Example:
50
51    >>> class SampleNode(object):
52    ...     child_attrs = ["head", "body"]
53    ...     def __init__(self, value, head=None, body=None):
54    ...         self.value = value
55    ...         self.head = head
56    ...         self.body = body
57    ...     def __repr__(self): return "SampleNode(%s)" % self.value
58    ...
59    >>> tree = SampleNode(0, SampleNode(1), [SampleNode(2), SampleNode(3)])
60    >>> class MyVisitor(TreeVisitor):
61    ...     def visit_SampleNode(self, node):
62    ...         print("in %s %s" % (node.value, self.access_path))
63    ...         self.visitchildren(node)
64    ...         print("out %s" % node.value)
65    ...
66    >>> MyVisitor().visit(tree)
67    in 0 []
68    in 1 [(SampleNode(0), 'head', None)]
69    out 1
70    in 2 [(SampleNode(0), 'body', 0)]
71    out 2
72    in 3 [(SampleNode(0), 'body', 1)]
73    out 3
74    out 0
75    """
76    def __init__(self):
77        super(TreeVisitor, self).__init__()
78        self.dispatch_table = {}
79        self.access_path = []
80
81    def dump_node(self, node):
82        ignored = list(node.child_attrs or []) + [
83            u'child_attrs', u'pos', u'gil_message', u'cpp_message', u'subexprs']
84        values = []
85        pos = getattr(node, 'pos', None)
86        if pos:
87            source = pos[0]
88            if source:
89                import os.path
90                source = os.path.basename(source.get_description())
91            values.append(u'%s:%s:%s' % (source, pos[1], pos[2]))
92        attribute_names = dir(node)
93        for attr in attribute_names:
94            if attr in ignored:
95                continue
96            if attr.startswith('_') or attr.endswith('_'):
97                continue
98            try:
99                value = getattr(node, attr)
100            except AttributeError:
101                continue
102            if value is None or value == 0:
103                continue
104            elif isinstance(value, list):
105                value = u'[...]/%d' % len(value)
106            elif not isinstance(value, _PRINTABLE):
107                continue
108            else:
109                value = repr(value)
110            values.append(u'%s = %s' % (attr, value))
111        return u'%s(%s)' % (node.__class__.__name__, u',\n    '.join(values))
112
113    def _find_node_path(self, stacktrace):
114        import os.path
115        last_traceback = stacktrace
116        nodes = []
117        while hasattr(stacktrace, 'tb_frame'):
118            frame = stacktrace.tb_frame
119            node = frame.f_locals.get(u'self')
120            if isinstance(node, Nodes.Node):
121                code = frame.f_code
122                method_name = code.co_name
123                pos = (os.path.basename(code.co_filename),
124                       frame.f_lineno)
125                nodes.append((node, method_name, pos))
126                last_traceback = stacktrace
127            stacktrace = stacktrace.tb_next
128        return (last_traceback, nodes)
129
130    def _raise_compiler_error(self, child, e):
131        trace = ['']
132        for parent, attribute, index in self.access_path:
133            node = getattr(parent, attribute)
134            if index is None:
135                index = ''
136            else:
137                node = node[index]
138                index = u'[%d]' % index
139            trace.append(u'%s.%s%s = %s' % (
140                parent.__class__.__name__, attribute, index,
141                self.dump_node(node)))
142        stacktrace, called_nodes = self._find_node_path(sys.exc_info()[2])
143        last_node = child
144        for node, method_name, pos in called_nodes:
145            last_node = node
146            trace.append(u"File '%s', line %d, in %s: %s" % (
147                pos[0], pos[1], method_name, self.dump_node(node)))
148        raise Errors.CompilerCrash(
149            getattr(last_node, 'pos', None), self.__class__.__name__,
150            u'\n'.join(trace), e, stacktrace)
151
152    @cython.final
153    def find_handler(self, obj):
154        # to resolve, try entire hierarchy
155        cls = type(obj)
156        pattern = "visit_%s"
157        mro = inspect.getmro(cls)
158        for mro_cls in mro:
159            handler_method = getattr(self, pattern % mro_cls.__name__, None)
160            if handler_method is not None:
161                return handler_method
162        print(type(self), cls)
163        if self.access_path:
164            print(self.access_path)
165            print(self.access_path[-1][0].pos)
166            print(self.access_path[-1][0].__dict__)
167        raise RuntimeError("Visitor %r does not accept object: %s" % (self, obj))
168
169    def visit(self, obj):
170        return self._visit(obj)
171
172    @cython.final
173    def _visit(self, obj):
174        try:
175            try:
176                handler_method = self.dispatch_table[type(obj)]
177            except KeyError:
178                handler_method = self.find_handler(obj)
179                self.dispatch_table[type(obj)] = handler_method
180            return handler_method(obj)
181        except Errors.CompileError:
182            raise
183        except Errors.AbortError:
184            raise
185        except Exception as e:
186            if DebugFlags.debug_no_exception_intercept:
187                raise
188            self._raise_compiler_error(obj, e)
189
190    @cython.final
191    def _visitchild(self, child, parent, attrname, idx):
192        self.access_path.append((parent, attrname, idx))
193        result = self._visit(child)
194        self.access_path.pop()
195        return result
196
197    def visitchildren(self, parent, attrs=None):
198        return self._visitchildren(parent, attrs)
199
200    @cython.final
201    @cython.locals(idx=int)
202    def _visitchildren(self, parent, attrs):
203        """
204        Visits the children of the given parent. If parent is None, returns
205        immediately (returning None).
206
207        The return value is a dictionary giving the results for each
208        child (mapping the attribute name to either the return value
209        or a list of return values (in the case of multiple children
210        in an attribute)).
211        """
212        if parent is None: return None
213        result = {}
214        for attr in parent.child_attrs:
215            if attrs is not None and attr not in attrs: continue
216            child = getattr(parent, attr)
217            if child is not None:
218                if type(child) is list:
219                    childretval = [self._visitchild(x, parent, attr, idx) for idx, x in enumerate(child)]
220                else:
221                    childretval = self._visitchild(child, parent, attr, None)
222                    assert not isinstance(childretval, list), 'Cannot insert list here: %s in %r' % (attr, parent)
223                result[attr] = childretval
224        return result
225
226
227class VisitorTransform(TreeVisitor):
228    """
229    A tree transform is a base class for visitors that wants to do stream
230    processing of the structure (rather than attributes etc.) of a tree.
231
232    It implements __call__ to simply visit the argument node.
233
234    It requires the visitor methods to return the nodes which should take
235    the place of the visited node in the result tree (which can be the same
236    or one or more replacement). Specifically, if the return value from
237    a visitor method is:
238
239    - [] or None; the visited node will be removed (set to None if an attribute and
240    removed if in a list)
241    - A single node; the visited node will be replaced by the returned node.
242    - A list of nodes; the visited nodes will be replaced by all the nodes in the
243    list. This will only work if the node was already a member of a list; if it
244    was not, an exception will be raised. (Typically you want to ensure that you
245    are within a StatListNode or similar before doing this.)
246    """
247    def visitchildren(self, parent, attrs=None, exclude=None):
248        # generic def entry point for calls from Python subclasses
249        if exclude is not None:
250            attrs = self._select_attrs(parent.child_attrs if attrs is None else attrs, exclude)
251        return self._process_children(parent, attrs)
252
253    @cython.final
254    def _select_attrs(self, attrs, exclude):
255        return [name for name in attrs if name not in exclude]
256
257    @cython.final
258    def _process_children(self, parent, attrs=None):
259        # fast cdef entry point for calls from Cython subclasses
260        result = self._visitchildren(parent, attrs)
261        for attr, newnode in result.items():
262            if type(newnode) is list:
263                newnode = self._flatten_list(newnode)
264            setattr(parent, attr, newnode)
265        return result
266
267    @cython.final
268    def _flatten_list(self, orig_list):
269        # Flatten the list one level and remove any None
270        newlist = []
271        for x in orig_list:
272            if x is not None:
273                if type(x) is list:
274                    newlist.extend(x)
275                else:
276                    newlist.append(x)
277        return newlist
278
279    def recurse_to_children(self, node):
280        self._process_children(node)
281        return node
282
283    def __call__(self, root):
284        return self._visit(root)
285
286
287class CythonTransform(VisitorTransform):
288    """
289    Certain common conventions and utilities for Cython transforms.
290
291     - Sets up the context of the pipeline in self.context
292     - Tracks directives in effect in self.current_directives
293    """
294    def __init__(self, context):
295        super(CythonTransform, self).__init__()
296        self.context = context
297
298    def __call__(self, node):
299        from . import ModuleNode
300        if isinstance(node, ModuleNode.ModuleNode):
301            self.current_directives = node.directives
302        return super(CythonTransform, self).__call__(node)
303
304    def visit_CompilerDirectivesNode(self, node):
305        old = self.current_directives
306        self.current_directives = node.directives
307        self._process_children(node)
308        self.current_directives = old
309        return node
310
311    def visit_Node(self, node):
312        self._process_children(node)
313        return node
314
315
316class ScopeTrackingTransform(CythonTransform):
317    # Keeps track of type of scopes
318    #scope_type: can be either of 'module', 'function', 'cclass', 'pyclass', 'struct'
319    #scope_node: the node that owns the current scope
320
321    def visit_ModuleNode(self, node):
322        self.scope_type = 'module'
323        self.scope_node = node
324        self._process_children(node)
325        return node
326
327    def visit_scope(self, node, scope_type):
328        prev = self.scope_type, self.scope_node
329        self.scope_type = scope_type
330        self.scope_node = node
331        self._process_children(node)
332        self.scope_type, self.scope_node = prev
333        return node
334
335    def visit_CClassDefNode(self, node):
336        return self.visit_scope(node, 'cclass')
337
338    def visit_PyClassDefNode(self, node):
339        return self.visit_scope(node, 'pyclass')
340
341    def visit_FuncDefNode(self, node):
342        return self.visit_scope(node, 'function')
343
344    def visit_CStructOrUnionDefNode(self, node):
345        return self.visit_scope(node, 'struct')
346
347
348class EnvTransform(CythonTransform):
349    """
350    This transformation keeps a stack of the environments.
351    """
352    def __call__(self, root):
353        self.env_stack = []
354        self.enter_scope(root, root.scope)
355        return super(EnvTransform, self).__call__(root)
356
357    def current_env(self):
358        return self.env_stack[-1][1]
359
360    def current_scope_node(self):
361        return self.env_stack[-1][0]
362
363    def global_scope(self):
364        return self.current_env().global_scope()
365
366    def enter_scope(self, node, scope):
367        self.env_stack.append((node, scope))
368
369    def exit_scope(self):
370        self.env_stack.pop()
371
372    def visit_FuncDefNode(self, node):
373        self.enter_scope(node, node.local_scope)
374        self._process_children(node)
375        self.exit_scope()
376        return node
377
378    def visit_GeneratorBodyDefNode(self, node):
379        self._process_children(node)
380        return node
381
382    def visit_ClassDefNode(self, node):
383        self.enter_scope(node, node.scope)
384        self._process_children(node)
385        self.exit_scope()
386        return node
387
388    def visit_CStructOrUnionDefNode(self, node):
389        self.enter_scope(node, node.scope)
390        self._process_children(node)
391        self.exit_scope()
392        return node
393
394    def visit_ScopedExprNode(self, node):
395        if node.expr_scope:
396            self.enter_scope(node, node.expr_scope)
397            self._process_children(node)
398            self.exit_scope()
399        else:
400            self._process_children(node)
401        return node
402
403    def visit_CArgDeclNode(self, node):
404        # default arguments are evaluated in the outer scope
405        if node.default:
406            attrs = [attr for attr in node.child_attrs if attr != 'default']
407            self._process_children(node, attrs)
408            self.enter_scope(node, self.current_env().outer_scope)
409            self.visitchildren(node, ('default',))
410            self.exit_scope()
411        else:
412            self._process_children(node)
413        return node
414
415
416class NodeRefCleanupMixin(object):
417    """
418    Clean up references to nodes that were replaced.
419
420    NOTE: this implementation assumes that the replacement is
421    done first, before hitting any further references during
422    normal tree traversal.  This needs to be arranged by calling
423    "self.visitchildren()" at a proper place in the transform
424    and by ordering the "child_attrs" of nodes appropriately.
425    """
426    def __init__(self, *args):
427        super(NodeRefCleanupMixin, self).__init__(*args)
428        self._replacements = {}
429
430    def visit_CloneNode(self, node):
431        arg = node.arg
432        if arg not in self._replacements:
433            self.visitchildren(arg)
434        node.arg = self._replacements.get(arg, arg)
435        return node
436
437    def visit_ResultRefNode(self, node):
438        expr = node.expression
439        if expr is None or expr not in self._replacements:
440            self.visitchildren(node)
441            expr = node.expression
442        if expr is not None:
443            node.expression = self._replacements.get(expr, expr)
444        return node
445
446    def replace(self, node, replacement):
447        self._replacements[node] = replacement
448        return replacement
449
450
451find_special_method_for_binary_operator = {
452    '<':  '__lt__',
453    '<=': '__le__',
454    '==': '__eq__',
455    '!=': '__ne__',
456    '>=': '__ge__',
457    '>':  '__gt__',
458    '+':  '__add__',
459    '&':  '__and__',
460    '/':  '__div__',
461    '//': '__floordiv__',
462    '<<': '__lshift__',
463    '%':  '__mod__',
464    '*':  '__mul__',
465    '|':  '__or__',
466    '**': '__pow__',
467    '>>': '__rshift__',
468    '-':  '__sub__',
469    '^':  '__xor__',
470    'in': '__contains__',
471}.get
472
473
474find_special_method_for_unary_operator = {
475    'not': '__not__',
476    '~':   '__inv__',
477    '-':   '__neg__',
478    '+':   '__pos__',
479}.get
480
481
482class MethodDispatcherTransform(EnvTransform):
483    """
484    Base class for transformations that want to intercept on specific
485    builtin functions or methods of builtin types, including special
486    methods triggered by Python operators.  Must run after declaration
487    analysis when entries were assigned.
488
489    Naming pattern for handler methods is as follows:
490
491    * builtin functions: _handle_(general|simple|any)_function_NAME
492
493    * builtin methods: _handle_(general|simple|any)_method_TYPENAME_METHODNAME
494    """
495    # only visit call nodes and Python operations
496    def visit_GeneralCallNode(self, node):
497        self._process_children(node)
498        function = node.function
499        if not function.type.is_pyobject:
500            return node
501        arg_tuple = node.positional_args
502        if not isinstance(arg_tuple, ExprNodes.TupleNode):
503            return node
504        keyword_args = node.keyword_args
505        if keyword_args and not isinstance(keyword_args, ExprNodes.DictNode):
506            # can't handle **kwargs
507            return node
508        args = arg_tuple.args
509        return self._dispatch_to_handler(node, function, args, keyword_args)
510
511    def visit_SimpleCallNode(self, node):
512        self._process_children(node)
513        function = node.function
514        if function.type.is_pyobject:
515            arg_tuple = node.arg_tuple
516            if not isinstance(arg_tuple, ExprNodes.TupleNode):
517                return node
518            args = arg_tuple.args
519        else:
520            args = node.args
521        return self._dispatch_to_handler(node, function, args, None)
522
523    def visit_PrimaryCmpNode(self, node):
524        if node.cascade:
525            # not currently handled below
526            self._process_children(node)
527            return node
528        return self._visit_binop_node(node)
529
530    def visit_BinopNode(self, node):
531        return self._visit_binop_node(node)
532
533    def _visit_binop_node(self, node):
534        self._process_children(node)
535        # FIXME: could special case 'not_in'
536        special_method_name = find_special_method_for_binary_operator(node.operator)
537        if special_method_name:
538            operand1, operand2 = node.operand1, node.operand2
539            if special_method_name == '__contains__':
540                operand1, operand2 = operand2, operand1
541            elif special_method_name == '__div__':
542                if Future.division in self.current_env().global_scope().context.future_directives:
543                    special_method_name = '__truediv__'
544            obj_type = operand1.type
545            if obj_type.is_builtin_type:
546                type_name = obj_type.name
547            else:
548                type_name = "object"  # safety measure
549            node = self._dispatch_to_method_handler(
550                special_method_name, None, False, type_name,
551                node, None, [operand1, operand2], None)
552        return node
553
554    def visit_UnopNode(self, node):
555        self._process_children(node)
556        special_method_name = find_special_method_for_unary_operator(node.operator)
557        if special_method_name:
558            operand = node.operand
559            obj_type = operand.type
560            if obj_type.is_builtin_type:
561                type_name = obj_type.name
562            else:
563                type_name = "object"  # safety measure
564            node = self._dispatch_to_method_handler(
565                special_method_name, None, False, type_name,
566                node, None, [operand], None)
567        return node
568
569    ### dispatch to specific handlers
570
571    def _find_handler(self, match_name, has_kwargs):
572        call_type = has_kwargs and 'general' or 'simple'
573        handler = getattr(self, '_handle_%s_%s' % (call_type, match_name), None)
574        if handler is None:
575            handler = getattr(self, '_handle_any_%s' % match_name, None)
576        return handler
577
578    def _delegate_to_assigned_value(self, node, function, arg_list, kwargs):
579        assignment = function.cf_state[0]
580        value = assignment.rhs
581        if value.is_name:
582            if not value.entry or len(value.entry.cf_assignments) > 1:
583                # the variable might have been reassigned => play safe
584                return node
585        elif value.is_attribute and value.obj.is_name:
586            if not value.obj.entry or len(value.obj.entry.cf_assignments) > 1:
587                # the underlying variable might have been reassigned => play safe
588                return node
589        else:
590            return node
591        return self._dispatch_to_handler(
592            node, value, arg_list, kwargs)
593
594    def _dispatch_to_handler(self, node, function, arg_list, kwargs):
595        if function.is_name:
596            # we only consider functions that are either builtin
597            # Python functions or builtins that were already replaced
598            # into a C function call (defined in the builtin scope)
599            if not function.entry:
600                return node
601            entry = function.entry
602            is_builtin = (
603                entry.is_builtin or
604                entry is self.current_env().builtin_scope().lookup_here(function.name))
605            if not is_builtin:
606                if function.cf_state and function.cf_state.is_single:
607                    # we know the value of the variable
608                    # => see if it's usable instead
609                    return self._delegate_to_assigned_value(
610                        node, function, arg_list, kwargs)
611                if arg_list and entry.is_cmethod and entry.scope and entry.scope.parent_type.is_builtin_type:
612                    if entry.scope.parent_type is arg_list[0].type:
613                        # Optimised (unbound) method of a builtin type => try to "de-optimise".
614                        return self._dispatch_to_method_handler(
615                            entry.name, self_arg=None, is_unbound_method=True,
616                            type_name=entry.scope.parent_type.name,
617                            node=node, function=function, arg_list=arg_list, kwargs=kwargs)
618                return node
619            function_handler = self._find_handler(
620                "function_%s" % function.name, kwargs)
621            if function_handler is None:
622                return self._handle_function(node, function.name, function, arg_list, kwargs)
623            if kwargs:
624                return function_handler(node, function, arg_list, kwargs)
625            else:
626                return function_handler(node, function, arg_list)
627        elif function.is_attribute:
628            attr_name = function.attribute
629            if function.type.is_pyobject:
630                self_arg = function.obj
631            elif node.self and function.entry:
632                entry = function.entry.as_variable
633                if not entry or not entry.is_builtin:
634                    return node
635                # C implementation of a Python builtin method - see if we find further matches
636                self_arg = node.self
637                arg_list = arg_list[1:]  # drop CloneNode of self argument
638            else:
639                return node
640            obj_type = self_arg.type
641            is_unbound_method = False
642            if obj_type.is_builtin_type:
643                if obj_type is Builtin.type_type and self_arg.is_name and arg_list and arg_list[0].type.is_pyobject:
644                    # calling an unbound method like 'list.append(L,x)'
645                    # (ignoring 'type.mro()' here ...)
646                    type_name = self_arg.name
647                    self_arg = None
648                    is_unbound_method = True
649                else:
650                    type_name = obj_type.name
651            else:
652                type_name = "object"  # safety measure
653            return self._dispatch_to_method_handler(
654                attr_name, self_arg, is_unbound_method, type_name,
655                node, function, arg_list, kwargs)
656        else:
657            return node
658
659    def _dispatch_to_method_handler(self, attr_name, self_arg,
660                                    is_unbound_method, type_name,
661                                    node, function, arg_list, kwargs):
662        method_handler = self._find_handler(
663            "method_%s_%s" % (type_name, attr_name), kwargs)
664        if method_handler is None:
665            if (attr_name in TypeSlots.method_name_to_slot
666                    or attr_name == '__new__'):
667                method_handler = self._find_handler(
668                    "slot%s" % attr_name, kwargs)
669            if method_handler is None:
670                return self._handle_method(
671                    node, type_name, attr_name, function,
672                    arg_list, is_unbound_method, kwargs)
673        if self_arg is not None:
674            arg_list = [self_arg] + list(arg_list)
675        if kwargs:
676            result = method_handler(
677                node, function, arg_list, is_unbound_method, kwargs)
678        else:
679            result = method_handler(
680                node, function, arg_list, is_unbound_method)
681        return result
682
683    def _handle_function(self, node, function_name, function, arg_list, kwargs):
684        """Fallback handler"""
685        return node
686
687    def _handle_method(self, node, type_name, attr_name, function,
688                       arg_list, is_unbound_method, kwargs):
689        """Fallback handler"""
690        return node
691
692
693class RecursiveNodeReplacer(VisitorTransform):
694    """
695    Recursively replace all occurrences of a node in a subtree by
696    another node.
697    """
698    def __init__(self, orig_node, new_node):
699        super(RecursiveNodeReplacer, self).__init__()
700        self.orig_node, self.new_node = orig_node, new_node
701
702    def visit_CloneNode(self, node):
703        if node is self.orig_node:
704            return self.new_node
705        if node.arg is self.orig_node:
706            node.arg = self.new_node
707        return node
708
709    def visit_Node(self, node):
710        self._process_children(node)
711        if node is self.orig_node:
712            return self.new_node
713        else:
714            return node
715
716def recursively_replace_node(tree, old_node, new_node):
717    replace_in = RecursiveNodeReplacer(old_node, new_node)
718    replace_in(tree)
719
720
721class NodeFinder(TreeVisitor):
722    """
723    Find out if a node appears in a subtree.
724    """
725    def __init__(self, node):
726        super(NodeFinder, self).__init__()
727        self.node = node
728        self.found = False
729
730    def visit_Node(self, node):
731        if self.found:
732            pass  # short-circuit
733        elif node is self.node:
734            self.found = True
735        else:
736            self._visitchildren(node, None)
737
738def tree_contains(tree, node):
739    finder = NodeFinder(node)
740    finder.visit(tree)
741    return finder.found
742
743
744# Utils
745def replace_node(ptr, value):
746    """Replaces a node. ptr is of the form used on the access path stack
747    (parent, attrname, listidx|None)
748    """
749    parent, attrname, listidx = ptr
750    if listidx is None:
751        setattr(parent, attrname, value)
752    else:
753        getattr(parent, attrname)[listidx] = value
754
755
756class PrintTree(TreeVisitor):
757    """Prints a representation of the tree to standard output.
758    Subclass and override repr_of to provide more information
759    about nodes. """
760    def __init__(self, start=None, end=None):
761        TreeVisitor.__init__(self)
762        self._indent = ""
763        if start is not None or end is not None:
764            self._line_range = (start or 0, end or 2**30)
765        else:
766            self._line_range = None
767
768    def indent(self):
769        self._indent += "  "
770
771    def unindent(self):
772        self._indent = self._indent[:-2]
773
774    def __call__(self, tree, phase=None):
775        print("Parse tree dump at phase '%s'" % phase)
776        self.visit(tree)
777        return tree
778
779    # Don't do anything about process_list, the defaults gives
780    # nice-looking name[idx] nodes which will visually appear
781    # under the parent-node, not displaying the list itself in
782    # the hierarchy.
783    def visit_Node(self, node):
784        self._print_node(node)
785        self.indent()
786        self.visitchildren(node)
787        self.unindent()
788        return node
789
790    def visit_CloneNode(self, node):
791        self._print_node(node)
792        self.indent()
793        line = node.pos[1]
794        if self._line_range is None or self._line_range[0] <= line <= self._line_range[1]:
795            print("%s- %s: %s" % (self._indent, 'arg', self.repr_of(node.arg)))
796        self.indent()
797        self.visitchildren(node.arg)
798        self.unindent()
799        self.unindent()
800        return node
801
802    def _print_node(self, node):
803        line = node.pos[1]
804        if self._line_range is None or self._line_range[0] <= line <= self._line_range[1]:
805            if len(self.access_path) == 0:
806                name = "(root)"
807            else:
808                parent, attr, idx = self.access_path[-1]
809                if idx is not None:
810                    name = "%s[%d]" % (attr, idx)
811                else:
812                    name = attr
813            print("%s- %s: %s" % (self._indent, name, self.repr_of(node)))
814
815    def repr_of(self, node):
816        if node is None:
817            return "(none)"
818        else:
819            result = node.__class__.__name__
820            if isinstance(node, ExprNodes.NameNode):
821                result += "(type=%s, name=\"%s\")" % (repr(node.type), node.name)
822            elif isinstance(node, Nodes.DefNode):
823                result += "(name=\"%s\")" % node.name
824            elif isinstance(node, ExprNodes.ExprNode):
825                t = node.type
826                result += "(type=%s)" % repr(t)
827            elif node.pos:
828                pos = node.pos
829                path = pos[0].get_description()
830                if '/' in path:
831                    path = path.split('/')[-1]
832                if '\\' in path:
833                    path = path.split('\\')[-1]
834                result += "(pos=(%s:%s:%s))" % (path, pos[1], pos[2])
835
836            return result
837
838if __name__ == "__main__":
839    import doctest
840    doctest.testmod()
841