https://github.com/rnk updated https://github.com/llvm/llvm-project/pull/228591
>From b72f6387cba4e31acfdfa32ebe092c0c85b67dce Mon Sep 17 00:00:00 2001 From: Reid Kleckner <[email protected]> Date: Wed, 30 Sep 2026 23:37:00 +0000 Subject: [PATCH] [clang][docs] Render attribute syntaxes from a Sphinx role Implements stage 2 of https://github.com/llvm/llvm-project/issues/227907 by generating a JSON syntax database from Attr.td and rendering supported syntaxes through the clang-attr-syntaxes Sphinx role. --- clang/docs/CMakeLists.txt | 2 + clang/docs/_extensions/clang_attr_syntaxes.py | 147 ++++++++++++++++++ clang/docs/conf.py | 3 + clang/test/TableGen/attr-docs.td | 30 ++-- clang/utils/TableGen/ClangAttrEmitter.cpp | 140 +++++++++++------ clang/utils/TableGen/TableGen.cpp | 6 + clang/utils/TableGen/TableGenBackends.h | 2 + 7 files changed, 275 insertions(+), 55 deletions(-) create mode 100644 clang/docs/_extensions/clang_attr_syntaxes.py diff --git a/clang/docs/CMakeLists.txt b/clang/docs/CMakeLists.txt index d7f4cfcd485e0..b074641fe3800 100644 --- a/clang/docs/CMakeLists.txt +++ b/clang/docs/CMakeLists.txt @@ -110,6 +110,7 @@ if (LLVM_ENABLE_SPHINX) set(clang_generated_docs AttributeReference.md _generated/AttributeReference.split.md + _generated/AttributeReferenceSyntaxes.json DiagnosticsReference.md AMDGPUBuiltinReference.rst ClangCommandLineReference.md @@ -156,6 +157,7 @@ if (LLVM_ENABLE_SPHINX) set(attr_reference_split_input "${CMAKE_CURRENT_BINARY_DIR}/_generated/AttributeReference.split.md") set(attr_reference_split_stamp "${CMAKE_CURRENT_BINARY_DIR}/_generated/AttributeReferenceSplit.stamp") gen_sphinx_file_from_td(_generated/AttributeReference.split.md -gen-attr-docs ../include/clang/Basic/Attr.td "") + gen_sphinx_file_from_td(_generated/AttributeReferenceSyntaxes.json -gen-attr-doc-syntaxes ../include/clang/Basic/Attr.td "${docs_targets}") add_custom_command( OUTPUT "${attr_reference_split_stamp}" COMMAND "${CMAKE_COMMAND}" -E rm -rf diff --git a/clang/docs/_extensions/clang_attr_syntaxes.py b/clang/docs/_extensions/clang_attr_syntaxes.py new file mode 100644 index 0000000000000..be5762faa997d --- /dev/null +++ b/clang/docs/_extensions/clang_attr_syntaxes.py @@ -0,0 +1,147 @@ +"""Sphinx support for Clang attribute syntax tables.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from docutils import nodes +from sphinx.errors import ExtensionError +from sphinx.transforms import SphinxTransform + + +class clang_attr_syntaxes(nodes.General, nodes.Element): + """Placeholder node for a Clang attribute syntax table.""" + + pass + + +def clang_attr_syntaxes_role( + name, rawtext, text, lineno, inliner, options=None, content=None +): + """Create a placeholder node keyed by documentation record name.""" + node = clang_attr_syntaxes(rawtext) + node["doc"] = text.strip() + return [node], [] + + +def load_attribute_syntaxes(app): + """Load the generated Clang attribute syntax manifest.""" + manifest_path = Path(app.srcdir) / app.config.clang_attr_syntaxes_manifest + try: + manifest = json.loads(manifest_path.read_text()) + except OSError as e: + raise ExtensionError( + f"unable to read Clang attribute syntaxes manifest: {manifest_path}" + ) from e + + syntaxes = {} + for entry in manifest["attributes"] + manifest["undocumented"]: + syntaxes[entry["doc"]] = entry + return syntaxes + + +def has_visible_syntax(entry): + """Return whether an attribute entry should render a syntax table.""" + for syntax in entry["syntaxes"]: + if syntax.get("supported"): + return True + if syntax.get("spellings"): + return True + return False + + +def make_entry(children): + """Wrap cell content in a table entry paragraph.""" + entry = nodes.entry() + paragraph = nodes.paragraph() + paragraph.extend(children) + entry += paragraph + return entry + + +def make_spelling_entry(spellings): + """Build a table entry containing one literal line per spelling.""" + entry = nodes.entry() + if not spellings: + entry += nodes.paragraph() + return entry + + line_block = nodes.line_block() + for spelling in spellings: + line = nodes.line() + line += nodes.literal(text=spelling) + line_block += line + entry += line_block + return entry + + +def make_table(entry): + """Build the rendered syntax table for one attribute entry.""" + table = nodes.table() + table["classes"].append("clang-attr-syntaxes") + + # Define one equal-width column for each supported syntax family. + syntaxes = entry["syntaxes"] + tgroup = nodes.tgroup(cols=len(syntaxes)) + table += tgroup + for _ in syntaxes: + tgroup += nodes.colspec(colwidth=1) + + # Render syntax family names as the table header row. + thead = nodes.thead() + tgroup += thead + header_row = nodes.row() + thead += header_row + for syntax in syntaxes: + header_row += make_entry([nodes.Text(syntax["name"])]) + + # Render support markers or spelling lists as the single body row. + tbody = nodes.tbody() + tgroup += tbody + body_row = nodes.row() + tbody += body_row + for syntax in syntaxes: + if "supported" in syntax: + body_row += make_entry([nodes.Text("Yes")] if syntax["supported"] else []) + else: + body_row += make_spelling_entry(syntax["spellings"]) + + return table + + +class ClangAttrSyntaxesTransform(SphinxTransform): + """Replace Clang attribute syntax placeholders with tables.""" + + default_priority = 500 + + def apply(self): + """Run the transform over all placeholder nodes in the document.""" + syntaxes = load_attribute_syntaxes(self.app) + for node in list(self.document.findall(clang_attr_syntaxes)): + doc = node["doc"] + if doc not in syntaxes: + raise ExtensionError(f"unknown Clang attribute syntax section: {doc}") + + parent = node.parent + replacement = ( + [] + if not has_visible_syntax(syntaxes[doc]) + else make_table(syntaxes[doc]) + ) + if isinstance(parent, nodes.paragraph) and len(parent.children) == 1: + parent.replace_self(replacement) + else: + node.replace_self(replacement) + + +def setup(app): + """Register the Clang attribute syntax extension with Sphinx.""" + app.add_config_value( + "clang_attr_syntaxes_manifest", + "_generated/AttributeReferenceSyntaxes.json", + "env", + ) + app.add_role("clang-attr-syntaxes", clang_attr_syntaxes_role) + app.add_transform(ClangAttrSyntaxesTransform) + return {"version": "1.0", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/clang/docs/conf.py b/clang/docs/conf.py index 6cf4134f1f2bc..f10cbf59af537 100644 --- a/clang/docs/conf.py +++ b/clang/docs/conf.py @@ -15,6 +15,8 @@ import sys, os from datetime import date +sys.path.insert(0, os.path.abspath("_extensions")) + from llvm_sphinx import * # see llvm-project/utils/docs/README.md globals().update(common_conf(tags)) @@ -32,6 +34,7 @@ "llvm_sphinx.ext.absolute_links", "llvm_sphinx.ext.ghlinks", "llvm_sphinx.ext.checks", + "clang_attr_syntaxes", ] llvm_sphinx_doc_url_prefixes = ("https://clang.llvm.org/docs/",) diff --git a/clang/test/TableGen/attr-docs.td b/clang/test/TableGen/attr-docs.td index c60e7333f5c1a..e490f3a64ba1a 100644 --- a/clang/test/TableGen/attr-docs.td +++ b/clang/test/TableGen/attr-docs.td @@ -1,4 +1,5 @@ -// RUN: clang-tblgen -gen-attr-docs -I%p/../../include %s -o - 2>&1 | FileCheck %s +// RUN: clang-tblgen -gen-attr-docs -I%p/../../include %s -o - 2>&1 | FileCheck %s --check-prefix=DOCS +// RUN: clang-tblgen -gen-attr-doc-syntaxes -I%p/../../include %s -o - 2>&1 | FileCheck %s --check-prefix=SYNTAX include "clang/Basic/Attr.td" @@ -21,12 +22,21 @@ def TestSplitAttr : InheritableAttr { let Documentation = [TestSplitDocs]; } -// CHECK: //--- AttributeReference.md -// CHECK: :::{include} AttributeReference/TestAttributes.md -// CHECK-NEXT: ::: -// CHECK: //--- AttributeReference/TestAttributes.md -// CHECK: ## Test Attributes -// CHECK: Test category introduction. -// CHECK: ### test_split_attr -// CHECK: `test_split_attr` -// CHECK: Test split documentation content. +// DOCS: //--- AttributeReference.md +// DOCS: :::{include} AttributeReference/TestAttributes.md +// DOCS-NEXT: ::: +// DOCS: //--- AttributeReference/TestAttributes.md +// DOCS: ## Test Attributes +// DOCS: Test category introduction. +// DOCS: ### test_split_attr +// DOCS: {clang-attr-syntaxes}`TestSplitDocs` +// DOCS: Test split documentation content. + +// SYNTAX: "doc": "TestSplitDocs" +// SYNTAX: "heading": "test_split_attr" +// SYNTAX: "category": "Test Attributes" +// SYNTAX: "attributes": [ +// SYNTAX-NEXT: "TestSplitAttr" +// SYNTAX: "name": "GNU" +// SYNTAX: "spellings": [ +// SYNTAX-NEXT: "test_split_attr" diff --git a/clang/utils/TableGen/ClangAttrEmitter.cpp b/clang/utils/TableGen/ClangAttrEmitter.cpp index 4b03c759c2758..69800c9a3ee8c 100644 --- a/clang/utils/TableGen/ClangAttrEmitter.cpp +++ b/clang/utils/TableGen/ClangAttrEmitter.cpp @@ -24,6 +24,7 @@ #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSwitch.h" #include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/raw_ostream.h" #include "llvm/TableGen/Error.h" #include "llvm/TableGen/Record.h" @@ -5587,53 +5588,16 @@ createDocumentationData(const Record &Documentation, return Doc; } -static void WriteDocumentation(const RecordKeeper &Records, - const DocumentationData &Doc, raw_ostream &OS) { +static void WriteDocumentation(const DocumentationData &Doc, raw_ostream &OS) { if (StringRef Label = Doc.Documentation->getValueAsString("Label"); !Label.empty()) OS << "(" << Label << ")=\n\n"; OS << "### " << Doc.Heading << "\n\n"; - if (Doc.SupportedSpellings.hasSpelling()) { - // List what spelling syntaxes the attribute supports. - // Note: "#pragma clang attribute" is handled outside the spelling kinds - // loop so it must be last. - OS << ":::{list-table} Supported Syntaxes\n"; - OS << ":header-rows: 1\n\n"; - OS << "* - GNU\n"; - OS << " - C++11\n"; - OS << " - C23\n"; - OS << " - `__declspec`\n"; - OS << " - Keyword\n"; - OS << " - `#pragma`\n"; - OS << " - HLSL Annotation\n"; - OS << " - `#pragma clang attribute`\n"; - OS << "*"; - for (size_t Kind = 0; Kind != NumSpellingKinds; ++Kind) { - SpellingKind K = (SpellingKind)Kind; - // TODO: List Microsoft (IDL-style attribute) spellings once we fully - // support them. - if (K == SpellingKind::Microsoft) - continue; - - OS << " - "; - bool PrintedAny = false; - for (StringRef Spelling : Doc.SupportedSpellings[K]) { - if (PrintedAny) - OS << " <br/> "; - OS << "`" << Spelling << "`"; - PrintedAny = true; - } - - OS << "\n "; - } - - OS << " - "; - if (getPragmaAttributeSupport(Records).isAttributedSupported( - *Doc.Attributes.front())) - OS << "Yes"; - OS << "\n:::\n\n"; - } + StringRef DocName = Doc.Documentation->getName(); + if (DocName == "Undocumented") + DocName = Doc.Attributes.front()->getName(); + OS << "{clang-attr-syntaxes}`" << DocName << "`\n\n"; // If the attribute is deprecated, print a message about it, and possibly // provide a replacement attribute. @@ -5647,13 +5611,44 @@ static void WriteDocumentation(const RecordKeeper &Records, OS << "\n\n"; } - const StringRef ContentStr = Doc.Documentation->getValueAsString("Content"); // Trim leading and trailing newlines and spaces. - OS << ContentStr.trim(); + OS << Doc.Documentation->getValueAsString("Content").trim(); OS << "\n\n\n"; } +static void WriteGeneratedSyntaxes(const RecordKeeper &Records, + const DocumentationData &Doc, + json::OStream &J) { + J.attributeArray("syntaxes", [&] { + auto EmitSpellings = [&](StringRef Name, SpellingKind Kind) { + J.object([&] { + J.attribute("name", Name); + J.attributeArray("spellings", [&] { + for (StringRef Spelling : Doc.SupportedSpellings[Kind]) + J.value(Spelling); + }); + }); + }; + + EmitSpellings("GNU", SpellingKind::GNU); + EmitSpellings("C++11", SpellingKind::CXX11); + EmitSpellings("C23", SpellingKind::C23); + // TODO: List Microsoft (IDL-style attribute) spellings once we fully + // support them. + EmitSpellings("__declspec", SpellingKind::Declspec); + EmitSpellings("Keyword", SpellingKind::Keyword); + EmitSpellings("#pragma", SpellingKind::Pragma); + EmitSpellings("HLSL Annotation", SpellingKind::HLSLAnnotation); + J.object([&] { + J.attribute("name", "#pragma clang attribute"); + J.attribute("supported", + getPragmaAttributeSupport(Records).isAttributedSupported( + *Doc.Attributes.front())); + }); + }); +} + using AttributesByDocumentation = DenseMap<const Record *, std::vector<const Record *>>; using DocumentationByCategory = @@ -5776,10 +5771,65 @@ void EmitClangAttrDocs(const RecordKeeper &Records, raw_ostream &OS) { << getAttributeReferenceCategoryFileName(CategoryName) << "\n"; WriteCategoryHeader(CategoryDocs.front().Category, OS); for (const DocumentationData &Doc : CategoryDocs) - WriteDocumentation(Records, Doc, OS); + WriteDocumentation(Doc, OS); } } +void EmitClangAttrDocSyntaxes(const RecordKeeper &Records, raw_ostream &OS) { + DocumentationByCategory DocsByCategory = + GatherAttributeDocumentation(Records); + json::OStream J(OS, /*IndentSize=*/2); + + auto EmitEntry = [&](const DocumentationData &Doc) { + J.object([&] { + StringRef DocName = Doc.Documentation->getName(); + if (DocName == "Undocumented") + DocName = Doc.Attributes.front()->getName(); + J.attribute("doc", DocName); + J.attribute("heading", Doc.Heading); + J.attribute("category", Doc.Category->getValueAsString("Name")); + J.attributeArray("attributes", [&] { + for (const Record *Attribute : Doc.Attributes) + J.value(Attribute->getName()); + }); + WriteGeneratedSyntaxes(Records, Doc, J); + }); + }; + + auto EmitCategory = [&](StringRef CategoryName, + const AttributesByDocumentation &AttributesByDoc) { + std::vector<DocumentationData> CategoryDocs; + for (const auto &[Doc, Attributes] : AttributesByDoc) { + if (CategoryName == "Undocumented") { + for (const Record *Attribute : Attributes) + CategoryDocs.push_back( + createDocumentationData(*Doc, ArrayRef(Attribute))); + } else { + CategoryDocs.push_back(createDocumentationData(*Doc, Attributes)); + } + } + llvm::sort(CategoryDocs, + [](const DocumentationData &L, const DocumentationData &R) { + return L.Heading < R.Heading; + }); + for (const DocumentationData &Doc : CategoryDocs) + EmitEntry(Doc); + }; + + J.object([&] { + J.attributeArray("attributes", [&] { + for (const auto &[CategoryName, AttributesByDoc] : DocsByCategory) + if (CategoryName != "Undocumented") + EmitCategory(CategoryName, AttributesByDoc); + }); + J.attributeArray("undocumented", [&] { + auto Undocumented = DocsByCategory.find("Undocumented"); + if (Undocumented != DocsByCategory.end()) + EmitCategory(Undocumented->first, Undocumented->second); + }); + }); +} + void EmitTestPragmaAttributeSupportedAttributes(const RecordKeeper &Records, raw_ostream &OS) { PragmaClangAttributeSupport Support = getPragmaAttributeSupport(Records); diff --git a/clang/utils/TableGen/TableGen.cpp b/clang/utils/TableGen/TableGen.cpp index 46e05f2207909..65b13b50c3ab7 100644 --- a/clang/utils/TableGen/TableGen.cpp +++ b/clang/utils/TableGen/TableGen.cpp @@ -122,6 +122,7 @@ enum ActionType { GenHLSLAliasIntrinsics, GenHLSLInlineIntrinsics, GenAttrDocs, + GenAttrDocSyntaxes, GenBuiltinDocs, GenDiagDocs, GenOptDocs, @@ -361,6 +362,8 @@ cl::opt<ActionType> Action( "hlsl_intrinsics.h"), clEnumValN(GenAttrDocs, "gen-attr-docs", "Generate attribute documentation"), + clEnumValN(GenAttrDocSyntaxes, "gen-attr-doc-syntaxes", + "Generate attribute documentation syntax database"), clEnumValN(GenBuiltinDocs, "gen-builtin-docs", "Generate builtin documentation"), clEnumValN(GenDiagDocs, "gen-diag-docs", @@ -681,6 +684,9 @@ bool ClangTableGenMain(raw_ostream &OS, const RecordKeeper &Records) { case GenAttrDocs: EmitClangAttrDocs(Records, OS); break; + case GenAttrDocSyntaxes: + EmitClangAttrDocSyntaxes(Records, OS); + break; case GenBuiltinDocs: EmitClangBuiltinDocs(Records, OS); break; diff --git a/clang/utils/TableGen/TableGenBackends.h b/clang/utils/TableGen/TableGenBackends.h index 764e6aff11cb4..19742c5257264 100644 --- a/clang/utils/TableGen/TableGenBackends.h +++ b/clang/utils/TableGen/TableGenBackends.h @@ -200,6 +200,8 @@ void EmitHLSLInlineIntrinsics(const llvm::RecordKeeper &Records, void EmitClangAttrDocs(const llvm::RecordKeeper &Records, llvm::raw_ostream &OS); +void EmitClangAttrDocSyntaxes(const llvm::RecordKeeper &Records, + llvm::raw_ostream &OS); void EmitClangDiagDocs(const llvm::RecordKeeper &Records, llvm::raw_ostream &OS); void EmitClangOptDocs(const llvm::RecordKeeper &Records, llvm::raw_ostream &OS); _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
