xref: /llvm-project/mlir/tools/mlir-tblgen/OpDocGen.cpp (revision e5521fae944c1f3f6905ce5902819a5c9be7f802)
1 //===- OpDocGen.cpp - MLIR operation documentation generator --------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 //
9 // OpDocGen uses the description of operations to generate documentation for the
10 // operations.
11 //
12 //===----------------------------------------------------------------------===//
13 
14 #include "DialectGenUtilities.h"
15 #include "DocGenUtilities.h"
16 #include "OpGenHelpers.h"
17 #include "mlir/Support/IndentedOstream.h"
18 #include "mlir/TableGen/AttrOrTypeDef.h"
19 #include "mlir/TableGen/Attribute.h"
20 #include "mlir/TableGen/GenInfo.h"
21 #include "mlir/TableGen/Operator.h"
22 #include "llvm/ADT/DenseMap.h"
23 #include "llvm/ADT/SetVector.h"
24 #include "llvm/ADT/StringExtras.h"
25 #include "llvm/ADT/StringRef.h"
26 #include "llvm/Support/CommandLine.h"
27 #include "llvm/Support/FormatVariadic.h"
28 #include "llvm/Support/Regex.h"
29 #include "llvm/Support/Signals.h"
30 #include "llvm/TableGen/Error.h"
31 #include "llvm/TableGen/Record.h"
32 #include "llvm/TableGen/TableGenBackend.h"
33 
34 #include <set>
35 #include <string>
36 
37 using namespace llvm;
38 using namespace mlir;
39 using namespace mlir::tblgen;
40 using mlir::tblgen::Operator;
41 
42 //===----------------------------------------------------------------------===//
43 // Commandline Options
44 //===----------------------------------------------------------------------===//
45 static cl::OptionCategory
46     docCat("Options for -gen-(attrdef|typedef|enum|op|dialect)-doc");
47 cl::opt<std::string>
48     stripPrefix("strip-prefix",
49                 cl::desc("Strip prefix of the fully qualified names"),
50                 cl::init("::mlir::"), cl::cat(docCat));
51 cl::opt<bool> allowHugoSpecificFeatures(
52     "allow-hugo-specific-features",
53     cl::desc("Allows using features specific to Hugo"), cl::init(false),
54     cl::cat(docCat));
55 
56 void mlir::tblgen::emitSummary(StringRef summary, raw_ostream &os) {
57   if (!summary.empty()) {
58     StringRef trimmed = summary.trim();
59     char first = std::toupper(trimmed.front());
60     StringRef rest = trimmed.drop_front();
61     os << "\n_" << first << rest << "_\n\n";
62   }
63 }
64 
65 // Emit the description by aligning the text to the left per line (e.g.,
66 // removing the minimum indentation across the block).
67 //
68 // This expects that the description in the tablegen file is already formatted
69 // in a way the user wanted but has some additional indenting due to being
70 // nested in the op definition.
71 void mlir::tblgen::emitDescription(StringRef description, raw_ostream &os) {
72   raw_indented_ostream ros(os);
73   StringRef trimmed = description.rtrim(" \t");
74   ros.printReindented(trimmed);
75   if (!trimmed.ends_with("\n"))
76     ros << "\n";
77 }
78 
79 void mlir::tblgen::emitDescriptionComment(StringRef description,
80                                           raw_ostream &os, StringRef prefix) {
81   if (description.empty())
82     return;
83   raw_indented_ostream ros(os);
84   StringRef trimmed = description.rtrim(" \t");
85   ros.printReindented(trimmed, (Twine(prefix) + "/// ").str());
86   if (!trimmed.ends_with("\n"))
87     ros << "\n";
88 }
89 
90 // Emits `str` with trailing newline if not empty.
91 static void emitIfNotEmpty(StringRef str, raw_ostream &os) {
92   if (!str.empty()) {
93     emitDescription(str, os);
94     os << "\n";
95   }
96 }
97 
98 /// Emit the given named constraint.
99 template <typename T>
100 static void emitNamedConstraint(const T &it, raw_ostream &os) {
101   if (!it.name.empty())
102     os << "| `" << it.name << "`";
103   else
104     os << "&laquo;unnamed&raquo;";
105   os << " | " << it.constraint.getSummary() << "\n";
106 }
107 
108 //===----------------------------------------------------------------------===//
109 // Operation Documentation
110 //===----------------------------------------------------------------------===//
111 
112 /// Emit the assembly format of an operation.
113 static void emitAssemblyFormat(StringRef opName, StringRef format,
114                                raw_ostream &os) {
115   os << "\nSyntax:\n\n```\noperation ::= `" << opName << "` ";
116 
117   // Print the assembly format aligned.
118   unsigned indent = strlen("operation ::= ");
119   std::pair<StringRef, StringRef> split = format.split('\n');
120   os << split.first.trim() << "\n";
121   do {
122     split = split.second.split('\n');
123     StringRef formatChunk = split.first.trim();
124     if (!formatChunk.empty())
125       os.indent(indent) << formatChunk << "\n";
126   } while (!split.second.empty());
127   os << "```\n\n";
128 }
129 
130 /// Place `text` between backticks so that the Markdown processor renders it as
131 /// inline code.
132 static std::string backticks(const std::string &text) {
133   return '`' + text + '`';
134 }
135 
136 static void emitOpTraitsDoc(const Operator &op, raw_ostream &os) {
137   // TODO: We should link to the trait/documentation of it. That also means we
138   // should add descriptions to traits that can be queried.
139   // Collect using set to sort effects, interfaces & traits.
140   std::set<std::string> effects, interfaces, traits;
141   for (auto &trait : op.getTraits()) {
142     if (isa<PredTrait>(&trait))
143       continue;
144 
145     std::string name = trait.getDef().getName().str();
146     StringRef ref = name;
147     StringRef traitName = trait.getDef().getValueAsString("trait");
148     traitName.consume_back("::Trait");
149     traitName.consume_back("::Impl");
150     if (ref.starts_with("anonymous_"))
151       name = traitName.str();
152     if (isa<InterfaceTrait>(&trait)) {
153       if (trait.getDef().isSubClassOf("SideEffectsTraitBase")) {
154         auto effectName = trait.getDef().getValueAsString("baseEffectName");
155         effectName.consume_front("::");
156         effectName.consume_front("mlir::");
157         std::string effectStr;
158         raw_string_ostream os(effectStr);
159         os << effectName << "{";
160         auto list = trait.getDef().getValueAsListOfDefs("effects");
161         interleaveComma(list, os, [&](const Record *rec) {
162           StringRef effect = rec->getValueAsString("effect");
163           effect.consume_front("::");
164           effect.consume_front("mlir::");
165           os << effect << " on " << rec->getValueAsString("resource");
166         });
167         os << "}";
168         effects.insert(backticks(effectStr));
169         name.append(formatv(" ({0})", traitName).str());
170       }
171       interfaces.insert(backticks(name));
172       continue;
173     }
174 
175     traits.insert(backticks(name));
176   }
177   if (!traits.empty()) {
178     interleaveComma(traits, os << "\nTraits: ");
179     os << "\n";
180   }
181   if (!interfaces.empty()) {
182     interleaveComma(interfaces, os << "\nInterfaces: ");
183     os << "\n";
184   }
185   if (!effects.empty()) {
186     interleaveComma(effects, os << "\nEffects: ");
187     os << "\n";
188   }
189 }
190 
191 static StringRef resolveAttrDescription(const Attribute &attr) {
192   StringRef description = attr.getDescription();
193   if (description.empty())
194     return attr.getBaseAttr().getDescription();
195   return description;
196 }
197 
198 static void emitOpDoc(const Operator &op, raw_ostream &os) {
199   std::string classNameStr = op.getQualCppClassName();
200   StringRef className = classNameStr;
201   (void)className.consume_front(stripPrefix);
202   os << formatv("### `{0}` ({1})\n", op.getOperationName(), className);
203 
204   // Emit the summary, syntax, and description if present.
205   if (op.hasSummary())
206     emitSummary(op.getSummary(), os);
207   if (op.hasAssemblyFormat())
208     emitAssemblyFormat(op.getOperationName(), op.getAssemblyFormat().trim(),
209                        os);
210   if (op.hasDescription())
211     mlir::tblgen::emitDescription(op.getDescription(), os);
212 
213   emitOpTraitsDoc(op, os);
214 
215   // Emit attributes.
216   if (op.getNumAttributes() != 0) {
217     os << "\n#### Attributes:\n\n";
218     // Note: This table is HTML rather than markdown so the attribute's
219     // description can appear in an expandable region. The description may be
220     // multiple lines, which is not supported in a markdown table cell.
221     os << "<table>\n";
222     // Header.
223     os << "<tr><th>Attribute</th><th>MLIR Type</th><th>Description</th></tr>\n";
224     for (const auto &it : op.getAttributes()) {
225       StringRef storageType = it.attr.getStorageType();
226       // Name and storage type.
227       os << "<tr>";
228       os << "<td><code>" << it.name << "</code></td><td>" << storageType
229          << "</td><td>";
230       StringRef description = resolveAttrDescription(it.attr);
231       if (allowHugoSpecificFeatures && !description.empty()) {
232         // Expandable description.
233         // This appears as just the summary, but when clicked shows the full
234         // description.
235         os << "<details>" << "<summary>" << it.attr.getSummary() << "</summary>"
236            << "{{% markdown %}}" << description << "{{% /markdown %}}"
237            << "</details>";
238       } else {
239         // Fallback: Single-line summary.
240         os << it.attr.getSummary();
241       }
242       os << "</td></tr>\n";
243     }
244     os << "</table>\n";
245   }
246 
247   // Emit each of the operands.
248   if (op.getNumOperands() != 0) {
249     os << "\n#### Operands:\n\n";
250     os << "| Operand | Description |\n"
251        << "| :-----: | ----------- |\n";
252     for (const auto &it : op.getOperands())
253       emitNamedConstraint(it, os);
254   }
255 
256   // Emit results.
257   if (op.getNumResults() != 0) {
258     os << "\n#### Results:\n\n";
259     os << "| Result | Description |\n"
260        << "| :----: | ----------- |\n";
261     for (const auto &it : op.getResults())
262       emitNamedConstraint(it, os);
263   }
264 
265   // Emit successors.
266   if (op.getNumSuccessors() != 0) {
267     os << "\n#### Successors:\n\n";
268     os << "| Successor | Description |\n"
269        << "| :-------: | ----------- |\n";
270     for (const auto &it : op.getSuccessors())
271       emitNamedConstraint(it, os);
272   }
273 
274   os << "\n";
275 }
276 
277 static void emitSourceLink(StringRef inputFilename, raw_ostream &os) {
278   size_t pathBegin = inputFilename.find("mlir/include/mlir/");
279   if (pathBegin == StringRef::npos)
280     return;
281 
282   StringRef inputFromMlirInclude = inputFilename.substr(pathBegin);
283 
284   os << "[source](https://github.com/llvm/llvm-project/blob/main/"
285      << inputFromMlirInclude << ")\n\n";
286 }
287 
288 static void emitOpDoc(const RecordKeeper &records, raw_ostream &os) {
289   auto opDefs = getRequestedOpDefinitions(records);
290 
291   os << "<!-- Autogenerated by mlir-tblgen; don't manually edit -->\n";
292   emitSourceLink(records.getInputFilename(), os);
293   for (const Record *opDef : opDefs)
294     emitOpDoc(Operator(opDef), os);
295 }
296 
297 //===----------------------------------------------------------------------===//
298 // Attribute Documentation
299 //===----------------------------------------------------------------------===//
300 
301 static void emitAttrDoc(const Attribute &attr, raw_ostream &os) {
302   os << "### " << attr.getSummary() << "\n\n";
303   emitDescription(attr.getDescription(), os);
304   os << "\n\n";
305 }
306 
307 //===----------------------------------------------------------------------===//
308 // Type Documentation
309 //===----------------------------------------------------------------------===//
310 
311 static void emitTypeDoc(const Type &type, raw_ostream &os) {
312   os << "### " << type.getSummary() << "\n\n";
313   emitDescription(type.getDescription(), os);
314   os << "\n\n";
315 }
316 
317 //===----------------------------------------------------------------------===//
318 // TypeDef Documentation
319 //===----------------------------------------------------------------------===//
320 
321 static void emitAttrOrTypeDefAssemblyFormat(const AttrOrTypeDef &def,
322                                             raw_ostream &os) {
323   ArrayRef<AttrOrTypeParameter> parameters = def.getParameters();
324   char prefix = isa<AttrDef>(def) ? '#' : '!';
325   if (parameters.empty()) {
326     os << "\nSyntax: `" << prefix << def.getDialect().getName() << "."
327        << def.getMnemonic() << "`\n";
328     return;
329   }
330 
331   os << "\nSyntax:\n\n```\n"
332      << prefix << def.getDialect().getName() << "." << def.getMnemonic()
333      << "<\n";
334   for (const auto &it : llvm::enumerate(parameters)) {
335     const AttrOrTypeParameter &param = it.value();
336     os << "  " << param.getSyntax();
337     if (it.index() < (parameters.size() - 1))
338       os << ",";
339     os << "   # " << param.getName() << "\n";
340   }
341   os << ">\n```\n";
342 }
343 
344 static void emitAttrOrTypeDefDoc(const AttrOrTypeDef &def, raw_ostream &os) {
345   os << formatv("### {0}\n", def.getCppClassName());
346 
347   // Emit the summary if present.
348   if (def.hasSummary())
349     os << "\n" << def.getSummary() << "\n";
350 
351   // Emit the syntax if present.
352   if (def.getMnemonic() && !def.hasCustomAssemblyFormat())
353     emitAttrOrTypeDefAssemblyFormat(def, os);
354 
355   // Emit the description if present.
356   if (def.hasDescription()) {
357     os << "\n";
358     mlir::tblgen::emitDescription(def.getDescription(), os);
359   }
360 
361   // Emit parameter documentation.
362   ArrayRef<AttrOrTypeParameter> parameters = def.getParameters();
363   if (!parameters.empty()) {
364     os << "\n#### Parameters:\n\n";
365     os << "| Parameter | C++ type | Description |\n"
366        << "| :-------: | :-------: | ----------- |\n";
367     for (const auto &it : parameters) {
368       auto desc = it.getSummary();
369       os << "| " << it.getName() << " | `" << it.getCppType() << "` | "
370          << (desc ? *desc : "") << " |\n";
371     }
372   }
373 
374   os << "\n";
375 }
376 
377 static void emitAttrOrTypeDefDoc(const RecordKeeper &records, raw_ostream &os,
378                                  StringRef recordTypeName) {
379   auto defs = records.getAllDerivedDefinitions(recordTypeName);
380 
381   os << "<!-- Autogenerated by mlir-tblgen; don't manually edit -->\n";
382   for (const Record *def : defs)
383     emitAttrOrTypeDefDoc(AttrOrTypeDef(def), os);
384 }
385 
386 //===----------------------------------------------------------------------===//
387 // Enum Documentation
388 //===----------------------------------------------------------------------===//
389 
390 static void emitEnumDoc(const EnumAttr &def, raw_ostream &os) {
391   os << formatv("### {0}\n", def.getEnumClassName());
392 
393   // Emit the summary if present.
394   if (!def.getSummary().empty())
395     os << "\n" << def.getSummary() << "\n";
396 
397   // Emit case documentation.
398   std::vector<EnumAttrCase> cases = def.getAllCases();
399   os << "\n#### Cases:\n\n";
400   os << "| Symbol | Value | String |\n"
401      << "| :----: | :---: | ------ |\n";
402   for (const auto &it : cases) {
403     os << "| " << it.getSymbol() << " | `" << it.getValue() << "` | "
404        << it.getStr() << " |\n";
405   }
406 
407   os << "\n";
408 }
409 
410 static void emitEnumDoc(const RecordKeeper &records, raw_ostream &os) {
411   os << "<!-- Autogenerated by mlir-tblgen; don't manually edit -->\n";
412   for (const Record *def : records.getAllDerivedDefinitions("EnumAttrInfo"))
413     emitEnumDoc(EnumAttr(def), os);
414 }
415 
416 //===----------------------------------------------------------------------===//
417 // Dialect Documentation
418 //===----------------------------------------------------------------------===//
419 
420 struct OpDocGroup {
421   const Dialect &getDialect() const { return ops.front().getDialect(); }
422 
423   // Returns the summary description of the section.
424   std::string summary = "";
425 
426   // Returns the description of the section.
427   StringRef description = "";
428 
429   // Instances inside the section.
430   std::vector<Operator> ops;
431 };
432 
433 static void maybeNest(bool nest, llvm::function_ref<void(raw_ostream &os)> fn,
434                       raw_ostream &os) {
435   std::string str;
436   raw_string_ostream ss(str);
437   fn(ss);
438   for (StringRef x : llvm::split(str, "\n")) {
439     if (nest && x.starts_with("#"))
440       os << "#";
441     os << x << "\n";
442   }
443 }
444 
445 static void emitBlock(ArrayRef<Attribute> attributes, StringRef inputFilename,
446                       ArrayRef<AttrDef> attrDefs, ArrayRef<OpDocGroup> ops,
447                       ArrayRef<Type> types, ArrayRef<TypeDef> typeDefs,
448                       ArrayRef<EnumAttr> enums, raw_ostream &os) {
449   if (!ops.empty()) {
450     os << "## Operations\n\n";
451     emitSourceLink(inputFilename, os);
452     for (const OpDocGroup &grouping : ops) {
453       bool nested = !grouping.summary.empty();
454       maybeNest(
455           nested,
456           [&](raw_ostream &os) {
457             if (nested) {
458               os << "## " << StringRef(grouping.summary).trim() << "\n\n";
459               emitDescription(grouping.description, os);
460               os << "\n\n";
461             }
462             for (const Operator &op : grouping.ops) {
463               emitOpDoc(op, os);
464             }
465           },
466           os);
467     }
468   }
469 
470   if (!attributes.empty()) {
471     os << "## Attribute constraints\n\n";
472     for (const Attribute &attr : attributes)
473       emitAttrDoc(attr, os);
474   }
475 
476   if (!attrDefs.empty()) {
477     os << "## Attributes\n\n";
478     for (const AttrDef &def : attrDefs)
479       emitAttrOrTypeDefDoc(def, os);
480   }
481 
482   // TODO: Add link between use and def for types
483   if (!types.empty()) {
484     os << "## Type constraints\n\n";
485     for (const Type &type : types)
486       emitTypeDoc(type, os);
487   }
488 
489   if (!typeDefs.empty()) {
490     os << "## Types\n\n";
491     for (const TypeDef &def : typeDefs)
492       emitAttrOrTypeDefDoc(def, os);
493   }
494 
495   if (!enums.empty()) {
496     os << "## Enums\n\n";
497     for (const EnumAttr &def : enums)
498       emitEnumDoc(def, os);
499   }
500 }
501 
502 static void emitDialectDoc(const Dialect &dialect, StringRef inputFilename,
503                            ArrayRef<Attribute> attributes,
504                            ArrayRef<AttrDef> attrDefs, ArrayRef<OpDocGroup> ops,
505                            ArrayRef<Type> types, ArrayRef<TypeDef> typeDefs,
506                            ArrayRef<EnumAttr> enums, raw_ostream &os) {
507   os << "# '" << dialect.getName() << "' Dialect\n\n";
508   emitIfNotEmpty(dialect.getSummary(), os);
509   emitIfNotEmpty(dialect.getDescription(), os);
510 
511   // Generate a TOC marker except if description already contains one.
512   Regex r("^[[:space:]]*\\[TOC\\]$", Regex::RegexFlags::Newline);
513   if (!r.match(dialect.getDescription()))
514     os << "[TOC]\n\n";
515 
516   emitBlock(attributes, inputFilename, attrDefs, ops, types, typeDefs, enums,
517             os);
518 }
519 
520 static bool emitDialectDoc(const RecordKeeper &records, raw_ostream &os) {
521   auto dialectDefs = records.getAllDerivedDefinitionsIfDefined("Dialect");
522   SmallVector<Dialect> dialects(dialectDefs.begin(), dialectDefs.end());
523   std::optional<Dialect> dialect = findDialectToGenerate(dialects);
524   if (!dialect)
525     return true;
526 
527   std::vector<const Record *> opDefs = getRequestedOpDefinitions(records);
528   auto attrDefs = records.getAllDerivedDefinitionsIfDefined("DialectAttr");
529   auto typeDefs = records.getAllDerivedDefinitionsIfDefined("DialectType");
530   auto typeDefDefs = records.getAllDerivedDefinitionsIfDefined("TypeDef");
531   auto attrDefDefs = records.getAllDerivedDefinitionsIfDefined("AttrDef");
532   auto enumDefs = records.getAllDerivedDefinitionsIfDefined("EnumAttrInfo");
533 
534   std::vector<Attribute> dialectAttrs;
535   std::vector<AttrDef> dialectAttrDefs;
536   std::vector<OpDocGroup> dialectOps;
537   std::vector<Type> dialectTypes;
538   std::vector<TypeDef> dialectTypeDefs;
539   std::vector<EnumAttr> dialectEnums;
540 
541   SmallDenseSet<const Record *> seen;
542   auto addIfNotSeen = [&](const Record *record, const auto &def, auto &vec) {
543     if (seen.insert(record).second) {
544       vec.push_back(def);
545       return true;
546     }
547     return false;
548   };
549   auto addIfInDialect = [&](const Record *record, const auto &def, auto &vec) {
550     return def.getDialect() == *dialect && addIfNotSeen(record, def, vec);
551   };
552 
553   SmallDenseMap<const Record *, OpDocGroup> opDocGroup;
554 
555   for (const Record *def : attrDefDefs)
556     addIfInDialect(def, AttrDef(def), dialectAttrDefs);
557   for (const Record *def : attrDefs)
558     addIfInDialect(def, Attribute(def), dialectAttrs);
559   for (const Record *def : opDefs) {
560     if (const Record *group = def->getValueAsOptionalDef("opDocGroup")) {
561       OpDocGroup &op = opDocGroup[group];
562       addIfInDialect(def, Operator(def), op.ops);
563     } else {
564       OpDocGroup op;
565       op.ops.emplace_back(def);
566       addIfInDialect(def, op, dialectOps);
567     }
568   }
569   for (const Record *rec :
570        records.getAllDerivedDefinitionsIfDefined("OpDocGroup")) {
571     if (opDocGroup[rec].ops.empty())
572       continue;
573     opDocGroup[rec].summary = rec->getValueAsString("summary");
574     opDocGroup[rec].description = rec->getValueAsString("description");
575     dialectOps.push_back(opDocGroup[rec]);
576   }
577   for (const Record *def : typeDefDefs)
578     addIfInDialect(def, TypeDef(def), dialectTypeDefs);
579   for (const Record *def : typeDefs)
580     addIfInDialect(def, Type(def), dialectTypes);
581   dialectEnums.reserve(enumDefs.size());
582   for (const Record *def : enumDefs)
583     addIfNotSeen(def, EnumAttr(def), dialectEnums);
584 
585   // Sort alphabetically ignorning dialect for ops and section name for
586   // sections.
587   // TODO: The sorting order could be revised, currently attempting to sort of
588   // keep in alphabetical order.
589   std::sort(dialectOps.begin(), dialectOps.end(),
590             [](const OpDocGroup &lhs, const OpDocGroup &rhs) {
591               auto getDesc = [](const OpDocGroup &arg) -> StringRef {
592                 if (!arg.summary.empty())
593                   return arg.summary;
594                 return arg.ops.front().getDef().getValueAsString("opName");
595               };
596               return getDesc(lhs).compare_insensitive(getDesc(rhs)) < 0;
597             });
598 
599   os << "<!-- Autogenerated by mlir-tblgen; don't manually edit -->\n";
600   emitDialectDoc(*dialect, records.getInputFilename(), dialectAttrs,
601                  dialectAttrDefs, dialectOps, dialectTypes, dialectTypeDefs,
602                  dialectEnums, os);
603   return false;
604 }
605 
606 //===----------------------------------------------------------------------===//
607 // Gen Registration
608 //===----------------------------------------------------------------------===//
609 
610 static mlir::GenRegistration
611     genAttrRegister("gen-attrdef-doc",
612                     "Generate dialect attribute documentation",
613                     [](const RecordKeeper &records, raw_ostream &os) {
614                       emitAttrOrTypeDefDoc(records, os, "AttrDef");
615                       return false;
616                     });
617 
618 static mlir::GenRegistration
619     genOpRegister("gen-op-doc", "Generate dialect documentation",
620                   [](const RecordKeeper &records, raw_ostream &os) {
621                     emitOpDoc(records, os);
622                     return false;
623                   });
624 
625 static mlir::GenRegistration
626     genTypeRegister("gen-typedef-doc", "Generate dialect type documentation",
627                     [](const RecordKeeper &records, raw_ostream &os) {
628                       emitAttrOrTypeDefDoc(records, os, "TypeDef");
629                       return false;
630                     });
631 
632 static mlir::GenRegistration
633     genEnumRegister("gen-enum-doc", "Generate dialect enum documentation",
634                     [](const RecordKeeper &records, raw_ostream &os) {
635                       emitEnumDoc(records, os);
636                       return false;
637                     });
638 
639 static mlir::GenRegistration
640     genRegister("gen-dialect-doc", "Generate dialect documentation",
641                 [](const RecordKeeper &records, raw_ostream &os) {
642                   return emitDialectDoc(records, os);
643                 });
644