[llvm] [SPIRV] Sign-extend operands of sign-sensitive ops on sub-pow2 widths (PR #203661)

Faijul Amin via llvm-commits llvm-commits at lists.llvm.org
Sat Jul 11 11:28:36 PDT 2026


https://github.com/mdfaijul updated https://github.com/llvm/llvm-project/pull/203661

>From a9b5170f51ee5784341966ad0beba386cf0d87f0 Mon Sep 17 00:00:00 2001
From: Faijul Amin <md.faijul.amin at intel.com>
Date: Fri, 12 Jun 2026 19:07:21 -0700
Subject: [PATCH 1/3] [SPIRV] Sign-extend operands of sign-sensitive ops on
 sub-pow2 widths

SPIRVPreLegalizer widens sub-pow2 scalars by relabeling the LLT only,
so signed G_ICMP / G_ASHR / G_SDIV / G_SREM read the sign bit at the
wrong position. Emit G_SEXT_INREG on each value operand before the
widening loop, and add G_SEXT_INREG.lower() so the legalizer expands
it to (x << k) ashr k. Adds a lit test covering i4 / i24 cases.
---
 llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp  |   4 +
 llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp   |  78 ++++++++++
 .../SPIRV/legalization/signed-narrow-int.ll   | 146 ++++++++++++++++++
 3 files changed, 228 insertions(+)
 create mode 100644 llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll

diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
index 6c9b7eb2ef37e..16f37d8c860d1 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
@@ -353,6 +353,10 @@ SPIRVLegalizerInfo::SPIRVLegalizerInfo(const SPIRVSubtarget &ST) {
       .legalForCartesianProduct(allScalarsAndVectors)
       .legalIf(extendedScalarsAndVectorsProduct);
 
+  // SPIR-V has no native sign-extend-in-register opcode; lower it to the
+  // canonical (x << k) ashr k pair, which the SPIR-V dialect supports.
+  getActionDefinitionsBuilder(G_SEXT_INREG).lower();
+
   getActionDefinitionsBuilder(G_PHI)
       .legalFor(allPtrsScalarsAndVectors)
       .legalIf(extendedPtrsScalarsAndVectors);
diff --git a/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp b/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
index 58e90acd52024..f485a6b99cdfb 100644
--- a/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
@@ -20,6 +20,7 @@
 #include "llvm/CodeGen/GlobalISel/GISelValueTracking.h"
 #include "llvm/IR/Attributes.h"
 #include "llvm/IR/Constants.h"
+#include "llvm/IR/InstrTypes.h"
 #include "llvm/IR/IntrinsicsSPIRV.h"
 
 #define DEBUG_TYPE "spirv-prelegalizer"
@@ -564,6 +565,83 @@ generateAssignInstrs(MachineFunction &MF, SPIRVGlobalRegistry *GR,
     }
     for (MachineInstr *MI : TruncToRemove)
       MI->eraseFromParent();
+
+    // The widening loop below only retypes registers, so sign-sensitive ops
+    // (G_ASHR, G_SDIV, G_SREM, signed G_ICMP) on sub-pow2 widths see the sign
+    // bit at the wrong position. Emit G_SEXT_INREG; the legalizer lowers it.
+    // TODO: handle vector operands.
+    auto IsSignSensitive = [](const MachineInstr &MI) {
+      switch (MI.getOpcode()) {
+      case TargetOpcode::G_ASHR:
+      case TargetOpcode::G_SDIV:
+      case TargetOpcode::G_SREM:
+        return true;
+      case TargetOpcode::G_ICMP:
+        return CmpInst::isSigned(
+            static_cast<CmpInst::Predicate>(MI.getOperand(1).getPredicate()));
+      default:
+        return false;
+      }
+    };
+
+    // Per-Reg original width: once we retype a Reg to its widened width,
+    // a later visit can no longer infer the narrow width from MRI. Record
+    // it on first visit so subsequent visits still know to sign-extend.
+    DenseMap<Register, unsigned> OrigWidth;
+    // Per-MI cache so the same vreg used as both operands of a single MI
+    // (e.g. G_ICMP slt %x, %x) gets one shared sext, not two — and so the
+    // second operand isn't left referring to the un-extended Reg after the
+    // first call retypes it.
+    DenseMap<Register, Register> SExtedThisMI;
+    auto SignExtendOperand = [&](MachineOperand &MOP, MachineInstr &MI) {
+      if (MOP.isCImm()) {
+        const ConstantInt *V = MOP.getCImm();
+        unsigned NewWidth = widenBitWidthToNextPow2(V->getBitWidth());
+        if (NewWidth != V->getBitWidth())
+          MOP.setCImm(ConstantInt::get(V->getType()->getContext(),
+                                       V->getValue().sextOrTrunc(NewWidth)));
+        return;
+      }
+      if (!MOP.isReg())
+        return;
+      Register Reg = MOP.getReg();
+      auto [OWIt, OWInserted] =
+          OrigWidth.try_emplace(Reg, MRI.getType(Reg).getScalarSizeInBits());
+      unsigned OldW = OWIt->second;
+      unsigned NewW = widenBitWidthToNextPow2(OldW);
+      if (NewW == OldW)
+        return;
+
+      auto [It, Inserted] = SExtedThisMI.try_emplace(Reg, Register());
+      if (Inserted) {
+        LLT NewLLT = LLT::scalar(NewW);
+        SPIRVTypeInst SpvTy = GR->getOrCreateSPIRVIntegerType(NewW, MIB);
+        Register SExted = MRI.createGenericVirtualRegister(NewLLT);
+        GR->assignSPIRVTypeToVReg(SpvTy, SExted, MF);
+        MRI.setRegClass(SExted, GR->getRegClass(SpvTy));
+        MRI.setType(Reg, NewLLT);
+        MIB.setInsertPt(*MI.getParent(), MI.getIterator());
+        MIB.buildSExtInReg(SExted, Reg, OldW);
+        It->second = SExted;
+      }
+      MOP.setReg(It->second);
+    };
+
+    for (MachineBasicBlock &MBB : MF) {
+      for (MachineInstr &MI : llvm::make_early_inc_range(MBB)) {
+        if (!IsSignSensitive(MI))
+          continue;
+        // For sign-sensitive instructions, the value operands are always
+        // the last two, regardless of any leading def or predicate operands.
+        unsigned N = MI.getNumOperands();
+        const MachineOperand &LHS = MI.getOperand(N - 2);
+        if (LHS.isReg() && !MRI.getType(LHS.getReg()).isScalar())
+          continue;
+        SExtedThisMI.clear();
+        SignExtendOperand(MI.getOperand(N - 2), MI);
+        SignExtendOperand(MI.getOperand(N - 1), MI);
+      }
+    }
   }
 
   for (MachineBasicBlock *MBB : post_order(&MF)) {
diff --git a/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll b/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll
new file mode 100644
index 0000000000000..f1d45b488d993
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll
@@ -0,0 +1,146 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -verify-machineinstrs -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv32-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -verify-machineinstrs -mtriple=spirv32-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; SPIR-V (without sub-byte int extensions) widens sub-pow2 scalars to the next
+; legal width by relabeling the LLT only, without inserting any sign-extension.
+; Sign-sensitive ops (icmp slt/sle/sgt/sge, ashr, sdiv, srem) on such operands
+; would then read the sign bit at the wrong position. The pre-legalizer must
+; emit a sign-extend-in-register before the widening so the wide-width signed
+; op observes the correct sign bit.
+
+; CHECK-DAG: %[[#I8:]] = OpTypeInt 8 0
+; CHECK-DAG: %[[#I32:]] = OpTypeInt 32 0
+; CHECK-DAG: %[[#K4:]] = OpConstant %[[#I8]] 4
+; CHECK-DAG: %[[#K8:]] = OpConstant %[[#I32]] 8
+
+; ----------------------------------------------------------------------------
+; icmp slt i4 against zero (the canonical XLA F4E2M1FN sign-bit-check pattern).
+; CHECK: OpFunction
+; CHECK: %[[#X1:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL1:]] = OpShiftLeftLogical %[[#I8]] %[[#X1]] %[[#K4]]
+; CHECK: %[[#SX1:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL1]] %[[#K4]]
+; CHECK: OpSLessThan {{%[0-9]+}} %[[#SX1]] {{%[0-9]+}}
+define spir_kernel void @icmp_slt_i4_zero(i4 %x, ptr addrspace(1) %out) {
+  %c = icmp slt i4 %x, 0
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; icmp slt i4 between two registers: both operands must be sign-extended.
+; CHECK: OpFunction
+; CHECK: %[[#X2:]] = OpFunctionParameter
+; CHECK: %[[#Y2:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHLA2:]] = OpShiftLeftLogical %[[#I8]] %[[#X2]] %[[#K4]]
+; CHECK: %[[#SXA2:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLA2]] %[[#K4]]
+; CHECK: %[[#SHLB2:]] = OpShiftLeftLogical %[[#I8]] %[[#Y2]] %[[#K4]]
+; CHECK: %[[#SXB2:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLB2]] %[[#K4]]
+; CHECK: OpSLessThan {{%[0-9]+}} %[[#SXA2]] %[[#SXB2]]
+define spir_kernel void @icmp_slt_i4_reg(i4 %x, i4 %y, ptr addrspace(1) %out) {
+  %c = icmp slt i4 %x, %y
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; ashr i4: arithmetic right shift on a widened operand needs the sign bit at
+; the top of the wider register.
+; CHECK: OpFunction
+; CHECK: %[[#X3:]] = OpFunctionParameter
+; CHECK: %[[#Y3:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHLA3:]] = OpShiftLeftLogical %[[#I8]] %[[#X3]] %[[#K4]]
+; CHECK: %[[#SXA3:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLA3]] %[[#K4]]
+; CHECK: %[[#SHLB3:]] = OpShiftLeftLogical %[[#I8]] %[[#Y3]] %[[#K4]]
+; CHECK: %[[#SXB3:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLB3]] %[[#K4]]
+; CHECK: OpShiftRightArithmetic %[[#I8]] %[[#SXA3]] %[[#SXB3]]
+define spir_kernel void @ashr_i4(i4 %x, i4 %y, ptr addrspace(1) %out) {
+  %r = ashr i4 %x, %y
+  %z = sext i4 %r to i32
+  store i32 %z, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; sdiv i4: signed division.
+; CHECK: OpFunction
+; CHECK: %[[#X4:]] = OpFunctionParameter
+; CHECK: %[[#Y4:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHLA4:]] = OpShiftLeftLogical %[[#I8]] %[[#X4]] %[[#K4]]
+; CHECK: %[[#SXA4:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLA4]] %[[#K4]]
+; CHECK: %[[#SHLB4:]] = OpShiftLeftLogical %[[#I8]] %[[#Y4]] %[[#K4]]
+; CHECK: %[[#SXB4:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLB4]] %[[#K4]]
+; CHECK: OpSDiv %[[#I8]] %[[#SXA4]] %[[#SXB4]]
+define spir_kernel void @sdiv_i4(i4 %x, i4 %y, ptr addrspace(1) %out) {
+  %r = sdiv i4 %x, %y
+  %z = sext i4 %r to i32
+  store i32 %z, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; srem i4: signed remainder.
+; CHECK: OpFunction
+; CHECK: %[[#X5:]] = OpFunctionParameter
+; CHECK: %[[#Y5:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHLA5:]] = OpShiftLeftLogical %[[#I8]] %[[#X5]] %[[#K4]]
+; CHECK: %[[#SXA5:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLA5]] %[[#K4]]
+; CHECK: %[[#SHLB5:]] = OpShiftLeftLogical %[[#I8]] %[[#Y5]] %[[#K4]]
+; CHECK: %[[#SXB5:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHLB5]] %[[#K4]]
+; CHECK: OpSRem %[[#I8]] %[[#SXA5]] %[[#SXB5]]
+define spir_kernel void @srem_i4(i4 %x, i4 %y, ptr addrspace(1) %out) {
+  %r = srem i4 %x, %y
+  %z = sext i4 %r to i32
+  store i32 %z, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; A non-pow2 width that widens to a different legal size: i24 -> i32, k = 8.
+; CHECK: OpFunction
+; CHECK: %[[#X6:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL6:]] = OpShiftLeftLogical %[[#I32]] %[[#X6]] %[[#K8]]
+; CHECK: %[[#SX6:]] = OpShiftRightArithmetic %[[#I32]] %[[#SHL6]] %[[#K8]]
+; CHECK: OpSLessThan {{%[0-9]+}} %[[#SX6]] {{%[0-9]+}}
+define spir_kernel void @icmp_slt_i24_zero(i24 %x, ptr addrspace(1) %out) {
+  %c = icmp slt i24 %x, 0
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; Negative test: unsigned compare must NOT emit sign-extension shifts.
+; CHECK: OpFunction
+; CHECK: %[[#X7:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK-NOT: OpShiftRightArithmetic
+; CHECK: OpULessThan {{%[0-9]+}} %[[#X7]] {{%[0-9]+}}
+define spir_kernel void @icmp_ult_i4_one(i4 %x, ptr addrspace(1) %out) {
+  %c = icmp ult i4 %x, 1
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; Negative test: logical right shift must NOT emit sign-extension shifts.
+; CHECK: OpFunction
+; CHECK-NOT: OpShiftRightArithmetic
+; CHECK: OpShiftRightLogical
+define spir_kernel void @lshr_i4(i4 %x, i4 %y, ptr addrspace(1) %out) {
+  %r = lshr i4 %x, %y
+  %z = zext i4 %r to i32
+  store i32 %z, ptr addrspace(1) %out
+  ret void
+}

>From 6f08f533cc6fb0fe0d2b37aa1d599fff0bd3cbc6 Mon Sep 17 00:00:00 2001
From: Faijul Amin <md.faijul.amin at intel.com>
Date: Sat, 11 Jul 2026 09:57:34 -0700
Subject: [PATCH 2/3] [SPIRV] Sign-extend narrow scalars for sign-sensitive ops

Sub-pow2 integers (e.g. i24, i40) are widened to the next pow2 by an LLT
relabel, which leaves the sign bit at its original position rather than
the widened MSB. Ops that depend on the sign bit (G_ASHR, G_SDIV, G_SREM,
signed G_ICMP) therefore need an explicit G_SEXT_INREG on each value
operand before the relabel takes effect.

Changes:
- Pre-scan sign-sensitive ops and record each value operand's original
  scalar width, since later passes retype those vregs to their widened
  LLTs and destroy the narrow width needed for G_SEXT_INREG.
- Move the sign-extension logic into a dedicated widenSignSensitiveOps
  helper (with a matching isSignSensitiveOp classifier) instead of
  inlining it in generateAssignInstrs.
- Drop the unnecessary per-op cache: each operand only needs a single
  G_SEXT_INREG at its use site.
- Add a TODO listing follow-up sign-sensitive opcodes (G_SMIN/G_SMAX,
  G_SADDSAT/G_SSUBSAT, signed overflow ops) to handle in future PRs.
- Extend the test coverage for G_ICMP with sle/sgt/sge predicates.

Assisted by: Claude
---
 llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp   | 198 +++++++++++-------
 .../SPIRV/legalization/signed-narrow-int.ll   |  88 ++++++++
 2 files changed, 210 insertions(+), 76 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp b/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
index 517254e83f567..735e9c23cf861 100644
--- a/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
@@ -467,6 +467,121 @@ void processInstr(MachineInstr &MI, MachineIRBuilder &MIB,
 }
 } // namespace llvm
 
+// Sign-sensitive integer ops: their result depends on the value of the input
+// sign bit at position (width-1). On sub-pow2 widths the general widening
+// loop is a pure LLT relabel, which leaves the sign bit at the *original*
+// position instead of the widened MSB. These ops therefore need an explicit
+// G_SEXT_INREG on each value operand to move the sign bit up.
+//
+// Signed-vs-unsigned G_ICMP is distinguished by its predicate operand.
+//
+// TODO: follow-up PRs will add the remaining sign-sensitive opcodes
+// (e.g. G_SMIN/G_SMAX, G_SADDSAT/G_SSUBSAT, signed overflow ops).
+static bool isSignSensitiveOp(const MachineInstr &MI) {
+  switch (MI.getOpcode()) {
+  case TargetOpcode::G_ASHR:
+  case TargetOpcode::G_SDIV:
+  case TargetOpcode::G_SREM:
+    return true;
+  case TargetOpcode::G_ICMP:
+    return CmpInst::isSigned(
+        static_cast<CmpInst::Predicate>(MI.getOperand(1).getPredicate()));
+  default:
+    return false;
+  }
+}
+
+// Record each sign-sensitive op's scalar value-operand widths before any
+// widening runs. Once later passes retype those vregs to their pow2 LLTs, the
+// original narrow width is gone; widenSignSensitiveOps needs it to know how
+// many low bits to sign-extend.
+static DenseMap<Register, unsigned>
+recordSignSensitiveOperandWidths(MachineFunction &MF,
+                                   MachineRegisterInfo &MRI) {
+  DenseMap<Register, unsigned> OrigWidth;
+  for (MachineBasicBlock &MBB : MF) {
+    for (MachineInstr &MI : MBB) {
+      if (!isSignSensitiveOp(MI))
+        continue;
+      // Value operands are the trailing two, past any def or predicate.
+      unsigned N = MI.getNumOperands();
+      for (unsigned I : {N - 2, N - 1}) {
+        const MachineOperand &MOP = MI.getOperand(I);
+        if (!MOP.isReg())
+          continue;
+        Register Reg = MOP.getReg();
+        if (!MRI.getType(Reg).isScalar())
+          continue;
+        OrigWidth.try_emplace(Reg, MRI.getType(Reg).getScalarSizeInBits());
+      }
+    }
+  }
+  return OrigWidth;
+}
+
+// For every sign-sensitive op, insert G_SEXT_INREG on each value operand
+// whose original width (from OrigWidth, captured before any retyping) is
+// narrower than the widened pow2 width. The rewritten operand's vreg LLT
+// is retyped in place to the widened width.
+//
+// OrigWidth must have been populated by recordSignSensitiveOperandWidths
+// before other passes retyped the vregs; otherwise the narrow widths
+// needed here are lost.
+//
+// TODO: handle vector operands.
+static void
+widenSignSensitiveOps(MachineFunction &MF, SPIRVGlobalRegistry *GR,
+                      MachineIRBuilder &MIB, MachineRegisterInfo &MRI,
+                      const DenseMap<Register, unsigned> &OrigWidth) {
+  // Emit G_SEXT_INREG from Reg's recorded narrow width; retypes Reg to the
+  // widened width. Returns an invalid Register if no widening is needed.
+  auto GetSignExtendedReg = [&](Register Reg, MachineInstr &MI) -> Register {
+    auto OWIt = OrigWidth.find(Reg);
+    assert(OWIt != OrigWidth.end() &&
+           "Sign-sensitive operand width was not recorded");
+    unsigned OldW = OWIt->second;
+    unsigned NewW = widenBitWidthToNextPow2(OldW);
+    if (NewW == OldW)
+      return Register();
+    LLT NewLLT = LLT::scalar(NewW);
+    SPIRVTypeInst SpvTy = GR->getOrCreateSPIRVIntegerType(NewW, MIB);
+    Register SExted = MRI.createGenericVirtualRegister(NewLLT);
+    GR->assignSPIRVTypeToVReg(SpvTy, SExted, MF);
+    MRI.setRegClass(SExted, GR->getRegClass(SpvTy));
+    MRI.setType(Reg, NewLLT);
+    MIB.setInsertPt(*MI.getParent(), MI.getIterator());
+    MIB.buildSExtInReg(SExted, Reg, OldW);
+    return SExted;
+  };
+
+  for (MachineBasicBlock &MBB : MF) {
+    for (MachineInstr &MI : llvm::make_early_inc_range(MBB)) {
+      if (!isSignSensitiveOp(MI))
+        continue;
+      unsigned N = MI.getNumOperands();
+      MachineOperand &LHS = MI.getOperand(N - 2);
+      MachineOperand &RHS = MI.getOperand(N - 1);
+      // Sign-sensitive opcodes carry register operands only
+      assert(LHS.isReg() && RHS.isReg());
+      if (!MRI.getType(LHS.getReg()).isScalar())
+        continue;
+
+      Register LHSReg = LHS.getReg();
+      Register RHSReg = RHS.getReg();
+      if (Register SExted = GetSignExtendedReg(LHSReg, MI))
+        LHS.setReg(SExted);
+      // Same vreg on both sides (e.g. G_ICMP slt %x, %x): reuse the sext
+      // just emitted for LHS instead of emitting a second one.
+      if (RHSReg == LHSReg) {
+        RHS.setReg(LHS.getReg());
+        continue;
+      }
+      if (Register SExted = GetSignExtendedReg(RHSReg, MI))
+        RHS.setReg(SExted);
+    }
+  }
+}
+
 static void
 generateAssignInstrs(MachineFunction &MF, SPIRVGlobalRegistry *GR,
                      MachineIRBuilder MIB,
@@ -490,6 +605,12 @@ generateAssignInstrs(MachineFunction &MF, SPIRVGlobalRegistry *GR,
     // integer widths of 8, 16, 32, 64. Non-standard widths (e.g., i24, i40)
     // must be widened to the next power of two.
     //
+    // Record the original widths of sign-sensitive operands before either
+    // the G_TRUNC handling or the general widening loop retypes vregs, then
+    // rewrite those ops after G_TRUNC processing using the recorded widths.
+    DenseMap<Register, unsigned> OrigWidth =
+        recordSignSensitiveOperandWidths(MF, MRI);
+
     // G_TRUNC requires special handling because its semantics depend on the
     // original destination width. For example:
     //   %dst:s24 = G_TRUNC %src:s64
@@ -567,82 +688,7 @@ generateAssignInstrs(MachineFunction &MF, SPIRVGlobalRegistry *GR,
     for (MachineInstr *MI : TruncToRemove)
       MI->eraseFromParent();
 
-    // The widening loop below only retypes registers, so sign-sensitive ops
-    // (G_ASHR, G_SDIV, G_SREM, signed G_ICMP) on sub-pow2 widths see the sign
-    // bit at the wrong position. Emit G_SEXT_INREG; the legalizer lowers it.
-    // TODO: handle vector operands.
-    auto IsSignSensitive = [](const MachineInstr &MI) {
-      switch (MI.getOpcode()) {
-      case TargetOpcode::G_ASHR:
-      case TargetOpcode::G_SDIV:
-      case TargetOpcode::G_SREM:
-        return true;
-      case TargetOpcode::G_ICMP:
-        return CmpInst::isSigned(
-            static_cast<CmpInst::Predicate>(MI.getOperand(1).getPredicate()));
-      default:
-        return false;
-      }
-    };
-
-    // Per-Reg original width: once we retype a Reg to its widened width,
-    // a later visit can no longer infer the narrow width from MRI. Record
-    // it on first visit so subsequent visits still know to sign-extend.
-    DenseMap<Register, unsigned> OrigWidth;
-    // Per-MI cache so the same vreg used as both operands of a single MI
-    // (e.g. G_ICMP slt %x, %x) gets one shared sext, not two — and so the
-    // second operand isn't left referring to the un-extended Reg after the
-    // first call retypes it.
-    DenseMap<Register, Register> SExtedThisMI;
-    auto SignExtendOperand = [&](MachineOperand &MOP, MachineInstr &MI) {
-      if (MOP.isCImm()) {
-        const ConstantInt *V = MOP.getCImm();
-        unsigned NewWidth = widenBitWidthToNextPow2(V->getBitWidth());
-        if (NewWidth != V->getBitWidth())
-          MOP.setCImm(ConstantInt::get(V->getType()->getContext(),
-                                       V->getValue().sextOrTrunc(NewWidth)));
-        return;
-      }
-      if (!MOP.isReg())
-        return;
-      Register Reg = MOP.getReg();
-      auto [OWIt, OWInserted] =
-          OrigWidth.try_emplace(Reg, MRI.getType(Reg).getScalarSizeInBits());
-      unsigned OldW = OWIt->second;
-      unsigned NewW = widenBitWidthToNextPow2(OldW);
-      if (NewW == OldW)
-        return;
-
-      auto [It, Inserted] = SExtedThisMI.try_emplace(Reg, Register());
-      if (Inserted) {
-        LLT NewLLT = LLT::scalar(NewW);
-        SPIRVTypeInst SpvTy = GR->getOrCreateSPIRVIntegerType(NewW, MIB);
-        Register SExted = MRI.createGenericVirtualRegister(NewLLT);
-        GR->assignSPIRVTypeToVReg(SpvTy, SExted, MF);
-        MRI.setRegClass(SExted, GR->getRegClass(SpvTy));
-        MRI.setType(Reg, NewLLT);
-        MIB.setInsertPt(*MI.getParent(), MI.getIterator());
-        MIB.buildSExtInReg(SExted, Reg, OldW);
-        It->second = SExted;
-      }
-      MOP.setReg(It->second);
-    };
-
-    for (MachineBasicBlock &MBB : MF) {
-      for (MachineInstr &MI : llvm::make_early_inc_range(MBB)) {
-        if (!IsSignSensitive(MI))
-          continue;
-        // For sign-sensitive instructions, the value operands are always
-        // the last two, regardless of any leading def or predicate operands.
-        unsigned N = MI.getNumOperands();
-        const MachineOperand &LHS = MI.getOperand(N - 2);
-        if (LHS.isReg() && !MRI.getType(LHS.getReg()).isScalar())
-          continue;
-        SExtedThisMI.clear();
-        SignExtendOperand(MI.getOperand(N - 2), MI);
-        SignExtendOperand(MI.getOperand(N - 1), MI);
-      }
-    }
+    widenSignSensitiveOps(MF, GR, MIB, MRI, OrigWidth);
   }
 
   for (MachineBasicBlock *MBB : post_order(&MF)) {
diff --git a/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll b/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll
index f1d45b488d993..9af2ca82fcd8f 100644
--- a/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll
+++ b/llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll
@@ -31,6 +31,51 @@ define spir_kernel void @icmp_slt_i4_zero(i4 %x, ptr addrspace(1) %out) {
   ret void
 }
 
+; ----------------------------------------------------------------------------
+; icmp sle i4: same widening as slt, different SPIR-V opcode.
+; CHECK: OpFunction
+; CHECK: %[[#X_SLE:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL_SLE:]] = OpShiftLeftLogical %[[#I8]] %[[#X_SLE]] %[[#K4]]
+; CHECK: %[[#SX_SLE:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_SLE]] %[[#K4]]
+; CHECK: OpSLessThanEqual {{%[0-9]+}} %[[#SX_SLE]] {{%[0-9]+}}
+define spir_kernel void @icmp_sle_i4_zero(i4 %x, ptr addrspace(1) %out) {
+  %c = icmp sle i4 %x, 0
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; icmp sgt i4.
+; CHECK: OpFunction
+; CHECK: %[[#X_SGT:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL_SGT:]] = OpShiftLeftLogical %[[#I8]] %[[#X_SGT]] %[[#K4]]
+; CHECK: %[[#SX_SGT:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_SGT]] %[[#K4]]
+; CHECK: OpSGreaterThan {{%[0-9]+}} %[[#SX_SGT]] {{%[0-9]+}}
+define spir_kernel void @icmp_sgt_i4_zero(i4 %x, ptr addrspace(1) %out) {
+  %c = icmp sgt i4 %x, 0
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; icmp sge i4.
+; CHECK: OpFunction
+; CHECK: %[[#X_SGE:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL_SGE:]] = OpShiftLeftLogical %[[#I8]] %[[#X_SGE]] %[[#K4]]
+; CHECK: %[[#SX_SGE:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_SGE]] %[[#K4]]
+; CHECK: OpSGreaterThanEqual {{%[0-9]+}} %[[#SX_SGE]] {{%[0-9]+}}
+define spir_kernel void @icmp_sge_i4_zero(i4 %x, ptr addrspace(1) %out) {
+  %c = icmp sge i4 %x, 0
+  %r = sext i1 %c to i8
+  store i8 %r, ptr addrspace(1) %out
+  ret void
+}
+
 ; ----------------------------------------------------------------------------
 ; icmp slt i4 between two registers: both operands must be sign-extended.
 ; CHECK: OpFunction
@@ -119,6 +164,49 @@ define spir_kernel void @icmp_slt_i24_zero(i24 %x, ptr addrspace(1) %out) {
   ret void
 }
 
+; ----------------------------------------------------------------------------
+; Same vreg on both operands of one instruction: only one sign-extension pair
+; is emitted and both operand slots reference it.
+; CHECK: OpFunction
+; CHECK: %[[#X_SAME:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL_SAME:]] = OpShiftLeftLogical %[[#I8]] %[[#X_SAME]] %[[#K4]]
+; CHECK: %[[#SX_SAME:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_SAME]] %[[#K4]]
+; CHECK-NOT: OpShiftRightArithmetic
+; CHECK: OpSDiv %[[#I8]] %[[#SX_SAME]] %[[#SX_SAME]]
+define spir_kernel void @sdiv_i4_same_operand(i4 %x, ptr addrspace(1) %out) {
+  %r = sdiv i4 %x, %x
+  %z = sext i4 %r to i32
+  store i32 %z, ptr addrspace(1) %out
+  ret void
+}
+
+; ----------------------------------------------------------------------------
+; Same vreg feeding two separate sign-sensitive instructions: each instruction
+; needs its own sign-extension since G_SEXT_INREG is not hoisted.
+; CHECK: OpFunction
+; CHECK: %[[#X_SHARED:]] = OpFunctionParameter
+; CHECK: %[[#Y_SHARED:]] = OpFunctionParameter
+; CHECK: OpFunctionParameter
+; CHECK: %[[#SHL_A:]] = OpShiftLeftLogical %[[#I8]] %[[#X_SHARED]] %[[#K4]]
+; CHECK: %[[#SXA:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_A]] %[[#K4]]
+; CHECK: %[[#SHL_B:]] = OpShiftLeftLogical %[[#I8]] %[[#Y_SHARED]] %[[#K4]]
+; CHECK: %[[#SXB:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_B]] %[[#K4]]
+; CHECK: OpSDiv %[[#I8]] %[[#SXA]] %[[#SXB]]
+; CHECK: %[[#SHL_A2:]] = OpShiftLeftLogical %[[#I8]] %[[#X_SHARED]] %[[#K4]]
+; CHECK: %[[#SXA2:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_A2]] %[[#K4]]
+; CHECK: %[[#SHL_B2:]] = OpShiftLeftLogical %[[#I8]] %[[#Y_SHARED]] %[[#K4]]
+; CHECK: %[[#SXB2:]] = OpShiftRightArithmetic %[[#I8]] %[[#SHL_B2]] %[[#K4]]
+; CHECK: OpSRem %[[#I8]] %[[#SXA2]] %[[#SXB2]]
+define spir_kernel void @sdiv_and_srem_i4_shared(i4 %x, i4 %y, ptr addrspace(1) %out) {
+  %q = sdiv i4 %x, %y
+  %r = srem i4 %x, %y
+  %s = add i4 %q, %r
+  %z = sext i4 %s to i32
+  store i32 %z, ptr addrspace(1) %out
+  ret void
+}
+
 ; ----------------------------------------------------------------------------
 ; Negative test: unsigned compare must NOT emit sign-extension shifts.
 ; CHECK: OpFunction

>From 58bcc59ff295344de5015c5226d959d4d7a0a728 Mon Sep 17 00:00:00 2001
From: Faijul Amin <md.faijul.amin at intel.com>
Date: Sat, 11 Jul 2026 11:24:05 -0700
Subject: [PATCH 3/3] [SPIRV] NFC: fix clang-format and reword G_SEXT_INREG
 lowering comment

---
 llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp | 4 ++--
 llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
index d63f67a052acd..457b731e658b8 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
@@ -356,8 +356,8 @@ SPIRVLegalizerInfo::SPIRVLegalizerInfo(const SPIRVSubtarget &ST) {
       .legalForCartesianProduct(allScalarsAndVectors)
       .legalIf(extendedScalarsAndVectorsProduct);
 
-  // SPIR-V has no native sign-extend-in-register opcode; lower it to the
-  // canonical (x << k) ashr k pair, which the SPIR-V dialect supports.
+  // Lower G_SEXT_INREG to the canonical shl/ashr pair, which map to
+  // OpShiftLeftLogical + OpShiftRightArithmetic.
   getActionDefinitionsBuilder(G_SEXT_INREG).lower();
 
   getActionDefinitionsBuilder(G_PHI)
diff --git a/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp b/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
index 735e9c23cf861..0463a06886936 100644
--- a/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVPreLegalizer.cpp
@@ -497,7 +497,7 @@ static bool isSignSensitiveOp(const MachineInstr &MI) {
 // many low bits to sign-extend.
 static DenseMap<Register, unsigned>
 recordSignSensitiveOperandWidths(MachineFunction &MF,
-                                   MachineRegisterInfo &MRI) {
+                                 MachineRegisterInfo &MRI) {
   DenseMap<Register, unsigned> OrigWidth;
   for (MachineBasicBlock &MBB : MF) {
     for (MachineInstr &MI : MBB) {



More information about the llvm-commits mailing list