diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index 7d010edb1cfcc0..e280b6c999a7e6 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -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: { diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 22478354a3ab8d..c3f23487aa01c1 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -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(); + } } } } @@ -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: @@ -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; } diff --git a/src/coreclr/jit/namedintrinsiclist.h b/src/coreclr/jit/namedintrinsiclist.h index 06c69ba2cd7c01..8aee4afb56c121 100644 --- a/src/coreclr/jit/namedintrinsiclist.h +++ b/src/coreclr/jit/namedintrinsiclist.h @@ -15,6 +15,7 @@ enum NamedIntrinsic : unsigned short NI_System_ArgumentNullException_ThrowIfNull, + NI_System_Enum_Equals, NI_System_Enum_HasFlag, NI_System_BitConverter_DoubleToInt64Bits, diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.cs index cb345ebac766e0..4e06f0e6da0ae4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.cs @@ -1187,6 +1187,7 @@ internal object GetValue() } /// + [Intrinsic] public override bool Equals([NotNullWhen(true)] object? obj) { if (obj is null) diff --git a/src/tests/JIT/Intrinsics/EnumIntrinsics.cs b/src/tests/JIT/Intrinsics/EnumIntrinsics.cs new file mode 100644 index 00000000000000..eae3911d563751 --- /dev/null +++ b/src/tests/JIT/Intrinsics/EnumIntrinsics.cs @@ -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 { 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 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 where T : Enum { public T field; } + public enum SimpleEnum { A, B, C } +} diff --git a/src/tests/JIT/Intrinsics/EnumIntrinsics.csproj b/src/tests/JIT/Intrinsics/EnumIntrinsics.csproj new file mode 100644 index 00000000000000..2b5c043e93af92 --- /dev/null +++ b/src/tests/JIT/Intrinsics/EnumIntrinsics.csproj @@ -0,0 +1,9 @@ + + + None + True + + + + +