字句解析の基礎:正規表現によるパターンマッチ
字句解析とは、文字列として入力されるプログラムをtokenの列に分解するフェーズである。前回のプログラムにおいて、字句解析を行う関数getTokenは、文字列を数字(NUM)、演算子(PLUS_OP,MINUS_OP)などのtokenに分けて、返す関数である。
どのような文字列がどのようなtokenになるかについては、正規表現(regular expression)で定義することができる。アルファベットA上の正規表現とは、
なお、(S)は、Sと同等であることを意味する。
例えば、正規表現とは、a(b|c)*は、最初にaがあり、bまたはcが続く文字列を表現する。abbcもabcbccも、abccもこの正規表現で表現される文字列である。
正規表現は、図のような規則で非決定性有限オートマトン(NDA: nondeterministic finite automaton)に変換できる。有限オートマトン(finite automaton)とは、有限の内部状態を持ち、与えられてた記号列を読みこみながら状態遷移し、その記号列があるパターンをもつ列であるかを決定するものである。非決定性有限オートマトンとは、入力によらない状態遷移(空列記号に対する状態遷移)をもち、それは非決定的に遷移してもしなくてもよいと状態をもつものである。図にそれぞれの規則に対応するオートマトンを図示する。
先にあげたa(b|c)*について、この規則で、NDAに変換した結果も示す。
NDA
では、空の状態遷移に対して、状態遷移するかしないかの両方の可能性をしらべなくてはならないので、実際にそのまま実装すると効率がわるい。そのため、非決定的な遷移を取り除き、決定性有限オートマトン(DFA: deterministic finite automaton)に変換する。DFAとは、以下の条件を満たす有限オートマトンである。(1)e による遷移がない。
(2)一つの状態から同じ記号による異なった状態への遷移はない。
変換には次の規則を適用し、上の条件にあったオートマトンに変換する。
このアルゴリズムで得られる
FDAは必ずしも、最小のオートマトンとはならない。最小にするには、同じ遷移が2つあった場合には、冗長なので1つにまとめることができ、これを繰り返すことにより、最小化ができる。自動字句解析生成プログラム:
lex正規表現が与えられた時に、その言語(文字のパターン)を認識する
DFAを機械的に作り出すことができる。そのアルゴリズムをプログラムにしたものが字句解析器生成系( lexical analyzer generator)である。このプログラムとして、有名なのが、lexである。lex
では、定義ファイルは以下の形式でかく。%{
任意の
Cプログラム。定数やCのマクロの宣言をここにかく。%}
下の定義で使う
lexのマクロの定義%%
正規表現による入力パターンの定義
正規表現パターン アクション という形で書く
%%
任意の
Cプログラム例えば、
a(b|c)*の正規表現を認識するじく解析は、%{
%}
%%
a(b|c)* { printf("OK\n"); } /*
このパターンを入力したらOKを出力する*/.
{printf("NG\n");} /*.は以上以外のパターンの場合のアクションを指定 */%%
/* なにもなし */
でよい。これをコンパイルするには、
% lex test.l
% lex lex.yy.c -ll
とする。これでできた
a.outを実行すると、abcにたいしてはOK, xxxにはNGと出力される。Linuxのflexのときには、-llの代わりに、-lflをつかうこと。前回示した数式の字句解析の部分は以下のように書ける。
%{
#include "expr.h"
%}
digit [0-9] /*
マクロの定義,digitを0-9の数字の集合と定義する */%%
{digit}+ return NUM; /*0-9
の1回以上の繰り返しは、NUMと認識する */"+" return PLUS_OP; /*+
はPLUS_OP +は繰り返しの意味なので、""で囲む*/"-" return MINUS_OP, /* -
はMINUS_OP */[ \t]
/* 空白は無視 */. printf("error?\n"); /* error */
%%
main(){
yystdin = stdout;
....
r = yylex(); /* token
の列はyytextに入る */printf(" token is %d,'%s'\n",r,yytext); }
lex
は、字句解析ルーチンとして、yylexを生成する。このルーチンは、actionで指定されたreturn分で返された値を返す。lex
の使い方については、% man lex
として、マニュアルを参照のこと。
数式の構文解析:
top-down parserの作り方前回、簡単な数式の処理系を解説した。構文は、以下のような
BNFによる構文規則で記述されることを述べた。足し算の式
:= 式 +の演算子 式引き算の式
:= 式 −の演算子 式式
:= 数字 | 足し算の式 | 引き算の式ここで、構文の最終的な要素に現れるものを終端記号
(terminal symbol)、それ以外のほかの構文規則によって定義される記号を非終端記号(non-terminal symbol)と呼ぶ。ここでは、非終端記号を<>で囲んで表すことにする。構文規則は、(...)
は、構文規則のまとまりを示す。e1|e2|e3は、((e1|e2)|e3)を、e1 e2 e3 は ((e1 e2) e3)と同じである。正規表現と似ていることを注意しよう。ここでは、構文規則の左辺が、一つの終端記号
<T>だけという文法を考える。これはすなわち、どのような場合でも<T> := eの規則を使って、右辺に置き換えることができることを意味し、このような文法を文脈自由文法とよぶ。この制限を取り払って、例えば、e1 <T> e2 := ...
というような、
e1 e2の間に構文要素<T>が現れたときだけ、置き換えることができるというような文法が考えられるが、このような文法を文脈依存文法と呼ぶ。構文規則に対し、構文解析を作る方法を紹介しよう。例えば、
という構文規則があったとすれば、
<A>のための構文解析関数readAは以下のように作ることができる。readA() {
a
を読み込む;readB(); /* B
を読み込むための関数を呼ぶ*/b
を読み込む;}
これは、これから読み込むものの形を先に仮定してしまってから、それに合致するかを調べていく構文解析法である。このような構文解析法を再帰的下向き構文解析
(recursive decent parsing)あるいはtop-down parserと呼ぶ。前回で解説した数式の構文解析もこの方法によるものである。さて、前回の数式を再度考え、括弧や乗算をいれて考えてみることにする。構文規則を再度定義してみると、
<expression> := <expression> <expr_op> <term>
<term> := <term> <term_op> <factor>
<factor> := number | '(' <expression> ')'
<expr_op> := '+' | '-'
<term_op> := '*'
ここの定義で、加減算の優先順位を考慮して、生成される構文木を反映して構文規則が作られていることに注目。
しかし、これを上の方法で書くと
readExpr();
readExprOp();
readTerm();
}
となってしまって、
readExprが再帰的に呼ばれて,うまく行かない。この問題を、top-down parserの 左再帰性の問題と呼ばれている。すなわち、最終的に<T> := <T> e
となる、文法規則ではうまくいかないのである。このために、前回のプログラムでは、
readTerm();
while(readExprOp() is OK)
readTerm();
}
とした。これは、
<expresssion> := <term> <expression1>
<expression1> := <expr_op> <term> <expression1> | e
と書き換えたのと同等である。e は空を示す。
このほかに、
<T> := b ( c| e )
はうまく行かない。
(c|e )は、cを読むか、それともなにもしないかという意味であるが、この場合は、<T>の次に何がくるかによって、cを読むかどうかが決まるので、top-down parser では、処理ができないことになってしまう。
演習課題
2:(1)と(2)のどちらかのプログラムを書きなさい。
浮動少数点
少数点数
:= (e | 数字列) . 数字列 | 数字列 .指数部 := E (e | 符号) 数字列
数字列
:= 数字 | 数字列 数字符号
:= - | +なお、数字は
0から9までの数字、浮動少数点数の符号は考えない。