| # 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 reggen.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 |