[llvm] [SPIRV] Legalize canonicalize, sincospi and the minimumnum/maximumnum pair (PR #218641)
Paulius Velesko via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 25 02:16:23 PDT 2026
https://github.com/pvelesko created https://github.com/llvm/llvm-project/pull/218641
Four target independent intrinsics have no path to SPIR-V today.
`llvm.canonicalize`, `llvm.minimumnum` and `llvm.maximumnum` stop in the legalizer:
```
LLVM ERROR: unable to legalize instruction: %4:id(s16) = G_FCANONICALIZE %0:fid (in function: test_canonicalize_half)
LLVM ERROR: unable to legalize instruction: %5:id(s32) = G_FMAXIMUMNUM %0:fid, %1:fid (in function: test_maximumnum_float)
LLVM ERROR: unable to legalize instruction: %5:id(s32) = G_FMINIMUMNUM %0:fid, %1:fid (in function: test_minimumnum_float)
```
`llvm.sincospi` stops in the instruction selector:
```
error: <unknown>:0:0: in function test_sincospi_scalar float (float): intrinsic selection not implemented.
LLVM ERROR: cannot select: %4:fid(s64), %5:fid(s64) = G_INTRINSIC intrinsic(@llvm.sincospi), %0:fid(s64) (in function: test_sincospi_scalar)
```
All four are reachable from ordinary source. `__builtin_canonicalize` is a Clang builtin, `fmaximum_num` and `fminimum_num` are C23 library functions, and `llvm.sincospi` is formed by Clang from `__builtin_sincospi` and its float and long double variants (clang/lib/CodeGen/CGBuiltin.cpp), which is its only formation site in the tree. That path is trunk only: a released clang 23 still lowers `__builtin_sincospif` to a plain `sincospif` libcall. An offline compile to SPIR-V then aborts with no source location, which is what makes these worth closing rather than leaving to the frontend.
G_FCANONICALIZE becomes a multiplication by 1.0. LangRef says of `llvm.canonicalize`:
```
This function should always be implementable as multiplication by 1.0, provided
that the compiler does not constant fold the operation.
```
Nothing that could fold the multiply away runs after instruction selection, so `SPIRVInstructionSelector` emits `OpFMulS` or `OpFMulV` against a constant 1.0 directly. This is the same expansion `TargetLowering::expandFCANONICALIZE` already applies for SelectionDAG targets. On an implementation without subnormals it also gives the behaviour LangRef describes, where subnormals "are treated as non-canonical encodings of zero and will be flushed to a zero of the same sign by this operation".
G_FMINIMUMNUM and G_FMAXIMUMNUM use the generic expansion, which quiets signaling NaN operands with G_FCANONICALIZE and then emits G_FMINNUM or G_FMAXNUM. LangRef on `llvm.maximumnum` says that it "behaves the same as `llvm.maxnum` other than its treatment of sNaN inputs", so with the operands quieted the existing OpenCL.std `fmin` and `fmax` selection is exact. With `nnan` the quieting is skipped and a bare `fmax` is emitted.
`llvm.sincospi` becomes the OpenCL.std `sinpi` and `cospi` pair, following the shape of the existing `llvm.sincos` handling, including not emitting `cospi` when the cosine result is unused. `sinpi` and `cospi` avoid the argument reduction error that a literal multiplication by pi would introduce. GLSL.std.450 has neither, so the shader environment still rejects `llvm.sincospi`, now with a targeted diagnostic rather than the generic one.
Five lit tests are added under `llvm/test/CodeGen/SPIRV/llvm-intrinsics/`, each with a `%if spirv-tools %{ ... spirv-val %}` RUN line. They are committed first, reproducing the errors above, and pass after the second commit. Vectors of 3, 4, 8 and 16 lanes, `half`, `float`, `double` and `bfloat` were all checked by hand and validate with `spirv-val`.
Caveats. `llvm.maxnum` and `llvm.minnum` already select to OpenCL.std `fmax` and `fmin`, which leave `fmax(+0.0, -0.0)` unspecified while LangRef orders `-0.0` below `+0.0`. That gap is pre-existing and is the one LangRef points at with #174730; this patch makes `llvm.maximumnum` and `llvm.minimumnum` inherit it rather than introducing it, and a fix belongs with the `llvm.maxnum` selection. A dedicated `G_FSINCOSPI` opcode plus IRTranslator support would be tidier than matching the raw `G_INTRINSIC`, but that is a cross target change, so this keeps it local to the SPIR-V selector, as the backend already does for `llvm.returnaddress` and `llvm.frameaddress`. Everything was checked with `llc` and `spirv-val` only; no GPU execution was run.
Two points a reviewer is likely to raise, answered up front.
On the canonicalize expansion, `TargetLowering::expandFCANONICALIZE` deliberately uses `STRICT_FMUL`, commenting that it "uses strict_fp operations even outside a strict_fp context in order to guarantee that the canonicalization is not optimized away by later passes". The SPIR-V path needs no equivalent guard for a checkable reason: the emitted `OpFMul` carries no `FPFastMathMode` decoration even when the source intrinsic has `fast` or `reassoc`, so a conforming consumer must not fold it away. I verified that on the emitted modules rather than assuming it.
On routing `minimumnum` and `maximumnum` through `minnum` and `maxnum`, note that `LegalizerHelper::lowerFMinNumMaxNum` opens with a FIXME saying "fminnum/fmaxnum and fminimumnum/fmaximumnum should not have identical handling. fminimumnum/fmaximumnum also need a path that do not depend on fminnum/fmaxnum." The signed zero deviation excused in LangRef for `minnum` and `maxnum` via issue #174730 is NOT excused for `minimumnum` and `maximumnum`, whose Semantics state flatly that -0.0 is less than +0.0. I still think `.lower()` is the right first step, because it is the generic path the helper exists to provide and SPIRV would be its first user for these opcodes, whereas AMDGPU handles them natively. Happy to add a dedicated path instead if reviewers prefer.
>From 33f006bb4998eab5a8e2339ae3fd09aad45981ce Mon Sep 17 00:00:00 2001
From: Paulius Velesko <pvelesko at pglc.io>
Date: Tue, 25 Aug 2026 11:22:02 +0300
Subject: [PATCH 1/2] [SPIRV] Add tests for canonicalize, sincospi and
minimumnum/maximumnum
None of llvm.canonicalize, llvm.sincospi, llvm.minimumnum and
llvm.maximumnum reach a SPIR-V instruction today.
llvm.canonicalize, llvm.minimumnum and llvm.maximumnum stop in the
legalizer:
LLVM ERROR: unable to legalize instruction: %4:id(s16) = G_FCANONICALIZE %0:fid (in function: test_canonicalize_half)
LLVM ERROR: unable to legalize instruction: %5:id(s32) = G_FMAXIMUMNUM %0:fid, %1:fid (in function: test_maximumnum_float)
LLVM ERROR: unable to legalize instruction: %5:id(s32) = G_FMINIMUMNUM %0:fid, %1:fid (in function: test_minimumnum_float)
llvm.sincospi stops in the instruction selector:
error: <unknown>:0:0: in function test_sincospi_scalar float (float): intrinsic selection not implemented.
LLVM ERROR: cannot select: %4:fid(s64), %5:fid(s64) = G_INTRINSIC intrinsic(@llvm.sincospi), %0:fid(s64) (in function: test_sincospi_scalar)
These tests record the lowering each intrinsic should get. They fail
until the following commit adds it.
---
.../llvm-intrinsics/canonicalize-glsl.ll | 29 +++++++++++
.../SPIRV/llvm-intrinsics/canonicalize.ll | 52 +++++++++++++++++++
.../SPIRV/llvm-intrinsics/maximumnum.ll | 50 ++++++++++++++++++
.../SPIRV/llvm-intrinsics/minimumnum.ll | 38 ++++++++++++++
.../SPIRV/llvm-intrinsics/sincospi-opencl.ll | 50 ++++++++++++++++++
5 files changed, 219 insertions(+)
create mode 100644 llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize-glsl.ll
create mode 100644 llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize.ll
create mode 100644 llvm/test/CodeGen/SPIRV/llvm-intrinsics/maximumnum.ll
create mode 100644 llvm/test/CodeGen/SPIRV/llvm-intrinsics/minimumnum.ll
create mode 100644 llvm/test/CodeGen/SPIRV/llvm-intrinsics/sincospi-opencl.ll
diff --git a/llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize-glsl.ll b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize-glsl.ll
new file mode 100644
index 0000000000000..e4db48cdb715a
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize-glsl.ll
@@ -0,0 +1,29 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-unknown-vulkan1.3-compute %s -o - -filetype=obj | spirv-val %}
+
+;; The shader environment uses the same multiplication by 1.0 as the kernel one,
+;; because OpFMul is a core instruction and needs no extended instruction set.
+
+; CHECK-DAG: %[[#Float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#Float4:]] = OpTypeVector %[[#Float]] 4
+; CHECK-DAG: %[[#OneFloat:]] = OpConstant %[[#Float]] 1
+; CHECK-DAG: %[[#OneFloat4:]] = OpConstantComposite %[[#Float4]] %[[#OneFloat]] %[[#OneFloat]] %[[#OneFloat]] %[[#OneFloat]]
+
+; CHECK: %[[#XFloat:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#RFloat:]] = OpFMul %[[#Float]] %[[#XFloat]] %[[#OneFloat]]
+; CHECK: OpReturnValue %[[#RFloat]]
+define float @test_canonicalize_float(float %x) {
+ %r = call float @llvm.canonicalize.f32(float %x)
+ ret float %r
+}
+
+; CHECK: %[[#XFloat4:]] = OpFunctionParameter %[[#Float4]]
+; CHECK: %[[#RFloat4:]] = OpFMul %[[#Float4]] %[[#XFloat4]] %[[#OneFloat4]]
+; CHECK: OpReturnValue %[[#RFloat4]]
+define <4 x float> @test_canonicalize_v4float(<4 x float> %x) {
+ %r = call <4 x float> @llvm.canonicalize.v4f32(<4 x float> %x)
+ ret <4 x float> %r
+}
+
+declare float @llvm.canonicalize.f32(float)
+declare <4 x float> @llvm.canonicalize.v4f32(<4 x float>)
diff --git a/llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize.ll b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize.ll
new file mode 100644
index 0000000000000..fb30f7f633775
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/canonicalize.ll
@@ -0,0 +1,52 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+;; SPIR-V has no canonicalize instruction. LangRef says llvm.canonicalize
+;; "should always be implementable as multiplication by 1.0", so the backend
+;; emits an OpFMul against a constant 1.0.
+
+; CHECK-DAG: %[[#Half:]] = OpTypeFloat 16
+; CHECK-DAG: %[[#Float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#Double:]] = OpTypeFloat 64
+; CHECK-DAG: %[[#Float4:]] = OpTypeVector %[[#Float]] 4
+; CHECK-DAG: %[[#OneHalf:]] = OpConstant %[[#Half]] 15360
+; CHECK-DAG: %[[#OneFloat:]] = OpConstant %[[#Float]] 1
+; CHECK-DAG: %[[#OneDouble:]] = OpConstant %[[#Double]] 1
+; CHECK-DAG: %[[#OneFloat4:]] = OpConstantComposite %[[#Float4]] %[[#OneFloat]] %[[#OneFloat]] %[[#OneFloat]] %[[#OneFloat]]
+
+; CHECK: %[[#XHalf:]] = OpFunctionParameter %[[#Half]]
+; CHECK: %[[#RHalf:]] = OpFMul %[[#Half]] %[[#XHalf]] %[[#OneHalf]]
+; CHECK: OpReturnValue %[[#RHalf]]
+define spir_func half @test_canonicalize_half(half %x) {
+ %r = call half @llvm.canonicalize.f16(half %x)
+ ret half %r
+}
+
+; CHECK: %[[#XFloat:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#RFloat:]] = OpFMul %[[#Float]] %[[#XFloat]] %[[#OneFloat]]
+; CHECK: OpReturnValue %[[#RFloat]]
+define spir_func float @test_canonicalize_float(float %x) {
+ %r = call float @llvm.canonicalize.f32(float %x)
+ ret float %r
+}
+
+; CHECK: %[[#XDouble:]] = OpFunctionParameter %[[#Double]]
+; CHECK: %[[#RDouble:]] = OpFMul %[[#Double]] %[[#XDouble]] %[[#OneDouble]]
+; CHECK: OpReturnValue %[[#RDouble]]
+define spir_func double @test_canonicalize_double(double %x) {
+ %r = call double @llvm.canonicalize.f64(double %x)
+ ret double %r
+}
+
+; CHECK: %[[#XFloat4:]] = OpFunctionParameter %[[#Float4]]
+; CHECK: %[[#RFloat4:]] = OpFMul %[[#Float4]] %[[#XFloat4]] %[[#OneFloat4]]
+; CHECK: OpReturnValue %[[#RFloat4]]
+define spir_func <4 x float> @test_canonicalize_v4float(<4 x float> %x) {
+ %r = call <4 x float> @llvm.canonicalize.v4f32(<4 x float> %x)
+ ret <4 x float> %r
+}
+
+declare half @llvm.canonicalize.f16(half)
+declare float @llvm.canonicalize.f32(float)
+declare double @llvm.canonicalize.f64(double)
+declare <4 x float> @llvm.canonicalize.v4f32(<4 x float>)
diff --git a/llvm/test/CodeGen/SPIRV/llvm-intrinsics/maximumnum.ll b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/maximumnum.ll
new file mode 100644
index 0000000000000..68c79849d6cf9
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/maximumnum.ll
@@ -0,0 +1,50 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+;; llvm.maximumnum differs from llvm.maxnum only in its treatment of signaling
+;; NaNs, so it is expanded into a pair of quieting canonicalizations, each of
+;; which becomes a multiplication by 1.0, followed by the OpenCL.std fmax that
+;; llvm.maxnum already uses.
+
+; CHECK-DAG: %[[#ExtInstId:]] = OpExtInstImport "OpenCL.std"
+; CHECK-DAG: %[[#Float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#Float4:]] = OpTypeVector %[[#Float]] 4
+; CHECK-DAG: %[[#OneFloat:]] = OpConstant %[[#Float]] 1
+; CHECK-DAG: %[[#OneFloat4:]] = OpConstantComposite %[[#Float4]] %[[#OneFloat]] %[[#OneFloat]] %[[#OneFloat]] %[[#OneFloat]]
+
+; CHECK: %[[#X:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#Y:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#QX:]] = OpFMul %[[#Float]] %[[#X]] %[[#OneFloat]]
+; CHECK: %[[#QY:]] = OpFMul %[[#Float]] %[[#Y]] %[[#OneFloat]]
+; CHECK: %[[#Res:]] = OpExtInst %[[#Float]] %[[#ExtInstId]] fmax %[[#QX]] %[[#QY]]
+; CHECK: OpReturnValue %[[#Res]]
+define spir_func float @test_maximumnum_float(float %x, float %y) {
+ %r = call float @llvm.maximumnum.f32(float %x, float %y)
+ ret float %r
+}
+
+; CHECK: %[[#Xv:]] = OpFunctionParameter %[[#Float4]]
+; CHECK: %[[#Yv:]] = OpFunctionParameter %[[#Float4]]
+; CHECK: %[[#QXv:]] = OpFMul %[[#Float4]] %[[#Xv]] %[[#OneFloat4]]
+; CHECK: %[[#QYv:]] = OpFMul %[[#Float4]] %[[#Yv]] %[[#OneFloat4]]
+; CHECK: %[[#Resv:]] = OpExtInst %[[#Float4]] %[[#ExtInstId]] fmax %[[#QXv]] %[[#QYv]]
+; CHECK: OpReturnValue %[[#Resv]]
+define spir_func <4 x float> @test_maximumnum_v4float(<4 x float> %x, <4 x float> %y) {
+ %r = call <4 x float> @llvm.maximumnum.v4f32(<4 x float> %x, <4 x float> %y)
+ ret <4 x float> %r
+}
+
+;; With nnan there is no signaling NaN to quiet, so no canonicalization is
+;; needed and fmax is emitted on the operands directly.
+; CHECK: %[[#Xn:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#Yn:]] = OpFunctionParameter %[[#Float]]
+; CHECK-NOT: OpFMul
+; CHECK: %[[#Resn:]] = OpExtInst %[[#Float]] %[[#ExtInstId]] fmax %[[#Xn]] %[[#Yn]]
+; CHECK: OpReturnValue %[[#Resn]]
+define spir_func float @test_maximumnum_nnan(float %x, float %y) {
+ %r = call nnan float @llvm.maximumnum.f32(float %x, float %y)
+ ret float %r
+}
+
+declare float @llvm.maximumnum.f32(float, float)
+declare <4 x float> @llvm.maximumnum.v4f32(<4 x float>, <4 x float>)
diff --git a/llvm/test/CodeGen/SPIRV/llvm-intrinsics/minimumnum.ll b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/minimumnum.ll
new file mode 100644
index 0000000000000..ce3ba0b00a402
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/minimumnum.ll
@@ -0,0 +1,38 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+;; llvm.minimumnum differs from llvm.minnum only in its treatment of signaling
+;; NaNs, so it is expanded into a pair of quieting canonicalizations, each of
+;; which becomes a multiplication by 1.0, followed by the OpenCL.std fmin that
+;; llvm.minnum already uses.
+
+; CHECK-DAG: %[[#ExtInstId:]] = OpExtInstImport "OpenCL.std"
+; CHECK-DAG: %[[#Float:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#Double:]] = OpTypeFloat 64
+; CHECK-DAG: %[[#OneFloat:]] = OpConstant %[[#Float]] 1
+; CHECK-DAG: %[[#OneDouble:]] = OpConstant %[[#Double]] 1
+
+; CHECK: %[[#X:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#Y:]] = OpFunctionParameter %[[#Float]]
+; CHECK: %[[#QX:]] = OpFMul %[[#Float]] %[[#X]] %[[#OneFloat]]
+; CHECK: %[[#QY:]] = OpFMul %[[#Float]] %[[#Y]] %[[#OneFloat]]
+; CHECK: %[[#Res:]] = OpExtInst %[[#Float]] %[[#ExtInstId]] fmin %[[#QX]] %[[#QY]]
+; CHECK: OpReturnValue %[[#Res]]
+define spir_func float @test_minimumnum_float(float %x, float %y) {
+ %r = call float @llvm.minimumnum.f32(float %x, float %y)
+ ret float %r
+}
+
+; CHECK: %[[#Xd:]] = OpFunctionParameter %[[#Double]]
+; CHECK: %[[#Yd:]] = OpFunctionParameter %[[#Double]]
+; CHECK: %[[#QXd:]] = OpFMul %[[#Double]] %[[#Xd]] %[[#OneDouble]]
+; CHECK: %[[#QYd:]] = OpFMul %[[#Double]] %[[#Yd]] %[[#OneDouble]]
+; CHECK: %[[#Resd:]] = OpExtInst %[[#Double]] %[[#ExtInstId]] fmin %[[#QXd]] %[[#QYd]]
+; CHECK: OpReturnValue %[[#Resd]]
+define spir_func double @test_minimumnum_double(double %x, double %y) {
+ %r = call double @llvm.minimumnum.f64(double %x, double %y)
+ ret double %r
+}
+
+declare float @llvm.minimumnum.f32(float, float)
+declare double @llvm.minimumnum.f64(double, double)
diff --git a/llvm/test/CodeGen/SPIRV/llvm-intrinsics/sincospi-opencl.ll b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/sincospi-opencl.ll
new file mode 100644
index 0000000000000..4563cc24ccbec
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/llvm-intrinsics/sincospi-opencl.ll
@@ -0,0 +1,50 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+;; Test that llvm.sincospi is lowered to the OpenCL.std sinpi and cospi pair,
+;; with both results reused by the original llvm.sincospi users.
+
+; CHECK-DAG: %[[#ExtInstId:]] = OpExtInstImport "OpenCL.std"
+; CHECK-DAG: %[[#FloatTy:]] = OpTypeFloat 32
+; CHECK-DAG: %[[#Vec2FloatTy:]] = OpTypeVector %[[#FloatTy]] 2
+
+; CHECK: %[[#XParam:]] = OpFunctionParameter %[[#FloatTy]]
+; CHECK: %[[#SinRes:]] = OpExtInst %[[#FloatTy]] %[[#ExtInstId]] sinpi %[[#XParam]]
+; CHECK: %[[#CosRes:]] = OpExtInst %[[#FloatTy]] %[[#ExtInstId]] cospi %[[#XParam]]
+; CHECK: %[[#Sum:]] = OpFAdd %[[#FloatTy]] %[[#SinRes]] %[[#CosRes]]
+; CHECK: OpReturnValue %[[#Sum]]
+define float @test_sincospi_scalar(float %x) {
+ %result = call { float, float } @llvm.sincospi.f32(float %x)
+ %sin = extractvalue { float, float } %result, 0
+ %cos = extractvalue { float, float } %result, 1
+ %sum = fadd float %sin, %cos
+ ret float %sum
+}
+
+; CHECK: %[[#XvParam:]] = OpFunctionParameter %[[#Vec2FloatTy]]
+; CHECK: %[[#SinResv:]] = OpExtInst %[[#Vec2FloatTy]] %[[#ExtInstId]] sinpi %[[#XvParam]]
+; CHECK: %[[#CosResv:]] = OpExtInst %[[#Vec2FloatTy]] %[[#ExtInstId]] cospi %[[#XvParam]]
+; CHECK: %[[#Sumv:]] = OpFAdd %[[#Vec2FloatTy]] %[[#SinResv]] %[[#CosResv]]
+; CHECK: OpReturnValue %[[#Sumv]]
+define <2 x float> @test_sincospi_vec2(<2 x float> %x) {
+ %result = call { <2 x float>, <2 x float> } @llvm.sincospi.v2f32(<2 x float> %x)
+ %sin = extractvalue { <2 x float>, <2 x float> } %result, 0
+ %cos = extractvalue { <2 x float>, <2 x float> } %result, 1
+ %sum = fadd <2 x float> %sin, %cos
+ ret <2 x float> %sum
+}
+
+;; An unused cosine result must not emit a cospi instruction.
+; CHECK: %[[#XoParam:]] = OpFunctionParameter %[[#FloatTy]]
+; CHECK-NOT: cospi
+; CHECK: %[[#SinOnlyRes:]] = OpExtInst %[[#FloatTy]] %[[#ExtInstId]] sinpi %[[#XoParam]]
+; CHECK-NOT: cospi
+; CHECK: OpReturnValue %[[#SinOnlyRes]]
+define float @test_sincospi_sin_only(float %x) {
+ %result = call { float, float } @llvm.sincospi.f32(float %x)
+ %sin = extractvalue { float, float } %result, 0
+ ret float %sin
+}
+
+declare { float, float } @llvm.sincospi.f32(float)
+declare { <2 x float>, <2 x float> } @llvm.sincospi.v2f32(<2 x float>)
>From ec6be339c0838d0f559cfee5a35d71048ae5664a Mon Sep 17 00:00:00 2001
From: Paulius Velesko <pvelesko at pglc.io>
Date: Tue, 25 Aug 2026 11:28:07 +0300
Subject: [PATCH 2/2] [SPIRV] Legalize canonicalize, sincospi and the
minimumnum/maximumnum pair
G_FCANONICALIZE becomes a multiplication by 1.0. LangRef says of
llvm.canonicalize that the operation "should always be implementable as
multiplication by 1.0, provided that the compiler does not constant fold
the operation". Nothing that could fold the multiply away runs after
instruction selection, so the selector emits OpFMulS/OpFMulV against a
constant 1.0 directly. This is the expansion
TargetLowering::expandFCANONICALIZE already uses for SelectionDAG
targets, and on an implementation that flushes subnormals it also gives
the flush to a zero of the same sign that LangRef describes.
G_FMINIMUMNUM and G_FMAXIMUMNUM use the generic expansion, which quiets
signaling NaN operands with G_FCANONICALIZE and then emits
G_FMINNUM/G_FMAXNUM. LangRef states that llvm.maximumnum "behaves the
same as llvm.maxnum other than its treatment of sNaN inputs", so once
the operands are quieted the existing fmin/fmax selection is exact. The
nnan case skips the quieting entirely.
llvm.sincospi becomes the OpenCL.std sinpi and cospi pair, following the
shape of the existing llvm.sincos handling, including not emitting cospi
when the cosine result is unused. GLSL.std.450 has no sinpi or cospi, so
the shader environment still rejects llvm.sincospi, now with a targeted
diagnostic instead of "intrinsic selection not implemented".
---
.../Target/SPIRV/SPIRVInstructionSelector.cpp | 58 +++++++++++++++++++
llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp | 7 +++
2 files changed, 65 insertions(+)
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index f3f9984190de5..0719e466b5eee 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -487,6 +487,10 @@ class SPIRVInstructionSelector : public InstructionSelector {
MachineInstr &I) const;
bool selectSincos(Register ResVReg, SPIRVTypeInst ResType,
MachineInstr &I) const;
+ bool selectSincospi(Register ResVReg, SPIRVTypeInst ResType,
+ MachineInstr &I) const;
+ bool selectFCanonicalize(Register ResVReg, SPIRVTypeInst ResType,
+ MachineInstr &I) const;
bool selectExp10(Register ResVReg, SPIRVTypeInst ResType,
MachineInstr &I) const;
bool selectDerivativeInst(Register ResVReg, SPIRVTypeInst ResType,
@@ -1190,6 +1194,9 @@ bool SPIRVInstructionSelector::spvSelect(Register ResVReg,
case TargetOpcode::G_FMAXIMUM:
return selectExtInst(ResVReg, ResType, I, CL::fmax, GL::NMax);
+ case TargetOpcode::G_FCANONICALIZE:
+ return selectFCanonicalize(ResVReg, ResType, I);
+
case TargetOpcode::G_FCOPYSIGN:
return selectCopySign(ResVReg, ResType, I);
@@ -1780,6 +1787,55 @@ bool SPIRVInstructionSelector::selectSincos(Register ResVReg,
return false;
}
+bool SPIRVInstructionSelector::selectSincospi(Register ResVReg,
+ SPIRVTypeInst ResType,
+ MachineInstr &I) const {
+ // OpenCL.std provides sinpi and cospi, which compute sin(pi * x) and
+ // cos(pi * x) without the argument reduction error a literal multiplication
+ // by pi would introduce. GLSL.std.450 has no equivalent, so the shader
+ // environment keeps reporting llvm.sincospi as unsupported.
+ if (!STI.canUseExtInstSet(SPIRV::InstructionSet::OpenCL_std))
+ return diagnoseUnsupported(
+ I, "llvm.sincospi requires the OpenCL.std extended instruction set.");
+
+ Register CosResVReg = I.getOperand(1).getReg();
+ // Operands are the two results, the intrinsic ID, and then the argument.
+ const MachineOperand &Src = I.getOperand(I.getNumExplicitDefs() + 1);
+ Register ResTypeReg = GR.getSPIRVTypeID(ResType);
+
+ BuildMI(*I.getParent(), I, I.getDebugLoc(), TII.get(SPIRV::OpExtInst))
+ .addDef(ResVReg)
+ .addUse(ResTypeReg)
+ .addImm(static_cast<uint32_t>(SPIRV::InstructionSet::OpenCL_std))
+ .addImm(CL::sinpi)
+ .add(Src)
+ .constrainAllUses(TII, TRI, RBI);
+ if (!MRI->use_nodbg_empty(CosResVReg))
+ BuildMI(*I.getParent(), I, I.getDebugLoc(), TII.get(SPIRV::OpExtInst))
+ .addDef(CosResVReg)
+ .addUse(ResTypeReg)
+ .addImm(static_cast<uint32_t>(SPIRV::InstructionSet::OpenCL_std))
+ .addImm(CL::cospi)
+ .add(Src)
+ .constrainAllUses(TII, TRI, RBI);
+ return true;
+}
+
+bool SPIRVInstructionSelector::selectFCanonicalize(Register ResVReg,
+ SPIRVTypeInst ResType,
+ MachineInstr &I) const {
+ // Neither SPIR-V core nor the extended instruction sets have a canonicalize
+ // instruction. LangRef says llvm.canonicalize "should always be
+ // implementable as multiplication by 1.0, provided that the compiler does
+ // not constant fold the operation", and nothing that could fold the multiply
+ // away runs after instruction selection.
+ Register One = buildOnesValF(ResType, I);
+ return selectOpWithSrcs(ResVReg, ResType, I, {I.getOperand(1).getReg(), One},
+ ResType->getOpcode() == SPIRV::OpTypeVector
+ ? SPIRV::OpFMulV
+ : SPIRV::OpFMulS);
+}
+
bool SPIRVInstructionSelector::selectOpWithSrcs(Register ResVReg,
SPIRVTypeInst ResType,
MachineInstr &I,
@@ -5822,6 +5878,8 @@ bool SPIRVInstructionSelector::selectIntrinsic(Register ResVReg,
return selectMaskedScatter(I);
return diagnoseUnsupported(
I, "llvm.masked.scatter requires SPV_INTEL_masked_gather_scatter");
+ case Intrinsic::sincospi:
+ return selectSincospi(ResVReg, ResType, I);
case Intrinsic::returnaddress:
case Intrinsic::frameaddress: {
// SPIR-V does not have a stack or return address. Lower to null.
diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
index e45321640458b..e70fbb556d878 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizerInfo.cpp
@@ -523,6 +523,7 @@ SPIRVLegalizerInfo::SPIRVLegalizerInfo(const SPIRVSubtarget &ST) {
G_FLOG2,
G_FLOG10,
G_FABS,
+ G_FCANONICALIZE,
G_FMINNUM,
G_FMAXNUM,
G_FCEIL,
@@ -553,6 +554,12 @@ SPIRVLegalizerInfo::SPIRVLegalizerInfo(const SPIRVSubtarget &ST) {
.moreElementsToNextPow2(0);
// clang-format on
+ // SPIR-V has no IEEE-754-2019 minimumNumber/maximumNumber instruction. Use
+ // the generic expansion, which quiets signaling NaN operands with
+ // G_FCANONICALIZE and then emits G_FMINNUM/G_FMAXNUM. Per LangRef those are
+ // the only operands for which the two families differ.
+ getActionDefinitionsBuilder({G_FMINIMUMNUM, G_FMAXIMUMNUM}).lower();
+
getActionDefinitionsBuilder(G_FCOPYSIGN)
.legalForCartesianProduct(allFloatScalarsAndVectors,
allFloatScalarsAndVectors);
More information about the llvm-commits
mailing list