第 11 回 構文解析木

本日の内容


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

11-1. 構文解析

バッカス・ナウア記法

文法の与え方にはさまざまな方法がありますが、ここではバッカス・ナ ウア記法を取り上げます。 バッカス・ナウア記法では生成する文字列を作るアルファベットを終端 記号と言います。終端記号の集合を VT で表します。 一方、それに属さない記号を非終端記号と言います。 非終端記号の集合を VN で表します。 ルールは左辺に一つの非終端記号、右辺は非終端記号と終端記号の列で表しま す。通常は左辺と右辺は ::= や → という記号で結びます。 そして最後に一つ非終端記号を開始記号として指定します。 ルールの集合を P 、開始記号を S としたとき、文法の定義を G= ( VN, VT, P, S) と表します。

このように定義した文法において、つぎのような手順により記号列を生成しま す。

  1. 開始記号を左辺に含むルールを一つ選ぶ
  2. 開始記号をそのルールの右辺に置き換える
  3. 置き換えたものの中から非終端記号を一つ選ぶ
  4. その非終端記号を左辺に含むルールを一つ選ぶ
  5. その非終端記号を右辺に置き換える
  6. すべてが終端記号なら終了、そうでなければ 3 に戻る

簡単のため、左辺が同じルールは、右辺を |(縦棒) で区切って列挙すること で一つにまとめることにします。

文法の例

以下に簡単な文法の例を示します。

G=({文, 主語, 述語, 補語, 名詞句, 動詞, 代名詞, 冠詞, 名詞}, {This, is, a, pen}, P, 文)

P の定義

このようなルールから、 This is a pen という記号列が導けます。

正規文法

バッカス・ナウア記法において、指定できるルールを「非終端記号→終端 記号, 非終端記号 → 終端記号 非終端記号」だけに制限した 文法を正規文法といいます。

例11-1

a*
S→ε, S→aS
[abc]
S→a,S→b,S→c
(abc|asd)
S→aA, A→bB, B→c, A→sC, C→d
.*(a|b)
(アルファベットが { a, b, c } の時) S→aS, S→bS, S→cS, S→a, S→b

演習11-1

整数をこのルールにより表現しなさい。

構文解析木

一般のバッカス・ナウア記法による記号列の生成では、一つの非終端記号が複数の 非終端、終端記号に対応します。そして、それに含まれる非終端記号がまた複 数の非終端、終端記号に対応します。 この導出の流れを図にすると、一つの非終端記号が複数の非終端、終端記号に 対応するので、木の構造になります。

A→bCD というルールの場合
ルールの図示

根は開始記号になり、葉は終端記号、葉以外の頂点は非終端記号になります。 与えられた記号列に対して、それの生成手順に対応した木を構文解析木 と言います。 例えば、上の This is a pen という記号列は次のような構文解析木を持ちま す。

This is a pen の構文解析木

一つの記号列について、複数の構文解析木を持つ文法をあいまい であると言います。あいまいな文法はコンピュータで機械的に処理することが できません。 因みに英語はあいまいです。 次の文章の解釈はなんと 4 通りあります。

Time flies like an arrow.

これは、動詞として Time, flies, like の三通りが考えられ、しかも Time が動詞の時は、 like an arrow の前が名詞になるので副詞句の他に形容詞句 になる可能性があるからです。

動詞 like an arrow 訳 構文解析木
Time 副詞句 矢のように蝿の所要時間を計りなさい 矢のように蝿の所要時間を計りなさい
Time 形容詞句 矢に似ている蝿の所要時間を計りなさい 矢に似ている蝿の所要時間を計りなさい
flies 副詞句 時間は矢のように飛ぶ 時間は矢のように飛ぶ
like 副詞句 時間蝿は一本の矢が好きである 時間蝿は一本の矢が好きである

英語の場合、前後の文章や蝿、矢などに関する知識などを利用してふさわしい 訳を選ぶ必要があります。

演習11-2

