23.構造を考える

23-01.driverクラス

driverクラスの役割
 Ubuntuでひな形を作る2で作成した計算機に、cpp側とのインターフェイスを作成します。21-01.Bison3.8.1で紹介したdriverクラスと似たようなクラスです。まずdriver.hです。
#ifndef DRIVER_H
#define DRIVER_H
#include <iostream>
#include <fstream>
#include <string>
#include <map>

using namespace std;

class driver
{
    driver ();
public:
    void output(double a);
    double createNUM(const string& a);

    int parse (const string& f);
    string m_file;
    static driver* get();

};

#endif // ! DRIVER_H

driver.cpp
 続いてdriver.cppです。日本語も入ってます。必ずutf-8で保存しましょう。
#include "driver.h"

driver::driver ()
{
}

void driver::output(double a){
    cout << ">>" << a << std::endl;
}

double driver::createNUM(const string& a){
    double d = (double)strtof(a.c_str(),nullptr);
    return d;
}

driver* driver::get(){
    static driver* instance = nullptr;
    if(!instance){
      instance = new driver();
    }
    return instance;
}

int
driver::parse (const string &f)
{
    extern int yyparse(void);
    extern FILE* yyin;
    m_file = f;

    if ((yyin = fopen(m_file.c_str(), "r")) == NULL) {
        cerr << "ファイル読み込みに失敗しました" << endl;
        return 1;
    }
    if (yyparse()) {
        cout << "プログラム終了" << endl;
        return 1;
    }
    return 0;
}
 ここで、赤くなっているdriver::get()関数について補足します。
 driverクラスはコンストラクタがprivateメンバになっています。こうすると、外部からインスタンスを作成できません。そのうえでdriver::get()関数を用意します。driver.hを見るとわかりますが、この関数はstatic関数になっていて、戻り値がdriver型のポインタを返す形となっています。
 driver::get()関数は以下のようになっていて
driver* driver::get(){
    static driver* instance = nullptr;
    if(!instance){
      instance = new driver();
    }
    return instance;
}
 static driver* instanceという変数を保持しています。これをnullptrで初期化しておきます。
 そしてinstanceがnullptrであればnewでインスタンスを作成します。static変数ですので、一度インスタンスが構築されると以降はそのポインタを使う形となります。
 こうしておけばdriver.hがインクルードされている場所であれば
    driver* drv = driver::get();
 という記述で、このインスタンスを取得できます。この方法はシングルトンと言って、アプリケーション中に一つのインスタンスのみ許す、時に使用します。今回は別にシングルトンにしなくても問題ありませんが、シングルトンにしておいたほうが、簡単にインスタンスにアクセスできるので、この方法を使用しました。
 driverクラスにはこのほかに
    void output(double a);
    double createNUM(const string& a);
 という2つのメンバ関数があります。output()関数は計算結果を表示する関数です。この計算機は、改行ードがあると、結果を出力して、構文解析は終了ですので、この先解析はありませんので、void型を返します。
 createNUM()関数string型の参照を受け取って、そこからdouble型を作り出し、それを返す関数です。数字から数値を作ります。
 parse()関数は、ファイル名の文字列を受け取って、bisonファイルとのやりとりを行います。具体的には以下ですが
int
driver::parse (const string &f)
{
    extern int yyparse(void);
    extern FILE* yyin;
    m_file = f;

    if ((yyin = fopen(m_file.c_str(), "r")) == NULL) {
        cerr << "ファイル読み込みに失敗しました" << endl;
        return 1;
    }
    if (yyparse()) {
        cout << "プログラム終了" << endl;
        return 1;
    }
    return 0;
}
 まず
    extern int yyparse(void);
    extern FILE* yyin;
 とbisonの関数と変数をextern宣言してます。これで、bisonファイル内のそれらの変数や関数にアクセスできます。
>
    if ((yyin = fopen(m_file.c_str(), "r")) == NULL) {
        cerr << "ファイル読み込みに失敗しました" << endl;
        return 1;
    }
 で、ファイルをオープンし、成功したらそのファイルポインタをyyinに渡します。そうしたうえで
    if (yyparse()) {
        cout << "プログラム終了" << endl;
        return 1;
    }
 と、baisonファイル内の構文解析関数(yyparse()関数)を呼び出します。この関数は失敗すると1を返すので、その場合は終了します。正常終了の場合は、0を返します。
 このdriver::parse()関数main()関数から呼ばれます。
