Doc.21 構文解析ツールbison(2)

前回はbisonを使った代入文の書式チェックについて書きました。そろそろ、ここまで読んでいただいた方の中には次のような疑問が出てくるかと思います。

  • 変数名や数字の内容を、個々に識別したい場合はどうすればいいのか。今はトークンだけしかやりとしていないではないか。
  • また、仮に識別できたとして、どのようにC言語の構造体や変数に入れるべきか。

・・・ごもっともな意見です。これらの解答は、re2cで紹介したcsvの書式チェックを再度取り上げて、改造することにします。車輪の開発 第二弾です。

csvの書式チェックと読み込み

スタートはcsv、ゴールは構造体、中間の処理はbison&re2cという流れになります。現実味を持たせるため、項目とデータは書籍「ダンジョンゲームプログラミング」からお借りさせていただきました。

・項目

  • 名前
  • 出現階層(開始)
  • 出現階層(終了)
  • AI(モード番号)
  • 経験値
  • 回避率
  • 攻撃力
  • 防御力
  • 行動ターン
  • 体力
  • 画像ID
  • ドロップアイテムID
  • ドロップアイテム率
  • 未鑑定率

・csv.txt

"Gスネーク",1,10,1,3,20,1,0,30,8,0,0,98,0
"ノーム",2,13,2,8,30,10,18,28,20,1,2,90,3
"ドラゴン",22,31,3, 810,40,75,180,15,500,3,2,100,15

・csv.h

#ifndef __CSV_H__
#define __CSV_H__


#include "lib/gba.h" // DUMMY

//---------------------------------------------------------------------------
#define MAX_CSV_CNT			3
#define MAX_CSV_NAME_CNT		20


//---------------------------------------------------------------------------

typedef struct {
	char name[MAX_CSV_NAME_CNT];
	u16  firstFloorCnt;
	u16  lastFloorCnt;
	u16  ai;
	s16  exp;
	s16  agi;
	s16  atk;
	s16  def;
	s16  speed;
	s16  hp;
	s16  graphic;
	s16  dropItemID;
	s16  dropPercent;
	s16  dropUnidentified;
} ST_CSV_DB;


//---------------------------------------------------------------------------
void CsvInit(char* pBuf);
bool CsvParse();

void CsvDispAll();


#endif

・csv.c

#include "csv.h"
#include "re2c.h"
#include "bison.h"


//---------------------------------------------------------------------------
ST_CSV_DB CsvDb[MAX_CSV_CNT];
s16       CsvCnt;


//---------------------------------------------------------------------------
void CsvInit(char* pBuf)
{
	yyInit(pBuf);
}
//---------------------------------------------------------------------------
bool CsvParse()
{
	return (yyparse() == 0) ? TRUE : FALSE;
}
//---------------------------------------------------------------------------
void CsvDispAll()
{
	s16 i;

	for(i=0; i<CsvCnt; i++)
	{
		printf("----------------------------------\n");
		printf("%s,", CsvDb[i].name);
		printf("%d,", CsvDb[i].firstFloorCnt);
		printf("%d,", CsvDb[i].lastFloorCnt);
		printf("%d,", CsvDb[i].ai);
		printf("%d,", CsvDb[i].exp);
		printf("%d,", CsvDb[i].agi);
		printf("%d,", CsvDb[i].atk);
		printf("%d,", CsvDb[i].def);
		printf("%d,", CsvDb[i].speed);
		printf("%d,", CsvDb[i].hp);
		printf("%d,", CsvDb[i].graphic);
		printf("%d,", CsvDb[i].dropItemID);
		printf("%d,", CsvDb[i].dropPercent);
		printf("%d\n", CsvDb[i].dropUnidentified);
	}
}

Csvモジュールは、字句解析や構文解析についての呼び出しを受け持つようにしています。このことにより、このモジュールを使う顧客側は、内部で何をしているのか隠蔽することができます。仮に修正があってもモジュール外に飛び火することはありません。

main.c

Csvモジュールの顧客側です。GBA上では不要な処理ですが、csvファイルをメモリにコピーしています。あとは順番に先ほどのCsvInit、CsvParse、CsvDispAll関数を実行しています。

#include <stdio.h>
#include <stdlib.h>
#include "csv.h"

//---------------------------------------------------------------------------


//---------------------------------------------------------------------------
int main(int argc, char **argv)
{
	if(argc != 2)
	{
		printf("err argc");
		return -1;
	}

	FILE* fp = fopen(argv[1], "rb");
	if(fp == NULL)
	{
		printf("err filename");
		return -1;
	}

	fpos_t fsize;
	fseek(fp,0,SEEK_END);
	fgetpos(fp, &fsize);
	fseek(fp,0,SEEK_SET);

	char* pBuf = (char*)calloc(fsize+1, sizeof(char));

	if(pBuf == NULL)
	{
		printf("err calloc");
		return -1;
	}

	int i = 0;
	int ch;

	while((ch = fgetc(fp)) != EOF)
	{
		pBuf[i++] = ch;
	}

	fclose(fp);


	CsvInit(pBuf);

	if(CsvParse() == FALSE)
	{
		printf("err yyparse");
		return -1;
	}

	CsvDispAll();
	return 0;
}

