タイトルの意味が分からないかも知れませんが、PHP のswitch文でdefaultをタイポした場合の挙動について、個人的には意外でおもしろい結果だったため、今回はそれについて書こうと思います。

switch と default について

まず、switch文について。

PHP には、switchという文が用意されています。

単一の値を評価して、その結果によって多岐にわたる分岐を書く場合に、if文より可読性を上げるために使用されることが多いです。(詳細はマニュアルを参照してください。)

switch文には、defaultという特殊なcaseがあります。マニュアルのコード例をそのまま用いて見てみます。

<?php
switch ($i) {
    case 0:
        echo "iは0に等しい";
        break;
    case 1:
        echo "iは1に等しい";
        break;
    case 2:
        echo "iは2に等しい";
        break;
    default:
       echo "iは0,1,2に等しくない";
}
// $i が 0, 1, 2 以外の場合の出力は以下。
// iは0,1,2に等しくない
?>

defaultはその名のとおり、どのcaseにも当てはまらない場合に実行される、デフォルトの処理を記述する部分になります。

さて、この default というワードを、もしタイポしてしまったらどうなるでしょうか?

default をタイポしたらどうなるのか

実際に試してみます。

<?php
switch ($i) {
    case 0:
        echo "iは0に等しい";
        break;
    case 1:
        echo "iは1に等しい";
        break;
    case 2:
        echo "iは2に等しい";
        break;
    defaulr: // t を r とタイポ
       echo "iは0,1,2に等しくない";
}
// $i が 0, 1, 2 以外の場合の出力は「なし」。
?>

出力はなく、構文エラー、実行時エラーにもなりません。

つまり、実行されることを期待しているコードが、実は実行されていないことに、とても気付きにくいことを意味します。

こういう結果になることが、常識なのかは分かりませんが、個人的にはまったく知らなかったし、とても意外でした。

この挙動をするのは PHP 5.3 以降

上記のような挙動をするのは、PHP 5.3 以降になります。
たとえば、PHP 5.2 で同じコードを実行すると、以下のように構文エラーが発生します。

<?php
switch ($i) {
    case 0:
        echo "iは0に等しい";
        break;
    case 1:
        echo "iは1に等しい";
        break;
    case 2:
        echo "iは2に等しい";
        break;
    defaulr: // t を r とタイポ
       echo "iは0,1,2に等しくない";
}
// Parse error: syntax error, unexpected ':' in ...
?>

なぜなんでしょう。

勘の良い人は、PHP 5.3 以降という事実から、すぐにあたりを付けられるかも知れません。
そうです。PHP 5.3 で登場した、goto演算子の存在が影響しています。

goto演算子は、他のプログラミング言語でも悪名高い?いわゆる、gotoと同じことができる演算子です。マニュアルのサンプルコードで、その動作を確認してみます。

<?php
goto a;
echo 'Foo';

a:
echo 'Bar';

// 上の例の出力は、'Bar'
?>

ラベルを指定することで、ラベルの定義場所にジャンプできるという機能です。便利そうですね。
便利で強力である反面、これを乱用すると、あっという間に人間が理解できないコードができあがるのも事実であるため、悪名高くなってしまっているわけですが、そのあたりの話は今回は関係ありませんのでスルーします。

ここで重要なのは、goto演算子の登場により、ラベルという概念が追加になっている点です。そして、その書式はどこかで見たことがある書式ですね。
そうです。switch文の中に現れる、case:default:と同じです。

どうやら、そのあたりの変更によって、PHP 5.3 以降では構文エラーにならなくなった雰囲気を感じますね。たぶんビンゴなのでしょうけど、ちゃんと確認しないとなんとなく気持ちが悪いものです。
せっかくなので、もう少し踏み込んでみましょう。

yacc のルールを確認してみる

構文エラーになるならないの話ですので、確認することは、yacc のルールということになります。

yacc については、私はまったく詳しくないため、詳細な説明はしませんが、有名なC言語のパーサー生成プログラムです。拡張子.yである yacc ファイルを入力として、C言語で書かれたパーサーを出力します。
入力ファイルには、トークンの定義や構文のルール定義などが記述されています。yacc によって生成されたパーサーは、そのルール定義を用いて、レキサーによって字句解析されたPHPコードのトークンをパースするわけですので、構文エラーになるかどうかは、ルール定義を確認すれば分かりそうです。

ではまず、PHP 5.2.17 の yacc ファイルを確認してみます。確認するファイルは、Zend/zend_language_parser.yです。
(抜粋です。)

