15 構文解析器の拡張(関数リテラル) [オリジナル言語インタプリタを作る]

15 構文解析器の拡張(関数リテラル) [オリジナル言語インタプリタを作る]

関数リテラル

これまでいろいろと構文解析を実装してきました。今回は関数リテラルを解析できるようにします。

関数もまた、真偽値や整数と同じくリテラルで定義できます。関数を宣言するのではなくリテラルで扱います。

つまり識別子や整数と同じように式として定義するので、引数で渡したり return で返したりできます。

fn(x, y) {
    return x + y;
}

これが関数リテラルです。キーワード fn から始まり括弧の内側にパラメータが任意の数だけカンマ区切りで続き、そのあとブロック文がきます。ブロック文はif式の時に使った内容と同じですね。

fn <parameters> <block statement>

構文規則はこうです。パラメータのリストは空の場合もありえます。このパラメータの扱いが少しややこしそうです。

ASTノードの定義

Ast/Expressions/FunctionLiteral.cs

using Gorilla.Ast.Statements;
using Gorilla.Lexing;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Gorilla.Ast.Expressions
{
    public class FunctionLiteral : IExpression
    {
        public Token Token { get; set; }
        public List<Identifier> Parameters { get; set; }
        public BlockStatement Body { get; set; }

        public string ToCode()
        {
            var parameters = this.Parameters.Select(p => p.ToCode());
            var builder = new StringBuilder();
            builder.Append("fn");
            builder.Append("(");
            builder.Append(string.Join(", ", parameters));
            builder.Append(")");
            builder.Append(this.Body.ToCode());
            return builder.ToString();
        }

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

まずは FunctionLiteral クラスを定義します。関数リテラルは式なので IExpression を実装します。

パラメータは識別子のリストとして管理します。Bodyは紺数の処理本文でブロック文です。

テストの作成

関数リテラル式をテストデータとして用意し、各部が正しく解析されるかテストします。

[TestMethod]
public void TestFunctionLiteral()
{
    var input = "fn(x, y) { x + 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 FunctionLiteral;
    if (expression == null)
    {
        Assert.Fail("expression が FunctionLiteral ではありません。");
    }

    Assert.AreEqual(
        expression.Parameters.Count, 2,
        "関数リテラルの引数の数が間違っています。"
    );
    this._TestIdentifier(expression.Parameters[0], "x");
    this._TestIdentifier(expression.Parameters[1], "y");

    Assert.AreEqual(
        expression.Body.Statements.Count, 1,
        "関数リテラルの本文の式の数が間違っています。"
    );

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

テストの結果はこうです。

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

結果  のスタック トレース:    
at UnitTestProject.ParserTest._CheckParserErrors(Parser parser) in E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs:line 91
   at UnitTestProject.ParserTest.TestFunctionLiteral() in E:\work\cs\Gorilla\UnitTestProject\ParserTest.cs:line 489
結果  のメッセージ:    Assert.Fail failed. 
FUNCTION に関連付けられた Prefix Parse Function が存在しません。
COMMA ではなく RPAREN が来なければなりません。
COMMA に関連付けられた Prefix Parse Function が存在しません。
RPAREN に関連付けられた Prefix Parse Function が存在しません。
LBRACE に関連付けられた Prefix Parse Function が存在しません。
RBRACE に関連付けられた Prefix Parse Function が存在しません。

これまでと同様 FUNCTION キーワード時に解析関数 ParseFunctionLiteral() を呼べるようにしてやればよさそうです。

ParseFunctionLiteral()

関数の解析関数です。

Parsing/Parser.cs

public class Parser
{
    // ..
    private void RegisterPrefixParseFns()
    {
        // ..
        this.PrefixParseFns.Add(TokenType.FUNCTION, this.ParseFunctionLiteral);
    }
    // ..
    public IExpression ParseFunctionLiteral()
    {
        var fn = new FunctionLiteral()
        {
            Token = this.CurrentToken
        };

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

        fn.Parameters = this.ParseParameters();

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

        fn.Body = this.ParseBlockStatement();

        return fn;
    }
}

ちゃんとパラメータ定義の括弧があるか確認し、あればパラメータの解析関数(後述)を呼んでいます。これがパラメータである識別子のリストを返してくれます。

パラメータの後は本文ブロックが必須なのでチェックし、以前作成したブロック式の解析関数を読んで関数リテラルの解析は完成です。

ParseParameters()

以下、パラメータ部分の構文解析を切り出した処理です。


public class Parser
{
    // ..
    public List<Identifier> ParseParameters()
    {
        var parameters = new List<Identifier>();

        // () となってパラメータがないときは空のリストを返す
        if (this.NextToken.Type == TokenType.RPAREN)
        {
            this.ReadToken();
            return parameters;
        }

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

        // 最初のパラメータ
        parameters.Add(new Identifier(this.CurrentToken, this.CurrentToken.Literal));

        // 2つ目以降のパラメータをカンマが続く限り処理する
        while (this.NextToken.Type == TokenType.COMMA)
        {
            // すでに処理した識別子とその後ろのカンマを飛ばす
            this.ReadToken();
            this.ReadToken();

            // 識別子を処理
            parameters.Add(new Identifier(this.CurrentToken, this.CurrentToken.Literal));
        }

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

        return parameters;
    }
}

ParseParameters() はパラメータの解析を行います。パラメータがあれば識別子のリストで、なければ空のリストで結果を返します。

パラメータなし、1つのとき、複数のとき、それぞれでどのように動くか処理確認しておきましょう。テストをパラメータの数で境界値テストしておくとよいでしょう。

[TestMethod]
public void TestFunctionParameter()
{
    var tests = new[]
    {
        ("fn() {};", new string[] { }),
        ("fn(x) {};", new string[] { "x" }),
        ("fn(x, y, z) {};", new string[] { "x", "y", "z" }),
    };

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

        var statement = root.Statements[0] as ExpressionStatement;
        var fn = statement.Expression as FunctionLiteral;


        Assert.AreEqual(
            fn.Parameters.Count, parameters.Length,
            "関数リテラルの引数の数が間違っています。"
        );
        for (int i = 0; i < parameters.Length; i++)
        {
            this._TestIdentifier(fn.Parameters[i], parameters[i]);
        }
    }
}

テストしたいのはパラメータの数についてのみなので、処理本文ほか関係のないテストは端折っています。パラメータの数と識別子の名前が一致するかのテストをしています。

まとめ

すべてのテストが通ることを確認できたら完了です。関数リテラル式の解析ができるようになりました。複数のパラメータに対応することもテストで確認済です。

ここまでのソース

mntm0/Gorilla at dc345e2012b907abe7bb1c78b38baf2a46505883

次回は関数の呼び出し式について構文解析を実装します。

以上。

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