[llvm] [TableGen][Intrinsics] Add KnownOverloadConflict allowlist for CheckOverloadSuffixConflicts (PR #195698)
Alexander Yermolovich via llvm-commits
llvm-commits at lists.llvm.org
Tue May 5 15:59:03 PDT 2026
https://github.com/ayermolo updated https://github.com/llvm/llvm-project/pull/195698
>From 4efb2e9cddca70f2240f36331286c7de0339fa3f Mon Sep 17 00:00:00 2001
From: Alexander Yermolovich <ayermolovich at nvidia.com>
Date: Mon, 4 May 2026 17:50:46 +0000
Subject: [PATCH 1/3] [TableGen][Intrinsics] Add KnownOverloadConflict
allowlist for CheckOverloadSuffixConflicts
CheckOverloadSuffixConflicts (in CodeGenIntrinsics.cpp) detects when a
non-overloaded "subordinate" intrinsic name (e.g. llvm.foo.f128) shares
a prefix with an overloaded "dominator" intrinsic (e.g. llvm.foo) such
that the mangled-name lookup could route the wrong way. Until now the
checker had no exemption mechanism, so any pre-existing conflict either
had to be cleaned up (renamed) or the check had to remain disabled.
This patch adds a `KnownOverloadConflict` string field to the Intrinsic
class. A subordinate may set it to the record name of an overloaded
dominator it is allowlisted to conflict with.
---
llvm/include/llvm/IR/Intrinsics.td | 8 ++
.../intrinsic-overload-conflict-allowlist.td | 81 +++++++++++++++++++
.../TableGen/Basic/CodeGenIntrinsics.cpp | 70 ++++++++++++++++
3 files changed, 159 insertions(+)
create mode 100644 llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td
diff --git a/llvm/include/llvm/IR/Intrinsics.td b/llvm/include/llvm/IR/Intrinsics.td
index 4469ff155b854..4577464062355 100644
--- a/llvm/include/llvm/IR/Intrinsics.td
+++ b/llvm/include/llvm/IR/Intrinsics.td
@@ -730,6 +730,14 @@ class Intrinsic<list<LLVMType> ret_types,
// IntrinsicProperty<1>
bit DisableDefaultAttributes = disable_default_attributes;
+ // TableGen *record name* of an overloaded intrinsic this intrinsic is
+ // allowlisted to share a prefix with (e.g., `"int_foo"`, NOT
+ // `"llvm.foo"` -- this is the literal identifier after `def` in the
+ // .td source, not the mangled `LLVMName`). Set this only on the
+ // subordinate (concrete, non-overloaded) side of a known conflict
+ // detected by `CheckOverloadSuffixConflicts`.
+ string KnownOverloadConflict = "";
+
TypeInfoGen TypeInfo = TypeInfoGen<RetTypes, ParamTypes>;
}
diff --git a/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td b/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td
new file mode 100644
index 0000000000000..245aabdab32b6
--- /dev/null
+++ b/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td
@@ -0,0 +1,81 @@
+// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS | FileCheck %s --check-prefix=CHECK-PASS
+// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DALLOWLIST 2>&1 | FileCheck %s --check-prefix=CHECK-ALLOWLIST
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NOT_ALLOWLISTED 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-ALLOWLISTED
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NONEXISTENT 2>&1 | FileCheck %s --check-prefix=CHECK-NONEXISTENT
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NOT_OVERLOADED 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-OVERLOADED
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NOT_INTRINSIC 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-INTRINSIC
+
+// =========================================================================
+// MAINTAINER NOTE -- test-case flags are mutually exclusive
+// =========================================================================
+// `-DTEST_INTRINSICS_SUPPRESS_DEFS` is shared infrastructure (it tells
+// `Intrinsics.td` to skip the bulk intrinsic defs so this file can
+// stand on its own) and is on EVERY RUN line.
+//
+// The five *test-case* flags below -- `ALLOWLIST`,
+// `ERROR_NOT_ALLOWLISTED`, `ERROR_NONEXISTENT`, `ERROR_NOT_OVERLOADED`,
+// `ERROR_NOT_INTRINSIC` -- are mutually exclusive. Three of them
+// (`ALLOWLIST`, `ERROR_NOT_ALLOWLISTED`, `ERROR_NONEXISTENT`) all
+// `def int_foo_f128`, so combining any two of them on the same tblgen
+// invocation would yield a duplicate-record error from TableGen
+// instead of exercising the intended check.
+//
+// To add a new test case that needs multiple test-case flags
+// simultaneously (e.g., "what happens when both a valid annotation
+// AND a typo annotation coexist?"), give the new `#ifdef` block a
+// flag-specific record name -- e.g., `int_foo_f128_combined_case` --
+// so its records are disjoint from the existing blocks.
+// =========================================================================
+
+include "llvm/IR/Intrinsics.td"
+
+// === Baseline (always defined) ===========================================
+// CHECK-PASS: foo
+// CHECK-PASS-NOT: error:
+
+def int_foo : Intrinsic<[llvm_any_ty]>;
+
+// === Allowlisted conflict ================================================
+#ifdef ALLOWLIST
+// CHECK-ALLOWLIST: foo
+// CHECK-ALLOWLIST-NOT: error:
+
+let KnownOverloadConflict = "int_foo" in
+def int_foo_f128 : Intrinsic<[llvm_i32_ty]>;
+#endif
+
+// === Conflict without allowlist ==========================================
+#ifdef ERROR_NOT_ALLOWLISTED
+// CHECK-NOT-ALLOWLISTED: error: intrinsic `llvm.foo.f128` cannot share prefix
+// CHECK-NOT-ALLOWLISTED: note: if intentional, add `let KnownOverloadConflict = "int_foo" in`
+def int_foo_f128 : Intrinsic<[llvm_i32_ty]>;
+#endif
+
+// === Allowlist names a nonexistent record ================================
+#ifdef ERROR_NONEXISTENT
+// CHECK-NONEXISTENT: error: intrinsic `int_foo_f128` has KnownOverloadConflict referencing unknown intrinsic `int_does_not_exist`
+let KnownOverloadConflict = "int_does_not_exist" in
+def int_foo_f128 : Intrinsic<[llvm_i32_ty]>;
+#endif
+
+// === Allowlist names a non-overloaded record =============================
+#ifdef ERROR_NOT_OVERLOADED
+// CHECK-NOT-OVERLOADED: error: intrinsic `int_baz_f128` has KnownOverloadConflict referencing `int_baz`, which is not an overloaded intrinsic
+def int_baz : Intrinsic<[llvm_i32_ty]>; // NOT overloaded (concrete return type)
+let KnownOverloadConflict = "int_baz" in
+def int_baz_f128 : Intrinsic<[llvm_i32_ty]>;
+#endif
+
+// === Allowlist names a record that exists but is not an Intrinsic ========
+// Defends against the failure mode where a typo or stale rename points
+// `KnownOverloadConflict` at some unrelated record (helper class, etc.).
+// Without the `isSubClassOf("Intrinsic")` guard, `getValueAsDef("TypeInfo")`
+// would `PrintFatalError` with a generic "no such field" message that
+// fails to mention `KnownOverloadConflict` at all.
+#ifdef ERROR_NOT_INTRINSIC
+// CHECK-NOT-INTRINSIC: error: intrinsic `int_qux_f128` has KnownOverloadConflict referencing `some_helper`, which is not an Intrinsic
+class Helper { string Whatever = ""; }
+def some_helper : Helper;
+let KnownOverloadConflict = "some_helper" in
+def int_qux_f128 : Intrinsic<[llvm_i32_ty]>;
+#endif
diff --git a/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp b/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp
index d90fcc25502e2..0dab779599caa 100644
--- a/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp
+++ b/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp
@@ -33,6 +33,10 @@ static constexpr unsigned MaxNumReturn = 257;
// CodeGenIntrinsic Implementation
//===----------------------------------------------------------------------===//
+// Forward declaration; full implementation appears alongside
+// `CheckOverloadSuffixConflicts` below.
+static void CheckKnownOverloadConflictExistence(const RecordKeeper &RC);
+
CodeGenIntrinsicContext::CodeGenIntrinsicContext(const RecordKeeper &RC) {
for (const Record *Rec : RC.getAllDerivedDefinitions("IntrinsicProperty"))
if (Rec->getValueAsBit("IsDefault"))
@@ -75,6 +79,7 @@ CodeGenIntrinsicTable::CodeGenIntrinsicTable(const RecordKeeper &RC) {
CheckDuplicateIntrinsics();
CheckTargetIndependentIntrinsics();
+ CheckKnownOverloadConflictExistence(RC);
CheckOverloadSuffixConflicts();
}
@@ -167,6 +172,57 @@ static bool doesSuffixLookLikeMangledType(StringRef Suffix) {
return is_contained(NamedTypes, Suffix);
}
+// Validate every `KnownOverloadConflict` annotation:
+// * The named record must exist.
+// * The named record must be a subclass of `Intrinsic` (otherwise it
+// has no `TypeInfo` field and `getValueAsDef("TypeInfo")` below
+// would `PrintFatalError` with a generic "no such field" message
+// that fails to connect the user back to their `KnownOverloadConflict`
+// annotation -- guard with `isSubClassOf` to emit a contextual error
+// instead).
+// * The named record must be an overloaded intrinsic.
+// The annotation stores the TableGen *record name* of the dominator
+// (e.g. `"int_foo"`), not the mangled intrinsic name (`"llvm.foo"`),
+// so the lookup goes through `RecordKeeper::getDef`.
+static void
+CheckKnownOverloadConflictExistence(const RecordKeeper &RC) {
+ for (const Record *Def : RC.getAllDerivedDefinitions("Intrinsic")) {
+ const StringRef Allow = Def->getValueAsString("KnownOverloadConflict");
+ if (Allow.empty())
+ continue;
+
+ const Record *Target = RC.getDef(Allow);
+ if (!Target) {
+ PrintError(Def->getLoc(),
+ "intrinsic `" + Def->getName() +
+ "` has KnownOverloadConflict referencing unknown "
+ "intrinsic `" + Allow + "`");
+ continue;
+ }
+
+ if (!Target->isSubClassOf("Intrinsic")) {
+ PrintError(Def->getLoc(),
+ "intrinsic `" + Def->getName() +
+ "` has KnownOverloadConflict referencing `" + Allow +
+ "`, which is not an Intrinsic; "
+ "KnownOverloadConflict must name an overloaded "
+ "intrinsic record");
+ continue;
+ }
+
+ const bool TgtOverloaded =
+ Target->getValueAsDef("TypeInfo")->getValueAsBit("isOverloaded");
+ if (!TgtOverloaded) {
+ PrintError(Def->getLoc(),
+ "intrinsic `" + Def->getName() +
+ "` has KnownOverloadConflict referencing `" + Allow +
+ "`, which is not an overloaded intrinsic; "
+ "KnownOverloadConflict must point at the overloaded "
+ "dominator");
+ }
+ }
+}
+
// Check for conflicts with overloaded intrinsics. If there exists an overloaded
// intrinsic with base name `llvm.target.foo`, LLVM will add a mangling suffix
// to it to encode the overload types. This mangling suffix is 1 or more .
@@ -229,6 +285,16 @@ void CodeGenIntrinsicTable::CheckOverloadSuffixConflicts() const {
continue;
unsigned SuffixSize = OverloadName.size() + 1 + Suffix0.size();
+ // Read the subordinate's allowlist annotation. The annotation
+ // stores the *record name* of the dominator (e.g. "int_foo"),
+ // not the mangled name (`OverloadName == Overloaded->Name`,
+ // which is "llvm.foo"). The comparison must therefore use
+ // `Overloaded->TheDef->getName()`.
+ const StringRef Allow =
+ Int.TheDef->getValueAsString("KnownOverloadConflict");
+ if (!Allow.empty() && Allow == Overloaded->TheDef->getName())
+ continue;
+
// If suffix looks like mangling suffix, flag it as an error.
PrintError(Int.TheDef->getLoc(),
"intrinsic `" + Name + "` cannot share prefix `" +
@@ -237,6 +303,10 @@ void CodeGenIntrinsicTable::CheckOverloadSuffixConflicts() const {
"`");
PrintNote(Overloaded->TheDef->getLoc(),
"Overloaded intrinsic `" + OverloadName + "` defined here");
+ PrintNote(Int.TheDef->getLoc(),
+ "if intentional, add `let KnownOverloadConflict = \"" +
+ Overloaded->TheDef->getName() +
+ "\" in` to this intrinsic's definition");
continue;
}
>From 877f666b8c39f1d4299f2e68b86048ad62c717bb Mon Sep 17 00:00:00 2001
From: Alexander Yermolovich <ayermolovich at nvidia.com>
Date: Mon, 4 May 2026 18:02:03 +0000
Subject: [PATCH 2/3] format
---
llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp b/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp
index 0dab779599caa..12edab6c5069f 100644
--- a/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp
+++ b/llvm/utils/TableGen/Basic/CodeGenIntrinsics.cpp
@@ -184,8 +184,7 @@ static bool doesSuffixLookLikeMangledType(StringRef Suffix) {
// The annotation stores the TableGen *record name* of the dominator
// (e.g. `"int_foo"`), not the mangled intrinsic name (`"llvm.foo"`),
// so the lookup goes through `RecordKeeper::getDef`.
-static void
-CheckKnownOverloadConflictExistence(const RecordKeeper &RC) {
+static void CheckKnownOverloadConflictExistence(const RecordKeeper &RC) {
for (const Record *Def : RC.getAllDerivedDefinitions("Intrinsic")) {
const StringRef Allow = Def->getValueAsString("KnownOverloadConflict");
if (Allow.empty())
@@ -196,7 +195,8 @@ CheckKnownOverloadConflictExistence(const RecordKeeper &RC) {
PrintError(Def->getLoc(),
"intrinsic `" + Def->getName() +
"` has KnownOverloadConflict referencing unknown "
- "intrinsic `" + Allow + "`");
+ "intrinsic `" +
+ Allow + "`");
continue;
}
>From 18ac05b58a81f09331299b334b9c35be3e1a88e6 Mon Sep 17 00:00:00 2001
From: Alexander Yermolovich <ayermolovich at nvidia.com>
Date: Tue, 5 May 2026 22:58:49 +0000
Subject: [PATCH 3/3] review comments
---
llvm/include/llvm/IR/Intrinsics.td | 29 ++++++++---
.../intrinsic-overload-conflict-allowlist.td | 48 ++++++++-----------
2 files changed, 44 insertions(+), 33 deletions(-)
diff --git a/llvm/include/llvm/IR/Intrinsics.td b/llvm/include/llvm/IR/Intrinsics.td
index 4577464062355..b476b07ca5ddc 100644
--- a/llvm/include/llvm/IR/Intrinsics.td
+++ b/llvm/include/llvm/IR/Intrinsics.td
@@ -730,12 +730,29 @@ class Intrinsic<list<LLVMType> ret_types,
// IntrinsicProperty<1>
bit DisableDefaultAttributes = disable_default_attributes;
- // TableGen *record name* of an overloaded intrinsic this intrinsic is
- // allowlisted to share a prefix with (e.g., `"int_foo"`, NOT
- // `"llvm.foo"` -- this is the literal identifier after `def` in the
- // .td source, not the mangled `LLVMName`). Set this only on the
- // subordinate (concrete, non-overloaded) side of a known conflict
- // detected by `CheckOverloadSuffixConflicts`.
+ // Names an overloaded intrinsic that this intrinsic is allowed to
+ // share a name prefix with, even though doing so would normally be
+ // flagged as a conflict by `CheckOverloadSuffixConflicts`. Set this
+ // only on the concrete (non-overloaded) side of a known conflict;
+ // the overloaded side has nothing to set.
+ //
+ // The value is the TableGen record name -- the identifier you write
+ // in the .td source -- not the mangled name LLVM uses at runtime.
+ // Use `"int_foo"`, NOT `"llvm.foo"`.
+ //
+ // How to figure out the record name in each case:
+ // * `def int_foo : Intrinsic<...>` -> use `"int_foo"`.
+ // * `defm int_foo : SomeMulticlass<...>` -> a `defm` runs a
+ // multiclass and prefixes every `def` inside it with `int_foo`.
+ // Use whatever full name the `defm` produced, for example
+ // `"int_foo_bar"` if the multiclass's inner `def` is named
+ // `_bar`.
+ // * `foreach v = [...] in def int_foo_ # v : ...`
+ // -> the record name
+ // is built per loop iteration. Write the annotation with the
+ // matching concatenation, e.g.
+ // `let KnownOverloadConflict = "int_dom_" # v in`, so each
+ // generated record points at its right dominator.
string KnownOverloadConflict = "";
TypeInfoGen TypeInfo = TypeInfoGen<RetTypes, ParamTypes>;
diff --git a/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td b/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td
index 245aabdab32b6..344e652daaef0 100644
--- a/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td
+++ b/llvm/test/TableGen/intrinsic-overload-conflict-allowlist.td
@@ -1,32 +1,17 @@
-// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS | FileCheck %s --check-prefix=CHECK-PASS
-// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DALLOWLIST 2>&1 | FileCheck %s --check-prefix=CHECK-ALLOWLIST
-// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NOT_ALLOWLISTED 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-ALLOWLISTED
-// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NONEXISTENT 2>&1 | FileCheck %s --check-prefix=CHECK-NONEXISTENT
-// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NOT_OVERLOADED 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-OVERLOADED
-// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DTEST_INTRINSICS_SUPPRESS_DEFS -DERROR_NOT_INTRINSIC 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-INTRINSIC
+// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s | FileCheck %s --check-prefix=CHECK-PASS
+// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DALLOWLIST 2>&1 | FileCheck %s --check-prefix=CHECK-ALLOWLIST
+// RUN: llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DALLOWLIST_EXPLICIT_NAME 2>&1 | FileCheck %s --check-prefix=CHECK-ALLOWLIST-EXPLICIT
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DERROR_NOT_ALLOWLISTED 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-ALLOWLISTED
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DERROR_NONEXISTENT 2>&1 | FileCheck %s --check-prefix=CHECK-NONEXISTENT
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DERROR_NOT_OVERLOADED 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-OVERLOADED
+// RUN: not llvm-tblgen -gen-intrinsic-enums -I %p/../../include %s -DERROR_NOT_INTRINSIC 2>&1 | FileCheck %s --check-prefix=CHECK-NOT-INTRINSIC
-// =========================================================================
-// MAINTAINER NOTE -- test-case flags are mutually exclusive
-// =========================================================================
-// `-DTEST_INTRINSICS_SUPPRESS_DEFS` is shared infrastructure (it tells
-// `Intrinsics.td` to skip the bulk intrinsic defs so this file can
-// stand on its own) and is on EVERY RUN line.
-//
-// The five *test-case* flags below -- `ALLOWLIST`,
-// `ERROR_NOT_ALLOWLISTED`, `ERROR_NONEXISTENT`, `ERROR_NOT_OVERLOADED`,
-// `ERROR_NOT_INTRINSIC` -- are mutually exclusive. Three of them
-// (`ALLOWLIST`, `ERROR_NOT_ALLOWLISTED`, `ERROR_NONEXISTENT`) all
-// `def int_foo_f128`, so combining any two of them on the same tblgen
-// invocation would yield a duplicate-record error from TableGen
-// instead of exercising the intended check.
-//
-// To add a new test case that needs multiple test-case flags
-// simultaneously (e.g., "what happens when both a valid annotation
-// AND a typo annotation coexist?"), give the new `#ifdef` block a
-// flag-specific record name -- e.g., `int_foo_f128_combined_case` --
-// so its records are disjoint from the existing blocks.
-// =========================================================================
+/// Tests the `KnownOverloadConflict` allowlist for intrinsic prefix conflicts:
+/// covers the pass paths (baseline + two allowlist forms) and four diagnostic
+/// paths (unallowlisted conflict, nonexistent target, non-overloaded target,
+/// and target that isn't an Intrinsic).
+#define TEST_INTRINSICS_SUPPRESS_DEFS
include "llvm/IR/Intrinsics.td"
// === Baseline (always defined) ===========================================
@@ -44,6 +29,15 @@ let KnownOverloadConflict = "int_foo" in
def int_foo_f128 : Intrinsic<[llvm_i32_ty]>;
#endif
+#ifdef ALLOWLIST_EXPLICIT_NAME
+// CHECK-ALLOWLIST-EXPLICIT: corge
+// CHECK-ALLOWLIST-EXPLICIT-NOT: error:
+
+def int_corge_dom : Intrinsic<[llvm_any_ty], [], [], "llvm.corge">;
+let KnownOverloadConflict = "int_corge_dom" in
+def int_corge_sub : Intrinsic<[llvm_i32_ty], [], [], "llvm.corge.f128">;
+#endif
+
// === Conflict without allowlist ==========================================
#ifdef ERROR_NOT_ALLOWLISTED
// CHECK-NOT-ALLOWLISTED: error: intrinsic `llvm.foo.f128` cannot share prefix
More information about the llvm-commits
mailing list