[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
Fri Jun 12 19:49:45 PDT 2026


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

## Problem

`SPIRVPreLegalizer` widens sub-pow2 scalars (`s4`, `s24`, ...) to the next legal width by relabeling the LLT only — no sign-extension is inserted. Sign-sensitive ops (signed `G_ICMP`, `G_ASHR`, `G_SDIV`, `G_SREM`) then read the sign bit at the wrong position. As a result `icmp slt i4 %x, 0` always returned `false` even for negative `i4`.

## Fix

- `SPIRVLegalizerInfo.cpp`: declare `G_SEXT_INREG.lower()` so the legalizer expands it to `(x << k) ashr k`.
- `SPIRVPreLegalizer.cpp`: before the widening loop, emit `G_SEXT_INREG` on each value operand of a sign-sensitive MI whose original width is sub-pow2; sign-extend immediate `CImm` operands instead of zero-extending.

A pass-wide `OrigWidth` map preserves the pre-widen width across re-visits; a per-MI cache shares one sext between operands of the same MI that reference the same vreg.

## Test

`llvm/test/CodeGen/SPIRV/legalization/signed-narrow-int.ll` covers `icmp slt`/`ashr`/`sdiv`/`srem` on `i4`, `icmp slt` on `i24`, and negative cases (`icmp ult`, `lshr`) that must not emit sign-extension shifts

>From 65b557d49e1024271b5f058a38444d6797611177 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] [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..594e587614395 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
+}



More information about the llvm-commits mailing list