re2c.h、re2c.re

・re2c.h

#ifndef __RE2C_H__
#define __RE2C_H__


#include "lib/gba.h" // DUMMY

//---------------------------------------------------------------------------
#define YYTEXT_MAX_CNT     256
#define YYMARKER           yymarker


//---------------------------------------------------------------------------
void yyInit(char* pSrc);

void yyResetText();
bool yyFillText();
int  yylex();

#endif

・re2c.c

#include "re2c.h"

#define YYSTYPE char*
#include "bison.h"


//---------------------------------------------------------------------------
char* yyin;
char* yyold;
char* yymarker;

int   yytextcnt;
char  yytext[YYTEXT_MAX_CNT];

//---------------------------------------------------------------------------
void yyInit(char* pSrc)
{
   yyin = pSrc;

   yyResetText();
}
//---------------------------------------------------------------------------
void yyResetText()
{
   yytextcnt = 0;
}
//---------------------------------------------------------------------------
bool yyFillText()
{
   char* pCh  = yyold;
   int   size = yyin - yyold;
   int   i;

   if(yytextcnt+size+1 >= YYTEXT_MAX_CNT)
   {
       return FALSE;
   }
   yylval = &yytext[yytextcnt];


   for(i=0; i<size; i++)
   {
       yytext[yytextcnt++] = *pCh++;
   }
   yytext[yytextcnt++] = '\0';


   return TRUE;
}
//---------------------------------------------------------------------------
int yylex()
{
start:
   yyold = yyin;


/*!re2c

   re2c:define:YYCTYPE  = "char";
   re2c:define:YYCURSOR = yyin;
   re2c:yyfill:enable   = 0;
   re2c:indent:top      = 1;

   ws      =   [\r\t ]+;
   lf      =   [\n];
   digit   =   [0-9]+;
   str     =   ('"').+('"');
   end     =   [\000];
   other   =   .;

   digit   { yyFillText(); return TOKEN_NUM; }
   str     { yyFillText(); return TOKEN_STR; }
   lf      { return TOKEN_LF;    }
   ","     { return TOKEN_COMMA; }
   ws      { goto start;         }
   end     { return 0;           }
   other   { return TOKEN_ERR;   }
*/
}

ポイントは、digitまたはstrだった場合、影響範囲の文字列をバッファにコピーしているところです。このとき、yyFillText関数内の、yylval(bison側の定義)がとても重要な仕事をしてくれます。詳しいことは次のbison.y で説明します。

bison.y

%{
#include "re2c.h"
#include "csv.h"

#define YYSTYPE char*
#include "bison.h"

//---------------------------------------------------------------------------
// #define YYDEBUG             1
// #define YYERROR_VERBOSE     1
// int yydebug = 1;


//---------------------------------------------------------------------------
// csv.c
extern ST_CSV_DB CsvDb[MAX_CSV_CNT];
extern s16       CsvCnt;


//---------------------------------------------------------------------------
void yyerror(char* str);

%}


%token TOKEN_NUM
%token TOKEN_STR
%token TOKEN_COMMA
%token TOKEN_LF
%token TOKEN_ERR


%%
csv  : csv line
     | line
     ;

line : TOKEN_STR TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_LF
            {
                if(CsvCnt >= MAX_CSV_CNT)
                {
                    yyerror("CsvCnt Max!");
                    YYERROR;
                }

                strncpy(CsvDb[CsvCnt].name, $1, MAX_CSV_NAME_CNT);
                CsvDb[CsvCnt].firstFloorCnt    = atoi($3);
                CsvDb[CsvCnt].lastFloorCnt     = atoi($5);
                CsvDb[CsvCnt].ai               = atoi($7);
                CsvDb[CsvCnt].exp              = atoi($9);
                CsvDb[CsvCnt].agi              = atoi($11);
                CsvDb[CsvCnt].atk              = atoi($13);
                CsvDb[CsvCnt].def              = atoi($15);
                CsvDb[CsvCnt].speed            = atoi($17);
                CsvDb[CsvCnt].hp               = atoi($19);
                CsvDb[CsvCnt].graphic          = atoi($21);
                CsvDb[CsvCnt].dropItemID       = atoi($23);
                CsvDb[CsvCnt].dropPercent      = atoi($25);
                CsvDb[CsvCnt].dropUnidentified = atoi($27);
                CsvCnt++;

                yyResetText();
            }
     ;
