dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.09k stars 4.04k forks source link

Abstract `Equals` on record causes "BadImageFormatException" #75957

Open mgaffigan opened 4 days ago

mgaffigan commented 4 days ago

Description

Record allows a abstract bool Equals(BaseType) method, but does not give a compiler error if it is not implemented on derived types.

Reproduction Steps

On a net8.0 or net9rc2 console project:

_ = new DerivedRecord() == new DerivedRecord();

abstract record BaseRecord()
{
    public abstract bool Equals(BaseRecord? other);
    public override int GetHashCode() => 0;
}

record DerivedRecord() : BaseRecord
{
}

Expected behavior

Compiler error requiring implementation of an Equals method on the derived type Or generation of a default equals. Or compiler error prohibiting abstract equals.

Actual behavior

Unhandled exception. System.BadImageFormatException: Bad IL format.
   at DerivedRecord.Equals(DerivedRecord other)
   at DerivedRecord.op_Equality(DerivedRecord left, DerivedRecord right)
   at Program.<Main>$(String[] args)

Regression?

I don't think so.

❌ net6.0-windows ❌ net7.0-windows ❌ net8.0-windows ❌ net9rc2-linux

Known Workarounds

Remove the abstract equals, or implement equals on all derived types:

record DerivedRecord() : BaseRecord
{
    public virtual bool Equals(DerivedRecord? other) => false;
}

Configuration

.Net 8.0.403 on W11 23H2 (22631) on x64 AnyCPU, x86, and x64 all produce same results

Other information

No response

huoyaoyuan commented 4 days ago

IL produced for derived class:

    .method public final hidebysig virtual 
        instance bool Equals (
            class BaseRecord other
        ) cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
            01 00 02 00 00
        )
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x2197
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: callvirt instance bool [System.Runtime]System.Object::Equals(object)
        IL_0007: ret
    } // end of method DerivedRecord::Equals

    .method public hidebysig newslot virtual 
        instance bool Equals (
            class DerivedRecord other
        ) cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
            01 00 02 00 00
        )
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x21a0
        // Code size 14 (0xe)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: beq.s IL_000c

        IL_0004: ldarg.0
        IL_0005: ldarg.1
        IL_0006: call instance bool BaseRecord::Equals(class BaseRecord)
        IL_000b: ret

        IL_000c: ldc.i4.1
        IL_000d: ret
    } // end of method DerivedRecord::Equals

It uses call instead of callvirt to call the abstract BaseRecord.Equals method.

The problem should belong to https://github.com/dotnet/roslyn since C# compiler generates invalid IL.

mgaffigan commented 4 days ago

@huoyaoyuan, thank you for your quick analysis! Should I re-submit there? Or will it be transferred.