LoginSignup
1
1

More than 3 years have passed since last update.

Pidginを使ったPEGパーザ構築の手習い

Posted at

前書き

軽量・軽快・拡張性バッチという謳い文句の benjamin-hodgson/Pidginというパーザコンビネータを通してPEGパーザの作成を学習した記録(継続中)
SQL:2003のBNFを題材として選んだ。

準備

毎度の如く、NUnitのプロジェクトを作成して振る舞いを確認。
Pidginの導入は、Nugetの手順に従って参照に追加した。

& dotnet add <<>MyProject>  package Pidgin --version 2.3.0

リテラルのパーザ

最初のマイルストーンとして、

select 2 * 3 as y from hoge

を解析できるようにする。
そのためにselect句のリテラルの解析を最初の目標とした。

Booleanリテラル

SQLは三値論理なので、TRUE / FALSE / UNKNOWNの3つの値が存在する。

using NUnit.Framework;

using Pidgin;

public class ParseSelectTest {
    [Test]
    public void _Bool値のパーズ() {
        var bool_p = Parser.OneOf(Parser.Try(Parser.CIString("TRUE")), Parser.Try(Parser.CIString("FALSE")), Parser.Try(Parser.CIString("UNKNOWN")));

        var result1_1 = bool_p.Parse("TRUE");
        Assert.That(result1_1.Success, Is.True, "[1.1]パーズは成功しなければならない");
        Assert.That(result1_1.Value, Is.EqualTo("TRUE"), "[1.1]取り出されたリテラル");

        var result1_2 = bool_p.Parse("true");
        Assert.That(result1_2.Success, Is.True, "[1.2]パーズは成功しなければならない");
        Assert.That(result1_2.Value, Is.EqualTo("true"), "[1.2]取り出されたリテラル");

        var result2 = bool_p.Parse("false");
        Assert.That(result2.Success, Is.True, "[2]パーズは成功しなければならない");
        Assert.That(result2.Value, Is.EqualTo("false"), "[2]取り出されたリテラル");

        var result3 = bool_p.Parse("unknown");
        Assert.That(result3.Success, Is.True, "[3]パーズは成功しなければならない");
        Assert.That(result3.Value, Is.EqualTo("unknown"), "[3]取り出されたリテラル");
    }
}

整数リテラル

手戻りを書いていないためか、消費できたところまででOKとしてしまうらしい。

public class ParseSelectTest {
    // (snip)
    [Test]
    public void _数字リテラルのパーズ_符号なし整数の場合() {
        var unum_p = Parser.Digit.AtLeastOnceString();

        var result1 = unum_p.Parse("8 ");
        Assert.That(result1.Success, Is.True, "[1]パーズは成功しなければならない");
        Assert.That(result1.Value, Is.EqualTo("8"), "[1]取り出された数字リテラル");

        var result2 = unum_p.Parse("9876543210987");
        Assert.That(result2.Success, Is.True, "[2]パーズは成功しなければならない");
        Assert.That(result2.Value, Is.EqualTo("9876543210987"), "[2]取り出された数字リテラル");

        var result3 = unum_p.Parse("9876543XYZ");
        Assert.That(result3.Success, Is.True);
        Assert.That(result3.Value, Is.EqualTo("9876543"), "[3]取り出された数字リテラル");
    }
}

小数リテラル

符号なし整数との共存に苦労した。

public class ParseSelectTest {
    // (snip)
    [Test]
    public void _数字リテラルのパーズ_小数の場合() {
        var unum_p = Parser.Digit.AtLeastOnceString();
        var frac_num_p = Parser.Char('.').Then(unum_p, (left, right) => left + right);

        var result4 = frac_num_p.Parse(".654");
        Assert.That(result4.Success, Is.True, "[4]パーズは成功しなければならない");
        Assert.That(result4.Value, Is.EqualTo(".654"), "[4]取り出された数字リテラル");

        var decimal_p = unum_p
            .Then(frac_num_p.Optional(), (left, right) => right.HasValue ? left + right.Value : left)
        ;

        var result5 = decimal_p.Parse("1234.567");
        Assert.That(result5.Success, Is.True, "[5]パーズは成功しなければならない");
        Assert.That(result5.Value, Is.EqualTo("1234.567"), "[5]取り出された数字リテラル");

        var result6 = decimal_p.Parse("567");
        Assert.That(result6.Success, Is.True, "[6]パーズは成功しなければならない");
        Assert.That(result6.Value, Is.EqualTo("567"), "[6]取り出された数字リテラル");

        var decimal_p2 = decimal_p.Then(Parser<char>.End, (l, r) => l);

        var result7 = decimal_p2.Parse("5678UV");
        Assert.That(result7.Success, Is.Not.True, "[7]パーズは失敗しなければならない");
        Assert.That(result7.Error, Is.Not.Null, "[7]エラーあり");
        Assert.That(result7.Error.ErrorPos.Col, Is.EqualTo(5), "数字ではないところで失敗");
    }
}

