While browsing [bugs.php.net][1], I found an [interesting ticket][2]. Someone reported a surprising behavior, and I wanted to understand what is happening under the hood—because I think this is a really important concept to understand in PHP internals.
Consider the following script and its output:
<?php
$NaN = sqrt(-1);
$array = [$NaN];
var_dump($array === $array); // always true
var_dump([$NaN] === [$NaN]); // always false
At first glance, this looks confusing. Let’s investigate it line by line.
Generating NaN
The first line contains a mathematical expression that is not defined in the real numbers:
$NaN = sqrt(-1);
PHP delegates this operation to its internal math implementation in [ext/standard/math.c][3], which ultimately relies on the system’s math library (for example, glibc on Linux, Apple libc on macOS, or musl on Alpine). These libraries follow the [IEEE 754 floating-point standard][4]. According to this standard, operations that are mathematically undefined (such as the square root of a negative number) produce NaN (“Not a Number”). Importantly, NaN is never equal to anything, including itself. This is a fundamental rule of IEEE 754 floating-point arithmetic.
Putting NaN into an array
Next, we store this NaN value in an array:
$array = [$NaN];
Comparing the same array
Now we compare the array to itself:
var_dump($array === $array);
In PHP, the === operator performs a strict comparison, meaning:
same type same value Here, both operands refer to the same variable, so the comparison returns true. This is effectively an identity shortcut. Internally, both $array operands **point to the same zval**.
A short note about zvals
------------------------
A **zval** (Zend value) is the fundamental data structure used by the [Zend Engine][5] to represent all PHP variables. Every variable in PHP is stored internally as a zval. PHP 7 redesigned the zval structure to be significantly more memory-efficient and faster compared to PHP 5.
A zval is essentially a container that holds:
- the value
- the type information
- flags related to references and garbage collection
PHP uses a **copy-on-write** (CoW) mechanism for efficiency. Multiple variables can point to the same underlying value without duplicating memory. Only when a modification occurs does PHP create a copy, ensuring changes don’t affect other variables.
Comparing two distinct arrays
-----------------------------
Finally, let’s look at this line:
```
var_dump([$NaN] === [$NaN]);
Here, we are comparing two distinct arrays, even though their contents look identical. To understand why this returns false, let’s briefly follow the [relevant internal comparison logic][6] in PHP:
zend_is_identical(op1, op2), (both operands are arrays):
- zend_compare_arrays()
- zend_compare_symbol_tables()
- zend_hash_compare()
- hash_zval_identical_function()
- zend_is_identical(v1, v2)
During this process, PHP compares each element of the arrays using **strict identity comparison**. Eventually, the comparison reaches the array elements themselves: NaN === NaN
According to the IEEE 754 standard, NaN is never equal to itself, so this comparison returns false. Because the first (and only) elements of the arrays are not identical, **the entire array comparison fails**.
Conclusion
----------
This behavior is not a bug in PHP, but a direct consequence of how floating-point numbers and strict comparisons are defined and implemented.
This example highlights why understanding PHP internals—such as zvals, copy-on-write, and strict comparison semantics—is crucial when working with edge cases involving floating-point numbers. What may initially look like inconsistent behavior is actually a predictable and well-defined result of PHP’s internal design.
[1]: https://bugs.php.net/
[2]: https://bugs.php.net/bug.php?id=80701
[3]: https://github.com/php/php-src/blob/master/ext/standard/math.c
[4]: https://ethw.org/Milestones:IEEE_Standard_754_for_Binary_Floating-Point_Arithmetic,_1985
[5]: https://www.phpinternalsbook.com/php7/zend_engine.html
[6]: https://github.com/php/php-src/blob/master/Zend/zend_operators.c