Either this is an INCREDIBLY SERIOUS bug or everything I know is wrong (fundamental integer arithmetic)

Simple, explain this:

UnityEngine.Debug.Log(3 * (long)int.MinValue); // -6442450944
long wtf = 3;
UnityEngine.Debug.Log(wtf * (long)int.MinValue); // !!!!!!! 6442450944
UnityEngine.Debug.Log(wtf * (long)int.MinValue == 3 * (long)int.MinValue); // !!!!!! FALSE

(2022 LTS and Unity 6)

Wait wait wait… LMAOOOOO… It get’s even crazier!

long wtf2 = int.MinValue;
UnityEngine.Debug.Log(wtf * wtf2); // !!!!!!! 6442450944
UnityEngine.Debug.Log(wtf * wtf2 == 3 * (long)int.MinValue); // !!!!!! TRUE, so 6442450944 == -6442450944

Obviously, executing the same code in a simple .NET8 console application logs the expected results.

This isn’t the first time Mono has caused me hours and/or days of incredible frustration and debugging. Like finding out that Mono evaluates float expressions on a line-by-line basis (YES, your source code! Same order of operations, just fewer named variables, and it’s not the double conversion that’s the issue, simply the number of lines you decide to write) and not on a operation-by-operation basis, yielding different results, or Mono simply changing how they cast very large doubles to ulongs some time mid 2022 by adding a horrendous piece of code (performance-wise) to handle that “logic”.
Having to track down “bugs” to such a FUNDAMENTAL level even if it’s only every now and then is completely unacceptable.

I ran your first snippet on ancient Unity5 and it works correctly:

I ran it in Unity2018.4.19f1 and got the correct results:

Same with Unity2020.3.41f1:

Finally, just for grins I thought I’d port it to native C and run it in good old clang:

// NOTE: This is not C#, compile with gcc at the command line!
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>

int	main(int argc, char **argv)
{
	long	foo1 = 3 * (long)INT_MIN;

	long	wtf = 3;

	long	foo2 = wtf * (long)INT_MIN;

	printf( "foo1 = %ld\n", foo1);
	printf( "foo2 = %ld\n", foo2);

	int tf = (wtf * (long)INT_MIN) == (3 * (long)INT_MIN);

	printf( "tf = %d\n", tf);

	printf( "----------- second snippet: ------------\n");

	long wtf2 = INT_MIN;
	long	foo3 = wtf * wtf2;

	printf( "foo3 = %ld\n", foo3);

	int tf2 = (wtf * wtf2 == 3 * (long)INT_MIN);

	printf( "tf2 = %d\n", tf2);
}

EDIT: corrected the error I had earlier above, and use INT_MIN;

I am not sure what to conclude from this. Did you try it after it goes through IL2CPP?

My environment also gave strange results.

Debug.Log(3 * (long)int.MinValue);
long wtf = 3;
Debug.Log(wtf * (long)int.MinValue);
Debug.Log(wtf * (long)int.MinValue == 3 * (long)int.MinValue);
long wtf2 = int.MinValue;
Debug.Log(wtf * wtf2);
Debug.Log(wtf * wtf2 == 3 * (long)int.MinValue);

Not sure why, but only the last result is different.

Unity6000.0.23f1
Windows10

The result of the IL2CPP build was correct.
I also checked the Mono build with ILSpy and there was no problem with the IL code, so I think it is an optimization problem of Mono JIT compiler.
Please submit a bug report.

these are the results I’ve got on unity 2022.3.50f1

ok so I’m cave man with this math. it appears to me it wraps around instead of capping out?

positive by negative should give negative. I’ve checked godot documentation and they also wrap the numbers out to positive if they overflow.

so how this thing should actually be?

I’m sure is a simple answer

@Kurt-Dekker Interesting. So it must be an issue with newer Mono versions. As stated, Mono changed at least some of its fundamental logic mid 2022, so this “fabulous” update must be the culprit. P.S.: Of course it’s gonna work in C :wink: I tried it in a C# .NET8 console app and it worked.

@Arithmetica Thankl you for reproducing the issue. Thank you also for going the extra mile and checking the IL. Very helpful. I will report this bug.

@altepTest You might be confused as to what int is in C# and what it is in godots scriptling language (I assume) - in C# int is always 32 bits, not 64. 3 * int.MinValue does not overflow 64 bits. And if it were, the results should still be the same, since signed overflow is not undefined behaviour in C#, it is in C though.

