2019-12-03

PHPでPHPを実装する

この記事はPHP Advent Calendar 2019の3日目の記事です。


言語実装入門編として、PHPでPHPを実装してみました。

という感じで作っていきます。

実装したものはこちら↓

composer global require tzmfreedom/phphp でもインストールできます

HelloWorld

まずは composer require nikic/php-parser してPHP-Parserを追加します。

基本形はこんな感じになります。

<?php

require_once 'vendor/autoload.php';

use PhpParser\ParserFactory;
use PhpParser\Parser;
use PhpParser\NodeDumper;

class PHPInterpreter
{
    /**
     * @var Parser
     */
    private $parser;

    public function __construct(Parser $parser)
    {
        $this->parser = $parser;
    }

    public function run(string $code)
    {
        $ast = $this->parser->parse($code);

        // $dumper = new NodeDumper;
        // echo $dumper->dump($ast);

        foreach ($ast as $stmt) {
          $this->evaluate($stmt);
        }
    }

    public function evaluate($node)
    {
        switch (get_class($node)) {
// ...
        }
    }
}

$code = file_get_contents($argv[1]);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$interpreter = new PHPInterpreter($parser);
$interpreter->run($code);

ParserFactoryでパーサを生成してParser#parseを呼び出すとPHPの文字列からASTを生成してくれます。 ASTはStatementの配列になっているので evaluate で順に処理していってます。

生成されたASTを確認したい場合はNodeDumperを使います

$dumper = new PhpParser\NodeDumper;
echo $dumper->dump($ast);

例えば <?php echo "Hello"; というPHPコードの場合は以下のような文字列がダンプされます。

array(
    0: Stmt_Echo(
        exprs: array(
            0: Scalar_String(
                value: hello
            )
        )
    )
)

このPHPコードを実行するにはStmt\Echoノードに対して処理をする必要があります。 処理内容は evaluate メソッドに集約しているのでそこに追加していきます。

echo "Hello" を実行するためには以下のように記述すればOK。

public function evaluate()
{
    switch (get_class($node)) {
        case PhpParser\Node\Stmt\Echo_::class:
            $ret = $this->evaluate($node->exprs[0]);
            echo $ret['value'];
            return;
        case PhpParser\Node\Scalar\String_::class:
            return ['value' => $node->value];
    }
}

簡単のため連想配列でやりとりしてますが、ガッチリ作るならプログラム上の値を表現するクラスを作るのが良いです。

上記のように構文に応じたノードを処理していくことで、パースしたASTを使ってPHPを実行することができます。

ifの実装

ifのノードには条件のノードと、ifの処理内容のノード(Statementの配列)が入っています。

以下のコードを実行できるようにevaluate部分を修正してみます。

<?php if (true) echo "hoge";

ASTをダンプした結果はこちら↓

array(
    0: Stmt_If(
        cond: Expr_ConstFetch(
            name: Name(
                parts: array(
                    0: true
                )
            )
        )
        stmts: array(
            0: Stmt_Echo(
                exprs: array(
                    0: Scalar_String(
                        value: hello
                    )
                )
            )
        )
        elseifs: array(
        )
        else: null
    )
)

condが条件部分、stmtsがtrueのときの処理になります。 elseifs, elseは今回は割愛します。

evaluateに以下のcase文を追加します。

// ...
case PhpParser\Node\Stmt\If_::class:
    $cond = $this->evaluate($node->cond);
    if ($cond['value']) {
        foreach ($node->stmts as $stmt) {
            $this->evaluate($stmt);
        }
    }
    return;
case PhpParser\Node\Expr\ConstFetch::class:
    if ($node->name->toString() === 'true') {
        return ['value' => true];
    } elseif ($node->name->toString() === 'false') {
        return ['value' => false];
    }
    throw new Exception("no const exists");
// ...

条件を評価して値がtrueな値ならStatementを実行しています。true/falseはPHP-ParserだとConstFetchだったので、このノードに関しても処理を加えています。

変数代入

変数は連想配列で管理できます。 以下のケースを実行できるようにしてみます。

<?php 
$i = 123;
echo $i;

ASTはこんな感じ

array(
    0: Stmt_Expression(
        expr: Expr_Assign(
            var: Expr_Variable(
                name: i
            )
            expr: Scalar_LNumber(
                value: 123
            )
        )
    )
    1: Stmt_Echo(
        exprs: array(
            0: Expr_Variable(
                name: i
            )
        )
    )
)

Stmt\Expressionは値を返す式の文(Statement)になります。

PHPInterpreterに$variableEnvのプロパティを追加して、evaluateに以下のcaseを入れます。

case PhpParser\Node\Stmt\Expression::class:
    $this->evaluate($node->expr);
    return;
case PhpParser\Node\Expr\Assign::class:
    return $this->variableEnv[$node->var->name] = $this->evaluate($node->expr);
case PhpParser\Node\Expr\Variable::class:
    return $this->variableEnv[$node->name];

簡単のため一階層で管理していますが、スコープを持つ場合は親スコープへの参照を持つように変数管理をする必要があります。 また、function間ではスコープを共有しないので、そのへんもよしなに分離するように対応します。

Apacheで頑張って動かす

CGIを使えばPHPで実装したPHPをApacheで動かせます。

CGIを有効にしたら以下のシェルを叩いてCGI用のスクリプトを作ります

$ echo "$(which php) $(which phphp) \$1" > /usr/local/bin/phphp

あとはCGIの対象のディレクトリに以下のようなコードを書けばPHPで実装したPHPがWebで動きます。

#!/bin/bash /usr/local/bin/phphp
Content-type: text/html

<?php

echo "Hello<br/>";

function hoge($i) { echo $i . "<br/>"; }

hoge("World");
?>

実際に動かすときには一行目のshebangを無視するようにスクリプトを調整しないとPHP-Parserがうまく解釈しないので注意してください。

おまけ

PHPの実コードと同じような挙動で動かしてきましたが、SyntaxはPHPでPHPよりも制約が厳しい言語を実装することも可能です。 例えば、変数代入のところを修正して二重代入をエラーにする制約を入れることが出来ます

case PhpParser\Node\Expr\Assign::class:
    if (array_key_exists($node->var->name, $this->variableEnv)) {
        throw new Exception('invalid assignment: ' . $node->var->name);
    }
    return $this->variableEnv[$node->var->name] = $this->evaluate($node->expr);

また、今回はTree Walkなインタープリター実装でしたが、evaluate時にバイトコードを生成したりアセンブリに変換して実行バイナリを生成することも可能です。

このエントリーをはてなブックマークに追加