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

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# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10from pathlib import Path 

11from typing import TYPE_CHECKING, Optional 

12 

13# Third party libraries 

14from hdl_registers.generator.html.page import HtmlPageGenerator 

15 

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) 

30 

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 

36 

37if TYPE_CHECKING: 

38 # Local folder libraries 

39 from .hdl_file import HdlFile 

40 from .module import BaseModule 

41 

42 

43class ModuleDocumentation: 

44 """ 

45 Methods for generating a reStructuredText document with module documentation. 

46 The content is extracted from VHDL source file headers. 

47 """ 

48 

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 

67 

68 assert (repository_url is None) == ( 

69 repository_name is None 

70 ), "Both or none of the repository arguments must be set" 

71 

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. 

75 

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) 

82 

83 return "" 

84 

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`. 

92 

93 Arguments: 

94 heading_character: Character to use for heading underline. 

95 

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 "" 

101 

102 heading = "Register interface" 

103 heading_underline = heading_character * len(heading) 

104 

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 = "" 

112 

113 if self._module.registers.register_objects: 

114 description = "is controlled and monitored over a register bus" 

115 else: 

116 description = "has register definitions" 

117 

118 return f"""\ 

119.. _{self._module.name}.register_interface: 

120 

121{heading} 

122{heading_underline} 

123 

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""" 

129 

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. 

141 

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"]. 

150 

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] 

156 

157 all_builds = self._module.get_build_projects() 

158 

159 rst = "" 

160 

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}" 

165 

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) 

174 

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 ) 

181 

182 return rst 

183 

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. 

188 

189 Arguments: 

190 exclude_module_folders: Folder names within the module root that shall be 

191 excluded from documentation. 

192 

193 Return: 

194 An RST document. 

195 """ 

196 heading_character_1 = "=" 

197 heading_character_2 = "-" 

198 heading_character_3 = "_" 

199 

200 heading = f"Module {self._module.name}" 

201 heading_underline = heading_character_1 * len(heading) 

202 

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 = "" 

211 

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 = "" 

219 

220 overview_rst = self.get_overview_rst() 

221 registers_rst = self.get_register_rst(heading_character=heading_character_2) 

222 

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 ) 

228 

229 rst = f"""\ 

230 

231.. _module_{self._module.name}: 

232 

233{heading} 

234{heading_underline} 

235 

236This document contains technical documentation for the ``{self._module.name}`` module. 

237{register_note_rst}\ 

238{url_rst}\ 

239 

240{overview_rst} 

241 

242{registers_rst} 

243 

244{submodule_rst} 

245""" 

246 

247 return rst 

248 

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. 

256 

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() 

265 

266 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders) 

267 create_file(output_path / f"{self._module.name}.rst", contents=rst) 

268 

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 ) 

279 

280 def file_should_be_included(hdl_file: "HdlFile") -> bool: 

281 if file_is_in_directory(hdl_file.path, exclude_folders): 

282 return False 

283 

284 return True 

285 

286 vhdl_file_paths = [ 

287 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file) 

288 ] 

289 

290 # Sort by file name 

291 def sort_key(path: Path) -> str: 

292 return path.name 

293 

294 vhdl_files = sorted(vhdl_file_paths, key=sort_key) 

295 

296 return vhdl_files 

297 

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) 

309 

310 file_rst = vhdl_file_documentation.get_header_rst() 

311 file_rst = "" if file_rst is None else file_rst 

312 

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 = "" 

320 

321 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation) 

322 symbolator_rst = "" if symbolator_rst is None else symbolator_rst 

323 

324 entity_name = vhdl_file_path.stem 

325 

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 ) 

331 

332 heading = f"{vhdl_file_path.name}" 

333 heading_underline = heading_character * len(heading) 

334 

335 rst = f""" 

336.. _{self._module.name}.{entity_name}: 

337 

338{heading} 

339{heading_underline} 

340 

341{url_rst} 

342 

343{symbolator_rst} 

344 

345{file_rst} 

346 

347{resource_utilization_rst} 

348""" 

349 

350 return rst 

351 

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 "" 

360 

361 indent = " " 

362 rst = ".. symbolator::\n\n" 

363 rst += indent + component.replace("\n", f"\n{indent}") 

364 

365 return rst 

366 

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) 

377 

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) 

383 

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) 

388 

389 build_checkers.append(checker_dict) 

390 

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) 

398 

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}``" 

404 

405 rst = f""" 

406.. _{self._module.name}.{entity_name}.resource_utilization: 

407 

408{heading} 

409{heading_underline} 

410 

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. 

416 

417.. list-table:: Resource utilization for **{entity_name}** netlist builds. 

418 :header-rows: 1 

419 

420""" 

421 

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]) 

438 

439 # Fill in the header row 

440 rst += " * - Generics\n" 

441 for checker_name in sorted_checker_names: 

442 rst += f" - {checker_name}\n" 

443 

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) 

448 

449 rst += f"""\ 

450 * - {generics_rst}""" 

451 

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 = "" 

466 

467 rst += f"""\ 

468{leader}(Using wrapper 

469 

470 {netlist_builds[build_idx].top}.vhd)""" 

471 

472 rst += "\n" 

473 

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" 

481 

482 return rst