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

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 

9from __future__ import annotations 

10 

11from typing import TYPE_CHECKING 

12 

13from hdl_registers.generator.html.page import HtmlPageGenerator 

14 

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) 

28 

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 

33 

34if TYPE_CHECKING: 

35 from pathlib import Path 

36 

37 from .hdl_file import HdlFile 

38 from .module import BaseModule 

39 

40 

41class ModuleDocumentation: 

42 """ 

43 Methods for generating a reStructuredText document with module documentation. 

44 The content is extracted from VHDL source file headers. 

45 """ 

46 

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 

65 

66 if (repository_url is None) != (repository_name is None): 

67 raise ValueError("Both or none of the repository arguments must be set") 

68 

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. 

72 

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) 

79 

80 return "" 

81 

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

89 

90 Arguments: 

91 heading_character: Character to use for heading underline. 

92 

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

98 

99 heading = "Register interface" 

100 heading_underline = heading_character * len(heading) 

101 

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

109 

110 if self._module.registers.register_objects: 

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

112 else: 

113 description = "has register definitions" 

114 

115 return f"""\ 

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

117 

118{heading} 

119{heading_underline} 

120 

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

126 

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. 

138 

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

147 

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] 

153 

154 all_builds = self._module.get_build_projects() 

155 

156 rst = "" 

157 

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

162 

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 ] 

173 

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 ) 

180 

181 return rst 

182 

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. 

187 

188 Arguments: 

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

190 excluded from documentation. 

191 

192 Return: 

193 An RST document. 

194 """ 

195 heading_character_1 = "=" 

196 heading_character_2 = "-" 

197 heading_character_3 = "_" 

198 

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

200 heading_underline = heading_character_1 * len(heading) 

201 

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

210 

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

218 

219 overview_rst = self.get_overview_rst() 

220 registers_rst = self.get_register_rst(heading_character=heading_character_2) 

221 

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 ) 

227 

228 return f"""\ 

229 

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

231 

232{heading} 

233{heading_underline} 

234 

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

236{register_note_rst}\ 

237{url_rst}\ 

238 

239{overview_rst} 

240 

241{registers_rst} 

242 

243{submodule_rst} 

244""" 

245 

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. 

253 

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

262 

263 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders) 

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

265 

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 ) 

276 

277 def file_should_be_included(hdl_file: HdlFile) -> bool: 

278 return not file_is_in_directory(hdl_file.path, exclude_folders) 

279 

280 vhdl_file_paths = [ 

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

282 ] 

283 

284 # Sort by file name 

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

286 return path.name 

287 

288 return sorted(vhdl_file_paths, key=sort_key) 

289 

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) 

301 

302 file_rst = vhdl_file_documentation.get_header_rst() 

303 file_rst = "" if file_rst is None else file_rst 

304 

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

312 

313 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation) 

314 symbolator_rst = "" if symbolator_rst is None else symbolator_rst 

315 

316 entity_name = vhdl_file_path.stem 

317 

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 ) 

323 

324 heading = f"{vhdl_file_path.name}" 

325 heading_underline = heading_character * len(heading) 

326 

327 return f""" 

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

329 

330{heading} 

331{heading_underline} 

332 

333{url_rst} 

334 

335{symbolator_rst} 

336 

337{file_rst} 

338 

339{resource_utilization_rst} 

340""" 

341 

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

350 

351 indent = " " 

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

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

354 

355 return rst 

356 

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) 

367 

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) 

373 

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) 

378 

379 build_checkers.append(checker_dict) 

380 

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) 

388 

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

394 

395 rst = f""" 

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

397 

398{heading} 

399{heading_underline} 

400 

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. 

406 

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

408 :header-rows: 1 

409 

410""" 

411 

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

428 

429 # Fill in the header row 

430 rst += " * - Generics\n" 

431 for checker_name in sorted_checker_names: 

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

433 

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) 

438 

439 rst += f"""\ 

440 * - {generics_rst}""" 

441 

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

456 

457 rst += f"""\ 

458{leader}(Using wrapper 

459 

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

461 

462 rst += "\n" 

463 

464 for checker_name in sorted_checker_names: 

465 checker_value = build_checkers[build_idx].get(checker_name, "") 

466 rst += f" - {checker_value}\n" 

467 

468 return rst