[llvm] [TableGen] Add support for sparse direct-lookup tables (PR #201158)

Igor Wodiany via llvm-commits llvm-commits at lists.llvm.org
Mon Jun 8 09:27:14 PDT 2026


https://github.com/IgWod updated https://github.com/llvm/llvm-project/pull/201158

>From 3261712dc0948c69de32e955083c77717d660437 Mon Sep 17 00:00:00 2001
From: Igor Wodiany <igor.wodiany at amd.com>
Date: Fri, 29 May 2026 16:32:52 +0100
Subject: [PATCH 1/4] [TableGen] Add support for sparse direct-lookup tables

This change is motivated by the recent AMDGPU patch (#200241)
where a sparse direct-lookup table is generated manually
to improve VOPD eligiblity lookup. The `GenericTable` only
generate a direct look-up for continues tables, defaulting to
binary search for non-contngious spaces. This patch adds a
`SparseLookupTable` that allows to trade-off table size with
look up speed by generating spare tables. For example the
following sparse table:

```
def VOPDYSource : SparseTableSource {
  let FilterClass = "VOPDY";
  let KeyField = "VOPDXYKey";
  let Fields = ["", "IsY"];
}

def VOPDXSource : SparseTableSource {
  let FilterClass = "VOPDX";
  let KeyField = "VOPDXYKey";
  let Fields = ["IsX", ""];
}

def VOPDXYTable : SparseTable {
  let CppTypeName = "CanBeVOPD";
  let KeyBits = 11;
  let ResultFields = ["X", "Y"];
  let Sources = [VOPDXSource, VOPDYSource];
  let LookupFuncName = "getCanBeVOPDXY";
}
```

Generates following C++ code:

```c++
const CanBeVOPD *getCanBeVOPDXY(unsigned Key);

constexpr CanBeVOPD VOPDXYTable[] = {
  {  }, // 0
  {  }, // 1
  {  }, // 2
  {  }, // 3
  {  }, // 4
  {  }, // 5
  {  }, // 6
  {  }, // 7
  {  }, // 8
  {  }, // 9
  {  }, // 10
  {  }, // 11
  {  }, // 12
  {  }, // 13
  {  }, // 14
  {  }, // 15
  {  }, // 16
  {  }, // 17
  {  }, // 18
  {  }, // 19
  { true, true }, // 20
  {  }, // 21
  { true, true }, // 22
  {  }, // 23
  { true, true }, // 24
  {  }, // 25
  { true, true }, // 26
  { true, true }, // 27
  { true, true }, // 28
  { true, true }, // 29
  {  }, // 30
  {  }, // 31
  {  }, // 32
  ...
  {  }, // 2047
 };
const CanBeVOPD *getCanBeVOPDXY(unsigned Key) {
  assert(Key < 2048 && "getCanBeVOPDXY: key out of range");
  return &VOPDXYTable[Key];
}
```
---
 llvm/include/llvm/TableGen/SearchableTable.td |  70 +++++++++
 .../utils/TableGen/SearchableTableEmitter.cpp | 146 ++++++++++++++++++
 2 files changed, 216 insertions(+)

diff --git a/llvm/include/llvm/TableGen/SearchableTable.td b/llvm/include/llvm/TableGen/SearchableTable.td
index 71b837731c440..a928872c4e2dd 100644
--- a/llvm/include/llvm/TableGen/SearchableTable.td
+++ b/llvm/include/llvm/TableGen/SearchableTable.td
@@ -148,6 +148,76 @@ class SearchIndex {
   bit EarlyOut = false;
 }
 
+// Describes one filter class contribution to a `SparseTable`.
+//
+// `Fields` is a list whose length must equal to the length of `ResultFields`
+// on the owning `SparseTable`. Each entry names a field on the FilterClass record
+// to read; an empty entry leaves that result member at its C++ default (`{}`).
+//
+// For example:
+//
+// def VOPDYSource : SparseTableSource {
+//   let FilterClass = "VOPDY";
+//   let KeyField = "VOPDXYKey";
+//   let Fields = ["", "IsY"];
+// }
+//
+// def VOPDXSource : SparseTableSource {
+//   let FilterClass = "VOPDX";
+//   let KeyField = "VOPDXYKey";
+//   let Fields = ["IsX", ""];
+// }
+//
+class SparseTableSource {
+  // Name of a class. The table will have one entry for each record that
+  // derives from that class.
+  string FilterClass;
+
+  // Name of the pre-packed key field (must be a bits<N> field on `FilterClass`).
+  // The user is responsible for packing multiple logical fields into this one
+  // field.
+  string KeyField;
+
+  // Parallel to `ResultFields` on the owning `SparseTable`. Non-empty entry
+  // contains a name of a field on the `FilterClass` record to read.
+  list<string> Fields = [];
+}
+
+// Generate a sparse directly-indexed lookup array of size 2 ^ `KeyBits`.
+//
+// Unlike `GenericTable` whose direct-lookup path requires contiguous key values,
+// `SparseTable` supports sparse key spaces.
+//
+// For example:
+//
+// def VOPDXYTable : SparseTable {
+//   let CppTypeName = "CanBeVOPD";
+//   let KeyBits = 11;
+//   let ResultFields = ["X", "Y"];
+//   let Sources = [VOPDXSource, VOPDYSource];
+//   let LookupFuncName = "getCanBeVOPDXY";
+// }
+//
+class SparseTable {
+  // Name of the C++ struct/class type that holds table entries. The
+  // declaration of this type is not generated automatically.
+  string CppTypeName;
+
+  // The emitted array has 2 ^ `KeyBits` elements. Must match the bit-width of the
+  // `KeyField` on every contributing `SparseTableSource`.
+  int KeyBits;
+
+  // Ordered list of result struct member names. The `Fields` list on
+  // each `SparseTableSource` aligns to this ordering.
+  list<string> ResultFields;
+
+  // Source classes that populate the table.
+  list<SparseTableSource> Sources;
+
+  // Name of the generated lookup function.
+  string LookupFuncName;
+}
+
 // Legacy table type with integrated enum.
 class SearchableTable {
   list<string> SearchableFields;
diff --git a/llvm/utils/TableGen/SearchableTableEmitter.cpp b/llvm/utils/TableGen/SearchableTableEmitter.cpp
index e100682bbcec3..c77a0a625779e 100644
--- a/llvm/utils/TableGen/SearchableTableEmitter.cpp
+++ b/llvm/utils/TableGen/SearchableTableEmitter.cpp
@@ -84,6 +84,23 @@ struct SearchIndex {
   bool ReturnRange = false;
 };
 
+struct SparseTableSource {
+  std::string FilterClass;
+  std::string KeyField;
+  SmallVector<std::string, 2> Fields;
+};
+
+struct SparseTable {
+  std::string Name;
+  ArrayRef<SMLoc> Locs; // Source locations from the Record instance.
+  std::string PreprocessorGuard;
+  std::string CppTypeName;
+  unsigned KeyBits;
+  SmallVector<std::string, 2> ResultFields;
+  SmallVector<SparseTableSource, 2> Sources;
+  std::string LookupFuncName;
+};
+
 struct GenericTable {
   std::string Name;
   ArrayRef<SMLoc> Locs; // Source locations from the Record instance.
@@ -218,6 +235,9 @@ class SearchableTableEmitter {
                           StringRef ValueField, ArrayRef<const Record *> Items);
   void collectTableEntries(GenericTable &Table, ArrayRef<const Record *> Items);
   int64_t getNumericKey(const SearchIndex &Index, const Record *Rec);
+
+  std::unique_ptr<SparseTable> parseSparseTable(const Record *Rec);
+  void emitSparseTable(const SparseTable &Table, raw_ostream &OS);
 };
 
 } // End anonymous namespace.
@@ -736,9 +756,128 @@ void SearchableTableEmitter::collectTableEntries(
   });
 }
 
