Open22

PHPでのパーサー、パーサージェネレータについて

tristartristar

PHPで利用出来るパーサー、パーサージェネレータでアクティブにメンテナンスされているものが少ない気がし、
独自の構文を持った式をパースする処理を実装する場合どのようなロジックが考えられるか調査しようと思います。

候補としては、自前でLexer,Compilerを実装しているテンプレートエンジンのTwigと、
LaravelのテンプレートエンジンであるBlade。Bladeは内部でどんな風にパースしているかまだ見たことがありません。

tristartristar

PHP製のパーサー、パーサージェネレータの状況について

TODO: 後で追記する

Project 調査結果
nikic/php-parser PHPの構文に対するパーサー。今回はPHPの構文とは異なるものをパースしたいため候補になりません。パーサーをPHPで実装しているので、パーサーを実装する方法としては参考になるかも。
tristartristar
tristartristar

最初に注目するのはCompiler。
https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Compiler.php#L68

パラメータとしてはNodeを受け取っている。
NodeはBlockNode, ForNodeなどがあり、compileメソッドを持っている。
compileメソッドはcompiler->writeを呼び出してPHPコードを出力する実装になっているので、
トークン分割された後の粒度のものに対しcompileメソッドを呼び出すことでPHPへの変換を行っていると思われる。
(トークン分割はもっと前段で行われている)

https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Node/ForNode.php#L42-L74

tristartristar

Twigのトークン分割

tristartristar

Lexerクラスは以下のようなものが定義されており、このクラスで「{% と %} で括られた範囲をブロックとする」ような定義がされている。
(オプションで変更可能なので定数にはなっていない)
スタートから終端でを抜き出す正規表現も定義されていて、正規表現は「whitespace_trim」を行うかどうかも含めているためか複雑になっている。

https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Lexer.php#L57-L89

tristartristar

最初にpreg_match_allで、 $this->regexes['lex_tokens_start'] に合致するものを全部抜き出している。
https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Lexer.php#L180-L183

$this->regexes['lex_tokens_start'] は以下のようになっていて、各種タグの開始とマッチする内容。

https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Lexer.php#L147-L160

この部分は「TwigはPHPのテンプレートエンジン」ということを前提にしていて、テンプレートの大半の部分はそのままで良く、
Twigのディレクティブが埋め込まれた部分を効率よく抜き出すために書かれている気がする。
({% <!-- %} -->のようなケースは考慮されていない...?こういうのはそもそも利用側の問題...?)

tristartristar

そういう訳で、汎用的なパーサーを考える場合はこの点は採用出来なそう。

tristartristar

preg_match_allで分割した後は、各タグの開始位置をpositionsに格納した状態で以下のループを行う。
https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Lexer.php#L184-L218

  • $this->stateに「今ただのテキストを処理しているのか、Blockを処理しているのか」などの状態を格納する
    • テキストの処理中はlexData()を呼び出し、Blockの処理中はlexBlock()を呼び出すように、ステートマシンになっている
    • カッコの出現はスタック上に積み上げられていて、最後につじつまが合わなければエラーとしている
    • 処理結果はTokenStreamとして返される
tristartristar

lexBlockの場合、基本的にはすぐlexExpressionを呼び出して式をパースしている。
この部分で正規表現を使いながらスペースやオペレータ(記号)、名前、数字などにトークン分割している。
括弧は「punctuation」というトークンとして処理されている。

https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Lexer.php#L307-L382

この辺りが、式をパースするにあたって必要な考え方になりそう。

tristartristar

Twigのparse()

tristartristar

parse()はLexerにより分割されたTokenStreamを受け取って処理する。
準備段階としてget_object_vars()やunset($vars['stack'], $vars['env'], ...)が呼ばれているので、
テンプレートエンジンの変数もこの辺りに展開されていそう。
ネストしたテンプレート内で名前の衝突が起きないようにするためか、getVarName()というメソッドが変数名の接頭辞を返すようになっている。

https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/Parser.php#L53-L63

tristartristar

ForTokenParserは以下のような実装になっていて、Parser経由でtokenStreamの現在位置を取得したり解析を進められるので、
Parserとやりとりしながらfor文用にtokenの解析を進めている。
式に相当する部分のパースはさらにExpressionParserクラスで行われていて、このクラスもParserを通してtokenStreamにアクセスして解析を行っている。
https://github.com/twigphp/Twig/blob/b46e93c7257fb01b7c77768210997b1e00643b91/src/TokenParser/ForTokenParser.php#L20-L77

tristartristar

TwigのExpressionParserについて

TODO: ExpressionParserの中で演算子の優先順位なども考慮しつつNodeの生成が行われているはずなので、そのあたりをもう少し見てみる。