Logical comparison produces incorrect result with underflowing arithmetic

Hardware: Arduino Mega 2560
Arduino: 1.8.13

The rather surprising issue I have encountered (after hours of pulling out my hair) can be reproduced with the following simple sketch (below).

When performing a one-line statement that compares the result of an underflowing 8-bit arithmetic operation against a threshold (see: data[4] assignment below), the run-time result which should always be TRUE is almost always FALSE (except when the subtraction result doesn’t cause an underflow). Exploring the issue (see data[3]) - performing the subtraction in a separate assignment, then comparing it to the threshold always produces the correct result during the comparison.

See examples of passing and failing results below resulting from these two scenarios.

What am I missing here? Is there some behind-the-scenes optimization that is messing with the subtraction results? Is there a strange type-ing issue that I am not aware of? Why does such a seemingly straightforward line of code produce such unintuitive results?

Some follow-up lines of inquiry that I have not explored - does this only happen with 8-bit unsigned comparisons? Or do multi-byte data types also respond this way?

The Sketch

uint8_t data[10];

void setup() {
  Serial.begin(115200);
  Serial.println("Start");
}

void loop() {  
  data[0] = TCNT0;                     //grab a 'random' value from the tcnt0 register to test with
  data[1] = data[0]+5;                 //increment it by 5
  data[2] = data[0]-data[1];           //perform a difference operation - always produces -5 (underflows to 251)
  data[3] = data[2]>0x7F;              //compare (>) the result of the operation against 8-bit-half-max (127) - always true
  data[4] = (data[0]-data[1])>0x7F;    //Now - do the subtraction and compare in 1 step. This Fails (most of the time). Except in case where data[0] is 251-255 (in which case, the subtraction does not produce an underflow))
  data[5] = 0x7F;                      //Prove that 0x7F is 127...
  data[6] = 0-5;                       //Prove that -5 is is 251...
  data[7] = (0-5)>0x7F;                //Perform an explicit subtraction and compare in one step - Still Fails
  data[8] = (251-0)>0x7F;              //Unless... the first number of the subtraction larger than the second number, and is larger than 127
  data[9] = 251>0x7F;                  //Yes, 251 is larger than 127

  //print the data for inspection
  for(uint8_t i = 0; i<10; i++){
    Serial.print("data["); Serial.print(i); Serial.print("] = "); Serial.println(data[i]);
  }
  Serial.println();
}

Example Failing Results:

data[0] = 104
data[1] = 109
data[2] = 251
data[3] = 1
data[4] = 0
data[5] = 127
data[6] = 251
data[7] = 0
data[8] = 1
data[9] = 1

Example Passing Results:

data[0] = 252
data[1] = 1
data[2] = 251
data[3] = 1
data[4] = 1
data[5] = 127
data[6] = 251
data[7] = 0
data[8] = 1
data[9] = 1
data[4] = (data[0]-data[1])>0x7F;

I think what you are seeing is that values smaller than 'int' get promoted to 'int' for math operations.

When data[2] (0xFB) gets promoted to 'int' it comes out as 0x00FB = 251.
251 is greater than 127

data[0] contains 104 (0x68) which gets promoted to 0x0068 (104).
data[1] contains 109 (0x6D) which gets promoted to 0c006D (109).
data[0] - data[1] comes out to -5 since integers are signed.
-5 is less than 127

All the results are exactly as they should be.

You have to understand that expressions like (0-5) are evaluated by default as the "int" data type. No promotion is involved in that case, nor is there promotion in evaluating the expression (0-5)>0x7F. Both sides of the comparison are treated as "int".

johnwasser:

data[4] = (data[0]-data[1])>0x7F;

I think what you are seeing is that values smaller than ‘int’ get promoted to ‘int’ for math operations.

When data[2] (0xFB) gets promoted to ‘int’ it comes out as 0x00FB = 251.
251 is greater than 127

data[0] contains 104 (0x68) which gets promoted to 0x0068 (104).
data[1] contains 109 (0x6D) which gets promoted to 0c006D (109).
data[0] - data[1] comes out to -5 since integers are signed.
-5 is less than 127

Ahh, interesting - and slightly annoying. Forcing a typecast of the result to uint8_t fixes the issue.

  data[4] = ((uint8_t)(data[0]-data[1]))>0x7F;

fixes the issue

Well, from your point of view, I suppose.

They are different comparisons, and the different answers are respectively correct.

Were you expecting negative numbers (-5) using unsigned variables?
5 - 9 = 4, 3, 2, 1, 0, 255, 254, 253, 252, 251.
Did you try:

int8_t data[10];

?

gallium_arsenide:
Ahh, interesting - and slightly annoying. Forcing a typecast of the result to uint8_t fixes the issue.

  data[4] = ((uint8_t)(data[0]-data[1]))>0x7F;

Yes, that will truncate the -5 (0xFFFB) to 0xFB and make it unsigned so it gets promoted to 0x00FB (251).

JCA34F:
Were you expecting negative numbers (-5) using unsigned variables?
5 - 9 = 4, 3, 2, 1, 0, 255, 254, 253, 252, 251.
Did you try:

int8_t data[10];

?

For this application, I am explicitly utilizing modulo 256 arithmetic, so unsigned is intentional.
Turns out I wasnt aware of type promotion "behind the scenes," so something that I though should work (performing subtraction on uint8's, comparing it to a explicit 8-bit value, then assigning the result to a uint8) was being muddied by c's promotion to an int halfway in between.
For the purposes of keeping my notes straightworward, I interchange -5 and 251 (which are the same under modulo 256 arithmetic)

gallium_arsenide:
comparing it to a explicit 8-bit value,

0x7F is NOT an 'explicit 8-bit value'. It is an 'int' constant, just like 0x7, 0x7FF and 0x7FFF. If the value is too big to fit in an 'int' (like 0x7FFFF) the compiler will make it a 'long' for you. Add a 'u' or 'U' at the end to make it unsigned. Add 'l' or 'L' to make it explicitly 'long'.
See: Constants - C++ Tutorials
To get an 8-bit (char) constant, write it as '\x7F'. It will be a signed character and I see no way to designate it an unsigned character. It will be upgraded to 'int' in the comparison so the result will be the same: 127. Note that the value is signed and will be sign-extended: '\FB' is a character but will act like -5 (0xFFFB) in a compare operation, after being upgraded to 'int'.

johnwasser:
0x7F is NOT an 'explicit 8-bit value'. It is an 'int' constant, just like 0x7, 0x7FF and 0x7FFF.

Ah, thanks for helping me understand. Still learning c even after all these years...