+std::unique_ptr<SparseTable>
+SearchableTableEmitter::parseSparseTable(const Record *Rec) {
+  auto Table = std::make_unique<SparseTable>();
+  Table->Name = Rec->getName().str();
+  Table->Locs = Rec->getLoc();
+  Table->PreprocessorGuard = Rec->getName().str();
+  Table->CppTypeName = Rec->getValueAsString("CppTypeName").str();
+  int64_t KeyBitsRaw = Rec->getValueAsInt("KeyBits");
+  if (KeyBitsRaw <= 0)
+    PrintFatalError(Rec, Twine("SparseTable '") + Table->Name +
+                             "' has non-positive KeyBits");
+  Table->KeyBits = static_cast<unsigned>(KeyBitsRaw);
+  Table->LookupFuncName = Rec->getValueAsString("LookupFuncName").str();
+
+  for (StringRef RF : Rec->getValueAsListOfStrings("ResultFields"))
+    Table->ResultFields.push_back(RF.str());
+
+  if (Table->ResultFields.empty())
+    PrintFatalError(Rec, Twine("SparseTable '") + Table->Name +
+                             "' has empty 'ResultFields'");
+
+  for (const Record *SrcRec : Rec->getValueAsListOfDefs("Sources")) {
+    SparseTableSource Src;
+    Src.FilterClass = SrcRec->getValueAsString("FilterClass").str();
+    Src.KeyField = SrcRec->getValueAsString("KeyField").str();
+
+    if (!Records.getClass(Src.FilterClass))
+      PrintFatalError(SrcRec->getValue("FilterClass"),
+                      Twine("SparseTableSource FilterClass '") +
+                          Src.FilterClass + "' does not exist");
+
+    for (StringRef SF : SrcRec->getValueAsListOfStrings("Fields"))
+      Src.Fields.push_back(SF.str());
+
+    if (Src.Fields.empty())
+      PrintFatalError(SrcRec, Twine("SparseTableSource for table '") +
+                                  Table->Name + "' has empty 'Fields'");
+    if (Src.Fields.size() != Table->ResultFields.size())
+      PrintFatalError(SrcRec, Twine("SparseTableSource for table '") +
+                                  Table->Name +
+                                  "', 'Fields' length must match "
+                                  "'ResultFields' length");
+
+    Table->Sources.push_back(std::move(Src));
+  }
+
+  if (Table->Sources.empty())
+    PrintFatalError(Rec, Twine("SparseTable '") + Table->Name +
+                             "' has empty 'Sources'");
+
+  return Table;
+}
+
+void SearchableTableEmitter::emitSparseTable(const SparseTable &Table,
+                                             raw_ostream &OS) {
+  unsigned NumRows = 1u << Table.KeyBits;
+  unsigned NumFields = Table.ResultFields.size();
+  SmallVector<SmallVector<std::string, 2>> Slots(
+      NumRows, SmallVector<std::string, 2>(NumFields));
+
+  for (const SparseTableSource &Src : Table.Sources) {
+    for (const Record *Entry :
+         Records.getAllDerivedDefinitions(Src.FilterClass)) {
+      const Init *KeyInit = Entry->getValueInit(Src.KeyField);
+      uint64_t Key = static_cast<uint64_t>(getAsInt(KeyInit));
+      if (Key >= NumRows)
+        PrintFatalError(Entry, Twine("In SparseTable '") + Table.Name +
+                                   "', key value " + Twine(Key) +
+                                   " exceeds 2^KeyBits (" + Twine(NumRows) +
+                                   ")");
+      for (unsigned I = 0; I < NumFields; ++I) {
+        if (Src.Fields[I].empty())
+          continue;
+        const Init *FieldInit = Entry->getValueInit(Src.Fields[I]);
+        GenericField F(Src.Fields[I]);
+        F.RecType = cast<TypedInit>(FieldInit)->getType();
+        std::string Val = primaryRepresentation(Table.Locs[0], F, FieldInit);
+        if (!Slots[Key][I].empty() && Slots[Key][I] != Val)
+          PrintFatalError(Entry, Twine("In SparseTable '") + Table.Name +
+                                     "', key value " + Twine(Key) +
+                                     " has conflicting values for field '" +
+                                     Src.Fields[I] + "'");
+        Slots[Key][I] = std::move(Val);
+      }
+    }
+  }
+
+  emitIfdef((Twine("GET_") + Table.PreprocessorGuard + "_DECL").str(), OS);
+
+  OS << "const " << Table.CppTypeName << " *" << Table.LookupFuncName
+     << "(unsigned Key);\n";
+  OS << "#endif\n\n";
+
+  emitIfdef((Twine("GET_") + Table.PreprocessorGuard + "_IMPL").str(), OS);
+
+  OS << "constexpr " << Table.CppTypeName << " " << Table.Name << "[] = {\n";
+  for (unsigned I = 0; I < NumRows; ++I) {
+    OS << "  { ";
+    if (llvm::any_of(Slots[I],
+                     [](const std::string &V) { return !V.empty(); })) {
+      ListSeparator LS;
+      for (const std::string &Val : Slots[I])
+        OS << LS << (Val.empty() ? "{}" : Val);
+    }
+    OS << " }, // " << I << "\n";
+  }
+  OS << " };\n";
+
+  OS << "const " << Table.CppTypeName << " *" << Table.LookupFuncName
+     << "(unsigned Key) {\n";
+  OS << "  assert(Key < " << NumRows << " && \"" << Table.LookupFuncName
+     << ": key out of range\");\n";
+  OS << "  return &" << Table.Name << "[Key];\n";
+  OS << "}\n";
+
+  OS << "#endif\n\n";
+}
+
 void SearchableTableEmitter::run(raw_ostream &OS) {
   // Emit tables in a deterministic order to avoid needless rebuilds.
   SmallVector<std::unique_ptr<GenericTable>, 4> Tables;
+  SmallVector<std::unique_ptr<SparseTable>, 4> SparseTables;
   DenseMap<const Record *, GenericTable *> TableMap;
   bool NeedsTarget =
       !Records.getAllDerivedDefinitionsIfDefined("Instruction").empty() ||
@@ -862,6 +1001,10 @@ void SearchableTableEmitter::run(raw_ostream &OS) {
         IndexRec->getValueAsBit("EarlyOut"), /*ReturnRange*/ false));
   }
 
