Edited at

C#でGPPG/GPLEXを使って電卓を作成する


概要

c言語ならyacc/lex、c++ならbison/flexがあるように、c#にはGPPG/GPLEXがあります。

これを使えば、字句解析/構文解析が可能です。

昔は導入が面倒だったのですが、いつからかNuGetで簡単に導入できるようになりました。

今回はサンプルとして電卓を作りたいと思います。


サンプルコード

以下に実際に動作するコードを置いてます。

https://github.com/minoru-nagasawa/GPPGCalculator


作成方法


1. プロジェクトを作成

今回はコンソールアプリで作ります。

名前はGPPGCalculatorとします。

.NET Coreは未対応ですので、.NET Frameworkにしてください。

image.png


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

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

image.png


3. サンプル電卓用のソースコードを自動生成

GPPG/GPLEXには、サンプル電卓のソースコードを自動生成する機能があるので使います。

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

これにより、Calculator.Language.grammar.yなどが自動生成されます。

image.png

パッケージマネージャコンソールが見つからない場合は、右上のクイック起動で「パッケージ」と検索すれば簡単です。

image.png


4. Mainを変更

なんと! あとはMainから呼び出すようにすれば完成です。


Program.cs

using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GPPGCalculator
{
class Program
{
static void Main(string[] args)
{
// 作った電卓のParserを生成する。
// 字句解析器は、この中で生成されている。
var parser = new Calculator.CalculatorParser();

do
{
// 式を入力。空文字なら終了。
Console.Write("> ");
var input = Console.ReadLine();
if (string.IsNullOrEmpty(input))
{
return;
}

// 式を解析。この中で結果も出力される。
parser.Parse(input);
} while (true);
}
}
}



実行例

数式を入力すると、演算結果を出力します。

構文解析の様子も出力されます。

> 1+1

token: 1
Rule -> number: 1
Rule -> factor: 1
Rule -> term: 1
token: +
Rule -> exp: 1
token: 1
Rule -> number: 1
Rule -> factor: 1
Rule -> term: 1
Rule -> exp: 1 + 1
result is 2

> 1 + 2 * 3
token: 1
Rule -> number: 1
Rule -> factor: 1
Rule -> term: 1
token: +
Rule -> exp: 1
token: 2
Rule -> number: 2
Rule -> factor: 2
Rule -> term: 2
token: *
token: 3
Rule -> number: 3
Rule -> factor: 3
Rule -> term: 2 * 3
Rule -> exp: 1 + 6
result is 7


補足

自動生成される字句解析と構文解析のコードを以下に貼り付けておきます。


Calculator.Language.analyzer.lex

%namespace Calculator

%scannertype CalculatorScanner
%visibility internal
%tokentype Token

%option stack, minimize, parser, verbose, persistbuffer, noembedbuffers

Eol (\r\n?|\n)
NotWh [^ \t\r\n]
Space [ \t]
Number [0-9]+
OpPlus \+
OpMinus \-
OpMult \*
OpDiv \/
POpen \(
PClose \)

%{

%}

%%

{Number} { Console.WriteLine("token: {0}", yytext); GetNumber(); return (int)Token.NUMBER; }

{Space}+ /* skip */

{OpPlus} { Console.WriteLine("token: {0}", yytext); return (int)Token.OP_PLUS; }
{OpMinus} { Console.WriteLine("token: {0}", yytext); return (int)Token.OP_MINUS; }
{OpMult} { Console.WriteLine("token: {0}", yytext); return (int)Token.OP_MULT; }
{OpDiv} { Console.WriteLine("token: {0}", yytext); return (int)Token.OP_DIV; }
{POpen} { Console.WriteLine("token: {0}", yytext); return (int)Token.P_OPEN; }
{PClose} { Console.WriteLine("token: {0}", yytext); return (int)Token.P_CLOSE; }

%%



Calculator.Language.grammar.y

%namespace Calculator

%partial
%parsertype CalculatorParser
%visibility internal
%tokentype Token

%union {
public int n;
public string s;
}

%start line

%token NUMBER, OP_PLUS, OP_MINUS, OP_MULT, OP_DIV, P_OPEN, P_CLOSE

%%

line : exp { Console.WriteLine("result is {0}\n", $1.n);}
;

exp : term { $$.n = $1.n; Console.WriteLine("Rule -> exp: {0}", $1.n); }
| exp OP_PLUS term { $$.n = $1.n + $3.n; Console.WriteLine("Rule -> exp: {0} + {1}", $1.n, $3.n); }
| exp OP_MINUS term { $$.n = $1.n - $3.n; Console.WriteLine("Rule -> exp: {0} - {1}", $1.n, $3.n); }
;

term : factor {$$.n = $1.n; Console.WriteLine("Rule -> term: {0}", $1.n); }
| term OP_MULT factor {$$.n = $1.n * $3.n; Console.WriteLine("Rule -> term: {0} * {1}", $1.n, $3.n); }
| term OP_DIV factor {$$.n = $1.n / $3.n; Console.WriteLine("Rule -> term: {0} / {1}", $1.n, $3.n); }
;

factor : number {$$.n = $1.n; Console.WriteLine("Rule -> factor: {0}", $1.n); }
| P_OPEN exp P_CLOSE {$$.n = $2.n; Console.WriteLine("Rule -> factor: ( {0} )", $3.n);}
;

number :
| NUMBER { Console.WriteLine("Rule -> number: {0}", $1.n); }
;

%%



参考URL

https://github.com/ernstc/YaccLexTools