-9223372036854775808がPHP_INT_MINじゃなかった


# 現象

PHP_INT_MAXやPHP_INT_MINは環境によって変わりますが、例として[3v4l](https://3v4l.org/QhAEV)では`PHP_INT_MAX` = `9223372036854775807`、`PHP_INT_MIN` = `-9223372036854775808`です。

```php
	var_dump(PHP_INT_MAX);
	var_dump(PHP_INT_MIN);
```

![01.png](https://qiita-image-store.s3.amazonaws.com/0/26088/3d9d6aae-fc33-fb05-bcb1-b442408bc3ce.png)

[マニュアル](http://php.net/manual/ja/reserved.constants.php)には`通常は PHP_INT_MIN === ~PHP_INT_MAX となる`と書かれています。
[試してみましょう](https://3v4l.org/Q4nSe)。

```php
	var_dump(PHP_INT_MIN === ~PHP_INT_MAX);
```

![02.png](https://qiita-image-store.s3.amazonaws.com/0/26088/4f74ce13-54f1-d2a8-ac40-0287cbba701f.png)

trueになりました。
めでたしめでたし。

ここで試しに[数値と比較](https://3v4l.org/fcqrh)してみましょう。

```php
	var_dump(PHP_INT_MAX, PHP_INT_MAX === 9223372036854775807);
	var_dump(PHP_INT_MIN, PHP_INT_MIN === -9223372036854775808);
```

![03.png](https://qiita-image-store.s3.amazonaws.com/0/26088/5b959d4a-778a-7b49-c50a-52470d244c7e.png)

あれ？

`PHP_INT_MIN === -9223372036854775808`がfalseになりました。


実はPHP_INT_MINだけではなく、数値同士の比較でもよくわからないことが起こります。

`-9223372036854775807-1`の計算結果は正しく[-9223372036854775808](https://3v4l.org/gaBom)になりますが、それを`-9223372036854775808`と比較すると[false](https://3v4l.org/eFgch)になります。
でも`-9223372036854775808`[同士の比較は](https://3v4l.org/b6edm)や`PHP_INT_MIN`[同士の比較](https://3v4l.org/MT9Ge)は`true`になるという。

どうなってるのかよくわかりませんね。


さて原因はというと、実は`PHP_INT_MIN`というよりむしろ`-9223372036854775808`[のほうにあります](https://3v4l.org/qGpVu)。

```php
	var_dump(PHP_INT_MIN);
	var_dump(-9223372036854775808);
```

![04.png](https://qiita-image-store.s3.amazonaws.com/0/26088/86250b8b-a2b4-1cb8-8e79-9359dc30a66f.png)

var_dumpするだけで`-9223372036854775808`が何故か`float(-9.2233720368548E+18)`になりました。
計算したりPHP_INT_MINから取ってくると正しく`-9223372036854775808`になるのに、値をそのまま出すとfloatになるとかさっぱりですね。


ちなみにこの現象、いつから発生しているのかというと、なんと[PHP4の時代からずっと](https://3v4l.org/KBkFH)です。
バグチケットは[2018/08/07に提出された](https://bugs.php.net/bug.php?id=76717)のですが、ここまで誰一人気付いてなかったことにびっくりだよ。
境界値のテストは[一応](https://github.com/php/php-src/blob/php-7.2.8/Zend/tests/int_overflow_64bit.phpt)[存在する](https://github.com/php/php-src/blob/php-7.2.8/Zend/tests/int_underflow_64bit.phpt)のですが、`(int)`って書いてあるせいで[検出されていません](https://3v4l.org/vs0ah)でした。

なお、バグチケットではvar_exportのせいだみたいに書いてありますが、実際はprintやecho、sprintfなんかでも起こるので数値処理の問題だと考えた方が適切でしょう。


# 原因調査

ここから下はソースを斜め読みしただけで実際にデバッグしたわけではないので、本当かどうかはわからないし保証もされません。
調査に使用したバージョンはPHP7.2.8です。


`===`による比較は`zend_operators.h`内の[fast_is_identical_function](https://github.com/php/php-src/blob/php-7.2.8/Zend/zend_operators.h#L805)で行われています。

```cpp
static zend_always_inline int fast_is_identical_function(zval *op1, zval *op2)
{
	if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) {
		return 0;
	} else if (Z_TYPE_P(op1) <= IS_TRUE) {
		return 1;
	}
	return zend_is_identical(op1, op2);
}
```

・型が異なればfalseを返す。
・片方の型がundefined/null/false/trueであればtrueを返す。
・それ以外はzend_is_identicalの結果を返す。

zend_is_identicalは[zend_operators.c](https://github.com/php/php-src/blob/php-7.2.8/Zend/zend_operators.c#L2112)で定義されています。

```cpp
switch (Z_TYPE_P(op1)) {
	case IS_LONG:
		return (Z_LVAL_P(op1) == Z_LVAL_P(op2));
```

`Z_LVAL_P`はlong値を持ってくるマクロです。
比較部分は特にひっかかるところがなさそうです。


ではどこが問題なのかというと、実はそれ以前、[字句解析](https://github.com/php/php-src/blob/php-7.2.8/Zend/zend_language_scanner.l#L1647)でスクリプトをパースするところです。
以下は数値をパースする部分の抜粋です。

```cpp
<ST_IN_SCRIPTING>{LNUM} {

	// 19桁未満なら
	if (yyleng < MAX_LENGTH_OF_LONG - 1) {
		// longにする
		ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
	
	// 19桁以上なら
	} else {
		// longにする
		ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
		
		// オーバーフローしていた
		if (errno == ERANGE) {
			// 1文字目が0だった
			if (yytext[0] == '0') {
				// 8進数からdoubleにする
				ZVAL_DOUBLE(zendlval, zend_oct_strtod(yytext, (const char **)&end));
			} else {
				// 10進数からdoubleにする
				ZVAL_DOUBLE(zendlval, zend_strtod(yytext, (const char **)&end));
			}
		}
	}
}
```

`LNUM`の定義は`[0-9]+`となっていて、ここには数値が入ってきます。
`-`は入りません。
そしてこの関数では、入ってきた値を`ZVAL_LONG`マクロでlong型にする、ただしオーバーフローしていたらdouble型にする、となっています。

ここから先は[zend_types.h](https://github.com/php/php-src/blob/php-7.2.8/Zend/zend_types.h)あたりに書いてあると思うのですが力尽きたので誰か続きよろ。
まあとにかく、ここで字句解析する際には`-`が入ってこないので、正負にかかわらず`9223372036854775807`は範囲内で、`9223372036854775808`はオーバーフローと判定されるみたいです。
ということで`-9223372036854775808`は、字句解析が終わった時点でfloat型になってしまいました。

`-9223372036854775807-1`は、字句解析の時点では`9223372036854775807`も`1`も範囲内なので正常にパースされるということですね。

実用上は`(int)`って入れればいいだけなので対処はたいしたことではないですが、根治のためには字句解析に手を入れないといけないということで、わりと深刻じゃないか、このバグ？