diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs index 584f6cd3e15965..f8460dd4c2e8f7 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs @@ -352,6 +352,13 @@ public static object GetUninitializedObject( /// true if given type is bitwise equatable (memcmp can be used for equality checking) /// /// Only use the result of this for Equals() comparison, not for CompareTo() comparison. + /// + /// A bitwise comparison may read the value using accesses wider than an individual field. Under an + /// unsynchronized concurrent mutation -- already a data race with undefined behavior -- this can observe + /// a torn value within a single field that a strictly field-wise comparison would not. A torn read cannot + /// fabricate an invalid managed reference; only the already-undefined total comparison result is affected. This is + /// acceptable for bitwise-based APIs such as . + /// /// [Intrinsic] internal static bool IsBitwiseEquatable() diff --git a/src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs b/src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs new file mode 100644 index 00000000000000..d694629537297c --- /dev/null +++ b/src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + public interface IEquatable + { + bool Equals(T other); + } +} diff --git a/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj b/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj index e1b9c28a56c2c9..f33002cf626c01 100644 --- a/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj +++ b/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj @@ -236,6 +236,7 @@ + diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs index e183c0f85b9d44..7760d7d1f39fb2 100644 --- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs +++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; using Debug = System.Diagnostics.Debug; @@ -317,6 +318,393 @@ public static bool CanCompareValueTypeBitsUntilOffset(MetadataType type, MethodD return result; } + /// + /// Determines whether is bitwise-equatable: an unmanaged, tightly-packed + /// type whose equality is provably a bitwise (memcmp) comparison. This is the single authority + /// behind . + /// + public static bool IsBitwiseEquatable(TypeDesc type) + { + // Integer-like primitives, enums, native ints, and pointers are memcmp-comparable. + if (IsBitwiseComparablePrimitive(type)) + return true; + + if (type is not MetadataType mdType || !mdType.IsValueType) + return false; + + bool? equatable = ImplementsIEquatable(mdType); + if (!equatable.HasValue) + return false; + + if (equatable.Value) + { + // Value type that implements IEquatable of self: bitwise-equatable when it is tightly + // packed and its Equals is a plain field-wise comparison. + return IsIEquatableEqualsFieldwise(mdType); + } + + // Value type that can use memcmp and that doesn't override object.Equals or implement + // IEquatable.Equals. + MethodDesc objectEquals = mdType.Context.GetWellKnownType(WellKnownType.Object).GetMethod("Equals"u8, null); + return mdType.FindVirtualFunctionTargetMethodOnObjectType(objectEquals).OwningType != mdType + && CanCompareValueTypeBits(mdType, objectEquals); + } + + /// + /// Determines whether a value type's implementation of self is a + /// plain field-wise comparison that is equivalent to a bitwise (memcmp) comparison. This lets a type + /// that implements IEquatable<T> still be reported as bitwise-equatable when its Equals does + /// nothing more than compare every field with ==. + /// + public static bool IsIEquatableEqualsFieldwise(MetadataType type) + { + // Unmanaged (so a byte-wise compare is meaningful) and tightly packed (no padding anywhere the + // compare would inspect) -- matching the CoreCLR VM, which checks these separately. + if (type.ContainsGCPointers) + return false; + + if (!IsTightlyPacked(type)) + return false; + + MethodDesc equalsImpl = GetIEquatableEqualsImplementation(type); + if (equalsImpl == null) + return false; + + MethodIL methodIL = GetScannableMethodIL(equalsImpl); + if (methodIL == null) + return false; + + // A common pattern forwards `bool Equals(T other) => this == other;` to a user-defined + // `op_Equality`. Follow that single forward before scanning the field-wise comparison. + if (TryGetOpEqualityForward(methodIL, type) is MethodDesc forwarded) + { + methodIL = GetScannableMethodIL(forwarded); + if (methodIL == null) + return false; + } + + return ScanFieldwiseEqualsBody(methodIL, type); + } + + // Builds the IL to scan for a method that may live on an instantiated type. The IL is defined on the + // typical (open) method; wrapping it in an InstantiatedMethodIL makes token lookups resolve fields + // and methods in the exact instantiation. Returns null if the method has no ECMA-backed body. + private static MethodIL GetScannableMethodIL(MethodDesc method) + { + if (method.GetTypicalMethodDefinition() is not EcmaMethod typicalMethod) + return null; + + MethodIL typicalIL = EcmaMethodIL.Create(typicalMethod); + if (typicalIL == null) + return null; + + return method == typicalMethod ? typicalIL : new InstantiatedMethodIL(method, typicalIL); + } + + private static bool IsTightlyPacked(MetadataType type) + { + // Mirrors the CoreCLR VM's MethodTable::IsTightlyPacked: a byte-wise compare equals comparing + // every field only if there is no padding anywhere. That needs the declared fields to exactly + // cover the instance size (no gaps, no overlap) and every nested value-type field to itself be + // tightly packed. The nested check makes this transitive. + if (type.IsInlineArray) + return false; + + if (type.IsGenericDefinition) + return false; + + OverlappingFieldTracker overlappingFieldTracker = new OverlappingFieldTracker(type); + int lastFieldEndOffset = 0; + + foreach (FieldDesc field in type.GetFields()) + { + if (field.IsStatic) + continue; + + lastFieldEndOffset = Math.Max(lastFieldEndOffset, field.Offset.AsInt + field.FieldType.GetElementSize().AsInt); + + if (!overlappingFieldTracker.TrackField(field)) + return false; + + TypeDesc fieldType = field.FieldType; + if (fieldType.IsValueType && !fieldType.IsPrimitive && !fieldType.IsEnum) + { + // Nested value type: recurse for transitive packing. Primitives, pointers, and + // references are leaves whose element size already accounts for their footprint. + if (fieldType is not MetadataType nestedType || !IsTightlyPacked(nestedType)) + return false; + } + } + + if (overlappingFieldTracker.HasGapsBeforeOffset(lastFieldEndOffset)) + return false; + + return lastFieldEndOffset == type.InstanceFieldSize.AsInt; + } + + private static MethodDesc GetIEquatableEqualsImplementation(MetadataType type) + { + MetadataType iequatableType = type.Context.SystemModule.GetKnownType("System"u8, "IEquatable`1"u8); + MethodDesc equalsInterfaceMethod = iequatableType.MakeInstantiatedType(type).GetMethod("Equals"u8, null); + if (equalsInterfaceMethod == null) + return null; + + return type.ResolveInterfaceMethodToVirtualMethodOnType(equalsInterfaceMethod); + } + + private static MethodDesc TryGetOpEqualityForward(MethodIL methodIL, MetadataType type) + { + // ldarg.0; ldobj T; ldarg.1; call op_Equality; ret + ILReader reader = new ILReader(methodIL.GetILBytes()); + + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldarg_0) + return null; + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldobj) + return null; + if (methodIL.GetObject(reader.ReadILToken()) as TypeDesc != type) + return null; + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldarg_1) + return null; + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.call) + return null; + MethodDesc callee = methodIL.GetObject(reader.ReadILToken()) as MethodDesc; + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ret) + return null; + if (reader.HasNext) + return null; + + if (callee == null || !callee.Signature.IsStatic || callee.OwningType != type || callee.Name != "op_Equality"u8) + return null; + + return callee; + } + + private static bool ScanFieldwiseEqualsBody(MethodIL methodIL, MetadataType type) + { + try + { + return ScanFieldwiseEqualsBodyCore(methodIL, type); + } + catch (TypeSystemException.InvalidProgramException) + { + // Malformed or truncated IL: stay conservative and treat it as not field-wise. + return false; + } + } + + private static bool ScanFieldwiseEqualsBodyCore(MethodIL methodIL, MetadataType type) + { + // Verifies the body is a plain field-wise equality: every instance field is compared exactly once + // (via `==`, its own `Equals`, or `EqualityComparer.Default.Equals`) and the results are ANDed + // together, which is equivalent to a bitwise (memcmp) comparison. + int instanceFieldCount = 0; + foreach (FieldDesc field in type.GetFields()) + { + if (!field.IsStatic) + instanceFieldCount++; + } + + if (instanceFieldCount == 0) + return false; + + HashSet comparedFields = new HashSet(); + ILReader reader = new ILReader(methodIL.GetILBytes()); + + int falseTarget = -1; + bool sawFinalCompare = false; + + while (!sawFinalCompare) + { + if (!reader.HasNext) + return false; + + // Optional EqualityComparer.Default lead-in: `call EqualityComparer::get_Default` + // before the operands. + MethodDesc getDefault = null; + if (reader.PeekILOpcode() == ILOpcode.call) + { + reader.ReadILOpcode(); + getDefault = methodIL.GetObject(reader.ReadILToken()) as MethodDesc; + } + + // Left operand: `ldarg.0; ldfld/ldflda F`. The EqualityComparer and inline `==` forms load by + // value; the `.Equals` call form loads the left side by address. + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldarg_0) + return false; + + ILOpcode leftLoad = reader.ReadILOpcode(); + if (leftLoad != ILOpcode.ldfld && leftLoad != ILOpcode.ldflda) + return false; + if (getDefault != null && leftLoad != ILOpcode.ldfld) + return false; + FieldDesc leftField = methodIL.GetObject(reader.ReadILToken()) as FieldDesc; + + if (reader.ReadILOpcode() != ILOpcode.ldarg_1) + return false; + if (reader.ReadILOpcode() != ILOpcode.ldfld) + return false; + FieldDesc rightField = methodIL.GetObject(reader.ReadILToken()) as FieldDesc; + + if (leftField == null || leftField != rightField || leftField.IsStatic || leftField.OwningType != type) + return false; + + // Each field must be compared exactly once. + if (!comparedFields.Add(leftField)) + return false; + + if (getDefault == null && leftLoad == ILOpcode.ldfld) + { + // Inline `==`: only integer-like primitives are memcmp-equivalent. + if (!IsBitwiseComparablePrimitive(leftField.FieldType)) + return false; + + ILOpcode compareOpcode = reader.ReadILOpcode(); + if (compareOpcode == ILOpcode.bne_un_s || compareOpcode == ILOpcode.bne_un) + { + // Non-final field: `bne.un[.s] FALSE` jumps to the shared `return false` tail. A + // body larger than a signed-byte range uses the long form. + int target = reader.ReadBranchDestination(compareOpcode); + if (falseTarget == -1) + falseTarget = target; + else if (falseTarget != target) + return false; + } + else if (compareOpcode == ILOpcode.ceq) + { + // Final field: `ceq; ret` produces the result directly. + if (reader.ReadILOpcode() != ILOpcode.ret) + return false; + sawFinalCompare = true; + } + else + { + return false; + } + + continue; + } + + if (getDefault != null) + { + // `callvirt EqualityComparer::Equals(!0, !0)`. + if (reader.ReadILOpcode() != ILOpcode.callvirt) + return false; + MethodDesc equals = methodIL.GetObject(reader.ReadILToken()) as MethodDesc; + if (!IsEqualityComparerDefaultEquals(getDefault, equals, leftField.FieldType)) + return false; + } + else + { + // `.Equals` call form: a primitive's own Equals, or a nested type's field-wise Equals. + if (reader.ReadILOpcode() != ILOpcode.call) + return false; + MethodDesc callee = methodIL.GetObject(reader.ReadILToken()) as MethodDesc; + if (!IsPrimitiveEqualsCall(callee, leftField.FieldType) && !IsNestedFieldwiseEquatable(callee, leftField.FieldType)) + return false; + } + + // The Equals call already yields a bool: `brfalse[.s]` to the shared tail, or `ret` if final. + ILOpcode terminator = reader.ReadILOpcode(); + if (terminator == ILOpcode.brfalse_s || terminator == ILOpcode.brfalse) + { + int target = reader.ReadBranchDestination(terminator); + if (falseTarget == -1) + falseTarget = target; + else if (falseTarget != target) + return false; + } + else if (terminator == ILOpcode.ret) + { + sawFinalCompare = true; + } + else + { + return false; + } + } + + if (falseTarget != -1) + { + // Shared tail for a mismatch: `ldc.i4.0; ret`. + if (reader.Offset != falseTarget) + return false; + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldc_i4_0) + return false; + if (reader.ReadILOpcode() != ILOpcode.ret) + return false; + } + + return !reader.HasNext && comparedFields.Count == instanceFieldCount; + } + + private static bool IsNestedFieldwiseEquatable(MethodDesc callee, TypeDesc fieldType) + { + // The nested field must be compared through the nested type's own IEquatable.Equals, and + // that Equals must itself be field-wise (its layout is validated by IsIEquatableEqualsFieldwise). + if (callee == null || fieldType is not MetadataType nestedType || !nestedType.IsValueType) + return false; + + if (callee != GetIEquatableEqualsImplementation(nestedType)) + return false; + + return IsIEquatableEqualsFieldwise(nestedType); + } + + private static bool IsPrimitiveEqualsCall(MethodDesc callee, TypeDesc fieldType) + { + // A primitive field compared via 'x.Equals(y)' instead of 'x == y'; for these integer-like + // types both lower to the same bit-for-bit compare. Confirm the callee is its IEquatable.Equals. + if (callee == null || !IsBitwiseComparablePrimitive(fieldType)) + return false; + + return fieldType is MetadataType primitiveType + && callee == GetIEquatableEqualsImplementation(primitiveType); + } + + private static bool IsEqualityComparerDefaultEquals(MethodDesc getDefault, MethodDesc equals, TypeDesc fieldType) + { + // A field compared with EqualityComparer.Default.Equals(this.F, other.F). That is a memcmp + // only when F is itself bitwise-equatable: a bit-comparable primitive, or a nested value type + // whose own IEquatable.Equals is field-wise. + if (!IsEqualityComparerMethod(getDefault, fieldType, "get_Default"u8, isStatic: true) || + !IsEqualityComparerMethod(equals, fieldType, "Equals"u8, isStatic: false)) + { + return false; + } + + if (IsBitwiseComparablePrimitive(fieldType)) + return true; + + return fieldType is MetadataType nestedType && nestedType.IsValueType + && GetIEquatableEqualsImplementation(nestedType) != null + && IsIEquatableEqualsFieldwise(nestedType); + } + + private static bool IsEqualityComparerMethod(MethodDesc method, TypeDesc fieldType, ReadOnlySpan name, bool isStatic) + { + if (method == null || method.Signature.IsStatic != isStatic || method.Name != name) + return false; + + TypeDesc owningType = method.OwningType; + return owningType.GetTypeDefinition() is MetadataType definition + && definition.Module == fieldType.Context.SystemModule + && definition.Name == "EqualityComparer`1"u8 + && definition.Namespace == "System.Collections.Generic"u8 + && owningType.Instantiation.Length == 1 + && owningType.Instantiation[0] == fieldType; + } + + private static bool IsBitwiseComparablePrimitive(TypeDesc fieldType) + { + if (fieldType.IsPrimitive || fieldType.IsEnum || fieldType.IsPointer || fieldType.IsFunctionPointer) + { + TypeFlags category = fieldType.UnderlyingType.Category; + return category != TypeFlags.Single && category != TypeFlags.Double; + } + + return false; + } + private struct OverlappingFieldTracker { private BitArray _usedBytes; diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs index 7ec96db0eb5fbb..473a4d9f3f15aa 100644 --- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs +++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs @@ -3,7 +3,6 @@ using System; -using Internal.Text; using Internal.TypeSystem; using Debug = System.Diagnostics.Debug; @@ -32,51 +31,8 @@ public static MethodIL EmitIL(MethodDesc method) bool result; if (method.Name == "IsBitwiseEquatable"u8) { - // Ideally we could detect automatically whether a type is trivially equatable - // (i.e., its operator == could be implemented via memcmp). But for now we'll - // do the simple thing and hardcode the list of types we know fulfill this contract. - // n.b. This doesn't imply that the type's CompareTo method can be memcmp-implemented, - // as a method like CompareTo may need to take a type's signedness into account. - switch (elementType.UnderlyingType.Category) - { - case TypeFlags.Boolean: - case TypeFlags.Byte: - case TypeFlags.SByte: - case TypeFlags.Char: - case TypeFlags.UInt16: - case TypeFlags.Int16: - case TypeFlags.UInt32: - case TypeFlags.Int32: - case TypeFlags.UInt64: - case TypeFlags.Int64: - case TypeFlags.IntPtr: - case TypeFlags.UIntPtr: - result = true; - break; - default: - result = false; - if (elementType is MetadataType mdType) - { - if (IsKnownBitwiseEquatableType(mdType)) - { - result = true; - } - else if (mdType.IsValueType) - { - bool? equatable = ComparerIntrinsics.ImplementsIEquatable(mdType.GetTypeDefinition()); - - if (equatable.HasValue && !equatable.Value) - { - // Value type that can use memcmp and that doesn't override object.Equals or implement IEquatable.Equals. - MethodDesc objectEquals = mdType.Context.GetWellKnownType(WellKnownType.Object).GetMethod("Equals"u8, null); - result = - mdType.FindVirtualFunctionTargetMethodOnObjectType(objectEquals).OwningType != mdType && - ComparerIntrinsics.CanCompareValueTypeBits(mdType, objectEquals); - } - } - } - break; - } + // The runtime and the ILC share a single determination of what is bitwise-equatable. + result = ComparerIntrinsics.IsBitwiseEquatable(elementType); } else { @@ -87,21 +43,5 @@ public static MethodIL EmitIL(MethodDesc method) return new ILStubMethodIL(method, new byte[] { (byte)opcode, (byte)ILOpcode.ret }, Array.Empty(), Array.Empty()); } - - private static bool IsKnownBitwiseEquatableType(MetadataType type) - { - if (type.Module != type.Context.SystemModule) - { - return false; - } - - Utf8Span ns = type.Namespace; - if (ns == "System"u8) - { - Utf8Span name = type.Name; - return name == "Guid"u8 || name == "Int128"u8 || name == "UInt128"u8; - } - return ns == "System.Text"u8 && type.Name == "Rune"u8; - } } } diff --git a/src/coreclr/vm/comutilnative.cpp b/src/coreclr/vm/comutilnative.cpp index 4412dfaec4eadf..c017d433b02f3d 100644 --- a/src/coreclr/vm/comutilnative.cpp +++ b/src/coreclr/vm/comutilnative.cpp @@ -1775,9 +1775,17 @@ BOOL CanCompareBitsOrUseFastGetHashCode(MethodTable* mt) return mt->CanCompareBitsOrUseFastGetHashCode(); } + if (mt->GetClass()->IsInlineArray()) + { + // Inline arrays must always throw from ValueType.Equals/GetHashCode, which only happens on the + // QCALL entry point. Return false without caching so the managed fast path keeps routing there + // instead of reading a cached 'false' (e.g. primed by an enclosing type's field recursion) that + // would silently skip the throw. + return FALSE; + } + if (mt->ContainsGCPointers() - || mt->IsNotTightlyPacked() - || mt->GetClass()->IsInlineArray()) + || !mt->IsTightlyPacked()) { mt->SetHasCheckedCanCompareBitsOrUseFastGetHashCode(); return FALSE; diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 7020945877cc67..c5ae735c51fa92 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -273,8 +273,6 @@ DEFINE_FIELD(ENC_HELPER, OBJECT_REFERENCE, _objectReference) DEFINE_CLASS(ENCODING, Text, Encoding) -DEFINE_CLASS(RUNE, Text, Rune) - DEFINE_CLASS(ENUM, System, Enum) DEFINE_CLASS(ENVIRONMENT, System, Environment) @@ -1301,6 +1299,7 @@ DEFINE_CLASS(ICOMPARABLEGENERIC, System, IComparable`1) DEFINE_METHOD(ICOMPARABLEGENERIC, COMPARE_TO, CompareTo, NoSig) DEFINE_CLASS(IEQUATABLEGENERIC, System, IEquatable`1) +DEFINE_METHOD(IEQUATABLEGENERIC, EQUALS, Equals, NoSig) DEFINE_CLASS_U(Reflection, LoaderAllocator, LoaderAllocatorObject) DEFINE_FIELD_U(m_slots, LoaderAllocatorObject, m_pSlots) @@ -1330,6 +1329,7 @@ DEFINE_METHOD(UTF8BUFFERMARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, NoSig) // Classes referenced in EqualityComparer.Default optimization +DEFINE_CLASS(EQUALITY_COMPARER, CollectionsGeneric, EqualityComparer`1) DEFINE_CLASS(STRING_EQUALITYCOMPARER, CollectionsGeneric, StringEqualityComparer) DEFINE_CLASS(ENUM_EQUALITYCOMPARER, CollectionsGeneric, EnumEqualityComparer`1) DEFINE_CLASS(NULLABLE_EQUALITYCOMPARER, CollectionsGeneric, NullableEqualityComparer`1) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index b382c42437cda2..7d359485ef808a 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -7287,29 +7287,609 @@ static bool getILIntrinsicImplementationForInterlocked(MethodDesc * ftn, return true; } -bool IsBitwiseEquatable(TypeHandle typeHandle, MethodTable * methodTable) +namespace { - if (!methodTable->IsValueType() || - !CanCompareBitsOrUseFastGetHashCode(methodTable)) + mdToken ReadILToken(const BYTE* pIL) + { + LIMITED_METHOD_CONTRACT; + return (mdToken)((uint32_t)pIL[0] | ((uint32_t)pIL[1] << 8) | ((uint32_t)pIL[2] << 16) | ((uint32_t)pIL[3] << 24)); + } + + // Reads a conditional branch that Roslyn emits in either short form (1-byte signed offset) or long + // form (4-byte signed offset); a body larger than a signed-byte range forces the long form (e.g. the + // field-wise Equals of a type with many fields). On a match, sets 'target' to the absolute + // destination, advances 'ip' past the instruction, and returns true. + bool TryReadBranch(const BYTE* pIL, unsigned codeSize, unsigned& ip, BYTE shortOp, BYTE longOp, int& target) { + LIMITED_METHOD_CONTRACT; + + if (ip < codeSize && pIL[ip] == shortOp) + { + if (ip + 2 > codeSize) + return false; + target = (int)(ip + 2) + (int)(signed char)pIL[ip + 1]; + ip += 2; + return true; + } + + if (ip < codeSize && pIL[ip] == longOp) + { + if (ip + 5 > codeSize) + return false; + target = (int)(ip + 5) + (int)ReadILToken(pIL + ip + 1); + ip += 5; + return true; + } + return false; } - // CanCompareBitsOrUseFastGetHashCode checks for an object.Equals override. - // We also need to check for an IEquatable implementation. - Instantiation inst(&typeHandle, 1); - if (typeHandle.CanCastTo(TypeHandle(CoreLibBinder::GetClass(CLASS__IEQUATABLEGENERIC)).Instantiate(inst))) + // Resolves a field token (FieldDef, or a MemberRef over a generic TypeSpec) using 'pContext'. Returns + // NULL for any other kind or on failure. + FieldDesc* TryResolveFieldToken(Module* pModule, mdToken token, const SigTypeContext* pContext) + { + STANDARD_VM_CONTRACT; + + mdToken kind = TypeFromToken(token); + if (kind != mdtFieldDef && kind != mdtMemberRef) + return NULL; + + FieldDesc* pField = NULL; + + EX_TRY + { + pField = MemberLoader::GetFieldDescFromMemberDefOrRef( + pModule, token, pContext, FALSE /* strictMetadataChecks */); + } + EX_CATCH + { + pField = NULL; + RethrowTerminalExceptions(); + } + EX_END_CATCH + + return pField; + } + + // Resolves a method token, including a cross-module MemberRef (a primitive's Equals lives in + // CoreLib) or a MethodSpec/MemberRef that mentions the enclosing instantiation. 'pContext' supplies + // that instantiation so tokens over a generic parameter resolve to the concrete argument. Returns + // NULL if the token kind is unexpected or resolution fails. + MethodDesc* TryResolveMethodToken(Module* pModule, mdToken token, const SigTypeContext* pContext) + { + STANDARD_VM_CONTRACT; + + mdToken kind = TypeFromToken(token); + if (kind != mdtMethodDef && kind != mdtMemberRef && kind != mdtMethodSpec) + return NULL; + + MethodDesc* pMD = NULL; + + EX_TRY + { + pMD = MemberLoader::GetMethodDescFromMemberDefOrRefOrSpec( + pModule, token, pContext, FALSE /* strictMetadataChecks */, FALSE /* allowInstParam */); + } + EX_CATCH + { + pMD = NULL; + RethrowTerminalExceptions(); + } + EX_END_CATCH + + return pMD; + } + + // Resolves a type token (TypeDef/TypeRef/TypeSpec) in 'pContext', or the null handle if the kind is + // unexpected or resolution fails. A generic type appears as a TypeSpec, so a raw token compare is not + // enough. + TypeHandle TryResolveTypeToken(Module* pModule, mdToken token, const SigTypeContext* pContext) + { + STANDARD_VM_CONTRACT; + + mdToken kind = TypeFromToken(token); + if (kind != mdtTypeDef && kind != mdtTypeRef && kind != mdtTypeSpec) + return TypeHandle(); + + TypeHandle th; + + EX_TRY + { + th = ClassLoader::LoadTypeDefOrRefOrSpecThrowing( + pModule, token, pContext, ClassLoader::ReturnNullIfNotFound, ClassLoader::FailIfUninstDefOrRef); + } + EX_CATCH + { + th = TypeHandle(); + RethrowTerminalExceptions(); + } + EX_END_CATCH + + return th; + } + + // Unwraps an unboxing stub (interface dispatch on a value type) to the instance method that has IL. + MethodDesc* UnwrapStub(MethodDesc* pMD) + { + WRAPPER_NO_CONTRACT; + if (pMD != NULL && pMD->IsWrapperStub()) + return pMD->GetWrappedMethodDesc(); + return pMD; + } + + // Resolves 'mt's IEquatable.Equals implementation (unboxing stub unwrapped), or NULL if 'mt' + // is not a value type that implements IEquatable of self. + MethodDesc* GetIEquatableEqualsImpl(MethodTable* mt) { + STANDARD_VM_CONTRACT; + + if (mt == NULL || !mt->IsValueType()) + return NULL; + + TypeHandle th(mt); + Instantiation inst(&th, 1); + TypeHandle iequatableOfSelf = TypeHandle(CoreLibBinder::GetClass(CLASS__IEQUATABLEGENERIC)).Instantiate(inst); + + if (!th.CanCastTo(iequatableOfSelf)) + return NULL; + + return UnwrapStub(mt->GetMethodDescForInterfaceMethod( + iequatableOfSelf, CoreLibBinder::GetMethod(METHOD__IEQUATABLEGENERIC__EQUALS), FALSE /* throwOnConflict */)); + } + + // Forward declaration: the field-wise scanner recurses into nested value-type fields. + // 'scannedMethods' collects every Equals body relied on so the caller can register a ReJIT dependency. + bool IsFieldwiseEqualsBitwiseEquivalent(MethodTable* valueTypeMT, MethodDesc* pEqualsMD, StackSArray& scannedMethods); + + // Integer-like primitives whose '==' and Equals are both a bit-for-bit compare. Float/double are + // excluded: neither form is a memcmp (for '==' NaN != NaN and +0.0 == -0.0; Equals treats all NaNs + // and both signed zeros as equal). + bool IsBitwiseComparablePrimitive(CorElementType et) + { + LIMITED_METHOD_CONTRACT; + switch (et) + { + case ELEMENT_TYPE_BOOLEAN: + case ELEMENT_TYPE_CHAR: + case ELEMENT_TYPE_I1: + case ELEMENT_TYPE_U1: + case ELEMENT_TYPE_I2: + case ELEMENT_TYPE_U2: + case ELEMENT_TYPE_I4: + case ELEMENT_TYPE_U4: + case ELEMENT_TYPE_I8: + case ELEMENT_TYPE_U8: + case ELEMENT_TYPE_I: + case ELEMENT_TYPE_U: + case ELEMENT_TYPE_PTR: + case ELEMENT_TYPE_FNPTR: + return true; + default: + return false; + } + } + + // A field is memcmp-comparable if it is a bit-comparable primitive or an enum (always integer-backed). + // 'fieldTh' is the field's exact type (resolved against the owning instantiation), so a generic field + // like 'T' is inspected as its concrete argument. + bool IsBitwiseComparableType(TypeHandle fieldTh) + { + STANDARD_VM_CONTRACT; + + if (fieldTh.IsNull()) + return false; + + CorElementType et = fieldTh.GetSignatureCorElementType(); + if (IsBitwiseComparablePrimitive(et)) + return true; + + if (et == ELEMENT_TYPE_VALUETYPE) + { + MethodTable* pFieldMT = fieldTh.GetMethodTable(); + if (pFieldMT != NULL && pFieldMT->IsEnum()) + return IsBitwiseComparablePrimitive(pFieldMT->GetInternalCorElementType()); + } + return false; } - return true; + // Accepts a primitive field compared via 'x.Equals(y)' instead of 'x == y'; for these integer-like + // types both lower to the same bit-for-bit compare. + bool IsPrimitiveEqualsCall(MethodDesc* pCallee, TypeHandle fieldTh) + { + STANDARD_VM_CONTRACT; + + if (pCallee == NULL || !IsBitwiseComparableType(fieldTh)) + return false; + + MethodDesc* pFieldEquals = GetIEquatableEqualsImpl(fieldTh.GetMethodTable()); + return pFieldEquals != NULL && pFieldEquals == UnwrapStub(pCallee); + } + + // Accepts a nested value-type field compared through its own IEquatable.Equals, but only when + // that Equals is itself a provable field-wise compare (its layout is covered by the recursion). + bool IsNestedFieldwiseEquatable(MethodDesc* pCallee, TypeHandle fieldTh, StackSArray& scannedMethods) + { + STANDARD_VM_CONTRACT; + + if (pCallee == NULL || fieldTh.GetSignatureCorElementType() != ELEMENT_TYPE_VALUETYPE) + return false; + + MethodTable* pNestedMT = fieldTh.GetMethodTable(); + if (pNestedMT == NULL) + return false; + + MethodDesc* pNestedEquals = GetIEquatableEqualsImpl(pNestedMT); + if (pNestedEquals == NULL || pNestedEquals != UnwrapStub(pCallee)) + return false; + + return IsFieldwiseEqualsBitwiseEquivalent(pNestedMT, pNestedEquals, scannedMethods); + } + + // True if 'pMD' is 'EqualityComparer::name' with the expected static-ness. The lead-in reaches + // Default through this base type: 'get_Default' (static) and the abstract 'Equals' (instance). + bool IsEqualityComparerMethod(MethodDesc* pMD, TypeHandle fieldTh, const char* name, bool isStatic) + { + STANDARD_VM_CONTRACT; + + if (pMD == NULL) + return false; + + MethodTable* pMT = pMD->GetMethodTable(); + if (pMT == NULL || !pMT->HasSameTypeDefAs(CoreLibBinder::GetClass(CLASS__EQUALITY_COMPARER))) + return false; + + Instantiation inst = pMT->GetInstantiation(); + if (inst.GetNumArgs() != 1 || inst[0] != fieldTh) + return false; + + return (pMD->IsStatic() != FALSE) == isStatic && strcmp(pMD->GetName(), name) == 0; + } + + // Accepts a field compared via 'EqualityComparer.Default.Equals(this.F, other.F)', but only when + // Default.Equals is itself a memcmp: F must be a bit-comparable primitive or a nested value type that + // is itself provably field-wise. 'fieldTh' is the field's exact type. + bool IsEqualityComparerDefaultEquals(MethodDesc* pGetDefault, MethodDesc* pEquals, TypeHandle fieldTh, StackSArray& scannedMethods) + { + STANDARD_VM_CONTRACT; + + if (!IsEqualityComparerMethod(pGetDefault, fieldTh, "get_Default", true /* isStatic */) || + !IsEqualityComparerMethod(pEquals, fieldTh, "Equals", false /* isStatic */)) + { + return false; + } + + if (IsBitwiseComparableType(fieldTh)) + return true; + + if (fieldTh.GetSignatureCorElementType() != ELEMENT_TYPE_VALUETYPE) + return false; + + MethodTable* pNestedMT = fieldTh.GetMethodTable(); + MethodDesc* pNestedEquals = GetIEquatableEqualsImpl(pNestedMT); + return pNestedEquals != NULL && IsFieldwiseEqualsBitwiseEquivalent(pNestedMT, pNestedEquals, scannedMethods); + } + + // Returns true only if 'pEqualsMD' compares every instance field of 'valueTypeMT' exactly once and + // ANDs the results, bit-for-bit like memcmp. Combined with the caller's 'tightly packed' guarantee, + // that makes the whole comparison a memcmp. + // + // The C# compiler lowers 'this.f0 == other.f0 && ...' to per-field units sharing one 'return false' + // tail. Operands are always arg0/arg1. A primitive is compared inline; a nested value type through + // its own IEquatable.Equals; a field may also go through EqualityComparer.Default.Equals. Every + // call-form callee must itself be field-wise (checked recursively): + // + // primitive, non-final: ldarg.0; ldfld F; ldarg.1; ldfld F; bne.un.s FALSE + // primitive, final: ldarg.0; ldfld F; ldarg.1; ldfld F; ceq; ret + // nested, non-final: ldarg.0; ldflda F; ldarg.1; ldfld F; call F::Equals; brfalse.s FALSE + // nested, final: ldarg.0; ldflda F; ldarg.1; ldfld F; call F::Equals; ret + // eqcmp, non-final: call EqualityComparer::get_Default; ldarg.0; ldfld F; ldarg.1; ldfld F; callvirt Equals; brfalse.s FALSE + // eqcmp, final: call EqualityComparer::get_Default; ldarg.0; ldfld F; ldarg.1; ldfld F; callvirt Equals; ret + // shared tail: FALSE: ldc.i4.0; ret + bool ScanFieldwiseEqualsBody(MethodDesc* pEqualsMD, MethodTable* valueTypeMT, StackSArray& scannedMethods) + { + STANDARD_VM_CONTRACT; + + if (!pEqualsMD->MayHaveILHeader()) + return false; + + COR_ILMETHOD* pILMethod = pEqualsMD->GetILHeader(); + if (pILMethod == NULL) + return false; + + COR_ILMETHOD_DECODER header(pILMethod); + const BYTE* pIL = header.Code; + if (pIL == NULL) + return false; + + const unsigned codeSize = header.GetCodeSize(); + Module* pModule = pEqualsMD->GetModule(); + TypeHandle valueTypeTh(valueTypeMT); + SigTypeContext sigTypeContext(valueTypeTh); + + // Track which instance fields have been compared so we can require full coverage. + const DWORD fieldCount = valueTypeMT->GetNumInstanceFields(); + if (fieldCount == 0) + return false; + + NewArrayHolder compared(new FieldDesc*[fieldCount]); + DWORD numCompared = 0; + + unsigned ip = 0; + int falseTarget = -1; // shared 'return false' offset, discovered from the first branch + bool sawFinalUnit = false; + + while (!sawFinalUnit) + { + // Optional EqualityComparer.Default lead-in: 'call EqualityComparer::get_Default' before + // the operands. + mdToken getDefaultTok = mdTokenNil; + if (ip + 5 <= codeSize && pIL[ip] == CEE_CALL) + { + getDefaultTok = ReadILToken(pIL + ip + 1); + ip += 5; + } + + // Left operand: ldarg.0; ldfld/ldflda F. The EqualityComparer and inline '==' forms load by + // value; the '.Equals' call form loads by address. + if (ip + 6 > codeSize || pIL[ip] != CEE_LDARG_0) + return false; + BYTE leftLoad = pIL[ip + 1]; + if (leftLoad != CEE_LDFLD && leftLoad != CEE_LDFLDA) + return false; + if (getDefaultTok != mdTokenNil && leftLoad != CEE_LDFLD) + return false; + mdToken leftFieldTok = ReadILToken(pIL + ip + 2); + ip += 6; + + // Right operand: ldarg.1; ldfld F. + if (ip + 6 > codeSize || pIL[ip] != CEE_LDARG_1 || pIL[ip + 1] != CEE_LDFLD) + return false; + mdToken rightFieldTok = ReadILToken(pIL + ip + 2); + ip += 6; + + if (leftFieldTok != rightFieldTok) + return false; + + FieldDesc* pField = TryResolveFieldToken(pModule, leftFieldTok, &sigTypeContext); + // The resolved field's enclosing MT is the open/approx definition, so match by type-def + // rather than an exact MT compare, which would fail for an instantiation. + if (pField == NULL || + pField->IsStatic() || + !pField->GetApproxEnclosingMethodTable()->HasSameTypeDefAs(valueTypeMT)) + { + return false; + } + + // Each field must be compared exactly once. + for (DWORD i = 0; i < numCompared; i++) + { + if (compared[i] == pField) + return false; + } + + if (numCompared >= fieldCount) + return false; + compared[numCompared++] = pField; + + // The field's exact type in this instantiation (e.g. 'T' -> its concrete argument). + TypeHandle fieldTh = pField->GetExactFieldType(valueTypeTh); + + if (getDefaultTok == mdTokenNil && leftLoad == CEE_LDFLD) + { + // Inline '==': only integer-like primitives (and enums) are memcmp-equivalent. + if (!IsBitwiseComparableType(fieldTh)) + return false; + + int target; + if (TryReadBranch(pIL, codeSize, ip, CEE_BNE_UN_S, CEE_BNE_UN, target)) + { + // Non-final field: branch to the shared 'return false'. + if (falseTarget == -1) + falseTarget = target; + else if (falseTarget != target) + return false; + } + else if (ip + 3 <= codeSize && pIL[ip] == CEE_PREFIX1 && pIL[ip + 1] == (CEE_CEQ & 0xFF) && pIL[ip + 2] == CEE_RET) + { + // Final field: ceq; ret. + ip += 3; + sawFinalUnit = true; + } + else + { + return false; + } + + continue; + } + + if (getDefaultTok != mdTokenNil) + { + // callvirt EqualityComparer::Equals(!0, !0). + if (ip + 5 > codeSize || pIL[ip] != CEE_CALLVIRT) + return false; + MethodDesc* pGetDefault = TryResolveMethodToken(pModule, getDefaultTok, &sigTypeContext); + MethodDesc* pEquals = TryResolveMethodToken(pModule, ReadILToken(pIL + ip + 1), &sigTypeContext); + if (!IsEqualityComparerDefaultEquals(pGetDefault, pEquals, fieldTh, scannedMethods)) + return false; + } + else + { + // '.Equals' call form: a primitive's own Equals, or a nested type's field-wise Equals. + if (ip + 5 > codeSize || pIL[ip] != CEE_CALL) + return false; + MethodDesc* pCallee = TryResolveMethodToken(pModule, ReadILToken(pIL + ip + 1), &sigTypeContext); + if (!IsPrimitiveEqualsCall(pCallee, fieldTh) && !IsNestedFieldwiseEquatable(pCallee, fieldTh, scannedMethods)) + return false; + } + ip += 5; + + // The Equals call already yields a bool: brfalse to the shared tail, or ret if final. + int target; + if (TryReadBranch(pIL, codeSize, ip, CEE_BRFALSE_S, CEE_BRFALSE, target)) + { + if (falseTarget == -1) + falseTarget = target; + else if (falseTarget != target) + return false; + } + else if (ip < codeSize && pIL[ip] == CEE_RET) + { + ip += 1; + sawFinalUnit = true; + } + else + { + return false; + } + } + + // Any branches must target the shared 'ldc.i4.0; ret' tail; a single-field compare has none. + if (falseTarget != -1) + { + if ((int)ip != falseTarget || + ip + 2 != codeSize || + pIL[ip] != CEE_LDC_I4_0 || + pIL[ip + 1] != CEE_RET) + { + return false; + } + } + else if (ip != codeSize) + { + return false; + } + + // Every instance field must have been compared. + return numCompared == fieldCount; + } + + // Determines whether 'valueTypeMT's IEquatable.Equals implementation is a plain field-wise + // comparison that is equivalent to memcmp. 'pEqualsMD' is that Equals method. Every Equals body the + // decision relies on is appended to 'scannedMethods' so the caller can register a ReJIT dependency. + bool IsFieldwiseEqualsBitwiseEquivalent(MethodTable* valueTypeMT, MethodDesc* pEqualsMD, StackSArray& scannedMethods) + { + STANDARD_VM_CONTRACT; + + // EnC can replace the Equals IL after the fold, so don't trust the scan for an editable module. + if (pEqualsMD->GetModule()->IsEditAndContinueEnabled()) + { + return false; + } + + // First-pass restrictions that keep the scan simple and unquestionably safe: the type must be + // unmanaged (so a byte-wise compare is meaningful), tightly packed (no padding anywhere -- the + // flag is transitive -- else memcmp inspects bytes Equals ignores), and not an inline array. + if (valueTypeMT->ContainsGCPointers() || + !valueTypeMT->IsTightlyPacked() || + valueTypeMT->GetClass()->IsInlineArray()) + { + return false; + } + + // Follow the extremely common 'Equals(T other) => this == other' forward into op_Equality: + // ldarg.0; ldobj T; ldarg.1; call op_Equality; ret + MethodDesc* pScanMD = pEqualsMD; + if (pEqualsMD->MayHaveILHeader()) + { + COR_ILMETHOD* pILMethod = pEqualsMD->GetILHeader(); + if (pILMethod != NULL) + { + COR_ILMETHOD_DECODER header(pILMethod); + const BYTE* pIL = header.Code; + const unsigned codeSize = header.GetCodeSize(); + Module* pModule = pEqualsMD->GetModule(); + TypeHandle valueTypeTh(valueTypeMT); + SigTypeContext sigTypeContext(valueTypeTh); + + // 02 71 03 28 2A. The 'ldobj' operand is a TypeSpec for a generic + // type, so resolve it in context rather than comparing the raw token. + if (pIL != NULL && codeSize == 13 && + pIL[0] == CEE_LDARG_0 && + pIL[1] == CEE_LDOBJ && + pIL[6] == CEE_LDARG_1 && + pIL[7] == CEE_CALL && + pIL[12] == CEE_RET && + TryResolveTypeToken(pModule, ReadILToken(pIL + 2), &sigTypeContext).GetMethodTable() == valueTypeMT) + { + MethodDesc* pOpEquality = TryResolveMethodToken(pModule, ReadILToken(pIL + 8), &sigTypeContext); + if (pOpEquality != NULL && + pOpEquality->IsStatic() && + pOpEquality->GetMethodTable() == valueTypeMT && + strcmp(pOpEquality->GetName(), "op_Equality") == 0) + { + pScanMD = pOpEquality; + } + } + } + } + + if (!ScanFieldwiseEqualsBody(pScanMD, valueTypeMT, scannedMethods)) + return false; + + // Record every body the fold relied on (the forwarder and the scanned op_Equality). + scannedMethods.Append(pEqualsMD); + if (pScanMD != pEqualsMD) + scannedMethods.Append(pScanMD); + return true; + } } +bool IsBitwiseEquatable(TypeHandle typeHandle, MethodTable * methodTable, StackSArray& scannedMethods) +{ + STANDARD_VM_CONTRACT; + + if (!methodTable->IsValueType()) + { + return false; + } + + // Scanning resolves field/method tokens and can force type loads, any of which may throw on bad or + // incomplete metadata. Constant folding must be conservative, so trap and fold to 'false' on failure. + bool result = false; + EX_TRY + { + Instantiation inst(&typeHandle, 1); + TypeHandle iequatableOfSelf = TypeHandle(CoreLibBinder::GetClass(CLASS__IEQUATABLEGENERIC)).Instantiate(inst); + + if (!typeHandle.CanCastTo(iequatableOfSelf)) + { + // No IEquatable of its own: bitwise equality is safe if the fields are bit-comparable and + // there is no custom object.Equals override. + result = CanCompareBitsOrUseFastGetHashCode(methodTable); + } + else + { + // Has IEquatable.Equals: bitwise only if that Equals is a plain field-wise memcmp equivalent. + // UnwrapStub turns the value-type interface dispatch into the underlying instance method. + MethodDesc* pEqualsMD = UnwrapStub(methodTable->GetMethodDescForInterfaceMethod( + iequatableOfSelf, CoreLibBinder::GetMethod(METHOD__IEQUATABLEGENERIC__EQUALS), FALSE /* throwOnConflict */)); + if (pEqualsMD != NULL) + { + result = IsFieldwiseEqualsBitwiseEquivalent(methodTable, pEqualsMD, scannedMethods); + } + } + } + EX_CATCH + { + result = false; + RethrowTerminalExceptions(); + } + EX_END_CATCH + + return result; +} + +#if defined FEATURE_REJIT && !defined(DACCESS_COMPILE) +static void TrackInliningForRejit(MethodDesc* pCaller, MethodDesc* pCallee); +#endif // defined FEATURE_REJIT && !defined(DACCESS_COMPILE) + static bool getILIntrinsicImplementationForRuntimeHelpers( MethodInfoWorkerContext& cxt, CORINFO_METHOD_INFO* methInfo, - SigPointer* localSig) + SigPointer* localSig, + MethodDesc* pMethodBeingCompiled) { STANDARD_VM_CONTRACT; @@ -7332,31 +7912,32 @@ static bool getILIntrinsicImplementationForRuntimeHelpers( // Ideally we could detect automatically whether a type is trivially equatable // (i.e., its operator == could be implemented via memcmp). The best we can do - // for now is hardcode a list of known supported types and then also include anything - // that doesn't provide its own object.Equals override / IEquatable implementation. + // for now is check a few known-good shapes and then also include anything the + // field-wise scanner proves memcmp-equivalent. // n.b. This doesn't imply that the type's CompareTo method can be memcmp-implemented, // as a method like CompareTo may need to take a type's signedness into account. - - if (methodTable == CoreLibBinder::GetClass(CLASS__BOOLEAN) - || methodTable == CoreLibBinder::GetClass(CLASS__BYTE) - || methodTable == CoreLibBinder::GetClass(CLASS__SBYTE) - || methodTable == CoreLibBinder::GetClass(CLASS__CHAR) - || methodTable == CoreLibBinder::GetClass(CLASS__INT16) - || methodTable == CoreLibBinder::GetClass(CLASS__UINT16) - || methodTable == CoreLibBinder::GetClass(CLASS__INT32) - || methodTable == CoreLibBinder::GetClass(CLASS__UINT32) - || methodTable == CoreLibBinder::GetClass(CLASS__INT64) - || methodTable == CoreLibBinder::GetClass(CLASS__UINT64) - || methodTable == CoreLibBinder::GetClass(CLASS__INT128) - || methodTable == CoreLibBinder::GetClass(CLASS__UINT128) - || methodTable == CoreLibBinder::GetClass(CLASS__INTPTR) - || methodTable == CoreLibBinder::GetClass(CLASS__UINTPTR) - || methodTable == CoreLibBinder::GetClass(CLASS__GUID) - || methodTable == CoreLibBinder::GetClass(CLASS__RUNE) - || methodTable->IsEnum() - || IsBitwiseEquatable(typeHandle, methodTable)) + // + // Integer-like primitives, native ints, and enums are memcmp-comparable but their Equals + // isn't field-wise (one side is the raw primitive arg), so they're matched by element type. + // Everything else -- including Guid, Rune, Int128, and UInt128 -- is proven by IsBitwiseEquatable, + // which scans the type's IEquatable.Equals for a field-wise shape. + StackSArray scannedMethods; + if (IsBitwiseComparablePrimitive(methodTable->GetInternalCorElementType()) + || IsBitwiseEquatable(typeHandle, methodTable, scannedMethods)) { methInfo->ILCode = const_cast(returnTrue); + +#if defined FEATURE_REJIT && !defined(DACCESS_COMPILE) + // The fold "inlined" each scanned Equals body, so a profiler ReJIT of one must rejit the + // method the fold is baked into. No bodies are recorded for the primitive/element-type path. + if (pMethodBeingCompiled != NULL) + { + for (COUNT_T i = 0; i < scannedMethods.GetCount(); i++) + { + TrackInliningForRejit(pMethodBeingCompiled, scannedMethods[i]); + } + } +#endif // defined FEATURE_REJIT && !defined(DACCESS_COMPILE) } else { @@ -7561,7 +8142,7 @@ COR_ILMETHOD_DECODER* CEEInfo::getMethodInfoWorker( } else if (CoreLibBinder::IsClass(pMT, CLASS__RUNTIME_HELPERS)) { - fILIntrinsic = getILIntrinsicImplementationForRuntimeHelpers(cxt, methInfo, &localSig); + fILIntrinsic = getILIntrinsicImplementationForRuntimeHelpers(cxt, methInfo, &localSig, m_pMethodBeingCompiled); } else if (CoreLibBinder::IsClass(pMT, CLASS__ACTIVATOR)) { @@ -8041,6 +8622,49 @@ void CEEInfo::beginInlining(CORINFO_METHOD_HANDLE inlinerHnd, // do nothing } +#if defined FEATURE_REJIT && !defined(DACCESS_COMPILE) +// Records that 'pCaller' incorporated 'pCallee's IL -- a real inline, or the field-wise Equals scan that +// folds RuntimeHelpers.IsBitwiseEquatable -- so that a profiler ReJIT of 'pCallee' also rejits 'pCaller'. +static void TrackInliningForRejit(MethodDesc* pCaller, MethodDesc* pCallee) +{ + STANDARD_VM_CONTRACT; + + pCallee->GetModule()->AddInlining(pCaller, pCallee); + + if (CORProfilerEnableRejit()) + { + ModuleID modId = 0; + mdMethodDef methodDef = mdMethodDefNil; + BOOL shouldCallReJIT = FALSE; + + { + // If ReJIT is enabled, there is a chance that a race happened where the profiler + // requested a ReJIT on a method, but before the ReJIT occurred an inlining happened. + // If we end up reporting an inlining on a method with non-default IL it means the race + // happened and we need to manually request ReJIT for it since it was missed. + CodeVersionManager* pCodeVersionManager = pCallee->GetCodeVersionManager(); + CodeVersionManager::LockHolder codeVersioningLockHolder; + ILCodeVersion ilVersion = pCodeVersionManager->GetActiveILCodeVersion(pCallee); + if (ilVersion.GetRejitState() != RejitFlags::kStateActive || !ilVersion.HasDefaultIL()) + { + shouldCallReJIT = TRUE; + modId = reinterpret_cast(pCaller->GetModule()); + methodDef = pCaller->GetMemberDef(); + // Do Not call RequestReJIT inside this scope, calling RequestReJIT while holding the CodeVersionManager lock + // will cause deadlocks with other threads calling RequestReJIT since it tries to obtain the CodeVersionManager lock + } + } + + if (shouldCallReJIT) + { + _ASSERTE(modId != 0); + _ASSERTE(methodDef != mdMethodDefNil); + ReJitManager::RequestReJIT(1, &modId, &methodDef, static_cast(0)); + } + } +} +#endif // defined FEATURE_REJIT && !defined(DACCESS_COMPILE) + void CEEInfo::reportInliningDecision (CORINFO_METHOD_HANDLE inlinerHnd, CORINFO_METHOD_HANDLE inlineeHnd, CorInfoInline inlineResult, @@ -8166,41 +8790,7 @@ void CEEInfo::reportInliningDecision (CORINFO_METHOD_HANDLE inlinerHnd, { // We don't want to track the chain of methods, so intentionally use m_pMethodBeingCompiled // to just track the methods that pCallee is eventually inlined in - MethodDesc *pCallee = GetMethod(inlineeHnd); - MethodDesc *pCaller = m_pMethodBeingCompiled; - pCallee->GetModule()->AddInlining(pCaller, pCallee); - - if (CORProfilerEnableRejit()) - { - ModuleID modId = 0; - mdMethodDef methodDef = mdMethodDefNil; - BOOL shouldCallReJIT = FALSE; - - { - // If ReJIT is enabled, there is a chance that a race happened where the profiler - // requested a ReJIT on a method, but before the ReJIT occurred an inlining happened. - // If we end up reporting an inlining on a method with non-default IL it means the race - // happened and we need to manually request ReJIT for it since it was missed. - CodeVersionManager* pCodeVersionManager = pCallee->GetCodeVersionManager(); - CodeVersionManager::LockHolder codeVersioningLockHolder; - ILCodeVersion ilVersion = pCodeVersionManager->GetActiveILCodeVersion(pCallee); - if (ilVersion.GetRejitState() != RejitFlags::kStateActive || !ilVersion.HasDefaultIL()) - { - shouldCallReJIT = TRUE; - modId = reinterpret_cast(pCaller->GetModule()); - methodDef = pCaller->GetMemberDef(); - // Do Not call RequestReJIT inside this scope, calling RequestReJIT while holding the CodeVersionManager lock - // will cause deadlocks with other threads calling RequestReJIT since it tries to obtain the CodeVersionManager lock - } - } - - if (shouldCallReJIT) - { - _ASSERTE(modId != 0); - _ASSERTE(methodDef != mdMethodDefNil); - ReJitManager::RequestReJIT(1, &modId, &methodDef, static_cast(0)); - } - } + TrackInliningForRejit(m_pMethodBeingCompiled, GetMethod(inlineeHnd)); } #endif // defined FEATURE_REJIT && !defined(DACCESS_COMPILE) diff --git a/src/coreclr/vm/methodtable.h b/src/coreclr/vm/methodtable.h index ba51bfaa9ae311..23f4d141dfe632 100644 --- a/src/coreclr/vm/methodtable.h +++ b/src/coreclr/vm/methodtable.h @@ -1996,7 +1996,7 @@ class MethodTable OBJECTHANDLE GetLoaderAllocatorObjectHandle(); NOINLINE BYTE *GetLoaderAllocatorObjectForGC(); - BOOL IsNotTightlyPacked(); + BOOL IsTightlyPacked(); BOOL IsAllGCPointers(); diff --git a/src/coreclr/vm/methodtable.inl b/src/coreclr/vm/methodtable.inl index 5bd6ddf711b680..c735a73166111e 100644 --- a/src/coreclr/vm/methodtable.inl +++ b/src/coreclr/vm/methodtable.inl @@ -230,10 +230,10 @@ inline DWORD MethodTable::GetAttrClass() } //========================================================================================== -inline BOOL MethodTable::IsNotTightlyPacked() +inline BOOL MethodTable::IsTightlyPacked() { WRAPPER_NO_CONTRACT; - return GetClass()->IsNotTightlyPacked(); + return !GetClass()->IsNotTightlyPacked(); } //========================================================================================== diff --git a/src/coreclr/vm/methodtablebuilder.cpp b/src/coreclr/vm/methodtablebuilder.cpp index c9984deef79a1b..1ca9cbeef87ce8 100644 --- a/src/coreclr/vm/methodtablebuilder.cpp +++ b/src/coreclr/vm/methodtablebuilder.cpp @@ -1822,7 +1822,8 @@ MethodTableBuilder::BuildMethodTableThrowing( BuildMethodTableThrowException(IDS_CLASSLOAD_FIELDTOOLARGE); } - if (CheckIfSIMDAndUpdateSize()) + bool fIsSIMDType = CheckIfSIMDAndUpdateSize(); + if (fIsSIMDType) { totalDeclaredFieldSize = bmtFP->NumInstanceFieldBytes; } @@ -1866,7 +1867,28 @@ MethodTableBuilder::BuildMethodTableThrowing( if (IsValueClass()) { - if (bmtFP->NumInstanceFieldBytes != totalDeclaredFieldSize || HasOverlaidField()) + // A value type is "tightly packed" when a byte-wise compare of two instances equals comparing + // every field, i.e. there is no padding anywhere. That needs (1) the declared fields to exactly + // cover the instance size with no gaps or overlap ('totalDeclaredFieldSize' sums each field's + // full size, so it catches gaps at this level) and (2) every nested value-type field to itself + // be tightly packed, else the compare reads padding inside it. Propagating (2) makes the flag + // transitive, so callers need not recurse. SIMD types are exempt (size treated as fully covered). + bool fIsNotTightlyPacked = (bmtFP->NumInstanceFieldBytes != totalDeclaredFieldSize) || HasOverlaidField(); + + if (!fIsNotTightlyPacked && !fIsSIMDType && pByValueClassCache != NULL) + { + for (DWORD i = 0; i < bmtEnumFields->dwNumInstanceFields; i++) + { + MethodTable* pFieldMT = pByValueClassCache[i]; + if (pFieldMT != NULL && !pFieldMT->IsTightlyPacked()) + { + fIsNotTightlyPacked = true; + break; + } + } + } + + if (fIsNotTightlyPacked) GetHalfBakedClass()->SetIsNotTightlyPacked(); #ifdef FEATURE_HFA @@ -4516,7 +4538,13 @@ VOID MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList, if (!fIsStatic) { pFD = &pFieldDescList[dwCurrentDeclaredField]; // lgtm [cpp/upcast-array-pointer-arithmetic] - The call of concern in FixupFieldDescForEnC, initializes this loop invariant to 1, so will never be > 1. - *totalDeclaredSize += (1 << dwLog2FieldSize); + + // Accumulate declared field sizes so the type can be flagged NotTightlyPacked when they + // don't exactly cover the instance size. A value-type field contributes its full instance + // size; dwLog2FieldSize is meaningless for value types (size isn't a power of two). + *totalDeclaredSize += fIsByValue + ? (*pByValueClassCache)[dwCurrentDeclaredField]->GetNumInstanceFieldBytes() + : (1 << dwLog2FieldSize); } else /* (dwMemberAttrs & mdStatic) */ { diff --git a/src/libraries/System.Private.CoreLib/src/System/Guid.cs b/src/libraries/System.Private.CoreLib/src/System/Guid.cs index e807450ceeb633..9a590e9fe7ae2a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Guid.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Guid.cs @@ -1081,28 +1081,13 @@ public override int GetHashCode() // Returns true if and only if the guid represented // by o is the same as this instance. - public override bool Equals([NotNullWhen(true)] object? o) => o is Guid g && EqualsCore(this, g); + public override bool Equals([NotNullWhen(true)] object? o) => o is Guid g && Equals(g); - public bool Equals(Guid g) => EqualsCore(this, g); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool EqualsCore(in Guid left, in Guid right) - { - if (Vector128.IsHardwareAccelerated) - { - return Unsafe.BitCast>(left) == Unsafe.BitCast>(right); - } - - ref int rA = ref Unsafe.AsRef(in left._a); - ref int rB = ref Unsafe.AsRef(in right._a); - - // Compare each element - - return rA == rB - && Unsafe.Add(ref rA, 1) == Unsafe.Add(ref rB, 1) - && Unsafe.Add(ref rA, 2) == Unsafe.Add(ref rB, 2) - && Unsafe.Add(ref rA, 3) == Unsafe.Add(ref rB, 3); - } + // Field-wise so the runtime can prove Guid is bitwise-equatable (see RuntimeHelpers.IsBitwiseEquatable). + // Equality funnels through Equals; == and != defer to it so this stays the single canonical comparison. + public bool Equals(Guid g) => + _a == g._a && _b == g._b && _c == g._c && _d == g._d && _e == g._e && _f == g._f && + _g == g._g && _h == g._h && _i == g._i && _j == g._j && _k == g._k; private static int GetResult(uint me, uint them) => me < them ? -1 : 1; @@ -1179,9 +1164,9 @@ public int CompareTo(Guid value) return 0; } - public static bool operator ==(Guid a, Guid b) => EqualsCore(a, b); + public static bool operator ==(Guid a, Guid b) => a.Equals(b); - public static bool operator !=(Guid a, Guid b) => !EqualsCore(a, b); + public static bool operator !=(Guid a, Guid b) => !a.Equals(b); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe int HexsToChars(TChar* guidChars, int a, int b) where TChar : unmanaged, IUtfChar diff --git a/src/libraries/System.Private.CoreLib/src/System/Int128.cs b/src/libraries/System.Private.CoreLib/src/System/Int128.cs index ab04caa25ecb47..85a9a1b6f9e394 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Int128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Int128.cs @@ -88,7 +88,7 @@ public override bool Equals([NotNullWhen(true)] object? obj) /// public bool Equals(Int128 other) { - return this == other; + return (_lower == other._lower) && (_upper == other._upper); } /// @@ -1088,10 +1088,10 @@ public static Int128 Log2(Int128 value) // /// - public static bool operator ==(Int128 left, Int128 right) => (left._lower == right._lower) && (left._upper == right._upper); + public static bool operator ==(Int128 left, Int128 right) => left.Equals(right); /// - public static bool operator !=(Int128 left, Int128 right) => (left._lower != right._lower) || (left._upper != right._upper); + public static bool operator !=(Int128 left, Int128 right) => !left.Equals(right); // // IIncrementOperators diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs index 61673fc1da8d51..0da17fbc04a60a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs @@ -131,9 +131,9 @@ private Rune(uint scalarValue, bool _) _value = scalarValue; } - public static bool operator ==(Rune left, Rune right) => left._value == right._value; + public static bool operator ==(Rune left, Rune right) => left.Equals(right); - public static bool operator !=(Rune left, Rune right) => left._value != right._value; + public static bool operator !=(Rune left, Rune right) => !left.Equals(right); public static bool operator <(Rune left, Rune right) => left._value < right._value; @@ -787,7 +787,7 @@ public int EncodeToUtf8(Span destination) public override bool Equals([NotNullWhen(true)] object? obj) => (obj is Rune other) && Equals(other); - public bool Equals(Rune other) => this == other; + public bool Equals(Rune other) => _value == other._value; /// /// Returns a value that indicates whether the current instance and a specified rune are equal using the specified comparison option. diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs index b66f13fc38bdea..81f37e0189cac5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs @@ -90,7 +90,7 @@ public override bool Equals([NotNullWhen(true)] object? obj) /// public bool Equals(UInt128 other) { - return this == other; + return (_lower == other._lower) && (_upper == other._upper); } /// @@ -1326,10 +1326,10 @@ public static UInt128 Log2(UInt128 value) // /// - public static bool operator ==(UInt128 left, UInt128 right) => (left._lower == right._lower) && (left._upper == right._upper); + public static bool operator ==(UInt128 left, UInt128 right) => left.Equals(right); /// - public static bool operator !=(UInt128 left, UInt128 right) => (left._lower != right._lower) || (left._upper != right._upper); + public static bool operator !=(UInt128 left, UInt128 right) => !left.Equals(right); // // IIncrementOperators diff --git a/src/tests/Loader/classloader/BitwiseEquatable/BitwiseEquatable.cs b/src/tests/Loader/classloader/BitwiseEquatable/BitwiseEquatable.cs new file mode 100644 index 00000000000000..5d3797ab6fdf63 --- /dev/null +++ b/src/tests/Loader/classloader/BitwiseEquatable/BitwiseEquatable.cs @@ -0,0 +1,327 @@ +// 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 System.Runtime.InteropServices; + +using Xunit; + +#pragma warning disable CS0649 // field is never assigned to + +namespace BitwiseEquatableTests +{ + public static class BitwiseEquatable + { + // Call the internal RuntimeHelpers.IsBitwiseEquatable directly via UnsafeAccessor so the + // intrinsic is exercised as the JIT/AOT compiler expands it, rather than through reflection + // (which wouldn't hit the intrinsic path and doesn't work under NativeAOT). + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "IsBitwiseEquatable")] + private static extern bool IsBitwiseEquatable([UnsafeAccessorType("System.Runtime.CompilerServices.RuntimeHelpers")] object o); + + private static void Check(bool expected) => Assert.Equal(expected, IsBitwiseEquatable(null!)); + + [Fact] + public static void IsBitwiseEquatable_MatchesExpected() + { + // Primitive: '==' and Equals lower to the same bit-for-bit compare. + Check(true); + // Int128/UInt128: field-wise IEquatable.Equals over two ulong halves. + Check(true); + Check(true); + // Guid is proven field-wise by the scanner (its Equals compares all 11 fields). GuidShape + // below covers the same many-field shape (which forces long-form branches) independently. + Check(true); + Check(true); + // Plain field-wise IEquatable.Equals. + Check(true); + Check(true); + Check(true); + // 'Equals(other) => this == other' forwarding into a field-wise op_Equality. + Check(true); + // Nested value-type fields compared through their own field-wise IEquatable.Equals. + Check(true); + Check(true); + Check(true); + // Nested type's Equals ignores a field, or is internally padded. + Check(false); + Check(false); + // No IEquatable at all: legacy path still accepts safe blittable fields. + Check(true); + // float/double are never bitwise (NaN and signed-zero semantics differ from memcmp). + Check(false); + // Equals ignores a field, does custom logic, or forwards to a non-op_Equality helper. + Check(false); + Check(false); + Check(false); + // Explicit padding means memcmp inspects bytes Equals does not. + Check(false); + // Overrides object.Equals only; no IEquatable. + Check(false); + // Primitive fields compared via '.Equals' rather than '=='. + Check(true); + Check(true); + Check(false); + // Record structs: Roslyn emits EqualityComparer.Default.Equals(this.F, other.F) per field. + Check(true); + Check(true); + Check(true); + Check(false); + Check(false); + // Enum fields are integer-backed, so they compare bitwise like their underlying primitive. + Check(true); + Check(true); + Check(true); + // Generic value types: the exact instantiation must be threaded through field and token + // resolution. A 'T' field is compared via EqualityComparer.Default.Equals (Roslyn cannot + // emit inline '==' for a type parameter), so this also exercises that path per instantiation. + Check>(true); + Check>(true); + Check>(true); + Check>(false); // reference argument: contains GC pointers + Check>(false); // float: Default.Equals is not bitwise + Check>(true); // inline '==' for an int field plus EqualityComparer for T + Check>(false); + Check>(true);// forwards into a generic op_Equality + Check>(false); // leading byte forces padding before the T field + } + + // The following structs have no Equals/GetHashCode override, so ValueType.Equals/GetHashCode go + // through CanCompareBitsOrUseFastGetHashCode. The IsNotTightlyPacked fix moved a struct with a + // multi-byte value-type field off the reflection slow path onto the memcmp fast path; either way + // the result must match the obvious value semantics. + + [Fact] + public static void TightlyPacked_EqualsAndHash_AreConsistent() + { + var a = new PlainOuter { X = new PlainInner { A = 1, B = 2 }, C = 3 }; + var b = new PlainOuter { X = new PlainInner { A = 1, B = 2 }, C = 3 }; + var c = new PlainOuter { X = new PlainInner { A = 1, B = 9 }, C = 3 }; + + Assert.True(a.Equals(b)); + Assert.False(a.Equals(c)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + } + + public struct PlainInner { public int A; public int B; } + public struct PlainOuter { public PlainInner X; public int C; } + + public record struct RecTwo(int X, int Y); + public record struct RecNested(RecTwo P, long Z); + public record struct RecMixed(long A, int B, short C, short D); + public record struct RecPadded(int X, byte Y); + public record struct RecFloat(float X, int Y); + public record struct RecEnum(ColorInt A, ColorInt B); + + // Generic value types. A 'T' field is compared via EqualityComparer.Default.Equals. + public record struct GenPair(T A, T B); + + public struct GenMixed : IEquatable> + { + public int X; public T Y; + public bool Equals(GenMixed o) => X == o.X && System.Collections.Generic.EqualityComparer.Default.Equals(Y, o.Y); + public override bool Equals(object o) => o is GenMixed p && Equals(p); + public override int GetHashCode() => 0; + } + + public struct GenForwardsToOp : IEquatable> + { + public T V; + public bool Equals(GenForwardsToOp o) => this == o; + public static bool operator ==(GenForwardsToOp a, GenForwardsToOp b) => System.Collections.Generic.EqualityComparer.Default.Equals(a.V, b.V); + public static bool operator !=(GenForwardsToOp a, GenForwardsToOp b) => !(a == b); + public override bool Equals(object o) => o is GenForwardsToOp p && Equals(p); + public override int GetHashCode() => 0; + } + + public struct GenPadded : IEquatable> + { + public byte B; public T V; + public bool Equals(GenPadded o) => B == o.B && System.Collections.Generic.EqualityComparer.Default.Equals(V, o.V); + public override bool Equals(object o) => o is GenPadded p && Equals(p); + public override int GetHashCode() => 0; + } + + // Int-backed enum: two fields pack to 8 bytes with no padding. + public enum ColorInt { A, B, C } + + public readonly struct EnumPair : IEquatable + { + public readonly ColorInt First; public readonly ColorInt Second; + public bool Equals(EnumPair o) => First == o.First && Second == o.Second; + public override bool Equals(object o) => o is EnumPair p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct EnumAndInt : IEquatable + { + public readonly int X; public readonly ColorInt E; + public bool Equals(EnumAndInt o) => X == o.X && E == o.E; + public override bool Equals(object o) => o is EnumAndInt p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct Point : IEquatable + { + public readonly int X; public readonly int Y; + public bool Equals(Point o) => X == o.X && Y == o.Y; + public override bool Equals(object o) => o is Point p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct ThreeFields : IEquatable + { + public readonly int A; public readonly int B; public readonly int C; + public bool Equals(ThreeFields o) => A == o.A && B == o.B && C == o.C; + public override bool Equals(object o) => o is ThreeFields p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct OneField : IEquatable + { + public readonly long V; + public bool Equals(OneField o) => V == o.V; + public override bool Equals(object o) => o is OneField p && Equals(p); + public override int GetHashCode() => 0; + } + + // 11 fields (int, 2x short, 8x byte = 16 bytes, tightly packed) shaped like Guid. Enough fields + // that Roslyn emits long-form branches in the field-wise Equals; the scanner must accept those. + public readonly struct GuidShape : IEquatable + { + public readonly int A; public readonly short B; public readonly short C; + public readonly byte D, E, F, G, H, I, J, K; + public bool Equals(GuidShape o) => + A == o.A && B == o.B && C == o.C && D == o.D && E == o.E && F == o.F && + G == o.G && H == o.H && I == o.I && J == o.J && K == o.K; + public override bool Equals(object o) => o is GuidShape g && Equals(g); + public override int GetHashCode() => 0; + } + + public readonly struct ForwardsToOp : IEquatable + { + public readonly int Lo; public readonly int Hi; + public bool Equals(ForwardsToOp o) => this == o; + public static bool operator ==(ForwardsToOp a, ForwardsToOp b) => a.Lo == b.Lo && a.Hi == b.Hi; + public static bool operator !=(ForwardsToOp a, ForwardsToOp b) => !(a == b); + public override bool Equals(object o) => o is ForwardsToOp p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct Nested : IEquatable + { + public readonly Point P; public readonly int Z; + public bool Equals(Nested o) => P.Equals(o.P) && Z == o.Z; + public override bool Equals(object o) => o is Nested p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct NestedLast : IEquatable + { + public readonly int Z; public readonly Point P; + public bool Equals(NestedLast o) => Z == o.Z && P.Equals(o.P); + public override bool Equals(object o) => o is NestedLast p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct AllNested : IEquatable + { + public readonly Point A; public readonly OneField B; + public bool Equals(AllNested o) => A.Equals(o.A) && B.Equals(o.B); + public override bool Equals(object o) => o is AllNested p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct WrapsPartial : IEquatable + { + public readonly IgnoresField A; public readonly int Z; + public bool Equals(WrapsPartial o) => A.Equals(o.A) && Z == o.Z; + public override bool Equals(object o) => o is WrapsPartial p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct WrapsPadded : IEquatable + { + public readonly WithPadding A; public readonly int Z; + public bool Equals(WrapsPadded o) => A.Equals(o.A) && Z == o.Z; + public override bool Equals(object o) => o is WrapsPadded p && Equals(p); + public override int GetHashCode() => 0; + } + + public struct PlainNoEquatable { public int A; public int B; } + + public readonly struct HasFloat : IEquatable + { + public readonly int A; public readonly float F; + public bool Equals(HasFloat o) => A == o.A && F == o.F; + public override bool Equals(object o) => o is HasFloat p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct IgnoresField : IEquatable + { + public readonly int A; public readonly int B; + public bool Equals(IgnoresField o) => A == o.A; + public override bool Equals(object o) => o is IgnoresField p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct CustomLogic : IEquatable + { + public readonly int A; + public bool Equals(CustomLogic o) => (A & 0xF) == (o.A & 0xF); + public override bool Equals(object o) => o is CustomLogic p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct CallsHelper : IEquatable + { + public readonly int A; public readonly int B; + public bool Equals(CallsHelper o) => Cmp(this, o); + private static bool Cmp(CallsHelper a, CallsHelper b) => a.A == b.A && a.B == b.B; + public override bool Equals(object o) => o is CallsHelper p && Equals(p); + public override int GetHashCode() => 0; + } + + [StructLayout(LayoutKind.Explicit, Size = 16)] + public readonly struct WithPadding : IEquatable + { + [FieldOffset(0)] public readonly byte A; + [FieldOffset(8)] public readonly int B; + public bool Equals(WithPadding o) => A == o.A && B == o.B; + public override bool Equals(object o) => o is WithPadding p && Equals(p); + public override int GetHashCode() => 0; + } + + public struct OverriddenOnly + { + public int A; + public override bool Equals(object o) => o is OverriddenOnly p && p.A == A; + public override int GetHashCode() => A; + } + + public readonly struct PrimEquals : IEquatable + { + public readonly byte A; public readonly sbyte B; public readonly short C; public readonly int D; public readonly long E; + public bool Equals(PrimEquals o) => A.Equals(o.A) && B.Equals(o.B) && C.Equals(o.C) && D.Equals(o.D) && E.Equals(o.E); + public override bool Equals(object o) => o is PrimEquals p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct MixedEquals : IEquatable + { + public readonly int A; public readonly int B; + public bool Equals(MixedEquals o) => A == o.A && B.Equals(o.B); + public override bool Equals(object o) => o is MixedEquals p && Equals(p); + public override int GetHashCode() => 0; + } + + public readonly struct FloatEquals : IEquatable + { + public readonly int A; public readonly float F; + public bool Equals(FloatEquals o) => A.Equals(o.A) && F.Equals(o.F); + public override bool Equals(object o) => o is FloatEquals p && Equals(p); + public override int GetHashCode() => 0; + } +} diff --git a/src/tests/Loader/classloader/BitwiseEquatable/BitwiseEquatable.csproj b/src/tests/Loader/classloader/BitwiseEquatable/BitwiseEquatable.csproj new file mode 100644 index 00000000000000..e84fc8869ba4ea --- /dev/null +++ b/src/tests/Loader/classloader/BitwiseEquatable/BitwiseEquatable.csproj @@ -0,0 +1,13 @@ + + + + true + + true + + + + +