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

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

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

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

GPPG/GPLEXには、サンプル電卓のソースコードを自動生成する機能があるので使います。
パッケージマネージャーコンソールから「Add-CalculatorExample」と入力してください。
これにより、Calculator.Language.grammar.yなどが自動生成されます。

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

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