17 REPLを構文解析に対応する [オリジナル言語インタプリタを作る]

17 REPLを構文解析に対応する [オリジナル言語インタプリタを作る]

TODO を修正する (return, let)

全ての構文解析機能の実装が終了しましたが、以前作成したTODOが残ったままになっています。これを修正することで本当の完成になります。

修正する箇所は以下の2点。ParseLetStatement()ParseReturnStatement() です。

Parsing/Parser.cs

public class Parser
{
    // ..
    public LetStatement ParseLetStatement()
    {
        var statement = new LetStatement();
        statement.Token = this.CurrentToken;

        if (!this.ExpectPeek(TokenType.IDENT)) return null;

        statement.Name = new Identifier(CurrentToken, this.CurrentToken.Literal);

        if (!this.ExpectPeek(TokenType.ASSIGN)) return null;

        // TODO: 後で実装。
        while (this.CurrentToken.Type != TokenType.SEMICOLON)
        {
            // セミコロンが見つかるまで
            this.ReadToken();
        }

        return statement;
    }

    public ReturnStatement ParseReturnStatement()
    {
        var statement = new ReturnStatement();
        statement.Token = this.CurrentToken;
        this.ReadToken();

        // TODO: 後で実装。
        while (this.CurrentToken.Type != TokenType.SEMICOLON)
        {
            // セミコロンが見つかるまで
            this.ReadToken();
        }

        return statement;
    }
    // ..
}

一番最初に実装したのがこの2つの文の構文解析だったので、まだその時は式の解析手段を知りませんでした。仕方がないのでとりあえず、セミコロンが見つかるまで読み飛ばしておいて、後で式を解析できる手段を手に入れたら戻ってくることにしたのでした。

もうすでに式の構文解析はできるようになりましたので、これを修正しましょう。

修正版

public class Parser
{
    // ..
    public LetStatement ParseLetStatement()
    {
        var statement = new LetStatement();
        statement.Token = this.CurrentToken;

        if (!this.ExpectPeek(TokenType.IDENT)) return null;

        statement.Name = new Identifier(CurrentToken, this.CurrentToken.Literal);

        if (!this.ExpectPeek(TokenType.ASSIGN)) return null;

        // = を読み飛ばす
        this.ReadToken();
        // 式を解析
        statement.Value = this.ParseExpression(Precedence.LOWEST);
        // セミコロンは必須ではない
        if (this.NextToken.Type == TokenType.SEMICOLON) this.ReadToken();

        return statement;
    }

    public ReturnStatement ParseReturnStatement()
    {
        var statement = new ReturnStatement();
        statement.Token = this.CurrentToken;
        this.ReadToken();

        // 式を解析
        statement.ReturnValue = this.ParseExpression(Precedence.LOWEST);
        // セミコロンは必須ではない
        if (this.NextToken.Type == TokenType.SEMICOLON) this.ReadToken();

        return statement;
    }
    // ..
}

コメントをした行が追加した行です。while文でセミコロンまで読み飛ばしていた部分に式を解析する処理を追加しました。

いずれの処理も新しいことはなく、式の解析関数 ParseExpression() を呼び出しています。そのあとのセミコロンは省略可能なので、セミコロンがある場合のみ読み進めます。

let文の場合は "=" が原罪のトークンとなっているので、これを読み飛ばして式の先頭トークンまで読み進めてから式の構文解析を行っています。

テストを修正する

let文、return文ともに式の解析を追加したので、テストも追加しましょう。

まずはlet文のテストです。とはいえ難しいことはなく、右辺にリテラル式を入れてヘルパ関数でのテストを追加します。テストケースは複数用意しました。

[TestMethod]
public void TestLetStatement1()
{
    var tests = new(string, string, object)[]
    {
        ("let x = 5;", "x", 5),
        ("let y = true;", "y", true),
        ("let z = x;", "z", "x"),
    };

    foreach (var (input, name, expected) in tests)
    {
        var lexer = new Lexer(input);
        var parser = new Parser(lexer);
        var root = parser.ParseProgram();
        this._CheckParserErrors(parser);

        Assert.AreEqual(
            root.Statements.Count, 1,
            "Root.Statementsの数が間違っています。"
        );

        var statement = root.Statements[0];
        this._TestLetStatement(statement, name);

        var value = (statement as LetStatement).Value;
        this._TestLiteralExpression(value, expected);
    }
}

次にretun文のテストです。これも同様に複数のテストケースを用意し、返すリテラル式をヘルパ関数でテストします。

[TestMethod]
public void TestReturnStatement1()
{
    var tests = new(string, object)[]
    {
        ("return 5;", 5),
        ("return true;", true),
        ("return x;", "x"),
    };

    foreach (var (input, expected) in tests)
    {
        var lexer = new Lexer(input);
        var parser = new Parser(lexer);
        var root = parser.ParseProgram();
        this._CheckParserErrors(parser);

        Assert.AreEqual(
            root.Statements.Count, 1,
            "Root.Statementsの数が間違っています。"
        );

        var returnStatement = root.Statements[0] as ReturnStatement;
        if (returnStatement == null)
        {
            Assert.Fail("statement が ReturnStatement ではありません。");
        }

        Assert.AreEqual(
            returnStatement.TokenLiteral(), "return",
            $"return のリテラルが間違っています。"
        );

        this._TestLiteralExpression(returnStatement.ReturnValue, expected);
    }
}

テストが通ることを確認して完成です。おめでとうございます! ついに構文解析器の完成です。

しかしまだテストで動かしただけなので実感がわきません。なので REPL から構文解析器の処理を呼び出して結果を表示できるようにしましょう。

REPL

以前作成したREPLを修正しましょう。今のところ入力したコードに対して、評価する代わりに字句解析を実行し、得られたトークン列を表示するといった動作をします。

いまだにコードを評価する方法は知りません。できるのは構文解析までです。したがって構文解析の毛化を表示できるようにしてみます。

using Gorilla.Lexing;
using Gorilla.Parsing;
using System;

namespace Gorilla
{
    public class Repl
    {
        const string PROMPT = ">> ";

        public void Start()
        {
            while (true)
            {
                Console.Write(PROMPT);

                var input = Console.ReadLine();
                if (string.IsNullOrEmpty(input)) return;

                var lexer = new Lexer(input);
                var parser = new Parser(lexer);
                var root = parser.ParseProgram();

                if (parser.Errors.Count > 0)
                {
                    foreach (var error in parser.Errors)
                    {
                        Console.WriteLine($"\t{error}");
                    }
                    continue;
                }

                Console.WriteLine(root.ToCode());
            }
        }
    }
}

字句解析だけでなく構文解析も行い、エラーがあればそれを列挙します。また、構文解析結果のASTを ToCode() で文字列化したデータも表示しています。

これで入力に対して構文解析を行った結果を対話的に得られます。

いろいろなコードを入力して動作を見てみるといいでしょう。

ここまでのソース

mntm0/Gorilla at d74df87e838947de152e4ba7fb7f21bd27e617c2

以上。

開発いろいろカテゴリの最新記事