hnwの日記

PHPの奇妙な8進数

さすがに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進リテラルを使っても安心ですね!



追記:AAを書きたいならaa記法を使えば?と教わりましたが、MacLinuxの人が見えないと残念ですから、キャプチャでいいことにします。

*1:PHP5.2.2のChangelogに書いてあります