blob: 3fee62588c912f97b331dd235dd7db4fafd091c6 [file] [log] [blame]
# 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))