概要
c言語ならyacc/lex、c++ならbison/flexがあるように、c#にはGPPG/GPLEXがあります。
これを使えば、字句解析/構文解析が可能です。
昔は導入が面倒だったのですが、いつからかNuGetで簡単に導入できるようになりました。
今回はサンプルとして電卓を作りたいと思います。
サンプルコード
以下に実際に動作するコードを置いてます。
https://github.com/minoru-nagasawa/GPPGCalculator
作成方法
1. プロジェクトを作成
今回はコンソールアプリで作ります。
名前はGPPGCalculatorとします。
.NET Coreは未対応ですので、.NET Frameworkにしてください。
2. NuGetでYaccLexToolsをインストール
検索で「YaccLex」や「GPPG」を入力すれば出てきます。
3. サンプル電卓用のソースコードを自動生成
GPPG/GPLEXには、サンプル電卓のソースコードを自動生成する機能があるので使います。
パッケージマネージャーコンソールから「Add-CalculatorExample」と入力してください。
これにより、Calculator.Language.grammar.yなどが自動生成されます。
パッケージマネージャコンソールが見つからない場合は、右上のクイック起動で「パッケージ」と検索すれば簡単です。
4. Mainを変更
なんと! あとはMainから呼び出すようにすれば完成です。
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
補足
自動生成される字句解析と構文解析のコードを以下に貼り付けておきます。
%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; }
%%
%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