parser.y
 続いてparser.yも書き換えます。
%{
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include "driver.h"

#define YYDEBUG 1

extern int yylex(void);

int
yyerror(char const *str)
{
    extern char *yytext;
    fprintf(stderr, "parser error near %s\n", yytext);
    return 0;
}

%}
%union {
    double  double_value;
    char* literal_value;
}
%token <double_value>    DOUBLE_VALUE
%token <literal_value>   DOUBLE_LITERAL_PTR
%token
  ADD "+"
  SUB "-"
  MUL "*"
  DIV "/"
  CR
;
%type <double_value> expression term primary_expression
%%
line_list
    : line
    | line_list line
    ;
line
    : expression CR
    {
        driver::get()->output($1);
    }
expression
    : term
    | expression "+" term
    {
        $$ = $1 + $3;
    }
    | expression "-" term
    {
        $$ = $1 - $3;
    }
    ;
term
    : primary_expression
    | term "*" primary_expression 
    {
        $$ = $1 * $3;
    }
    | term "/" primary_expression
    {
        $$ = $1 / $3;
    }
    ;
primary_expression
    : DOUBLE_LITERAL_PTR
    {
        $$ = driver::get()->createNUM($1);
    }
    ;
%%
 最初の%{から%}はC++のソースファイルを記述します。ここでは、ライブラリのインクルードとか、エラー関数を実装します。
%{
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include "driver.h"

#define YYDEBUG 1

extern int yylex(void);

int
yyerror(char const *str)
{
    extern char *yytext;
    fprintf(stderr, "parser error near %s\n", yytext);
    return 0;
}

%}
 続くブロックはBison宣言を記述します。
%union {
    double  double_value;
    char* literal_value;
}
%token <double_value>    DOUBLE_VALUE
%token <literal_value>   DOUBLE_LITERAL_PTR
%token
  ADD "+"
  SUB "-"
  MUL "*"
  DIV "/"
  CR
;
%type <double_value> expression term primary_expression
 %union共用体です。C/C++のunionに展開します。double double_value;という変数とchar* literal_value;同じ領域を使うという意味です。
 次の
%token <double_value>    DOUBLE_VALUE
%token <literal_value>   DOUBLE_LITERAL_PTR
 は%unionで定義された内容のうちdouble_value該当する(すなわちdouble型の時)はDOUBLE_LITERALというトークンを返しなさい、という意味です。
 同様、char*の時はDOUBLE_LITERAL_PTRを返しなさい、という意味になります。
 続く
%token
  ADD "+"
  SUB "-"
  MUL "*"
  DIV "/"
  CR
;
 は、終端記号です。DOUBLE_VALUEDOUBLE_LITERAL_PTRもそうですが%tokenで定義されるトークンは$$に入れることはできません。
 続く
%type <double_value> expression term primary_expression
 は非終端記号です。$$に設定することができる。21.Ubuntuでひな形を作るでも説明した、つまりは以下のresult:部)に置けるキーワードです。
result: components...
        ;
 そして、そのあとの%%で囲まれた領域が文法規則の構文になります。
%%
line_list
    : line
    | line_list line
    ;
line
    : expression CR
    {
        driver::get()->output($1);
    }
expression
    : term
    | expression "+" term
    {
        $$ = $1 + $3;
    }
    | expression "-" term
    {
        $$ = $1 - $3;
    }
    ;
term
    : primary_expression
    | term "*" primary_expression 
    {
        $$ = $1 * $3;
    }
    | term "/" primary_expression
    {
        $$ = $1 / $3;
    }
    ;
primary_expression
    : DOUBLE_LITERAL_PTR
    {
        $$ = driver::get()->createNUM($1);
    }
    ;
%%
 このブロックは、下から読んでいきます。
 まず
primary_expression
    : DOUBLE_LITERAL_PTR
    {
        $$ = driver::get()->createNUM($1);
    }
    ;
 は%typeで定義されたprimary_expression非終端記号になります。
 DOUBLE_LITERAL_PTR%tokenchar*ですから、$$ = driver::get()->createNUM($1);により、DOUBLE_LITERAL_PTR$1として渡され、driver::get()->createNUM()関数を呼び出します。
 これはdriver.cppにあるように
double driver::createNUM(const string& a){
    double d = (double)strtof(a.c_str(),nullptr);
    return d;
}
 としてconst string& aに変換されます。std::stringchar*const string&として受けることができます。そのstringの参照aを使ってdouble dを作り出し、それを返しています。
 ですので$$ = driver::get()->createNUM($1);$$すなわちprimary_expressionにdouble型の値を渡すことになります。
 そしてprimary_expressionは一つ上のブロック
