前書き
軽量・軽快・拡張性バッチという謳い文句の 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
タグがインスタントメッセンジャーの方のために作られた感があったので、混乱を避けるため泣く泣くタグから除外した。