Edited at

C#でGPPG/GPLEXを使って電卓を作成する(応用編)


概要

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にしてください。

image.png


2. NuGetでYaccLexToolsをインストール

検索で「YaccLex」や「GPPG」を入力すれば出てきます。

image.png


3. 字句解析器(Lexer)と構文解析器(Parser)の生成

パッケージマネージャーコンソールから「Add-Parser Xxxxx」と入力してください。

XxxxxScannerとXxxxxParserが生成されます。

今回はCalculatorとします。

PM> Add-Parser Calculator

実行するとポップアップが出ますので「ソリューションの再読み込み」を押してください。

image.png


4. 字句解析器の変更

以下のように変更します。


Calculator.Language.analyzer.lex

// ---------------------------

// 定義部
// ---------------------------

// 出力するクラスの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 */

%%
// ---------------------------
// コード部
// ---------------------------



Calculator.Scanner.cs

// 不要な関数を削除したぐらいで大きな変更なし

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. 構文解析器の変更

以下のように変更します。


Calculator.Language.grammer.y

// ---------------------------

// 宣言部
// ---------------------------

// 出力するクラスの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; }
;

%%
// ---------------------------
// コード部
// ---------------------------



Calculator.Parser.cs

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から呼び出すようにすれば完成です。


Program.cs

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