[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