演算子の字句解析

ゲームに簡単なプログラムを組み込むためにスクリプト言語とそれを実行するインタープリタをC++で作ってしまおうというコーナーです。
前回で小数を読み込めるスキャナができました。

'+'と'-'

今回は演算子を解析するスキャナでも作ってさっさとパーサ作りに入ってしまおうかと思ったのですが、・・・ ・・・ やっぱり演算子を解析するスキャナでも作っちゃいます。深いことを考えるのは後だ!
というわけで、プラスとマイナスです。
後々'++'とか'+='とかも出てきますが、どうせ拡張は簡単なので後のことは後で考えるとして、単なる'+'と'-'だけを考えてみます。
'+'と'-'は意味は正反対だけど立場はよく似ているということで、この二つを同じ種類のトークンとして扱うことは第5回で述べました。
この手抜きにより単項演算子'+'も生まれてしまいますが、別に実行に悪影響を及ぼすものではないので深く考えないでおきます。

さて、今回もまた前々回と同様にトークンをあらわすクラス作りから始めます。

class CMSPlusMinusToken : public CMSToken
{
public:
	double m_dSign;
	CMSPlusMinusToken(double sign) { m_dSign=sign; }
};

何度か'+'と'-'が反対の意味だといいましたが、「足す」ということは「1を掛けて足す」ということと同じであり、「引く」こともまた、「-1を掛けて足す」という点で、「足す」と同じ意味を持ちます。
だから、'+'と'-'とを区別するためには、何を掛けてから足すかという情報を数値として持っておけば事足ります。
メンバ変数のm_dSignはそのような意味を持ちます。double型なのは単なる気まぐれです。

else if (*ptr=='+' || *ptr=='-') {
	m_TokenList.push_back(new CMSPlusMinusToken(*ptr=='+'?1:-1));
}

字句解析のほうはこんなもの。簡単です。
いうまでもありませんが数値の解析の直後に置いてます。

'*'と'/'

先ほどと同じ理屈でいえば、「掛ける」は「1乗して掛ける」、「割る」は「-1乗して掛ける」ということになります。
コード略。クラス名はCMSMultiplyDivideTokenにします。
ただ、n乗の計算はかなり遅いし、コード的にも条件分岐で書いたほうが速く済みます。

'('と')'

'('と')'は意味的に全く異なり、異なるトークンとして扱うので、今回出た4つの演算子や前回までの数値より圧倒的に簡単です。
書くのもバカバカしいですが、一応コードを示しておきます。

class CMSBracketStartToken : public CMSToken
{
};
class CMSBracketEndToken : public CMSToken
{
};

だって、やることないんですもん!
スキャナもあっさりしてます。

else if (*ptr=='(') {
	m_TokenList.push_back(new CMSBracketStartToken());
}
else if (*ptr==')') {
	m_TokenList.push_back(new CMSBracketEndToken());
}

字句の種類

さて、これで数式に使う字句程度は解析できるようになりましたが、しかし、この先パーサに渡して構文解析してもらわなければ全然意味がないのです。
でも、スキャナに入っているトークンのリストは、あくまでもCMSToken*のリストであり、いろんな種類のトークンはみんなCMSToken*として仲良く入っているわけです。
そのままじゃCMSTokenのポインタであることしかわからなくて、なんとかしてリストに入ってるトークンの正体を知らなければまともに使えないのです。
だからトークンの種類を識別するための識別子を用意してみます。

enum EMSTokenType {
	eMSNumericalValueToken,
	eMSPlusMinusToken,
	eMSMultiplyDivideToken,
	eMSBracketStartToken,
	eMSBracketEndToken,
};

で、これらをメンバ変数に入れて、と言いたいところですが、メンバ変数は値を入れてなくてもコンパイルが通ってしまうので、トークンの種類を増やしたとき値を入れ忘れたりすると一大事です。
よって、次のように純粋仮想関数を使ってCMSTokenの派生クラス、つまり、実際のトークンの内容を示すクラスで種類がわかるようにしなければならないようにしておきます。

virtual EMSTokenType GetTokenType() = 0;

各々の派生クラスでのコードは省略。
そういえば第10回でも似たようなことをしてましたね。

テスト

テストではトークンの種類を判別して数値ならその値を、演算子ならその種類を表示します。
久々に「15+7-2-2-(2+3)*12/(14-4)」の数式を使ってみます。

int main(int argc, char* argv[])
{
	// テスト
	CMSScanner scanner;
	scanner.Scan("15+7-2-2-(2+3)*12/(14-4)");
	for (std::list<CMSToken*>::iterator it=scanner.m_TokenList.begin(); it!=scanner.m_TokenList.end(); it++) {
		if ((*it)->GetTokenType() == eMSNumericalValueToken) {
			printf("N\t:%f\n", ((CMSNumericalValueToken*)*it)->m_dValue);
		}else if ((*it)->GetTokenType() == eMSPlusMinusToken) {
			printf("PM\t:%c\n", ((CMSPlusMinusToken*)*it)->m_dSign==1?'+':'-');
		}else if ((*it)->GetTokenType() == eMSMultiplyDivideToken) {
			printf("MD\t:%c\n", ((CMSMultiplyDivideToken*)*it)->m_dSign==1?'*':'/');
		}else if ((*it)->GetTokenType() == eMSBracketStartToken) {
			printf("BS\t:(\n");
		}else if ((*it)->GetTokenType() == eMSBracketEndToken) {
			printf("BE\t:)\n");
		}
	}
	return 0;
}

結果は次のようになります。
正しく動いていることがわかります。

N       :15.000000
PM      :+
N       :7.000000
PM      :-
N       :2.000000
PM      :-
N       :2.000000
PM      :-
BS      :(
N       :2.000000
PM      :+
N       :3.000000
BE      :)
MD      :*
N       :12.000000
MD      :/
BS      :(
N       :14.000000
PM      :-
N       :4.000000
BE      :)

今回までのソース(VC++6)