# 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