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

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 BlockRams, 

17 DspBlocks, 

18 Ffs, 

19 LogicLuts, 

20 LutRams, 

21 MaximumLogicLevel, 

22 Ramb, 

23 Ramb18, 

24 Ramb36, 

25 Srls, 

26 TotalLuts, 

27 Uram, 

28) 

29 

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 

35 

36if TYPE_CHECKING: 

37 from pathlib import Path 

38 

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

67 

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

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

70 

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. 

74 

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) 

81 

82 return "" 

83 

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

91 

92 Arguments: 

93 heading_character: Character to use for heading underline. 

94 

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

100 

101 heading = "Register interface" 

102 heading_underline = heading_character * len(heading) 

103 

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

111 

112 if self._module.registers.register_objects: 

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

114 else: 

115 description = "has register definitions" 

116 

117 return f"""\ 

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

119 

120{heading} 

121{heading_underline} 

122 

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

128 

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. 

140 

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

149 

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] 

155 

156 all_builds = self._module.get_build_projects() 

157 

158 rst = "" 

159 

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

164 

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 ] 

175 

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 ) 

182 

183 return rst 

184 

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. 

189 

190 Arguments: 

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

192 excluded from documentation. 

193 

194 Return: 

195 An RST document. 

196 """ 

197 heading_character_1 = "=" 

198 heading_character_2 = "-" 

199 heading_character_3 = "_" 

200 

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

202 heading_underline = heading_character_1 * len(heading) 

203 

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

212 

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

220 

221 overview_rst = self.get_overview_rst() 

222 registers_rst = self.get_register_rst(heading_character=heading_character_2) 

223 

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 ) 

229 

230 return f"""\ 

231 

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

233 

234{heading} 

235{heading_underline} 

236 

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

238{register_note_rst}\ 

239{url_rst}\ 

240 

241{overview_rst} 

242 

243{registers_rst} 

244 

245{submodule_rst} 

246""" 

247 

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. 

255 

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

264 

265 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders) 

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

267 

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 ) 

278 

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

280 return not file_is_in_directory(hdl_file.path, exclude_folders) 

281 

282 vhdl_file_paths = [ 

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

284 ] 

285 

286 # Sort by file name 

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

288 return path.name 

289 

290 return sorted(vhdl_file_paths, key=sort_key) 

291 

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) 

303 

304 file_rst = vhdl_file_documentation.get_header_rst() 

305 file_rst = "" if file_rst is None else file_rst 

306 

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

314 

315 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation) 

316 symbolator_rst = "" if symbolator_rst is None else symbolator_rst 

317 

318 entity_name = vhdl_file_path.stem 

319 

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 ) 

325 

326 heading = f"{vhdl_file_path.name}" 

327 heading_underline = heading_character * len(heading) 

328 

329 return f""" 

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

331 

332{heading} 

333{heading_underline} 

334 

335{url_rst} 

336 

337{symbolator_rst} 

338 

339{file_rst} 

340 

341{resource_utilization_rst} 

342""" 

343 

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

352 

353 indent = " " 

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

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

356 

357 return rst 

358 

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) 

372 

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) 

378 

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) 

383 

384 build_checkers.append(checker_dict) 

385 

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) 

393 

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

399 

400 rst = f""" 

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

402 

403{heading} 

404{heading_underline} 

405 

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. 

411 

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

413 :header-rows: 1 

414 

415""" 

416 

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 ) 

439 

440 # Fill in the header row 

441 rst += " * - Generics\n" 

442 for checker_name in sorted_checker_names: 

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

444 

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) 

449 

450 rst += f"""\ 

451 * - {generics_rst}""" 

452 

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

467 

468 rst += f"""\ 

469{leader}(Using wrapper 

470 

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

472 

473 rst += "\n" 

474 

475 for checker_name in sorted_checker_names: 

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

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

478 

479 return rst