don’t mind me, they say they wrap things around, unity looks like they do it also for some reason. maybe is the way it needs to be?

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 ?

@altepTest of course they do. what else are you gonna do. BUT it doesn’t apply here, since there is no overflow that would cause wrapping around.

@meredoth Right, I tested it and it doesn’t happen in a checked context. Since the wrong result is the absolute value of the actual result, this must be related to casting an int32 to an int64 without sign bit propagation, i.e. casting as if it were a uint. but the strange this is that

UnityEngine.Debug.Log((long)int.MinValue);

shows the correct value. Furthermore this invalid cast only appears once FOR SOME FREAKING REASON this CONSTANT is involved in an operation with a non-constant variable, i.e.

3 * (long)int.MinValue

yields the expected result, but

long wtf = 3; wtf * (long)int.MinValue

does not.

Why do I have to deal with this :smiley: Why can I not continue my work because of this…

ok mystery solved.

kind off

you are using this computer programming wrong

if you check my screenshot visual studio (somewhat) clearly informs you that you do not try to add that float there because it pointless or something

also did you tried changing the int to float :face_with_hand_over_mouth: that is a new can of worms there

edit:ok, nevermind i leave this here because is funny. back to drawing board.

time to go to bed. that cast was pointless is what visual studio is telling me

edit2: actually :face_with_peeking_eye: why it tells me the long cast is redundant in the line where the wtffloat is used? that is a float not a long

This has nothing to do with floating point arithmetic, in the example provided everything was an integer type. Doing the same with float instead of long is a completely different thing.

or maybe it does? when he is multiplying 3 * (long)int.MinValue the 3 what type of number is inside unity?

No it doesn’t, the type is long. Floating point numbers never existed in his example.

It has nothing to do with Unity. C# compiler will implicitly convert the literal to long to satisfy the expression.

This is the reason why we have suffixes for the literal values. If you want 3 to be a float (or double) you would have to type 3f (or 3d or 3.0 for a double). However, compiler would then have cast the right-hand side to single precision, again to satisfy the expression.

Proof

Console.WriteLine($"{Type.GetTypeCode((3f * (long)int.MinValue).GetType())}");  // Single
Console.WriteLine($"{Type.GetTypeCode((3.0 * (long)int.MinValue).GetType())}"); // Double
Console.WriteLine($"{Type.GetTypeCode((3 * (long)int.MinValue).GetType())}"); // Int64

Be mindful of your types (esp with literals), and learn more about how casting system works in expressions.

Edit:
The above are equivalent to the following

Console.WriteLine($"{Type.GetTypeCode((3f * (float)(long)int.MinValue).GetType())}");
Console.WriteLine($"{Type.GetTypeCode((3.0 * (double)(long)int.MinValue).GetType())}");
Console.WriteLine($"{Type.GetTypeCode((3L * (long)int.MinValue).GetType())}");

(Although in practice, I think the compiler would remove long casts because they’re unecessary. I haven’t checked the IL though, just my hunch.)

integer literals
real literals

Yes… and in some compilers (dunno about those involved here) the compiler might notice that wtf is essentially employed as a constant and end up collapsing the above non-constant expression into a constant too, just as an expedience.

If I had to guess, the problem to me appears to be an incorrect sign extension choice during the cast from int to long

Be thankful we’re doing games. I seem to recall reading that the Lockheed Martin RQ-3 “Dark Star” unmanned aerial vehicle crashed on takeoff due to an arithmetic overflow issue in one of the control filter routines. Oops!

why is visual studio saying that casting to long is unnecessary in my previous screen, this line here where wtffloat is a float?

print(wtffloat * (long)int.MinValue);

Because of implicit “upcasting” of both int and long to a float when a float and int are involved.

This brings back fond memories of discovering the -S option on C compilers back in the day so that I could easily stare at the source assembly language generated from any given C snippet. That was a real eye-opener, explaining so much mystery.

Why wouldn’t it say so? It is unecessary, by definition of that word. You don’t require cast to long if the value was already int. When you multiply that with a 32-bit float, you gain nothing.

that because these hitech weapons use graphic packages (if not literally directly engines like unity or unreal) for detecting/processing visual information. like navigation or target identification.