] > Tree(3)

第 10 回 木(3)

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

10-1. 構文解析

素朴な解析法

バッカス・ナウア記法の素朴な解析法として次のようなものがあります。 例えば、 A→bCD で A,C,D が非終端記号、 b が終端記号だった時、関数 A を次のように作ります。


void A(){
   記号 b を読む;
   C();
   D();
}

そして非終端記号 C, D に対しても同様に関数を作ります。 このようにすると構文木通りに関数が呼び出されて、与えられた記号列を処理 できる場合があります。 しかし、実際は一つの非終端記号から複数の導出規則が存在します。 例えば、下の例では S から A か B のどちらを導出するかはすぐには決めら れません。

S→A|B
A→ab
B→ac

素朴な方法としては、適当に導出規則を選び、途中で失敗したら別の導出規則 を選ぶ方法が考えられます。これをバックトラックと言います。 確かにこの方法でも構文解析できますが、しかし、効率が悪いです。

ここで、次に読み込む記号から if 文でどちらの導出規則を適用するかが決め られたら、効率的に解析が可能になります。 どんな非終端記号に対しても、次の先頭の文字を読むだけで一意に導出が可能 な文法を LL(1)文法 と言います。

10-2. 数式の文法

足し算の文法

足し算だけの数式を解釈する文法を考えます。 最初に次の文法を考えます。

文法 G0

=({和},{数, +}, P0, 和)

+

この文法では、 1+1 のような式は解釈できますが、 1+2+3 は駄目です。そこ で、足し算がいくつつながっていても解釈可能な文法を考えます。 数式は、一般に左から右へ解釈します。したがって、 1+2+3 は 1+2 を解釈し た後、その和に対して +3 を加えたものを新しい和とします。 つまり、次のようなルールが必要になります。

+

このルールがあれば、一番左の項以外はこれで解釈できます。 一番左の項はそれだけで和とみなせば 1+2+3 を解釈できます。

文法 G1

=({和},{数, +}, P1, 和)

+
和の構文解析木1

なお、次のようにしてしまうと和の優先順序を指定できず、あいまいになりま す。

文法 G1x

=({和},{数, +}, P1x, 和)

+
あいまいな文法

拡張バッカス・ナウア記法

拡張バッカス・ナウア記法は、バッカス・ナウア記法に中括弧による繰返しの 記法を付け加えたものです。 また、丸括弧の中に | を使うことで、括弧内を選択できます。 例えば、拡張バッカス・ナウア記法を使うと、上の足し算は「+ 数」を任意の 回数繰返したものとして次のように書くことができます。

{+}

左再帰性の除去

上の + というルールを入力列に適応するには、その先頭部分が「和」の形になってい るかどうかを調べる必要があります。そして、そのためにはその先頭部分が 「和」の形になっているかどうかを調べる必要があります。 このように左辺と同じ非終端記号が右辺の先頭に来ていると、入力列を順に読 む構文解析ができなくなります。 これを左再帰性と言います。 この左再帰性は次のようにすれば除去できます。 次のような左再帰性を持つ生成規則があったとします。

AAα|β

A は非終端記号で、α と β は非終端記号、終端記号からなる列を 表し、β は A で始まらないとします。 この時、次のように書き直すと左再帰性がなくなります。

AβA' A' αA' | ε

上の足し算を解釈する文法G1の左再帰性を除去すると次のように なります。

文法 G2

=({和, 和'},{数, +}, P2, 和)

' '+ '|ε

再帰的下向き構文解析法

文法に対して、左辺から右辺への導出が、次の一文字を読むだけで決定できれ ば、その文法をLL(1) 文法と言います。 次の一文字を読むだけで決定できるかどうかは次の計算を行うことで判定でき ます。なお次の一文字を読んでプログラムの次の動きを決めますので、入力文 字が終ったことを示す特殊な文字┤を考えることがあります。

α は非終端記号、終端記号からなる列とします。 また A は非終端記号とします。 この時 First(α), Follow(A), Director(A,α) を次のように定義 します。

First(α):
α から導出可能な列の先頭になりうる終端記号の集合。 もし、α から ε へ導出可能なら、ε も含めます。
Follow(A):
開始記号から導出をした時、 A の直後になりうる終端記号の集合。
Director(A,α):
First(α) 但し、 α から ε へ導出可能なら Follow(A) も含めます。

文法 G2についてこれらを計算すると次のようになります。 なお、数式の最後として = があることとし、Director は生成規則に関するも のだけ求めます。

First' = + = First = Follow = = Follow' = = Director ' = Director ' +' = + Director ' ε = =

このようにすべての非終端記号に対して、その生成規則を示す Director 同士 に共通部分がない文法を LL(1) 文法と言います。 G2 は 計算した Director に共通部分がないので LL(1) 文法です。

LL(1) 文法は、先に示した素朴な関数呼出による構文解析に対して、次に来る ものを判断して構文規則を変えることで構文解析ができるようになります。 上の G2 は計算した Director により次のように計算できます(わ かりやすいように日本語を使ってますので、このままでは動きません)。 なお、構文解析のことをparse、構文解析をするプログラムなどをparserといいます。


void 和(){
   数を読み込む;
   次を読む;
   和'();
}
void 和'(){
   if(次=='='){ /* Director(和',ε) */
     return;
   }else if(次=='+'){ /* Director(和',+数和') */
     +を読み込む;
     次を読む;
     数を読み込む;
     次を読む;
     和'();
   }
}  
main(){
  次を読む;
  和();
}

構文解析の意味付け

構文解析の手続きにおいて、出力を考えます。 これは構文に関する意味を与えることになります。

ここでは、数式から構文解析木を作ることを考えます。 始めに G1 を考えます。 各ルールで、次のように木が作られます。

ルール 構文解析木
+ ルール1の導出木
ルール2の導出木

和の部分を考えると、導出により木の下の方に移動していくので、上から下へ 木を作っていく 一方 G2 は 次のように木が作られます。

ルール 構文解析木
' ルール1の導出木
' + ' ルール2の導出木
'ε そのまま

この場合、下から上へ木を作ることになります。 これを上に示したプログラムを利用して、次のように生成させるます。


typedef struct tr {
  char *item ;
  struct tr *left,*right;
} TREE;
TREE *root=NULL;
void 和(){
   数を読み込む;
   root=(TREE *)malloc(sizeof(TREE));
   root->item=数;
   root->left=NULL;
   root->right=NULL;
   次を読む;
   和'();
}
void 和'(){
   TREE *op,*num;
   if(次=='='){ /* Director(和',ε) */
     return;
   }else if(次=='+'){ /* Director(和',+数和') */
     +を読み込む;
     op=(TREE *)malloc(sizeof(TREE));
     op->item="+";
     op->left=root;
     次を読む;
     数を読み込む;
     num=(TREE *)malloc(sizeof(TREE));
     num->item=数;
     num->left=NULL;
     num->right=NULL;
     op->right=num;
     root=op;
     次を読む;
     和'();
   }
}  
main(){
  次を読む;
  和();
}

