Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correctly handing NAN in PHP versions

Tags:

Can anyone explain why NAN and a variable equal to NAN behave differently depending on the version of PHP?

Consider the following code:

$nan = NAN;
print "PHP Version: " . phpversion(). "\n" .
  '0 <  NAN ? ' . ( 0 < NAN   ? 'TRUE' : 'FALSE' ) . "\n" .
  '0 >  NAN ? ' . ( 0 > NAN   ? 'TRUE' : 'FALSE' ) . "\n" . 
  '0 == NAN ? ' . ( 0 == NAN  ? 'TRUE' : 'FALSE' ) . "\n" . 
  '0 <  $nan ? ' . ( 0 < $nan  ? 'TRUE' : 'FALSE' ) . "\n" .
  '0 >  $nan ? ' . ( 0 > $nan  ? 'TRUE' : 'FALSE' ) . "\n" .
  '0 == $nan ? ' . ( 0 == $nan ? 'TRUE' : 'FALSE' ) . "\n" .
  'is_nan(NAN) ' . ( is_nan(NAN)  ? 'TRUE' : 'FALSE' ) . "\n" .
  'is_nan($nan) ' . ( is_nan($nan) ? 'TRUE' : 'FALSE' ) . "\n" .
  'gettype(NAN) is '  . gettype(NAN)  . "\n" .
  'gettype($nan) is ' . gettype($nan) . "\n";

Now, if I run this code against many versions of PHP (using MAMP) the following are results:

PHP Version: 5.3.5
0 <  NAN ? TRUE
0 >  NAN ? TRUE
0 == NAN ? FALSE
0 <  $nan ? TRUE
0 >  $nan ? TRUE
0 == $nan ? FALSE
is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double

PHP Version: 5.6.30 (and 5.5.30, 5.4.45)
0 <  NAN ? FALSE
0 >  NAN ? FALSE
0 == NAN ? FALSE
0 <  $nan ? FALSE
0 >  $nan ? FALSE
0 == $nan ? FALSE
is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double

PHP Version: 7.1.1 (and 7.0.15)
0 <  NAN ? TRUE
0 >  NAN ? TRUE
0 == NAN ? FALSE
0 <  $nan ? FALSE
0 >  $nan ? FALSE
0 == $nan ? FALSE
is_nan(NAN) TRUE
is_nan($nan) TRUE
gettype(NAN) is double
gettype($nan) is double

Can a function in PHP rely on a comparison to NAN or should NAN only be used with is_nan()?

like image 818
Andy Avatar asked Jul 21 '17 23:07

Andy


1 Answers

This bug has now been fixed by this commit. The explanation bellow shows what was causing it.

About your first question, it seems that since PHP7 you get different results depending on whether the expression is evaluated during compilation or during runtime.

First off, it is important to note that according to IEEE754 all comparisions where one of the elements is a NAN, should return false. You can find some details in this answer. Taking that into account, the behavior of 5.6 seems to be the correct one.


So, for the case of runtime evaluation(0 < $nan), that comparison is done in the VM by this code:

result = ((double)Z_LVAL_P(op1) < Z_DVAL_P(op2));

That operation will assign 0 to result which is eventually returned. This will give us the expected output of FALSE.


In the case of compile time evaluation(0 < NAN), the comparison ir done during compilation by this code:

case TYPE_PAIR(IS_LONG, IS_DOUBLE):
    Z_DVAL_P(result) = (double)Z_LVAL_P(op1) - Z_DVAL_P(op2);
    ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_DVAL_P(result)));
    return SUCCESS;

Here there are some other things happening, the result of the subtraction 0 - NAN = NAN that value then goes through the ZEND_NORMALIZE_BOOL macro that reads as follows:

#define ZEND_NORMALIZE_BOOL(n)          \
    ((n) ? (((n)>0) ? 1 : -1) : 0)

As you can see, if (n) > 0 is false (which is the case for NAN), the macro will return -1. That value is then passed to the is_smaller_function where you'll find:

ZVAL_BOOL(result, (Z_LVAL_P(result) < 0));

Given that at this point result holds -1, this will be evaluated to TRUE.


About your second question, I would suggest you should never rely on comparisons with NAN and stick to using is_nan(). Note that even NAN == NAN evaluates to false.

like image 163
pmmaga Avatar answered Oct 14 '22 20:10

pmmaga