| # Copyright lowRISC contributors. | 
 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. | 
 | # SPDX-License-Identifier: Apache-2.0 | 
 |  | 
 | import re | 
 | from collections.abc import MutableMapping | 
 | from typing import Dict, Iterator, List, Optional, Tuple | 
 |  | 
 | from .lib import check_keys, check_str, check_int, check_bool, check_list | 
 |  | 
 | REQUIRED_FIELDS = { | 
 |     'name': ['s', "name of the item"], | 
 | } | 
 |  | 
 | OPTIONAL_FIELDS = { | 
 |     'desc': ['s', "description of the item"], | 
 |     'type': ['s', "item type. int by default"], | 
 |     'default': ['s', "item default value"], | 
 |     'local': ['pb', "to be localparam"], | 
 |     'expose': ['pb', "to be exposed to top"], | 
 |     'randcount': [ | 
 |         's', "number of bits to randomize in the parameter. 0 by default." | 
 |     ], | 
 |     'randtype': ['s', "type of randomization to perform. none by default"], | 
 | } | 
 |  | 
 |  | 
 | class BaseParam: | 
 |     def __init__(self, name: str, desc: Optional[str], param_type: str): | 
 |         self.name = name | 
 |         self.desc = desc | 
 |         self.param_type = param_type | 
 |  | 
 |     def apply_default(self, value: str) -> None: | 
 |         if self.param_type[:3] == 'int': | 
 |             check_int(value, | 
 |                       'default value for parameter {} ' | 
 |                       '(which has type {})' | 
 |                       .format(self.name, self.param_type)) | 
 |         self.default = value | 
 |  | 
 |     def as_dict(self) -> Dict[str, object]: | 
 |         rd = {}  # type: Dict[str, object] | 
 |         rd['name'] = self.name | 
 |         if self.desc is not None: | 
 |             rd['desc'] = self.desc | 
 |         rd['type'] = self.param_type | 
 |         return rd | 
 |  | 
 |  | 
 | class LocalParam(BaseParam): | 
 |     def __init__(self, | 
 |                  name: str, | 
 |                  desc: Optional[str], | 
 |                  param_type: str, | 
 |                  value: str): | 
 |         super().__init__(name, desc, param_type) | 
 |         self.value = value | 
 |  | 
 |     def expand_value(self, when: str) -> int: | 
 |         try: | 
 |             return int(self.value, 0) | 
 |         except ValueError: | 
 |             raise ValueError("When {}, the {} value expanded as " | 
 |                              "{}, which doesn't parse as an integer." | 
 |                              .format(when, self.name, self.value)) from None | 
 |  | 
 |     def as_dict(self) -> Dict[str, object]: | 
 |         rd = super().as_dict() | 
 |         rd['local'] = True | 
 |         rd['default'] = self.value | 
 |         return rd | 
 |  | 
 |  | 
 | class Parameter(BaseParam): | 
 |     def __init__(self, | 
 |                  name: str, | 
 |                  desc: Optional[str], | 
 |                  param_type: str, | 
 |                  default: str, | 
 |                  expose: bool): | 
 |         super().__init__(name, desc, param_type) | 
 |         self.default = default | 
 |         self.expose = expose | 
 |  | 
 |     def as_dict(self) -> Dict[str, object]: | 
 |         rd = super().as_dict() | 
 |         rd['default'] = self.default | 
 |         rd['expose'] = 'true' if self.expose else 'false' | 
 |         return rd | 
 |  | 
 |  | 
 | class RandParameter(BaseParam): | 
 |     def __init__(self, | 
 |                  name: str, | 
 |                  desc: Optional[str], | 
 |                  param_type: str, | 
 |                  randcount: int, | 
 |                  randtype: str): | 
 |         assert randcount > 0 | 
 |         assert randtype in ['perm', 'data'] | 
 |         super().__init__(name, desc, param_type) | 
 |         self.randcount = randcount | 
 |         self.randtype = randtype | 
 |  | 
 |     def apply_default(self, value: str) -> None: | 
 |         raise ValueError('Cannot apply a default value of {!r} to ' | 
 |                          'parameter {}: it is a random netlist constant.' | 
 |                          .format(self.name, value)) | 
 |  | 
 |     def as_dict(self) -> Dict[str, object]: | 
 |         rd = super().as_dict() | 
 |         rd['randcount'] = self.randcount | 
 |         rd['randtype'] = self.randtype | 
 |         return rd | 
 |  | 
 |  | 
 | class MemSizeParameter(BaseParam): | 
 |     def __init__(self, | 
 |                  name: str, | 
 |                  desc: Optional[str], | 
 |                  param_type: str): | 
 |         super().__init__(name, desc, param_type) | 
 |  | 
 |  | 
 | def _parse_parameter(where: str, raw: object) -> BaseParam: | 
 |     rd = check_keys(raw, where, | 
 |                     list(REQUIRED_FIELDS.keys()), | 
 |                     list(OPTIONAL_FIELDS.keys())) | 
 |  | 
 |     # TODO: Check if PascalCase or ALL_CAPS | 
 |     name = check_str(rd['name'], 'name field of ' + where) | 
 |  | 
 |     r_desc = rd.get('desc') | 
 |     if r_desc is None: | 
 |         desc = None | 
 |     else: | 
 |         desc = check_str(r_desc, 'desc field of ' + where) | 
 |  | 
 |     # TODO: We should probably check that any register called RndCnstFoo has | 
 |     #       randtype and randcount. | 
 |     if name.lower().startswith('rndcnst') and 'randtype' in rd: | 
 |         # This is a random netlist constant and should be parsed as a | 
 |         # RandParameter. | 
 |         randtype = check_str(rd.get('randtype', 'none'), | 
 |                              'randtype field of ' + where) | 
 |         if randtype not in ['perm', 'data']: | 
 |             raise ValueError('At {}, parameter {} has a name that implies it ' | 
 |                              'is a random netlist constant, which means it ' | 
 |                              'must specify a randtype of "perm" or "data", ' | 
 |                              'rather than {!r}.' | 
 |                              .format(where, name, randtype)) | 
 |  | 
 |         r_randcount = rd.get('randcount') | 
 |         if r_randcount is None: | 
 |             raise ValueError('At {}, the random netlist constant {} has no ' | 
 |                              'randcount field.' | 
 |                              .format(where, name)) | 
 |         randcount = check_int(r_randcount, 'randcount field of ' + where) | 
 |         if randcount <= 0: | 
 |             raise ValueError('At {}, the random netlist constant {} has a ' | 
 |                              'randcount of {}, which is not positive.' | 
 |                              .format(where, name, randcount)) | 
 |  | 
 |         r_type = rd.get('type') | 
 |         if r_type is None: | 
 |             raise ValueError('At {}, parameter {} has no type field (which is ' | 
 |                              'required for random netlist constants).' | 
 |                              .format(where, name)) | 
 |         param_type = check_str(r_type, 'type field of ' + where) | 
 |  | 
 |         local = check_bool(rd.get('local', 'false'), 'local field of ' + where) | 
 |         if local: | 
 |             raise ValueError('At {}, the parameter {} specifies local = true, ' | 
 |                              'meaning that it is a localparam. This is ' | 
 |                              'incompatible with being a random netlist ' | 
 |                              'constant (how would it be set?)' | 
 |                              .format(where, name)) | 
 |  | 
 |         r_default = rd.get('default') | 
 |         if r_default is not None: | 
 |             raise ValueError('At {}, the parameter {} specifies a value for ' | 
 |                              'the "default" field. This is incompatible with ' | 
 |                              'being a random netlist constant: the value will ' | 
 |                              'be set by the random generator.' | 
 |                              .format(where, name)) | 
 |  | 
 |         expose = check_bool(rd.get('expose', 'false'), | 
 |                             'expose field of ' + where) | 
 |         if expose: | 
 |             raise ValueError('At {}, the parameter {} specifies expose = ' | 
 |                              'true, meaning that the parameter is exposed to ' | 
 |                              'the top-level. This is incompatible with being ' | 
 |                              'a random netlist constant.' | 
 |                              .format(where, name)) | 
 |  | 
 |         return RandParameter(name, desc, param_type, randcount, randtype) | 
 |  | 
 |     # This doesn't have a name like a random netlist constant. Check that it | 
 |     # doesn't define randcount or randtype. | 
 |     for fld in ['randcount', 'randtype']: | 
 |         if fld in rd: | 
 |             raise ValueError("At {where}, the parameter {name} specifies " | 
 |                              "{fld} but the name doesn't look like a random " | 
 |                              "netlist constant. To use {fld}, prefix the name " | 
 |                              "with RndCnst." | 
 |                              .format(where=where, name=name, fld=fld)) | 
 |  | 
 |     if name.lower().startswith('memsize'): | 
 |         r_type = rd.get('type') | 
 |         if r_type is None: | 
 |             raise ValueError('At {}, parameter {} has no type field (which is ' | 
 |                              'required for memory size parameters).' | 
 |                              .format(where, name)) | 
 |         param_type = check_str(r_type, 'type field of ' + where) | 
 |  | 
 |         if rd.get('type') != "int": | 
 |             raise ValueError('At {}, memory size parameter {} must be of type integer.' | 
 |                              .format(where, name)) | 
 |  | 
 |         local = check_bool(rd.get('local', 'false'), 'local field of ' + where) | 
 |         if local: | 
 |             raise ValueError('At {}, the parameter {} specifies local = true, ' | 
 |                              'meaning that it is a localparam. This is ' | 
 |                              'incompatible with being a memory size parameter.' | 
 |                              .format(where, name)) | 
 |  | 
 |         expose = check_bool(rd.get('expose', 'false'), | 
 |                             'expose field of ' + where) | 
 |         if expose: | 
 |             raise ValueError('At {}, the parameter {} specifies expose = ' | 
 |                              'true, meaning that the parameter is exposed to ' | 
 |                              'the top-level. This is incompatible with ' | 
 |                              'being a memory size parameter.' | 
 |                              .format(where, name)) | 
 |  | 
 |         return MemSizeParameter(name, desc, param_type) | 
 |  | 
 |     r_type = rd.get('type') | 
 |     if r_type is None: | 
 |         param_type = 'int' | 
 |     else: | 
 |         param_type = check_str(r_type, 'type field of ' + where) | 
 |  | 
 |     local = check_bool(rd.get('local', 'true'), 'local field of ' + where) | 
 |     expose = check_bool(rd.get('expose', 'false'), 'expose field of ' + where) | 
 |  | 
 |     r_default = rd.get('default') | 
 |     if r_default is None: | 
 |         raise ValueError('At {}, the {} param has no default field.' | 
 |                          .format(where, name)) | 
 |     else: | 
 |         default = check_str(r_default, 'default field of ' + where) | 
 |         if param_type[:3] == 'int': | 
 |             check_int(default, | 
 |                       'default field of {}, (an integer parameter)' | 
 |                       .format(name)) | 
 |  | 
 |     if local: | 
 |         if expose: | 
 |             raise ValueError('At {}, the localparam {} cannot be exposed to ' | 
 |                              'the top-level.' | 
 |                              .format(where, name)) | 
 |         return LocalParam(name, desc, param_type, value=default) | 
 |     else: | 
 |         return Parameter(name, desc, param_type, default, expose) | 
 |  | 
 |  | 
 | # Note: With a modern enough Python, we'd like this to derive from | 
 | #       "MutableMapping[str, BaseParam]". Unfortunately, this doesn't work with | 
 | #       Python 3.6 (where collections.abc.MutableMapping isn't subscriptable). | 
 | #       So we derive from just "MutableMapping" and tell mypy not to worry | 
 | #       about it. | 
 | class Params(MutableMapping):  # type: ignore | 
 |     def __init__(self) -> None: | 
 |         self.by_name = {}  # type: Dict[str, BaseParam] | 
 |  | 
 |     def __getitem__(self, key: str) -> BaseParam: | 
 |         return self.by_name[key] | 
 |  | 
 |     def __delitem__(self, key: str) -> None: | 
 |         del self.by_name[key] | 
 |  | 
 |     def __setitem__(self, key: str, value: BaseParam) -> None: | 
 |         self.by_name[key] = value | 
 |  | 
 |     def __iter__(self) -> Iterator[str]: | 
 |         return iter(self.by_name) | 
 |  | 
 |     def __len__(self) -> int: | 
 |         return len(self.by_name) | 
 |  | 
 |     def __repr__(self) -> str: | 
 |         return f"{type(self).__name__}({self.by_name})" | 
 |  | 
 |     def add(self, param: BaseParam) -> None: | 
 |         assert param.name not in self.by_name | 
 |         self.by_name[param.name] = param | 
 |  | 
 |     def apply_defaults(self, defaults: List[Tuple[str, str]]) -> None: | 
 |         for idx, (key, value) in enumerate(defaults): | 
 |             param = self.by_name[key] | 
 |             if param is None: | 
 |                 raise KeyError('Cannot find parameter ' | 
 |                                '{} to set default value.' | 
 |                                .format(key)) | 
 |  | 
 |             param.apply_default(value) | 
 |  | 
 |     def _expand_one(self, value: str, when: str) -> int: | 
 |         # Check whether value is already an integer: if so, return that. | 
 |         try: | 
 |             return int(value, 0) | 
 |         except ValueError: | 
 |             pass | 
 |  | 
 |         param = self.by_name.get(value) | 
 |         if param is None: | 
 |             raise ValueError('Cannot find a parameter called {} when {}. ' | 
 |                              'Known parameters: {}.' | 
 |                              .format(value, | 
 |                                      when, | 
 |                                      ', '.join(self.by_name.keys()))) | 
 |  | 
 |         # Only allow localparams in the expansion (because otherwise we're at | 
 |         # the mercy of whatever instantiates the block). | 
 |         if not isinstance(param, LocalParam): | 
 |             raise ValueError("When {}, {} is a not a local parameter." | 
 |                              .format(when, value)) | 
 |  | 
 |         return param.expand_value(when) | 
 |  | 
 |     def expand(self, value: str, where: str) -> int: | 
 |         # Here, we want to support arithmetic expressions with + and -. We | 
 |         # don't support other operators, or parentheses (so can parse with just | 
 |         # a regex). | 
 |         # | 
 |         # Use re.split, capturing the operators. This turns e.g. "a + b-c" into | 
 |         # ['a ', '+', ' b', '-', 'c']. If there's a leading operator ("+a"), | 
 |         # the first element of the results is an empty string. This means | 
 |         # elements with odd positions are always operators and elements with | 
 |         # even positions are values. | 
 |         acc = 0 | 
 |         is_neg = False | 
 |  | 
 |         for idx, tok in enumerate(re.split(r'([+-])', value)): | 
 |             if idx == 0 and not tok: | 
 |                 continue | 
 |             if idx % 2: | 
 |                 is_neg = (tok == '-') | 
 |                 continue | 
 |  | 
 |             term = self._expand_one(tok.strip(), | 
 |                                     'expanding term {} of {}' | 
 |                                     .format(idx // 2, where)) | 
 |             acc += -term if is_neg else term | 
 |  | 
 |         return acc | 
 |  | 
 |     def as_dicts(self) -> List[Dict[str, object]]: | 
 |         return [p.as_dict() for p in self.by_name.values()] | 
 |  | 
 |  | 
 | class ReggenParams(Params): | 
 |     @staticmethod | 
 |     def from_raw(where: str, raw: object) -> 'ReggenParams': | 
 |         ret = ReggenParams() | 
 |         rl = check_list(raw, where) | 
 |         for idx, r_param in enumerate(rl): | 
 |             entry_where = 'entry {} in {}'.format(idx + 1, where) | 
 |             param = _parse_parameter(entry_where, r_param) | 
 |             if param.name in ret: | 
 |                 raise ValueError('At {}, found a duplicate parameter with ' | 
 |                                  'name {}.' | 
 |                                  .format(entry_where, param.name)) | 
 |             ret.add(param) | 
 |         return ret | 
 |  | 
 |     def get_localparams(self) -> List[LocalParam]: | 
 |         ret = [] | 
 |         for param in self.by_name.values(): | 
 |             if isinstance(param, LocalParam): | 
 |                 ret.append(param) | 
 |         return ret |