Coverage for tsfpga/module_documentation.py: 64%
140 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-13 07:59 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-13 07:59 +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# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11from typing import TYPE_CHECKING
13from hdl_registers.generator.html.page import HtmlPageGenerator
15from tsfpga.vivado.build_result_checker import (
16 DspBlocks,
17 Ffs,
18 LogicLuts,
19 LutRams,
20 MaximumLogicLevel,
21 Ramb,
22 Ramb18,
23 Ramb36,
24 Srls,
25 TotalLuts,
26 Uram,
27)
29from .about import WEBSITE_URL
30from .system_utils import create_file, file_is_in_directory, read_file
31from .vhdl_file_documentation import VhdlFileDocumentation
32from .vivado.project import VivadoNetlistProject
34if TYPE_CHECKING:
35 from pathlib import Path
37 from .hdl_file import HdlFile
38 from .module import BaseModule
41class ModuleDocumentation:
42 """
43 Methods for generating a reStructuredText document with module documentation.
44 The content is extracted from VHDL source file headers.
45 """
47 def __init__(
48 self,
49 module: BaseModule,
50 repository_url: str | None = None,
51 repository_name: str | None = None,
52 ) -> None:
53 """
54 Arguments:
55 module: The module which shall be documented.
56 repository_url: Optionally specify an URL where the source code can be viewed.
57 If this argument is specified, links will be added to the documentation.
58 URL should be to the module folder within the repository.
59 repository_name: Optionally specify the name of the repository URL.
60 For example "GitLab".
61 """
62 self._module = module
63 self._repository_url = repository_url
64 self._repository_name = repository_name
66 if (repository_url is None) != (repository_name is None):
67 raise ValueError("Both or none of the repository arguments must be set")
69 def get_overview_rst(self) -> str:
70 """
71 Get the contents of the module's ``doc/<name>.rst``, i.e. the module "overview" document.
73 Return:
74 Module overview RST. Empty string if file does not exist.
75 """
76 overview_rst_file = self._module.path / "doc" / f"{self._module.name}.rst"
77 if overview_rst_file.exists():
78 return read_file(overview_rst_file)
80 return ""
82 def get_register_rst(self, heading_character: str) -> str:
83 """
84 Get an RST snippet with a link to the module's register documentation, if available.
85 Note that this will create an RST ``:download:`` statement to the register .html page.
86 When building, the ``.html`` file must be present in the same directory as the
87 ``.rst`` file.
88 This is done automatically by :meth:`.create_rst_document`.
90 Arguments:
91 heading_character: Character to use for heading underline.
93 Return:
94 RST snippet with link to register HTML. Empty string if module does not have registers.
95 """
96 if self._module.registers is None:
97 return ""
99 heading = "Register interface"
100 heading_underline = heading_character * len(heading)
102 if self._repository_url:
103 file_name = f"regs_{self._module.name}.toml"
104 toml_link = (
105 f" based on the `{file_name} <{self._repository_url}/{file_name}>`_ data file"
106 )
107 else:
108 toml_link = ""
110 if self._module.registers.register_objects:
111 description = "is controlled and monitored over a register bus"
112 else:
113 description = "has register definitions"
115 return f"""\
116.. _{self._module.name}.register_interface:
118{heading}
119{heading_underline}
121This module {description}.
122Please see :download:`separate HTML page <{self._module.name}_regs.html>` for \
123register documentation.
124Register code is generated using `hdl-registers <https://hdl-registers.com>`_{toml_link}.
125"""
127 def get_submodule_rst(
128 self,
129 heading_character: str,
130 heading_character_2: str,
131 exclude_files: set[Path] | None = None,
132 exclude_module_folders: list[str] | None = None,
133 ) -> str:
134 """
135 Get RST code with documentation of the different sub-modules (files) of the module.
136 Contains documentation that is extracted from the file headers, as well as a
137 symbolator symbol of the entity.
139 Arguments:
140 heading_character: Character to use for heading underline.
141 heading_character_2: Character to use for next level of heading underline.
142 exclude_files: Files that shall be excluded from the documentation.
143 exclude_module_folders: Folder names within the module root that shall be
144 excluded from documentation. For example, if you chosen module structure places
145 only netlist build wrappers in the "rtl/" folder within modules, and you do not
146 want them included in the documentation, then pass the argument ["rtl"].
148 Return:
149 RST code with sub-module documentation.
150 """
151 exclude_module_folders = [] if exclude_module_folders is None else exclude_module_folders
152 exclude_module_paths = [self._module.path / name for name in exclude_module_folders]
154 all_builds = self._module.get_build_projects()
156 rst = ""
158 for vhdl_file_path in self._get_vhdl_files(
159 exclude_files=exclude_files, exclude_folders=exclude_module_paths
160 ):
161 netlist_build_base_name = f"{self._module.library_name}.{vhdl_file_path.stem}"
163 # Include all netlist builds whose project name matches this file
164 netlist_builds = [
165 project
166 for project in all_builds
167 if isinstance(project, VivadoNetlistProject)
168 and (
169 project.name == netlist_build_base_name
170 or project.name.startswith(f"{netlist_build_base_name}.")
171 )
172 ]
174 rst += self._get_vhdl_file_rst(
175 vhdl_file_path=vhdl_file_path,
176 heading_character=heading_character,
177 heading_character_2=heading_character_2,
178 netlist_builds=netlist_builds,
179 )
181 return rst
183 def get_rst_document(self, exclude_module_folders: list[str] | None = None) -> str:
184 """
185 Get a complete RST document with the content of :meth:`.get_overview_rst`,
186 :meth:`.get_register_rst`, and :meth:`.get_submodule_rst`, as well as a top level heading.
188 Arguments:
189 exclude_module_folders: Folder names within the module root that shall be
190 excluded from documentation.
192 Return:
193 An RST document.
194 """
195 heading_character_1 = "="
196 heading_character_2 = "-"
197 heading_character_3 = "_"
199 heading = f"Module {self._module.name}"
200 heading_underline = heading_character_1 * len(heading)
202 if self._module.registers is not None:
203 register_note_rst = (
204 "This module has a register interface, so make sure to study the "
205 f":ref:`register interface documentation <{self._module.name}.register_interface>` "
206 "as well as this top-level document.\n"
207 )
208 else:
209 register_note_rst = ""
211 if self._repository_url:
212 url_rst = (
213 f"To browse the source code, visit the "
214 f"`repository on {self._repository_name} <{self._repository_url}>`__.\n"
215 )
216 else:
217 url_rst = ""
219 overview_rst = self.get_overview_rst()
220 registers_rst = self.get_register_rst(heading_character=heading_character_2)
222 submodule_rst = self.get_submodule_rst(
223 heading_character=heading_character_2,
224 heading_character_2=heading_character_3,
225 exclude_module_folders=exclude_module_folders,
226 )
228 return f"""\
230.. _module_{self._module.name}:
232{heading}
233{heading_underline}
235This document contains technical documentation for the ``{self._module.name}`` module.
236{register_note_rst}\
237{url_rst}\
239{overview_rst}
241{registers_rst}
243{submodule_rst}
244"""
246 def create_rst_document(
247 self, output_path: Path, exclude_module_folders: list[str] | None = None
248 ) -> None:
249 """
250 Create an ``.rst`` file in ``output_path`` with the content from :meth:`.get_rst_document`.
251 If the module has registers, the HTML page will also be generated in ``output_path``, so
252 that e.g. sphinx can be run directly.
254 Arguments:
255 output_path: Document will be placed here.
256 exclude_module_folders: Folder names within the module root that shall be
257 excluded from documentation.
258 """
259 register_list = self._module.registers
260 if register_list is not None:
261 HtmlPageGenerator(register_list=register_list, output_folder=output_path).create()
263 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders)
264 create_file(output_path / f"{self._module.name}.rst", contents=rst)
266 def _get_vhdl_files(
267 self, exclude_files: set[Path] | None, exclude_folders: list[Path]
268 ) -> list[Path]:
269 """
270 Get VHDL files that shall be included in the documentation, in order.
271 """
272 # Exclude all file types except VHDL.
273 hdl_files = self._module.get_documentation_files(
274 files_avoid=exclude_files, include_verilog=False, include_systemverilog=False
275 )
277 def file_should_be_included(hdl_file: HdlFile) -> bool:
278 return not file_is_in_directory(hdl_file.path, exclude_folders)
280 vhdl_file_paths = [
281 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file)
282 ]
284 # Sort by file name
285 def sort_key(path: Path) -> str:
286 return path.name
288 return sorted(vhdl_file_paths, key=sort_key)
290 def _get_vhdl_file_rst(
291 self,
292 vhdl_file_path: Path,
293 heading_character: str,
294 heading_character_2: str,
295 netlist_builds: list[VivadoNetlistProject],
296 ) -> str:
297 """
298 Get reStructuredText documentation for a VHDL file.
299 """
300 vhdl_file_documentation = VhdlFileDocumentation(vhdl_file_path)
302 file_rst = vhdl_file_documentation.get_header_rst()
303 file_rst = "" if file_rst is None else file_rst
305 if self._repository_url:
306 url_rst = (
307 f"`View source code on {self._repository_name} "
308 f"<{self._repository_url}/{vhdl_file_path.relative_to(self._module.path)}>`__."
309 )
310 else:
311 url_rst = ""
313 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation)
314 symbolator_rst = "" if symbolator_rst is None else symbolator_rst
316 entity_name = vhdl_file_path.stem
318 resource_utilization_rst = self._get_resource_utilization_rst(
319 entity_name=entity_name,
320 heading_character=heading_character_2,
321 netlist_builds=netlist_builds,
322 )
324 heading = f"{vhdl_file_path.name}"
325 heading_underline = heading_character * len(heading)
327 return f"""
328.. _{self._module.name}.{entity_name}:
330{heading}
331{heading_underline}
333{url_rst}
335{symbolator_rst}
337{file_rst}
339{resource_utilization_rst}
340"""
342 @staticmethod
343 def _get_symbolator_rst(vhdl_file_documentation: VhdlFileDocumentation) -> str:
344 """
345 Get RST for rendering a symbolator component.
346 """
347 component = vhdl_file_documentation.get_symbolator_component()
348 if component is None:
349 return ""
351 indent = " "
352 rst = ".. symbolator::\n\n"
353 rst += indent + component.replace("\n", f"\n{indent}")
355 return rst
357 def _get_resource_utilization_rst( # noqa: C901
358 self, entity_name: str, heading_character: str, netlist_builds: list[VivadoNetlistProject]
359 ) -> str:
360 # First, loop over all netlist builds for this module and assemble information
361 build_generics = []
362 build_checkers = []
363 all_checker_names = set()
364 for netlist_build in netlist_builds:
365 if netlist_build.build_result_checkers:
366 build_generics.append(netlist_build.static_generics)
368 # Create a dictionary for each build, that maps "Checker name": "value"
369 checker_dict = {}
370 for checker in netlist_build.build_result_checkers:
371 # Casting the limit to string yields e.g. "< 4", "4" or "> 4"
372 checker_dict[checker.name] = str(checker.limit)
374 # Add to the set of checker names for this entity.
375 # Note that different netlist builds of the same entity might check a different
376 # set of resources.
377 all_checker_names.add(checker.name)
379 build_checkers.append(checker_dict)
381 # Make RST of the information.
382 # But make a heading and table only if there are any netlist builds with checkers, so
383 # that we don't get an empty table.
384 rst = ""
385 if all_checker_names:
386 heading = "Resource utilization"
387 heading_underline = heading_character * len(heading)
389 module_py_name = f"module_{self._module.name}.py"
390 if self._repository_url:
391 module_py_rst = f"`{module_py_name} <{self._repository_url}/{module_py_name}>`__"
392 else:
393 module_py_rst = f"``{module_py_name}``"
395 rst = f"""
396.. _{self._module.name}.{entity_name}.resource_utilization:
398{heading}
399{heading_underline}
401This entity has `netlist builds <{WEBSITE_URL}/netlist_build.html>`__ set up with
402`automatic size checkers <{WEBSITE_URL}/netlist_build.html#build-result-checkers>`__
403in {module_py_rst}.
404The following table lists the resource utilization for the entity, depending on
405generic configuration.
407.. list-table:: Resource utilization for **{entity_name}** netlist builds.
408 :header-rows: 1
410"""
412 # Sort so that we always get a consistent order in the table, no matter what order
413 # the user has added the checkers.
414 sort_keys = {
415 TotalLuts.name: 0,
416 LogicLuts.name: 1,
417 LutRams.name: 2,
418 Srls.name: 3,
419 Ffs.name: 4,
420 Ramb36.name: 5,
421 Ramb18.name: 6,
422 Ramb.name: 7,
423 Uram.name: 8,
424 DspBlocks.name: 9,
425 MaximumLogicLevel.name: 10,
426 }
427 sorted_checker_names = sorted(all_checker_names, key=lambda name: sort_keys[name])
429 # Fill in the header row
430 rst += " * - Generics\n"
431 for checker_name in sorted_checker_names:
432 rst += f" - {checker_name}\n"
434 # Make one row for each netlist build
435 for build_idx, generic_dict in enumerate(build_generics):
436 generic_strings = [f"{name} = {value}" for name, value in generic_dict.items()]
437 generics_rst = "\n\n ".join(generic_strings)
439 rst += f"""\
440 * - {generics_rst}"""
442 # If the "top" of the project is different than the entity, we assume that it
443 # is a netlist build wrapper.
444 # Add a note to the table about this.
445 # This occurs e.g. in the 'register_file' and 'fifo' modules.
446 if netlist_builds[build_idx].top != entity_name:
447 if generic_strings: # noqa: SIM108
448 # If there is already something in the generic column, this note shall be
449 # placed on a new line.
450 leader = "\n\n "
451 else:
452 # Otherwise, i.e. if the netlist build has no generics set,
453 # the note shall be placed as the first thing. This is the case with
454 # two builds in the register_file module.
455 leader = ""
457 rst += f"""\
458{leader}(Using wrapper
460 {netlist_builds[build_idx].top}.vhd)"""
462 rst += "\n"
464 for checker_name in sorted_checker_names:
465 checker_value = build_checkers[build_idx].get(checker_name, "")
466 rst += f" - {checker_value}\n"
468 return rst