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

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

構文解析器の拡張

ここまでで、式の構文解析を完成させることができました。return文、let文についても実装済です。

今回は真偽値リテラルの構文解析処理の実装を行います。

テスト用ヘルパ関数の作成

具体的な実装に移る前に、テスト用のヘルパ関数を先に作成しましょう。

整数リテラルの実装時に _TestIntegerLiteral() というヘルパ関数を作成しました。同様に _TestIdentifier() を作成します。これらを組み合わせることでリテラル式を簡潔にテストするコードが書けるようになります。

private void _TestIdentifier(IExpression expression, string value)
{
    var ident = expression as Identifier;
    if (ident == null)
    {
        Assert.Fail("Expression が Identifier ではありません。");
    }
    if (ident.Value != value)
    {
        Assert.Fail($"ident.Value が {value} ではありません。({ident.Value})");
    }
    if (ident.TokenLiteral() != value)
    {
        Assert.Fail($"ident.TokenLiteral が {value} ではありません。({ident.TokenLiteral()})");
    }
}

TestIdentifierExpression1() から処理を切り出しました。

組み合わせて _TestLiteralExpression() を作成します。さらに中置演算式(InfixExpression)のテスト用ヘルパ関数も作成します。

private void _TestLiteralExpression(IExpression expression, object expected)
{
    switch (expected)
    {
        case int intValue:
            this._TestIntegerLiteral(expression, intValue);
            break;
        case string stringValue:
            this._TestIdentifier(expression, stringValue);
            break;
        default:
            Assert.Fail("予期せぬ型です。");
            break;
    }
}

private void _TestInfixExpression(IExpression expression, object left, string op, object right)
{
    var infixExpression = expression as InfixExpression;
    if (infixExpression == null)
    {
        Assert.Fail("expression が InfixExpression ではありません。");
    }

    this._TestLiteralExpression(infixExpression.Left, left);

    if (infixExpression.Operator != op)
    {
        Assert.Fail($"Operator が {infixExpression.Operator} ではありません。({op})");
    }

    this._TestLiteralExpression(infixExpression.Right, right);
}

これでヘルパ関数を使ってテストが簡潔に書けるようになりました。例えば前回作成したテスト TestInfixExpressions1() について、以下のように書けます。。

[TestMethod]
public void TestInfixExpressions1()
{
    var tests = new[] {
        ("1 + 1;", 1, "+", 1),
        ("1 - 1;", 1, "-", 1),
        ("1 * 1;", 1, "*", 1),
        ("1 / 1;", 1, "/", 1),
        ("1 < 1;", 1, "<", 1),
        ("1 > 1;", 1, ">", 1),
        ("1 == 1;", 1, "==", 1),
        ("1 != 1;", 1, "!=", 1),
    };

    foreach (var (input, leftValue, op, rightValue) 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] as ExpressionStatement;
        if (statement == null)
        {
            Assert.Fail("statement が ExpressionStatement ではありません。");
        }

        this._TestInfixExpression(statement.Expression, leftValue, op, rightValue);
    }
}

式と左右の値、演算子を渡すと中身をテストしてくれます。繰り返し同じような記述でテストしていた箇所が1行で済んでいます。

同様に過去に作成したテストの一部(識別子や整数リテラルのテスト)をヘルパ関数を使って簡潔に書き直すことができます。長くなるのでここには載せませんが、適宜書き直してください。

別に書き直さなくても実装自体には問題ないので、面倒であれば放っておいてもいいのですが..。書き直したソース配下のリンクからどうぞ。一応一通りは確認して直しました。

mntm0/Gorilla at 59127cda363a5c838c385353264e93699050d406

真偽値リテラル

テストの修正が終わったので、簡単な真偽値リテラルの実装から行きましょう。識別子、整数リテラルと実装してきたので何も難しいことはありません。整数リテラルも式です。

true;
false;
let x = true;
let y = false;

ではASTを定義します。

真偽値リテラルAST

Ast/Expressions/BooleanLiteral.cs

using Gorilla.Lexing;

