前回の
字句解析器の作り方
(2)
正規表現で処理するのは正解?
字句解析器や構文解析器を開発するときによく利用するものとして正規表現があります。正規表現は文字列処理を行ううえでとても有益ですが、
正規表現で表現できないシンタックスが存在する場合
当然ながら、
もちろん、
同じ文字を何度も探索しなければならない場合
1つの正規表現で表現できない場合、
字句解析器の基本構成
本項では、
- トークン
(Token) - 字句解析器によって切り出された文字列
- カーソル
(Cursor) - 現在処理している文字の位置を表す
- スキャナ
(Scanner) - デリミタの開始位置から終了位置までカーソルを進める
(また、 その間の文字列をトークンとして切り出す) - アノテータ
(Annotator) - 切り出したトークンに解析情報を付加する
これらの用語を用いて字句解析器のテンプレートを表現すると、
std::vector<char *> token_array;
Scanner scanner;
// 字句解析器のメインループ
for ( ソースコードの終端まで、カーソルを1 文字ずつ進める) {
// カーソルがある位置の文字を得る
char current_char = get_current_char(cursor,
source_code);
switch (current_char) {
case '\'': '"': // 文字列の開始デリミタの場合
// スキャナによってcursor を進めつつ、
// 文字列の終端を見つけたら、トークンに切り出す
char *token = scanner.scanQuote(current_char,
source_code,
cursor);
// 切り出したトークンをトークン列に加える
token_array.push_back(token);
break;
...
default:
break;
}
}
Annotator annotator;
for (トークン列を端から端までなめる) {
// トークンに解析情報を付加する
annotator.annotateInformation(token);
}
前項で説明したように、
本項からは、
「デリミタ」の判断がすべて
字句解析器の目的は文字列から文字列を切り出すことです。文字列を切り出すためには、
上記を踏まえ字句解析器が内部で行う処理を整理すると、
- ソースコードを1文字ずつ読み進めながら、
その文字があるトークンの開始デリミタかどうかを判断し、 開始デリミタだと判断できた場合には、 スキャナに開始デリミタとカーソル位置を渡して、 終了デリミタまで読み進めてもらう - 読み進める際には、
文字をすべてバッファに貯めておき、 終了デリミタに到着した際にトークンに切り出す
となります。これをそのままコードにすることで、
状態をできる限り持たない
「必要以上に状態を持たないようにする」
しかし、
my $a =<<HERE_DOCUMENT . $ext_string;
… document …
HERE_DOCUMENT
上記のヒアドキュメントを処理する場合、<<HERE_
を解析した際に、
このようなやむを得ない場合を除き、
先読みをしない
ある文字がデリミタかどうか判断するために、
# 「/」は正規表現の開始デリミタ
my $a = split / 1; $a++; $a =~ /,1/i; …①
# 「/」は除算演算子
my $a = $b / 1; $a++; $a =~ /,1/i; …②
両者の違いはsplit
関数か$b
というスカラ変数かの違いだけですが、
先読みでは判断できないケースがある
字句解析器で先頭から文字列を探索していき、/
の位置にあり、/
が除算演算子なのか、/
の判定をしようとすると、/
の存在を確認するまで先読みをしなければなりません。①の例ではそれでもうまくいきますが、/
のあとの1が登場した時点で除算演算子と判断するとしましょう。すると今度は①の例が正しく切り出せなくなってしまいます。つまり、
前のトークンを読むことで判断する
以上を踏まえデリミタの判断は、split
関数が第一引数に正規表現を指定できることが自明なので、split
、/
ときた時点で/
を正規表現の開始デリミタと判断できます。同様に②では、$b
というスカラ変数のあとに正規表現を続けて記述できないため、$b
、/
ときた時点で除算演算子だと判断できます。
基本的には上記で示したように、
前のトークンを読んでも判断できないケースがある
しかし、
my $a = func / 1; $a++; $a =~ /,1/i; …③
func
は別packageで定義してあり、func
の次の/
は正規表現の開始デリミタでしょうか、func
のプロトタイプ定義を知る必要があり、
たとえばfunc
の定義が次のようになっていたとしましょう。
sub func() { …… }
重要なのは、func
キーワードのあとのプロトタイプ定義です。ここでは空が指定してあるため、func
は引数をとらないため、/
は正規表現の開始デリミタではなく、
それではfunc
の定義が次のようになっていたらどうでしょうか。
sub func($) { …… }
今度はプロトタイプ定義が($)
となっているため、func
は引数をとるため、/
は正規表現の開始デリミタだと判断できます。
Perlはビルトイン関数やプロトタイプ定義のある関数に関して括弧を省略できる仕様になっており、
PPI、func
の定義によらず、
しかし、
<続きの