これまでの3回で、Lisp のインタプリタを作成した。今回からは C 言語のごく小さなサブセット
Tiny C のコンパイラを作っていく。
最初の作業は構文解析である。 字句解析部が返してくる token を、個々の式や文ごとにグループ化し、木構造に変換する。
BNF で定義した文法は次のようなものである。
program : statement | program statement statement : VARIABLE '=' expression ';' | PRINT VARIABLE ';' expression : NUMBER | VARIABLE | '(' expression ')' | expression '+' expression一番上の規則は、program は statement か、または program の後に statement が続いたである、と読む。大文字で書いた名前や引用符で囲った文字は token である。小文字で書いた名前は非終端記号(non-terminal symbol)と呼ぶ。
この文法に合うプログラムは例えば次のようになる。
a = 31; b = a + 3; print b;yacc が自動合成する構文解析のプログラムは、与えられたプログラム(token 列)に文法規則をどのように当てはめていくと、program に行き着くかを計算する。例えば、上のプログラムの場合、字句解析後は、
VARIABLE = NUMBER ; VARIABLE = VARIABLE + NUMBER ; PRINT VARIABLE ;なので、
VARIABLE = expression ; VARIABLE = expression + expression ; statementであり、
statement VARIABLE = expression ; statementとなり、
program statement statementとなり、
program statementとなり、
programと最後に program に行き着く。
もし規則をうまく当てはめて最後に program に行き着くなら、yacc が合成した関数
yyparse() は 1 を返し、行き着かないなら 0 を返す。つまり yyparse() は入力プログラムを検査して、文法エラーがあるかないかを判断する関数なのである。
文脈依存文法は、次のような形の生成規則を含む文法である。
文脈自由文法である文法は、文脈依存文法でもある。これは定義から明らかである。 しかし文脈依存文法である文法が、文脈自由文法であるとは限らない。したがって、文脈依存文法は、文脈自由文法より強力である。
正規文法とは、正規表現で記述できる文法のクラスである。正規表現の記法は、すべて BNF に書き換えることができる。
例えば次のような文脈自由文法は正規文法でもある。
expression : NUMBER | VARIABLE | expression '+' expression | '(' expression ')'最後の規則は無限の入れ子構造を作るので、正規表現で扱えない。仮にこの文法を正規表現でも表わせたとしよう。すると対応する有限状態オートマトンがあるはずだが、この文法は無限の入れ子構造を作るので、途中の状態(括弧のネストが何段になっているか)は無限個なければならず、矛盾するので、この文法は正規文法ではない。
例えば最初に定義した文法にそって、
まず最初の token は VARIABLE である。これは規則
次の token も '=' であるから、さらに遷移をおこなえる。ここで、もし '=' 以外の token が来たら、それ以上遷移をおこなえなくなるので、エラーとなる。
さて次の token は VARIABLE である。ここで、現在は次に expression が来るのを待っている状態なので、該当する規則を探すと、
さて expression を見つけたのだから、途中で止めていた最初の規則についての遷移を再開するのだろうか。次の token は '+' である。残念ながら expression の次は ';' であるはずなので、この遷移はおこなえない。代わりに別な規則について遷移をおこなえる。
LR 構文解析で、還元は token と非終端記号の列が、ある規則に一致していることを発見して、全体をより上位の一個の非終端記号に置き換えたことを意味する。これを繰り返してゆき、最後に最上位の非終端記号
program になれば、文法エラーなしで構文解析が終了したことになり、そうでなければ文法エラーで解析失敗ということになる。
shift/reduce conflict をおこす文法の例として有名なのは if 文である。
statement : IF '(' expression ')' statement | IF '(' expression ')' statement ELSE statementこれは次の場合にあいまいになる。
if (a > 0) if (b > 0) c = true; else c = false;else を読んだとき、この token は内側の if 文の一部であると考え、遷移すればよいのだろうか、それとも、内側の if 文は完了したと考え、還元して、読みこんだ else は外側の if 文の一部であるとして遷移すればよいのだろうか?
一般に yacc は、shift/reduce conflict がおきたときには、例外条件として、遷移(shift)を優先させる。したがって上の else は内側の if 文の一部と解釈される。この解釈は、C 言語を始めほとんどの言語の仕様と一致するので、一般に if 文にまつわる shift/reduce conflict はそのままにしておいて問題ない。
さて文法によっては、遷移と還元のどちらを優先してもかまわないこともある。
expression : NUMBER | Identifier | expression '+' expressionこれは 1 + 2 + 3 のような式の場合、'+' を読んだ際に、1 + 2 を先に expression に還元するか、それとも遷移して、2 + 3 を最終的に expression として還元するか、2つの選択肢がある。意味的には (1+ 2) + 3 か 1 + (2 + 3) かの違いになるが、どちらでもかまわない。
もし文法のあいまいさを取り除きたいのなら、次のように文法を書きかえればよい。
expression : term | expression '+' term term : NUMBER | Identifier一方、遷移を優先しては困ることもある。
expression : NUMBER | expression '+' expression | expression '*' expressionこれは 1 * 2 + 3 のような式の場合、'+' を読んだ際に、1 * 2 を先に expression に還元するか、それとも遷移して、2 + 3 を最終的に expression として還元するか、2つの選択肢がある。演算子の優先順位としては、還元すべきだが、yacc は遷移を優先させてしまう。
これを回避するには、+ と * の間の演算子の優先順位を明示的に指定するか、あるいは次のように文法を書きかえればよい。
expression : term | expression '+' term term : NUMBER | term '*' NUMBER
%{ 任意の C 言語プログラム。#include など。 %} %token%token というのは文法中にでてくる token を宣言するためのものである。このプログラム sample.y を... %% BNF による文法定義 %% 任意の C 言語プログラム。main() など。
y.tab.c の中には、sample.y の最後のパートで定義された main() 関数などの他、yacc
が生成する yyparse() という関数も含まれる。この関数は呼ばれると、yylex()
を使って token をひとつづつ読みながら、構文解析をおこなう。そして解析が文法エラーなく終了すると、0
(false) を、そうでなければ 1 (true) を返す。
さらに
% yacc -dv grammar.y % lex token.l % gcc y.tab.c lex.yy.c -llとコンパイルし、
% ./a.out int foo(int x, int y) { return x + y - 3; } ^Dなどとプログラムを入力し、正しいプログラムならエラーがでず、間違ったプログラムなら Syntax error と表示されることを確認せよ。(なお最後の ^D は入力の終了をあらわす制御文字である。)
注: yacc が使えない場合は bison コマンドを試してみよ。
さてgrammar.yはいくつかの shift/reduce
conflict を発生させる。
文法を書き直して、shift/reduce conflict がひとつだけになるようにせよ。
残ったひとつは if 文によるものである。
Copyright (C) 1999-2000 Shigeru Chiba
Email: chiba@is.tsukuba.ac.jp