+  for (const Record *TableRec :
+       Records.getAllDerivedDefinitionsIfDefined("SparseTable"))
+    SparseTables.push_back(parseSparseTable(TableRec));
+
   // Translate legacy tables.
   const Record *SearchableTable = Records.getClass("SearchableTable");
   for (auto &NameRec : Records.getClasses()) {
@@ -929,6 +1072,9 @@ void SearchableTableEmitter::run(raw_ostream &OS) {
   for (const auto &Table : Tables)
     emitGenericTable(*Table, OS);
 
+  for (const auto &Table : SparseTables)
+    emitSparseTable(*Table, OS);
+
   // Put all #undefs last, to allow multiple sections guarded by the same
   // define.
   for (const auto &Guard : PreprocessorGuards)

>From 2fc6170ed0642800c218d4365827518c3747998a Mon Sep 17 00:00:00 2001
From: Igor Wodiany <igor.wodiany at amd.com>
Date: Fri, 5 Jun 2026 21:36:59 +0100
Subject: [PATCH 2/4] Extend GenericTable

---
 llvm/include/llvm/TableGen/SearchableTable.td |  77 +------
 .../utils/TableGen/SearchableTableEmitter.cpp | 218 ++++++------------
 2 files changed, 76 insertions(+), 219 deletions(-)

diff --git a/llvm/include/llvm/TableGen/SearchableTable.td b/llvm/include/llvm/TableGen/SearchableTable.td
index a928872c4e2dd..092ac74c982d2 100644
--- a/llvm/include/llvm/TableGen/SearchableTable.td
+++ b/llvm/include/llvm/TableGen/SearchableTable.td
@@ -125,6 +125,13 @@ class GenericTable {
   // iterator range of pointers giving the starting and end value of the range.
   // e.g. lookupSysRegByEncoding returns multiple CSRs for same encoding.
   bit PrimaryKeyReturnRange = false;
+
+  // Opt out of emitting the primary table as a direct-lookup sparse array
+  // indexed by the primary key value.
+  // TODO: Eventually this should default to `false`, so it is up to heuristics
+  // in the emitter to pick the best lookup scheme. It will then be used to
+  // override that logic and force an emission of non-sparse array.
+  bit DisallowSparseTable = true;
 }
 
 // Define a record derived from this class to generate an additional search
@@ -148,76 +155,6 @@ class SearchIndex {
   bit EarlyOut = false;
 }
 
-// Describes one filter class contribution to a `SparseTable`.
-//
-// `Fields` is a list whose length must equal to the length of `ResultFields`
-// on the owning `SparseTable`. Each entry names a field on the FilterClass record
-// to read; an empty entry leaves that result member at its C++ default (`{}`).
-//
-// For example:
-//
-// def VOPDYSource : SparseTableSource {
-//   let FilterClass = "VOPDY";
-//   let KeyField = "VOPDXYKey";
-//   let Fields = ["", "IsY"];
-// }
-//
-// def VOPDXSource : SparseTableSource {
-//   let FilterClass = "VOPDX";
-//   let KeyField = "VOPDXYKey";
-//   let Fields = ["IsX", ""];
-// }
-//
-class SparseTableSource {
-  // Name of a class. The table will have one entry for each record that
-  // derives from that class.
-  string FilterClass;
-
-  // Name of the pre-packed key field (must be a bits<N> field on `FilterClass`).
-  // The user is responsible for packing multiple logical fields into this one
-  // field.
-  string KeyField;
-
-  // Parallel to `ResultFields` on the owning `SparseTable`. Non-empty entry
-  // contains a name of a field on the `FilterClass` record to read.
-  list<string> Fields = [];
-}
-
-// Generate a sparse directly-indexed lookup array of size 2 ^ `KeyBits`.
-//
-// Unlike `GenericTable` whose direct-lookup path requires contiguous key values,
-// `SparseTable` supports sparse key spaces.
-//
-// For example:
-//
-// def VOPDXYTable : SparseTable {
-//   let CppTypeName = "CanBeVOPD";
-//   let KeyBits = 11;
-//   let ResultFields = ["X", "Y"];
-//   let Sources = [VOPDXSource, VOPDYSource];
-//   let LookupFuncName = "getCanBeVOPDXY";
-// }
-//
-class SparseTable {
-  // Name of the C++ struct/class type that holds table entries. The
-  // declaration of this type is not generated automatically.
-  string CppTypeName;
-
-  // The emitted array has 2 ^ `KeyBits` elements. Must match the bit-width of the
-  // `KeyField` on every contributing `SparseTableSource`.
-  int KeyBits;
-
-  // Ordered list of result struct member names. The `Fields` list on
-  // each `SparseTableSource` aligns to this ordering.
-  list<string> ResultFields;
-
-  // Source classes that populate the table.
-  list<SparseTableSource> Sources;
-
-  // Name of the generated lookup function.
-  string LookupFuncName;
-}
-
 // Legacy table type with integrated enum.
 class SearchableTable {
   list<string> SearchableFields;
diff --git a/llvm/utils/TableGen/SearchableTableEmitter.cpp b/llvm/utils/TableGen/SearchableTableEmitter.cpp
index c77a0a625779e..adbca498307e3 100644
--- a/llvm/utils/TableGen/SearchableTableEmitter.cpp
+++ b/llvm/utils/TableGen/SearchableTableEmitter.cpp
@@ -84,23 +84,6 @@ struct SearchIndex {
   bool ReturnRange = false;
 };
 
-struct SparseTableSource {
-  std::string FilterClass;
-  std::string KeyField;
-  SmallVector<std::string, 2> Fields;
-};
-
-struct SparseTable {
-  std::string Name;
-  ArrayRef<SMLoc> Locs; // Source locations from the Record instance.
-  std::string PreprocessorGuard;
-  std::string CppTypeName;
-  unsigned KeyBits;
-  SmallVector<std::string, 2> ResultFields;
-  SmallVector<SparseTableSource, 2> Sources;
-  std::string LookupFuncName;
-};
-
 struct GenericTable {
   std::string Name;
   ArrayRef<SMLoc> Locs; // Source locations from the Record instance.
@@ -112,6 +95,8 @@ struct GenericTable {
   std::unique_ptr<SearchIndex> PrimaryKey;
   SmallVector<std::unique_ptr<SearchIndex>, 2> Indices;
 
+  bool AllowSparseTable;
+
   const GenericField *getFieldByName(StringRef Name) const {
     for (const auto &Field : Fields) {
       if (Name == Field.Name)
@@ -235,9 +220,6 @@ class SearchableTableEmitter {
                           StringRef ValueField, ArrayRef<const Record *> Items);
   void collectTableEntries(GenericTable &Table, ArrayRef<const Record *> Items);
   int64_t getNumericKey(const SearchIndex &Index, const Record *Rec);
-
-  std::unique_ptr<SparseTable> parseSparseTable(const Record *Rec);
-  void emitSparseTable(const SparseTable &Table, raw_ostream &OS);
 };
 
 } // End anonymous namespace.
@@ -595,25 +577,56 @@ void SearchableTableEmitter::emitGenericTable(const GenericTable &Table,
 
   emitIfdef((Twine("GET_") + Table.PreprocessorGuard + "_IMPL").str(), OS);
 
+  SmallVector<const Record *, 0> SparseEntries;
+  ArrayRef<const Record *> Entries;
+  unsigned DirectLookupSlots = 0;
+  if (Table.AllowSparseTable) {
+    const auto *KeyBits = cast<BitsRecTy>(Table.PrimaryKey->Fields[0].RecType);
+    DirectLookupSlots = 1u << KeyBits->getNumBits();
+    SparseEntries.resize(DirectLookupSlots);
+    StringRef KeyFieldName = Table.PrimaryKey->Fields[0].Name;
+    for (const Record *Entry : Table.Entries) {
+      uint64_t Key = static_cast<uint64_t>(getInt(Entry, KeyFieldName));
+      assert(Key < DirectLookupSlots && "key exceeds table size");
+      if (SparseEntries[Key])
+        PrintFatalError(Entry, Twine("In table '") + Table.Name +
+                                   "', duplicate primary key value " +
+                                   Twine(Key));
+      SparseEntries[Key] = Entry;
+    }
+    Entries = SparseEntries;
+  } else {
+    Entries = Table.Entries;
+  }
+
   // The primary data table contains all the fields defined for this map.
   OS << "constexpr " << Table.CppTypeName << " " << Table.Name << "[] = {\n";
-  for (const auto &[Idx, Entry] : enumerate(Table.Entries)) {
+  for (const auto &[Idx, Entry] : enumerate(Entries)) {
     OS << "  { ";
-
-    ListSeparator LS;
-    for (const auto &Field : Table.Fields)
-      OS << LS
-         << primaryRepresentation(Table.Locs[0], Field,
-                                  Entry->getValueInit(Field.Name));
-
+    if (Entry) {
+      ListSeparator LS;
+      for (const auto &Field : Table.Fields)
+        OS << LS
+           << primaryRepresentation(Table.Locs[0], Field,
+                                    Entry->getValueInit(Field.Name));
+    }
     OS << " }, // " << Idx << "\n";
   }
   OS << " };\n";
 
-  // Indexes are sorted "{ Thing, PrimaryIdx }" arrays, so that a binary
-  // search can be performed by "Thing".
-  if (Table.PrimaryKey)
+  if (Table.AllowSparseTable) {
+    OS << "\n";
+    emitLookupDeclaration(Table, *Table.PrimaryKey, OS);
+    OS << " {\n";
+    const GenericField &Field = Table.PrimaryKey->Fields[0];
+    OS << "  assert(" << Field.Name << " < " << DirectLookupSlots << ");\n";
+    OS << "  return &" << Table.Name << "[" << Field.Name << "];\n";
+    OS << "}\n";
+  } else if (Table.PrimaryKey) {
+    // Indexes are sorted "{ Thing, PrimaryIdx }" arrays, so that a binary
+    // search can be performed by "Thing".
     emitLookupFunction(Table, *Table.PrimaryKey, /*IsPrimary=*/true, OS);
+  }
   for (const auto &Index : Table.Indices)
     emitLookupFunction(Table, *Index, /*IsPrimary=*/false, OS);
 
@@ -756,128 +769,37 @@ void SearchableTableEmitter::collectTableEntries(
   });
 }
 
-std::unique_ptr<SparseTable>
-SearchableTableEmitter::parseSparseTable(const Record *Rec) {
-  auto Table = std::make_unique<SparseTable>();
-  Table->Name = Rec->getName().str();
-  Table->Locs = Rec->getLoc();
-  Table->PreprocessorGuard = Rec->getName().str();
-  Table->CppTypeName = Rec->getValueAsString("CppTypeName").str();
-  int64_t KeyBitsRaw = Rec->getValueAsInt("KeyBits");
-  if (KeyBitsRaw <= 0)
-    PrintFatalError(Rec, Twine("SparseTable '") + Table->Name +
-                             "' has non-positive KeyBits");
-  Table->KeyBits = static_cast<unsigned>(KeyBitsRaw);
-  Table->LookupFuncName = Rec->getValueAsString("LookupFuncName").str();
-
-  for (StringRef RF : Rec->getValueAsListOfStrings("ResultFields"))
-    Table->ResultFields.push_back(RF.str());
-
-  if (Table->ResultFields.empty())
-    PrintFatalError(Rec, Twine("SparseTable '") + Table->Name +
-                             "' has empty 'ResultFields'");
-
-  for (const Record *SrcRec : Rec->getValueAsListOfDefs("Sources")) {
-    SparseTableSource Src;
-    Src.FilterClass = SrcRec->getValueAsString("FilterClass").str();
-    Src.KeyField = SrcRec->getValueAsString("KeyField").str();
-
-    if (!Records.getClass(Src.FilterClass))
-      PrintFatalError(SrcRec->getValue("FilterClass"),
-                      Twine("SparseTableSource FilterClass '") +
-                          Src.FilterClass + "' does not exist");
-
-    for (StringRef SF : SrcRec->getValueAsListOfStrings("Fields"))
-      Src.Fields.push_back(SF.str());
-
-    if (Src.Fields.empty())
-      PrintFatalError(SrcRec, Twine("SparseTableSource for table '") +
-                                  Table->Name + "' has empty 'Fields'");
-    if (Src.Fields.size() != Table->ResultFields.size())
-      PrintFatalError(SrcRec, Twine("SparseTableSource for table '") +
-                                  Table->Name +
-                                  "', 'Fields' length must match "
-                                  "'ResultFields' length");
-
-    Table->Sources.push_back(std::move(Src));
-  }
-
-  if (Table->Sources.empty())
-    PrintFatalError(Rec, Twine("SparseTable '") + Table->Name +
-                             "' has empty 'Sources'");
-
-  return Table;
-}
-
-void SearchableTableEmitter::emitSparseTable(const SparseTable &Table,
-                                             raw_ostream &OS) {
-  unsigned NumRows = 1u << Table.KeyBits;
-  unsigned NumFields = Table.ResultFields.size();
-  SmallVector<SmallVector<std::string, 2>> Slots(
-      NumRows, SmallVector<std::string, 2>(NumFields));
-
-  for (const SparseTableSource &Src : Table.Sources) {
-    for (const Record *Entry :
-         Records.getAllDerivedDefinitions(Src.FilterClass)) {
-      const Init *KeyInit = Entry->getValueInit(Src.KeyField);
-      uint64_t Key = static_cast<uint64_t>(getAsInt(KeyInit));
-      if (Key >= NumRows)
-        PrintFatalError(Entry, Twine("In SparseTable '") + Table.Name +
-                                   "', key value " + Twine(Key) +
-                                   " exceeds 2^KeyBits (" + Twine(NumRows) +
-                                   ")");
-      for (unsigned I = 0; I < NumFields; ++I) {
-        if (Src.Fields[I].empty())
-          continue;
-        const Init *FieldInit = Entry->getValueInit(Src.Fields[I]);
-        GenericField F(Src.Fields[I]);
-        F.RecType = cast<TypedInit>(FieldInit)->getType();
-        std::string Val = primaryRepresentation(Table.Locs[0], F, FieldInit);
-        if (!Slots[Key][I].empty() && Slots[Key][I] != Val)
-          PrintFatalError(Entry, Twine("In SparseTable '") + Table.Name +
-                                     "', key value " + Twine(Key) +
-                                     " has conflicting values for field '" +
-                                     Src.Fields[I] + "'");
-        Slots[Key][I] = std::move(Val);
-      }
-    }
-  }
+static bool canUseSparseTable(const Record *TableRec,
+                              const std::unique_ptr<GenericTable> &Table) {
+  if (TableRec->getValueAsBit("DisallowSparseTable"))
+    return false;
 
-  emitIfdef((Twine("GET_") + Table.PreprocessorGuard + "_DECL").str(), OS);
+  // Sparse tables are only supported with a single primary key.
+  if (Table->PrimaryKey->Fields.size() != 1)
+    return false;
 
-  OS << "const " << Table.CppTypeName << " *" << Table.LookupFuncName
-     << "(unsigned Key);\n";
-  OS << "#endif\n\n";
+  const auto *KeyBits =
+      dyn_cast<BitsRecTy>(Table->PrimaryKey->Fields[0].RecType);
 
-  emitIfdef((Twine("GET_") + Table.PreprocessorGuard + "_IMPL").str(), OS);
+  // Sparse tables only support `bits` key.
+  if (!KeyBits)
+    return false;
 
-  OS << "constexpr " << Table.CppTypeName << " " << Table.Name << "[] = {\n";
-  for (unsigned I = 0; I < NumRows; ++I) {
-    OS << "  { ";
-    if (llvm::any_of(Slots[I],
-                     [](const std::string &V) { return !V.empty(); })) {
-      ListSeparator LS;
-      for (const std::string &Val : Slots[I])
-        OS << LS << (Val.empty() ? "{}" : Val);
-    }
-    OS << " }, // " << I << "\n";
-  }
-  OS << " };\n";
+  // Sparse tables are not compatible with PrimaryKeyReturnRange.
+  if (TableRec->getValueAsBit("PrimaryKeyReturnRange"))
+    return false;
 
-  OS << "const " << Table.CppTypeName << " *" << Table.LookupFuncName
-     << "(unsigned Key) {\n";
-  OS << "  assert(Key < " << NumRows << " && \"" << Table.LookupFuncName
-     << ": key out of range\");\n";
-  OS << "  return &" << Table.Name << "[Key];\n";
-  OS << "}\n";
+  // Only support tables up to 4k in size.
+  constexpr unsigned MaxKeyBits = 12;
+  if (KeyBits->getNumBits() > MaxKeyBits)
+    return false;
 
-  OS << "#endif\n\n";
+  return true;
 }
 
 void SearchableTableEmitter::run(raw_ostream &OS) {
   // Emit tables in a deterministic order to avoid needless rebuilds.
   SmallVector<std::unique_ptr<GenericTable>, 4> Tables;
-  SmallVector<std::unique_ptr<SparseTable>, 4> SparseTables;
   DenseMap<const Record *, GenericTable *> TableMap;
   bool NeedsTarget =
       !Records.getAllDerivedDefinitionsIfDefined("Instruction").empty() ||
@@ -978,6 +900,8 @@ void SearchableTableEmitter::run(raw_ostream &OS) {
                         [&](const Record *LHS, const Record *RHS) {
                           return compareBy(LHS, RHS, *Table->PrimaryKey);
                         });
+
+      Table->AllowSparseTable = canUseSparseTable(TableRec, Table);
     }
 
     TableMap.try_emplace(TableRec, Table.get());
@@ -999,11 +923,10 @@ void SearchableTableEmitter::run(raw_ostream &OS) {
         Table, IndexRec->getValue("Key"), IndexRec->getName(),
         IndexRec->getValueAsListOfStrings("Key"),
         IndexRec->getValueAsBit("EarlyOut"), /*ReturnRange*/ false));
-  }
 
-  for (const Record *TableRec :
-       Records.getAllDerivedDefinitionsIfDefined("SparseTable"))
-    SparseTables.push_back(parseSparseTable(TableRec));
+    // Sparse tables with secondary search indices are not supported.
+    Table.AllowSparseTable = false;
+  }
 
   // Translate legacy tables.
   const Record *SearchableTable = Records.getClass("SearchableTable");
@@ -1072,9 +995,6 @@ void SearchableTableEmitter::run(raw_ostream &OS) {
   for (const auto &Table : Tables)
     emitGenericTable(*Table, OS);
 
-  for (const auto &Table : SparseTables)
-    emitSparseTable(*Table, OS);
-
   // Put all #undefs last, to allow multiple sections guarded by the same
   // define.
   for (const auto &Guard : PreprocessorGuards)

>From fbb6d9e4aa2bc38a57360b75112398ec62db9040 Mon Sep 17 00:00:00 2001
From: Igor Wodiany <igor.wodiany at amd.com>
Date: Mon, 8 Jun 2026 15:07:41 +0100
Subject: [PATCH 3/4] Return nullptr

---
 .../utils/TableGen/SearchableTableEmitter.cpp | 20 ++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/llvm/utils/TableGen/SearchableTableEmitter.cpp b/llvm/utils/TableGen/SearchableTableEmitter.cpp
index adbca498307e3..61e7a72fc79ca 100644
--- a/llvm/utils/TableGen/SearchableTableEmitter.cpp
+++ b/llvm/utils/TableGen/SearchableTableEmitter.cpp
@@ -603,12 +603,22 @@ void SearchableTableEmitter::emitGenericTable(const GenericTable &Table,
   OS << "constexpr " << Table.CppTypeName << " " << Table.Name << "[] = {\n";
   for (const auto &[Idx, Entry] : enumerate(Entries)) {
     OS << "  { ";
+    ListSeparator LS;
     if (Entry) {
-      ListSeparator LS;
       for (const auto &Field : Table.Fields)
         OS << LS
            << primaryRepresentation(Table.Locs[0], Field,
                                     Entry->getValueInit(Field.Name));
+    } else if (Table.AllowSparseTable) {
+      // For empty rows emit a sentinel value as a key, so we can return null
+      // during lookup. Value is constructed such that LookupKey != KeyField.
+      for (const auto &Field : Table.Fields) {
+        OS << LS;
+        if (Field.Name == Table.PrimaryKey->Fields[0].Name)
+          OS << "0x" << utohexstr((DirectLookupSlots - 1) ^ Idx);
+        else
+          OS << "{}";
+      }
     }
     OS << " }, // " << Idx << "\n";
   }
@@ -619,8 +629,12 @@ void SearchableTableEmitter::emitGenericTable(const GenericTable &Table,
     emitLookupDeclaration(Table, *Table.PrimaryKey, OS);
     OS << " {\n";
     const GenericField &Field = Table.PrimaryKey->Fields[0];
-    OS << "  assert(" << Field.Name << " < " << DirectLookupSlots << ");\n";
-    OS << "  return &" << Table.Name << "[" << Field.Name << "];\n";
+    OS << "  if (" << Field.Name << " >= " << DirectLookupSlots << ")\n";
+    OS << "    return nullptr;\n";
+    OS << "  const auto *Entry = &" << Table.Name << "[" << Field.Name
+       << "];\n";
+    OS << "  return Entry->" << Field.Name << " == " << Field.Name
+       << " ? Entry : nullptr;\n";
     OS << "}\n";
   } else if (Table.PrimaryKey) {
     // Indexes are sorted "{ Thing, PrimaryIdx }" arrays, so that a binary

>From 7cead95c174efb8315f33677dd3c2224b23b60c2 Mon Sep 17 00:00:00 2001
From: Igor Wodiany <igor.wodiany at amd.com>
Date: Mon, 8 Jun 2026 17:26:47 +0100
Subject: [PATCH 4/4] Update docs

---
 llvm/docs/TableGen/BackEnds.rst | 84 +++++++++++++++++++++++++++++++--
 1 file changed, 80 insertions(+), 4 deletions(-)

diff --git a/llvm/docs/TableGen/BackEnds.rst b/llvm/docs/TableGen/BackEnds.rst
index ea74dfd217b2e..4900e4457484f 100644
--- a/llvm/docs/TableGen/BackEnds.rst
+++ b/llvm/docs/TableGen/BackEnds.rst
@@ -702,7 +702,7 @@ TableGen produces C++ code to define the table entries and also produces
 the declaration and definition of a function to search the table based on a
 primary key. To define the table, define a record whose parent class is
 ``GenericTable`` and whose name is the name of the global table of entries.
-This class provides six fields.
+This class provides the following fields.
 
 * ``string FilterClass``. The table will have one entry for each record
   that derives from this class.
@@ -735,6 +735,11 @@ This class provides six fields.
   specified by the lookup function. Currently, it is supported only for primary
   lookup functions. Refer to the second example below for further details.
 
+* ``bit DisallowSparseTable``. When set to 1 (the default), prevents the
+  emitter from generating a sparse direct-lookup array for this table. Set to 
+  0 to allow the emitter to emit a sparse directly-indexed array when the
+  primary key qualifies (see below).
+
 TableGen attempts to deduce the type of each of the table fields so that it
 can format the C++ initializers in the emitted table. It can deduce ``bit``,
 ``bits<n>``, ``string``, ``Intrinsic``, and ``Instruction``.  These can be
@@ -835,9 +840,24 @@ The table entries in ``ATable`` are sorted in order by ``Val1``, and within
 each of those values, by ``Val2``. This allows a binary search of the table,
 which is performed in the lookup function by ``std::lower_bound``. The
 lookup function returns a reference to the found table entry, or the null
-pointer if no entry is found. If the table has a single primary key field
-which is integral and densely numbered, a direct lookup is generated rather
-than a binary search.
+pointer if no entry is found.
+
+The emitter selects the lookup strategy automatically based on the primary key:
+
+* **Dense direct lookup**: used when the primary key is a single integral field
+  whose values form a contiguous range. A compact array is emitted and the
+  lookup function indexes into it directly.
+
+* **Sparse direct lookup**: used when ``DisallowSparseTable = 0``, the primary
+  key is a single ``bits<N>`` field with N ≤ 12, ``PrimaryKeyReturnRange`` is
+  false, and the table has no secondary search indexes. The emitter allocates a
+  directly-indexed array of ``2^N`` entries. Empty slots are filled with a
+  sentinel value in the key field such that the in-place key comparison in the
+  lookup function returns ``nullptr`` for them. This trades memory for
+  O(1) lookup.
+
+* **Binary search**: the default for all other cases. Entries are sorted by the
+  primary key and ``std::lower_bound`` is used in the lookup function.
 
 This example includes a field whose type TableGen cannot deduce. The ``Kind``
 field uses the enumerated type ``CEnum`` defined above. To inform TableGen
@@ -1068,6 +1088,62 @@ The generated tables are:
     { 0x9 }, // 4
   };
 
+Here is an example of a sparse direct-lookup table. ``FTable`` uses a 4-bit
+primary key (``bits<4>``), so the emitter allocates an array of
+``2^4 = 16`` entries.
+
+.. code-block:: text
+
+  def FTable : GenericTable {
+    let FilterClass = "FEntry";
+    let Fields = ["Key", "Val"];
+    let PrimaryKey = ["Key"];
+    let PrimaryKeyName = "lookupFTableByKey";
+    let DisallowSparseTable = false;
+  }
+
+Here is the generated C++ code. The declaration of ``lookupFTableByKey`` is
+guarded by ``GET_FTable_DECL``, while the definitions are guarded by
+``GET_FTable_IMPL``.
+
+.. code-block:: text
+
+  #ifdef GET_FTable_DECL
+  const FEntry *lookupFTableByKey(uint8_t Key);
+  #endif
+
+  #ifdef GET_FTable_IMPL
+  constexpr FEntry FTable[] = {
+    { 0xF, 0x0 }, // 0
+    { 0xE, 0x0 }, // 1
+    { 0x2, 0xA }, // 2
+    { 0xC, 0x0 }, // 3
+    { 0xB, 0x0 }, // 4
+    { 0x5, 0x14 }, // 5
+    { 0x9, 0x0 }, // 6
+    { 0x8, 0x0 }, // 7
+    { 0x7, 0x0 }, // 8
+    { 0x6, 0x0 }, // 9
+    { 0x5, 0x0 }, // 10
+    { 0x4, 0x0 }, // 11
+    { 0x3, 0x0 }, // 12
+    { 0xD, 0x1E }, // 13
+    { 0x1, 0x0 }, // 14
+    { 0x0, 0x0 }, // 15
+  };
+
+  const FEntry *lookupFTableByKey(uint8_t Key) {
+    if (Key >= 16)
+      return nullptr;
+    const auto *Entry = &FTable[Key];
+    return Entry->Key == Key ? Entry : nullptr;
+  }
+  #endif
+
+Empty slots are filled with a sentinel value in the ``Key`` field
+(``(15 ^ Idx)``) so that the in-place comparison ``Entry->Key == Key``
+returns ``nullptr`` for them without a separate presence array.
+
 Search Indexes
 ~~~~~~~~~~~~~~
 



More information about the llvm-commits mailing list