Docs: Preliminary autocellgroup usage

Remove `/source/cell` from .gitignore.
Add a few initial cell pages.
Add YosysCellGroup documenter and cell:group directive.
Update Documenters to use nested json.
Better nested tocs for group.module.source layout.
This commit is contained in:
Krystine Sherwin 2024-05-21 18:10:20 +12:00
parent 5a4a4191af
commit b127ac07f8
No known key found for this signature in database
9 changed files with 331 additions and 130 deletions

1
docs/.gitignore vendored
View File

@ -1,6 +1,5 @@
/build/
/source/cmd
/source/cell
/source/generated
/source/_images/**/*.log
/source/_images/**/*.aux

View File

@ -0,0 +1,5 @@
.. autocellgroup:: gate_other
:caption: Other gate-level cells
:members:
:source:
:linenos:

View File

@ -0,0 +1,5 @@
.. autocellgroup:: word_other
:caption: Other word-level cells
:members:
:source:
:linenos:

View File

@ -0,0 +1,50 @@
.. role:: verilog(code)
:language: Verilog
Unary operators
---------------
All unary RTL cells have one input port ``A`` and one output port ``Y``. They
also have the following parameters:
``A_SIGNED``
Set to a non-zero value if the input ``A`` is signed and therefore should be
sign-extended when needed.
``A_WIDTH``
The width of the input port ``A``.
``Y_WIDTH``
The width of the output port ``Y``.
.. table:: Cell types for unary operators with their corresponding Verilog expressions.
================== ==============
Verilog Cell Type
================== ==============
:verilog:`Y = ~A` `$not`
:verilog:`Y = +A` `$pos`
:verilog:`Y = -A` `$neg`
:verilog:`Y = &A` `$reduce_and`
:verilog:`Y = |A` `$reduce_or`
:verilog:`Y = ^A` `$reduce_xor`
:verilog:`Y = ~^A` `$reduce_xnor`
:verilog:`Y = |A` `$reduce_bool`
:verilog:`Y = !A` `$logic_not`
================== ==============
For the unary cells that output a logical value (`$reduce_and`, `$reduce_or`,
`$reduce_xor`, `$reduce_xnor`, `$reduce_bool`, `$logic_not`), when the
``Y_WIDTH`` parameter is greater than 1, the output is zero-extended, and only
the least significant bit varies.
Note that `$reduce_or` and `$reduce_bool` actually represent the same logic
function. But the HDL frontends generate them in different situations. A
`$reduce_or` cell is generated when the prefix ``|`` operator is being used. A
`$reduce_bool` cell is generated when a bit vector is used as a condition in an
``if``-statement or ``?:``-expression.
.. autocellgroup:: unary
:members:
:source:
:linenos:

View File

@ -3,7 +3,6 @@ Gate-level cells
.. toctree::
:caption: Gate-level cells
:maxdepth: 1
:glob:
:maxdepth: 2
/cell/gate_*
/cell/gate_other

View File

@ -2,8 +2,8 @@ Word-level cells
----------------
.. toctree::
:caption: Word-level cells
:maxdepth: 1
:maxdepth: 2
:glob:
/cell/word_*
/cell/word_unary
/cell/word_other

