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 8 parser. The following parser is designed to accept a stage 7 parser,
18whose output it consumes. This parser's purpose is to resolve settings that
19reference other attributes.
20'''
21
22from __future__ import absolute_import, division, print_function, \
23    unicode_literals
24from camkes.internal.seven import cmp, filter, map, zip
25
26from camkes.ast import AttributeReference
27from .base import Transformer
28from .exception import ParseError
29import collections
30
31# Re-use the post-condition of the stage 6 parser as our pre-condition; that
32# only a single assembly remains.
33from .stage6 import postcondition as precondition
34
35def postcondition(ast_lifted):
36    '''
37    All settings are resolved.
38    '''
39    return all(not isinstance(x.value, AttributeReference) for x in
40        ast_lifted.assembly.configuration.settings)
41
42def resolve(ast_lifted):
43    '''
44    Recursively resolve all setting references to concrete values.
45
46    There's not a lot of type safety in this resolution process because the
47    attributes themselves inherently aren't strongly typed. We rely on the type
48    checking done when freeze is called on an assembly.
49    '''
50    to_resolve = []
51
52    assembly = ast_lifted.assembly
53    new_settings = []
54    for s in assembly.configuration.settings:
55        if isinstance(s.value, AttributeReference):
56            to_resolve.append(s)
57        else:
58            new_settings.append(s)
59
60    def sub_resolve(setting, depth):
61        '''
62        Recursive function for resolving setting references.  If a setting
63        is a reference to another setting we try and resolve the next one (depth first).  If there
64        is a circular reference we error out.  If any setting doesn't have
65        a setting to resolve to, but has a default attribute value we use that.
66        If there is no default and no setting then we 'forget' the alias so that we
67        can fall back to defaults that other attributes have.  An attribute that doesn't
68        get a setting or a default will not generate a symbol in the generated template.
69        Thus if the attribute requires a setting the code compiler should generate an error later.
70        '''
71
72        if setting.value.reference in depth:
73            errstring = ""
74            for value in depth:
75                errstring += value + "<-"
76            raise ParseError('Loop detected in attribute references: %s<-...'
77                    % (errstring+setting.value.reference), setting.location)
78        # TODO Refactor AttributeReference to handle namespacing better than a
79        # string containing '.' characters.
80        instance_name, attribute_name = setting.value.reference.rsplit('.', 1)
81        referents = [x for x in assembly.configuration.settings
82            if x.instance == instance_name and
83                x.attribute == attribute_name]
84        if len(referents) == 0:
85            # No existing settings for the attribute that our current attribute
86            # refers to.  Check if it has a default value and use that.
87            attribute = assembly.get_attribute(instance_name, attribute_name)
88            if attribute is not None and attribute.default is not None:
89                setting.value = attribute.default
90                return True
91
92            # If we didn't find a default, then try and use our own default if we have one
93            attribute = assembly.get_attribute(setting.instance, setting.attribute)
94            if attribute is not None and attribute.default is not None:
95                setting.value = attribute.default
96                return True
97
98            setting.value = None
99            return False
100
101        elif len(referents) > 1:
102            raise ParseError('setting refers to an attribute that '
103                'is set multiple times', setting.location)
104
105        if isinstance(referents[0].value, AttributeReference):
106            if not sub_resolve(referents[0], depth +  [setting.value.reference]):
107                setting.value = None
108                return False
109            setting.value = referents[0].value
110        elif setting.value.dict_lookup and isinstance(referents[0].value, dict):
111            value = referents[0].value
112            for key in setting.value.dict_lookup.lookup:
113                value = value[key]
114            setting.value = value
115        else:
116            setting.value = referents[0].value
117        return True
118
119    # Iterate through each setting we need to resolve
120    for setting in to_resolve:
121        if isinstance(setting.value, AttributeReference):
122            if sub_resolve(setting, []):
123                new_settings.append(setting)
124        elif setting.value != None:
125            # Already resolved
126            new_settings.append(setting)
127
128    assembly.configuration.settings = new_settings
129    assembly.claim_children()
130
131class Parse8(Transformer):
132    def precondition(self, ast_lifted, _):
133        return precondition(ast_lifted)
134
135    def postcondition(self, ast_lifted, _):
136        return postcondition(ast_lifted)
137
138    def transform(self, ast_lifted, read):
139        resolve(ast_lifted)
140        return ast_lifted, read
141