[Mlir-commits] [mlir] [mlir][SPIR-V] Add OpenCL ctz, ilogb, and sincos operations (PR #231713)

Alexander Shaposhnikov llvmlistbot at llvm.org
Tue Oct 13 22:18:46 PDT 2026


https://github.com/alexander-shaposhnikov updated https://github.com/llvm/llvm-project/pull/231713

>From e71296ec3cb9971e4c1b1522572f8b1f415eefd0 Mon Sep 17 00:00:00 2001
From: Alexander Shaposhnikov <alexander.v.shaposhnikov at gmail.com>
Date: Tue, 13 Oct 2026 17:26:33 -0700
Subject: [PATCH 1/2] [mlir][SPIR-V] Add OpenCL ctz, ilogb, and sincos
 operations

Add spirv.CL.ctz, spirv.CL.ilogb, and spirv.CL.sincos with type and shape
verification and generated serialization support. Verify that sincos
writes through a pointer in an OpenCL-supported storage class.

Lower math.cttz to OpenCL.std ctz on Kernel targets. When narrow integers
are emulated using wider types, set a sentinel bit above the source width
to preserve the trailing-zero count, including the zero-input case.
Keep the existing GLSL lowering for Shader targets.

Add scalar and vector parsing, verification, conversion, and round-trip
tests, including native and emulated integer widths and invalid sincos
output pointers.
---
 .../mlir/Dialect/SPIRV/IR/SPIRVCLOps.td       | 111 ++++++++
 .../Conversion/MathToSPIRV/MathToSPIRV.cpp    |  46 +++-
 mlir/lib/Dialect/SPIRV/IR/SPIRVOps.cpp        |  16 ++
 .../MathToSPIRV/math-to-opencl-spirv.mlir     |  44 ++++
 mlir/test/Dialect/SPIRV/IR/ocl-ops.mlir       | 242 ++++++++++++++++++
 mlir/test/Target/SPIRV/ocl-ops.mlir           |  28 ++
 6 files changed, 485 insertions(+), 2 deletions(-)

diff --git a/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVCLOps.td b/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVCLOps.td
index 7a25c42d03e77..4b13c8d2d17d4 100644
--- a/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVCLOps.td
+++ b/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVCLOps.td
@@ -724,6 +724,44 @@ def SPIRV_CLFmaOp : SPIRV_CLTernaryArithmeticOp<"fma", 26, SPIRV_Float, [AlwaysS
 
 // -----
 
+def SPIRV_CLIlogbOp : SPIRV_CLOp<"ilogb", 33, [Pure, SameOperandsAndResultShape]> {
+  let summary = "Return the exponent as an integer value.";
+
+  let description = [{
+    Result is the unbiased binary exponent of x, as a signed integer value.
+
+    The operand x must be a scalar or vector whose component type is
+    floating-point.
+
+    Result Type must be a scalar or vector whose component type is a 32-bit
+    integer. The number of components in x and Result Type must be the same.
+    Results are computed per component.
+
+    #### Example:
+
+    ```mlir
+    %2 = spirv.CL.ilogb %0 : f32 -> i32
+    %3 = spirv.CL.ilogb %1 : vector<3xf32> -> vector<3xi32>
+    ```
+  }];
+
+  let arguments = (ins
+    SPIRV_ScalarOrVectorOf<SPIRV_Float>:$x
+  );
+
+  let results = (outs
+    SPIRV_ScalarOrVectorOf<AnyI32>:$result
+  );
+
+  let assemblyFormat = [{
+    attr-dict $x `:` type($x) `->` type($result)
+  }];
+
+  let hasVerifier = 0;
+}
+
+// -----
+
 def SPIRV_CLLdexpOp : SPIRV_CLOp<"ldexp", 34, [Pure, AllTypesMatch<["x", "y"]>]> {
   let summary = "Builds y such that y = significand * 2^exponent.";
 
@@ -1138,6 +1176,31 @@ def SPIRV_CLClzOp : SPIRV_CLUnaryArithmeticOp<"clz", 151, SPIRV_Integer> {
 
 // -----
 
+def SPIRV_CLCtzOp : SPIRV_CLUnaryArithmeticOp<"ctz", 152, SPIRV_Integer> {
+  let summary = "Count trailing zeros in operand";
+
+  let description = [{
+    Returns the number of least-significant 0-bits in x. If x is 0, the
+    result is the bit width of x, or of the component type of x if x is a
+    vector.
+
+    Result Type and x must be integer or vector(2,3,4,8,16) of
+    integer values.
+
+    All of the operands, including the Result Type operand, must be of the
+    same type.
+
+    #### Example:
+
+    ```mlir
+    %2 = spirv.CL.ctz %0 : i32
+    %3 = spirv.CL.ctz %1 : vector<3xi16>
+    ```
+  }];
+}
+
+// -----
+
 def SPIRV_CLSMaxOp : SPIRV_CLBinaryArithmeticOp<"s_max", 156, SPIRV_Integer> {
   let summary = "Return maximum of two signed integer operands";
 
@@ -1201,6 +1264,54 @@ def SPIRV_CLSinOp : SPIRV_CLUnaryArithmeticOp<"sin", 57, SPIRV_Float> {
 
 // -----
 
+def SPIRV_CLSincosOp : SPIRV_CLOp<"sincos", 58, [
+    AllTypesMatch<["x", "result"]>,
+    TypesMatchWith<"`x` type matches pointee type of `cosval`",
+                   "cosval", "x",
+                   "::llvm::cast<::mlir::spirv::PointerType>($_self)"
+                     ".getPointeeType()">]> {
+  let summary = "Compute sine and cosine of x radians.";
+
+  let description = [{
+    The computed sine is the return value and the computed cosine is written
+    to the memory that cosval points to.
+
+    The operand x must be a scalar or vector whose component type is
+    floating-point.
+
+    The cosval operand must be a pointer to a value of the same type as x,
+    in the CrossWorkgroup, Workgroup, Function, or Generic storage class.
+
+    Result Type must be the same type as the type of x. Results are computed
+    per component.
+
+    #### Example:
+
+    ```mlir
+    %sin = spirv.CL.sincos %x, %cosval : f32, !spirv.ptr<f32, Function> -> f32
+    %sin = spirv.CL.sincos %x, %cosval : vector<3xf32>,
+             !spirv.ptr<vector<3xf32>, Function> -> vector<3xf32>
+    ```
+  }];
+
+  let arguments = (ins
+    SPIRV_ScalarOrVectorOf<SPIRV_Float>:$x,
+    SPIRV_AnyPtr:$cosval
+  );
+
+  let results = (outs
+    SPIRV_ScalarOrVectorOf<SPIRV_Float>:$result
+  );
+
+  let assemblyFormat = [{
+    attr-dict $x `,` $cosval `:` type($x) `,` type($cosval) `->` type($result)
+  }];
+
+  let hasVerifier = 1;
+}
+
+// -----
+
 def SPIRV_CLSinhOp : SPIRV_CLUnaryArithmeticOp<"sinh", 59, SPIRV_Float > {
   let summary = "Compute hyperbolic sine of x radians.";
 
diff --git a/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp b/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp
index 094caf7fcc2b1..e74a9526e5d6e 100644
--- a/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp
+++ b/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp
@@ -251,6 +251,8 @@ struct CountLeadingZerosPattern final
 /// Converts math.cttz to GL FindILsb. GL FindILsb returns -1 for a zero
 /// input while math.cttz must return the bitwidth, so the zero case is
 /// patched up with a select.
+///
+/// OpenCL targets use OpenCL.std ctz. This pattern handles the shader fallback.
 struct CountTrailingZerosPattern final
     : public OpConversionPattern<math::CountTrailingZerosOp> {
   using Base::Base;
@@ -292,6 +294,47 @@ struct CountTrailingZerosPattern final
   }
 };
 
+/// Converts math.cttz to OpenCL.std ctz, preserving the source bit width when
+/// narrow integers are emulated using wider integers.
+struct CountTrailingZerosOpenCLPattern final
+    : public OpConversionPattern<math::CountTrailingZerosOp> {
+  using Base::Base;
+
+  LogicalResult
+  matchAndRewrite(math::CountTrailingZerosOp op, OpAdaptor adaptor,
+                  ConversionPatternRewriter &rewriter) const override {
+    if (failed(checkSourceOpTypes(rewriter, op)))
+      return failure();
+
+    auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>();
+    if (!typeConverter.getTargetEnv().allows(spirv::Capability::Kernel))
+      return rewriter.notifyMatchFailure(op, "requires Kernel capability");
+    Type type = typeConverter.convertType(op.getType());
+    if (!type)
+      return failure();
+
+    Type elementType = getElementTypeOrSelf(type);
+    unsigned sourceWidth =
+        getElementTypeOrSelf(op.getType()).getIntOrFloatBitWidth();
+    unsigned targetWidth = elementType.getIntOrFloatBitWidth();
+    Value input = adaptor.getOperand();
+    if (sourceWidth < targetWidth) {
+      // Set the bit immediately above the original value. This bounds the
+      // count by sourceWidth, even when the emulated value has nonzero high
+      // bits, and gives the correct result for a zero source value.
+      Attribute sentinel = IntegerAttr::get(
+          elementType, APInt::getOneBitSet(targetWidth, sourceWidth));
+      if (auto vectorType = dyn_cast<VectorType>(type))
+        sentinel = SplatElementsAttr::get(vectorType, sentinel);
+      Value mask =
+          spirv::ConstantOp::create(rewriter, op.getLoc(), type, sentinel);
+      input = spirv::BitwiseOrOp::create(rewriter, op.getLoc(), input, mask);
+    }
+    rewriter.replaceOpWithNewOp<spirv::CLCtzOp>(op, type, input);
+    return success();
+  }
+};
+
 /// Converts math.expm1 to SPIR-V ops.
 ///
 /// SPIR-V does not have a direct operations for exp(x)-1. Explicitly lower to
@@ -629,7 +672,6 @@ struct RoundOpPattern final : public OpConversionPattern<math::RoundOp> {
     return success();
   }
 };
-
 } // namespace
 
 //===----------------------------------------------------------------------===//
@@ -691,6 +733,7 @@ void populateMathToSPIRVPatterns(const SPIRVTypeConverter &typeConverter,
       CheckedElementwiseOpPattern<math::AbsFOp, spirv::CLFAbsOp>,
       CheckedElementwiseOpPattern<math::AbsIOp, spirv::CLSAbsOp>,
       CheckedElementwiseOpPattern<math::CountLeadingZerosOp, spirv::CLClzOp>,
+      CountTrailingZerosOpenCLPattern,
       CheckedElementwiseOpPattern<math::AtanOp, spirv::CLAtanOp>,
       CheckedElementwiseOpPattern<math::Atan2Op, spirv::CLAtan2Op>,
       CheckedElementwiseOpPattern<math::CbrtOp, spirv::CLCbrtOp>,
@@ -724,5 +767,4 @@ void populateMathToSPIRVPatterns(const SPIRVTypeConverter &typeConverter,
       CheckedElementwiseOpPattern<math::AtanhOp, spirv::CLAtanhOp>>(
       typeConverter, patterns.getContext());
 }
-
 } // namespace mlir
diff --git a/mlir/lib/Dialect/SPIRV/IR/SPIRVOps.cpp b/mlir/lib/Dialect/SPIRV/IR/SPIRVOps.cpp
index 37ae6a942e491..5204e36997109 100644
--- a/mlir/lib/Dialect/SPIRV/IR/SPIRVOps.cpp
+++ b/mlir/lib/Dialect/SPIRV/IR/SPIRVOps.cpp
@@ -2291,6 +2291,22 @@ LogicalResult spirv::CLRootnOp::verify() {
                                    getN().getType());
 }
 
+//===----------------------------------------------------------------------===//
+// spirv.CL.sincos
+//===----------------------------------------------------------------------===//
+
+LogicalResult spirv::CLSincosOp::verify() {
+  auto storageClass =
+      cast<spirv::PointerType>(getCosval().getType()).getStorageClass();
+  if (storageClass != spirv::StorageClass::CrossWorkgroup &&
+      storageClass != spirv::StorageClass::Workgroup &&
+      storageClass != spirv::StorageClass::Function &&
+      storageClass != spirv::StorageClass::Generic)
+    return emitOpError("cosval must point to CrossWorkgroup, Workgroup, "
+                       "Function, or Generic storage");
+  return success();
+}
+
 //===----------------------------------------------------------------------===//
 // spirv.ShiftLeftLogicalOp
 //===----------------------------------------------------------------------===//
diff --git a/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir b/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir
index faa509851a2e2..66b0068fd8296 100644
--- a/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir
+++ b/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir
@@ -183,6 +183,8 @@ func.func @int_unary(%arg0: i32) {
   %0 = math.absi %arg0 : i32
   // CHECK: spirv.CL.clz %{{.*}} : i32
   %1 = math.ctlz %arg0 : i32
+  // CHECK: spirv.CL.ctz %{{.*}} : i32
+  %2 = math.cttz %arg0 : i32
   return
 }
 
@@ -190,6 +192,8 @@ func.func @int_unary(%arg0: i32) {
 func.func @int_unary_vector(%arg0: vector<2xi32>) {
   // CHECK: spirv.CL.clz %{{.*}} : vector<2xi32>
   %0 = math.ctlz %arg0 : vector<2xi32>
+  // CHECK: spirv.CL.ctz %{{.*}} : vector<2xi32>
+  %1 = math.cttz %arg0 : vector<2xi32>
   return
 }
 
@@ -268,3 +272,43 @@ func.func @sincos_vector(%arg0: vector<3xf32>) {
 }
 
 } // end module
+
+// -----
+
+// Emulating narrow types must preserve cttz(0) == source bit width.
+module attributes { spirv.target_env = #spirv.target_env<#spirv.vce<v1.0, [Kernel], []>, #spirv.resource_limits<>> } {
+  // CHECK-LABEL: @cttz_emulated_i8
+  func.func @cttz_emulated_i8(%arg0: i8) -> i8 {
+    // CHECK: [[INPUT:%.*]] = builtin.unrealized_conversion_cast %arg0 : i8 to i32
+    // CHECK: [[SENTINEL:%.*]] = spirv.Constant 256 : i32
+    // CHECK: [[BOUNDED:%.*]] = spirv.BitwiseOr [[INPUT]], [[SENTINEL]] : i32
+    // CHECK: spirv.CL.ctz [[BOUNDED]] : i32
+    %0 = math.cttz %arg0 : i8
+    return %0 : i8
+  }
+  // CHECK-LABEL: @cttz_emulated_vector
+  func.func @cttz_emulated_vector(%arg0: vector<2xi16>) -> vector<2xi16> {
+    // CHECK: [[INPUT:%.*]] = builtin.unrealized_conversion_cast %arg0 : vector<2xi16> to vector<2xi32>
+    // CHECK: [[SENTINEL:%.*]] = spirv.Constant dense<65536> : vector<2xi32>
+    // CHECK: [[BOUNDED:%.*]] = spirv.BitwiseOr [[INPUT]], [[SENTINEL]] : vector<2xi32>
+    // CHECK: spirv.CL.ctz [[BOUNDED]] : vector<2xi32>
+    %0 = math.cttz %arg0 : vector<2xi16>
+    return %0 : vector<2xi16>
+  }
+}
+
+// -----
+
+module attributes { spirv.target_env = #spirv.target_env<#spirv.vce<v1.0, [Kernel, Int8, Int16, Int64], []>, #spirv.resource_limits<>> } {
+  // CHECK-LABEL: @cttz_native_widths
+  func.func @cttz_native_widths(%a: i8, %b: vector<2xi16>, %c: i64) {
+    // CHECK-NOT: spirv.BitwiseOr
+    // CHECK: spirv.CL.ctz %{{.*}} : i8
+    %0 = math.cttz %a : i8
+    // CHECK: spirv.CL.ctz %{{.*}} : vector<2xi16>
+    %1 = math.cttz %b : vector<2xi16>
+    // CHECK: spirv.CL.ctz %{{.*}} : i64
+    %2 = math.cttz %c : i64
+    return
+  }
+}
diff --git a/mlir/test/Dialect/SPIRV/IR/ocl-ops.mlir b/mlir/test/Dialect/SPIRV/IR/ocl-ops.mlir
index 468a710c13a91..82f8ea0fd62a2 100644
--- a/mlir/test/Dialect/SPIRV/IR/ocl-ops.mlir
+++ b/mlir/test/Dialect/SPIRV/IR/ocl-ops.mlir
@@ -418,6 +418,56 @@ func.func @clz(%arg0 : i32) -> () {
 
 // -----
 
+//===----------------------------------------------------------------------===//
+// spirv.CL.ctz
+//===----------------------------------------------------------------------===//
+
+func.func @ctz(%arg0 : i32) -> () {
+  // CHECK: spirv.CL.ctz {{%.*}} : i32
+  %2 = spirv.CL.ctz %arg0 : i32
+  return
+}
+
+func.func @ctzvec(%arg0 : vector<3xi16>) -> () {
+  // CHECK: spirv.CL.ctz {{%.*}} : vector<3xi16>
+  %2 = spirv.CL.ctz %arg0 : vector<3xi16>
+  return
+}
+
+// -----
+
+func.func @ctz(%arg0 : f32) -> () {
+  // expected-error @+1 {{op operand #0 must be 8/16/32/64-bit integer or fixed-length vector of 8/16/32/64-bit integer values}}
+  %2 = spirv.CL.ctz %arg0 : f32
+  return
+}
+
+// -----
+
+func.func @ctz(%arg0 : vector<5xi32>) -> () {
+  // expected-error @+1 {{op operand #0 must be 8/16/32/64-bit integer or fixed-length vector of 8/16/32/64-bit integer values of length 2/3/4}}
+  %2 = spirv.CL.ctz %arg0 : vector<5xi32>
+  return
+}
+
+// -----
+
+func.func @ctz(%arg0 : i32, %arg1 : i32) -> () {
+  // expected-error @+1 {{expected ':'}}
+  %2 = spirv.CL.ctz %arg0, %arg1 : i32
+  return
+}
+
+// -----
+
+func.func @ctz(%arg0 : i32) -> () {
+  // expected-error @+1 {{expected non-function type}}
+  %2 = spirv.CL.ctz %arg0 :
+  return
+}
+
+// -----
+
 //===----------------------------------------------------------------------===//
 // spirv.CL.fma
 //===----------------------------------------------------------------------===//
@@ -796,6 +846,90 @@ func.func @atan2(%arg0 : vector<4xf16>, %arg1 : vector<4xf16>) -> () {
 
 // -----
 
+//===----------------------------------------------------------------------===//
+// spirv.CL.ilogb
+//===----------------------------------------------------------------------===//
+
+func.func @ilogb(%arg0 : f32) -> () {
+  // CHECK: {{%.*}} = spirv.CL.ilogb {{%.*}} : f32 -> i32
+  %0 = spirv.CL.ilogb %arg0 : f32 -> i32
+  return
+}
+
+// -----
+
+func.func @ilogb_vec(%arg0 : vector<3xf16>) -> () {
+  // CHECK: {{%.*}} = spirv.CL.ilogb {{%.*}} : vector<3xf16> -> vector<3xi32>
+  %0 = spirv.CL.ilogb %arg0 : vector<3xf16> -> vector<3xi32>
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_operand_type(%arg0 : i32) -> () {
+  // expected-error @+1 {{op operand #0 must be 16/32/64-bit float or fixed-length vector of 16/32/64-bit float values}}
+  %0 = spirv.CL.ilogb %arg0 : i32 -> i32
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_result_type(%arg0 : f32) -> () {
+  // expected-error @+1 {{op result #0 must be 32-bit integer or fixed-length vector of 32-bit integer values}}
+  %0 = spirv.CL.ilogb %arg0 : f32 -> f32
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_result_width(%arg0 : f64) -> () {
+  // expected-error @+1 {{op result #0 must be 32-bit integer or fixed-length vector of 32-bit integer values}}
+  %0 = spirv.CL.ilogb %arg0 : f64 -> i64
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_result_width_vec(%arg0 : vector<3xf32>) -> () {
+  // expected-error @+1 {{op result #0 must be 32-bit integer or fixed-length vector of 32-bit integer values}}
+  %0 = spirv.CL.ilogb %arg0 : vector<3xf32> -> vector<3xi16>
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_shape_scalar(%arg0 : f32) -> () {
+  // expected-error @+1 {{op requires the same shape for all operands and results}}
+  %0 = spirv.CL.ilogb %arg0 : f32 -> vector<2xi32>
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_shape_vec_1(%arg0 : vector<3xf32>) -> () {
+  // expected-error @+1 {{op requires the same shape for all operands and results}}
+  %0 = spirv.CL.ilogb %arg0 : vector<3xf32> -> i32
+  return
+}
+
+// -----
+
+func.func @ilogb_wrong_shape_vec_2(%arg0 : vector<3xf32>) -> () {
+  // expected-error @+1 {{op requires the same shape for all operands and results}}
+  %0 = spirv.CL.ilogb %arg0 : vector<3xf32> -> vector<2xi32>
+  return
+}
+
+// -----
+
+func.func @ilogb_missing_result_type(%arg0 : f32) -> () {
+  // expected-error @+1 {{expected '->'}}
+  %0 = spirv.CL.ilogb %arg0 : f32
+  return
+}
+
+// -----
+
 //===----------------------------------------------------------------------===//
 // spirv.CL.ldexp
 //===----------------------------------------------------------------------===//
@@ -912,6 +1046,74 @@ func.func @rootn_wrong_type_vec(%arg0 : vector<3xf32>, %arg1 : vector<2xi32>) ->
 
 // -----
 
+//===----------------------------------------------------------------------===//
+// spirv.CL.sincos
+//===----------------------------------------------------------------------===//
+
+func.func @sincos(%arg0 : f32, %arg1 : !spirv.ptr<f32, Function>) -> () {
+  // CHECK: {{%.*}} = spirv.CL.sincos {{%.*}}, {{%.*}} : f32, !spirv.ptr<f32, Function> -> f32
+  %0 = spirv.CL.sincos %arg0, %arg1 : f32, !spirv.ptr<f32, Function> -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_vec(%arg0 : vector<3xf16>, %arg1 : !spirv.ptr<vector<3xf16>, Function>) -> () {
+  // CHECK: {{%.*}} = spirv.CL.sincos {{%.*}}, {{%.*}} : vector<3xf16>, !spirv.ptr<vector<3xf16>, Function> -> vector<3xf16>
+  %0 = spirv.CL.sincos %arg0, %arg1 : vector<3xf16>, !spirv.ptr<vector<3xf16>, Function> -> vector<3xf16>
+  return
+}
+
+// -----
+
+func.func @sincos_wrong_operand_type(%arg0 : i32, %arg1 : !spirv.ptr<i32, Function>) -> () {
+  // expected-error @+1 {{op operand #0 must be 16/32/64-bit float or fixed-length vector of 16/32/64-bit float values}}
+  %0 = spirv.CL.sincos %arg0, %arg1 : i32, !spirv.ptr<i32, Function> -> i32
+  return
+}
+
+// -----
+
+func.func @sincos_cosval_not_pointer(%arg0 : f32, %arg1 : f32) -> () {
+  // expected-error @+1 {{op operand #1 must be any SPIR-V pointer type, but got 'f32'}}
+  %0 = spirv.CL.sincos %arg0, %arg1 : f32, f32 -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_wrong_pointee_type(%arg0 : f32, %arg1 : !spirv.ptr<f64, Function>) -> () {
+  // expected-error @+1 {{op failed to verify that `x` type matches pointee type of `cosval`}}
+  %0 = spirv.CL.sincos %arg0, %arg1 : f32, !spirv.ptr<f64, Function> -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_wrong_pointee_shape(%arg0 : vector<3xf32>, %arg1 : !spirv.ptr<f32, Function>) -> () {
+  // expected-error @+1 {{op failed to verify that `x` type matches pointee type of `cosval`}}
+  %0 = spirv.CL.sincos %arg0, %arg1 : vector<3xf32>, !spirv.ptr<f32, Function> -> vector<3xf32>
+  return
+}
+
+// -----
+
+func.func @sincos_wrong_result_type(%arg0 : f32, %arg1 : !spirv.ptr<f32, Function>) -> () {
+  // expected-error @+1 {{op failed to verify that all of {x, result} have same type}}
+  %0 = spirv.CL.sincos %arg0, %arg1 : f32, !spirv.ptr<f32, Function> -> f64
+  return
+}
+
+// -----
+
+func.func @sincos_missing_cosval(%arg0 : f32) -> () {
+  // expected-error @+1 {{expected ','}}
+  %0 = spirv.CL.sincos %arg0 : f32 -> f32
+  return
+}
+
+// -----
+
 //===----------------------------------------------------------------------===//
 // spirv.CL.copysign
 //===----------------------------------------------------------------------===//
@@ -1029,3 +1231,43 @@ func.func @log1p(%arg0 : vector<4xf16>) -> () {
   %2 = spirv.CL.log1p %arg0 : vector<4xf16>
   return
 }
+
+// -----
+
+func.func @sincos_CrossWorkgroup(%x: f32, %p: !spirv.ptr<f32, CrossWorkgroup>) {
+  // CHECK: spirv.CL.sincos {{%.*}}, {{%.*}} : f32, !spirv.ptr<f32, CrossWorkgroup> -> f32
+  %0 = spirv.CL.sincos %x, %p : f32, !spirv.ptr<f32, CrossWorkgroup> -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_Workgroup(%x: f32, %p: !spirv.ptr<f32, Workgroup>) {
+  // CHECK: spirv.CL.sincos {{%.*}}, {{%.*}} : f32, !spirv.ptr<f32, Workgroup> -> f32
+  %0 = spirv.CL.sincos %x, %p : f32, !spirv.ptr<f32, Workgroup> -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_Generic(%x: f32, %p: !spirv.ptr<f32, Generic>) {
+  // CHECK: spirv.CL.sincos {{%.*}}, {{%.*}} : f32, !spirv.ptr<f32, Generic> -> f32
+  %0 = spirv.CL.sincos %x, %p : f32, !spirv.ptr<f32, Generic> -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_invalid_UniformConstant(%x: f32, %p: !spirv.ptr<f32, UniformConstant>) {
+  // expected-error @+1 {{cosval must point to CrossWorkgroup, Workgroup, Function, or Generic storage}}
+  %0 = spirv.CL.sincos %x, %p : f32, !spirv.ptr<f32, UniformConstant> -> f32
+  return
+}
+
+// -----
+
+func.func @sincos_invalid_Private(%x: f32, %p: !spirv.ptr<f32, Private>) {
+  // expected-error @+1 {{cosval must point to CrossWorkgroup, Workgroup, Function, or Generic storage}}
+  %0 = spirv.CL.sincos %x, %p : f32, !spirv.ptr<f32, Private> -> f32
+  return
+}
diff --git a/mlir/test/Target/SPIRV/ocl-ops.mlir b/mlir/test/Target/SPIRV/ocl-ops.mlir
index b94c8ab1e8ee0..33d0da6421200 100644
--- a/mlir/test/Target/SPIRV/ocl-ops.mlir
+++ b/mlir/test/Target/SPIRV/ocl-ops.mlir
@@ -63,6 +63,14 @@ spirv.module Physical64 OpenCL requires #spirv.vce<v1.0, [Kernel, Addresses, Vec
     %0 = spirv.CL.s_abs %arg0 : i32
     // CHECK: {{%.*}} = spirv.CL.clz {{%.*}} : i32
     %1 = spirv.CL.clz %arg0 : i32
+    // CHECK: {{%.*}} = spirv.CL.ctz {{%.*}} : i32
+    %2 = spirv.CL.ctz %arg0 : i32
+    spirv.Return
+  }
+
+  spirv.func @integer_vec_insts(%arg0 : vector<3xi32>) "None" {
+    // CHECK: {{%.*}} = spirv.CL.ctz {{%.*}} : vector<3xi32>
+    %0 = spirv.CL.ctz %arg0 : vector<3xi32>
     spirv.Return
   }
 
@@ -98,6 +106,26 @@ spirv.module Physical64 OpenCL requires #spirv.vce<v1.0, [Kernel, Addresses, Vec
     spirv.Return
   }
 
+  spirv.func @ilogb(%arg0 : f32, %arg1 : vector<3xf32>) "None" {
+    // CHECK: {{%.*}} = spirv.CL.ilogb {{%.*}} : f32 -> i32
+    %0 = spirv.CL.ilogb %arg0 : f32 -> i32
+    // CHECK: {{%.*}} = spirv.CL.ilogb {{%.*}} : vector<3xf32> -> vector<3xi32>
+    %1 = spirv.CL.ilogb %arg1 : vector<3xf32> -> vector<3xi32>
+    spirv.Return
+  }
+
+  spirv.func @sincos(%arg0 : f32, %arg1 : vector<3xf32>) "None" {
+    // CHECK: [[COS:%.*]] = spirv.Variable : !spirv.ptr<f32, Function>
+    // CHECK: [[COSVEC:%.*]] = spirv.Variable : !spirv.ptr<vector<3xf32>, Function>
+    %cos = spirv.Variable : !spirv.ptr<f32, Function>
+    %cosvec = spirv.Variable : !spirv.ptr<vector<3xf32>, Function>
+    // CHECK: {{%.*}} = spirv.CL.sincos {{%.*}}, [[COS]] : f32, !spirv.ptr<f32, Function> -> f32
+    %0 = spirv.CL.sincos %arg0, %cos : f32, !spirv.ptr<f32, Function> -> f32
+    // CHECK: {{%.*}} = spirv.CL.sincos {{%.*}}, [[COSVEC]] : vector<3xf32>, !spirv.ptr<vector<3xf32>, Function> -> vector<3xf32>
+    %1 = spirv.CL.sincos %arg1, %cosvec : vector<3xf32>, !spirv.ptr<vector<3xf32>, Function> -> vector<3xf32>
+    spirv.Return
+  }
+
   spirv.func @maxmin(%arg0 : f32, %arg1 : f32, %arg2 : i32, %arg3 : i32) "None" {
     // CHECK: {{%.*}} = spirv.CL.fmax {{%.*}}, {{%.*}} : f32
     %1 = spirv.CL.fmax %arg0, %arg1 : f32

>From a8b5cd9a84e7b32ce00371c2258db3a6e598c4ec Mon Sep 17 00:00:00 2001
From: Alexander Shaposhnikov <alexander.v.shaposhnikov at gmail.com>
Date: Tue, 13 Oct 2026 22:18:33 -0700
Subject: [PATCH 2/2] Address comments

---
 .../Conversion/MathToSPIRV/MathToSPIRV.cpp    | 15 ++++++--
 .../MathToSPIRV/math-to-opencl-spirv.mlir     | 37 ++++++++++++++++++-
 2 files changed, 48 insertions(+), 4 deletions(-)

diff --git a/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp b/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp
index e74a9526e5d6e..147d3c328f786 100644
--- a/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp
+++ b/mlir/lib/Conversion/MathToSPIRV/MathToSPIRV.cpp
@@ -313,11 +313,20 @@ struct CountTrailingZerosOpenCLPattern final
     if (!type)
       return failure();
 
+    Value input = adaptor.getOperand();
     Type elementType = getElementTypeOrSelf(type);
-    unsigned sourceWidth =
-        getElementTypeOrSelf(op.getType()).getIntOrFloatBitWidth();
+    if (elementType.isInteger(1)) {
+      // cttz(false) is true and cttz(true) is false.
+      rewriter.replaceOpWithNewOp<spirv::LogicalNotOp>(op, type, input);
+      return success();
+    }
+
+    Type sourceElementType = getElementTypeOrSelf(op.getType());
     unsigned targetWidth = elementType.getIntOrFloatBitWidth();
-    Value input = adaptor.getOperand();
+    // Index values use the bit width chosen by the type converter.
+    unsigned sourceWidth = sourceElementType.isIndex()
+                               ? targetWidth
+                               : sourceElementType.getIntOrFloatBitWidth();
     if (sourceWidth < targetWidth) {
       // Set the bit immediately above the original value. This bounds the
       // count by sourceWidth, even when the emulated value has nonzero high
diff --git a/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir b/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir
index 66b0068fd8296..22248987fe0ee 100644
--- a/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir
+++ b/mlir/test/Conversion/MathToSPIRV/math-to-opencl-spirv.mlir
@@ -197,6 +197,42 @@ func.func @int_unary_vector(%arg0: vector<2xi32>) {
   return
 }
 
+// CHECK-LABEL: @cttz_i1
+func.func @cttz_i1(%arg0: i1) -> i1 {
+  // CHECK: [[RESULT:%.*]] = spirv.LogicalNot %arg0 : i1
+  // CHECK: return [[RESULT]] : i1
+  %0 = math.cttz %arg0 : i1
+  return %0 : i1
+}
+
+// CHECK-LABEL: @cttz_i1_vector
+func.func @cttz_i1_vector(%arg0: vector<2xi1>) -> vector<2xi1> {
+  // CHECK: [[RESULT:%.*]] = spirv.LogicalNot %arg0 : vector<2xi1>
+  // CHECK: return [[RESULT]] : vector<2xi1>
+  %0 = math.cttz %arg0 : vector<2xi1>
+  return %0 : vector<2xi1>
+}
+
+// CHECK-LABEL: @cttz_index
+func.func @cttz_index(%arg0: index) -> index {
+  // CHECK: [[INPUT:%.*]] = builtin.unrealized_conversion_cast %arg0 : index to i32
+  // CHECK: [[RESULT:%.*]] = spirv.CL.ctz [[INPUT]] : i32
+  // CHECK: [[CAST:%.*]] = builtin.unrealized_conversion_cast [[RESULT]] : i32 to index
+  // CHECK: return [[CAST]] : index
+  %0 = math.cttz %arg0 : index
+  return %0 : index
+}
+
+// CHECK-LABEL: @cttz_index_vector
+func.func @cttz_index_vector(%arg0: vector<2xindex>) -> vector<2xindex> {
+  // CHECK: [[INPUT:%.*]] = builtin.unrealized_conversion_cast %arg0 : vector<2xindex> to vector<2xi32>
+  // CHECK: [[RESULT:%.*]] = spirv.CL.ctz [[INPUT]] : vector<2xi32>
+  // CHECK: [[CAST:%.*]] = builtin.unrealized_conversion_cast [[RESULT]] : vector<2xi32> to vector<2xindex>
+  // CHECK: return [[CAST]] : vector<2xindex>
+  %0 = math.cttz %arg0 : vector<2xindex>
+  return %0 : vector<2xindex>
+}
+
 } // end module
 
 // -----
@@ -302,7 +338,6 @@ module attributes { spirv.target_env = #spirv.target_env<#spirv.vce<v1.0, [Kerne
 module attributes { spirv.target_env = #spirv.target_env<#spirv.vce<v1.0, [Kernel, Int8, Int16, Int64], []>, #spirv.resource_limits<>> } {
   // CHECK-LABEL: @cttz_native_widths
   func.func @cttz_native_widths(%a: i8, %b: vector<2xi16>, %c: i64) {
-    // CHECK-NOT: spirv.BitwiseOr
     // CHECK: spirv.CL.ctz %{{.*}} : i8
     %0 = math.cttz %a : i8
     // CHECK: spirv.CL.ctz %{{.*}} : vector<2xi16>



More information about the Mlir-commits mailing list