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