Coverage for hdl_registers/parser/parser.py: 97%
230 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-28 20:51 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-28 20:51 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the hdl-registers project, an HDL register generator fast enough to run
5# in real time.
6# https://hdl-registers.com
7# https://github.com/hdl-registers/hdl-registers
8# --------------------------------------------------------------------------------------------------
10# Standard libraries
11import copy
12import json
13from pathlib import Path
14from typing import TYPE_CHECKING, Any, Optional
16# Third party libraries
17import tomli_w
18import yaml
19from tsfpga import DEFAULT_FILE_ENCODING
21# First party libraries
22from hdl_registers.about import WEBSITE_URL
23from hdl_registers.constant.bit_vector_constant import UnsignedVector
24from hdl_registers.register_list import RegisterList
25from hdl_registers.register_modes import REGISTER_MODES
27if TYPE_CHECKING:
28 # First party libraries
29 from hdl_registers.register import Register
30 from hdl_registers.register_mode import RegisterMode
33class RegisterParser:
34 """
35 Parse register data in the form of a dictionary into a :class:`.RegisterList` object.
36 See :ref:`toml_format` for further documentation.
38 A note on sanity check strategy:
39 The parser performs only the basic sanity checks related to the data file format.
40 For example, missing properties, unknown properties, etc.
41 A lot of other sanity checks are performed in the :class:`.Register`, :class:`.RegisterArray`,
42 :class:`.RegisterField`, etc, classes themselves.
44 For example, the default value of a bit field should be a string with the value "0" or "1".
45 This is checked in the constructor of the :class:`.Bit` class, not here in the parser.
46 Similar for a lot of other things.
48 This is because these objects can be created from the Python API also, without involving
49 the parser.
50 Hence these sanity checks have to be present there.
51 Having them also in the parser would enable better error messages, but would be redundant
52 and would slow down the parser.
53 Since the parser is run in real time, the performance is critical, and we can not afford
54 to slow it down.
55 """
57 # Attributes of the constant.
58 recognized_constant_items = {"type", "value", "description", "data_type"}
59 # Note that "type" being present is implied. We would not be parsing a constant unless we
60 # know it to be a "constant" type.
61 # So we save some CPU cycles by not checking for it.
62 required_constant_items = ["value"]
64 # Attributes of the register.
65 # Anything apart from these are names of fields.
66 default_register_items = {
67 "type",
68 "mode",
69 "description",
70 }
71 # While a 'mode' is required for a register, it may NOT be specified/changed in the data file
72 # for a default register.
73 # Hence this property is handled separately.
74 # And hence, registers have no required items.
76 # Attributes of the register array.
77 # Anything apart from these are names of registers.
78 default_register_array_items = {"type", "array_length", "description"}
79 # Note that "type" being present is implied.
80 # We would not be parsing a register array unless we know it to be a "register_array" type.
81 # So we save some CPU cycles by not checking for it.
82 required_register_array_items = ["array_length"]
84 # Attributes of the "bit" register field.
85 recognized_bit_items = {"type", "description", "default_value"}
86 # Note that "type" being present is implied.
87 # We would not be parsing a bit unless we know it to be a "bit" type.
88 # So we save some CPU cycles by not checking for it.
89 required_bit_items: list[str] = []
91 # Attributes of the "bit_vector" register field.
92 recognized_bit_vector_items = {"type", "description", "width", "default_value"}
93 # Note that "type" being present is implied.
94 # We would not be parsing a bit_vector unless we know it to be a "bit_vector" type.
95 # So we save some CPU cycles by not checking for it.
96 required_bit_vector_items = ["width"]
98 # Attributes of the "enumeration" register field.
99 recognized_enumeration_items = {"type", "description", "default_value", "element"}
100 # Note that "type" being present is implied.
101 # We would not be parsing a enumeration unless we know it to be a "enumeration" type.
102 # So we save some CPU cycles by not checking for it.
103 required_enumeration_items = ["element"]
105 # Attributes of the "integer" register field.
106 recognized_integer_items = {"type", "description", "min_value", "max_value", "default_value"}
107 # Note that "type" being present is implied.
108 # We would not be parsing a integer unless we know it to be a "integer" type.
109 # So we save some CPU cycles by not checking for it.
110 required_integer_items = ["max_value"]
112 def __init__(
113 self,
114 name: str,
115 source_definition_file: Path,
116 default_registers: Optional[list["Register"]] = None,
117 ):
118 """
119 Arguments:
120 name: The name of the register list.
121 source_definition_file: The source file that defined this register list.
122 Will be displayed in generated source code and documentation
123 for traceability.
124 default_registers: List of default registers.
125 Note that this list with :class:`.Register` objects will be deep copied, so you can
126 use the same list many times without worrying about mutability.
127 """
128 self._register_list = RegisterList(name=name, source_definition_file=source_definition_file)
129 self._source_definition_file = source_definition_file
131 self._default_register_names = []
132 if default_registers:
133 # Perform deep copy of the mutable register objects.
134 # Ignore a mypy error that seems buggy.
135 # We are assigning list[Register] to list[Register | RegisterArray]
136 # which should be absolutely fine, but mypy warns.
137 self._register_list.register_objects = copy.deepcopy(
138 default_registers # type: ignore[arg-type]
139 )
140 for register in default_registers:
141 self._default_register_names.append(register.name)
143 def parse(self, register_data: dict[str, Any]) -> RegisterList:
144 """
145 Parse the register data.
147 Arguments:
148 register_data: Register data as a dictionary.
149 Preferably read by the :func:`.from_toml`, :func:`.from_json` or
150 :func:`.from_yaml` functions.
152 Return:
153 The resulting register list.
154 """
155 for old_top_level_key_name in ["constant", "register", "register_array"]:
156 if old_top_level_key_name in register_data:
157 source_file = self._source_definition_file
158 output_file = (
159 source_file.parent.resolve()
160 / f"{source_file.stem}_version_6_format{source_file.suffix}"
161 )
163 print(
164 f"""
165ERROR: Parsing register data that appears to be in the old pre-6.0.0 format.
166ERROR: For more information, see: {WEBSITE_URL}/rst/basic_feature/basic_feature_register_modes.html
167ERROR: Your data will be automatically converted to the new format and saved to: {output_file}
168ERROR: Please inspect that file and update your data file to the new format.
169"""
170 )
171 _save_to_new_format(old_data=register_data, output_file=output_file)
172 raise ValueError("Found register data in old format. See message above.")
174 parser_methods = {
175 "constant": self._parse_constant,
176 "register": self._parse_plain_register,
177 "register_array": self._parse_register_array,
178 }
180 for top_level_name, top_level_items in register_data.items():
181 if not isinstance(top_level_items, dict):
182 message = (
183 f"Error while parsing {self._source_definition_file}: "
184 f'Got unknown top-level property "{top_level_name}".'
185 )
186 raise ValueError(message)
188 top_level_type = top_level_items.get("type", "register")
190 if top_level_type not in parser_methods:
191 valid_types_str = ", ".join(f'"{parser_key}"' for parser_key in parser_methods)
192 message = (
193 f'Error while parsing "{top_level_name}" in {self._source_definition_file}: '
194 f'Got unknown type "{top_level_type}". Expected one of {valid_types_str}.'
195 )
196 raise ValueError(message)
198 parser_methods[top_level_type](name=top_level_name, items=top_level_items)
200 return self._register_list
202 def _parse_constant(self, name: str, items: dict[str, Any]) -> None:
203 for item_name in self.required_constant_items:
204 if item_name not in items:
205 message = (
206 f'Error while parsing constant "{name}" in {self._source_definition_file}: '
207 f'Missing required property "{item_name}".'
208 )
209 raise ValueError(message)
211 for item_name in items.keys():
212 if item_name not in self.recognized_constant_items:
213 message = (
214 f'Error while parsing constant "{name}" in {self._source_definition_file}: '
215 f'Got unknown property "{item_name}".'
216 )
217 raise ValueError(message)
219 value = items["value"]
220 description = items.get("description", "")
221 data_type_str = items.get("data_type")
223 if data_type_str is not None:
224 if not isinstance(value, str):
225 raise ValueError(
226 f'Error while parsing constant "{name}" in '
227 f"{self._source_definition_file}: "
228 'May not set "data_type" for non-string constant.'
229 )
231 if data_type_str == "unsigned":
232 value = UnsignedVector(value)
233 else:
234 raise ValueError(
235 f'Error while parsing constant "{name}" in '
236 f"{self._source_definition_file}: "
237 f'Invalid data type "{data_type_str}".'
238 )
240 self._register_list.add_constant(name=name, value=value, description=description)
242 def _parse_plain_register(self, name: str, items: dict[str, Any]) -> None:
243 description = items.get("description", "")
245 if name in self._default_register_names:
246 # Default registers can be "updated" in the sense that the user can set a custom
247 # 'description' and add whatever fields they want in the current register list.
248 # They may not, however, change the 'mode' which is part of the default definition.
249 if "mode" in items:
250 message = (
251 f'Error while parsing register "{name}" in {self._source_definition_file}: '
252 'A "mode" may not be specified for a default register.'
253 )
254 raise ValueError(message)
256 register = self._register_list.get_register(register_name=name)
257 register.description = description
259 else:
260 # If it is a new register however, the 'mode' has to be specified.
261 if "mode" not in items:
262 message = (
263 f'Error while parsing register "{name}" in {self._source_definition_file}: '
264 f'Missing required property "mode".'
265 )
266 raise ValueError(message)
268 mode = self._get_mode(mode_name=items["mode"], register_name=name)
270 register = self._register_list.append_register(
271 name=name, mode=mode, description=description
272 )
274 self._parse_register_fields(register=register, register_items=items, register_array_note="")
276 def _get_mode(self, mode_name: str, register_name: str) -> "RegisterMode":
277 if mode_name in REGISTER_MODES:
278 return REGISTER_MODES[mode_name]
280 valid_modes_str = ", ".join(f'"{mode_key}"' for mode_key in REGISTER_MODES)
281 message = (
282 f'Error while parsing register "{register_name}" in {self._source_definition_file}: '
283 f'Got unknown mode "{mode_name}". Expected one of {valid_modes_str}.'
284 )
285 raise ValueError(message)
287 def _parse_register_fields(
288 self,
289 register_items: dict[str, Any],
290 register: "Register",
291 register_array_note: str,
292 ) -> None:
293 # Add any fields that are specified.
294 for item_name, item_value in register_items.items():
295 # Skip default items so we only get the fields.
296 if item_name in self.default_register_items:
297 continue
299 if not isinstance(item_value, dict):
300 message = (
301 f'Error while parsing register "{register.name}"{register_array_note} '
302 f"in {self._source_definition_file}: "
303 f'Got unknown property "{item_name}".'
304 )
305 raise ValueError(message)
307 if "type" not in item_value:
308 message = (
309 f'Error while parsing field "{item_name}" in register '
310 f'"{register.name}"{register_array_note} in {self._source_definition_file}: '
311 'Missing required property "type".'
312 )
313 raise ValueError(message)
315 field_type = item_value["type"]
317 parser_methods = {
318 "bit": self._parse_bit,
319 "bit_vector": self._parse_bit_vector,
320 "enumeration": self._parse_enumeration,
321 "integer": self._parse_integer,
322 }
324 if field_type not in parser_methods:
325 valid_types_str = ", ".join(f'"{parser_key}"' for parser_key in parser_methods)
326 message = (
327 f'Error while parsing field "{item_name}" in register '
328 f'"{register.name}"{register_array_note} in {self._source_definition_file}: '
329 f'Unknown field type "{field_type}". Expected one of {valid_types_str}.'
330 )
331 raise ValueError(message)
333 parser_methods[field_type](
334 register=register, field_name=item_name, field_items=item_value
335 )
337 def _parse_register_array(self, name: str, items: dict[str, Any]) -> None:
338 for required_property in self.required_register_array_items:
339 if required_property not in items:
340 message = (
341 f'Error while parsing register array "{name}" in '
342 f"{self._source_definition_file}: "
343 f'Missing required property "{required_property}".'
344 )
345 raise ValueError(message)
347 register_array_length = items["array_length"]
348 register_array_description = items.get("description", "")
349 register_array = self._register_list.append_register_array(
350 name=name, length=register_array_length, description=register_array_description
351 )
353 # Add all registers that are specified.
354 found_at_least_one_register = False
355 for item_name, item_value in items.items():
356 # Skip default items so we only get the registers.
357 if item_name in self.default_register_array_items:
358 continue
360 found_at_least_one_register = True
362 if not isinstance(item_value, dict):
363 message = (
364 f'Error while parsing register array "{name}" in '
365 f"{self._source_definition_file}: "
366 f'Got unknown property "{item_name}".'
367 )
368 raise ValueError(message)
370 item_type = item_value.get("type", "register")
371 if item_type != "register":
372 message = (
373 f'Error while parsing register "{item_name}" within array "{name}" in '
374 f"{self._source_definition_file}: "
375 f'Got unknown type "{item_type}". Expected "register".'
376 )
377 raise ValueError(message)
379 # A 'mode' is semi-required for plain registers, but always required for
380 # array registers.
381 if "mode" not in item_value:
382 raise ValueError(
383 f'Error while parsing register "{item_name}" within array "{name}" in '
384 f"{self._source_definition_file}: "
385 f'Missing required property "mode".'
386 )
387 register_mode = self._get_mode(mode_name=item_value["mode"], register_name=item_name)
389 register_description = item_value.get("description", "")
391 register = register_array.append_register(
392 name=item_name, mode=register_mode, description=register_description
393 )
395 self._parse_register_fields(
396 register_items=item_value,
397 register=register,
398 register_array_note=f' within array "{name}"',
399 )
401 if not found_at_least_one_register:
402 message = (
403 f'Error while parsing register array "{name}" in {self._source_definition_file}: '
404 "Array must contain at least one register."
405 )
406 raise ValueError(message)
408 def _check_field_items(
409 self,
410 register_name: str,
411 field_name: str,
412 field_items: dict[str, Any],
413 recognized_items: set[str],
414 required_items: list[str],
415 ) -> None:
416 """
417 Will raise exception if anything is wrong.
418 """
419 for item_name in required_items:
420 if item_name not in field_items:
421 message = (
422 f'Error while parsing field "{field_name}" in register "{register_name}" in '
423 f"{self._source_definition_file}: "
424 f'Missing required property "{item_name}".'
425 )
426 raise ValueError(message)
428 for item_name in field_items.keys():
429 if item_name not in recognized_items:
430 message = (
431 f'Error while parsing field "{field_name}" in register '
432 f'"{register_name}" in {self._source_definition_file}: '
433 f'Unknown property "{item_name}".'
434 )
435 raise ValueError(message)
437 def _parse_bit(
438 self, register: "Register", field_name: str, field_items: dict[str, Any]
439 ) -> None:
440 self._check_field_items(
441 register_name=register.name,
442 field_name=field_name,
443 field_items=field_items,
444 recognized_items=self.recognized_bit_items,
445 required_items=self.required_bit_items,
446 )
448 description = field_items.get("description", "")
449 default_value = field_items.get("default_value", "0")
451 register.append_bit(name=field_name, description=description, default_value=default_value)
453 def _parse_bit_vector(
454 self, register: "Register", field_name: str, field_items: dict[str, Any]
455 ) -> None:
456 self._check_field_items(
457 register_name=register.name,
458 field_name=field_name,
459 field_items=field_items,
460 recognized_items=self.recognized_bit_vector_items,
461 required_items=self.required_bit_vector_items,
462 )
464 width = field_items["width"]
466 description = field_items.get("description", "")
467 default_value = field_items.get("default_value", "0" * width)
469 register.append_bit_vector(
470 name=field_name, description=description, width=width, default_value=default_value
471 )
473 def _parse_enumeration(
474 self, register: "Register", field_name: str, field_items: dict[str, Any]
475 ) -> None:
476 self._check_field_items(
477 register_name=register.name,
478 field_name=field_name,
479 field_items=field_items,
480 recognized_items=self.recognized_enumeration_items,
481 # Check that we have at least one element.
482 # This is checked also in the Enumeration class, which is needed if the user
483 # is working directly with the Python API.
484 # That is where we usually sanity check, to avoid duplication.
485 # However, this particular check is needed here also since the logic for default
486 # value below does not work if there are no elements.
487 required_items=self.required_enumeration_items,
488 )
490 description = field_items.get("description", "")
491 # We assert above that the enumeration has at least one element.
492 # Meaning that the result of this get can not be None, as mypy thinks.
493 elements: dict[str, str] = field_items.get("element") # type: ignore[assignment]
495 # The default "default value" is the first declared enumeration element.
496 # Note that this works because dictionaries in Python are guaranteed ordered since
497 # Python 3.7.
498 default_value = field_items.get("default_value", list(elements)[0])
500 register.append_enumeration(
501 name=field_name,
502 description=description,
503 elements=elements,
504 default_value=default_value,
505 )
507 def _parse_integer(
508 self, register: "Register", field_name: str, field_items: dict[str, Any]
509 ) -> None:
510 self._check_field_items(
511 register_name=register.name,
512 field_name=field_name,
513 field_items=field_items,
514 recognized_items=self.recognized_integer_items,
515 required_items=self.required_integer_items,
516 )
518 max_value = field_items["max_value"]
520 description = field_items.get("description", "")
521 min_value = field_items.get("min_value", 0)
522 default_value = field_items.get("default_value", min_value)
524 register.append_integer(
525 name=field_name,
526 description=description,
527 min_value=min_value,
528 max_value=max_value,
529 default_value=default_value,
530 )
533def _convert_to_new_format( # pylint: disable=too-many-locals
534 old_data: dict[str, Any]
535) -> dict[str, Any]:
536 """
537 Convert pre-6.0.0 format to the new format.
538 """
540 def _get_register_dict(register_items: dict[str, Any]) -> dict[str, Any]:
541 register_dict = dict()
543 for register_item_name, register_item_value in register_items.items():
544 if register_item_name in RegisterParser.default_register_items:
545 register_dict[register_item_name] = register_item_value
547 elif register_item_name in ["bit", "bit_vector", "enumeration", "integer"]:
548 for field_name, field_items in register_item_value.items():
549 field_dict = dict(type=register_item_name)
551 for field_item_name, field_item_value in field_items.items():
552 field_dict[field_item_name] = field_item_value
554 register_dict[field_name] = field_dict
556 else:
557 raise ValueError(
558 f"Unknown item {register_item_name}. "
559 "Looks like an error in the user data file."
560 )
562 return register_dict
564 result = dict()
566 def _add_item(name: str, items: dict[str, Any]) -> None:
567 if name in result:
568 raise ValueError(f"Duplicate item {name}")
570 result[name] = items
572 if "register" in old_data:
573 for register_name, register_items in old_data["register"].items():
574 register_dict = _get_register_dict(register_items=register_items)
575 _add_item(name=register_name, items=register_dict)
577 if "register_array" in old_data:
578 for register_array_name, register_array_items in old_data["register_array"].items():
579 register_array_dict: dict[str, Any] = dict(type="register_array")
581 for register_array_item_name, register_array_item_value in register_array_items.items():
582 if register_array_item_name in RegisterParser.default_register_array_items:
583 register_array_dict[register_array_item_name] = register_array_item_value
585 elif register_array_item_name == "register":
586 for register_name, register_items in register_array_item_value.items():
587 register_array_dict[register_name] = _get_register_dict(
588 register_items=register_items
589 )
591 else:
592 raise ValueError(
593 f"Unknown item {register_array_item_name}. "
594 "Looks like an error in the user data file."
595 )
597 _add_item(name=register_array_name, items=register_array_dict)
599 if "constant" in old_data:
600 for constant_name, constant_items in old_data["constant"].items():
601 constant_dict = dict(type="constant")
603 for constant_item_name, constant_item_value in constant_items.items():
604 constant_dict[constant_item_name] = constant_item_value
606 _add_item(name=constant_name, items=constant_dict)
608 return result
611def _save_to_new_format(old_data: dict[str, Any], output_file: Path) -> None:
612 """
613 Save the old data to the new format.
614 """
615 new_data = _convert_to_new_format(old_data=old_data)
617 if output_file.suffix == ".toml":
618 with open(output_file, "wb") as file_handle:
619 tomli_w.dump(new_data, file_handle, multiline_strings=True)
621 return
623 if output_file.suffix == ".json":
624 with open(output_file, "w", encoding=DEFAULT_FILE_ENCODING) as file_handle:
625 json.dump(new_data, file_handle, indent=4)
627 return
629 if output_file.suffix == ".yaml":
630 with open(output_file, "w", encoding=DEFAULT_FILE_ENCODING) as file_handle:
631 yaml.dump(new_data, file_handle)
633 return
635 raise ValueError(f"Unknown file format {output_file}")