Why is the enumeration value from a multi dimensional array not equal to itself?

Consider:

using System;

public class Test
{
    enum State : sbyte { OK = 0, BUG = -1 }

    static void Main(string[] args)
    {
        var s = new State[1, 1];
        s[0, 0] = State.BUG;
        State a = s[0, 0];
        Console.WriteLine(a == s[0, 0]); // False
    }
}

How can this be explained? It occurs in debug builds in Visual Studio 2015 when running in the x86 JIT. A release build or running in the x64 JIT prints True as expected.

To reproduce from the command line:

csc Test.cs /platform:x86 /debug

(/debug:pdbonly, /debug:portable and /debug:full also reproduce.)


Solution 1:

You found a code generation bug in the .NET 4 x86 jitter. It is a very unusual one, it only fails when the code is not optimized. The machine code looks like this:

        State a = s[0, 0];
013F04A9  push        0                            ; index 2 = 0
013F04AB  mov         ecx,dword ptr [ebp-40h]      ; s[] reference
013F04AE  xor         edx,edx                      ; index 1 = 0
013F04B0  call        013F0058                     ; eax = s[0, 0]
013F04B5  mov         dword ptr [ebp-4Ch],eax      ; $temp1 = eax 
013F04B8  movsx       eax,byte ptr [ebp-4Ch]       ; convert sbyte to int
013F04BC  mov         dword ptr [ebp-44h],eax      ; a = s[0, 0]
        Console.WriteLine(a == s[0, 0]); // False
013F04BF  mov         eax,dword ptr [ebp-44h]      ; a
013F04C2  mov         dword ptr [ebp-50h],eax      ; $temp2 = a
013F04C5  push        0                            ; index 2 = 0
013F04C7  mov         ecx,dword ptr [ebp-40h]      ; s[] reference 
013F04CA  xor         edx,edx                      ; index 1 = 0
013F04CC  call        013F0058                     ; eax = s[0, 0]
013F04D1  mov         dword ptr [ebp-54h],eax      ; $temp3 = eax 
                                               ; <=== Bug here!
013F04D4  mov         eax,dword ptr [ebp-50h]      ; a == s[0, 0] 
013F04D7  cmp         eax,dword ptr [ebp-54h]  
013F04DA  sete        cl  
013F04DD  movzx       ecx,cl  
013F04E0  call        731C28F4  

A plodding affair with lots of temporaries and code duplication, that's normal for unoptimized code. The instruction at 013F04B8 is notable, that is where the necessary conversion from sbyte to a 32-bit integer occurs. The array getter helper function returned 0x0000000FF, equal to State.BUG, and that needs to be converted to -1 (0xFFFFFFFF) before the value can be compared. The MOVSX instruction is a Sign eXtension instruction.

Same thing happens again at 013F04CC, but this time there is no MOVSX instruction to make the same conversion. That's where the chips fall down, the CMP instruction compares 0xFFFFFFFF with 0x000000FF and that is false. So this is an error of omission, the code generator failed to emit MOVSX again to perform the same sbyte to int conversion.

What is particularly unusual about this bug is that this works correctly when you enable the optimizer, it now knows to use MOVSX in both cases.

The probable reason that this bug went undetected for so long is the usage of sbyte as the base type of the enum. Quite rare to do. Using a multi-dimensional array is instrumental as well, the combination is fatal.

Otherwise a pretty critical bug I'd say. How widespread it might be is hard to guess, I only have the 4.6.1 x86 jitter to test. The x64 and the 3.5 x86 jitter generate very different code and avoid this bug. The temporary workaround to keep going is to remove sbyte as the enum base type and let it be the default, int, so no sign extension is necessary.

You can file the bug at connect.microsoft.com, linking to this Q+A should be enough to tell them everything they need to know. Let me know if you don't want to take the time and I'll take care of it.

Solution 2:

Let's consider OP's declaration:

enum State : sbyte { OK = 0, BUG = -1 }

Since the bug only occurs when BUG is negative (from -128 to -1) and State is an enum of signed byte I started to suppose that there were a cast issue somewhere.

If you run this:

Console.WriteLine((sbyte)s[0, 0]);
Console.WriteLine((sbyte)State.BUG);
Console.WriteLine(s[0, 0]);
unchecked
{
    Console.WriteLine((byte) State.BUG);
}

it will output :

255

-1

BUG

255

For a reason that I ignore(as of now)s[0, 0] is cast to a byte before evaluation and that's why it claims that a == s[0,0] is false.