C#
コンパイラ
構文解析
字句解析

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