[llvm] [GlobalISel] Add GIHasOneUse builtin for combiner match patterns (PR #221804)

Deepak Shirke via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 10 06:47:23 PDT 2026


https://github.com/deepakshirkem updated https://github.com/llvm/llvm-project/pull/221804

>From 904244acbd6fb8ac69c2ee1a9fb2759fc75c89de Mon Sep 17 00:00:00 2001
From: deepakshirkem <deepakshirke509 at gmail.com>
Date: Tue, 8 Sep 2026 00:40:51 +0530
Subject: [PATCH] [GlobalISel][TableGen] Add GIHasOneUse for combiner match
 patterns

Add GIHasOneUse which allows expressing one-use checks in GlobalISel
combiner rules without requiring C++ predicates. Supports two syntaxes:

  // Wrapper syntax (inline with instruction):
  (match (GIHasOneUse (G_MUL $mul, $x, $c)), ...)

  // Standalone syntax (separate predicate):
  (match (G_MUL $mul, $x, $c), ..., (GIHasOneUse $mul))

Both emit GIM_CheckHasOneUse which was already implemented in the
executor but never emitted by the combiner. Convert sub_of_mul_const
to use GIHasOneUse as a demonstration.
---
 .../include/llvm/Target/GlobalISel/Combine.td | 11 +++--
 .../has-one-use-error.td                      | 19 ++++++++
 .../GlobalISelCombinerEmitter/has-one-use.td  | 34 ++++++++++++++
 .../Common/GlobalISel/PatternParser.cpp       | 44 ++++++++++++++++++
 .../TableGen/Common/GlobalISel/Patterns.h     |  8 +++-
 .../TableGen/GlobalISelCombinerEmitter.cpp    | 46 +++++++++++++++----
 6 files changed, 148 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use-error.td
 create mode 100644 llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use.td

diff --git a/llvm/include/llvm/Target/GlobalISel/Combine.td b/llvm/include/llvm/Target/GlobalISel/Combine.td
index 4b0a438cd6455..19749e03182ec 100644
--- a/llvm/include/llvm/Target/GlobalISel/Combine.td
+++ b/llvm/include/llvm/Target/GlobalISel/Combine.td
@@ -174,6 +174,12 @@ def GIReplaceReg : GIBuiltinInst;
 //
 // TODO: Allow using this directly, like (apply GIEraseRoot)
 def GIEraseRoot : GIBuiltinInst;
+/// Check that the first result of an instruction has only one use.
+/// Can be used in two ways:
+///   1. As a wrapper: (GIHasOneUse (G_MUL $x, $y, $z))
+///   2. As standalone: (G_MUL $mul, $x, $y), (GIHasOneUse $mul)
+class GIHasOneUse_Class;
+def GIHasOneUse : GIBuiltinInst, GIHasOneUse_Class;
 
 //===----------------------------------------------------------------------===//
 // Pattern MIFlags
@@ -403,9 +409,8 @@ def sub_to_add : GICombineRule<
 def sub_of_mul_const : GICombineRule<
   (defs root:$d),
   (match (G_CONSTANT $c, $imm),
-         (G_MUL $mul, $x, $c),
-         (G_SUB $d, $a, $mul),
-         [{ return MRI.hasOneNonDBGUse(${mul}.getReg()); }]),
+         (GIHasOneUse (G_MUL $mul, $x, $c)),
+         (G_SUB $d, $a, $mul)),
   (apply (G_SUB $negc, 0, $c),
          (G_MUL $newmul, $x, $negc),
          (G_ADD $d, $a, $newmul))>;
