さすがにround関数の話が続き過ぎだという声が多いので、先日PHP勉強会で話した小ネタを披露してみます。
早速ですが、PHPの5.2.2より前のバージョンを用意してください。
$ php --version PHP 5.2.1 (cli) (built: Jul 4 2007 20:25:55) Copyright (c) 1997-2007 The PHP Group Zend Engine v2.2.0, Copyright (c) 1998-2007 Zend Technologies $ php -r 'printf("%11.0f\n", 017777777777);' 2147483647 $ php -r 'printf("%11.0f\n", 020000000000);' 20000000000
8進数で017777777777の次の数である020000000000を書いたつもりだったのですが、なぜか180億ほど大きい数になってしまいました。
これはどういうことかというと、PHP5.2.1以前では2の31乗以上の数は8進表記ができません。2の31乗以上になると頭に0が付いていても10進リテラルとして扱われます。
ちなみにこのバグは#41118で修正されており、下記の通り5.2.2以降*1では期待通りに動きます。
$ php --version PHP 5.2.3 (cli) (built: Jun 3 2007 03:18:58) Copyright (c) 1997-2007 The PHP Group Zend Engine v2.2.0, Copyright (c) 1998-2007 Zend Technologies $ php -r 'printf("%11.0f\n", 017777777777);' 2147483647 $ php -r 'printf("%11.0f\n", 020000000000);' 2147483648
さて、この処理がソースコード上でどうなっているのか、中身を追いかけてみましょう。8進リテラルの処理はZend/zend_language_scanner.lで行っています。PHPの字句解析部ですね。PHP5.2.1では下記のようになっています。
LNUM [0-9]+
(途中略)
<ST_IN_SCRIPTING>{LNUM} { if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */ zendlval->value.lval = strtol(yytext, NULL, 0); } else { errno = 0; zendlval->value.lval = strtol(yytext, NULL, 0); if (errno == ERANGE) { /* Overflow */ zendlval->value.dval = zend_strtod(yytext, NULL); zendlval->type = IS_DOUBLE; return T_DNUMBER; } } zendlval->type = IS_LONG; return T_LNUMBER; }
プログラム中の数値リテラルについて、10進数でも8進数でもstrtol(3)に処理を丸投げしています。これは十分小さい数については期待通りに動くはずです。ただし、8進数でも10進数でもlongをオーバーフローしてしまった場合にはzend_strtod()でdoubleにしようとします。ところが、この関数は浮動小数点数リテラルを処理する関数です。10進リテラルは浮動小数点数リテラルとしても正しく処理できますが、8進リテラルは処理できません。これが今回のバグの原因です。
PHP5.2.2の同じ処理を見てみましょう。
<ST_IN_SCRIPTING>{LNUM} { if (yyleng < MAX_LENGTH_OF_LONG - 1) { /* Won't overflow */ zendlval->value.lval = strtol(yytext, NULL, 0); } else { errno = 0; zendlval->value.lval = strtol(yytext, NULL, 0); if (errno == ERANGE) { /* Overflow */ if (yytext[0] == '0') { /* octal overflow */ zendlval->value.dval = zend_oct_strtod(yytext, NULL); } else { zendlval->value.dval = zend_strtod(yytext, NULL); } zendlval->type = IS_DOUBLE; return T_DNUMBER; } } zendlval->type = IS_LONG; return T_LNUMBER; }
数値リテラルの1文字目が0だったらzend_oct_strtod()で処理するように変更されています。もう巨大な8進リテラルを使っても安心ですね!