blob: 8411d68bba66d46ec2926455a7d11b1637168f37 [file] [log] [blame]
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +00001# Copyright lowRISC contributors.
2# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3# SPDX-License-Identifier: Apache-2.0
4
5from pathlib import Path
Philipp Wagnerb246f272021-09-27 17:53:22 +01006from typing import Any, Dict, Optional, Union
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +00007
8import hjson # type: ignore
9from reggen.lib import check_int, check_keys, check_list, check_name, check_str
10from reggen.params import BaseParam, Params
11
12
13class TemplateParseError(Exception):
14 pass
15
16
17class TemplateParameter(BaseParam):
18 """ A template parameter. """
Philipp Wagnerb246f272021-09-27 17:53:22 +010019 VALID_PARAM_TYPES = (
20 'int',
21 'string',
Philipp Wagnere1b13492021-09-27 18:22:48 +010022 'object',
Philipp Wagnerb246f272021-09-27 17:53:22 +010023 )
24
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000025 def __init__(self, name: str, desc: Optional[str], param_type: str,
26 default: str):
Philipp Wagnerb246f272021-09-27 17:53:22 +010027 assert param_type in self.VALID_PARAM_TYPES
28
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000029 super().__init__(name, desc, param_type)
30 self.default = default
31 self.value = None
32
33 def as_dict(self) -> Dict[str, object]:
34 rd = super().as_dict()
35 rd['default'] = self.default
36 return rd
37
38
39def _parse_template_parameter(where: str, raw: object) -> TemplateParameter:
40 rd = check_keys(raw, where, ['name', 'desc', 'type'], ['default'])
41
42 name = check_str(rd['name'], 'name field of ' + where)
43
44 r_desc = rd.get('desc')
45 if r_desc is None:
46 desc = None
47 else:
48 desc = check_str(r_desc, 'desc field of ' + where)
49
50 r_type = rd.get('type')
51 param_type = check_str(r_type, 'type field of ' + where)
Philipp Wagnerb246f272021-09-27 17:53:22 +010052 if param_type not in TemplateParameter.VALID_PARAM_TYPES:
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000053 raise ValueError('At {}, the {} param has an invalid type field {!r}. '
Philipp Wagnerb246f272021-09-27 17:53:22 +010054 'Allowed values are: {}.'.format(
55 where, name, param_type,
56 ', '.join(TemplateParameter.VALID_PARAM_TYPES)))
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000057
58 r_default = rd.get('default')
Philipp Wagnerb246f272021-09-27 17:53:22 +010059 if param_type == 'int':
60 default = check_int(
61 r_default,
62 'default field of {}, (an integer parameter)'.format(name))
63 elif param_type == 'string':
64 default = check_str(r_default, 'default field of ' + where)
Philipp Wagnere1b13492021-09-27 18:22:48 +010065 elif param_type == 'object':
66 default = IpConfig._check_object(r_default, 'default field of ' + where)
Philipp Wagnerb246f272021-09-27 17:53:22 +010067 else:
68 assert False, f"Unknown parameter type found: {param_type!r}"
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +000069
70 return TemplateParameter(name, desc, param_type, default)
71
72
73class TemplateParams(Params):
74 """ A group of template parameters. """
75 @classmethod
76 def from_raw(cls, where: str, raw: object) -> 'TemplateParams':
77 """ Produce a TemplateParams instance from an object as it is in Hjson.
78 """
79 ret = cls()
80 rl = check_list(raw, where)
81 for idx, r_param in enumerate(rl):
82 entry_where = 'entry {} in {}'.format(idx + 1, where)
83 param = _parse_template_parameter(entry_where, r_param)
84 if param.name in ret:
85 raise ValueError('At {}, found a duplicate parameter with '
86 'name {}.'.format(entry_where, param.name))
87 ret.add(param)
88 return ret
89
90
91class IpTemplate:
92 """ An IP template.
93
94 An IP template is an IP block which needs to be parametrized before it
95 can be transformed into an actual IP block (which can then be instantiated
96 in a hardware design).
97 """
98
99 name: str
Philipp Wagnerb246f272021-09-27 17:53:22 +0100100 params: TemplateParams
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000101 template_path: Path
102
Philipp Wagnerb246f272021-09-27 17:53:22 +0100103 def __init__(self, name: str, params: TemplateParams, template_path: Path):
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000104 self.name = name
105 self.params = params
106 self.template_path = template_path
107
108 @classmethod
109 def from_template_path(cls, template_path: Path) -> 'IpTemplate':
110 """ Create an IpTemplate from a template directory.
111
112 An IP template directory has a well-defined structure:
113
114 - The IP template name (TEMPLATE_NAME) is equal to the directory name.
115 - It contains a file 'data/TEMPLATE_NAME.tpldesc.hjson' containing all
116 configuration information related to the template.
117 - It contains zero or more files ending in '.tpl'. These files are
118 Mako templates and rendered into an file in the same location without
119 the '.tpl' file extension.
120 """
121
122 # Check if the directory structure matches expectations.
123 if not template_path.is_dir():
124 raise TemplateParseError(
125 "Template path {!r} is not a directory.".format(
126 str(template_path)))
127 if not (template_path / 'data').is_dir():
128 raise TemplateParseError(
129 "Template path {!r} does not contain the required 'data' directory."
130 .format(str(template_path)))
131
132 # The template name equals the name of the template directory.
133 template_name = template_path.stem
134
135 # Find the template description file.
136 tpldesc_file = template_path / 'data/{}.tpldesc.hjson'.format(
137 template_name)
138
139 # Read the template description from file.
140 try:
141 tpldesc_obj = hjson.load(open(tpldesc_file, 'r'), use_decimal=True)
142 except (OSError, FileNotFoundError) as e:
143 raise TemplateParseError(
144 "Unable to read template description file {!r}: {}".format(
145 str(tpldesc_file), str(e)))
146
147 # Parse the template description file.
148 where = 'template description file {!r}'.format(str(tpldesc_file))
149 if 'template_param_list' not in tpldesc_obj:
150 raise TemplateParseError(
151 f"Required key 'variables' not found in {where}")
152
153 try:
154 params = TemplateParams.from_raw(
155 f"list of parameters in {where}",
156 tpldesc_obj['template_param_list'])
157 except ValueError as e:
158 raise TemplateParseError(e) from None
159
160 return cls(template_name, params, template_path)
161
162
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000163class IpConfig:
164 def __init__(self,
Philipp Wagnerb246f272021-09-27 17:53:22 +0100165 template_params: TemplateParams,
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000166 instance_name: str,
167 param_values: Dict[str, Union[str, int]] = {}):
Philipp Wagnerb246f272021-09-27 17:53:22 +0100168 self.template_params = template_params
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000169 self.instance_name = instance_name
Philipp Wagnerb246f272021-09-27 17:53:22 +0100170 self.param_values = IpConfig._check_param_values(
171 template_params, param_values)
172
173 @staticmethod
Philipp Wagnere1b13492021-09-27 18:22:48 +0100174 def _check_object(obj: object, what: str) -> object:
175 """Check that obj is a Hjson-serializable object.
176
177 If not, raise a ValueError; the what argument names the object.
178
179 """
180 try:
181 # Round-trip objects through the JSON encoder to get the
182 # same representation no matter if we load the config from
183 # file, or directly pass it on to the template. Also, catch
184 # encoding/decoding errors when setting the object.
185 json = hjson.dumps(obj,
186 ensure_ascii=False,
187 use_decimal=True,
188 for_json=True,
189 encoding='UTF-8')
190 obj_checked = hjson.loads(json,
191 use_decimal=True,
192 encoding='UTF-8')
193 except TypeError as e:
194 raise ValueError('{} cannot be serialized as Hjson: {}'
195 .format(what, str(e))) from None
196 return obj_checked
197
198 @staticmethod
Philipp Wagnerb246f272021-09-27 17:53:22 +0100199 def _check_param_values(template_params: TemplateParams,
200 param_values: Any) -> Dict[str, Union[str, int]]:
201 """Check if parameter values are valid.
202
203 Returns the parameter values in typed form if successful, and throws
204 a ValueError otherwise.
205 """
206 param_values_typed = {}
207 for key, value in param_values.items():
208 if not isinstance(key, str):
209 raise ValueError(
210 f"The IP configuration has a key {key!r} which is not a "
211 "string.")
212
213 if key not in template_params:
214 raise ValueError(
215 f"The IP configuration has a key {key!r} which is a "
216 "valid parameter.")
217
218 if template_params[key].param_type == 'string':
219 param_value_typed = check_str(
220 value, f"the key {key} of the IP configuration")
221 elif template_params[key].param_type == 'int':
222 param_value_typed = check_int(
223 value, f"the key {key} of the IP configuration")
Philipp Wagnere1b13492021-09-27 18:22:48 +0100224 elif template_params[key].param_type == 'object':
225 param_value_typed = IpConfig._check_object(
226 value, f"the key {key} of the IP configuration")
Philipp Wagnerb246f272021-09-27 17:53:22 +0100227 else:
228 assert True, "Unexpeced parameter type found, expand this check"
229
230 param_values_typed[key] = param_value_typed
231
232 return param_values_typed
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000233
234 @classmethod
Philipp Wagnerb246f272021-09-27 17:53:22 +0100235 def from_raw(cls, template_params: TemplateParams, raw: object,
236 where: str) -> 'IpConfig':
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000237 """ Load an IpConfig from a raw object """
238
239 rd = check_keys(raw, 'configuration file ' + where, ['instance_name'],
240 ['param_values'])
241 instance_name = check_name(rd.get('instance_name'),
242 "the key 'instance_name' of " + where)
243
Philipp Wagnerb246f272021-09-27 17:53:22 +0100244 if not isinstance(raw, dict):
245 raise ValueError(
246 "The IP configuration is expected to be a dict, but was "
247 "actually a " + type(raw).__name__)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000248
Philipp Wagnerb246f272021-09-27 17:53:22 +0100249 param_values = IpConfig._check_param_values(template_params,
250 rd['param_values'])
251
252 return cls(template_params, instance_name, param_values)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000253
254 @classmethod
255 def from_text(cls, txt: str, where: str) -> 'IpConfig':
256 """Load an IpConfig from an Hjson description in txt"""
Philipp Wagnerd8c65ee2021-09-27 16:49:25 +0100257 return cls.from_raw(
258 hjson.loads(txt, use_decimal=True, encoding="UTF-8"), where)
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000259
260 def to_file(self, file_path: Path, header: Optional[str] = ""):
261 obj = {}
262 obj['instance_name'] = self.instance_name
Philipp Wagner934763c2021-09-27 18:04:15 +0100263 obj['param_values'] = self.param_values
Philipp Wagnerdcc5f9f2021-03-10 22:21:39 +0000264
265 with open(file_path, 'w') as fp:
266 if header:
267 fp.write(header)
268 hjson.dump(obj,
269 fp,
270 ensure_ascii=False,
271 use_decimal=True,
272 for_json=True,
273 encoding='UTF-8',
274 indent=2)
275 fp.write("\n")