diff --git a/llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use-error.td b/llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use-error.td
new file mode 100644
index 0000000000000..b224eb6ee30bf
--- /dev/null
+++ b/llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use-error.td
@@ -0,0 +1,19 @@
+// RUN: not llvm-tblgen -I %p/../../../include -gen-global-isel-combiner \
+// RUN:     -combiners=MyCombiner %s 2>&1 | \
+// RUN: FileCheck %s
+
+include "llvm/Target/Target.td"
+include "llvm/Target/GlobalISel/Combine.td"
+
+def MyTargetISA : InstrInfo;
+def MyTarget : Target { let InstructionSet = MyTargetISA; }
+
+// CHECK: GIHasOneUse expects exactly one instruction argument
+def test_error_noarg: GICombineRule<
+  (defs root:$d),
+  (match (GIHasOneUse),
+         (G_SUB $d, $a, $b)),
+  (apply (GIReplaceReg $d, $a))
+>;
+
+def MyCombiner: GICombiner<"MyCombinerImpl", [test_error_noarg]>;
diff --git a/llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use.td b/llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use.td
new file mode 100644
index 0000000000000..22169176020e8
--- /dev/null
+++ b/llvm/test/TableGen/GlobalISelCombinerEmitter/has-one-use.td
@@ -0,0 +1,34 @@
+// RUN: llvm-tblgen -I %p/../../../include -gen-global-isel-combiner \
+// RUN:     -combiners=MyCombiner %s | \
+// RUN: FileCheck %s
+
+include "llvm/Target/Target.td"
+include "llvm/Target/GlobalISel/Combine.td"
+
+def MyTargetISA : InstrInfo;
+def MyTarget : Target { let InstructionSet = MyTargetISA; }
+
+// Test wrapper syntax: (GIHasOneUse (G_MUL ...))
+// CHECK: GIM_CheckOpcode, /*MI*/1, GIMT_Encode2(TargetOpcode::G_MUL)
+// CHECK-NEXT: GIM_CheckHasOneUse, /*MI*/1
+// CHECK: Combiner Rule #0: test_wrapper
+def test_wrapper: GICombineRule<
+  (defs root:$d),
+  (match (GIHasOneUse (G_MUL $mul, $x, $c)),
+         (G_SUB $d, $a, $mul)),
+  (apply (GIReplaceReg $d, $x))
+>;
+
+// Test standalone syntax: (G_MUL ...), (GIHasOneUse $mul)
+// CHECK: GIM_CheckOpcode, /*MI*/1, GIMT_Encode2(TargetOpcode::G_MUL)
+// CHECK-NEXT: GIM_CheckHasOneUse, /*MI*/1
+// CHECK: Combiner Rule #1: test_standalone
+def test_standalone: GICombineRule<
+  (defs root:$d),
+  (match (G_MUL $mul, $x, $c),
+         (G_SUB $d, $a, $mul),
+         (GIHasOneUse $mul)),
+  (apply (GIReplaceReg $d, $x))
+>;
+
+def MyCombiner: GICombiner<"MyCombinerImpl", [test_wrapper, test_standalone]>;
diff --git a/llvm/utils/TableGen/Common/GlobalISel/PatternParser.cpp b/llvm/utils/TableGen/Common/GlobalISel/PatternParser.cpp
index 8e4a8507e1aca..dd062080b7c1b 100644
--- a/llvm/utils/TableGen/Common/GlobalISel/PatternParser.cpp
+++ b/llvm/utils/TableGen/Common/GlobalISel/PatternParser.cpp
@@ -63,12 +63,56 @@ bool PatternParser::parsePatternList(
                            ? List.getArgName(I)->getValue().str()
                            : ("__" + AnonPatNamePrefix + "_" + Twine(I)).str();
 
+    // Check for GIHasOneUse wrapper BEFORE parseInstructionPattern
+    // since GIHasOneUse is also a GIBuiltinInst.
+    if (const auto *DagArg = dyn_cast<DagInit>(Arg)) {
+      if (DagArg->getOperatorAsDef(DiagLoc)->getName() == "GIHasOneUse" &&
+          DagArg->getNumArgs() == 1 && isa<DagInit>(DagArg->getArg(0))) {
+        if (auto Pat = parseInstructionPattern(*DagArg->getArg(0), Name)) {
+          if (auto *CGP = dyn_cast<CodeGenInstructionPattern>(Pat.get()))
+            CGP->setHasOneUse();
+          else {
+            PrintError(
+                DiagLoc,
+                "GIHasOneUse can only wrap a CodeGenInstruction pattern");
+            return false;
+          }
+          if (!ParseAction(std::move(Pat)))
+            return false;
+          continue;
+        }
+      }
+    }
+
     if (auto Pat = parseInstructionPattern(*Arg, Name)) {
       if (!ParseAction(std::move(Pat)))
         return false;
       continue;
     }
 
+    if (const auto *DagArg = dyn_cast<DagInit>(Arg)) {
+      if (DagArg->getOperatorAsDef(DiagLoc)->getName() == "GIHasOneUse") {
+        if (DagArg->getNumArgs() != 1) {
+          PrintError(DiagLoc,
+                     "GIHasOneUse expects exactly one instruction argument");
+          return false;
+        }
+        if (auto Pat = parseInstructionPattern(*DagArg->getArg(0), Name)) {
+          if (auto *CGP = dyn_cast<CodeGenInstructionPattern>(Pat.get()))
+            CGP->setHasOneUse();
+          else {
+            PrintError(
+                DiagLoc,
+                "GIHasOneUse can only wrap a CodeGenInstruction pattern");
+            return false;
+          }
+          if (!ParseAction(std::move(Pat)))
+            return false;
+          continue;
+        }
+      }
+    }
+
     if (auto Pat = parseWipMatchOpcodeMatcher(*Arg, Name)) {
       if (!ParseAction(std::move(Pat)))
         return false;
diff --git a/llvm/utils/TableGen/Common/GlobalISel/Patterns.h b/llvm/utils/TableGen/Common/GlobalISel/Patterns.h
index 2bf2a22ee9998..f095069224865 100644
--- a/llvm/utils/TableGen/Common/GlobalISel/Patterns.h
+++ b/llvm/utils/TableGen/Common/GlobalISel/Patterns.h
@@ -476,6 +476,9 @@ class CodeGenInstructionPattern : public InstructionPattern {
   MIFlagsInfo &getOrCreateMIFlagsInfo();
   const MIFlagsInfo *getMIFlagsInfo() const { return FI.get(); }
 
+  void setHasOneUse(bool V = true) { HasOneUse = V; }
+  bool hasOneUse() const { return HasOneUse; }
+
   const CodeGenInstruction &getInst() const { return I; }
   StringRef getInstName() const override;
 
@@ -485,6 +488,7 @@ class CodeGenInstructionPattern : public InstructionPattern {
   const CodeGenInstruction &I;
   const CodeGenIntrinsic *IntrinInfo = nullptr;
   std::unique_ptr<MIFlagsInfo> FI;
+  bool HasOneUse = false;
 };
 
 //===- OperandTypeChecker -------------------------------------------------===//
@@ -692,6 +696,7 @@ class PatFragPattern : public InstructionPattern {
 enum BuiltinKind {
   BI_ReplaceReg,
   BI_EraseRoot,
+  BI_HasOneUse,
 };
 
 class BuiltinPattern : public InstructionPattern {
@@ -702,9 +707,10 @@ class BuiltinPattern : public InstructionPattern {
     unsigned NumDefs;
   };
 
-  static constexpr std::array<BuiltinInfo, 2> KnownBuiltins = {{
+  static constexpr std::array<BuiltinInfo, 3> KnownBuiltins = {{
       {"GIReplaceReg", BI_ReplaceReg, 2, 1},
       {"GIEraseRoot", BI_EraseRoot, 0, 0},
+      {"GIHasOneUse", BI_HasOneUse, 1, 0},
   }};
 
 public:
diff --git a/llvm/utils/TableGen/GlobalISelCombinerEmitter.cpp b/llvm/utils/TableGen/GlobalISelCombinerEmitter.cpp
index bb93d0248e55f..0ccbf03dd0112 100644
--- a/llvm/utils/TableGen/GlobalISelCombinerEmitter.cpp
+++ b/llvm/utils/TableGen/GlobalISelCombinerEmitter.cpp
@@ -1051,11 +1051,13 @@ bool CombineRuleBuilder::addMatchPattern(std::unique_ptr<Pattern> Pat) {
     return false;
   }
 
-  // For now, none of the builtins can appear in 'match'.
+  // Most builtins cannot appear in 'match', except GIHasOneUse.
   if (const auto *BP = dyn_cast<BuiltinPattern>(Pat.get())) {
-    PrintError("'" + BP->getInstName() +
-               "' cannot be used in a 'match' pattern");
-    return false;
+    if (BP->getBuiltinKind() != BI_HasOneUse) {
+      PrintError("'" + BP->getInstName() +
+                 "' cannot be used in a 'match' pattern");
+      return false;
+    }
   }
 
   MatchPats[Name] = std::move(Pat);
@@ -1371,6 +1373,9 @@ bool CombineRuleBuilder::checkSemantics() {
       }
       break;
     }
+    case BI_HasOneUse:
+      // GIHasOneUse is a match-only predicate, not valid in apply patterns.
+      break;
     }
   }
 
@@ -1620,8 +1625,12 @@ bool CombineRuleBuilder::emitMatchPattern(CodeExpansions &CE,
 
     if (!emitPatFragMatchPattern(CE, Alts, M, &IM, *PFP, SeenPats))
       return false;
-  } else if (isa<BuiltinPattern>(&IP)) {
-    llvm_unreachable("No match builtins known!");
+  } else if (const auto *BP = dyn_cast<BuiltinPattern>(&IP)) {
+    if (BP->getBuiltinKind() == BI_HasOneUse) {
+      IM.addPredicate<OneUsePredicateMatcher>();
+    } else {
+      llvm_unreachable("No match builtins known!");
+    }
   } else {
     llvm_unreachable("Unknown kind of InstructionPattern!");
   }
@@ -1643,9 +1652,23 @@ bool CombineRuleBuilder::emitMatchPattern(CodeExpansions &CE,
         return false;
       continue;
     }
-    case Pattern::K_Builtin:
+    case Pattern::K_Builtin: {
+      const auto *BP = cast<BuiltinPattern>(Pat.get());
+      if (BP->getBuiltinKind() == BI_HasOneUse) {
+        assert(BP->getNumInstOperands() == 1 && "GIHasOneUse takes 1 operand");
+        StringRef OpName = BP->getOperand(0).getOperandName();
+        const auto *DefPat = MatchOpTable.getDef(OpName);
+        if (!DefPat) {
+          PrintError("GIHasOneUse: operand '" + OpName + "' not defined");
+          return false;
+        }
+        auto &InsnMatcher = M.getInstructionMatcher(DefPat->getName());
+        InsnMatcher.addPredicate<OneUsePredicateMatcher>();
+        continue;
+      }
       PrintError("No known match builtins");
       return false;
+    }
     case Pattern::K_CodeGenInstruction:
       cast<InstructionPattern>(Pat.get())->reportUnreachable(RuleDef.getLoc());
       return false;
@@ -1701,9 +1724,6 @@ bool CombineRuleBuilder::emitMatchPattern(CodeExpansions &CE,
           return false;
         continue;
       }
-      case Pattern::K_Builtin:
-        PrintError("No known match builtins");
-        return false;
       case Pattern::K_CodeGenInstruction:
         cast<InstructionPattern>(Pat.get())->reportUnreachable(
             RuleDef.getLoc());
@@ -2182,6 +2202,9 @@ bool CombineRuleBuilder::emitBuiltinApplyPattern(
       M.addAction<EraseInstAction>(/*InsnID*/ 0);
     return true;
   }
+  case BI_HasOneUse:
+    llvm_unreachable("GIHasOneUse cannot be used in apply patterns!");
+
   case BI_ReplaceReg: {
     StringRef Old = P.getOperand(0).getOperandName();
     StringRef New = P.getOperand(1).getOperandName();
@@ -2234,6 +2257,9 @@ bool CombineRuleBuilder::emitCodeGenInstructionMatchPattern(
   IM.addPredicate<InstructionOpcodeMatcher>(&P.getInst());
   declareInstExpansion(CE, IM, P.getName());
 
+  if (P.hasOneUse())
+    IM.addPredicate<OneUsePredicateMatcher>();
+
   // If this is an intrinsic, check the intrinsic ID.
   if (P.isIntrinsic()) {
     // The IntrinsicID's operand is the first operand after the defs.



More information about the llvm-commits mailing list