科学表記の小数リテラル

小数の焼き直しでなんとかなった。

public class ParseSelectTest {
    // (snip)
    [Test]
    public void _数字リテラルのパーズ_小数の場合() {
        var unum_p = Parser.Digit.AtLeastOnceString();
        var exp_part_p = Parser.CIChar('E').Then(unum_p, (l, r) => l + r);

        var result8 = exp_part_p.Parse("E31");
        Assert.That(result8.Success, Is.True, "[8]パーズは成功しなければならない");
        Assert.That(result8.Value, Is.EqualTo("E31"), "[8]取り出された数字リテラル");

        var result8_2 = exp_part_p.Parse("e13");
        Assert.That(result8_2.Success, Is.True, "[8_2]パーズは成功しなければならない");
        Assert.That(result8_2.Value, Is.EqualTo("e13"), "[8_2]取り出された数字リテラル");

        var exp_num_p = unum_p.Then(exp_part_p.Optional(), (l, r) => r.HasValue ? l + r.Value : l);

        var result9 = exp_num_p.Parse("1234E31");
        Assert.That(result9.Success, Is.True, "[9]パーズは成功しなければならない");
        Assert.That(result9.Value, Is.EqualTo("1234E31"), "[9]取り出された数字リテラル");

        var result10 = exp_num_p.Parse("234");
        Assert.That(result10.Success, Is.True, "[10]パーズは成功しなければならない");
        Assert.That(result10.Value, Is.EqualTo("234"), "[10]取り出された数字リテラル");
    }
}

任意の数字リテラル

ここまでの数字パーザを組み合わせただけ。

public class ParseSelectTest {
    // (snip)
    [Test]
    public void _数字のパーズ_Parserのチョイス() {
        var uint_p = Parser.Digit.AtLeastOnceString();

        var frac_num_p = Parser.Char('.').Then(uint_p, (l, r) => l + r);
        var exact_num_p = uint_p.Then(frac_num_p, (l, r) => l + r);
        var exp_part_p = Parser.CIChar('E').Then(uint_p, (l, r) => l + r);
        var exp_num_p = uint_p.Then(exp_part_p.Optional(), (l, r) => r.HasValue ? l + r.Value : l);

        var unum_p = Parser.OneOf(Parser.Try(exact_num_p), Parser.Try(exp_num_p), uint_p);

        var result1_1 = uint_p.Parse("1234567890123");
        Assert.That(result1_1.Success, Is.True, "[1.1]パーズは成功しなければならない");
        Assert.That(result1_1.Value, Is.EqualTo("1234567890123"), "[1.1]取り出された数字リテラル");

        var result1_2 = unum_p.Parse("1234567890123");
        Assert.That(result1_2.Success, Is.True, "[1.2]パーズは成功しなければならない");
        Assert.That(result1_2.Value, Is.EqualTo("1234567890123"), "[1.2]取り出された数字リテラル");

        var result2 = unum_p.Parse("1234.567");
        Assert.That(result2.Success, Is.True, "[2]パーズは成功しなければならない");
        Assert.That(result2.Value, Is.EqualTo("1234.567"), "[2]取り出された数字リテラル");

        var result3 = unum_p.Parse("1234E31");
        Assert.That(result3.Success, Is.True, "[3]パーズは成功しなければならない");
        Assert.That(result3.Value, Is.EqualTo("1234E31"), "[3]取り出された数字リテラル");

        var sign_p = Parser.CIOneOf('+', '-');

        var snum_p = sign_p.Optional().Then(unum_p, (l, r) => l.HasValue ? l.Value  + r: r);

        var result4 = snum_p.Parse("1234567890123");
        Assert.That(result4.Success, Is.True, "[4]パーズは成功しなければならない");
        Assert.That(result4.Value, Is.EqualTo("1234567890123"), "[4]取り出された数字リテラル");

        var result5 = snum_p.Parse("-98765432");
        Assert.That(result5.Success, Is.True, "[5]パーズは成功しなければならない");
        Assert.That(result5.Value, Is.EqualTo("-98765432"), "[5]取り出された数字リテラル");

        var result6 = snum_p.Parse("+2224444");
        Assert.That(result6.Success, Is.True, "[6]パーズは成功しなければならない");
        Assert.That(result6.Value, Is.EqualTo("+2224444"), "[6]取り出された数字リテラル");
    }
}

今日はここまで

余談

Pidginでググると、真っ先にインスタントメッセンジャーの方が引っかかるのでとてもググラビリティが低い。
あとQiitaのPidginタグがインスタントメッセンジャーの方のために作られた感があったので、混乱を避けるため泣く泣くタグから除外した。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1