概要
GPPG/GPLEXを使えば、C#で字句解析器/構文解析器を簡単に作成できます。
前回は自動生成で電卓を作りましたが、今回はもう少し踏み込んで電卓を作りたいと思います。
https://qiita.com/minoru-nagasawa/items/8c188135ab131b7fbedc
サンプルコード
以下に実際に動作するコードを置いてます。
https://github.com/minoru-nagasawa/GPPGCalculator2
実行結果
今回は変数を使える電卓を作ります。
以下のように実行できます。
> a = 3
3
> b = 5
5
> a+b
8
>
作成方法
1. プロジェクトを作成
今回はコンソールアプリで作ります。
名前はGPPGCalculator2とします。
.NET Coreは未対応ですので、.NET Frameworkにしてください。
2. NuGetでYaccLexToolsをインストール
検索で「YaccLex」や「GPPG」を入力すれば出てきます。
3. 字句解析器(Lexer)と構文解析器(Parser)の生成
パッケージマネージャーコンソールから「Add-Parser Xxxxx」と入力してください。
XxxxxScannerとXxxxxParserが生成されます。
今回はCalculatorとします。
PM> Add-Parser Calculator
実行するとポップアップが出ますので「ソリューションの再読み込み」を押してください。
4. 字句解析器の変更
以下のように変更します。
// ---------------------------
// 定義部
// ---------------------------
// 出力するクラスのnamespace
%namespace GPPGCalculator2
// 出力するクラス名
%scannertype CalculatorScanner
// 出力するクラスのアクセシビリティ
%visibility internal
// トークンに使用するenumの型名
%tokentype Token
// オプション
// stack : これを付けると、状態をスタックに保存できるようになる。
// C言語のコメント(/* */)の解釈のように、状態を管理したい場合に使用する。
// 電卓では不要だが付けておく。
// minimize : これを付けると、内部構造のDFSA(決定性有限オートマトン)を最小化してくれる
// verbose : これを付けると、ビルドの途中経過が出力される
// persistbuffer : これを付けると、入力を全てバッファに格納してから解析するようになる。
// それにより、ScanBuff.GetString()で任意の位置から読めるようになる。
// しかし、入力ファイルのサイズが大きいと、メモリ使用量も増えるので注意が必要となる。
// noembedbuffers : これを付けると、バッファとしてGplexBuffersクラスを使うようになる。
// これにより、アプリ側でバッファを利用したい場合に便利になる。
// 付けないと、バッファにアクセスできない。
%option stack, minimize, parser, verbose, persistbuffer, noembedbuffers
D [0-9]
L [a-zA-Z_]
H [a-fA-F0-9]
E [Ee][+\-]?{D}+
%{
%}
%%
// ---------------------------
// ルール部
// ---------------------------
// 変数名
{L}({L}|{D})* { yylval.text = yytext; return (int)Token.VARIABLE; }
// 16進数
0[xX]{H}+ { yylval.real = (double)Convert.ToInt32(yytext, 16); return (int)Token.CONSTANT; }
// 8進数
0{D}+ { yylval.real = (double)Convert.ToInt32(yytext, 8); return (int)Token.CONSTANT; }
// 10進数の整数
{D}+ { yylval.real = double.Parse(yytext); return (int)Token.CONSTANT; }
// 指数表示の10進数の整数
{D}+{E} { yylval.real = double.Parse(yytext); return (int)Token.CONSTANT; }
// 実数
{D}*"."{D}+({E})? { yylval.real = double.Parse(yytext); return (int)Token.CONSTANT; }
{D}+"."{D}*({E})? { yylval.real = double.Parse(yytext); return (int)Token.CONSTANT; }
// 演算子
"=" { return '='; }
"(" { return '('; }
")" { return ')'; }
"-" { return '-'; }
"+" { return '+'; }
"*" { return '*'; }
"/" { return '/'; }
// 該当しない文字は無視する
. /* Skip */
%%
// ---------------------------
// コード部
// ---------------------------
// 不要な関数を削除したぐらいで大きな変更なし
using System;
using System.Collections.Generic;
using System.Text;
namespace GPPGCalculator2
{
internal partial class CalculatorScanner
{
public override void yyerror(string format, params object[] args)
{
base.yyerror(format, args);
Console.WriteLine(format, args);
Console.WriteLine();
}
}
}
5. 構文解析器の変更
以下のように変更します。
// ---------------------------
// 宣言部
// ---------------------------
// 出力するクラスのnamespace
%namespace GPPGCalculator2
// 生成するクラスがpartialクラスになる
// そうすることで、実装部分を*.yではなく、*.csに書けるようになるので便利
%partial
// 出力するクラス名
%parsertype CalculatorParser
// 出力するクラスのアクセシビリティ
%visibility internal
// トークンに使用するenumの型名
%tokentype Token
%union {
public double real;
public string text;
}
%token <text> VARIABLE
%token <real> CONSTANT
%start main
%%
// ---------------------------
// ルール部
// ---------------------------
main : assignment { m_result = $1.real; }
;
assignment : additive { $$.real = $1.real; }
| VARIABLE '=' assignment {
m_variables[$1] = $3.real;
$$.real = $3.real;
}
;
additive : multiplicative { $$.real = $1.real; }
| additive '+' multiplicative { $$.real = $1.real + $3.real; }
| additive '-' multiplicative { $$.real = $1.real - $3.real; }
;
multiplicative : primary { $$.real = $1.real; }
| multiplicative '*' primary { $$.real = $1.real * $3.real; }
| multiplicative '/' primary { $$.real = $1.real / $3.real; }
;
primary : VARIABLE {
double v;
if (m_variables.TryGetValue($1, out v))
{
$$.real = v;
}
else
{
$$.real = 0;
}
}
| CONSTANT { $$.real = $1; }
| '(' additive ')' { $$.real = $2.real; }
;
%%
// ---------------------------
// コード部
// ---------------------------
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace GPPGCalculator2
{
internal partial class CalculatorParser
{
// Parseした結果が格納される。
// 格納する処理は、Calculator.Language.grammer.yの中に記述している。
private double m_result;
// 変数を格納するためのディクショナリ
// Keyが変数名、Valueが格納している値
private Dictionary<string, double> m_variables = new Dictionary<string, double>();
public CalculatorParser() : base(null) { }
public bool Parse(string s, out double result)
{
result = 0;
byte[] inputBuffer = System.Text.Encoding.Default.GetBytes(s);
using (var stream = new MemoryStream(inputBuffer))
{
this.Scanner = new CalculatorScanner(stream);
bool rc = this.Parse();
if (rc)
{
result = m_result;
}
return rc;
}
}
}
}
6. Mainを変更
Mainから呼び出すようにすれば完成です。
using System;
namespace GPPGCalculator2
{
class Program
{
static void Main(string[] args)
{
// 作った電卓のParserを生成
// 字句解析器も、この中で生成されている
var parser = new CalculatorParser();
do
{
// 式を入力。空文字なら終了。
Console.Write("> ");
var input = Console.ReadLine();
if (string.IsNullOrEmpty(input))
{
return;
}
// 式を解析して結果を出力
double result;
bool rc = parser.Parse(input, out result);
if (rc)
{
Console.WriteLine(result.ToString());
}
} while (true);
}
}
}
参考URL
gplexのドキュメント
オリジナルが見つからなかったので、Internet Archiveを指してます。
https://web.archive.org/web/20120325033719/http://plas.fit.qut.edu.au/gplex/files/gplex.pdf
gppgのドキュメント
オリジナルが見つからなかったので、Internet Archiveを指してます。
https://web.archive.org/web/20120325042446/http://plas.fit.qut.edu.au/gppg/files/gppg.pdf