Repository: dotnet/runtime
Pull request #114848: Fix side effect handling for certain TernaryLogic cases
Author: login: BruceForstall
Labels: name: area-CodeGen-coreclr
description: CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Created: 2025-04-20T20:50:06Z | Merged: 2025-04-23T16:32:17Z

There were a few cases of TernaryLogic with NOT/AND and NOT/OR that swapped operands without considering side effects. Add some new logic to hoist those side effects if necessary.

Fixes #114324

Linked issue #114324: JIT: Likely incorrect side effect ordering with `Avx512F.VL.TernaryLogic`
```csharp
// Generated by Fuzzlyn v2.5 on 2025-04-06 16:30:45
// Run on X64 Windows
// Seed: 13621799076737484256-vectort,vector128,vector256,x86aes,x86avx,x86avx2,x86avx512bw,x86avx512bwvl,x86avx512cd,x86avx512cdvl,x86avx512dq,x86avx512dqvl,x86avx512f,x86avx512fvl,x86avx512fx64,x86bmi1,x86bmi1x64,x86bmi2,x86bmi2x64,x86fma,x86lzcnt,x86lzcntx64,x86pclmulqdq,x86popcnt,x86popcntx64,x86sse,x86ssex64,x86sse2,x86sse2x64,x86sse3,x86sse41,x86sse41x64,x86sse42,x86sse42x64,x86ssse3,x86x86base
// Reduced from 20.1 KiB to 0.7 KiB in 00:01:13
// Debug: Runs successfully
// Release: Throws 'System.NullReferenceException'
using System;
using System.Numerics;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;

public class Program
{
    public static ulong[] s_3;
    public static void Main()
    {
        var vr1 = Vector128.Create<int>(1);
        Vector128<int> vr2 = Avx512F.VL.TernaryLogic(vr1, Sse2.ShiftLeftLogical128BitLane(Vector128.Create<int>((int)Sse42.Crc32(M2(), 0)), 0), Avx512F.VL.ConvertToVector128Int32(Vector256.Create<ulong>(s_3[0])), 221);
        System.Console.WriteLine(vr2);
    }

    public static uint M2()
    {
        s_3 = new ulong[]
        {
            0
        };
        return M3();
    }

    public static byte M3()
    {
        return default(byte);
    }
}
```

cc @dotnet/jit-contrib
Linked issue comments: user: login: dotnet-policy-service[bot]
created_at: 2025-04-07T09:29:41Z
body: Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR
user: login: JulieLeeMSFT
created_at: 2025-04-07T16:25:30Z
body: CC @tannergooding and @anthonycanino. 
author_association: MEMBER
user: login: BruceForstall
created_at: 2025-04-20T20:47:15Z
body: I'll take this one.
author_association: CONTRIBUTOR

Diff:
diff --git a/src/coreclr/jit/hwintrinsicxarch.cpp b/src/coreclr/jit/hwintrinsicxarch.cpp
index 6098fed4b4485c..84f47d878299dd 100644
--- a/src/coreclr/jit/hwintrinsicxarch.cpp
+++ b/src/coreclr/jit/hwintrinsicxarch.cpp
@@ -4405,6 +4405,65 @@ GenTree* Compiler::impSpecialIntrinsic(NamedIntrinsic        intrinsic,
                         }
                     }
 
