385 lines
14 KiB
Python
385 lines
14 KiB
Python
# 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
|