1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4#
5# Copyright 2017, Data61
6# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
7# ABN 41 687 119 230.
8#
9# This software may be distributed and modified according to the terms of
10# the BSD 2-Clause license. Note that NO WARRANTY is provided.
11# See "LICENSE_BSD2.txt" for details.
12#
13# @TAG(DATA61_BSD)
14#
15
16'''
17Stage 7 parser. The following parser is designed to accept a stage 6 parser,
18whose output it consumes. This parser's purpose is to resolve hierarchical
19systems into a flat, top-level assembly.
20'''
21
22from __future__ import absolute_import, division, print_function, \
23    unicode_literals
24from camkes.internal.seven import cmp, filter, map, zip
25
26from .base import Transformer
27from .exception import ParseError
28from camkes.ast import Assembly, ASTObject, AttributeReference, \
29    Component, Composition, Configuration, Consumes, Dataport, Emits, \
30    Instance, Interface, Provides, Setting, Uses
31import copy, six
32
33# The pre-condition of this stage is simply the post-condition of the previous
34# stage; that only a single assembly remains.
35from .stage6 import postcondition as precondition
36
37class InterfacePointer(object):
38    '''
39    A representation of a connection end that we will potentially relabel. See
40    usage of this below.
41    '''
42    def __init__(self, instance, interface):
43        assert isinstance(instance, Instance)
44        assert isinstance(interface, Interface)
45        self.instance = instance
46        self.interface = interface
47
48    # We need to implement the following methods to allow `InterfacePointer`s
49    # to be stored sensibly in dicts. Note that we just implement equality
50    # based on memory addresses because we know precisely how this will be
51    # used.
52    def __hash__(self):
53        return hash(self.instance) ^ hash(self.interface)
54    def __eq__(self, other):
55        if not isinstance(other, InterfacePointer):
56            return False
57        return self.instance is other.instance and \
58               self.interface is other.interface
59    def __ne__(self, other):
60        return not self.__eq__(other)
61
62def derive(obj, namespace):
63    '''
64    Make a copy of the given object, mangling the new object's name such that
65    it will not conflict with existing objects.
66    '''
67    assert isinstance(obj, ASTObject)
68    assert namespace is None or isinstance(namespace, six.string_types)
69    if namespace is None:
70        # No replication necessary.
71        return obj
72    new = copy.copy(obj)
73    if isinstance(new, Setting):
74        new.instance = '%s.%s' % (namespace, new.instance)
75        if isinstance(new.value, AttributeReference):
76            new.value = copy.copy(new.value)
77            new.value.reference = '%s.%s' % (namespace, new.value.reference)
78    else:
79        new.name = '%s.%s' % (namespace, new.name)
80        # If this is a component instance, we need to name-mangle its address
81        # space as well. If we don't do this, their address space (custom or
82        # implicit) can collide with another entity in the hierarchy and users'
83        # components can accidentally be combined into a single address space.
84        if isinstance(new, Instance):
85            new.address_space = '%s.%s' % (namespace, new.address_space)
86    return new
87
88def infer_all(item, parent=None):
89    '''
90    Infer all relevant objects that are direct or indirect children of this
91    item. We do this by recursively "hoisting" things from nested compositions
92    and configurations. Though the lifted AST has a built-in traversal
93    mechanism, we do not use it here because we want to only visit certain
94    entities and we want to propagate learned information upwards.
95    '''
96    assert isinstance(item, (Assembly, Component))
97    assert parent is None or isinstance(parent, Instance)
98
99    # A prefix we'll use to name-mangle children of the current item.
100    if parent is None:
101        prefix = None
102    else:
103        prefix = parent.name
104
105    # Below we'll discover and then derive two types of instances: our
106    # immediate children and indirect children, which will be accumulated in
107    # these lists, respectively. We use two separate lists because we want to
108    # maintain the ordering of immediate children first.
109    immediate_instances = []
110    child_instances = []
111
112    # Indirect settings we'll accumulate.
113    child_settings = []
114
115    # Final connections and settings we'll return.
116    connections = []
117    final_settings = []
118
119    # In the context of connections, we may need to adjust their ends based on
120    # export statements. Rather than trying to do this as we go, we just track
121    # which interfaces are just aliases for others. These will be eventually
122    # used by our outermost caller.
123    aliases = {}
124
125    if item.composition is not None:
126
127        # As we go through the AST, we'll derive new instances. These will have
128        # new names (and addresses), but will potentially be referred to later
129        # under their old pointers. We track this derivation in a mapping from
130        # old instances to new instances in order to adjust these references.
131        derived = {}
132
133        # We'll accumulate indirect child connections here.
134        child_connections = []
135
136        for i in item.composition.instances:
137            assert i.name is not None, 'unnamed instance in AST (bug in ' \
138                'stage 3 parser?)'
139
140            # Hoist everything from this instance.
141            insts, conns, alias, settings = infer_all(i.type, i)
142
143            # Name-mangle the instances.
144            for i2 in insts:
145                n = derive(i2, prefix)
146                child_instances.append(n)
147                derived[i2] = n
148            n = derive(i, prefix)
149            immediate_instances.append(n)
150            derived[i] = n
151
152            child_connections.extend(conns)
153
154            # Adjust the connection aliases for the name-mangling we just
155            # performed.
156            for k, v in alias.items():
157                assert k.instance in derived
158                assert v.instance in derived
159                k = InterfacePointer(derived[k.instance], k.interface)
160                v = InterfacePointer(derived[v.instance], v.interface)
161                aliases[k] = v
162
163            child_settings.extend(settings)
164
165        for c in item.composition.connections + child_connections:
166
167            # Derive and then re-adjust the ends of each connection.
168            n = derive(c, prefix)
169
170            from_ends = []
171            for f in n.from_ends:
172                e = copy.copy(f)
173                if e.instance is None:
174                    if isinstance(item, Assembly):
175                        raise ParseError('top-level connection end with no '
176                            'instance', e.location)
177                    e.instance = parent
178                else:
179                    assert e.instance in derived
180                    e.instance = derived[e.instance]
181                from_ends.append(e)
182            n.from_ends = from_ends
183
184            to_ends = []
185            for t in n.to_ends:
186                e = copy.copy(t)
187                if e.instance is None:
188                    if isinstance(item, Assembly):
189                        raise ParseError('top-level connection end with no '
190                            'instance', e.location)
191                    e.instance = parent
192                else:
193                    assert e.instance in derived
194                    e.instance = derived[e.instance]
195                to_ends.append(e)
196            n.to_ends = to_ends
197
198            n.claim_children()
199            connections.append(n)
200
201        assert len(item.composition.exports) == 0 or not \
202            isinstance(item, Assembly), 'export statement in assembly block ' \
203            '(bug in stage 4 parser?)'
204
205        # Accrue any new aliases we have.
206        for e in item.composition.exports:
207            p = InterfacePointer(parent, e.destination)
208            assert e.source_instance in derived
209            d = InterfacePointer(derived[e.source_instance], e.source_interface)
210            aliases[p] = d
211
212    # Accrue any settings we have.
213    for s in (item.configuration.settings if item.configuration is not None
214            else []) + child_settings:
215        n = derive(s, prefix)
216        final_settings.append(n)
217
218    return immediate_instances + child_instances, connections, aliases, \
219        final_settings
220
221class Parse7(Transformer):
222    def precondition(self, ast_lifted, _):
223        return precondition(ast_lifted)
224
225    def postcondition(self, ast_lifted, _):
226        '''
227        There is no natural post-condition for this transformation because the
228        action taken has been to augment the top-level assembly with
229        information that still remains in the AST. This could be formulated
230        with an expensive and complex traversal, but it is not worth it.
231        '''
232        return True
233
234    def transform(self, ast_lifted, read):
235        assembly = ast_lifted.assembly
236
237        # Hoist everything relevant from the assembly and its children.
238        instances, connections, aliases, settings = infer_all(assembly)
239
240        assembly.composition.instances = instances
241
242        # Replace the connections. Now we take into account the interface
243        # aliases.
244        assembly.composition.connections = []
245        for c in connections:
246            for e in c.from_ends + c.to_ends:
247                p = InterfacePointer(e.instance, e.interface)
248                while p in aliases:
249                    p = aliases[p]
250                e.instance = p.instance
251                e.interface = p.interface
252            assembly.composition.connections.append(c)
253
254        # Replace the settings.
255        assembly.configuration.settings = settings
256        assembly.claim_children()
257        return ast_lifted, read
258