Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/coreclr/jit/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13839,6 +13839,50 @@ GenTree* Compiler::gtFoldExprCall(GenTreeCall* call)
break;
}

case NI_System_Enum_Equals:
{
assert(call->AsCall()->gtArgs.CountUserArgs() == 2);
GenTree* arg0 = call->AsCall()->gtArgs.GetArgByIndex(0)->GetNode();
GenTree* arg1 = call->AsCall()->gtArgs.GetArgByIndex(1)->GetNode();

bool isArg0Exact;
bool isArg1Exact;
bool isNonNull; // Unused here.

CORINFO_CLASS_HANDLE cls0 = gtGetClassHandle(arg0, &isArg0Exact, &isNonNull);
CORINFO_CLASS_HANDLE cls1 = gtGetClassHandle(arg1, &isArg1Exact, &isNonNull);
if ((cls0 != cls1) || (cls0 == NO_CLASS_HANDLE) || !isArg0Exact || !isArg1Exact)
{
break;
}

assert(info.compCompHnd->isEnum(cls1, nullptr) == TypeCompareState::Must);

CorInfoType corTyp = info.compCompHnd->getTypeForPrimitiveValueClass(cls1);
if (corTyp == CORINFO_TYPE_UNDEF)
{
break;
}

var_types typ = JITtype2varType(corTyp);
if (!varTypeIsIntegral(typ))
{
// Ignore non-integral enums e.g. enums based on float/double
break;
}

// Unbox both integral arguments and compare their underlying values
GenTree* offset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
GenTree* addr0 = gtNewOperNode(GT_ADD, TYP_BYREF, arg0, offset);
GenTree* addr1 = gtNewOperNode(GT_ADD, TYP_BYREF, arg1, gtCloneExpr(offset));
GenTree* cmpNode = gtNewOperNode(GT_EQ, TYP_INT, gtNewIndir(typ, addr0), gtNewIndir(typ, addr1));
JITDUMP("Optimized Enum.Equals call to comparison of underlying values:\n");
DISPTREE(cmpNode);
JITDUMP("\n");

return gtFoldExpr(cmpNode);
}

case NI_System_Type_op_Equality:
case NI_System_Type_op_Inequality:
{
Expand Down
15 changes: 13 additions & 2 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,13 @@ var_types Compiler::impImportCall(OPCODE opcode,
if ((callInfo->methodFlags & CORINFO_FLG_INTRINSIC) != 0)
{
call->AsCall()->gtCallMoreFlags |= GTF_CALL_M_SPECIAL_INTRINSIC;

GenTree* foldedCall = gtFoldExprCall(call->AsCall());
if ((foldedCall != call) || !call->IsCall())
{
impPushOnStack(foldedCall, typeInfo(foldedCall->TypeGet()));
return foldedCall->TypeGet();
}
}
}
}
Expand Down Expand Up @@ -1571,7 +1578,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
}

//------------------------------------------------------------------------
// impThrowIfNull: Remove redundandant boxing from ArgumentNullException_ThrowIfNull
// impThrowIfNull: Remove redundant boxing from ArgumentNullException_ThrowIfNull
// it is done for Tier0 where we can't remove it without inlining otherwise.
//
// Arguments:
Expand Down Expand Up @@ -10344,7 +10351,11 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
if (strcmp(className, "Enum") == 0)
{
if (strcmp(methodName, "HasFlag") == 0)
if (strcmp(methodName, "Equals") == 0)
{
result = NI_System_Enum_Equals;
}
else if (strcmp(methodName, "HasFlag") == 0)
{
result = NI_System_Enum_HasFlag;
}
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum NamedIntrinsic : unsigned short

NI_System_ArgumentNullException_ThrowIfNull,

NI_System_Enum_Equals,
NI_System_Enum_HasFlag,

NI_System_BitConverter_DoubleToInt64Bits,
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Private.CoreLib/src/System/Enum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,7 @@ internal object GetValue()
}