term
    : primary_expression
    | term "*" primary_expression 
    {
        $$ = $1 * $3;
    }
    | term "/" primary_expression
    {
        $$ = $1 / $3;
    }
    ;
 に現れます。term(すなわち$$)に渡せ、という意味です。
 定義部({ }で囲まれた部分)が省略されると$1$$に渡されます。ですから
term
    : primary_expression
 は
term
    : primary_expression
    {
      $$ = $1;
    }
 と等価です。
 このようにして文法規則の構文が積み上げられ、最後に
line_list
    : line
    | line_list line
    ;
 にたどり着きます。  line_listlineである。またはline_list lineである、ということですが、このline_list line任意の数の行を読み込んだ後で、もし可能ならば、 もう1行読み込むという意味になります。
scanner.l
 scanner.lも書き換えます。
%{
#include <stdio.h>
#include "parser.hpp"

#define YY_SKIP_YYWRAP 1

int
yywrap()
{
    return 1;
}
%}
%%
"+"     return ADD;
"-"     return SUB;
"*"     return MUL;
"/"     return DIV;
"\n"    return CR;
[1-9][0-9]* {
    yylval.literal_value = yytext;
    return DOUBLE_LITERAL_PTR;
}
[0-9]*\.[0-9]* {
    yylval.literal_value = yytext;
    return DOUBLE_LITERAL_PTR;
}
[ \t] ;
%%
 スキャナーの役割は字句解析です。ソースコードをトークンに分割し、それをパーサーに渡します。
 まず、冒頭の
%{
#include <stdio.h>
#include "parser.hpp"

#define YY_SKIP_YYWRAP 1

int
yywrap()
{
    return 1;
}
%}
 は、parser.y同様にC/C++の構文を記述します。ここではいくつかヘッダをインクルードしてyywrap()関数を記述します。
 インクルードされるファイルのうちparser.hppは、ヘッダファイルです。Makefileで今回はparser.ybisonで実行するときに-dオプションを付けています。こうするとヘッダファイルを作成してくれます。そうするとscanner.lではparser.yで定義された%unionyylval.literal_valueにアクセスできます。yylvalunionの変数名です。
 続くyywrap()関数は、あまり考えずにreturn 1;を書いておきましょう。
 次の%%%%に囲まれたブロックがscanner.lの本体です。
%%
"+"     return ADD;
"-"     return SUB;
"*"     return MUL;
"/"     return DIV;
"\n"    return CR;
[1-9][0-9]* {
    yylval.literal_value = yytext;
    return DOUBLE_LITERAL_PTR;
}
[0-9]*\.[0-9]* {
    yylval.literal_value = yytext;
    return DOUBLE_LITERAL_PTR;
}
[ \t] ;
%%
 ここでは正規表現を使ってトークンに分けます。そしてそれぞれのトークンをparser.yの定義と互換を保ちながらreturnします。
 ここで出てくるyytextとは、トークン分けされた文字列そのものです(char*型)。yylval.literal_value = yytext;数字の文字列unionの変数に代入しています。
main.cpp
 main.cppも書き換えます。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include "driver.h"

using namespace std;

int
main (int argc, char *argv[])
{
    driver* drv = driver::get();
    for (int i = 1; i < argc; ++i){
        return drv->parse(argv[i]);
    }
    return 0;
}
 ここでは、コマンドラインからスクリプトのファイル名を取り出し、driverクラスparse()関数に渡しています。今回のスクリプトはtest.txtですね。
 1つのファイルを処理したら、そのままリターンするので、複数のスクリプトファイルには対応していません。
Makefile
 Makefileも書き換えます。今回はparser.yからヘッダファイル(parser.hpp)を書き出すような内容になっています(bisonコマンドに-dオプションをつけています)。
calcCpp : scanner.cpp parser.hpp parser.cpp main.cpp driver.cpp driver.h
	g++ -o calcCpp scanner.cpp parser.cpp driver.cpp main.cpp

scanner.cpp : scanner.l
	flex -o scanner.cpp scanner.l

parser.hpp parser.cpp : parser.y
	bison -d -o parser.cpp parser.y
 これでmakeして
./calcCpp test.txt
 を実行してみましょう。前項と同じ出力が出れば成功です。