PHP で簡単なインタプリタをつくる

この記事は [オープンロジアドベントカレンダー](https://qiita.com/advent-calendar/2021/openlogi) の5日目の記事です。

## はじめに

最近読んだ [Go言語でつくるインタプリタ](https://www.oreilly.co.jp/books/9784873118222/) という書籍がとても面白かったので、復習も兼ねて弊社のサーバーサイド開発で利用されている PHP で再実装してみようと思います。インタプリタといっても、今回は四則演算のみをサポートする簡易なものに留めます。

例えば、 `1 + 2` という入力を受けると、`3` という出力を返します。スペースは含まれていても無視されます。

```bash
$ ./run.sh
> 1 + 2
3
```

また、優先順位もサポートします。例えば `1 + 2 * (3 + 4)` を受けると `15` を返します。

```bash
$ ./run.sh
> 1 + 2 * (3 + 4)
15
```

完成したソースコードは [こちら](https://github.com/yshir/calculator-php) にアップしています。

尚、詳しい説明に関しては今回省略します。適宜文献などを参照して下さい。

## プログラムの流れ

インタプリタとはなんでしょうか。 [Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF) によると、おおよそ以下のようなものを指すようです。

> インタプリタは、およそ次のいずれかの動作をするプログラムである。

> 1. ソースコードを直接解釈実行する。
> 2. ソースコードを何らかの効率的な中間的なコード（中間表現）に、最初に全て変換して、あるいは、逐次変換しながら、解釈実行する。
> 3. 何らかのコンパイラが生成し出力した、何らかの効率的な（マシンに依存しない、あるいは、マシン依存の）中間表現を解釈実行する。

ソースコードをその場で解釈実行する、というのがポイントのようです。今回は 2. の説明にあるような、ソースコードをそのまま評価するのではなく、プログラムが扱いやすい中間表現に一旦変換してから、それを対象として評価を行うようなインタプリタを作成します。

具体的には、以下の3つの工程に分けて処理を行います。

1. 字句解析 (Lexer) ... 文字列 (ソースコード) → トークン列 に変換
2. 構文解析 (Parser) ... トークン列 → 抽象構文木 に変換
3. 評価 (Evaluator) ... 抽象構文木 → 計算結果 に変換

![aaa.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224299/22f4de36-16d8-75f3-173f-e5efc29788ea.jpeg)


まず `字句解析` で、文字列を `トークン列` に変換します。トークンというのは `+`, `(`, あるいは `10` のような、意味のある最小単位のかたまりのことです。例えば `1 + 2` という文字列が与えられたときに、字句解析では `1`, `+`, `2` というトークンを生成し、列にして返します。この際、空白文字は意味のない文字なので無視されます。

次に `構文解析` で、トークン列を `抽象構文木` に変換します。構文木というのは、ソースコードの解析結果を木構造で表したものを指します。そして構文木から余分な情報 (優先順位のためのカッコなど) を排したものを抽象構文木といいます。構文解析の手法はいくつかありますが、今回は [再帰下降構文解析](https://ja.wikipedia.org/wiki/%E5%86%8D%E5%B8%B0%E4%B8%8B%E9%99%8D%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90) を用いて行います。

最後に `評価` です。抽象構文木を解釈、実行します。これにより最終的な `計算結果` が得られることになります。

コードで書くと、以下のような感じです。

```php
// ユーザー入力を受け取る
$stdin = trim(fgets(STDIN)); // <- ex. "1 + 2"

// 字句解析
$lexer = new Lexer();
$tokens = $lexer->lex($stdin); // 文字列を受け取ってトークン列を返す

// 構文解析
$parser = new Parser();
$ast = $parser->parse($tokens); // トークン列を受け取って抽象構文木を返す

// 評価
$evaluator = new Evaluator();
$result = $evaluator->eval($ast); // 抽象構文木を受け取って評価した結果を返す

print_r($result); // "3" を出力
```

それでは、 `Lexer`, `Parser`, `Evaluator` をそれぞれ実装していきます。

### 字句解析

字句解析では、文字列をトークンという単位に分割し、その変換したトークンの列を返すことが目標です。これをコードで書くと以下のようになります。

```php:lexer.php
<?php

// トークンを表現するクラス
class Token
{
    public const PLUS = "+";
    public const MINUS = "-";
    public const ASTERISK = "*";
    public const SLASH = "/";
    public const LPAREN = "(";
    public const RPAREN = ")";
    public const NUM = "NUM";

    public string $kind;
    public ?int $value; // num のときは数値が入る

    public function __construct(string $kind, ?int $value = null)
    {
        $this->kind = $kind;
        $this->value = $value;
    }
}

// 字句解析を行うクラス
class Lexer
{
    private string $input; // 入力文字列
    private int $pos; // 次に読む文字のインデックス。この $pos を移動させていきながら1文字ずつ読んでいく

    /**
     * @return Token[]
     */
    public function lex(string $input): array
    {
        $this->input = $input;
        $this->pos = 0;

        $tokens = [];

        while ($this->char()) {
            $this->skipSpaces();

            $ch = $this->char();
            switch ($ch) {
                case '+':
                    $tokens[] = new Token(Token::PLUS); $this->pos++; break;
                case '-':
                    $tokens[] = new Token(Token::MINUS); $this->pos++; break;
                case '*':
                    $tokens[] = new Token(Token::ASTERISK); $this->pos++; break;
                case '/':
                    $tokens[] = new Token(Token::SLASH); $this->pos++; break;
                case '(':
                    $tokens[] = new Token(Token::LPAREN); $this->pos++; break;
                case ')':
                    $tokens[] = new Token(Token::RPAREN); $this->pos++; break;
                default:
                    if (preg_match('/^[0-9]$/', $ch)) {
                        $tokens[] = new Token(Token::NUM, $this->readNumber());
                        break;
                    }
                    throw new Exception("invalid char: {$ch}");
            }
        }

        return $tokens;
    }

    // 次の文字を返す (最後まで読み終えていれば NULL)
    private function char(): ?string
    {
        return $this->input[$this->pos];
    }

    // 空白文字を読み飛ばす
    private function skipSpaces(): void
    {
        while ($this->char() && ctype_space($this->char())) {
            $this->pos++;
        }
    }

    // 数値を読んで返す
    private function readNumber(): int
    {
        $prev = $this->pos;
        while ($this->char() && preg_match('/^[0-9]$/', $this->char())) {
            $this->pos++;
        }
        return (int) mb_substr($this->input, $prev, $this->pos - $prev);
    }
}

```

`Token` クラスでトークンを表現しています。例えば `new Token(Token::NUM, 10)` とすると、これは数値の10を表すトークンです。

`Lexer` クラスの `lex` メソッドで字句解析を実行します。受け取った文字列をループの中で1文字ずつ読み取りながら、途中で現れる空白文字をスキップしつつ、対応する `Token` を素直に生成していきます。最後まで文字を読み終えると、ループを抜けて生成したトークン列を配列で返します。

### 構文解析

次に構文解析です。構文解析は、トークン列を受け取り抽象構文木を返すことが目標です。これをコードで書くと以下のようになります。

```php:parser.php
<?php

require_once('lexer.php');

// 抽象構文木のノード
interface Node
{
}

// 数値ノード
class NumNode implements Node
{
    public int $value;

    public function __construct(int $value)
    {
        $this->value = $value;
    }
}

// 単項演算ノード
class UnaryNode implements Node
{
    public UnaryOp $op;
    public Node $rhs;

    public function __construct(UnaryOp $op, Node $rhs)
    {
        $this->op = $op;
        $this->rhs = $rhs;
    }
}

// 二項演算ノード
class BinaryNode implements Node
{
    public BinaryOp $op;
    public Node $lhs;
    public Node $rhs;

    public function __construct(BinaryOp $op, Node $lhs, Node $rhs)
    {
        $this->op = $op;
        $this->lhs = $lhs;
        $this->rhs = $rhs;
    }
}

// 単項演算子
class UnaryOp
{
    public const PLUS = "+";
    public const MINUS = "-";

    public string $kind;

    public function __construct(string $kind)
    {
        $this->kind = $kind;
    }
}

// 二項演算子
class BinaryOp
{
    public const ADD = "+";
    public const SUB = "-";
    public const MUL = "*";
    public const DIV = "/";

    public string $kind;

    public function __construct(string $kind)
    {
        $this->kind = $kind;
    }
}

// 構文解析を行うクラス
class Parser
{
    private array $tokens; // 入力トークン列
    private int $pos; // 次に読むトークンのインデックス

    // program = expr ;
    // expr = term ("+"|"-" term)* ;
    // term = unary ("*"|"/" unary)* ;
    // unary = ("+"|"-")? factor ;
    // factor = number | "(" expr ")" ;
    public function parse(array $tokens): Node
    {
        $this->tokens = $tokens;
        $this->pos = 0;

        return $this->parseProgram();
    }

    // program = expr ;
    private function parseProgram(): Node
    {
        return $this->parseExpr();
    }

    // expr = term ("+"|"-" term)* ;
    private function parseExpr(): Node
    {
        $lhs = $this->parseTerm();
        while (1) {
            if ($this->peek(Token::PLUS)) {
                $this->pos++;
                $lhs = new BinaryNode(new BinaryOp(BinaryOp::ADD), $lhs, $this->parseTerm());
            } elseif ($this->peek(Token::MINUS)) {
                $this->pos++;
                $lhs = new BinaryNode(new BinaryOp(BinaryOp::SUB), $lhs, $this->parseTerm());
            } else {
                return $lhs;
            }
        }
    }

    // term = unary ("*"|"/" unary)* ;
    private function parseTerm(): Node
    {
        $lhs = $this->parseUnary();
        while (1) {
            if ($this->peek(Token::ASTERISK)) {
                $this->pos++;
                $lhs = new BinaryNode(new BinaryOp(BinaryOp::MUL), $lhs, $this->parseUnary());
            } elseif ($this->peek(Token::SLASH)) {
                $this->pos++;
                $lhs = new BinaryNode(new BinaryOp(BinaryOp::DIV), $lhs, $this->parseUnary());
            } else {
                return $lhs;
            }
        }
    }

    // unary = ("+"|"-")? factor ;
    private function parseUnary(): Node
    {
        if ($this->peek(Token::PLUS)) {
            $this->pos++;
            return new UnaryNode(new UnaryOp(UnaryOp::PLUS), $this->parseFactor());
        } elseif ($this->peek(Token::MINUS)) {
            $this->pos++;
            return new UnaryNode(new UnaryOp(UnaryOp::MINUS), $this->parseFactor());
        } else {
            return $this->parseFactor();
        }
    }

    // factor = number | "(" expr ")" ;
    private function parseFactor(): Node
    {
        if ($this->peek(Token::NUM)) {
            $node = new NumNode($this->token()->value);
            $this->pos++;
            return $node;
        }

        $this->expect(Token::LPAREN);
        $expr = $this->parseExpr();
        $this->expect(Token::RPAREN);
        return $expr;
    }

    // 次に読むトークン
    private function token() : ?Token
    {
        return $this->tokens[$this->pos];
    }

    // 次に読むトークンを確認する。期待と一致していれば true
    private function peek(string $tokenKind): bool
    {
        return $this->token() && $this->token()->kind === $tokenKind;
    }

    // トークンを1つ読みすすめる。ただし期待と一致してなければエラー
    private function expect(string $tokenKind): void
    {
        if (!$this->peek($tokenKind)) {
            throw new Exception($tokenKind." expected, but got ".$this->token()->kind);
        }
        $this->pos++;
    }
}
```

構文解析はいくつか手法がありますが、今回は[再起下降構文解析](https://ja.wikipedia.org/wiki/再帰下降構文解析)で実装していきます。

`Parser` クラスの `parse` メソッドで字句解析を実行します。各メソッドで自分の担当するノードを生成しながら、構文木をトップダウンで組み立てていきます。

### 評価

最後に評価を行います。ここでは、構文木を受け取り最終的な計算結果を返すことが目標です。これをコードで書くと以下のようになります。

```php:evaluator.php
<?php

require_once('lexer.php');
require_once('parser.php');

class Evaluator
{
    public function eval(Node $node): int
    {
        switch (get_class($node)) {
            case NumNode::class:
                return $node->value;
            case UnaryNode::class:
                return $this->evalUnaryNode(
                    $node->op,
                    $this->eval($node->rhs)
                );
            case BinaryNode::class:
                return $this->evalBinaryNode(
                    $node->op,
                    $this->eval($node->lhs),
                    $this->eval($node->rhs)
                );
            default:
                throw new Exception('unreachable');
        }
    }

    private function evalBinaryNode(BinaryOp $op, int $lhs, int $rhs): int
    {
        switch ($op->kind) {
            case BinaryOp::ADD:
                return $lhs + $rhs;
            case BinaryOp::SUB:
                return $lhs - $rhs;
            case BinaryOp::MUL:
                return $lhs * $rhs;
            case BinaryOp::DIV:
                if ($rhs === 0) {
                    throw new Exception('devision by zero');
                }
                return $lhs / $rhs;
            default:
                throw new Exception('unreachable');
        }
    }

    private function evalUnaryNode(UnaryOp $op, int $value): int
    {
        switch ($op->kind) {
            case UnaryOp::PLUS:
                return $value;
            case UnaryOp::MINUS:
                return $value * -1;
            default:
                throw new Exception('unreachable');
        }
    }
}
```

ここまでくるとパーツは揃っているので、あとは計算をするだけです。 `Eval` クラスの `eval` メソッドで評価を実行します。ここでも再帰的に処理を行っていますが、最終的に数値を返して終わるのでこの処理は収束します。

以上で、四則演算をするプログラムは完成です。

## おわりに

今回は PHP で書籍の内容を再実装してみましたが、同じ題材をサンプルコードとは別の言語で書いてみると、単なる写経よりも高い学習効果が得られそうだと感じました。今後も積極的に取り入れていこうと思います。

また、今回は簡単な四則演算器までを実装しましたが、書籍では文字列, 配列, ハッシュなどのさらなるデータ型や、変数や関数の実装まで含まれているので、興味がある方は是非読んでみて下さい。



## 参考

- [Go言語でつくるインタプリタ](https://www.oreilly.co.jp/books/9784873118222/)
- [インタプリタ - Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF)
- [抽象構文木 - Wikipedia](https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8)
- [再起下降構文解析 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%86%8D%E5%B8%B0%E4%B8%8B%E9%99%8D%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90)