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 4 parser. The following parser is designed to accept a stage 3 parser,
18whose output it consumes. Note that this parser, and all parsers from here on
19up consume and return the same type of AST representation. Parsers from this
20stage and beyond are defined by their postcondition, which should hold on its
21output in isolation and in conjunction with any subset of its previous parsers'
22postconditions.
23'''
24
25from __future__ import absolute_import, division, print_function, \
26    unicode_literals
27from camkes.internal.seven import cmp, filter, map, zip
28
29from .base import Transformer
30from camkes.ast import Assembly, ASTObject, Connection, Group, Instance, \
31    Interface, Reference, TraversalAction
32from .exception import ParseError
33from .scope import ForwardScopingContext, ScopingContext
34import itertools
35
36def precondition(ast_lifted):
37    '''
38    Precondition of this parser.
39
40    All items in the AST should have been lifted into `ASTObject`s.
41    '''
42    return all(x is None or isinstance(x, ASTObject) for x in ast_lifted)
43
44def postcondition(ast_lifted):
45    '''
46    Postcondition of the AST returned by the stage 4 parser. No references
47    should remain in the AST.
48    '''
49    return all(not isinstance(x, Reference) for x in ast_lifted)
50
51def resolve(ast_lifted, allow_forward=False):
52
53    class Resolver(TraversalAction):
54        def __init__(self, context, assembly_scope, allow_forward):
55            self.context = context
56            self.assembly_scope = assembly_scope
57            self.allow_forward = allow_forward
58            self.last_seen = None
59        def __call__(self, obj):
60            if obj is not None:
61                if isinstance(obj, Reference):
62                    # This loop is expected to exit on the first iteration in the
63                    # normal case where we find the referent. Note that we chain
64                    # in the assembly scope to allow cross-assembly references.
65                    for referent in itertools.chain(
66                            self.context.lookup(obj.name, obj.type),
67                            self.assembly_scope.lookup(obj.name, obj.type)):
68                        return referent
69                    else:
70                        # If forward references are allowed, let this resolution
71                        # failure silently pass. This is to support connections
72                        # that reference the interfaces of component instances
73                        # that have not yet been defined. A forward lookup of
74                        # such a thing fails at this point because the type of
75                        # the instance is still a reference. The extra pass
76                        # later will resolve these references.
77                        if allow_forward and (obj.type is Instance or \
78                                obj.type is Interface):
79                            return obj
80
81                        # Try to be helpful and look up symbols the user may have
82                        # meant.
83                        mistyped = ['%s:%s: %s of type %s' %
84                            (m.filename or '<unnamed>', m.lineno, m.name,
85                            type(m).__name__) for m in self.context.lookup(obj.name)]
86                        if len(mistyped) > 0:
87                            extra = '; entities of the same name but incorrect ' \
88                                'type: \n %s' % '\n '.join(mistyped)
89                        else:
90                            extra = ''
91
92                        raise ParseError('unknown reference to \'%s\'%s' %
93                            ('.'.join(obj.name), extra), obj.location)
94
95                elif not self.allow_forward:
96                    self.context.register(obj)
97
98                    # If this is a child of a top-level composition, add it to
99                    # the assembly scope. This permits (backwards) references
100                    # from one assembly block to another.
101                    if isinstance(obj, (Instance, Group, Connection)) and \
102                            obj.parent is not None and \
103                            isinstance(obj.parent.parent, Assembly):
104                        self.assembly_scope.register(obj)
105
106                # Update the last object we operated on. Note that this
107                # tracking is irrelevant unless we are handling forward
108                # references
109                self.last_seen = obj
110
111            return obj
112
113    assembly_scope = ScopingContext()
114    assembly_scope.open()
115
116    if allow_forward:
117        ctxt = ForwardScopingContext()
118        ctxt.open()
119
120        r = Resolver(ctxt, assembly_scope, allow_forward)
121        # Set up the last seen object such that all top-level objects are
122        # immediately registered when entering the `with` block below.
123        r.last_seen = ast_lifted
124
125        # Note everything in all assemblies. This is to support cross-assembly
126        # references.
127        for assembly in (x for x in ast_lifted.items if isinstance(x, Assembly)):
128            [assembly_scope.register(y) for y in assembly.composition.children
129                if y is not None and not isinstance(y, Reference)]
130
131        with ctxt(r):
132            ast_lifted.preorder(r, ctxt)
133
134        # We now need to do another pass through the AST to resolve connection
135        # ends that still contain references because their referent was hidden
136        # behind other references in the first pass.
137        scope = ScopingContext()
138        scope.open()
139        for assembly in (x for x in ast_lifted.items if isinstance(x, Assembly)):
140            [scope.register(y) for y in assembly.composition.children
141                if y is not None and not isinstance(y, Reference)]
142        for assembly in (x for x in ast_lifted.items if isinstance(x, Assembly)):
143            for c in assembly.composition.connections:
144                for end in c.from_ends + c.to_ends:
145                    if isinstance(end.instance, Reference):
146                        try:
147                            end.instance = next(scope.lookup(end.instance.name, end.instance.type))
148                        except StopIteration:
149                            raise ParseError('unknown reference to \'%s\'' %
150                                '.'.join(end.instance.name),
151                                end.instance.location)
152                    if isinstance(end.interface, Reference):
153                        try:
154                            end.interface = next(scope.lookup(end.interface.name, end.interface.type))
155                        except StopIteration:
156                            raise ParseError('unknown reference to \'%s\'' %
157                                '.'.join(end.interface.name),
158                                end.interface.location)
159
160        # With forward references in play, it is possible for the user to
161        # induce cycles in the AST. This will explode in interesting ways in
162        # later parsing stages, so we check here for cycles to prevent later
163        # stages having to cope with this possibility. Note that it is actually
164        # possible to create a cyclic AST programmatically even without forward
165        # references, but we assume developers will never do this.
166        check_acyclic(ast_lifted)
167
168        # At this point, it's possible that we overzealously let a resolution
169        # failure pass in the first pass that was not resolved or detected as
170        # an error in the follow on pass. Validate that we really have
171        # eradicated all references. Note that this deliberately comes after
172        # the acyclicity check to avoid an infinite loop on malformed
173        # specifications.
174        for obj in ast_lifted:
175            if isinstance(obj, Reference):
176                raise ParseError('unknown reference to \'%s\'' %
177                    '.'.join(obj.name), obj.location)
178
179    else:
180        ctxt = ScopingContext()
181        ctxt.open()
182
183        r = Resolver(ctxt, assembly_scope, allow_forward)
184
185        ast_lifted.postorder(r, ctxt)
186
187    return ast_lifted
188
189class Parse4(Transformer):
190    def __init__(self, subordinate_parser, allow_forward=False):
191        assert isinstance(allow_forward, bool)
192        super(Parse4, self).__init__(subordinate_parser)
193        self.allow_forward = allow_forward
194
195    def precondition(self, ast_lifted, _):
196        return precondition(ast_lifted)
197
198    def postcondition(self, ast_lifted, _):
199        return postcondition(ast_lifted)
200
201    def transform(self, ast_lifted, read):
202        return resolve(ast_lifted, self.allow_forward), read
203
204def check_acyclic(obj, path=None):
205    if path is None:
206        path = []
207
208    if any(x is obj for x in path):
209        raise ParseError('AST cycle involving entity %s' %
210            (obj.name if hasattr(obj, 'name') else '<unnamed>'), obj.location)
211
212    for c in (x for x in obj.children if x is not None):
213        check_acyclic(c, path + [obj])
214