Skip to content

Commit 573bd3d

Browse files
Docs: Cell reference as a custom documenter
Use autodocs to perform cell reference docs generation instead of generating rst files directly. e.g. ``` .. autocell:: simlib.v:$alu :source: :linenos: ```
1 parent de6cbb6 commit 573bd3d

File tree

3 files changed

+526
-9
lines changed

3 files changed

+526
-9
lines changed

docs/source/conf.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@
7070
sys.path += [os.path.dirname(__file__) + "/../"]
7171
extensions.append('util.cmdref')
7272

73-
def setup(sphinx):
74-
from util.RtlilLexer import RtlilLexer
75-
sphinx.add_lexer("RTLIL", RtlilLexer)
73+
# use autodocs
74+
extensions.append('sphinx.ext.autodoc')
75+
extensions.append('util.cellref')
7676

77-
from util.YoscryptLexer import YoscryptLexer
78-
sphinx.add_lexer("yoscrypt", YoscryptLexer)
77+
from sphinx.application import Sphinx
78+
def setup(app: Sphinx) -> None:
79+
from util.RtlilLexer import RtlilLexer
80+
app.add_lexer("RTLIL", RtlilLexer)
81+
82+
from util.YoscryptLexer import YoscryptLexer
83+
app.add_lexer("yoscrypt", YoscryptLexer)

docs/util/cellref.py