10-3. 木の探索と表示

木の内部を検索、表示する際に、順序を複数決めることができます。 以前は「木の左側を処理」、「注目頂点を表示」、「木の右側を処理」 と、もっとも深い頂点から順に表示される表示法を紹介しました。 これを 深さ優先探索 と言います。 一方、同じ深さごとに探索、表示することもあります。これを 幅優先探 索と言います。

また、深さ優先探索に関しても「木の左側を処理」、「注目頂点を表示」、 「木の右側を処理」の他にも、これらの出力の順番を変える方法が考えられま す。 ここで、例えば、次のような構文解析木が得られたとします。

構文解析木

その時「木の左側を処理」、「注目頂点を表示」、 「木の右側を処理」とす れば中置記法の表示と一致します。一方 「木の左側を処理」、 「木の右側を処理」、「注目頂点を表示」とすると後 置記法(逆ポーランド記法)と一致します。

演習10-1

以上の議論により、数式(足し算のみ)を逆ポーランド記法に変換するプログラ ムを作りなさい。

完全な数式処理

ここまでは足し算のみの数式を使って、文法、構文解析、構文解析による計算 の手法を紹介しました。 ここでは、完全な数式の文法を与えます。

まず、かけ算を考えます。かけ算だけの式の場合は計算の順序などは足し算と 同様です。したがって、かけ算だけのルールを考えると次のようになります。

*

ここで、積と和の優先順位を考えます。 積の方が優先されますので、積を計算した後、和を計算します。 これを実現させるには、和の計算で「数」という終端記号を計算の対象にしま したが、積を計算の対象に変更します。 変更した後のルールは次のようになります。

+ *

さらに括弧の処理を考えます。 括弧の中にはどんな式も入ります。一方括弧はその中の値を計算したら、計算 の対象となる値として数と同じように扱われます。 今まで「数」という終端記号を使ってきましたが、ここで「値」 という非終端記号を導入します。 すると、値に関するルールは次のようになります。

()

これらをすべてまとめると完全な数式処理の文法 G3を定義できま す。

文法 G3

=({和, 積, 値},{数, +, *, (, )}, P3, 和)

+ * ()

演習10-2

次の数式の構文解析木を書きなさい。

  1. 1 + 2 + 3
  2. 1 * 2 + 3
  3. 1 + 2 * 3
  4. (1 + 2) * 3
  5. 1 * (2 + 3)

演習10-3

G3 を拡張バッカス・ナウア記法で書きなさい

演習10-4

G3 の左再帰性を取り除いた文法 G4 を求めなさい。

演習10-5

G4 の各導出規則に対して Director を求め、 LL(1)文法であるこ とを確かめなさい。

演習10-6

中置記法で書かれた数式を逆ポーランド記法に変換するプログラムを書きなさ い。

演習10-7

数式を計算するプログラムを書きなさい。

10-4. コンパイラコンパイラ

バッカス・ナウア記法を与えて、それを処理するプログラムを出力する処理系 をコンパイラコンパイラと言います。 C 言語のプログラムを出力するものに yacc があります(bison と いう互換性のあるフリーソフトもあります)。 yacc は、数を取り出すなど構文解析の前段階である字句解析はし ません。そのために lex という字句解析ソフトを使います(flex という互換性のあるフリーソフトもあります)。 一方、 java 用のコンパイラコンパイラに JavaCCというクラスラ イブラリがあります。 JavaCC は構文解析も字句解析もできます。また、構文解析木を出力する JJTree というツールも付属してきます。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科