%%


//---------------------------------------------------------------------------
void yyerror(char* str)
{
   printf("!! yyerror !! = %s\n", str);
}

ソースコードの解説です。

csv  : csv line
     | line
     ;

csv, lineは非終端記号です。日本語訳すると「csvは、csv lineまたはlineで構成される」といった意味になります。よくみるとcsvは左側と右側の両方に現れていて、どういう動きになるのか不思議に思うかもしれません。lineの中身はデータの1行分に相当するので具体的な動きは以下のとおりです。

line -> csv(A)         # 1行目
csv(A) line -> csv(B)  # 2行目 
csv(B) line -> csv(C)  # 3行目 

csvを同一するのはちょっと読みにくいので、(A)(B)(C)でそれぞれ表してみました。このことから構文上、問題ないことがわかります。

line : TOKEN_STR TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_LF
            {
                if(CsvCnt >= MAX_CSV_CNT)
                {
                    yyerror("CsvCnt Max!");
                    YYERROR;
                }

                strncpy(CsvDb[CsvCnt].name, $1, MAX_CSV_NAME_CNT);
                CsvDb[CsvCnt].firstFloorCnt    = atoi($3);
                CsvDb[CsvCnt].lastFloorCnt     = atoi($5);
                CsvDb[CsvCnt].ai               = atoi($7);
                CsvDb[CsvCnt].exp              = atoi($9);
                CsvDb[CsvCnt].agi              = atoi($11);
                CsvDb[CsvCnt].atk              = atoi($13);
                CsvDb[CsvCnt].def              = atoi($15);
                CsvDb[CsvCnt].speed            = atoi($17);
                CsvDb[CsvCnt].hp               = atoi($19);
                CsvDb[CsvCnt].graphic          = atoi($21);
                CsvDb[CsvCnt].dropItemID       = atoi($23);
                CsvDb[CsvCnt].dropPercent      = atoi($25);
                CsvDb[CsvCnt].dropUnidentified = atoi($27);
                CsvCnt++;

                yyResetText();
            }
     ;

・・・実はここで訂正が1つありまして(^^;。前回紹介したところの

非終端記号 : 構成される要素
           ;

は、正確に書くと

非終端記号 : 構成される要素 { アクション }
           ;

になります。アクションはC言語で記載することができ、構成される要素が確定した時に実行されます。一度に説明するのは苦しいと思い、嘘をつかせて頂きましたm(__)m。

                if(CsvCnt >= MAX_CSV_CNT)
                {
                    yyerror("CsvCnt Max!");
                    YYERROR;
                }

アクションの中身の最初です。csv.cで定義されている、ST_CSV_DB構造体の領域内に収まるかチェックをしていて、収まらない場合、YYERRORマクロを使って強制的にエラーにしています。詳しくはマニュアルをご覧ください。

                strncpy(CsvDb[CsvCnt].name, $1, MAX_CSV_NAME_CNT);
                CsvDb[CsvCnt].firstFloorCnt    = atoi($3);
                CsvDb[CsvCnt].lastFloorCnt     = atoi($5);
                CsvDb[CsvCnt].ai               = atoi($7);
                CsvDb[CsvCnt].exp              = atoi($9);
                CsvDb[CsvCnt].agi              = atoi($11);
                CsvDb[CsvCnt].atk              = atoi($13);
                CsvDb[CsvCnt].def              = atoi($15);
                CsvDb[CsvCnt].speed            = atoi($17);
                CsvDb[CsvCnt].hp               = atoi($19);
                CsvDb[CsvCnt].graphic          = atoi($21);
                CsvDb[CsvCnt].dropItemID       = atoi($23);
                CsvDb[CsvCnt].dropPercent      = atoi($25);
                CsvDb[CsvCnt].dropUnidentified = atoi($27);
                CsvCnt++;

                yyResetText();

構造体にデータを格納しています。この$1〜$27は、

line : TOKEN_STR TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_COMMA
       TOKEN_NUM TOKEN_LF

と対応しています。$1はTOKEN_STR, $3は2行目のTOKEN_NUMといったことを指し示しています。もちろん$2はTOKEN_COMMAですが、構造体には不要なので使わないでいます。型はbison.yの最初の方で定義済みです。

#define YYSTYPE char*

このように整然と受け渡しができるのは、yylvalのおかげといっても過言ではありません。トークンが確定したとき、bisonがyylvalを通してポインタをどこかに逐次格納していると思っていただければ幸いです。次回はbisonの動作原理について見ていきます。

Doc.22 構文解析ツールbison(3)に続きます・・・。

履歴

  • 2008/11/07

Last-modified: 2008-11-08 (土) 22:41:22 (4166d)