/// <inheritdoc/>
[Intrinsic]
public override bool Equals([NotNullWhen(true)] object? obj)
{
if (obj is null)
Expand Down
108 changes: 108 additions & 0 deletions src/tests/JIT/Intrinsics/EnumIntrinsics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
//

using System;
using System.Runtime.CompilerServices;
using Xunit;

public class EnumIntrinsics
{
public enum SByteEnum : sbyte { Min = sbyte.MinValue, Neg = -1, Zero = 0, Max = sbyte.MaxValue }
public enum ByteEnum : byte { Min = 0, Max = 255 }
public enum ShortEnum : short { Min = short.MinValue, Max = short.MaxValue }
public enum UShortEnum : ushort { Min = 0, Max = ushort.MaxValue }
public enum IntEnum : int { Min = int.MinValue, Zero = 0, Max = int.MaxValue }
public enum UIntEnum : uint { Min = 0, Max = uint.MaxValue }
public enum LongEnum : long { Min = long.MinValue, Max = long.MaxValue }
public enum ULongEnum : ulong { Min = 0, Max = ulong.MaxValue }
[Flags] public enum FlagsEnum { None = 0, A = 1, B = 2, All = 3 }

[Fact]
public static void TestEntryPoint()
{
TestSimpleEnums();
TestGenericEnums();
TestDifferentUnderlyingTypes();
TestCornerCases();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestSimpleEnums()
{
// Testing bit-pattern identities
Assert.True(SByteEnum.Neg.Equals((SByteEnum)(-1)));
Assert.True(ByteEnum.Max.Equals((ByteEnum)255));
Assert.False(SByteEnum.Max.Equals(SByteEnum.Min));

// Flags behavior (bitwise equality)
FlagsEnum flags = FlagsEnum.A | FlagsEnum.B;
Assert.True(flags.Equals(FlagsEnum.All));
Assert.False(flags.Equals(FlagsEnum.A));

// 64-bit boundaries
Assert.True(ULongEnum.Max.Equals((ULongEnum)ulong.MaxValue));
Assert.True(LongEnum.Min.Equals((LongEnum)long.MinValue));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestGenericEnums()
{
// Testing generic instantiation for every width
Assert.True(CheckGenericEquals(SByteEnum.Max, SByteEnum.Max));
Assert.True(CheckGenericEquals(UShortEnum.Max, UShortEnum.Max));
Assert.True(CheckGenericEquals(UIntEnum.Max, UIntEnum.Max));
Assert.True(CheckGenericEquals(ULongEnum.Max, ULongEnum.Max));

var container = new GenericEnumClass<IntEnum> { field = IntEnum.Min };
Assert.True(CheckGenericEquals(container.field, IntEnum.Min));
Assert.False(CheckGenericEquals(container.field, IntEnum.Max));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static bool CheckGenericEquals<T>(T left, T right) where T : Enum => left.Equals(right);

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestDifferentUnderlyingTypes()
{
// 0xFF pattern
Assert.False(((SByteEnum)(-1)).Equals((ByteEnum)255));

// 0x00 pattern
Assert.False(SByteEnum.Zero.Equals(ByteEnum.Min));

// 0xFFFF pattern
Assert.False(((ShortEnum)(-1)).Equals((UShortEnum)ushort.MaxValue));

// 0xFFFFFFFF pattern
Assert.False(((IntEnum)(-1)).Equals((UIntEnum)uint.MaxValue));

// Signed vs Unsigned same width
Assert.False(IntEnum.Max.Equals((UIntEnum)int.MaxValue));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestCornerCases()
{
// Ensure no false positives with primitive types (boxing checks)
Assert.False(IntEnum.Zero.Equals(0));
Assert.False(LongEnum.Max.Equals(long.MaxValue));

// Different enum types entirely
Assert.False(SimpleEnum.A.Equals(FlagsEnum.A));

// Null and Object references
object obj = new object();
Assert.False(SimpleEnum.B.Equals(obj));
Assert.False(SimpleEnum.C.Equals(null));

// Double boxing scenarios
object boxedA = SimpleEnum.A;
object boxedB = SimpleEnum.A;
Assert.True(boxedA.Equals(boxedB));
Assert.True(SimpleEnum.A.Equals(boxedB));
}

public class GenericEnumClass<T> where T : Enum { public T field; }
public enum SimpleEnum { A, B, C }
}
9 changes: 9 additions & 0 deletions src/tests/JIT/Intrinsics/EnumIntrinsics.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DebugType>None</DebugType>
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>
<Compile Include="EnumIntrinsics.cs" />
</ItemGroup>
</Project>
Loading