このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
文法の与え方にはさまざまな方法がありますが、ここではバッカス・ナ ウア記法を取り上げます。 バッカス・ナウア記法では生成する文字列を作るアルファベットを終端 記号と言います。終端記号の集合を で表します。 一方、それに属さない記号を非終端記号と言います。 非終端記号の集合を で表します。 ルールは左辺に一つの非終端記号、右辺は非終端記号と終端記号の列で表しま す。通常は左辺と右辺は ::= や → という記号で結びます。 そして最後に一つ非終端記号を開始記号として指定します。 ルールの集合を P 、開始記号を S としたとき、文法の定義を と表します。
このように定義した文法において、つぎのような手順により記号列を生成しま す。
簡単のため、左辺が同じルールは、右辺を |(縦棒) で区切って列挙すること で一つにまとめることにします。
以下に簡単な文法の例を示します。
このようなルールから、 This is a pen という記号列が導けます。
バッカス・ナウア記法において、指定できるルールを「非終端記号→終端 記号, 非終端記号 → 終端記号 非終端記号」だけに制限した 文法を正規文法といいます。
整数をこのルールにより表現しなさい。
一般のバッカス・ナウア記法による記号列の生成では、一つの非終端記号が複数の 非終端、終端記号に対応します。そして、それに含まれる非終端記号がまた複 数の非終端、終端記号に対応します。 この導出の流れを図にすると、一つの非終端記号が複数の非終端、終端記号に 対応するので、木の構造になります。
根は開始記号になり、葉は終端記号、葉以外の頂点は非終端記号になります。 与えられた記号列に対して、それの生成手順に対応した木を構文解析木 と言います。 例えば、上の This is a pen という記号列は次のような構文解析木を持ちま す。
一つの記号列について、複数の構文解析木を持つ文法をあいまい であると言います。あいまいな文法はコンピュータで機械的に処理することが できません。 因みに英語はあいまいです。 次の文章の解釈はなんと 4 通りあります。
これは、動詞として Time, flies, like の三通りが考えられ、しかも Time が動詞の時は、 like an arrow の前が名詞になるので副詞句の他に形容詞句 になる可能性があるからです。
動詞 | like an arrow | 訳 | 構文解析木 |
---|---|---|---|
Time | 副詞句 | 矢のように蝿の所要時間を計りなさい | |
Time | 形容詞句 | 矢に似ている蝿の所要時間を計りなさい | |
flies | 副詞句 | 時間は矢のように飛ぶ | |
like | 副詞句 | 時間蝿は一本の矢が好きである |
英語の場合、前後の文章や蝿、矢などに関する知識などを利用してふさわしい 訳を選ぶ必要があります。
自然言語の構文解析はとても難しいです。 次の日本語の構文解析木を書きなさい。
次の日本語を、前後関係などで構文を意識して、英訳しなさい
これは、料理屋で注文を確認された時に、発言した言葉です。
これは孫の誕生を話題にしている人同士の会話です。
正規文法では取り扱えなかった括弧も、このバッカスナウア記法では比較的簡 単に扱えます。 単純な括弧のみの言語は次のように書けます。
S→ε
S→SS
S→(S)
上記の文法にさらに [](角括弧)も使えるようにしなさい
足し算だけの数式を解釈する文法を考えます。 最初に次の文法を考えます。
=({和},{数, +}, P0, 和)
この文法では、 1+1 のような式は解釈できますが、 1+2+3 は駄目です。そこ で、足し算がいくつつながっていても解釈可能な文法を考えます。 数式は、一般に左から右へ解釈します。したがって、 1+2+3 は 1+2 を解釈し た後、その和に対して +3 を加えたものを新しい和とします。 つまり、次のようなルールが必要になります。
このルールがあれば、一番左の項以外はこれで解釈できます。 一番左の項はそれだけで和とみなせば 1+2+3 を解釈できます。
=({和},{数, +}, P1, 和)
なお、次のようにしてしまうと和の優先順序を指定できず、あいまいになりま す。
=({和},{数, +}, P1x, 和)
バッカス・ナウア記法を与えて、それを処理するプログラムを出力する処理系 をコンパイラコンパイラと言います。 C 言語のプログラムを出力するものに yacc があります(bison と いう互換性のあるフリーソフトもあります)。 yacc は、数を取り出すなど構文解析の前段階である字句解析はし ません。そのために lex という字句解析ソフトを使います(flex という互換性のあるフリーソフトもあります)。 一方、 java 用のコンパイラコンパイラに JavaCCというクラスラ イブラリがあります。 JavaCC は構文解析も字句解析もできます。また、構文解析木を出力する JJTree というツールも付属してきます。
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
括弧を加えるにはどうしたら良いか考えなさい。