For integer overflow, there is no universal convention. In most programming languages, how overflow is handled is left to the discretion of the programmer. In this particular example, the result differs depending on whether the final value is computed at compile time or runtime.
The expression 3 * (long)int.MinValue is known at compile time, so the value can be calculated immediately. In contrast, wtf * (long)int.MinValue is calculated at runtime, which leads to the discrepancy in results. It is unusual that different implementation choices were made between compile-time and runtime.
This is likely due to performance reasons. Best practices suggest that when dealing with operations that could result in overflow, programmers should use checked operations. Wrapping this entire block in a checked block would produce consistent results. Checked operations aren’t always used because they can be slower, but in edge cases like this one, they should be, to ensure correct results or trigger an exception.
From here https://learn.microsoft.com/en-us/dotnet/api/system.overflowexception?view=net-8.0 it says " for the arithmetic, casting, or conversion operation to throw an OverflowException, the operation must occur in a checked context. By default, arithmetic operations and overflows in Visual Basic are checked; in C# and F#, they are not. If the operation occurs in an unchecked context, the result is truncated by discarding any high-order bits that do not fit into the destination type"
So my take is this: Since in checked context this error doesn’t happen, mono has some kind of optimization for runtime operations that results in a bug by discarding the most significant bit during a potential overflow but the overflow eventually doesn’t happen because of the casting and the truncation propagates to the long number type. In contrast, in the checked context, this optimization doesn’t exist and no exception is thrown because of the casting.
EDIT: I think I know where the bug is, although I don’t have the mono code , from the dotnet runtime here: https://raw.githubusercontent.com/dotnet/runtime/919d316fa81bb0f77361e43b4eb8d1faf8d1b126/src%2Flibraries%2FSystem.Linq.Expressions%2Fsrc%2FSystem%2FLinq%2FExpressions%2FCompiler%2FILGen.cs there is a comment in the Conv_U8 opcode that is being used for the conversion to long that says: “While often not of consequence depending on what follows, there are cases where this casting matters. Values [0, int.MaxValue] can use either safely, but negative values must use conv.i8 and those (int.MaxValue, uint.MaxValue] must use conv.u8, or else the higher bits will be wrong.”
Maybe the Mono team uses Conv_U8 for both conversions ?