[llvm-branch-commits] [clang] [clang][docs] Render attribute syntaxes from a Sphinx role (PR #228591)

Reid Kleckner via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Sat Oct 10 09:13:40 PDT 2026


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 <rkleckner at nvidia.com>
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);



More information about the llvm-branch-commits mailing list