これまで、式のインタプリターでは構文解析にtopdown parserを使って解説し た。top down parserは再帰的下方構文解析の代表的な手法であり、次に何が 来るのかを推定しながら構文解析を進めていく方法である。比較的構成がわか りやすく、人手で書いていく場合などには適した方法とされている。
このほかにも構文解析には、 上方構文解析法 (bottom-up parser、上昇型ともいう) という方法がある。この方法は人手で直接実現するには向かない方法で あるが、理論的に構成されており、構文解析のプログラムを自動的に生成する ためには重要な方法になっている。今回は、この下向き構文解析法についてみ ていく。
簡単に説明するためにいつもの式の構文解析を考えてみる。文法は以下のもの を考える。F,T,Eは非終端記号であり、idは変数のようなシンボルを仮定する。
(1) E := E + T (2) E := T (3) T := T * F (4) T := F (5) F := ( E ) (6) F := id
構文解析の重要な役割は、入力がこの文法にあっているかどうかを認識するこ とである。下方構文解析では、まず、Eであることを仮定して解析をはじめ、 それぞれの非終端記号に対応する関数を呼び出し、最終的に必要な終端記号列 になっているかを認識する方法であった。つまり、構文木という観点からみれ ば、構文木の根から葉に向かって解析を進めていく。(ここで、この文法は左 再帰で書いてあるため、そのままではtop-down parserができないことは以前 に説明したとおり)
これに対し、上方構文解析では葉すなわち終端記号から、根すなわち非終端記 号へ向かって文法を適用して、最終的にEになっているかを認識する。例えば, a+c*dを考えてみる。これをtokenの列にしてみると、
さらに、(6)の文法を適用して、
id + id*id
(3)(4)の文法を適用して
F+F*F
さらに、(1)(2) を適用して
F+T*F ⇒ T + T*F ⇒ T + T
となって、認識される。 この適用して、非終端記号に置き換えていくことを 還元(reduction) と呼ぶ。 上方構文解析で、構文木を構成する過程は、文字列を 非終端記号に還元していく過程である。この例では、順番を考えずにできると ころから還元していったが、これをするためには入力を全部みてからやること になるため、あまり現実的ではない。
E + T ⇒ E
上向き構文解析では、入力を左から右に見ながら(つまり、一文字づつ入力し ながら)還元していく。入力の右側から適用できる構文規則を逆順にたどって 最終的に最後の構文規則まで還元できる部分列をhandle(把手)という。上方構 文解析はhandleを見つけて還元する過程とみなすことができる。
右文形式 handle 還元につかった規則 a + b * c a (6) (4) (2) E + b * c b (6) (4) E + T * c c (6) E + T * F T*F (3) E + T E+T (1) E --- ---
このような構文解析を(自動的に)構成するために、現在の構文解析の状態 を記憶するためのスタックと入力の動作として以下のものを考える。
スタックの状態 入力 動作 $
$a
$E
$E +
$E + b
$E + T
$E + T*
$E + T*c
$E + T*F
$E + T
$Ea + b * c $
+ b * c $
+ b * c $
b * c $
* c $
* c $
c $
$
$
$
$shift
(6)(4)(2)によるreduce
shift
shift
(6)(4)によるreduce
shift
shift
(6)によるreduce
(3)によるreduce
(1)によるreduce
accept
さて、上方構文解析のためのアルゴリズムについて、考えていくことにしよう。 演算子順位構文解析法は、演算子順位文法(operator precedence grammar)に 対する簡単な上方構文解析法である。演算子文法とは、どの構文生成規則の右 辺も空ではなく、しかも、隣接する2つの非終端記号を持たないという文法で ある。つまり、算術式E := E + Tのように、必ず非終端記号の間には演算子 (終端記号)が入るものである。演算子順位文法とは、終端記号について、優 先度> < =が定義されている文法のことである。
例についていえば括弧( 、 )に関しては=、E:= E+TとT:=T*Fにより、+<*である。 つまり、数式の直感的な優先度に対応している文法とおもえばよい。2,3の 規則は構文規則で、構文木を作ったとき下流にある演算子が強いことを示す。 このような関係にしたがって、構文規則より、演算子順位行列を作ることがで きる。
+ * ( ) id + > < < > < * > > < > < ( < < < = < ) > > > id > > >
これを使って、以下のアルゴリズムをつかえばよい。
演算子文法に関しては比較的簡単なアルゴリズムで構成することができたが、 一般の文法には使えない。ここで説明する方法は入力を左から右へ走査し、最 右の規則を導くので、 LR(left-to-right scanning right most derivation)構文解析法 と呼ばれるものである。
LR構文解析は入力とスタック、構文解析表からなる。入力は1記号ずつ左から 右に読む。スタックには、
という記号列を積む。sは状態に対応した記号である。Xは文法記号で、実際 は必要ないが説明のために加えてある。構文解析表は構文解析動作関数ACTION と行き先関数GOTOの2つの部分からなる。LR構文解析のプログラムは現在のス タックの最上段の状態smと入力記号aiをもちいて、ACTION[sm, ai]を引いて、 以下の動作のどれかをとる。s0 X1 s1 X2 s2 X3 s3.... Xm sm
ここで、siはshiftで状態iをスタックに積む動 作を意味する。また、rjは文法jによるreduce動作を意味する。ここで、 a*b+c$を入力として考えてみよ。
ACTION GOTO
(非終端記号)state id + * ( ) $ E T F 0 s5 s4 1 2 3 1 s6 acc 2 r2 s7 r2 r2 3 r4 r4 r4 r4 4 s5 s4 8 2 3 5 r6 r6 r6 r6 6 s5 s4 9 3 7 s5 s4 10 8 s6 s11 9 r1 s7 r1 r1 10 r3 r3 r3 r3 11 r5 r5 r5 r5
このような表を作ることによって、LR構文解析ができる。この表の作りかたについてはこの講義では説明しないが、重要な点は字句解析のところでオートマトンから字句解析を自動的に生成できると同様に、この表を自動的に作る方法があり、構文解析ルーチンを自動的に生成できることである。
スタック 入力 (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) 0 0 id 5 0 F 3 0 T 2 0 T 2 * 7 0 T 2 * 7 id 5 0 T 2 * 7 id F 10 0 T 2 0 E 1 0 E 1 + 6 0 E 1 + 6 id 5 0 E 1 + 6 F 3 0 E 1 + 6 T 9 0 E 1 id*id+id $ *id+id$ *id+id$ *id+id$ id+id$ +id$ +id$ +id$ +id$ id$ $ $ $ $
LR構文解析ルーチンを自動生成するプログラムの一つがyaccである。実際、 構文解析ルーチンはtop-down parserで書くことがあるが、複雑になると手に 負えなくなるため、yaccのような自動生成プログラムを使う。(linuxのフリー な構文解析は実際bisonというプログラムであるが、yaccというコマンドになっ ている)実際の場面ではyaccの使い方を習得しておくことが重要になる。
yaccは、LR構文解析に一文字の先読み機能を付け加えたLALR(Look-ahead LR)という文法のクラスを扱うことができる。yaccの入力(文法の定義)は、 例として以下のように書く。
tokenで定義されているものは、defineされるので、lex.cのなかでそのまま使 うことができる。lex.cでは、これまででやった字句解析のルーチンが定義す る。構文解析から呼び出される字句解析のルーチンは、yylexという名前で定 義しなくてはならない。これは、lexを使っても生成できる。
%token NUM /* yylexから返すtokenの定義、文字を直接返してもよい*/ %token SYM %token STRING %{ #include/*Cのプログラムのヘッダー、なんでもかける*/ %} %start expression /* yyparseで何の認識をするかの指定*/ %% /* 文法の定義の始まり*/ expression: term | expression '+' term ; term: factor | term '*' factor ; factor: NUM | SYM ; %% /* 文法の定義の終わり*/ #include "lex.c" /* ここからは何のCのプログラムをかいてもいい*/
このプログラムをexpression.yとすると、
で、構文解析プログラムがy.tab.cという名前で生成される。このプログラム には、構文解析ルーチンyyparseが含まれており、、mainプログラムを以下の ようにして、リンクすればよい。% yacc expression.y
yyerrorは構文解析でエラーになったときに呼び出される関数で、ユーザが与える。
main() { yyparse(); printf("ok\n"); } void yyerror() { printf("syntax error!\n"); exit(1); }