|  | # Copyright lowRISC contributors. | 
|  | # Licensed under the Apache License, Version 2.0, see LICENSE for details. | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  |  | 
|  | '''A wrapper for loading hjson files as used by dvsim's FlowCfg''' | 
|  |  | 
|  | from utils import parse_hjson, subst_wildcards | 
|  |  | 
|  |  | 
|  | # A set of fields that can be overridden on the command line and shouldn't be | 
|  | # loaded from the hjson in that case. | 
|  | _CMDLINE_FIELDS = {'tool'} | 
|  |  | 
|  |  | 
|  | def load_hjson(path, initial_values): | 
|  | '''Load an hjson file and any includes | 
|  |  | 
|  | Combines them all into a single dictionary, which is then returned. This | 
|  | does wildcard substitution on include names (since it might be needed to | 
|  | find included files), but not otherwise. | 
|  |  | 
|  | initial_values is a starting point for the dictionary to be returned (which | 
|  | is not modified). It needs to contain values for anything needed to resolve | 
|  | include files (typically, this is 'proj_root' and 'tool' (if set)). | 
|  |  | 
|  | ''' | 
|  | worklist = [path] | 
|  | seen = {path} | 
|  | ret = initial_values.copy() | 
|  | is_first = True | 
|  |  | 
|  | # Figure out a list of fields that had a value from the command line. These | 
|  | # should have been passed in as part of initial_values and we need to know | 
|  | # that we can safely ignore updates. | 
|  | arg_keys = _CMDLINE_FIELDS & initial_values.keys() | 
|  |  | 
|  | while worklist: | 
|  | next_path = worklist.pop() | 
|  | new_paths = _load_single_file(ret, next_path, is_first, arg_keys) | 
|  | paths_seen = set(new_paths) & seen | 
|  | if paths_seen: | 
|  | raise RuntimeError('The files {!r} appears more than once ' | 
|  | 'when processing include {!r} for {!r}.' | 
|  | .format(list(paths_seen), next_path, path)) | 
|  | seen |= set(new_paths) | 
|  | worklist += new_paths | 
|  | is_first = False | 
|  |  | 
|  | return ret | 
|  |  | 
|  |  | 
|  | def _load_single_file(target, path, is_first, arg_keys): | 
|  | '''Load a single hjson file, merging its keys into target | 
|  |  | 
|  | Returns a list of further includes that should be loaded. | 
|  |  | 
|  | ''' | 
|  | hjson = parse_hjson(path) | 
|  | if not isinstance(hjson, dict): | 
|  | raise RuntimeError('{!r}: Top-level hjson object is not a dictionary.' | 
|  | .format(path)) | 
|  |  | 
|  | import_cfgs = [] | 
|  | for key, dict_val in hjson.items(): | 
|  | # If this key got set at the start of time and we want to ignore any | 
|  | # updates: ignore them! | 
|  | if key in arg_keys: | 
|  | continue | 
|  |  | 
|  | # If key is 'import_cfgs', this should be a list. Add each item to the | 
|  | # list of cfgs to process | 
|  | if key == 'import_cfgs': | 
|  | if not isinstance(dict_val, list): | 
|  | raise RuntimeError('{!r}: import_cfgs value is {!r}, but ' | 
|  | 'should be a list.' | 
|  | .format(path, dict_val)) | 
|  | import_cfgs += dict_val | 
|  | continue | 
|  |  | 
|  | # 'use_cfgs' is a bit like 'import_cfgs', but is only used for primary | 
|  | # config files (where it is a list of the child configs). This | 
|  | # shouldn't be used except at top-level (the first configuration file | 
|  | # to be loaded). | 
|  | # | 
|  | # If defined, check that it's a list, but then allow it to be set in | 
|  | # the target dictionary as usual. | 
|  | if key == 'use_cfgs': | 
|  | if not is_first: | 
|  | raise RuntimeError('{!r}: File is included by another one, ' | 
|  | 'but defines "use_cfgs".' | 
|  | .format(path)) | 
|  | if not isinstance(dict_val, list): | 
|  | raise RuntimeError('{!r}: use_cfgs must be a list. Saw {!r}.' | 
|  | .format(path, dict_val)) | 
|  |  | 
|  | # Otherwise, update target with this attribute | 
|  | set_target_attribute(path, target, key, dict_val) | 
|  |  | 
|  | # Expand the names of imported configuration files as we return them | 
|  | return [subst_wildcards(cfg_path, | 
|  | target, | 
|  | ignored_wildcards=[], | 
|  | ignore_error=False) | 
|  | for cfg_path in import_cfgs] | 
|  |  | 
|  |  | 
|  | def set_target_attribute(path, target, key, dict_val): | 
|  | '''Set an attribute on the target dictionary | 
|  |  | 
|  | This performs checks for conflicting values and merges lists / | 
|  | dictionaries. | 
|  |  | 
|  | ''' | 
|  | old_val = target.get(key) | 
|  | if old_val is None: | 
|  | # A new attribute (or the old value was None, in which case it's | 
|  | # just a placeholder and needs writing). Set it and return. | 
|  | target[key] = dict_val | 
|  | return | 
|  |  | 
|  | if isinstance(old_val, list): | 
|  | if not isinstance(dict_val, list): | 
|  | raise RuntimeError('{!r}: Conflicting types for key {!r}: was ' | 
|  | '{!r}, a list, but loaded value is {!r}, ' | 
|  | 'of type {}.' | 
|  | .format(path, key, old_val, dict_val, | 
|  | type(dict_val).__name__)) | 
|  |  | 
|  | # Lists are merged by concatenation | 
|  | target[key] += dict_val | 
|  | return | 
|  |  | 
|  | # The other types we support are "scalar" types. | 
|  | scalar_types = [(str, [""]), (int, [0, -1]), (bool, [False])] | 
|  | defaults = None | 
|  | for st_type, st_defaults in scalar_types: | 
|  | if isinstance(dict_val, st_type): | 
|  | defaults = st_defaults | 
|  | break | 
|  | if defaults is None: | 
|  | raise RuntimeError('{!r}: Value for key {!r} is {!r}, of ' | 
|  | 'unknown type {}.' | 
|  | .format(path, key, dict_val, | 
|  | type(dict_val).__name__)) | 
|  | if not isinstance(old_val, st_type): | 
|  | raise RuntimeError('{!r}: Value for key {!r} is {!r}, but ' | 
|  | 'we already had the value {!r}, of an ' | 
|  | 'incompatible type.' | 
|  | .format(path, key, dict_val, old_val)) | 
|  |  | 
|  | # The types are compatible. If the values are equal, there's nothing more | 
|  | # to do | 
|  | if old_val == dict_val: | 
|  | return | 
|  |  | 
|  | old_is_default = old_val in defaults | 
|  | new_is_default = dict_val in defaults | 
|  |  | 
|  | # Similarly, if new value looks like a default, ignore it (regardless | 
|  | # of whether the current value looks like a default). | 
|  | if new_is_default: | 
|  | return | 
|  |  | 
|  | # If the existing value looks like a default and the new value doesn't, | 
|  | # take the new value. | 
|  | if old_is_default: | 
|  | target[key] = dict_val | 
|  | return | 
|  |  | 
|  | # Neither value looks like a default. Raise an error. | 
|  | raise RuntimeError('{!r}: Value for key {!r} is {!r}, but ' | 
|  | 'we already had a conflicting value of {!r}.' | 
|  | .format(path, key, dict_val, old_val)) |