+                    // Some normalization cases require us to swap the operands, which might require
+                    // spilling side effects. Check that here.
+                    //
+                    // cast in switch clause is needed for old gcc
+                    switch ((TernaryLogicOperKind)info.oper1)
+                    {
+                        case TernaryLogicOperKind::Not:
+                        {
+                            assert(info.oper1Use != TernaryLogicUseFlags::None);
+
+                            bool needSideEffectSpill = false;
+
+                            if (info.oper2 == TernaryLogicOperKind::And)
+                            {
+                                assert(info.oper2Use != TernaryLogicUseFlags::None);
+
+                                if ((control == static_cast<uint8_t>(~0xCC & 0xF0)) || // ~B & A
+                                    (control == static_cast<uint8_t>(~0xAA & 0xF0)) || // ~C & A
+                                    (control == static_cast<uint8_t>(~0xAA & 0xCC)))   // ~C & B
+                                {
+                                    // We're normalizing to ~B & C, so we need another swap
+                                    std::swap(val2, val3);
+                                    needSideEffectSpill = (control == static_cast<uint8_t>(~0xAA & 0xCC)); // ~C & B
+                                }
+                            }
+                            else if (info.oper2 == TernaryLogicOperKind::Or)
+                            {
+                                assert(info.oper2Use != TernaryLogicUseFlags::None);
+
+                                if ((control == static_cast<uint8_t>(~0xCC | 0xF0)) || // ~B | A
+                                    (control == static_cast<uint8_t>(~0xAA | 0xF0)) || // ~C | A
+                                    (control == static_cast<uint8_t>(~0xAA | 0xCC)))   // ~C | B
+                                {
+                                    // We're normalizing to ~B | C, so we need another swap
+                                    std::swap(val2, val3);
+                                    needSideEffectSpill = (control == static_cast<uint8_t>(~0xAA | 0xCC)); // ~C | B
+                                }
+                            }
+
+                            if (needSideEffectSpill)
+                            {
+                                // Side-effect cases:
+                                // ~B op A ; order before swap C A B
+                                //    op1 & op2 already set to be spilled; no further spilling necessary
+                                // ~C op A ; order before swap B A C
+                                //    op1 already set to be spilled; no further spilling necessary
+                                // ~C op B ; order before swap A B C
+                                //    nothing already set to be spilled; op1 & op2 need to be spilled
+
+                                spillOp1 = true;
+                                spillOp2 = true;
+                            }
+                            break;
+                        }
+
+                        default:
+                            break;
+                    }
+
                     if (spillOp1)
                     {
                         impSpillSideEffect(true, stackState.esStackDepth -
@@ -4529,8 +4588,7 @@ GenTree* Compiler::impSpecialIntrinsic(NamedIntrinsic        intrinsic,
                                     (control == static_cast<uint8_t>(~0xAA & 0xF0)) || // ~C & A
                                     (control == static_cast<uint8_t>(~0xAA & 0xCC)))   // ~C & B
                                 {
-                                    // We're normalizing to ~B & C, so we need another swap
-                                    std::swap(*val2, *val3);
+                                    // We already normalized to ~B & C above.
                                 }
                                 else
                                 {
@@ -4556,8 +4614,7 @@ GenTree* Compiler::impSpecialIntrinsic(NamedIntrinsic        intrinsic,
                                     (control == static_cast<uint8_t>(~0xAA | 0xF0)) || // ~C | A
                                     (control == static_cast<uint8_t>(~0xAA | 0xCC)))   // ~C | B
                                 {
-                                    // We're normalizing to ~B & C, so we need another swap
-                                    std::swap(*val2, *val3);
+                                    // We already normalized to ~B | C above.
                                 }
                                 else
                                 {
diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.cs b/src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.cs
new file mode 100644
index 00000000000000..7be85d5a712540
--- /dev/null
+++ b/src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Generated by Fuzzlyn v2.5 on 2025-04-06 16:30:45
+// Run on X64 Windows
+// Seed: 13621799076737484256-vectort,vector128,vector256,x86aes,x86avx,x86avx2,x86avx512bw,x86avx512bwvl,x86avx512cd,x86avx512cdvl,x86avx512dq,x86avx512dqvl,x86avx512f,x86avx512fvl,x86avx512fx64,x86bmi1,x86bmi1x64,x86bmi2,x86bmi2x64,x86fma,x86lzcnt,x86lzcntx64,x86pclmulqdq,x86popcnt,x86popcntx64,x86sse,x86ssex64,x86sse2,x86sse2x64,x86sse3,x86sse41,x86sse41x64,x86sse42,x86sse42x64,x86ssse3,x86x86base
+// Reduced from 20.1 KiB to 0.7 KiB in 00:01:13
+// Debug: Runs successfully
+// Release: Throws 'System.NullReferenceException'
+using System;
+using System.Numerics;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+using Xunit;
+
+public class Runtime_114324
+{
+    public static ulong[] s_3;
+
+    [Fact]
+    public static void TestEntryPoint()
+    {
+        if (Avx512F.VL.IsSupported)
+        {
+            var vr1 = Vector128.Create<int>(1);
+            Vector128<int> vr2 = Avx512F.VL.TernaryLogic(
+                    vr1,
+                    Sse2.ShiftLeftLogical128BitLane(
+                        Vector128.Create<int>((int)Sse42.Crc32(M2(), 0)),
+                        0),
+                    Avx512F.VL.ConvertToVector128Int32(Vector256.Create<ulong>(s_3[0])),
+                    221);
+            System.Console.WriteLine(vr2);
+            Assert.Equal(Vector128.Create(-1,-1,-1,-1), vr2);
+        }
+    }
+
+    public static uint M2()
+    {
+        s_3 = new ulong[]
+        {
+            0
+        };
+        return M3();
+    }
+
+    public static byte M3()
+    {
+        return default(byte);
+    }
+}
diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.csproj b/src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.csproj
new file mode 100644
index 00000000000000..de6d5e08882e86
--- /dev/null
+++ b/src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.csproj
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <Optimize>True</Optimize>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="$(MSBuildProjectName).cs" />
+  </ItemGroup>
+</Project>


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes a bug in the handling of TernaryLogic cases where side effects were not properly preserved when swapping operands in NOT/AND and NOT/OR operations.  
- Added a regression test (Runtime_114324.cs) to capture the issue.  
- Updated hwintrinsicxarch.cpp to ensure that side effects are correctly hoisted during operand normalization.

### Reviewed Changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

| File | Description |
| ---- | ----------- |
| src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.cs | Added regression test for TernaryLogic side effect handling |
| src/coreclr/jit/hwintrinsicxarch.cpp | Updated operand swapping logic to correctly hoist side effects |

<details>
<summary>Files not reviewed (1)</summary>

* **src/tests/JIT/Regression/JitBlue/Runtime_114324/Runtime_114324.csproj**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T20:50:19Z
user: login: tannergooding
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T15:04:20Z
user: login: tannergooding
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T15:04:25Z
user: login: BruceForstall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T16:32:02Z

Review comments:
diff_hunk: @@ -4529,8 +4588,7 @@ GenTree* Compiler::impSpecialIntrinsic(NamedIntrinsic        intrinsic,
                                     (control == static_cast<uint8_t>(~0xAA & 0xF0)) || // ~C & A
                                     (control == static_cast<uint8_t>(~0xAA & 0xCC)))   // ~C & B
                                 {
-                                    // We're normalizing to ~B & C, so we need another swap
-                                    std::swap(*val2, *val3);
+                                    // We already normalized to ~B & C above.
path: src/coreclr/jit/hwintrinsicxarch.cpp
user: login: tannergooding
body: nit: This first if could be merged with the assert below. We may not even need the assert at all since it's the only possible 6 permutations of `NOT AND` and `NOT OR`
created_at: 2025-04-23T15:04:20Z
author_association: MEMBER
diff_hunk: @@ -4529,8 +4588,7 @@ GenTree* Compiler::impSpecialIntrinsic(NamedIntrinsic        intrinsic,
                                     (control == static_cast<uint8_t>(~0xAA & 0xF0)) || // ~C & A
                                     (control == static_cast<uint8_t>(~0xAA & 0xCC)))   // ~C & B
                                 {
-                                    // We're normalizing to ~B & C, so we need another swap
-                                    std::swap(*val2, *val3);
+                                    // We already normalized to ~B & C above.
path: src/coreclr/jit/hwintrinsicxarch.cpp
user: login: BruceForstall
body: I kept the logic here stylized to be the same as the hoisted logic, for better or worse...
created_at: 2025-04-23T16:32:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: BruceForstall
created_at: 2025-04-20T20:50:21Z
body: /azp run runtime-coreclr outerloop
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-04-20T20:50:34Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: dotnet-policy-service[bot]
created_at: 2025-04-20T20:50:39Z
body: Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR
user: login: BruceForstall
created_at: 2025-04-20T20:51:48Z
body: @tannergooding @dotnet/intel PTAL
cc @dotnet/jit-contrib 
author_association: CONTRIBUTOR