[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