PHPのコア:字句解析と構文解析によるAST生成

PHPにおけるプログラムの実行フローは、ソースコードが抽象構文木(AST)に変換される「解析フェーズ」から始まります。この重要なプロセスでは、字句解析器と構文解析器という二つの主要なコンポーネントが連携して動作します。

  • 字句解析(Lexical Analysis):ソースコードを意味のある最小単位である「トークン」のストリームに分解します。PHPでは主にre2cによってこの作業が行われます。
  • 構文解析(Syntax Analysis):字句解析器から得られたトークンが、言語の文法規則に則ってどのように構成されているかを確認し、最終的にASTを構築します。PHPではBisonがこの役割を担っています。

例えば、次のPHPコードスニペットを考えてみましょう。

$value = 10 + 5;

字句解析器はこれを$value, =, 10, +, 5といった個別のトークンに分割します。その後、構文解析器はこれらのトークンが「変数$value105の加算結果を代入する」という構文規則に従っていることを認識し、それに対応する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; // トークンタイプを返す
}

ここで重要な点がいくつかあります。

  1. トークン値の取得:字句解析器が識別したトークン自体の内容は、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);
    }
            

    zendlvallex_scanに渡されるzval*に対応します。

  2. セマンティック値の型(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_astzend_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を表すノードを持つツリー構造になります。

タグ: PHP AST 字句解析 構文解析 Bison

5月27日 00:31 投稿