View File

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
# cell signature
cell_ext_sig_re = re.compile(
r'''^ (?:([^:\s]+):)? # explicit file name
r'''^ ([^:\s]+::)? # optional group or file name
([\w$._]+?) # module name
(?:\.([\w_]+))? # optional: thing name
(::[\w_]+)? # attribute
@ -25,7 +25,7 @@ cell_ext_sig_re = re.compile(
@dataclass
class YosysCell:
cell: str
name: str
title: str
ports: str
source: str
@ -34,22 +34,26 @@ class YosysCell:
inputs: list[str]
outputs: list[str]
properties: dict[str, bool]
class YosysCellDocumenter(Documenter):
objtype = 'cell'
object: YosysCell
class YosysCellGroupDocumenter(Documenter):
objtype = 'cellgroup'
priority = 10
object: tuple[str, list[str]]
lib_key = 'groups'
option_spec = {
'caption': autodoc.annotation_option,
'members': autodoc.members_option,
'source': autodoc.bool_option,
'linenos': autodoc.bool_option,
}
__cell_lib: dict[str, YosysCell] | None = None
__cell_lib: dict[str, list[str] | dict[str]] | None = None
@property
def cell_lib(self) -> dict[str, YosysCell]:
def cell_lib(self) -> dict[str, list[str] | dict[str]]:
if not self.__cell_lib:
self.__cell_lib = {}
cells_obj: dict[str, list[dict[str, list[dict[str]]]]]
cells_obj: dict[str, dict[str, list[str] | dict[str]]]
try:
with open(self.config.cells_json, "r") as f:
cells_obj = json.loads(f.read())
@ -60,12 +64,10 @@ class YosysCellDocumenter(Documenter):
subtype = 'cell_lib'
)
else:
for group in cells_obj.get("groups", []):
for cell in group.get("cells", []):
yosysCell = YosysCell(**cell)
self.__cell_lib[yosysCell.cell] = yosysCell
for (name, obj) in cells_obj.get(self.lib_key, {}).items():
self.__cell_lib[name] = obj
return self.__cell_lib
@classmethod
def can_document_member(
cls,
@ -74,75 +76,51 @@ class YosysCellDocumenter(Documenter):
isattr: bool,
parent: Any
) -> bool:
sourcename = str(member).split(":")[0]
if not sourcename.endswith(".v"):
return False
if membername == "__source":
return False
return False
def parse_name(self) -> bool:
try:
matched = cell_ext_sig_re.match(self.name)
path, modname, thing, attribute = matched.groups()
except AttributeError:
logger.warning(('invalid signature for auto%s (%r)') % (self.objtype, self.name),
type='cellref')
return False
self.modname = modname
self.objpath = [path]
self.attribute = attribute
self.fullname = ((self.modname) + (thing or ''))
if not self.options.caption:
self.content_indent = ''
self.fullname = self.modname = self.name
return True
def import_object(self, raiseerror: bool = False) -> bool:
# get cell
try:
self.object = self.cell_lib[self.modname]
self.object = (self.modname, self.cell_lib[self.modname])
except KeyError:
if raiseerror:
raise
return False
self.real_modname = self.modname or ''
self.real_modname = self.modname
return True
def get_sourcename(self) -> str:
return self.object.source.split(":")[0]
return self.env.doc2path(self.env.docname)
def format_name(self) -> str:
return self.object.cell
return self.options.caption or ''
def format_signature(self, **kwargs: Any) -> str:
return f"{self.object.cell} {self.object.ports}"
return self.modname
def add_directive_header(self, sig: str) -> None:
domain = getattr(self, 'domain', self.objtype)
directive = getattr(self, 'directivetype', 'def')
domain = getattr(self, 'domain', 'cell')
directive = getattr(self, 'directivetype', 'group')
name = self.format_name()
sourcename = self.get_sourcename()
cell = self.object
cell_list = self.object
# cell definition
self.add_line(f'.. {domain}:{directive}:: {name}', sourcename)
# options
opt_attrs = ["title", ]
for attr in opt_attrs:
val = getattr(cell, attr, None)
if val:
self.add_line(f' :{attr}: {val}', sourcename)
self.add_line(f'.. {domain}:{directive}:: {sig}', sourcename)
self.add_line(f' :caption: {name}', sourcename)
if self.options.noindex:
self.add_line(' :noindex:', sourcename)
def add_content(self, more_content: Any | None) -> None:
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
startline = int(self.object.source.split(":")[1])
for i, line in enumerate(self.object.desc.splitlines(), startline):
self.add_line(line, sourcename, i)
# groups have no native content
# add additional content (e.g. from document), if present
if more_content:
for line, src in zip(more_content.data, more_content.items):
@ -161,14 +139,25 @@ class YosysCellDocumenter(Documenter):
) -> tuple[bool, list[tuple[str, Any]]]:
ret: list[tuple[str, str]] = []
if self.options.source:
ret.append(('__source', self.real_modname))
if want_all:
for member in self.object[1]:
ret.append((member, self.modname))
else:
memberlist = self.options.members or []
for name in memberlist:
if name in self.object:
ret.append((name, self.modname))
else:
logger.warning(('unknown module mentioned in :members: option: '
f'group {self.modname}, module {name}'),
type='cellref')
return False, ret
def document_members(self, all_members: bool = False) -> None:
want_all = (all_members or
self.options.inherited_members)
self.options.inherited_members or
self.options.members is autodoc.ALL)
# find out which members are documentable
members_check_module, members = self.get_object_members(want_all)
@ -184,7 +173,7 @@ class YosysCellDocumenter(Documenter):
classes.sort(key=lambda cls: cls.priority)
# give explicitly separated module name, so that members
# of inner classes can be documented
full_mname = self.real_modname + '::' + mname
full_mname = self.format_signature() + '::' + mname
documenter = classes[-1](self.directive, full_mname, self.indent)
memberdocumenters.append((documenter, isattr))
@ -247,6 +236,101 @@ class YosysCellDocumenter(Documenter):
# document members, if possible
self.document_members(all_members)
class YosysCellDocumenter(YosysCellGroupDocumenter):
objtype = 'cell'
priority = 15
object: YosysCell
lib_key = 'cells'
@classmethod
def can_document_member(
cls,
member: Any,
membername: str,
isattr: bool,
parent: Any
) -> bool:
if membername == "__source":
return False
if not membername.startswith('$'):
return False
return isinstance(parent, YosysCellGroupDocumenter)
def parse_name(self) -> bool:
try:
matched = cell_ext_sig_re.match(self.name)
group, modname, thing, attribute = matched.groups()
except AttributeError:
logger.warning(('invalid signature for auto%s (%r)') % (self.objtype, self.name),
type='cellref')
return False
self.modname = modname
self.groupname = group or ''
self.attribute = attribute or ''
self.fullname = ((self.modname) + (thing or ''))
return True
def import_object(self, raiseerror: bool = False) -> bool:
if super().import_object(raiseerror):
self.object = YosysCell(self.modname, **self.object[1])
return True
return False
def get_sourcename(self) -> str:
return self.object.source.split(":")[0]
def format_name(self) -> str:
return self.object.name
def format_signature(self, **kwargs: Any) -> str:
return self.groupname + self.fullname + self.attribute
def add_directive_header(self, sig: str) -> None:
domain = getattr(self, 'domain', self.objtype)
directive = getattr(self, 'directivetype', 'def')
name = self.format_name()
sourcename = self.get_sourcename()
cell = self.object
# cell definition
self.add_line(f'.. {domain}:{directive}:: {sig}', sourcename)
# options
opt_attrs = ["title", ]
for attr in opt_attrs:
val = getattr(cell, attr, None)
if val:
self.add_line(f' :{attr}: {val}', sourcename)
if self.options.noindex:
self.add_line(' :noindex:', sourcename)
def add_content(self, more_content: Any | None) -> None:
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
startline = int(self.object.source.split(":")[1])
for i, line in enumerate(self.object.desc.splitlines(), startline):
self.add_line(line, sourcename, i)
# add additional content (e.g. from document), if present
if more_content:
for line, src in zip(more_content.data, more_content.items):
self.add_line(line, src[0], src[1])
def get_object_members(
self,
want_all: bool
) -> tuple[bool, list[tuple[str, Any]]]:
ret: list[tuple[str, str]] = []
if self.options.source:
ret.append(('__source', self.real_modname))
return False, ret
class YosysCellSourceDocumenter(YosysCellDocumenter):
objtype = 'cellsource'
priority = 20
@ -273,7 +357,7 @@ class YosysCellSourceDocumenter(YosysCellDocumenter):
cell = self.object
# cell definition
self.add_line(f'.. {domain}:{directive}:: {name}', sourcename)
self.add_line(f'.. {domain}:{directive}:: {sig}', sourcename)
if self.options.linenos:
self.add_line(f' :source: {cell.source.split(":")[0]}', sourcename)
@ -312,6 +396,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.setup_extension('sphinx.ext.autodoc')
app.add_autodocumenter(YosysCellDocumenter)
app.add_autodocumenter(YosysCellSourceDocumenter)
app.add_autodocumenter(YosysCellGroupDocumenter)
return {
'version': '1',
'parallel_read_safe': True,

View File

@ -15,7 +15,16 @@ from sphinx.directives.code import container_wrapper
from sphinx.util.nodes import make_refnode
from sphinx import addnodes
class TocNode(ObjectDescription):
class TocNode(ObjectDescription):
def add_target_and_index(
self,
name: str,
sig: str,
signode: addnodes.desc_signature
) -> None:
idx = ".".join(name.split("::"))
signode['ids'].append(idx)
def _object_hierarchy_parts(self, sig_node: addnodes.desc_signature) -> tuple[str, ...]:
if 'fullname' not in sig_node:
return ()
@ -34,17 +43,13 @@ class TocNode(ObjectDescription):
config = self.env.app.config
objtype = sig_node.parent.get('objtype')
if config.add_function_parentheses and objtype in {'function', 'method'}:
parens = '()'
else:
parens = ''
*parents, name = sig_node['_toc_parts']
if config.toc_object_entries_show_parents == 'domain':
return sig_node.get('fullname', name) + parens
return sig_node.get('tocname', name)
if config.toc_object_entries_show_parents == 'hide':
return name + parens
return name
if config.toc_object_entries_show_parents == 'all':
return '.'.join(parents + [name + parens])
return '.'.join(parents + [name])
return ''
class CommandNode(TocNode):
@ -59,11 +64,10 @@ class CommandNode(TocNode):
}
def handle_signature(self, sig, signode: addnodes.desc_signature):
fullname = sig
signode['fullname'] = fullname
signode['fullname'] = sig
signode += addnodes.desc_addname(text="yosys> help ")
signode += addnodes.desc_name(text=sig)
return fullname
return signode['fullname']
def add_target_and_index(self, name_cls, sig, signode):
signode['ids'].append(type(self).name + '-' + sig)
@ -82,11 +86,47 @@ class CommandNode(TocNode):
type(self).name + '-' + sig,
0))
class CellNode(CommandNode):
class CellNode(TocNode):
"""A custom node that describes an internal cell."""
name = 'cell'
option_spec = {
'title': directives.unchanged,
'ports': directives.unchanged,
}
def handle_signature(self, sig: str, signode: addnodes.desc_signature):
signode['fullname'] = sig
signode['tocname'] = tocname = sig.split('::')[-1]
signode += addnodes.desc_addname(text="yosys> help ")
signode += addnodes.desc_name(text=tocname)
return signode['fullname']
def add_target_and_index(
self,
name: str,
sig: str,
signode: addnodes.desc_signature
) -> None:
idx = ".".join(name.split("::"))
signode['ids'].append(idx)
if 'noindex' not in self.options:
tocname: str = signode.get('tocname', name)
tagmap = self.env.domaindata[self.domain]['obj2tag']
tagmap[name] = list(self.options.get('tags', '').split(' '))
title: str = self.options.get('title', sig)
titlemap = self.env.domaindata[self.domain]['obj2title']
titlemap[name] = title
objs = self.env.domaindata[self.domain]['objects']
# (name, sig, typ, docname, anchor, prio)
objs.append((name,
tocname,
title,
self.env.docname,
idx,
0))
class CellSourceNode(TocNode):
"""A custom code block for including cell source."""
@ -104,21 +144,12 @@ class CellSourceNode(TocNode):
signode: addnodes.desc_signature
) -> str:
language = self.options.get('language')
fullname = sig + "::" + language
signode['fullname'] = fullname
signode['fullname'] = sig
signode['tocname'] = f"{sig.split('::')[-2]} {language}"
signode += addnodes.desc_name(text="Simulation model")
signode += addnodes.desc_sig_space()
signode += addnodes.desc_addname(text=f'({language})')
return fullname
def add_target_and_index(
self,
name: str,
sig: str,
signode: addnodes.desc_signature
) -> None:
idx = f'{".".join(self.name.split(":"))}.{sig}'
signode['ids'].append(idx)
return signode['fullname']
def run(self) -> list[Node]:
"""Override run to parse content as a code block"""
@ -192,6 +223,29 @@ class CellSourceNode(TocNode):
return [self.indexnode, node, literal]
class CellGroupNode(TocNode):
name = 'cellgroup'
option_spec = {
'caption': directives.unchanged,
}
def add_target_and_index(self, name: str, sig: str, signode: addnodes.desc_signature) -> None:
if self.options.get('caption', ''):
super().add_target_and_index(name, sig, signode)
def handle_signature(
self,
sig,
signode: addnodes.desc_signature
) -> str:
signode['fullname'] = fullname = sig
caption = self.options.get("caption", fullname)
if caption:
signode['tocname'] = caption
signode += addnodes.desc_name(text=caption)
return fullname
class TagIndex(Index):
"""A custom directive that creates a tag matrix."""
@ -368,6 +422,7 @@ class CellDomain(CommandDomain):
directives = {
'def': CellNode,
'source': CellSourceNode,
'group': CellGroupNode,
}
indices = {

View File

@ -958,7 +958,8 @@ struct HelpPass : public Pass {
json.entry("version", "Yosys internal cells");
json.entry("generator", yosys_version_str);
dict<string, dict<string, pair<SimHelper, CellType>>> groups;
dict<string, vector<string>> groups;
dict<string, pair<SimHelper, CellType>> cells;
// iterate over cells
bool raise_error = false;
@ -966,59 +967,61 @@ struct HelpPass : public Pass {
auto name = it.first.str();
if (cell_help_messages.contains(name)) {
auto cell_help = cell_help_messages.get(name);
dict<string, pair<SimHelper, CellType>> *cell_group;
if (groups.count(cell_help.group) != 0) {
cell_group = &groups.at(cell_help.group);
auto group_cells = &groups.at(cell_help.group);
group_cells->push_back(name);
} else {
cell_group = new dict<string, pair<SimHelper, CellType>>();
groups.emplace(cell_help.group, *cell_group);
auto group_cells = new vector<string>(1, name);
groups.emplace(cell_help.group, *group_cells);
}
auto cell_pair = pair<SimHelper, CellType>(cell_help, it.second);
cell_group->emplace(name, cell_pair);
cells.emplace(name, cell_pair);
} else {
log("ERROR: Missing cell help for cell '%s'.\n", name.c_str());
raise_error |= true;
}
}
for (auto &it : cell_help_messages.cell_help) {
if (cells.count(it.first) == 0) {
log_warning("Found cell model '%s' without matching cell type.\n", it.first.c_str());
}
}
// write to json
json.name("groups");
json.begin_array();
json.name("groups"); json.begin_object();
groups.sort();
for (auto &it : groups) {
json.begin_object();
json.name("group"); json.value(it.first.c_str());
json.name("cells"); json.begin_array();
for (auto &it2 : it.second) {
auto ch = it2.second.first;
auto ct = it2.second.second;
json.begin_object();
json.name("cell"); json.value(ch.name);
json.name("title"); json.value(ch.title);
json.name("ports"); json.value(ch.ports);
json.name("source"); json.value(ch.source);
json.name("desc"); json.value(ch.desc);
json.name("code"); json.value(ch.code);
json.name("inputs"); json.begin_array();
for (auto &input : ct.inputs)
json.value(input.c_str());
json.end_array();
json.name("outputs"); json.begin_array();
for (auto &output : ct.outputs)
json.value(output.c_str());
json.end_array();
dict<string, bool> prop_dict = {
{"is_evaluable", ct.is_evaluable},
{"is_combinatorial", ct.is_combinatorial},
{"is_synthesizable", ct.is_synthesizable},
};
json.name("properties"); json.value(prop_dict);
json.end_object();
}
json.end_array();
json.name(it.first.c_str()); json.value(it.second);
}
json.end_object();
json.name("cells"); json.begin_object();
cells.sort();
for (auto &it : cells) {
auto ch = it.second.first;
auto ct = it.second.second;
json.name(ch.name.c_str()); json.begin_object();
json.name("title"); json.value(ch.title);
json.name("ports"); json.value(ch.ports);
json.name("source"); json.value(ch.source);
json.name("desc"); json.value(ch.desc);
json.name("code"); json.value(ch.code);
vector<string> inputs, outputs;
for (auto &input : ct.inputs)
inputs.push_back(input.str());
json.name("inputs"); json.value(inputs);
for (auto &output : ct.outputs)
outputs.push_back(output.str());
json.name("outputs"); json.value(outputs);
dict<string, bool> prop_dict = {
{"is_evaluable", ct.is_evaluable},
{"is_combinatorial", ct.is_combinatorial},
{"is_synthesizable", ct.is_synthesizable},
};
json.name("properties"); json.value(prop_dict);
json.end_object();
}
json.end_array();
json.end_object();
json.end_object();
return raise_error;