namespace Gorilla.Ast.Expressions
{
    public class BooleanLiteral : IExpression
    {
        public Token Token { get; set; }
        public bool Value { get; set; }

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

非常にシンプルな内容です。Value プロパティで真偽値の値(true or fale)を保持できます。

テストの作成

テストの内容も識別子、整数リテラルと同じような内容です。同様に真偽値リテラル式用のヘルパ関数を作成してテストします。

[TestMethod]
public void TestBooleanLiteralExpression()
{
    var tests = new[]
    {
        ("true;", true),
        ("false;", false),
    };

    foreach (var (input, value) 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] as ExpressionStatement;
        if (statement == null)
        {
            Assert.Fail("statement が ExpressionStatement ではありません。");
        }

        this._TestBooleanLiteral(statement.Expression, value);
    }
}

private void _TestBooleanLiteral(IExpression expression, bool value)
{
    var booleanLiteral = expression as BooleanLiteral;
    if (booleanLiteral == null)
    {
        Assert.Fail("Expression が BooleanLiteral ではありません。");
    }
    if (booleanLiteral.Value != value)
    {
        Assert.Fail($"booleanLiteral.Value が {value} ではありません。({booleanLiteral.Value})");
    }
    // bool値をToString()すると "True", "False" になるので小文字化してます
    if (booleanLiteral.TokenLiteral() != value.ToString().ToLower())
    {
        Assert.Fail($"booleanLiteral.TokenLiteral が {value.ToString().ToLower()} ではありません。({booleanLiteral.TokenLiteral()})");
    }
}

このテストの実行結果は以下のようになります。

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

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

“TRUE に関連付けられた Prefix Parse Function が存在しません。” というエラーです。真偽値用の解析関数を実装すればうまく動きそうです。

つまり ParseBooleanLiteral() が必要ですね。これをトークンのTRUEとFALSEに関連付けると解析できます。

真偽値の構文解析関数

Parsing/Parser.cs

public class Parser
{
    // ..
    private void RegisterPrefixParseFns()
    {
        // ..
        this.PrefixParseFns.Add(TokenType.TRUE, this.ParseBooleanLiteral);
        this.PrefixParseFns.Add(TokenType.FALSE, this.ParseBooleanLiteral);
    }
    // ..
    public IExpression ParseBooleanLiteral()
    {
        return new BooleanLiteral()
        {
            Token = this.CurrentToken,
            Value = this.CurrentToken.Type == TokenType.TRUE,
        };
    }
}

ParseBooleanLiteral() の実装は非常にシンプルです。真偽値については現在トークンがTRUEであれば true、それ以外の場合は false としています。この処理が呼ばれるとき、現在のトークンが TRUE, FALSE のいずれかであるということです。

トークンの種類(TRUE, FALSE)について、PrefixParseFnsに解析関数を登録しておきます。こうすることで後はいい感じに解析関数が呼ばれることになります。

再度テストを動かして確認しておきましょう。

さらにテストを追加

まだ終わりません。TestOperatorPrecedenceParsing() に真偽値リテラルを使った式も追加しておきましょう。

[TestMethod]
public void TestOperatorPrecedenceParsing()
{
    var tests = new[]
    {
        // ..
        ("true", "true"),
        ("true == false", "(true == false)"),
        ("1 > 2 == false", "((1 > 2) == false)"),
    };
    // ..
}

実装は変更せずともこのテストは通ります。これで真偽値リテラルも優先度を考慮して計算式の中で解析できるようになったことが確認できました。

さらに、TestInfixExpressions1() のテストも真偽値に対応できるように修正します。

[TestMethod]
public void TestInfixExpressions1()
{
    var tests = new (string, object, string, object)[] {
        // ..
        ("true == false", true, "==", false),
        ("false != false", false, "!=", false),
    };
    // ..

テストデータ配列について、型を明示的に追加しています。左辺と右辺の値を object 型でとるようにして、bool値に対応しました。

このテストはまだ通りません。bool型に対応したリテラル式テスト用ヘルパ関数を呼び出す分岐がないため予期せぬエラーとしてテストが失敗します。_TestLiteralExpression() を修正します。


private void _TestLiteralExpression(IExpression expression, object expected)
{
    switch (expected)
    {
        case int intValue:
            this._TestIntegerLiteral(expression, intValue);
            break;
        case string stringValue:
            this._TestIdentifier(expression, stringValue);
            break;
        case bool boolValue:
            this._TestBooleanLiteral(expression, boolValue);
            break;
        default:
            Assert.Fail("予期せぬ型です。");
            break;
    }
}

先ほど作成した _TestBooleanLiteral() を呼び出す分岐を追加しています。これで真偽値の場合には真偽値用のテストが呼ばれます。テストもクリアできます。

これでテストの拡張は終了です。そしてこれまで実装してきた計算式の解析に正しく真偽値リテラルが組み込まれたことがテストによって確認できました。

まとめ

ここまでのソース

mntm0/Gorilla at 77972c70f81160e3062423ba99c882b1502149f8

真偽値リテラルの実装が済みました。これでリテラルの実装はすべてです。整数と真偽値、識別子が式の中に登場するすべてということです。

次回は式の解析機能にグループ化の機能を追加します。用は括弧による優先順位の設定です。

(1 + 2) * 3

こんな感じの式をうまく解析できるようにします。

以上。

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