14 構文解析器の拡張(if式) [オリジナル言語インタプリタを作る]

14 構文解析器の拡張(if式) [オリジナル言語インタプリタを作る]

if式

現在実装中のプログラミング言語 Gorilla では多くのプログラミング言語と同じような構文で if, else を使えます。

if (x > y) {
    return x;
} else {
    return y;
}

また、if文ではなくif式です。つまり値を返すということです。次のようにif式の結果を代入できるということです。なお、returnも省略可能です。その場合、最後に評価された値が返されることになります。

let max = if (x, y) { x } else { y };

構文は問題ないでしょう。この式の各部の名称を明確にします。

if (<condition>) <consequence> else <alternative>

中括弧 “{}” は consequence と alternative の一部でどちらもブロック文です。ブロック文はルートのプログラムと同様複数の文からなります。

さて、実装に進みましょう。ASTのノードを定義し、テストを書き、実装し、テストを通し、それでハッピーエンドです。

AST の定義

BlockStatement

まずはif式で出てくるブロック文のASTを定義します。これはif式以外でも使います。関数定義時の処理部分などで使うことになるでしょう。

using Gorilla.Lexing;
using System.Collections.Generic;
using System.Text;

namespace Gorilla.Ast.Statements
{
    public class BlockStatement : IStatement
    {
        public Token Token { get; set; }
        public List<IStatement> Statements { get; set; }

        public string ToCode()
        {
            var builder = new StringBuilder();
            foreach (var statement in this.Statements)
            {
                builder.Append(statement.ToCode());
            }
            return builder.ToString();
        }

        public string TokenLiteral() => this.Token.Literal;
    }
}

内容は単純に処理の内容である文(Statement)のリストを保持します。それだけです。

IfExpression

ではif式のASTです。

using Gorilla.Ast.Statements;
using Gorilla.Lexing;
using System.Text;

namespace Gorilla.Ast.Expressions
{
    public class IfExpression : IExpression
    {
        public Token Token { get; set; }
        public IExpression Condition { get; set; }
        public BlockStatement Consequence { get; set; }
        public BlockStatement Alternative { get; set; }

        public string ToCode()
        {
            var builder = new StringBuilder();
            builder.Append("if");
            builder.Append(this.Condition.ToCode());
            builder.Append(" ");
            builder.Append(this.Consequence.ToCode());

            if (this.Alternative != null)
            {
                builder.Append("else ");
                builder.Append(this.Alternative.ToCode());
            }

            return builder.ToString();
        }

        public string TokenLiteral() => this.Token.Literal;
    }
}

構文規則の通り、構成要素をプロパティとして持ちます。

難しいこともありません。

テストの作成

ifのみのパターンと、if-elseのパターン双方をテストします。

[TestMethod]
public void TestIfExpression()
{
    var input = "if (x < y) { x }";
    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] as ExpressionStatement;
    if (statement == null)
    {
        Assert.Fail("statement が ExpressionStatement ではありません。");
    }

    var expression = statement.Expression as IfExpression;
    if (expression == null)
    {
        Assert.Fail("expression が IfExpression ではありません。");
    }

    this._TestInfixExpression(expression.Condition, "x", "<", "y");

    if (expression.Consequence.Statements.Count != 1)
    {
        Assert.Fail("Consequence の 文の数が 1 ではありません。");
    }

    var consequence = expression.Consequence.Statements[0] as ExpressionStatement;
    if (consequence == null)
    {
        Assert.Fail("consequence が ExpressionStatement ではありません。");
    }

    this._TestIdentifier(consequence.Expression, "x");

    if (expression.Alternative != null)
    {
        Assert.Fail("expression.Alternative が null ではありません。");
    }
}

[TestMethod]
public void TestIfElseExpression()
{
    var input = "if (x < y) { x } else { y; }";
    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] as ExpressionStatement;
    if (statement == null)
    {
        Assert.Fail("statement が ExpressionStatement ではありません。");
    }

    var expression = statement.Expression as IfExpression;
    if (expression == null)
    {
        Assert.Fail("expression が IfExpression ではありません。");
    }

    this._TestInfixExpression(expression.Condition, "x", "<", "y");

    if (expression.Consequence.Statements.Count != 1)
    {
        Assert.Fail("Consequence の 文の数が 1 ではありません。");
    }

    var consequence = expression.Consequence.Statements[0] as ExpressionStatement;
    if (consequence == null)
    {
        Assert.Fail("consequence が ExpressionStatement ではありません。");
    }
    this._TestIdentifier(consequence.Expression, "x");

    if (expression.Alternative == null)
    {
        Assert.Fail("expression.Alternative が null です。");
    }

    if (expression.Alternative.Statements.Count != 1)
    {
        Assert.Fail("Consequence の 文の数が 1 ではありません。");
    }

    var alternative = expression.Alternative.Statements[0] as ExpressionStatement;
    if (consequence == null)
    {
        Assert.Fail("alternative が ExpressionStatement ではありません。");
    }
    this._TestIdentifier(alternative.Expression, "y");
}