inner_statement_list:
>--->---inner_statement_list  { zend_do_extended_info(TSRMLS_C); } inner_statement { HANDLE_INTERACTIVE(); }
>---|>--/* empty */
;


inner_statement:
>--->---statement
>---|>--function_declaration_statement
>---|>--class_declaration_statement
>---|>--T_HALT_COMPILER '(' ')' ';'   { zend_error(E_COMPILE_ERROR, "__HALT_COMPILER() can only be used from the outermost scope"); }
;


statement:
>--->---unticked_statement { zend_do_ticks(TSRMLS_C); }
;

unticked_statement:
>--->---'{' inner_statement_list '}'
>---|>--T_IF '(' expr ')' { zend_do_if_cond(&$3, &$4 TSRMLS_CC); } statement { zend_do_if_after_statement(&$4, 1 TSRMLS_CC); } elseif_list else_single { zend_do_if_end
>---|>--T_IF '(' expr ')' ':' { zend_do_if_cond(&$3, &$4 TSRMLS_CC); } inner_statement_list { zend_do_if_after_statement(&$4, 1 TSRMLS_CC); } new_elseif_list new_else_
>---|>--T_WHILE '(' { $1.u.opline_num = get_next_op_number(CG(active_op_array));  } expr  ')' { zend_do_while_cond(&$4, &$5 TSRMLS_CC); } while_statement { zend_do_whi
>---|>--T_DO { $1.u.opline_num = get_next_op_number(CG(active_op_array));  zend_do_do_while_begin(TSRMLS_C); } statement T_WHILE '(' { $5.u.opline_num = get_next_op_nu
>---|>--T_FOR
>--->--->---'('
>--->--->--->---for_expr
>--->--->---';' { zend_do_free(&$3 TSRMLS_CC); $4.u.opline_num = get_next_op_number(CG(active_op_array)); }
>--->--->--->---for_expr
>--->--->---';' { zend_do_extended_info(TSRMLS_C); zend_do_for_cond(&$6, &$7 TSRMLS_CC); }
>--->--->--->---for_expr
>--->--->---')' { zend_do_free(&$9 TSRMLS_CC); zend_do_for_before_statement(&$4, &$7 TSRMLS_CC); }
>--->--->---for_statement { zend_do_for_end(&$7 TSRMLS_CC); }
>---|>--T_SWITCH '(' expr ')'>--{ zend_do_switch_cond(&$3 TSRMLS_CC); } switch_case_list { zend_do_switch_end(&$6 TSRMLS_CC); }
>---|>--T_BREAK ';'>>--->--->---{ zend_do_brk_cont(ZEND_BRK, NULL TSRMLS_CC); }
>---|>--T_BREAK expr ';'>--->---{ zend_do_brk_cont(ZEND_BRK, &$2 TSRMLS_CC); }
>---|>--T_CONTINUE ';'>->--->---{ zend_do_brk_cont(ZEND_CONT, NULL TSRMLS_CC); }
>---|>--T_CONTINUE expr ';'>>---{ zend_do_brk_cont(ZEND_CONT, &$2 TSRMLS_CC); }


switch_case_list:
>--->---'{' case_list '}'>-->--->--->--->---{ $$ = $2; }
>---|>--'{' ';' case_list '}'>-->--->--->---{ $$ = $3; }
>---|>--':' case_list T_ENDSWITCH ';'>-->---{ $$ = $2; }
>---|>--':' ';' case_list T_ENDSWITCH ';'>--{ $$ = $3; }
;


case_list:
>--->---/* empty */>{ $$.op_type = IS_UNUSED; }
>---|>--case_list T_CASE expr case_separator { zend_do_extended_info(TSRMLS_C);  zend_do_case_before_statement(&$1, &$2, &$3 TSRMLS_CC); } inner_statement_list { zend_
>---|>--case_list T_DEFAULT case_separator { zend_do_extended_info(TSRMLS_C);  zend_do_default_before_statement(&$1, &$2 TSRMLS_CC); } inner_statement_list { zend_do_c
;


case_separator:
>--->---':'
>---|>--';'
;

順を追って見ていきましょう。

まず、switch文の開始については、以下のような定義になっています。

unticked_statement:
(省略)
>---|>--T_SWITCH '(' expr ')'>--{ zend_do_switch_cond(&$3 TSRMLS_CC); } switch_case_list { zend_do_switch_end(&$6 TSRMLS_CC); }
(省略)

unticked_statementに含まれるトークンであり、switch文の構文が定義されています。case部分については、switch_case_listで定義されているようですので見てみます。

switch_case_list:
>--->---'{' case_list '}'>-->--->--->--->---{ $$ = $2; }
>---|>--'{' ';' case_list '}'>-->--->--->---{ $$ = $3; }
>---|>--':' case_list T_ENDSWITCH ';'>-->---{ $$ = $2; }
>---|>--':' ';' case_list T_ENDSWITCH ';'>--{ $$ = $3; }
;

ふむふむ。caseと記述する部分についてはさらにcase_listとして定義されているようなので、そちらを見てみます。

case_list:
>--->---/* empty */>{ $$.op_type = IS_UNUSED; }
>---|>--case_list T_CASE expr case_separator { zend_do_extended_info(TSRMLS_C);  zend_do_case_before_statement(&$1, &$2, &$3 TSRMLS_CC); } inner_statement_list { zend_
>---|>--case_list T_DEFAULT case_separator { zend_do_extended_info(TSRMLS_C);  zend_do_default_before_statement(&$1, &$2 TSRMLS_CC); } inner_statement_list { zend_do_c
;

case_listで書ける内容は、T_CASEまたはT_DEFAULT(つまり、caseまたはdefault)という定義になっていますね。そしてそれぞれの内容については、inner_statement_list でなければならないと定義されているようです。

では、お次は、inner_statement_listの定義を見てみましょう。

inner_statement_list:
>--->---inner_statement_list  { zend_do_extended_info(TSRMLS_C); } inner_statement { HANDLE_INTERACTIVE(); }
>---|>--/* empty */
;

inner_statement_listは、inner_statement_listinner_statementを連結した定義のようですので、続いてinner_statementの定義を見てみます。

inner_statement:
>--->---statement
>---|>--function_declaration_statement
>---|>--class_declaration_statement
>---|>--T_HALT_COMPILER '(' ')' ';'   { zend_error(E_COMPILE_ERROR, "__HALT_COMPILER() can only be used from the outermost scope"); }
;

inner_statementは、statementなどで書かれる必要があるようですね。では、statementの定義を見てみましょう。

statement:
>--->---unticked_statement { zend_do_ticks(TSRMLS_C); }
;

おや。unticked_statementで定義されているようですが、unticked_statementは、そもそもはじめに switchを含んでいた定義ですね。つまり、switch文の中にswitch文が書けるということが分かりますね。(実際に書けます。)

ここまで確認したことをまとめると、switch文の中身には、statementで定義されている構文を含めることができるというのは、まず言えそうです。

さて、上記を踏まえて今度は PHP 5.3.0 の yacc ルールを見てみましょう。

ここでは、核心に迫る部分のみ記述します。

statement:
>--->---unticked_statement { zend_do_ticks(TSRMLS_C); }
>---|>--T_STRING ':' { zend_do_label(&$1 TSRMLS_CC); }
;

見て分かるとおり statementの定義として、T_STRING ':'という構文が追加されていますね。
T_STRINGというトークンは、selfparent、関数名などのクォートされていない文字列を示すトークンです。

これは、まさに PHP 5.3 以降で導入された、goto演算子のためのラベルの書式に他なりません。

ここで重要なのは、goto演算子の登場により、ラベルという概念が追加になっている点です。そして、その書式はどこかで見たことがある書式ですね。
そうです。switch文の中に現れる、case:default:と同じです。

どうやら、そのあたりの変更によって、PHP 5.3 以降では構文エラーにならなくなった雰囲気を感じますね。

上記で立てた仮説を、パーサーが使用する構文ルールレベルで裏付けることが出来ました。はー、スッキリ。

まとめ

PHP 5.3 以降では、switch文の中に構文上含めることができるものに、ラベルが追加されているため、default:をタイポしてdefaulr:と書いてしまった場合、それはラベルとして解析されているので、構文エラーにならないということが分かりました。

このため、先に述べていますが、デフォルト処理として実行されることを期待しているコードが、タイポにより実行されていないけど気付いていないという状況が発生し得ることについては、注意が必要そうです。
もっとも、ちゃんとユニットテストを書いていれば、テストの段階で検知できるので、問題にはならないでしょう。

今回、ひょんなことから、最終的に PHP の実行のしくみのおさらいや、レキサーとパーサーの関係や、yacc の概要について知ることが出来ました。普段は、PHP のコードを書くレイヤであれこれしているわけですが、たまには言語処理系の実装に飛び込んでみるのもおもしろいものだなと思いました。

おまけとして、今回の事象は、Ruby の case文でも同様の挙動でしたので、そちらはまた Ruby としての理由がありそうですし、見てみるとおもしろいかも知れませんね。(Ruby の caseを PHP のswitchと同一視しているわけではありません。)