PHP 整数型のリテラルの最小値が違うのはプログラマが昔から苦しめられた名残りだったようだ
TOP > てきとうにこらむ > ゲーム作りとプログラミング日記 > PHP 整数型のリテラルの最小値が違うのはプログラマが昔から苦しめられた名残りだったようだ
※ 追記しました 下へどうぞ。
PHPのInteger
PHPのInteger(整数型)は、C言語のlongで実装されている。最小値は通常であれば-2の63乗である
-9223372036854775808
であるはず。
PHPでのリテラル値では…
だが、PHPでこのリテラルを指定すると
$ php -r 'var_dump(-9223372036854775808);'
float(-9.2233720368548E+18)
となってしまう。すなわち、何故かFloat(浮動小数点型)になってしまうのだ。整数型の範囲を外れればFloatになるのがPHPの仕様だが、何故か範囲内である-9223372036854775808なのにもかかわらずFloatになってしまう。
ちなみに、PHP7から新しく出てきた定数、PHP_INT_MINを指定すると、きちんと-9223372036854775808となっている。また、 pow関数を使うと、やはり-9223372036854775808となる。
# php -r 'var_dump(PHP_INT_MIN);'
int(-9223372036854775808)
$ php -r 'var_dump(pow(-2, 63));'
int(-9223372036854775808)
うーんと…?
ソースコード
まず最初に疑ったのは、字句解析の部分で、zend_language_scanner.l。1118行目にはこう書いてある。(PHP 7.0.9)
LNUM [0-9]+
で、これを解析している部分は1655行目。
<ST_IN_SCRIPTING>{LNUM} {
if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */
errno = 0;
ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
/* This isn't an assert, we need to ensure 019 isn't valid octal
* Because the lexing itself doesn't do that for us
*/
if (end != yytext + yyleng) {
zend_throw_exception(zend_ce_parse_error, "Invalid numeric literal", 0);
ZVAL_UNDEF(zendlval);
RETURN_TOKEN(T_LNUMBER);
}
}
オーバーフローしていなかったらここに到達のはず。ZEND_STRTOLとは、strtolのプリプロセッサである。文字列をlong型に変更してくれる。しかし、これだと、マイナス値が取れないというか、別のトークンになる。マイナスのリテラルはどこにあるのかな?
Zend/zend_language_scanner.lの1127行目に定義されてたTOKENSの中にあるようだ。
TOKENS [;:,.\[\]()|^&+-/*=%!~$<>?@]
デバッグしてみる
configureに--enable-debugオプションつけてコンパイル。んで、gdbを起動。
$ gdb --command .gdbinit sapi/cli/php
で、ブレークポイントを設定。zend_language_scanner.cの2780行目。
(gdb) b zend_language_scanner.c:2734
Note: breakpoint 1 also set at pc 0x5dfece.
(gdb) run -r '-9223372036854775808;'
Starting program: /home/tekitoh/php-7.0.9/sapi/cli/php -r '-9223372036854775808;'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, lex_scan (zendlval=0x7fffffffc390) at Zend/zend_language_scanner.c:2780
2780 yyaccept = 2;
(gdb) l
2775 RETURN_TOKEN(T_LNUMBER);
2776 }
2777 #line 2778 "Zend/zend_language_scanner.c"
2778 yy137:
2779 YYDEBUG(137, *YYCURSOR);
2780 yyaccept = 2;
2781 yych = *(YYMARKER = ++YYCURSOR);
2782 if (yych <= '9') {
2783 if (yych == '.') goto yy151;
2784 if (yych <= '/') goto yy136;
(gdb)
ここから、とりあえずnばっかり打ちまくる。
(gdb) n
3022 yyaccept = 2;
(gdb) n
3023 YYMARKER = ++YYCURSOR;
(gdb) n
3024 YYFILL(3);
(gdb) n
3025 yych = *YYCURSOR;
(gdb) n
3027 if (yych <= '9') {
(gdb) n
3028 if (yych == '.') goto yy151;
(gdb) n
3029 if (yych <= '/') goto yy136;
(gdb) n
3030 goto yy154;
(gdb) n
3022 yyaccept = 2;
(gdb) n
3023 YYMARKER = ++YYCURSOR;
(gdb) n
3024 YYFILL(3);
(gdb) n
3025 yych = *YYCURSOR;
(gdb) n
3027 if (yych <= '9') {
(gdb) n
3028 if (yych == '.') goto yy151;
(gdb) n
3029 if (yych <= '/') goto yy136;
(gdb) n
3030 goto yy154;
(gdb) n
3022 yyaccept = 2;
(gdb) n
3023 YYMARKER = ++YYCURSOR;
(gdb) n
3024 YYFILL(3);
(gdb) n
3025 yych = *YYCURSOR;
(gdb) n
3027 if (yych <= '9') {
(gdb) n
3028 if (yych == '.') goto yy151;
(gdb) n
3029 if (yych <= '/') goto yy136;
(gdb) n
3030 goto yy154;
(gdb)
たぶん、ここで一文字ずつポインタを移動させて、数値である限り取得し続けていることがわかる。たぶんそうだろう。
(gdb) l
1665 ZVAL_UNDEF(zendlval);
1666 RETURN_TOKEN(T_LNUMBER);
1667 }
1668 } else {
1669 errno = 0;
1670 ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
1671 if (errno == ERANGE) { /* Overflow */
1672 errno = 0;
1673 if (yytext[0] == '0') { /* octal overflow */
1674 errno = 0;
(gdb)
数値のパースが終わったら、ここにたどり着く。格納されているyytextに対してzval構造体のzendlvalにlongで突っ込もうとしている。strtol関数にて文字列になっている数値がlong型に変換されることになる。…そのはずである。
(gdb) n
1671 if (errno == ERANGE) { /* Overflow */
(gdb) n
1672 errno = 0;
(gdb)
しかし、結果はOverflowのコメントが付いたこのブロックにたどり着く。その後はオーバーフローをしているのでdouble型になる。ということになる。
(gdb) l
1668 } else {
1669 errno = 0;
1670 ZVAL_LONG(zendlval, ZEND_STRTOL(yytext, &end, 0));
1671 if (errno == ERANGE) { /* Overflow */
1672 errno = 0;
1673 if (yytext[0] == '0') { /* octal overflow */
1674 errno = 0;
1675 ZVAL_DOUBLE(zendlval, zend_oct_strtod(yytext, (const char **)&end));
1676 } else {
1677 ZVAL_DOUBLE(zendlval, zend_strtod(yytext, (const char **)&end));
(gdb)
しかし、これでは-2の63乗である「-9223372036854775808」を指定すると、longの範囲内(一般的に64bit環境)のはずなのに、オーバーフローしているのでdouble型に変換されてしまうというおかしなことになっている。
そもそも、字句解析でデータ型を決めるというよりも、おそらくは構文解析で決めるべきものではないだろうかと思う。…とはいえなんとも言えない。
Pythonでは
参考までに、Python(CPython)を調べてみる。Pythonではどうなのだろうか。字句解析部分のソースを読んでみた。
https://hg.python.org/cpython/file/default/Parser/tokenizer.c#l1650 1650行目から数値の字句解析が始まっている。仕組みとしては単純で、一文字ずつisdigitを使って数値かどうかチェックしている。そして、それがすべて数値であるとわかったら NUMBERという定数を返す。
少なくとも、字句解析でFloatかIntかを判断していない。判断しているのは「実数か小数点か、n進数か」であると思う。
https://hg.python.org/cpython/file/default/Python/ast.c#l2096
ここで、変換してるのかな
static expr_ty
ast_for_atom(struct compiling *c, const node *n)
case NUMBER: {
PyObject *pynum = parsenumber(c, STR(ch));
if (!pynum)
return NULL;
if (PyArena_AddPyObject(c->c_arena, pynum) < 0) {
Py_DECREF(pynum);
return NULL;
}
return Num(pynum, LINENO(n), n->n_col_offset, c->c_arena);
}
Python/ast.cにあった。strtolへのエイリアス関数。
assert(s != NULL);
errno = 0;
end = s + strlen(s) - 1;
imflag = *end == 'j' || *end == 'J';
if (s[0] == '0') {
x = (long) PyOS_strtoul(s, (char **)&end, 0);
if (x < 0 && errno == 0) {
return PyLong_FromString(s, (char **)0, 0);
}
}
else
x = PyOS_strtol(s, (char **)&end, 0);
要するに、AST(抽象構文木)に突っ込む際にchar *からlong型に変更しているということになる。これならわかる。
これはなんだろう
うーん。わからない。少なくとも、現状以下のコードがtrueになっていなければならないのにfalseになることだけは確か。(PHP 7.0.9 Debian ソースからインストール)
# php -r 'var_dump(-9223372036854775808 === PHP_INT_MIN);'
bool(false)
コレって既知のバグ?ちょっとわからない。
PHPの整数型のマニュアルには、「使用可能なリテラルの定数は以下の様なものがあります」とある。
decimal : [1-9][0-9]*
| 0
hexadecimal : 0[xX][0-9a-fA-F]+
octal : 0[0-7]+
binary : 0b[01]+
integer : [+-]?decimal
| [+-]?hexadecimal
| [+-]?octal
| [+-]?binary
うーん?
追記
そもそも、C言語自体がリテラルの処理に苦心しているようだった。
#include <stdio.h>
int main(void) {
long a = -9223372036854775808L;
printf("%ld\n", a);
return 0;
}
まず、「-9223372036854775808」とするとwarningを出す。
$ gcc -o long_hoge long_hoge.c
long_hoge.c: In function ‘main’:
long_hoge.c:4:15: warning: integer constant is so large that it is unsigned
long a = -9223372036854775808;
^
long_hoge.c:4:5: warning: this decimal constant is unsigned only in ISO C90
long a = -9223372036854775808;
^
一応、手元で試した限りは「-9223372036854775808UL」(unsigned longとして処理させる)とすれば警告は出なかったけど、それはそれでおかしい。
limits.h(/usr/include/limits.h)にはLONG_MINの定義があるのだけど、以下のように-LONG_MAX-1Lとされている。
# define LONG_MIN (-LONG_MAX - 1L)
すなわち、PHPのIntegerとFloatとのリテラル上のバグというよりも、浮動小数点のページにも記載されている通り、双方の解釈のしようがあるとも取れるのでどっちかというとバグとは言いづらいというか怪しいということになる模様。
LNUM [0-9]+
浮動小数点のページにもLNUMがありどっちともつかないと。
結論として
明確にリテラルとして指定したい場合にはPHP_INT_MINを使うか、PHP 5系の場合には「-9223372036854775807 - 1」などとしましょう。
$ php -r 'var_dump(-9223372036854775807 - 1);'
int(-9223372036854775808)
参考
教えていただいたり、調べていただいたみなさま、ありがとうございます。