diff --git a/docs/.gitignore b/docs/.gitignore index 78a1f48e1a0..65bbcdeaeb8 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,6 +1,5 @@ /build/ /source/cmd -/source/cell /source/generated /source/_images/**/*.log /source/_images/**/*.aux diff --git a/docs/source/cell/gate_other.rst b/docs/source/cell/gate_other.rst new file mode 100644 index 00000000000..7696ba824e9 --- /dev/null +++ b/docs/source/cell/gate_other.rst @@ -0,0 +1,5 @@ +.. autocellgroup:: gate_other + :caption: Other gate-level cells + :members: + :source: + :linenos: diff --git a/docs/source/cell/word_other.rst b/docs/source/cell/word_other.rst new file mode 100644 index 00000000000..9e5ac2a3778 --- /dev/null +++ b/docs/source/cell/word_other.rst @@ -0,0 +1,5 @@ +.. autocellgroup:: word_other + :caption: Other word-level cells + :members: + :source: + :linenos: diff --git a/docs/source/cell/word_unary.rst b/docs/source/cell/word_unary.rst new file mode 100644 index 00000000000..59f03397c83 --- /dev/null +++ b/docs/source/cell/word_unary.rst @@ -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: diff --git a/docs/source/cell_gate.rst b/docs/source/cell_gate.rst index 74dd57bc727..0e977c58798 100644 --- a/docs/source/cell_gate.rst +++ b/docs/source/cell_gate.rst @@ -3,7 +3,6 @@ Gate-level cells .. toctree:: :caption: Gate-level cells - :maxdepth: 1 - :glob: + :maxdepth: 2 - /cell/gate_* + /cell/gate_other diff --git a/docs/source/cell_word.rst b/docs/source/cell_word.rst index f7205e6eee3..9b781132468 100644 --- a/docs/source/cell_word.rst +++ b/docs/source/cell_word.rst @@ -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 diff --git a/docs/util/cellref.py b/docs/util/cellref.py index 0d5c9ba563a..3bba278763b 100644 --- a/docs/util/cellref.py +++ b/docs/util/cellref.py @@ -16,7 +16,7 @@ # 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 @@ @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 @@ def cell_lib(self) -> dict[str, YosysCell]: 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 @@ def can_document_member( 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 @@ def get_object_members( ) -> 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 @@ def document_members(self, all_members: bool = False) -> None: 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 @@ def generate( # 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 @@ def add_directive_header(self, sig: str) -> None: 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, diff --git a/docs/util/cmdref.py b/docs/util/cmdref.py index df1f3fb21ab..6fbd813726d 100644 --- a/docs/util/cmdref.py +++ b/docs/util/cmdref.py @@ -15,7 +15,16 @@ 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 @@ def _toc_entry_name(self, sig_node: addnodes.desc_signature) -> str: 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 @@ def add_target_and_index(self, name_cls, sig, signode): 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 @@ def handle_signature( 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 @@ def run(self) -> list[Node]: 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 = { diff --git a/kernel/register.cc b/kernel/register.cc index 4e8e62741a0..bc49f4cb914 100644 --- a/kernel/register.cc +++ b/kernel/register.cc @@ -958,7 +958,8 @@ struct HelpPass : public Pass { json.entry("version", "Yosys internal cells"); json.entry("generator", yosys_version_str); - dict>> groups; + dict> groups; + dict> 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> *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>(); - groups.emplace(cell_help.group, *cell_group); + auto group_cells = new vector(1, name); + groups.emplace(cell_help.group, *group_cells); } auto cell_pair = pair(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 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 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 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;