[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