長いですがif式の各部をテストしています。テストを実行すると、次のようなエラーになります。

テスト名:    TestIfElseExpression
テストの完全名:    UnitTestProject.ParserTest.TestIfElseExpression
テスト ソース:    E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs : 行 425
テスト成果:    失敗
テスト継続時間:    0:00:00.0026759

結果  のスタック トレース:    
at UnitTestProject.ParserTest._CheckParserErrors(Parser parser) in E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs:line 91
   at UnitTestProject.ParserTest.TestIfElseExpression() in E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs:line 431
結果  のメッセージ:    Assert.Fail failed. 
IF に関連付けられた Prefix Parse Function が存在しません。
LBRACE に関連付けられた Prefix Parse Function が存在しません。
RBRACE に関連付けられた Prefix Parse Function が存在しません。
ELSE に関連付けられた Prefix Parse Function が存在しません。
LBRACE に関連付けられた Prefix Parse Function が存在しません。
RBRACE に関連付けられた Prefix Parse Function が存在しません。

このテスト結果からトークン “if” に対応する前置解析関数を定義する必要があるとわかります。

実装

ParseBlockStatement()

まずはブロック文の解析関数 ParseBlockStatement() を実装します。

public BlockStatement ParseBlockStatement()
{
    var block = new BlockStatement()
    {
        Token = this.CurrentToken, // "{"
        Statements = new List<IStatement>(),
    };

    // "{" を読み飛ばす
    this.ReadToken();

    while (this.CurrentToken.Type != TokenType.RBRACE
        && this.CurrentToken.Type != TokenType.EOF)
    {
        // ブロックの中身を解析する
        var statement = this.ParseStatement();
        if (statement != null)
        {
            block.Statements.Add(statement);
        }
        this.ReadToken();
    }

    return block;
}

この解析関数が呼ばれるとき、現在のトークンは “LBRACE” です。ブロックの中身は “}” か “EOF” に遭遇するまで続けます。

ParseProgram() と同じような処理になっています。

では本丸の、ParseIfExpression() の実装に移ります。この中で ParseBlockStatement() が呼ばれます。

Parsing/Parser.cs

public class Parser
{
    private void RegisterPrefixParseFns()
    {
        // ..
        this.PrefixParseFns.Add(TokenType.IF, this.ParseIfExpression);
    }
    // ..
    public IExpression ParseIfExpression()
    {
        var expression = new IfExpression()
        {
            Token = this.CurrentToken // IF
        };

        // if の後は括弧 ( でなければならない
        if (!this.ExpectPeek(TokenType.LPAREN)) return null;

        // 括弧 ( を読み飛ばす
        this.ReadToken();

        // ifの条件式を解析する
        expression.Condition = this.ParseExpression(Precedence.LOWEST);

        // 閉じ括弧 ) 中括弧が続く 
        if (!this.ExpectPeek(TokenType.RPAREN)) return null;
        if (!this.ExpectPeek(TokenType.LBRACE)) return null;

        // この時点で { が現在のトークン
        // ブロック文の解析関数を呼ぶ
        expression.Consequence = this.ParseBlockStatement();

        // else句があれば解析する
        if (this.NextToken.Type == TokenType.ELSE)
        {
            // else に読み進める
            this.ReadToken();
            // else の後に { が続かなければならない
            if (!this.ExpectPeek(TokenType.LBRACE)) return null;

            // この時点で { が現在のトークン
            // ブロック文の解析関数を呼ぶ
            expression.Alternative = this.ParseBlockStatement();
        }

        return expression;
    }

    public BlockStatement ParseBlockStatement()
    {
        // ..
    }
}

大まかな処理の流れはコメントに書きました。

if の後に “(” があり、条件式を解析、閉じ括弧 “)” が必ず必要です。そのあとにはブロック文が必須です。”{” に続いてブロック内の全文を解析します。

ブロック文の解析後は閉じ括弧 “}” が現在のトークンです。”}” があるかどうかの確認は ParseBlockStatement() の中で見つかるまで処理を続けることで確認しています。

else句は省略可能です。したがって、ExpectedPeek() を使わずに次のトークンが else かどうかを確認し、そうであればブロックの中身を解析します。

ブロック文の解析に進む前に閉じ括弧 “}” が現在のトークンなのでこれを読み飛ばし、”else” に進めます。そこからさらに “{” が続かなければ、エラーにします。ここでようやく正しいelse句の実装であることがわかります。そしてようやくブロック文の解析に移れます。

テストが通ることを確認できます。ifのみのパターン、if-elseのパターン、両方ともこれでOKです。

まとめ

ここまでのソース

mntm0/Gorilla at a422fb42a398c0490fdfc82cdb7a81c34d5990e5

次回は関数リテラルの実装です。

以上。

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