Search code examples
c#operatorsmoduluscompiler-bugoverflowexception

Why does the compiler evaluate remainder MinValue % -1 different than runtime?


I think this looks like a bug in the C# compiler.

Consider this code (inside a method):

const long dividend = long.MinValue;
const long divisor = -1L;
Console.WriteLine(dividend % divisor);

It compiles with no errors (or warnings). Seems like a bug. When run, prints 0 on console.

Then without the const, the code:

long dividend = long.MinValue;
long divisor = -1L;
Console.WriteLine(dividend % divisor);

When this is run, it correctly results in an OverflowException being thrown.

The C# Language Specification mentions this case specifically and says a System.OverflowException shall be thrown. It does not depend on the context checked or unchecked it seems (also the bug with the compile-time constant operands to the remainder operator is the same with checked and unchecked).

Same bug happens with int (System.Int32), not just long (System.Int64).

For comparison, the compiler handles dividend / divisor with const operands much better than dividend % divisor.

My questions:

Am I right this is a bug? If yes, is it a well-known bug that they do not wish to fix (because of backwards compatibility, even if it is rather silly to use % -1 with a compile-time constant -1)? Or should we report it so that they can fix it in upcoming versions of the C# compiler?


Solution

  • This corner-case is very specifically addressed in the compiler. Most relevant comments and code in the Roslyn source:

    // Although remainder and division always overflow at runtime with arguments int.MinValue/long.MinValue and -1     
    // (regardless of checked context) the constant folding behavior is different.     
    // Remainder never overflows at compile time while division does.    
    newValue = FoldNeverOverflowBinaryOperators(kind, valueLeft, valueRight);
    

    And:

    // MinValue % -1 always overflows at runtime but never at compile time    
    case BinaryOperatorKind.IntRemainder:
        return (valueRight.Int32Value != -1) ? valueLeft.Int32Value % valueRight.Int32Value : 0;
    case BinaryOperatorKind.LongRemainder:
        return (valueRight.Int64Value != -1) ? valueLeft.Int64Value % valueRight.Int64Value : 0;
    

    Also the behavior of the legacy C++ version of compiler, going all the way back to version 1. From the SSCLI v1.0 distribution, clr/src/csharp/sccomp/fncbind.cpp source file:

    case EK_MOD:
        // if we don't check this, then 0x80000000 % -1 will cause an exception...
        if (d2 == -1) {
            result = 0;
        } else {
            result = d1 % d2;
        }
        break;
    

    So conclusion to draw that this was not overlooked or forgotten about, at least by the programmers that worked on the compiler, it could perhaps be qualified as insufficiently precise language in the C# language specification. More about the runtime trouble caused by this killer poke in this post.