Coverage for tsfpga/module_documentation.py: 85%
137 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 23:46 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 23:46 +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 BlockRams,
17 DspBlocks,
18 Ffs,
19 LogicLuts,
20 LutRams,
21 MaximumLogicLevel,
22 Ramb,
23 Ramb18,
24 Ramb36,
25 Srls,
26 TotalLuts,
27 Uram,
28)
30from .about import WEBSITE_URL
31from .system_utils import create_file, file_is_in_directory, read_file
32from .vhdl_file_documentation import VhdlFileDocumentation
33from .vivado.project import VivadoNetlistProject
34from .yosys.project import YosysNetlistBuild
36if TYPE_CHECKING:
37 from pathlib import Path
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: str | None = None,
53 repository_name: str | None = 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 if (repository_url is None) != (repository_name is None):
69 raise ValueError("Both or none of the repository arguments must be set")
71 def get_overview_rst(self) -> str:
72 """
73 Get the contents of the module's ``doc/<name>.rst``, i.e. the module "overview" document.
75 Return:
76 Module overview RST. Empty string if file does not exist.
77 """
78 overview_rst_file = self._module.path / "doc" / f"{self._module.name}.rst"
79 if overview_rst_file.exists():
80 return read_file(overview_rst_file)
82 return ""
84 def get_register_rst(self, heading_character: str) -> str:
85 """
86 Get an RST snippet with a link to the module's register documentation, if available.
87 Note that this will create an RST ``:download:`` statement to the register .html page.
88 When building, the ``.html`` file must be present in the same directory as the
89 ``.rst`` file.
90 This is done automatically by :meth:`.create_rst_document`.
92 Arguments:
93 heading_character: Character to use for heading underline.
95 Return:
96 RST snippet with link to register HTML. Empty string if module does not have registers.
97 """
98 if self._module.registers is None:
99 return ""
101 heading = "Register interface"
102 heading_underline = heading_character * len(heading)
104 if self._repository_url:
105 file_name = f"regs_{self._module.name}.toml"
106 toml_link = (
107 f" based on the `{file_name} <{self._repository_url}/{file_name}>`_ data file"
108 )
109 else:
110 toml_link = ""
112 if self._module.registers.register_objects:
113 description = "is controlled and monitored over a register bus"
114 else:
115 description = "has register definitions"
117 return f"""\
118.. _{self._module.name}.register_interface:
120{heading}
121{heading_underline}
123This module {description}.
124Please see :download:`separate HTML page <{self._module.name}_regs.html>` for \
125register documentation.
126Register code is generated using `hdl-registers <https://hdl-registers.com>`_{toml_link}.
127"""
129 def get_submodule_rst(
130 self,
131 heading_character: str,
132 heading_character_2: str,
133 exclude_files: set[Path] | None = None,
134 exclude_module_folders: list[str] | None = None,
135 ) -> str:
136 """
137 Get RST code with documentation of the different sub-modules (files) of the module.
138 Contains documentation that is extracted from the file headers, as well as a
139 symbolator symbol of the entity.
141 Arguments:
142 heading_character: Character to use for heading underline.
143 heading_character_2: Character to use for next level of heading underline.
144 exclude_files: Files that shall be excluded from the documentation.
145 exclude_module_folders: Folder names within the module root that shall be
146 excluded from documentation. For example, if you chosen module structure places
147 only netlist build wrappers in the "rtl/" folder within modules, and you do not
148 want them included in the documentation, then pass the argument ["rtl"].
150 Return:
151 RST code with sub-module documentation.
152 """
153 exclude_module_folders = [] if exclude_module_folders is None else exclude_module_folders
154 exclude_module_paths = [self._module.path / name for name in exclude_module_folders]
156 all_builds = self._module.get_build_projects()
158 rst = ""
160 for vhdl_file_path in self._get_vhdl_files(
161 exclude_files=exclude_files, exclude_folders=exclude_module_paths
162 ):
163 netlist_build_base_name = f"{self._module.library_name}.{vhdl_file_path.stem}"
165 # Include all netlist builds whose project name matches this file
166 netlist_builds = [
167 project
168 for project in all_builds
169 if isinstance(project, (VivadoNetlistProject, YosysNetlistBuild))
170 and (
171 project.name == netlist_build_base_name
172 or project.name.startswith(f"{netlist_build_base_name}.")
173 )
174 ]
176 rst += self._get_vhdl_file_rst(
177 vhdl_file_path=vhdl_file_path,
178 heading_character=heading_character,
179 heading_character_2=heading_character_2,
180 netlist_builds=netlist_builds,
181 )
183 return rst
185 def get_rst_document(self, exclude_module_folders: list[str] | None = None) -> str:
186 """
187 Get a complete RST document with the content of :meth:`.get_overview_rst`,
188 :meth:`.get_register_rst`, and :meth:`.get_submodule_rst`, as well as a top level heading.
190 Arguments:
191 exclude_module_folders: Folder names within the module root that shall be
192 excluded from documentation.
194 Return:
195 An RST document.
196 """
197 heading_character_1 = "="
198 heading_character_2 = "-"
199 heading_character_3 = "_"
201 heading = f"Module {self._module.name}"
202 heading_underline = heading_character_1 * len(heading)
204 if self._module.registers is not None:
205 register_note_rst = (
206 "This module has a register interface, so make sure to study the "
207 f":ref:`register interface documentation <{self._module.name}.register_interface>` "
208 "as well as this top-level document.\n"
209 )
210 else:
211 register_note_rst = ""
213 if self._repository_url:
214 url_rst = (
215 f"To browse the source code, visit the "
216 f"`repository on {self._repository_name} <{self._repository_url}>`__.\n"
217 )
218 else:
219 url_rst = ""
221 overview_rst = self.get_overview_rst()
222 registers_rst = self.get_register_rst(heading_character=heading_character_2)
224 submodule_rst = self.get_submodule_rst(
225 heading_character=heading_character_2,
226 heading_character_2=heading_character_3,
227 exclude_module_folders=exclude_module_folders,
228 )
230 return f"""\
232.. _module_{self._module.name}:
234{heading}
235{heading_underline}
237This document contains technical documentation for the ``{self._module.name}`` module.
238{register_note_rst}\
239{url_rst}\
241{overview_rst}
243{registers_rst}
245{submodule_rst}
246"""
248 def create_rst_document(
249 self, output_path: Path, exclude_module_folders: list[str] | None = None
250 ) -> None:
251 """
252 Create an ``.rst`` file in ``output_path`` with the content from :meth:`.get_rst_document`.
253 If the module has registers, the HTML page will also be generated in ``output_path``, so
254 that e.g. sphinx can be run directly.
256 Arguments:
257 output_path: Document will be placed here.
258 exclude_module_folders: Folder names within the module root that shall be
259 excluded from documentation.
260 """
261 register_list = self._module.registers
262 if register_list is not None:
263 HtmlPageGenerator(register_list=register_list, output_folder=output_path).create()
265 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders)
266 create_file(output_path / f"{self._module.name}.rst", contents=rst)
268 def _get_vhdl_files(
269 self, exclude_files: set[Path] | None, exclude_folders: list[Path]
270 ) -> list[Path]:
271 """
272 Get VHDL files that shall be included in the documentation, in order.
273 """
274 # Exclude all file types except VHDL.
275 hdl_files = self._module.get_documentation_files(
276 files_avoid=exclude_files, include_verilog=False, include_systemverilog=False
277 )
279 def file_should_be_included(hdl_file: HdlFile) -> bool:
280 return not file_is_in_directory(hdl_file.path, exclude_folders)
282 vhdl_file_paths = [
283 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file)
284 ]
286 # Sort by file name
287 def sort_key(path: Path) -> str:
288 return path.name
290 return sorted(vhdl_file_paths, key=sort_key)
292 def _get_vhdl_file_rst(
293 self,
294 vhdl_file_path: Path,
295 heading_character: str,
296 heading_character_2: str,
297 netlist_builds: list[VivadoNetlistProject | YosysNetlistBuild],
298 ) -> str:
299 """
300 Get reStructuredText documentation for a VHDL file.
301 """
302 vhdl_file_documentation = VhdlFileDocumentation(vhdl_file_path)
304 file_rst = vhdl_file_documentation.get_header_rst()
305 file_rst = "" if file_rst is None else file_rst
307 if self._repository_url:
308 url_rst = (
309 f"`View source code on {self._repository_name} "
310 f"<{self._repository_url}/{vhdl_file_path.relative_to(self._module.path)}>`__."
311 )
312 else:
313 url_rst = ""
315 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation)
316 symbolator_rst = "" if symbolator_rst is None else symbolator_rst
318 entity_name = vhdl_file_path.stem
320 resource_utilization_rst = self._get_resource_utilization_rst(
321 entity_name=entity_name,
322 heading_character=heading_character_2,
323 netlist_builds=netlist_builds,
324 )
326 heading = f"{vhdl_file_path.name}"
327 heading_underline = heading_character * len(heading)
329 return f"""
330.. _{self._module.name}.{entity_name}:
332{heading}
333{heading_underline}
335{url_rst}
337{symbolator_rst}
339{file_rst}
341{resource_utilization_rst}
342"""
344 @staticmethod
345 def _get_symbolator_rst(vhdl_file_documentation: VhdlFileDocumentation) -> str:
346 """
347 Get RST for rendering a symbolator component.
348 """
349 component = vhdl_file_documentation.get_symbolator_component()
350 if component is None:
351 return ""
353 indent = " "
354 rst = ".. symbolator::\n\n"
355 rst += indent + component.replace("\n", f"\n{indent}")
357 return rst
359 def _get_resource_utilization_rst( # noqa: C901
360 self,
361 entity_name: str,
362 heading_character: str,
363 netlist_builds: list[VivadoNetlistProject | YosysNetlistBuild],
364 ) -> str:
365 # First, loop over all netlist builds for this module and assemble information
366 build_generics = []
367 build_checkers = []
368 all_checker_names = set()
369 for netlist_build in netlist_builds:
370 if netlist_build.build_result_checkers:
371 build_generics.append(netlist_build.static_generics)
373 # Create a dictionary for each build, that maps "Checker name": "value"
374 checker_dict = {}
375 for checker in netlist_build.build_result_checkers:
376 # Casting the limit to string yields e.g. "< 4", "4" or "> 4"
377 checker_dict[checker.name] = str(checker.limit)
379 # Add to the set of checker names for this entity.
380 # Note that different netlist builds of the same entity might check a different
381 # set of resources.
382 all_checker_names.add(checker.name)
384 build_checkers.append(checker_dict)
386 # Make RST of the information.
387 # But make a heading and table only if there are any netlist builds with checkers, so
388 # that we don't get an empty table.
389 rst = ""
390 if all_checker_names:
391 heading = "Resource utilization"
392 heading_underline = heading_character * len(heading)
394 module_py_name = f"module_{self._module.name}.py"
395 if self._repository_url:
396 module_py_rst = f"`{module_py_name} <{self._repository_url}/{module_py_name}>`__"
397 else:
398 module_py_rst = f"``{module_py_name}``"
400 rst = f"""
401.. _{self._module.name}.{entity_name}.resource_utilization:
403{heading}
404{heading_underline}
406This entity has `netlist builds <{WEBSITE_URL}/netlist_build.html>`__ set up with
407`automatic size checkers <{WEBSITE_URL}/netlist_build.html#build-result-checkers>`__
408in {module_py_rst}.
409The following table lists the resource utilization for the entity, depending on
410generic configuration.
412.. list-table:: Resource utilization for **{entity_name}** netlist builds.
413 :header-rows: 1
415"""
417 # Sort so that we always get a consistent order in the table, no matter what order
418 # the user has added the checkers.
419 sort_order = (
420 TotalLuts.name,
421 LogicLuts.name,
422 LutRams.name,
423 Srls.name,
424 Ffs.name,
425 Ramb36.name,
426 Ramb18.name,
427 Ramb.name,
428 BlockRams.name,
429 Uram.name,
430 DspBlocks.name,
431 MaximumLogicLevel.name,
432 )
433 # Any checker that is not listed above, e.g. a user-defined one, is sorted last
434 # rather than raising an exception.
435 sorted_checker_names = sorted(
436 all_checker_names,
437 key=lambda name: sort_order.index(name) if name in sort_order else len(sort_order),
438 )
440 # Fill in the header row
441 rst += " * - Generics\n"
442 for checker_name in sorted_checker_names:
443 rst += f" - {checker_name}\n"
445 # Make one row for each netlist build
446 for build_idx, generic_dict in enumerate(build_generics):
447 generic_strings = [f"{name} = {value}" for name, value in generic_dict.items()]
448 generics_rst = "\n\n ".join(generic_strings)
450 rst += f"""\
451 * - {generics_rst}"""
453 # If the "top" of the project is different than the entity, we assume that it
454 # is a netlist build wrapper.
455 # Add a note to the table about this.
456 # This occurs e.g. in the 'register_file' and 'fifo' modules.
457 if netlist_builds[build_idx].top != entity_name:
458 if generic_strings: # noqa: SIM108
459 # If there is already something in the generic column, this note shall be
460 # placed on a new line.
461 leader = "\n\n "
462 else:
463 # Otherwise, i.e. if the netlist build has no generics set,
464 # the note shall be placed as the first thing. This is the case with
465 # two builds in the register_file module.
466 leader = ""
468 rst += f"""\
469{leader}(Using wrapper
471 {netlist_builds[build_idx].top}.vhd)"""
473 rst += "\n"
475 for checker_name in sorted_checker_names:
476 checker_value = build_checkers[build_idx].get(checker_name, "")
477 rst += f" - {checker_value}\n"
479 return rst