1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2017, Data61
5# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
6# ABN 41 687 119 230.
7#
8# This software may be distributed and modified according to the terms of
9# the BSD 2-Clause license. Note that NO WARRANTY is provided.
10# See "LICENSE_BSD2.txt" for details.
11#
12# @TAG(DATA61_BSD)
13#
14
15from __future__ import absolute_import, division, print_function, \
16    unicode_literals
17from camkes.internal.seven import cmp, filter, map, zip
18
19from camkes.internal.hash import camkes_hash
20from .exception import ASTError
21from .location import SourceLocation
22from .traversal import NullContext, TraversalAction, TraversalContext
23import abc, collections, six
24
25class ASTObject(six.with_metaclass(abc.ABCMeta, object)):
26
27    child_fields = ()
28
29    # Fields that should be ignored when calculating an object hash or
30    # performing comparisons. Inheriting classes should extend this if
31    # necessary.
32    no_hash = ('child_fields', '_frozen', '_location', 'no_hash', '_parent')
33
34    def __init__(self, location=None):
35        assert location is None or isinstance(location, SourceLocation)
36        self._frozen = False
37        self._location = location
38        self._parent = None
39
40    @property
41    def frozen(self):
42        return self._frozen
43    @frozen.setter
44    def frozen(self, value):
45        assert isinstance(value, bool)
46        if self._frozen and not value:
47            raise TypeError('you cannot unfreeze an AST object')
48        self._frozen = value
49
50    @property
51    def location(self):
52        return self._location
53    @location.setter
54    def location(self, value):
55        assert value is None or isinstance(value, SourceLocation)
56        if self.frozen:
57            raise TypeError('you cannot change the location of a frozen AST '
58                'object')
59        self._location = value
60
61    @property
62    def parent(self):
63        return self._parent
64    @parent.setter
65    def parent(self, value):
66        assert value is None or isinstance(value, ASTObject)
67        if self.frozen:
68            raise TypeError('you cannot change the parent of a frozen AST '
69                'object')
70        self._parent = value
71
72    def freeze(self):
73        if self.frozen:
74            return
75        for f in self.child_fields:
76            assert hasattr(self, f)
77            item = getattr(self, f)
78            if isinstance(item, (list, tuple)):
79                for i in item:
80                    i.freeze()
81            elif item is not None:
82                item.freeze()
83        self.frozen = True
84
85    @property
86    def filename(self):
87        if self.location is None:
88            return None
89        return self.location.filename
90
91    @property
92    def lineno(self):
93        if self.location is None:
94            return None
95        return self.location.lineno
96
97    @property
98    def children(self):
99        '''Returns the contained descendents of this object.'''
100        assert isinstance(self.child_fields, tuple), 'child_fields is not a ' \
101            'tuple; accidentally declared as a string?'
102        kids = []
103        for f in self.child_fields:
104            assert hasattr(self, f)
105            item = getattr(self, f)
106            if isinstance(item, (list, tuple)):
107                kids.extend(item)
108            else:
109                kids.append(item)
110        return kids
111
112    def adopt(self, child):
113        child.parent = self
114
115    def claim_children(self):
116        pass
117
118    def __cmp__(self, other):
119        if type(self) != type(other):
120            return cmp(str(type(self)), str(type(other)))
121
122        for f in (k for k in self.__dict__.keys() if k not in self.no_hash):
123            if not hasattr(other, f):
124                return 1
125            elif getattr(self, f) is getattr(other, f):
126                # PERF: Field `f` in both items references the exact same
127                # object. Skip the remainder of this iteration of the loop to
128                # avoid unnecessarily comparing an object with itself.
129                continue
130            elif getattr(self, f) != getattr(other, f):
131                if type(getattr(self, f)) != type(getattr(other, f)):
132                    return cmp(str(type(getattr(self, f))),
133                        str(type(getattr(other, f))))
134                elif isinstance(getattr(self, f), type):
135                    assert isinstance(getattr(other, f), type), 'incorrect ' \
136                        'control flow in __cmp__ (bug in AST base?)'
137                    return cmp(str(getattr(self, f)), str(getattr(other, f)))
138                return cmp(getattr(self, f), getattr(other, f))
139
140        return 0
141
142    def __hash__(self):
143        return camkes_hash((k, v) for k, v in self.__dict__.items()
144                if k not in self.no_hash)
145
146    # When comparing `ASTObject`s, we always want to invoke
147    # `ASTObject.__cmp__`, but unfortunately we inherit rich comparison methods
148    # from `object`. Override these here, to force `ASTObject.__cmp__`. Note
149    # that you cannot call `cmp` in any of the following functions or they will
150    # infinitely recurse.
151    def __lt__(self, other):
152        return self.__cmp__(other) < 0
153    def __le__(self, other):
154        return self.__cmp__(other) <= 0
155    def __eq__(self, other):
156        return self.__cmp__(other) == 0
157    def __ne__(self, other):
158        return self.__cmp__(other) != 0
159    def __gt__(self, other):
160        return self.__cmp__(other) > 0
161    def __ge__(self, other):
162        return self.__cmp__(other) >= 0
163
164    def preorder(self, f, context=None):
165        '''
166        Pre-order traversal. Note that, unlike the post-order traversal below,
167        this does *not* recurse into the children of nodes you replace. The
168        rationale for this is that there is no other way to indicate to the
169        traversal algorithm that you do not wish to recurse into the children
170        and, if you *do* want to recurse, you can accomplish this manually
171        yourself before returning the replacement.
172        '''
173        assert isinstance(f, TraversalAction)
174        assert context is None or isinstance(context, TraversalContext)
175        assert isinstance(self.child_fields, tuple), 'child_fields is not a ' \
176            'tuple; accidentally declared as a string?'
177        if context is None:
178            context = NullContext()
179        for field in self.child_fields:
180            assert hasattr(self, field)
181            item = getattr(self, field)
182            if isinstance(item, (list, tuple)):
183                for i in six.moves.range(len(item)):
184                    replacement = f(item[i])
185                    if replacement is item[i]:
186                        with context(f):
187                            replacement.preorder(f, context)
188                    else:
189                        getattr(self, field)[i] = replacement
190            else:
191                replacement = f(item)
192                if replacement is item and replacement is not None:
193                    with context(f):
194                        replacement.preorder(f, context)
195                elif replacement is not item:
196                    setattr(self, field, replacement)
197
198    def postorder(self, f, context=None):
199        assert isinstance(f, TraversalAction)
200        assert context is None or isinstance(context, TraversalContext)
201        assert isinstance(self.child_fields, tuple), 'child_fields is not a ' \
202            'tuple; accidentally declared as a string?'
203        if context is None:
204            context = NullContext()
205        for field in self.child_fields:
206            assert hasattr(self, field)
207            item = getattr(self, field)
208            if isinstance(item, (list, tuple)):
209                for i in six.moves.range(len(item)):
210                    with context(f):
211                        item[i].postorder(f, context)
212                    replacement = f(item[i])
213                    if replacement is not item[i]:
214                        getattr(self, field)[i] = replacement
215            else:
216                if item is not None:
217                    with context(f):
218                        item.postorder(f, context)
219                replacement = f(item)
220                if replacement is not item:
221                    setattr(self, field, replacement)
222
223    def label(self):
224        return None
225
226class MapLike(six.with_metaclass(abc.ABCMeta, ASTObject, collections.Mapping)):
227
228    no_hash = ASTObject.no_hash + ('_mapping',)
229
230    def __init__(self, location=None):
231        super(MapLike, self).__init__(location)
232        self._mapping = None
233
234    def freeze(self):
235        if self.frozen:
236            return
237        super(MapLike, self).freeze()
238        self._mapping = {}
239        def add(d, i):
240            duplicate = d.get(i.name)
241            if duplicate is not None:
242                raise ASTError('duplicate entity \'%s\' defined, '
243                    'collides with %s at %s:%s' % (i.name,
244                    type(duplicate).__name__, duplicate.filename,
245                    duplicate.lineno), i)
246            d[i.name] = i
247        for field in self.child_fields:
248            assert hasattr(self, field)
249            item = getattr(self, field)
250            if isinstance(item, (list, tuple)):
251                [add(self._mapping, x) for x in item
252                    if hasattr(x, 'name') and x.name is not None]
253            elif item is not None and hasattr(item, 'name') and \
254                    item.name is not None:
255                add(self._mapping, item)
256
257    def __getitem__(self, key):
258        assert self.frozen, 'dict access on non-frozen object'
259        return self._mapping[key]
260    def __iter__(self):
261        assert self.frozen, 'dict access on non-frozen object'
262        return iter(self._mapping)
263    def __len__(self):
264        assert self.frozen, 'dict access on non-frozen object'
265        return len(self._mapping)
266