[llvm] [GlobalISel] Add nomerge attribute support (PR #224332)

Milica Kovacevic via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 17 08:03:12 PDT 2026


https://github.com/mkovacevic99 created https://github.com/llvm/llvm-project/pull/224332

GlobalISel currently ignores `nomerge`, so BranchFolding is free to tail-merge calls the source explicitly marked as needing distinct.

Added:
- CallLowering::lowerCall(MachineIRBuilder&, const CallBase&, ...) sets CallLoweringInfo::NoMerge from CB.cannotMerge(), then scans the instructions the target just built for the one flagged isCall() and tags it NoMerge. Generic across targets, unlike SelectionDAG which tags this per-target in each LowerCall.

- IRTranslator::translateTrap() lowers trap intrinsics outside that path, so it sets NoMerge explicitly in both of its branches.

Tests added for RISC-V, X86, and AArch64 (calls and tail-calls), plus X86 coverage for bare-opcode trap intrinsics. The named trap-func-name form isn't covered: it hits a pre-existing, unrelated GlobalISel miscompile when two calls share a named callee, independent of nomerge.

>From 93373e7eba711301506662a79a5c2cad997f707f Mon Sep 17 00:00:00 2001
From: Milica Kovacevic <mkovacevic at baylibre.com>
Date: Tue, 8 Sep 2026 12:44:41 +0200
Subject: [PATCH] [GlobalISel] Add nomerge attribute support

GlobalISel currently ignores `nomerge`, so BranchFolding is free to
tail-merge calls the source explicitly marked as needing distinct.

Added:
- CallLowering::lowerCall(MachineIRBuilder&, const CallBase&, ...) sets
  CallLoweringInfo::NoMerge from CB.cannotMerge(), then scans the
  instructions the target just built for the one flagged isCall() and
  tags it NoMerge. Generic across targets, unlike SelectionDAG which
  tags this per-target in each LowerCall.

- IRTranslator::translateTrap() lowers trap intrinsics outside that
  path, so it sets NoMerge explicitly in both of its branches.

Tests added for RISC-V, X86, and AArch64 (calls and tail-calls), plus
X86 coverage for bare-opcode trap intrinsics. The named trap-func-name
form isn't covered: it hits a pre-existing, unrelated GlobalISel
miscompile when two calls share a named callee, independent of
nomerge.
---
 .../llvm/CodeGen/GlobalISel/CallLowering.h    |   4 +
 llvm/lib/CodeGen/GlobalISel/CallLowering.cpp  |  21 +++
 llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp  |  13 +-
 .../CodeGen/AArch64/GlobalISel/nomerge.ll     |  66 ++++++++
 llvm/test/CodeGen/RISCV/GlobalISel/nomerge.ll |  78 +++++++++
 llvm/test/CodeGen/X86/GlobalISel/nomerge.ll   | 155 ++++++++++++++++++
 6 files changed, 335 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/CodeGen/AArch64/GlobalISel/nomerge.ll
 create mode 100644 llvm/test/CodeGen/RISCV/GlobalISel/nomerge.ll
 create mode 100644 llvm/test/CodeGen/X86/GlobalISel/nomerge.ll

diff --git a/llvm/include/llvm/CodeGen/GlobalISel/CallLowering.h b/llvm/include/llvm/CodeGen/GlobalISel/CallLowering.h
index 110f40a817770..db688408333c0 100644
--- a/llvm/include/llvm/CodeGen/GlobalISel/CallLowering.h
+++ b/llvm/include/llvm/CodeGen/GlobalISel/CallLowering.h
@@ -161,6 +161,10 @@ class LLVM_ABI CallLowering {
     /// True if this call results in convergent operations.
     bool IsConvergent = true;
 
+    /// True if this call must not be merged with another call site (the
+    /// `nomerge` attribute)
+    bool NoMerge = false;
+
     GlobalValue *DeactivationSymbol = nullptr;
   };
 
diff --git a/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp b/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp
index 90a0791a04e06..b630a501d2af2 100644
--- a/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp
+++ b/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp
@@ -243,9 +243,30 @@ bool CallLowering::lowerCall(MachineIRBuilder &MIRBuilder, const CallBase &CB,
   Info.IsMustTailCall = CB.isMustTailCall();
   Info.IsTailCall = CanBeTailCalled;
   Info.IsVarArg = IsVarArg;
+  Info.NoMerge = CB.cannotMerge();
+
+  // Remember the insertion point and containing block so that, once the
+  // target has built whatever instructions it needs for this call, we can
+  // find the actual call instruction among them and mark it NoMerge. This
+  // avoids requiring every target's lowerCall() to do so itself.
+  MachineBasicBlock &CallMBB = MIRBuilder.getMBB();
+  MachineBasicBlock::iterator InsertPtBefore = MIRBuilder.getInsertPt();
+  bool InsertAtBegin = InsertPtBefore == CallMBB.begin();
+  if (!InsertAtBegin)
+    --InsertPtBefore;
+
   if (!lowerCall(MIRBuilder, Info))
     return false;
 
+  if (Info.NoMerge && &MIRBuilder.getMBB() == &CallMBB) {
+    MachineBasicBlock::iterator Start =
+        InsertAtBegin ? CallMBB.begin() : std::next(InsertPtBefore);
+    for (MachineBasicBlock::iterator It = Start, End = MIRBuilder.getInsertPt();
+         It != End; ++It)
+      if (It->isCall())
+        It->setFlag(MachineInstr::MIFlag::NoMerge);
+  }
+
   if (ReturnHintAlignReg && !Info.LoweredTailCall) {
     MIRBuilder.buildAssertAlign(ResRegs[0], ReturnHintAlignReg,
                                 ReturnHintAlign);
diff --git a/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp b/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
index 74a1ecf69655f..d8d8ae1267670 100644
--- a/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
+++ b/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
@@ -2611,12 +2611,17 @@ bool IRTranslatorImpl::translateTrap(const CallInst &CI,
   StringRef TrapFuncName =
       CI.getAttributes().getFnAttr("trap-func-name").getValueAsString();
   if (TrapFuncName.empty()) {
+    MachineInstrBuilder MIB;
     if (Opcode == TargetOpcode::G_UBSANTRAP) {
       uint64_t Code = cast<ConstantInt>(CI.getOperand(0))->getZExtValue();
-      MIRBuilder.buildInstr(Opcode, {}, ArrayRef<llvm::SrcOp>{Code});
+      MIB = MIRBuilder.buildInstr(Opcode, {}, ArrayRef<llvm::SrcOp>{Code});
     } else {
-      MIRBuilder.buildInstr(Opcode);
+      MIB = MIRBuilder.buildInstr(Opcode);
     }
+    // This doesn't go through the generic CallBase-based lowerCall(), so it
+    // never picks up CallLoweringInfo::NoMerge -- set it here directly.
+    if (CI.cannotMerge())
+      MIB.setMIFlag(MachineInstr::MIFlag::NoMerge);
     return true;
   }
 
@@ -2628,6 +2633,10 @@ bool IRTranslatorImpl::translateTrap(const CallInst &CI,
   Info.Callee = MachineOperand::CreateES(TrapFuncName.data());
   Info.CB = &CI;
   Info.OrigRet = {Register(), Type::getVoidTy(CI.getContext()), 0};
+  // This calls the CallLoweringInfo-based lowerCall() overload directly,
+  // bypassing the CallBase-based one that normally populates NoMerge -- set
+  // it here explicitly.
+  Info.NoMerge = CI.cannotMerge();
   return CLI->lowerCall(MIRBuilder, Info);
 }
 
diff --git a/llvm/test/CodeGen/AArch64/GlobalISel/nomerge.ll b/llvm/test/CodeGen/AArch64/GlobalISel/nomerge.ll
new file mode 100644
index 0000000000000..db228be4ef417
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/GlobalISel/nomerge.ll
@@ -0,0 +1,66 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 2
+; RUN: llc < %s -mtriple=aarch64 -global-isel -global-isel-abort=1 -o - | FileCheck %s
+
+define void @foo(i32 %i) nounwind {
+; CHECK-LABEL: foo:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    str x30, [sp, #-16]! // 8-byte Folded Spill
+; CHECK-NEXT:    cmp w0, #7
+; CHECK-NEXT:    b.eq .LBB0_3
+; CHECK-NEXT:  // %bb.1: // %entry
+; CHECK-NEXT:    cmp w0, #5
+; CHECK-NEXT:    b.ne .LBB0_4
+; CHECK-NEXT:  // %bb.2: // %if.then
+; CHECK-NEXT:    bl bar
+; CHECK-NEXT:    ldr x30, [sp], #16 // 8-byte Folded Reload
+; CHECK-NEXT:    b bar
+; CHECK-NEXT:  .LBB0_3: // %if.then2
+; CHECK-NEXT:    bl bar
+; CHECK-NEXT:  .LBB0_4: // %if.end3
+; CHECK-NEXT:    ldr x30, [sp], #16 // 8-byte Folded Reload
+; CHECK-NEXT:    b bar
+entry:
+  switch i32 %i, label %if.end3 [
+    i32 5, label %if.then
+    i32 7, label %if.then2
+  ]
+
+if.then:
+  tail call void @bar() #0
+  br label %if.end3
+
+if.then2:
+  tail call void @bar() #0
+  br label %if.end3
+
+if.end3:
+  tail call void @bar() #0
+  ret void
+}
+
+define void @foo_tail(i1 %i) nounwind {
+; CHECK-LABEL: foo_tail:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    tbz w0, #0, .LBB1_2
+; CHECK-NEXT:  // %bb.1: // %if.then
+; CHECK-NEXT:    b bar
+; CHECK-NEXT:  .LBB1_2: // %if.else
+; CHECK-NEXT:    b bar
+entry:
+  br i1 %i, label %if.then, label %if.else
+
+if.then:
+  tail call void @bar() #0
+  br label %if.end
+
+if.else:
+  tail call void @bar() #0
+  br label %if.end
+
+if.end:
+  ret void
+}
+
+declare void @bar()
+
+attributes #0 = { nomerge }
diff --git a/llvm/test/CodeGen/RISCV/GlobalISel/nomerge.ll b/llvm/test/CodeGen/RISCV/GlobalISel/nomerge.ll
new file mode 100644
index 0000000000000..d1df655db4115
--- /dev/null
+++ b/llvm/test/CodeGen/RISCV/GlobalISel/nomerge.ll
@@ -0,0 +1,78 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 2
+; RUN: llc < %s -mtriple=riscv64 -global-isel -global-isel-abort=1 -o - | FileCheck %s
+
+define void @foo(i32 %i) nounwind {
+; CHECK-LABEL: foo:
+; CHECK:       # %bb.0: # %entry
+; CHECK-NEXT:    addi sp, sp, -16
+; CHECK-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
+; CHECK-NEXT:    li a1, 7
+; CHECK-NEXT:    sext.w a0, a0
+; CHECK-NEXT:    beq a0, a1, .LBB0_3
+; CHECK-NEXT:  # %bb.1: # %entry
+; CHECK-NEXT:    li a1, 5
+; CHECK-NEXT:    bne a0, a1, .LBB0_4
+; CHECK-NEXT:  # %bb.2: # %if.then
+; CHECK-NEXT:    call bar
+; CHECK-NEXT:    j .LBB0_4
+; CHECK-NEXT:  .LBB0_3: # %if.then2
+; CHECK-NEXT:    call bar
+; CHECK-NEXT:  .LBB0_4: # %if.end3
+; CHECK-NEXT:    call bar
+; CHECK-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
+; CHECK-NEXT:    addi sp, sp, 16
+; CHECK-NEXT:    ret
+entry:
+  switch i32 %i, label %if.end3 [
+    i32 5, label %if.then
+    i32 7, label %if.then2
+  ]
+
+if.then:
+  tail call void @bar() #0
+  br label %if.end3
+
+if.then2:
+  tail call void @bar() #0
+  br label %if.end3
+
+if.end3:
+  tail call void @bar() #0
+  ret void
+}
+
+define void @foo_tail(i1 %i) nounwind {
+; CHECK-LABEL: foo_tail:
+; CHECK:       # %bb.0: # %entry
+; CHECK-NEXT:    addi sp, sp, -16
+; CHECK-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
+; CHECK-NEXT:    xori a0, a0, 1
+; CHECK-NEXT:    andi a0, a0, 1
+; CHECK-NEXT:    bnez a0, .LBB1_2
+; CHECK-NEXT:  # %bb.1: # %if.then
+; CHECK-NEXT:    call bar
+; CHECK-NEXT:    j .LBB1_3
+; CHECK-NEXT:  .LBB1_2: # %if.else
+; CHECK-NEXT:    call bar
+; CHECK-NEXT:  .LBB1_3: # %if.then
+; CHECK-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
+; CHECK-NEXT:    addi sp, sp, 16
+; CHECK-NEXT:    ret
+entry:
+  br i1 %i, label %if.then, label %if.else
+
+if.then:
+  tail call void @bar() #0
+  br label %if.end
+
+if.else:
+  tail call void @bar() #0
+  br label %if.end
+
+if.end:
+  ret void
+}
+
+declare void @bar()
+
+attributes #0 = { nomerge }
diff --git a/llvm/test/CodeGen/X86/GlobalISel/nomerge.ll b/llvm/test/CodeGen/X86/GlobalISel/nomerge.ll
new file mode 100644
index 0000000000000..0b4940df2e600
--- /dev/null
+++ b/llvm/test/CodeGen/X86/GlobalISel/nomerge.ll
@@ -0,0 +1,155 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 2
+; RUN: llc < %s -mtriple=x86_64-linux-gnu -global-isel -global-isel-abort=1 -o - | FileCheck %s
+
+define void @foo(i32 %i) nounwind {
+; CHECK-LABEL: foo:
+; CHECK:       # %bb.0: # %entry
+; CHECK-NEXT:    pushq %rax
+; CHECK-NEXT:    cmpl $7, %edi
+; CHECK-NEXT:    sete %al
+; CHECK-NEXT:    testb $1, %al
+; CHECK-NEXT:    jne .LBB0_3
+; CHECK-NEXT:  # %bb.1: # %entry
+; CHECK-NEXT:    cmpl $5, %edi
+; CHECK-NEXT:    sete %al
+; CHECK-NEXT:    testb $1, %al
+; CHECK-NEXT:    je .LBB0_4
+; CHECK-NEXT:  # %bb.2: # %if.then
+; CHECK-NEXT:    callq bar
+; CHECK-NEXT:    jmp .LBB0_4
+; CHECK-NEXT:  .LBB0_3: # %if.then2
+; CHECK-NEXT:    callq bar
+; CHECK-NEXT:  .LBB0_4: # %if.end3
+; CHECK-NEXT:    callq bar
+; CHECK-NEXT:    popq %rax
+; CHECK-NEXT:    retq
+entry:
+  switch i32 %i, label %if.end3 [
+    i32 5, label %if.then
+    i32 7, label %if.then2
+  ]
+
+if.then:
+  tail call void @bar() #0
+  br label %if.end3
+
+if.then2:
+  tail call void @bar() #0
+  br label %if.end3
+
+if.end3:
+  tail call void @bar() #0
+  ret void
+}
+
+define void @foo_tail(i1 %i) nounwind {
+; CHECK-LABEL: foo_tail:
+; CHECK:       # %bb.0: # %entry
+; CHECK-NEXT:    pushq %rax
+; CHECK-NEXT:    testb $1, %dil
+; CHECK-NEXT:    je .LBB1_2
+; CHECK-NEXT:  # %bb.1: # %if.then
+; CHECK-NEXT:    callq bar
+; CHECK-NEXT:    popq %rax
+; CHECK-NEXT:    retq
+; CHECK-NEXT:  .LBB1_2: # %if.else
+; CHECK-NEXT:    callq bar
+; CHECK-NEXT:    popq %rax
+; CHECK-NEXT:    retq
+entry:
+  br i1 %i, label %if.then, label %if.else
+
+if.then:
+  tail call void @bar() #0
+  br label %if.end
+
+if.else:
+  tail call void @bar() #0
+  br label %if.end
+
+if.end:
+  ret void
+}
+
+declare dso_local void @bar()
+
+define void @nomerge_trap(i32 %i) {
+; CHECK-LABEL: nomerge_trap:
+; CHECK:       # %bb.0: # %entry
+; CHECK-NEXT:    cmpl $7, %edi
+; CHECK-NEXT:    sete %al
+; CHECK-NEXT:    testb $1, %al
+; CHECK-NEXT:    jne .LBB2_3
+; CHECK-NEXT:  # %bb.1: # %entry
+; CHECK-NEXT:    cmpl $5, %edi
+; CHECK-NEXT:    sete %al
+; CHECK-NEXT:    testb $1, %al
+; CHECK-NEXT:    je .LBB2_4
+; CHECK-NEXT:  # %bb.2: # %if.then
+; CHECK-NEXT:    ud2
+; CHECK-NEXT:  .LBB2_3: # %if.then2
+; CHECK-NEXT:    ud2
+; CHECK-NEXT:  .LBB2_4: # %if.end3
+; CHECK-NEXT:    ud2
+entry:
+  switch i32 %i, label %if.end3 [
+    i32 5, label %if.then
+    i32 7, label %if.then2
+  ]
+
+if.then:
+  tail call void @llvm.trap() #0
+  unreachable
+
+if.then2:
+  tail call void @llvm.trap() #0
+  unreachable
+
+if.end3:
+  tail call void @llvm.trap() #0
+  unreachable
+}
+
+declare dso_local void @llvm.trap()
+
+define void @nomerge_debugtrap(i32 %i) {
+; CHECK-LABEL: nomerge_debugtrap:
+; CHECK:       # %bb.0: # %entry
+; CHECK-NEXT:    cmpl $7, %edi
+; CHECK-NEXT:    sete %al
+; CHECK-NEXT:    testb $1, %al
+; CHECK-NEXT:    jne .LBB3_3
+; CHECK-NEXT:  # %bb.1: # %entry
+; CHECK-NEXT:    cmpl $5, %edi
+; CHECK-NEXT:    sete %al
+; CHECK-NEXT:    testb $1, %al
+; CHECK-NEXT:    je .LBB3_4
+; CHECK-NEXT:  # %bb.2: # %if.then
+; CHECK-NEXT:    int3
+; CHECK-NEXT:  .LBB3_3: # %if.then2
+; CHECK-NEXT:    int3
+; CHECK-NEXT:  .LBB3_4: # %if.end3
+; CHECK-NEXT:    int3
+entry:
+  switch i32 %i, label %if.end3 [
+    i32 5, label %if.then
+    i32 7, label %if.then2
+  ]
+
+if.then:
+  tail call void @llvm.debugtrap() #0
+  unreachable
+
+if.then2:
+  tail call void @llvm.debugtrap() #0
+  unreachable
+
+if.end3:
+  tail call void @llvm.debugtrap() #0
+  unreachable
+}
+
+declare dso_local void @llvm.debugtrap()
+
+attributes #0 = { nomerge }
+



More information about the llvm-commits mailing list