+367
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
from pathlib import Path
5+
import re
6+
7+
from typing import Any
8+
from sphinx.application import Sphinx
9+
from sphinx.ext import autodoc
10+
from sphinx.ext.autodoc import Documenter
11+
from sphinx.util import logging
12+
13+
logger = logging.getLogger(__name__)
14+
15+
# cell signature
16+
cell_ext_sig_re = re.compile(
17+
r'''^ (?:([\w._/]+):)? # explicit file name
18+
([\w$._]+?)? # module and/or class name(s)
19+
(?:\.([\w_]+))? # optional: thing name
20+
(::[\w_]+)? # attribute
21+
\s* $ # and nothing more
22+
''', re.VERBOSE)
23+
24+
class SimHelper:
25+
name: str = ""
26+
title: str = ""
27+
ports: str = ""
28+
source: str = ""
29+
desc: list[str]
30+
code: list[str]
31+
group: str = ""
32+
ver: str = "1"
33+
34+
def __init__(self) -> None:
35+
self.desc = []
36+
37+
def simcells_reparse(cell: SimHelper):
38+
# cut manual signature
39+
cell.desc = cell.desc[3:]
40+
41+
# code-block truth table
42+
new_desc = []
43+
indent = ""
44+
for line in cell.desc:
45+
if line.startswith("Truth table:"):
46+
indent = " "
47+
new_desc.pop()
48+
new_desc.extend(["::", ""])
49+
new_desc.append(indent + line)
50+
cell.desc = new_desc
51+
52+
# set version
53+
cell.ver = "2a"
54+
55+
def load_cell_lib(file: Path):
56+
simHelpers: dict[str, SimHelper] = {}
57+
simHelper = SimHelper()
58+
with open(file, "r") as f:
59+
lines = f.readlines()
60+
61+
for lineno, line in enumerate(lines, 1):
62+
line = line.rstrip()
63+
# special comments
64+
if line.startswith("//-"):
65+
simHelper.desc.append(line[4:] if len(line) > 4 else "")
66+
elif line.startswith("//* "):
67+
_, key, val = line.split(maxsplit=2)
68+
setattr(simHelper, key, val)
69+
70+
# code parsing
71+
if line.startswith("module "):
72+
clean_line = line[7:].replace("\\", "").replace(";", "")
73+
simHelper.name, simHelper.ports = clean_line.split(maxsplit=1)
74+
simHelper.code = []
75+
simHelper.source = f'{file.name}:{lineno}'
76+
elif not line.startswith("endmodule"):
77+
line = " " + line
78+
try:
79+
simHelper.code.append(line.replace("\t", " "))
80+
except AttributeError:
81+
# no module definition, ignore line
82+
pass
83+
if line.startswith("endmodule"):
84+
if simHelper.ver == "1" and file.name == "simcells.v":
85+
# default simcells parsing
86+
simcells_reparse(simHelper)
87+
if not simHelper.desc:
88+
# no help
89+
simHelper.desc.append("No help message for this cell type found.\n")
90+
elif simHelper.ver == "1" and file.name == "simlib.v" and simHelper.desc[1].startswith(' '):
91+
simHelper.desc.pop(1)
92+
simHelpers[simHelper.name] = simHelper
93+
simHelper = SimHelper()
94+
return simHelpers
95+
96+
class YosysCellDocumenter(Documenter):
97+
objtype = 'cell'
98+
parsed_libs: dict[Path, dict[str, SimHelper]] = {}
99+
object: SimHelper
100+
101+
option_spec = {
102+
'source': autodoc.bool_option,
103+
'linenos': autodoc.bool_option,
104+
}
105+
106+
@classmethod
107+
def can_document_member(
108+
cls,
109+
member: Any,
110+
membername: str,
111+
isattr: bool,
112+
parent: Any
113+
) -> bool:
114+
sourcename = str(member).split(":")[0]
115+
if not sourcename.endswith(".v"):
116+
return False
117+
if membername == "__source":
118+
return False
119+
120+
def parse_name(self) -> bool:
121+
try:
122+
matched = cell_ext_sig_re.match(self.name)
123+
path, modname, thing, attribute = matched.groups()
124+
except AttributeError:
125+
logger.warning(('invalid signature for auto%s (%r)') % (self.objtype, self.name),
126+
type='cellref')
127+
return False
128+
129+
if not path:
130+
return False
131+
132+
self.modname = modname
133+
self.objpath = [path]
134+
self.attribute = attribute
135+
self.fullname = ((self.modname) + (thing or ''))
136+
137+
return True
138+
139+
def import_object(self, raiseerror: bool = False) -> bool:
140+
# find cell lib file
141+
objpath = Path('/'.join(self.objpath))
142+
if not objpath.exists():
143+
objpath = Path('source') / 'generated' / objpath
144+
145+
# load cell lib
146+
try:
147+
parsed_lib = self.parsed_libs[objpath]
148+
except KeyError:
149+
parsed_lib = load_cell_lib(objpath)
150+
self.parsed_libs[objpath] = parsed_lib
151+
152+
# get cell
153+
try:
154+
self.object = parsed_lib[self.modname]
155+
except KeyError:
156+
return False
157+
158+
self.real_modname = f'{objpath}:{self.modname}'
159+
return True
160+
161+
def get_sourcename(self) -> str:
162+
return self.objpath
163+
164+
def format_name(self) -> str:
165+
return self.object.name
166+
167+
def format_signature(self, **kwargs: Any) -> str:
168+
return f"{self.object.name} {self.object.ports}"
169+
170+
def add_directive_header(self, sig: str) -> None:
171+
domain = getattr(self, 'domain', self.objtype)
172+
directive = getattr(self, 'directivetype', 'def')
173+
name = self.format_name()
174+
sourcename = self.get_sourcename()
175+
cell = self.object
176+
177+
# cell definition
178+
self.add_line(f'.. {domain}:{directive}:: {name}', sourcename)
179+
180+
# options
181+
opt_attrs = ["title", ]
182+
for attr in opt_attrs:
183+
val = getattr(cell, attr, None)
184+
if val:
185+
self.add_line(f' :{attr}: {val}', sourcename)
186+
187+
if self.options.noindex:
188+
self.add_line(' :noindex:', sourcename)
189+
190+
def add_content(self, more_content: Any | None) -> None:
191+
# set sourcename and add content from attribute documentation
192+
sourcename = self.get_sourcename()
193+
startline = int(self.object.source.split(":")[1])
194+
195+
for i, line in enumerate(self.object.desc, startline):
196+
self.add_line(line, sourcename, i)
197+
198+
# add additional content (e.g. from document), if present
199+
if more_content:
200+
for line, src in zip(more_content.data, more_content.items):
201+
self.add_line(line, src[0], src[1])
202+
203+
def filter_members(
204+
self,
205+
members: list[tuple[str, Any]],
206+
want_all: bool
207+
) -> list[tuple[str, Any, bool]]:
208+
return [(x[0], x[1], False) for x in members]
209+
210+
def get_object_members(
211+
self,
212+
want_all: bool
213+
) -> tuple[bool, list[tuple[str, Any]]]:
214+
ret: list[tuple[str, str]] = []
215+
216+
if self.options.source:
217+
ret.append(('__source', self.real_modname))
218+
219+
return False, ret
220+
221+
def document_members(self, all_members: bool = False) -> None:
222+
want_all = (all_members or
223+
self.options.inherited_members)
224+
# find out which members are documentable
225+
members_check_module, members = self.get_object_members(want_all)
226+
227+
# document non-skipped members
228+
memberdocumenters: list[tuple[Documenter, bool]] = []
229+
for (mname, member, isattr) in self.filter_members(members, want_all):
230+
classes = [cls for cls in self.documenters.values()
231+
if cls.can_document_member(member, mname, isattr, self)]
232+
if not classes:
233+
# don't know how to document this member
234+
continue
235+
# prefer the documenter with the highest priority
236+
classes.sort(key=lambda cls: cls.priority)
237+
# give explicitly separated module name, so that members
238+
# of inner classes can be documented
239+
full_mname = self.real_modname + '::' + mname
240+
documenter = classes[-1](self.directive, full_mname, self.indent)
241+
memberdocumenters.append((documenter, isattr))
242+
243+
member_order = self.options.member_order or self.config.autodoc_member_order
244+
memberdocumenters = self.sort_members(memberdocumenters, member_order)
245+
246+
for documenter, isattr in memberdocumenters:
247+
documenter.generate(
248+
all_members=True, real_modname=self.real_modname,
249+
check_module=members_check_module and not isattr)
250+
251+
def generate(
252+
self,
253+
more_content: Any | None = None,
254+
real_modname: str | None = None,
255+
check_module: bool = False,
256+
all_members: bool = False
257+
) -> None:
258+
if not self.parse_name():
259+
# need a cell lib to import from
260+
logger.warning(
261+
f"don't know which cell lib to import for autodocumenting {self.name}",
262+
type = 'cellref'
263+
)
264+
return
265+
266+
self.import_object()
267+
268+
# check __module__ of object (for members not given explicitly)
269+
# if check_module:
270+
# if not self.check_module():
271+
# return
272+
273+
sourcename = self.get_sourcename()
274+
self.add_line('', sourcename)
275+
276+
# format the object's signature, if any
277+
try:
278+
sig = self.format_signature()
279+
except Exception as exc:
280+
logger.warning(('error while formatting signature for %s: %s'),
281+
self.fullname, exc, type='cellref')
282+
return
283+
284+
# generate the directive header and options, if applicable
285+
self.add_directive_header(sig)
286+
self.add_line('', sourcename)
287+
288+
# e.g. the module directive doesn't have content
289+
self.indent += self.content_indent
290+
291+
# add all content (from docstrings, attribute docs etc.)
292+
self.add_content(more_content)
293+
294+
# document members, if possible
295+
self.document_members(all_members)
296+
297+
class YosysCellSourceDocumenter(YosysCellDocumenter):
298+
objtype = 'cellsource'
299+
priority = 20
300+
301+
@classmethod
302+
def can_document_member(
303+
cls,
304+
member: Any,
305+
membername: str,
306+
isattr: bool,
307+
parent: Any
308+
) -> bool:
309+
sourcename = str(member).split(":")[0]
310+
if not sourcename.endswith(".v"):
311+
return False
312+
if membername != "__source":
313+
return False
314+
if isinstance(parent, YosysCellDocumenter):
315+
return True
316+
return False
317+
318+
def add_directive_header(self, sig: str) -> None:
319+
domain = getattr(self, 'domain', 'cell')
320+
directive = getattr(self, 'directivetype', 'source')
321+
name = self.format_name()
322+
sourcename = self.get_sourcename()
323+
cell = self.object
324+
325+
# cell definition
326+
self.add_line(f'.. {domain}:{directive}:: {name}', sourcename)
327+
328+
if self.options.linenos:
329+
self.add_line(f' :source: {cell.source.split(":")[0]}', sourcename)
330+
else:
331+
self.add_line(f' :source: {cell.source}', sourcename)
332+
self.add_line(f' :language: verilog', sourcename)
333+
334+
if self.options.linenos:
335+
startline = int(self.object.source.split(":")[1])
336+
self.add_line(f' :lineno-start: {startline}', sourcename)
337+
338+
if self.options.noindex:
339+
self.add_line(' :noindex:', sourcename)
340+
341+
def add_content(self, more_content: Any | None) -> None:
342+
# set sourcename and add content from attribute documentation
343+
sourcename = self.get_sourcename()
344+
startline = int(self.object.source.split(":")[1])
345+
346+
for i, line in enumerate(self.object.code, startline-1):
347+
self.add_line(line, sourcename, i)
348+
349+
# add additional content (e.g. from document), if present
350+
if more_content:
351+
for line, src in zip(more_content.data, more_content.items):
352+
self.add_line(line, src[0], src[1])
353+
354+
def get_object_members(
355+
self,
356+
want_all: bool
357+
) -> tuple[bool, list[tuple[str, Any]]]:
358+
return False, []
359+
360+
def setup(app: Sphinx) -> dict[str, Any]:
361+
app.setup_extension('sphinx.ext.autodoc')
362+
app.add_autodocumenter(YosysCellDocumenter)
363+
app.add_autodocumenter(YosysCellSourceDocumenter)
364+
return {
365+
'version': '1',
366+
'parallel_read_safe': True,
367+
}

0 commit comments

Comments
 (0)