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

# 前書き
軽量・軽快・拡張性バッチという謳い文句の[ benjamin-hodgson/Pidgin](https://github.com/benjamin-hodgson/Pidgin)というパーザコンビネータを通してPEGパーザの作成を学習した記録（継続中)
[SQL:2003のBNF](https://ronsavage.github.io/SQL/sql-2003-2.bnf.html#Unicode%20escape%20value)を題材として選んだ。

# 準備

毎度の如く、`NUnit`のプロジェクトを作成して振る舞いを確認。
`Pidgin`の導入は、[Nugetの手順](https://www.nuget.org/packages/Pidgin/)に従って参照に追加した。

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

# リテラルのパーザ

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

```sql
select 2 * 3 as y from hoge
```

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

## Booleanリテラル

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

```csharp
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としてしまうらしい。

```csharp
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]取り出された数字リテラル");
    }
}
```

## 小数リテラル

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

```csharp
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), "数字ではないところで失敗");
    }
}
```

## 科学表記の小数リテラル

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

```csharp
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]取り出された数字リテラル");
    }
}
```

## 任意の数字リテラル

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

```csharp
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`タグがインスタントメッセンジャーの方のために作られた感があったので、混乱を避けるため泣く泣くタグから除外した。