Coverage for tsfpga/module_documentation.py: 66%
146 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-21 20:51 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-21 20:51 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project, a project platform for modern FPGA development.
5# https://tsfpga.com
6# https://github.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9# Standard libraries
10from pathlib import Path
11from typing import TYPE_CHECKING, Optional
13# Third party libraries
14from hdl_registers.generator.html.page import HtmlPageGenerator
16# First party libraries
17from tsfpga.vivado.build_result_checker import (
18 DspBlocks,
19 Ffs,
20 LogicLuts,
21 LutRams,
22 MaximumLogicLevel,
23 Ramb,
24 Ramb18,
25 Ramb36,
26 Srls,
27 TotalLuts,
28 Uram,
29)
31# Local folder libraries
32from .about import WEBSITE_URL
33from .system_utils import create_file, file_is_in_directory, read_file
34from .vhdl_file_documentation import VhdlFileDocumentation
35from .vivado.project import VivadoNetlistProject
37if TYPE_CHECKING:
38 # Local folder libraries
39 from .hdl_file import HdlFile
40 from .module import BaseModule
43class ModuleDocumentation:
44 """
45 Methods for generating a reStructuredText document with module documentation.
46 The content is extracted from VHDL source file headers.
47 """
49 def __init__(
50 self,
51 module: "BaseModule",
52 repository_url: Optional[str] = None,
53 repository_name: Optional[str] = None,
54 ) -> None:
55 """
56 Arguments:
57 module: The module which shall be documented.
58 repository_url: Optionally specify an URL where the source code can be viewed.
59 If this argument is specified, links will be added to the documentation.
60 URL should be to the module folder within the repository.
61 repository_name: Optionally specify the name of the repository URL.
62 For example "GitLab".
63 """
64 self._module = module
65 self._repository_url = repository_url
66 self._repository_name = repository_name
68 assert (repository_url is None) == (
69 repository_name is None
70 ), "Both or none of the repository arguments must be set"
72 def get_overview_rst(self) -> str:
73 """
74 Get the contents of the module's ``doc/<name>.rst``, i.e. the module "overview" document.
76 Return:
77 Module overview RST. Empty string if file does not exist.
78 """
79 overview_rst_file = self._module.path / "doc" / f"{self._module.name}.rst"
80 if overview_rst_file.exists():
81 return read_file(overview_rst_file)
83 return ""
85 def get_register_rst(self, heading_character: str) -> str:
86 """
87 Get an RST snippet with a link to the module's register documentation, if available.
88 Note that this will create an RST ``:download:`` statement to the register .html page.
89 When building, the ``.html`` file must be present in the same directory as the
90 ``.rst`` file.
91 This is done automatically by :meth:`.create_rst_document`.
93 Arguments:
94 heading_character: Character to use for heading underline.
96 Return:
97 RST snippet with link to register HTML. Empty string if module does not have registers.
98 """
99 if self._module.registers is None:
100 return ""
102 heading = "Register interface"
103 heading_underline = heading_character * len(heading)
105 if self._repository_url:
106 file_name = f"regs_{self._module.name}.toml"
107 toml_link = (
108 f" based on the `{file_name} <{self._repository_url}/{file_name}>`_ data file"
109 )
110 else:
111 toml_link = ""
113 if self._module.registers.register_objects:
114 description = "is controlled and monitored over a register bus"
115 else:
116 description = "has register definitions"
118 return f"""\
119.. _{self._module.name}.register_interface:
121{heading}
122{heading_underline}
124This module {description}.
125Please see :download:`separate HTML page <{self._module.name}_regs.html>` for \
126register documentation.
127Register code is generated using `hdl-registers <https://hdl-registers.com>`_{toml_link}.
128"""
130 def get_submodule_rst(
131 self,
132 heading_character: str,
133 heading_character_2: str,
134 exclude_files: Optional[set[Path]] = None,
135 exclude_module_folders: Optional[list[str]] = None,
136 ) -> str:
137 """
138 Get RST code with documentation of the different sub-modules (files) of the module.
139 Contains documentation that is extracted from the file headers, as well as a
140 symbolator symbol of the entity.
142 Arguments:
143 heading_character: Character to use for heading underline.
144 heading_character_2: Character to use for next level of heading underline.
145 exclude_files: Files that shall be excluded from the documentation.
146 exclude_module_folders: Folder names within the module root that shall be
147 excluded from documentation. For example, if you chosen module structure places
148 only netlist build wrappers in the "rtl/" folder within modules, and you do not
149 want them included in the documentation, then pass the argument ["rtl"].
151 Return:
152 RST code with sub-module documentation.
153 """
154 exclude_module_folders = [] if exclude_module_folders is None else exclude_module_folders
155 exclude_module_paths = [self._module.path / name for name in exclude_module_folders]
157 all_builds = self._module.get_build_projects()
159 rst = ""
161 for vhdl_file_path in self._get_vhdl_files(
162 exclude_files=exclude_files, exclude_folders=exclude_module_paths
163 ):
164 netlist_build_base_name = f"{self._module.library_name}.{vhdl_file_path.stem}"
166 netlist_builds = []
167 # Include all netlist builds whose project name matches this file
168 for project in all_builds:
169 if isinstance(project, VivadoNetlistProject) and (
170 project.name == netlist_build_base_name
171 or project.name.startswith(f"{netlist_build_base_name}.")
172 ):
173 netlist_builds.append(project)
175 rst += self._get_vhdl_file_rst(
176 vhdl_file_path=vhdl_file_path,
177 heading_character=heading_character,
178 heading_character_2=heading_character_2,
179 netlist_builds=netlist_builds,
180 )
182 return rst
184 def get_rst_document(self, exclude_module_folders: Optional[list[str]] = None) -> str:
185 """
186 Get a complete RST document with the content of :meth:`.get_overview_rst`,
187 :meth:`.get_register_rst`, and :meth:`.get_submodule_rst`, as well as a top level heading.
189 Arguments:
190 exclude_module_folders: Folder names within the module root that shall be
191 excluded from documentation.
193 Return:
194 An RST document.
195 """
196 heading_character_1 = "="
197 heading_character_2 = "-"
198 heading_character_3 = "_"
200 heading = f"Module {self._module.name}"
201 heading_underline = heading_character_1 * len(heading)
203 if self._module.registers is not None:
204 register_note_rst = (
205 "This module has a register interface, so make sure to study the "
206 f":ref:`register interface documentation <{self._module.name}.register_interface>` "
207 "as well as this top-level document.\n"
208 )
209 else:
210 register_note_rst = ""
212 if self._repository_url:
213 url_rst = (
214 f"To browse the source code, visit the "
215 f"`repository on {self._repository_name} <{self._repository_url}>`__.\n"
216 )
217 else:
218 url_rst = ""
220 overview_rst = self.get_overview_rst()
221 registers_rst = self.get_register_rst(heading_character=heading_character_2)
223 submodule_rst = self.get_submodule_rst(
224 heading_character=heading_character_2,
225 heading_character_2=heading_character_3,
226 exclude_module_folders=exclude_module_folders,
227 )
229 rst = f"""\
231.. _module_{self._module.name}:
233{heading}
234{heading_underline}
236This document contains technical documentation for the ``{self._module.name}`` module.
237{register_note_rst}\
238{url_rst}\
240{overview_rst}
242{registers_rst}
244{submodule_rst}
245"""
247 return rst
249 def create_rst_document(
250 self, output_path: Path, exclude_module_folders: Optional[list[str]] = None
251 ) -> None:
252 """
253 Create an ``.rst`` file in ``output_path`` with the content from :meth:`.get_rst_document`.
254 If the module has registers, the HTML page will also be generated in ``output_path``, so
255 that e.g. sphinx can be run directly.
257 Arguments:
258 output_path: Document will be placed here.
259 exclude_module_folders: Folder names within the module root that shall be
260 excluded from documentation.
261 """
262 register_list = self._module.registers
263 if register_list is not None:
264 HtmlPageGenerator(register_list=register_list, output_folder=output_path).create()
266 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders)
267 create_file(output_path / f"{self._module.name}.rst", contents=rst)
269 def _get_vhdl_files(
270 self, exclude_files: Optional[set[Path]], exclude_folders: list[Path]
271 ) -> list[Path]:
272 """
273 Get VHDL files that shall be included in the documentation, in order.
274 """
275 # Exclude all file types except VHDL.
276 hdl_files = self._module.get_documentation_files(
277 files_avoid=exclude_files, include_verilog=False, include_systemverilog=False
278 )
280 def file_should_be_included(hdl_file: "HdlFile") -> bool:
281 if file_is_in_directory(hdl_file.path, exclude_folders):
282 return False
284 return True
286 vhdl_file_paths = [
287 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file)
288 ]
290 # Sort by file name
291 def sort_key(path: Path) -> str:
292 return path.name
294 vhdl_files = sorted(vhdl_file_paths, key=sort_key)
296 return vhdl_files
298 def _get_vhdl_file_rst(
299 self,
300 vhdl_file_path: Path,
301 heading_character: str,
302 heading_character_2: str,
303 netlist_builds: list["VivadoNetlistProject"],
304 ) -> str:
305 """
306 Get reStructuredText documentation for a VHDL file.
307 """
308 vhdl_file_documentation = VhdlFileDocumentation(vhdl_file_path)
310 file_rst = vhdl_file_documentation.get_header_rst()
311 file_rst = "" if file_rst is None else file_rst
313 if self._repository_url:
314 url_rst = (
315 f"`View source code on {self._repository_name} "
316 f"<{self._repository_url}/{vhdl_file_path.relative_to(self._module.path)}>`__."
317 )
318 else:
319 url_rst = ""
321 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation)
322 symbolator_rst = "" if symbolator_rst is None else symbolator_rst
324 entity_name = vhdl_file_path.stem
326 resource_utilization_rst = self._get_resource_utilization_rst(
327 entity_name=entity_name,
328 heading_character=heading_character_2,
329 netlist_builds=netlist_builds,
330 )
332 heading = f"{vhdl_file_path.name}"
333 heading_underline = heading_character * len(heading)
335 rst = f"""
336.. _{self._module.name}.{entity_name}:
338{heading}
339{heading_underline}
341{url_rst}
343{symbolator_rst}
345{file_rst}
347{resource_utilization_rst}
348"""
350 return rst
352 @staticmethod
353 def _get_symbolator_rst(vhdl_file_documentation: VhdlFileDocumentation) -> str:
354 """
355 Get RST for rendering a symbolator component.
356 """
357 component = vhdl_file_documentation.get_symbolator_component()
358 if component is None:
359 return ""
361 indent = " "
362 rst = ".. symbolator::\n\n"
363 rst += indent + component.replace("\n", f"\n{indent}")
365 return rst
367 def _get_resource_utilization_rst( # pylint: disable=too-many-locals,too-many-branches
368 self, entity_name: str, heading_character: str, netlist_builds: list["VivadoNetlistProject"]
369 ) -> str:
370 # First, loop over all netlist builds for this module and assemble information
371 build_generics = []
372 build_checkers = []
373 all_checker_names = set()
374 for netlist_build in netlist_builds:
375 if netlist_build.build_result_checkers:
376 build_generics.append(netlist_build.static_generics)
378 # Create a dictionary for each build, that maps "Checker name": "value"
379 checker_dict = {}
380 for checker in netlist_build.build_result_checkers:
381 # Casting the limit to string yields e.g. "< 4", "4" or "> 4"
382 checker_dict[checker.name] = str(checker.limit)
384 # Add to the set of checker names for this entity.
385 # Note that different netlist builds of the same entity might check a different
386 # set of resources.
387 all_checker_names.add(checker.name)
389 build_checkers.append(checker_dict)
391 # Make RST of the information.
392 # But make a heading and table only if there are any netlist builds with checkers, so
393 # that we don't get an empty table.
394 rst = ""
395 if all_checker_names:
396 heading = "Resource utilization"
397 heading_underline = heading_character * len(heading)
399 module_py_name = f"module_{self._module.name}.py"
400 if self._repository_url:
401 module_py_rst = f"`{module_py_name} <{self._repository_url}/{module_py_name}>`__"
402 else:
403 module_py_rst = f"``{module_py_name}``"
405 rst = f"""
406.. _{self._module.name}.{entity_name}.resource_utilization:
408{heading}
409{heading_underline}
411This entity has `netlist builds <{WEBSITE_URL}/netlist_build.html>`__ set up with
412`automatic size checkers <{WEBSITE_URL}/netlist_build.html#build-result-checkers>`__
413in {module_py_rst}.
414The following table lists the resource utilization for the entity, depending on
415generic configuration.
417.. list-table:: Resource utilization for **{entity_name}** netlist builds.
418 :header-rows: 1
420"""
422 # Sort so that we always get a consistent order in the table, no matter what order
423 # the user has added the checkers.
424 sort_keys = {
425 TotalLuts.name: 0,
426 LogicLuts.name: 1,
427 LutRams.name: 2,
428 Srls.name: 3,
429 Ffs.name: 4,
430 Ramb36.name: 5,
431 Ramb18.name: 6,
432 Ramb.name: 7,
433 Uram.name: 8,
434 DspBlocks.name: 9,
435 MaximumLogicLevel.name: 10,
436 }
437 sorted_checker_names = sorted(all_checker_names, key=lambda name: sort_keys[name])
439 # Fill in the header row
440 rst += " * - Generics\n"
441 for checker_name in sorted_checker_names:
442 rst += f" - {checker_name}\n"
444 # Make one row for each netlist build
445 for build_idx, generic_dict in enumerate(build_generics):
446 generic_strings = [f"{name} = {value}" for name, value in generic_dict.items()]
447 generics_rst = "\n\n ".join(generic_strings)
449 rst += f"""\
450 * - {generics_rst}"""
452 # If the "top" of the project is different than the entity, we assume that it
453 # is a netlist build wrapper.
454 # Add a note to the table about this.
455 # This occurs e.g. in the 'register_file' and 'fifo' modules.
456 if netlist_builds[build_idx].top != entity_name:
457 if generic_strings:
458 # If there is already something in the generic column, this note shall be
459 # placed on a new line.
460 leader = "\n\n "
461 else:
462 # Otherwise, i.e. if the netlist build has no generics set,
463 # the note shall be placed as the first thing. This is the case with
464 # two builds in the register_file module.
465 leader = ""
467 rst += f"""\
468{leader}(Using wrapper
470 {netlist_builds[build_idx].top}.vhd)"""
472 rst += "\n"
474 for checker_name in sorted_checker_names:
475 checker_value = (
476 build_checkers[build_idx][checker_name]
477 if checker_name in build_checkers[build_idx]
478 else ""
479 )
480 rst += f" - {checker_value}\n"
482 return rst