Coverage for hdl_registers/generator/register_code_generator.py: 98%
161 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-01 20:50 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-01 20:50 +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 datetime
12import re
13from abc import ABC, abstractmethod
14from pathlib import Path
15from typing import TYPE_CHECKING, Any, Optional
17# Third party libraries
18from tsfpga.git_utils import get_git_commit, git_commands_are_available
19from tsfpga.svn_utils import get_svn_revision_information, svn_commands_are_available
20from tsfpga.system_utils import create_file, path_relative_to, read_file
22# First party libraries
23from hdl_registers import __version__ as hdl_registers_version
25# Local folder libraries
26from .register_code_generator_helpers import RegisterCodeGeneratorHelpers
27from .reserved_keywords import RESERVED_KEYWORDS
29if TYPE_CHECKING:
30 # First party libraries
31 from hdl_registers.register_list import RegisterList
34class RegisterCodeGenerator(ABC, RegisterCodeGeneratorHelpers):
35 """
36 Abstract interface and common functions for generating register code.
37 Should be inherited by all custom code generators.
39 Note that this base class also inherits from :class:`.RegisterCodeGeneratorHelpers`,
40 meaning some useful methods are available in subclasses.
41 """
43 @property
44 @abstractmethod
45 def SHORT_DESCRIPTION(self) -> str: # pylint: disable=invalid-name
46 """
47 A short description of what this generator produces.
48 Will be used when printing status messages.
50 Overload in subclass by setting e.g.
52 .. code-block:: python
54 SHORT_DESCRIPTION = "C++ header"
56 as a static class member at the top of the class.
57 """
59 @property
60 @abstractmethod # type: ignore[override]
61 def COMMENT_START(self) -> str: # pylint: disable=invalid-name
62 """
63 The character(s) that start a comment line in the programming language that we are
64 generating code for.
66 Overload in subclass by setting e.g.
68 .. code-block:: python
70 COMMENT_START = "#"
72 as a static class member at the top of the class.
73 Note that for some languages you might have to set :attr:`.COMMENT_END` as well.
74 """
76 @property
77 @abstractmethod
78 def output_file(self) -> Path:
79 """
80 Result will be placed in this file.
82 Overload in a subclass to give the proper name to the code artifact.
83 Probably using a combination of ``self.output_folder`` and ``self.name``.
84 For example:
86 .. code-block:: python
88 @property
89 def output_file(self) -> Path:
90 return self.output_folder / f"{self.name}_regs.html"
91 """
93 @abstractmethod
94 def get_code(self, **kwargs: Any) -> str: # pylint: disable=unused-argument
95 """
96 Get the generated code as a string.
98 Overload in a subclass where the code generation is implemented.
100 Arguments:
101 kwargs: Further optional parameters that can be used.
102 Can send any number of named arguments, per the requirements of :meth:`.get_code`
103 of any custom generators that inherit this class.
104 """
106 # Optionally set a non-zero default indentation level, expressed in number of spaces, in a
107 # subclass for your code generator.
108 # Will be used by e.g. the 'self.comment' method.
109 DEFAULT_INDENTATION_LEVEL = 0
111 # For some languages, comment lines have to be ended with special characters.
112 # Optionally set this to a non-null string in a subclass.
113 # For best formatting, leave a space character at the start of this string.
114 COMMENT_END = ""
116 # For a custom code generator, overload this value in a subclass.
117 # This version number of the custom code generator class is added to the file header of
118 # generated artifacts.
119 # Changing the number will trigger a re-generate of all artifacts when 'create_if_needed'
120 # is called.
121 __version__ = "0.0.1"
123 def __init__(self, register_list: "RegisterList", output_folder: Path):
124 """
125 Arguments:
126 register_list: Registers and constants from this register list will be included
127 in the generated artifacts.
128 output_folder: Result file will be placed in this folder.
129 """
130 self.register_list = register_list
131 self.output_folder = output_folder
133 self.name = register_list.name
135 def create(self, **kwargs: Any) -> Path:
136 """
137 Generate the result artifact.
138 I.e. create the :meth:`.output_file` containing the result from :meth:`.get_code` method.
140 Arguments:
141 kwargs: Further optional parameters that will be sent on to the
142 :meth:`.get_code` method.
143 See :meth:`.get_code` for details.
145 Return:
146 The path to the created file, i.e. :meth:`.output_file`.
147 """
148 output_file = self.output_file
150 try:
151 path_to_print = path_relative_to(path=output_file, other=Path("."))
152 except ValueError:
153 # Fails on Windows if CWD and the file are on different drives.
154 path_to_print = output_file
155 print(f"Creating {self.SHORT_DESCRIPTION} file: {path_to_print}")
157 self._sanity_check()
159 code = self.get_code(**kwargs)
161 # Will create the containing folder unless it already exists.
162 create_file(file=output_file, contents=code)
164 return output_file
166 def create_if_needed(self, **kwargs: Any) -> tuple[bool, Path]:
167 """
168 Generate the result file if needed.
169 I.e. call :meth:`.create` if :meth:`.should_create` is ``True``.
171 This method is recommended rather than :meth:`.create` in time-critical scenarios,
172 such as before a user simulation run.
173 The :meth:`.should_create` check is very fast (0.5 ms on a decent computer with a typical
174 register list), while a typical register generator is quite slow by comparison.
175 Hence it makes sense to run this method in order to save execution time.
176 This increased speed gives a much nicer user experience.
178 Arguments:
179 kwargs: Further optional parameters that will be sent on to the
180 :meth:`.get_code` method.
181 See :meth:`.get_code` for details.
183 Return:
184 Tuple, where first element is a boolean status, and second element is the path to the
185 artifact that may or may not have been created.
187 The boolean is the return value of :meth:`.should_create`.
189 The path is the :meth:`.output_file` and is set always, even if the file was
190 not created.
191 """
192 if self.should_create:
193 create_path = self.create(**kwargs)
194 return True, create_path
196 return False, self.output_file
198 @property
199 def should_create(self) -> bool:
200 """
201 Indicates if a (re-)create of artifacts is needed.
202 Will be True if any of these conditions are true:
204 * Output file does not exist.
205 * Generator version of artifact does not match current code version.
206 * Artifact hash does not match :meth:`.RegisterList.object_hash` of the current
207 register list.
208 I.e. something in the register list has changed since the previous file was generated
209 (e.g. a new register added).
211 The version and hash checks above are dependent on the artifact file having a header
212 as given by :meth:`.header`.
213 """
214 output_file = self.output_file
216 if not output_file.exists():
217 return True
219 if (
220 hdl_registers_version,
221 self.__version__,
222 self.register_list.object_hash,
223 ) != self._find_versions_and_hash_of_existing_file(file_path=output_file):
224 return True
226 return False
228 def _find_versions_and_hash_of_existing_file(
229 self, file_path: Path
230 ) -> tuple[Optional[str], Optional[str], Optional[str]]:
231 """
232 Returns the matching strings in a tuple. Either field can be ``None`` if nothing found.
233 """
234 existing_file_content = read_file(file=file_path)
236 result_package_version = None
237 result_generator_version = None
238 result_hash = None
240 # This is either the very first line of the file, or starting on a new line.
241 package_version_re = re.compile(
242 (
243 rf"(^|\n){self.COMMENT_START} This file is automatically generated by "
244 rf"hdl-registers version (\S+)\.{self.COMMENT_END}\n"
245 )
246 )
247 package_version_match = package_version_re.search(existing_file_content)
248 if package_version_match:
249 result_package_version = package_version_match.group(2)
251 generator_version_re = re.compile(
252 rf"\n{self.COMMENT_START} Code generator {self.__class__.__name__} "
253 rf"version (\S+).{self.COMMENT_END}\n",
254 )
255 generator_version_match = generator_version_re.search(existing_file_content)
256 if generator_version_match:
257 result_generator_version = generator_version_match.group(1)
259 hash_re = re.compile(
260 rf"\n{self.COMMENT_START} Register hash ([0-9a-f]+)\.{self.COMMENT_END}\n"
261 )
262 hash_match = hash_re.search(existing_file_content)
263 if hash_match:
264 result_hash = hash_match.group(1)
266 return result_package_version, result_generator_version, result_hash
268 @property
269 def header(self) -> str:
270 """
271 Get file header informing the user that the file is automatically generated.
272 Basically the information from :meth:`.generated_source_info` formatted as a comment block.
273 """
274 return self.comment_block(text=self.generated_source_info, indent=0)
276 @property
277 def generated_source_info(self) -> list[str]:
278 """
279 Return lines informing the user that the file is automatically generated.
280 Containing info about the source of the generated register information.
281 """
282 # Default: Get git SHA from the user's current working directory.
283 directory = Path(".")
285 time_info = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
287 file_info = ""
288 if self.register_list.source_definition_file is not None:
289 # If the source definition file does exist, get git SHA from that directory instead.
290 directory = self.register_list.source_definition_file.parent
291 file_info = f" from file {self.register_list.source_definition_file.name}"
293 commit_info = ""
294 if git_commands_are_available(directory=directory):
295 commit_info = f" at commit {get_git_commit(directory=directory)}"
296 elif svn_commands_are_available(cwd=directory):
297 commit_info = f" at revision {get_svn_revision_information(cwd=directory)}"
299 info = f"Generated {time_info}{file_info}{commit_info}."
301 return [
302 (
303 "This file is automatically generated by hdl-registers "
304 f"version {hdl_registers_version}."
305 ),
306 f"Code generator {self.__class__.__name__} version {self.__version__}.",
307 info,
308 f"Register hash {self.register_list.object_hash}.",
309 ]
311 def _sanity_check(self) -> None:
312 """
313 Do some basic checks that no naming errors are present.
314 Will raise exception if there is any error.
316 In general, the user will know if these errors are present when the generated code is
317 compiled/used since it will probably crash.
318 But it is better to warn early, rather than the user finding out when compiling headers
319 after a 1 hour FPGA build.
321 We run this check at creation time, always and for every single generator.
322 Hence the user will hopefully get warned when they generate e.g. a VHDL package at the start
323 of the FPGA build that a register uses a reserved C++ name.
324 This check takes roughly 140 us on a decent computer with a typical register list.
325 Hence it is not a big deal that it might be run more than once for each register list.
327 It is better to have it here in the generator rather than in the parser:
328 1. Here it runs only when necessary, not adding time to parsing which is often done in
329 real time.
330 2. We also catch things that were added with the Python API.
331 """
332 self._check_reserved_keywords()
333 self._check_for_name_clashes()
335 def _check_reserved_keywords(self) -> None:
336 """
337 Check that no item in the register list matches a reserved keyword in any of the targeted
338 generator languages.
339 To minimize the risk that a generated artifact does not compile.
340 """
342 def check(name: str, description: str) -> None:
343 if name.lower() in RESERVED_KEYWORDS:
344 message = (
345 f'Error in register list "{self.name}": '
346 f'{description} name "{name}" is a reserved keyword.'
347 )
348 raise ValueError(message)
350 for constant in self.register_list.constants:
351 check(name=constant.name, description="Constant")
353 for register, _ in self.iterate_registers():
354 check(name=register.name, description="Register")
356 for field in register.fields:
357 check(name=field.name, description="Field")
359 for register_array in self.iterate_register_arrays():
360 check(name=register_array.name, description="Register array")
362 def _check_for_name_clashes(self) -> None:
363 """
364 Check that there are no name clashes between items in the register list.
365 To minimize the risk that a generated artifact does not compile.
366 """
367 self._check_for_constant_name_clashes()
368 self._check_for_top_level_name_clashes()
369 self._check_for_field_name_clashes()
370 self._check_for_qualified_name_clashes()
372 def _check_for_constant_name_clashes(self) -> None:
373 """
374 Check that there are no constants with the same name.
375 """
376 constant_names = set()
377 for constant in self.register_list.constants:
378 if constant.name in constant_names:
379 message = (
380 f'Error in register list "{self.name}": '
381 f'Duplicate constant name "{constant.name}".'
382 )
383 raise ValueError(message)
385 constant_names.add(constant.name)
387 def _check_for_top_level_name_clashes(self) -> None:
388 """
389 Check that there are no
390 * duplicate register names,
391 * duplicate register array names,
392 * register array names that clash with register names.
393 """
394 plain_register_names = set()
395 for register in self.iterate_plain_registers():
396 if register.name in plain_register_names:
397 message = (
398 f'Error in register list "{self.name}": '
399 f'Duplicate plain register name "{register.name}".'
400 )
401 raise ValueError(message)
403 plain_register_names.add(register.name)
405 register_array_names = set()
406 for register_array in self.iterate_register_arrays():
407 if register_array.name in register_array_names:
408 message = (
409 f'Error in register list "{self.name}": '
410 f'Duplicate register array name "{register_array.name}".'
411 )
412 raise ValueError(message)
414 if register_array.name in plain_register_names:
415 message = (
416 f'Error in register list "{self.name}": '
417 f'Register array "{register_array.name}" may not have same name as register.'
418 )
419 raise ValueError(message)
421 register_array_names.add(register_array.name)
423 def _check_for_field_name_clashes(self) -> None:
424 """
425 Check that no register contains fields with the same name.
426 """
427 for register, register_array in self.iterate_registers():
428 field_names = set()
430 for field in register.fields:
431 if field.name in field_names:
432 register_description = (
433 f"{register_array.name}.{register.name}"
434 if register_array
435 else register.name
436 )
437 message = (
438 f'Error in register list "{self.name}": '
439 f'Duplicate field name "{field.name}" in register "{register_description}".'
440 )
441 raise ValueError(message)
443 field_names.add(field.name)
445 def _check_for_qualified_name_clashes(self) -> None:
446 """
447 Check that there are no name clashes when names of registers and fields are qualified.
449 The register 'apa_hest' will give a conflict with the field 'apa.hest' since both will get
450 e.g. a VHDL simulation method 'read_apa_hest'.
451 Hence we need to check for these conflicts.
452 """
453 qualified_names = set()
455 for register, register_array in self.iterate_registers():
456 register_name = self.qualified_register_name(
457 register=register, register_array=register_array
458 )
459 register_description = (
460 f"{register_array.name}.{register.name}" if register_array else register.name
461 )
463 if register_name in qualified_names:
464 message = (
465 f'Error in register list "{self.name}": '
466 f'Qualified name of register "{register_description}" '
467 f'("{register_name}") clashes with another item.'
468 )
469 raise ValueError(message)
471 qualified_names.add(register_name)
473 for field in register.fields:
474 field_name = self.qualified_field_name(
475 register=register, register_array=register_array, field=field
476 )
478 if field_name in qualified_names:
479 field_description = f"{register_description}.{field.name}"
480 message = (
481 f'Error in register list "{self.name}": '
482 f'Qualified name of field "{field_description}" '
483 f'("{field_name}") clashes with another item.'
484 )
485 raise ValueError(message)
487 qualified_names.add(field_name)