PHPにおけるプログラムの実行フローは、ソースコードが抽象構文木(AST)に変換される「解析フェーズ」から始まります。この重要なプロセスでは、字句解析器と構文解析器という二つの主要なコンポーネントが連携して動作します。
- 字句解析(Lexical Analysis):ソースコードを意味のある最小単位である「トークン」のストリームに分解します。PHPでは主にre2cによってこの作業が行われます。
- 構文解析(Syntax Analysis):字句解析器から得られたトークンが、言語の文法規則に則ってどのように構成されているかを確認し、最終的にASTを構築します。PHPではBisonがこの役割を担っています。
例えば、次のPHPコードスニペットを考えてみましょう。
$value = 10 + 5;
字句解析器はこれを$value, =, 10, +, 5といった個別のトークンに分割します。その後、構文解析器はこれらのトークンが「変数$valueに10と5の加算結果を代入する」という構文規則に従っていることを認識し、それに対応するASTを構築します。
字句・構文規則の簡略化された表現
字句解析器と構文解析器の動作を理解するために、簡略化された規則の例を見てみましょう。
字句規則(re2c風)
以下は、re2cで記述される字句規則の概念的な例です。変数名や数値リテラルを識別します。
/*!re2c
// 識別子(変数名など)のパターン
IDENTIFIER [a-zA-Z_][a-zA-Z0-9_]*
// 10進数リテラルのパターン
DEC_NUM [0-9]+
// 実際の字句規則
"$"{IDENTIFIER} { return TOKEN_VARIABLE; } // 例: $myVar -> TOKEN_VARIABLE
{DEC_NUM} { return TOKEN_INTEGER; } // 例: 123 -> TOKEN_INTEGER
"=" { return TOKEN_ASSIGN; }
"+" { return TOKEN_PLUS; }
"-" { return TOKEN_MINUS; }
*/
構文規則(Bison風)
次に、Bisonで記述される構文規則の概念的な例です。先ほどの字句解析器が生成したトークンを受け取り、文の構造を定義します。
// トークンの定義
%token TOKEN_VARIABLE "変数"
%token TOKEN_INTEGER "整数リテラル"
%token TOKEN_ASSIGN "="
%token TOKEN_PLUS "+"
%token TOKEN_MINUS "-"
// 構文規則の定義
program:
statement_list
;
statement_list:
statement_list statement
| /* empty */
;
statement:
assignment_statement ';'
;
assignment_statement:
TOKEN_VARIABLE TOKEN_ASSIGN expression
{ /* ここでASTノードを構築するアクションを実行 */ }
;
expression:
TOKEN_INTEGER
{ /* 数値ノードを作成 */ }
| expression TOKEN_PLUS expression
{ /* 加算ノードを作成 */ }
| expression TOKEN_MINUS expression
{ /* 減算ノードを作成 */ }
;
このexpressionルールは再帰的に定義されており、$x = 10 + 5 - 2; のような複雑な算術式も解析できます。
PHP内部での解析プロセス
PHPのコンパイルフェーズにおける解析は、主にzendparse()関数によって行われます。この関数は実際にはBisonが生成するyyparse()関数の別名です。
#define yyparse zendparse
yyparse()は、文法規則に基づいて継続的にyylex()(PHPではzendlex())を呼び出し、必要なトークンを取得します。
#define yylex zendlex
// zend_compile.c 内
int zendlex(zend_parser_stack_elem *elem) {
zval zv;
int retval;
again:
ZVAL_UNDEF(&zv);
retval = lex_scan(&zv); // 字句解析を実行し、トークンタイプと値をzvに格納
if (EG(exception)) { // 構文エラー発生時
return T_ERROR;
}
if (Z_TYPE(zv) != IS_UNDEF) { // zvに値が格納されていれば、ASTノードとして保持
elem->ast = zend_ast_create_zval(&zv);
}
return retval; // トークンタイプを返す
}
ここで重要な点がいくつかあります。
- トークン値の取得:字句解析器が識別したトークン自体の内容は、
zval構造体を通じてやり取りされます。lex_scan関数はzval*を引数に取り、見つかったトークンの内容(例:変数名、数値リテラルの値)をこのzvalに格納します。例えば、変数
$myVarを解析する字句規則は、$myVarからmyVarの部分を抽出し、zvalにコピーします。// PHPの字句規則(一部抜粋) <ST_IN_SCRIPTING, ...>"$"{LABEL} { // マッチしたトークンの値をzvalに保存 // yytextはマッチした文字列の先頭を指し、yylengは長さを表す zend_copy_value(zendlval, (yytext+1), (yyleng-1)); // '$'を除いた部分をコピー RETURN_TOKEN(T_VARIABLE); }zendlvalはlex_scanに渡されるzval*に対応します。 - セマンティック値の型(YYSTYPE):Bisonは字句解析器からトークンタイプと、それに付随するセマンティック値(トークン値)を受け取ります。セマンティック値のデフォルトの型は
intですが、PHPではこれをzend_parser_stack_elemという共用体(union)で定義しています。これがzendlex関数の引数型となる理由です。#define YYSTYPE zend_parser_stack_elem typedef union _zend_parser_stack_elem { zend_ast *ast; // 抽象構文木ノード zend_string *str; // 文字列値 zend_ulong num; // 整数値 } zend_parser_stack_elem;この共用体の中で最も頻繁に使われるのは
zend_ast *astです。そのため、zend_language_parser.yでは、%token <ast>や%type <ast>ディレクティブを使って、構文規則のアクション内で$$(現在の規則のセマンティック値)や$1, $2, ...(子要素のセマンティック値)がzend_ast*型として扱われるように指定されています。// zend_language_parser.y 内の定義例 %token <ast> T_LNUMBER "integer number (T_LNUMBER)" %token <ast> T_DNUMBER "floating-point number (T_DNUMBER)" %token <ast> T_STRING "identifier (T_STRING)" %token <ast> T_VARIABLE "variable (T_VARIABLE)" %type <ast> top_statement namespace_name name statement function_declaration_statement %type <ast> class_declaration_statement trait_declaration_statement
抽象構文木の構築
構文解析器は、startルールから始まり、階層的に文法規則を照合していきます。規則がマッチするたびに、対応するASTノードが生成され、最終的に全体のASTのルートノードがCG(ast)に格納されます。
%% /* Rules */
start:
top_statement_list { CG(ast) = $1; }
;
top_statement_list:
top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); }
| /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;
最初に空のリストを表すルートノードが作成され、その後、各トップレベルのステートメント(top_statement)に対応するASTノードがこのリストに追加されていきます。
ASTノードの構造
主要なASTノードの構造体はzend_astとzend_ast_listです。
enum _zend_ast_kind {
ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT, // 定数値ノード
ZEND_AST_ZNODE, // 古いopcodeノード (PHP7では非推奨)
/* リストノード */
ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT, // 引数リストノード
// ... その他多くのノード種類
};
struct _zend_ast {
zend_ast_kind kind; // ノードの種類 (ZEND_AST_* 列挙型)
zend_ast_attr attr; // 付加属性(ノードの種類による)
uint32_t lineno; // ソースコード上の行番号
zend_ast *child[1]; // 子ノードの配列(struct hackを利用)
};
typedef struct _zend_ast_list {
zend_ast_kind kind;
zend_ast_attr attr;
uint32_t lineno;
uint32_t children; // 子ノードの数
zend_ast *child[1]; // 子ノードの配列
} zend_ast_list;
kindフィールドはノードの種類を識別し、その後のオペコード生成の基礎となります。また、関数やクラス宣言のような特定の構造には、より詳細な情報を持つzend_ast_decl構造体が使用されます。
typedef struct _zend_ast_decl {
zend_ast_kind kind;
zend_ast_attr attr; // 未使用(互換性のため)
uint32_t start_lineno; // 宣言の開始行番号
uint32_t end_lineno; // 宣言の終了行番号
uint32_t flags;
unsigned char *lex_pos;
zend_string *doc_comment; // ドキュメントコメント
zend_string *name; // 宣言の名前(関数名、クラス名など)
zend_ast *child[4]; // 親クラス、インターフェース、宣言内のステートメントなどが格納される
} zend_ast_decl;
簡単なPHPコードのAST
例として、以下のPHPコードがどのようにASTに変換されるかを概念的に見てみましょう。
$variableA = 123;
$variableB = "Hello PHP!";
echo $variableA, $variableB;
このコードは、top_statement_listのルートノードの下に、3つのtop_statement(代入文2つ、echo文1つ)に対応するASTノードを生成します。各ステートメントノードの内部では、変数、リテラル(数値、文字列)、演算子、関数呼び出しなどがそれぞれの子ノードとして構造化されます。例えば、$variableA = 123;は、代入演算子の親ノードを持ち、その子に変数$variableAを表すノードと整数リテラル123を表すノードを持つツリー構造になります。