自然言語の構文解析はとても難しいです。 次の日本語の構文解析木を書きなさい。

  1. これはペンです
  2. すもももももももものうち
  3. きしゃのきしゃはきしゃできしゃした

演習11-3

次の日本語を、前後関係などで構文を意識して、英訳しなさい

  1. わたしはうなぎです。

    これは、料理屋で注文を確認された時に、発言した言葉です。

  2. 私の娘は男だった。

    これは孫の誕生を話題にしている人同士の会話です。

11-2. 括弧の処理

正規文法では取り扱えなかった括弧も、このバッカスナウア記法では比較的簡 単に扱えます。 単純な括弧のみの言語は次のように書けます。

例11-2


S→ε
S→SS
S→(S)

演習11-4

上記の文法にさらに [](角括弧)も使えるようにしなさい

11-3. 数式の文法

足し算の文法

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

文法 G0

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

和→数+数

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

和→和+数

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

文法 G1

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

和→和+数 和→数
和の構文解析木1

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

文法 G1x

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

和 →和+和 和→数
あいまいな文法

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

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

yacc

C 言語のソースを出力する yacc は lex と組み合わせて使います。 非終端記号にあたるものだけ yacc で記述し、「数」や「単語」などの トークン は lex で解釈します。

yacc の定義ファイルは %% で区切り前半が定義部で、後半が規則部になりま す。 定義部では lex に解釈させるトークンの名前の定義をします。 一方規則部では、非終端記号を表す名前や、トークンの名前、さらに文字など を利用して文法をバッカスナウア記法で記述します。 但し、矢印の代わりに :(コロン)を用い、左辺が共通の式は右辺を |(縦棒)で 区切って記述します。そして、文法の式の後に lex と同様に {}で括った C 言語のプログラムを書くことができます。 なお、その時、シンボル値という値を扱うことができます。 これは、終端記号、非終端記号などのシンボルに値を割り当てて、規則にマッ チした時に計算ができるようにしたものです。 規則の右辺のシンボルの位置に対応して $1, $2, ... という記号で参照でき ます。そして、この規則の値として $$ という変数に代入できます。 これが別の規則のシンボルに使われる場合、シンボル値が参照されます。 また、終端記号には lex がグローバル変数として返してくる yyvalue という 値が割り当てられます。

上記の G1 を yacc で書くと次のようになります。


%{
#include <stdio.h>
yyerror(char *s){
  fprintf(stderr,"%s\n",s);
}
%}
%token KAZU EOL
%%
start:  
      | start wa EOL        { printf("=%d\n",$2); }
;

wa: wa '+' KAZU { $$ = $1 + $3;}
  | KAZU        { $$ = $1;}
;

%%
int main(void){
    yyparse();
    return 0;
  }

このように定義した yacc の定義ファイルに対して、 lex は次のように定義 します。 まず、lex のみの時は lex の定義ファイルのアクションに printf などを書きまし たが、 yacc と連結する際は認識したら return でトークンの種類を返すよう にします。 また、yacc とトークンの定義やグローバル変数などを共有するため、定義部 に y.tab.h ファイルの include と yyval 変数の外部宣言を行います。


%{
#include "y.tab.h"
extern int yylval;
%}
%%
[0-9]+ { yylval=atoi(yytext); return KAZU;}
[ \t] ;
\n     { return EOL; }
\d     { return 0; }
.      { return yytext[0]; }
%%

なおこれらのファイルに対する Makefile は次のようになります。


TARGET=wa
YACC=bison -y
$(TARGET).exe:	y.tab.o lex.yy.o
	$(CC) -o$@ $^ -lfl

y.tab.c: $(TARGET).y
	$(YACC) -d $<

y.tab.h: $(TARGET).y
	$(YACC) -d $<

lex.yy.c: $(TARGET).l
	$(LEX) $<

y.tab.o: y.tab.c y.tab.h
lex.yy.o: lex.yy.c

演習11-5

括弧を加えるにはどうしたら良いか考えなさい。


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