2024 年 1 巻 2 号 p. 61-81
私たちの世界を動かすプログラムは事実上すべて,人間がPythonやJava,Cなどの高級プログラミング言語で記述し,それをコンパイルして低レベルのコードにまとめて実行する.現代のプログラミング言語のためのコンパイルを行う技術,すなわち,コンパイラの多くは,Alfred V. AhoとJeffrey D. Ullmanの貢献が大きく,それにより2020年度に,コンピュータ科学の最高峰のチューリング賞を受賞した [1].コンパイラでは字句解析,構文解析,コード生成などのための技術やアルゴリズムが重要である. 本解説記事では,コンパイラの構造及び,コンパイラで最重要で最難関な構文解析,特にLR構文解析 [7] について, 類書にない構文解析手法の関係を明確化して, 有効な事例を新規に追加して, 解説を行う.これにより,現在と未来の社会を支えるコンピュータ・ソフトウェアの理解を深めることができる.さらに重要なことは, 構文解析の理論, 技術, 実装は他の多くのソフトウェアの原理となっており, 今後のソフトウェアの作成に大いに有益である. なお,本解説記事はドラゴンブック [2] や優れた教科書 [3,4,5,6] などを参考にしている.
All programs that run our world are written by humans in high-level programming languages such as Python, Java, and C, which are then compiled into low-level code and executed. Much of the technology for compiling for modern programming languages, i.e., compilers, is due to the contributions of Alfred V. Aho and Jeffrey D. Ullman, which earned them the Turing Prize, the pinnacle of computer science, in FY2020 [1]. Techniques and algorithms for lexical analysis, parsing, and code generation are important for compilers. This article describes the structure of compilers and the most important and most difficult part of compilers, parsing, especially LR parsing [7] , by clarifying the relationship between parsing methods and adding new effective examples not found in other books. This will deepen your understanding of computer software that supports present and future society. More importantly, the theory, techniques, and implementation of parsing are the principles of many other software, and will be of great benefit in the development of future software. This article is based on Dragon Book [2] and excellent textbooks [3,4,5,6].
【総説解説論文】
プログラミング言語のコンパイラの構文解析
Parsing theory of compilers of programming languages
山根 智
Satoshi Yamane1
1下関市立大学
Shimonoseki City University
要旨
私たちの世界を動かすプログラムは事実上すべて,人間がPythonやJava,Cなどの高級プログラミング言語で記述し,それをコンパイルして低レベルのコードにまとめて実行する.現代のプログラミング言語のためのコンパイルを行う技術,すなわち,コンパイラの多くは,Alfred V. AhoとJeffrey D. Ullmanの貢献が大きく,それにより2020年度に,コンピュータ科学の最高峰のチューリング賞を受賞した [1].コンパイラでは字句解析,構文解析,コード生成などのための技術やアルゴリズムが重要である. 本解説記事では,コンパイラの構造及び,コンパイラで最重要で最難関な構文解析,特にLR構文解析 [7] について, 類書にない構文解析手法の関係を明確化して, 有効な事例を新規に追加して, 解説を行う.これにより,現在と未来の社会を支えるコンピュータ・ソフトウェアの理解を深めることができる.さらに重要なことは, 構文解析の理論, 技術, 実装は他の多くのソフトウェアの原理となっており, 今後のソフトウェアの作成に大いに有益である. なお,本解説記事はドラゴンブック [2] や優れた教科書 [3,4,5,6] などを参考にしている.
キーワード: プログラミング言語,コンパイラ,構文解析,LR構文解析
Abstract
All programs that run our world are written by humans in high-level programming languages such as Python, Java, and C, which are then compiled into low-level code and executed. Much of the technology for compiling for modern programming languages, i.e., compilers, is due to the contributions of Alfred V. Aho and Jeffrey D. Ullman, which earned them the Turing Prize, the pinnacle of computer science, in FY2020 [1]. Techniques and algorithms for lexical analysis, parsing, and code generation are important for compilers. This article describes the structure of compilers and the most important and most difficult part of compilers, parsing, especially LR parsing [7] , by clarifying the relationship between parsing methods and adding new effective examples not found in other books. This will deepen your understanding of computer software that supports present and future society. More importantly, the theory, techniques, and implementation of parsing are the principles of many other software, and will be of great benefit in the development of future software. This article is based on Dragon Book [2] and excellent textbooks [3,4,5,6].
Keywords: programming language, compiler, parser, LR parser
1.はじめに
コンピュータ・ソフトウェアは,私たちが関わるほとんどすべての技術に力を与える.携帯電話や自動車で動作するものから,大手ウェブ企業内の巨大サーバーファームで動作するものまで,私たちの世界を動かすプログラムは事実上すべて,人間がPythonやJava,Cなどの高級プログラミング言語で記述し,それをコンパイルして低レベルのコードにまとめて実行する.現代のプログラミング言語のためのコンパイルを行う技術,すなわち,コンパイラの多くは,Alfred V. AhoとUllman D. Jeffreyの貢献が大きく,それにより2020年度に,コンピュータ科学の最高峰のチューリング賞を受賞した [1].
また,Alfred V. AhoとJeffrey D. Ullmanらの共著『Compilers principles, techniques & tools』はコンパイラ技術に関する決定的な本であり,形式言語理論と構文指向翻訳技術をコンパイラ設計プロセスに統合したものである [2].カバーデザインからしばしば「ドラゴンブック」と呼ばれており,高レベルのプログラミング言語をマシンコードに変換する段階を明晰に示し,コンパイラ構築の全プロセスをモジュール化している.この書籍には,字句解析,構文解析,コード生成のための効率的な技術に対して,彼らが行ったアルゴリズム的な貢献も含まれている.ドラゴンブックの最新版は2007年に出版され,コンパイラ設計に関する標準的な教科書として世界で広く読まれており,日本語の翻訳もあり,大学や大学院の講義や研究において有効活用されている.
本解説記事では,コンパイラの構造及び,コンパイラで最重要で最難関な構文解析について, 類書にない構文解析手法の間の関連および新たな事例を盛り込んで解説を行う.これにより,現在と未来の社会を支えるコンピュータ・ソフトウェアのコンパイラの構文解析手法の理解を深めることができる.さらに重要なことは, 構文解析の理論, 技術, 実装は他の多くのソフトウェアの原理となっており, 今後のソフトウェアの作成に大いに有益である. なお,ドラゴンブックを手本に,日本語の優れた教科書も多数あり、その代表的な書籍 [3,4,5,6] を参考文献にあげており,本解説記事の作成にも参考にしている.
2.コンパイラの位置づけと構造
最初に,原始プログラムを読み込んで,機械コードを出力するまでの典型的な流れを図1に示す.以下では,ドラゴンブック [2] に従って簡単に概要を説明する.
まず,コンパイラは高級プログラミング言語で書かれた原始プログラムを読み込んで,必要に応じて前処理を行い,コンパイル可能な原始プログラムを出力する.なお,前処理では,別のファイルに格納している原始プログラムやライブラリなどを取り込んで原始プログラムに展開して,コンパイル可能な原始プログラムを作る.次に,その原始プログラムをコンパイラが読み込んで,ターゲットマシンに対応したアセンブリプログラムに変換する.次に,アセンブリプログラムをアセンブラに入力して,再配置可能な機械コードを出力する.最後に,リンカまたはローダに再配置可能な機械コードを入力して,リンカまたはローダはライブラリファイルや再配置可能目的ファイルを取り込んで,最終的に実行可能な機械コードを出力する.
図1 コンパイラの位置づけ
次に,コンパイラの典型的な内部の構造を図2に示す.以下では,ドラゴンブック [2] に従って簡単に概要を説明する.コンパイラの内部の構造は,大まかには,解析と合成の2つの部分に分かれている.
図2 コンパイラの構造
3.プログラミング言語の文法
構文解析の説明の前に,ドラゴンブック [2] や和文教科書 [5] に従い,プログラミング言語の文法について説明する.
まず,構文解析するためには,プログラミング言語の文法を数学的に定義する必要がある.プログラミング言語は文法的には文脈自由文法であり,以下の4つ組<VN, VT, P, S>で定義する.
(定義)文脈自由文法は4つ組<VN, VT, P, S>である.
ここで, 各記号は以下である:
非終端記号(Non-terminal Variable)の集合 VN
終端記号(Terminal Variable)の集合 VT
生成規則(Production Rule)の集合 P
開始記号(Start Symbole) S∈VN
ただし, VN∩VT={}
また, 生成規則は”A→α”∈Pであり, 左辺は一つの終端記号A∈VN, 右辺はα∈(VN∪VT)*である. ただし, 生成規則の集合PはSを左辺とする生成規則を必ず含む.
なお, 左辺が同じで右辺が異なる2つ以上の生成規則を以下のように書く.
A→α1,…, A→αnはA→α1|…|αnとまとめて書く.
非終端記号VNは変数のようなものであり,終端記号VTはプログラムに出現する記号である.重要なことは,生成規則の集合により文法を定義するものであり,生成規則は左辺の非終端記号から右辺の非終端記号または終端記号の連結された文字列に書き換える規則である.
次に,プログラミング言語に現れる典型的な文法の一部(多くの教科書で出現する文法)を文脈自由文法で記述してみる.
G=<VN, VT, P, E>の各組は以下である.
VN={E}
VT={+, * (, ), i}
P={E→E+E
E→E*E
E→(E)
E→i}
開始記号E∈VN
次に,文法Gからi+i*iに対する構文木を作るが,図3のように2つの構文木が存在する.ここで,⇒は生成規則を1回適用して左辺から右辺が得られることを意味する.
図3 i+i*iに対する2つの構文木
この文法は2つの構文木が生成でき,曖昧性を持つ文法である.掛け算が足し算よりも優先度が高いという演算子の優先順位を考慮すると,①が正しい構文木である.文法の曖昧性を除去するために,演算子の優先順位と結合性を生成規則に埋め込む必要がある.その結果,以下の文法が作れ,この文法がプログラム言語の文法として採用されており,以降の章でもこの文法G’を用いる.
G’=<VN, VT, P, E>の各組は以下である.
VN={E, T, F}
VT={+, * (, ), i}
P={E→E+T
E→T
T→T*F
T→F
F→(E)
F→i}
開始記号E∈VN
4.構文解析の概要
構文解析はプログラミング言語の文法に従って,原始プログラムのトークンの列から構文木を作るものである.構文解析の手法として,代表的なものが2つあり,図4に示すように,根からトップダウンに構文木を作る手法(再帰的下向き構文解析手法)と葉からボトムアップに構文木を作る手法(LR構文解析手法)がある [2].
図4 代表的な2つの構文解析手法
再帰的下向き構文解析手法は最左導出を繰り返して,根から葉に向かって構文木を作る.一方,LR構文解析手法は最右導出の逆,つまり最右還元を繰り返して,葉から根に向かって構文木を作る.2つの構文解析手法ともにトークンの列を左から右へバックトラックなしに一度読み込んで解析して構文木を作る.また,再帰的下向き構文解析器を作るには,生成規則毎に解析関数を手作り,再帰的に解析関数を呼び出して構文木を作る必要がある.一方,LR構文解析器は文法から自動的に構文解析プログラム生成器できて,構文解析可能な文法クラスも広く,実用上から多用されている.
5.再帰的下向き構文解析手法
5.1 基本的考え方
この章では,3章で導入した以下の文法を対象とする.
G’=<VN, VT, P, E>の各組は以下である.
VN={E, T, F}
VT={+, * (, ), i}
P={E→E+T, E→T, T→T*F, T→F, F→(E), F→i}
開始記号E∈VN
まず,再帰的下向き構文解析の考え方に従い,図5に示すように,トークン列i*(i+i)から,最左導出により,木の根から葉に向けゴールまでの構文木を生成する.ここで,図6に示すように,無限ループとバックトラッキングが発生する.G’は左再帰性のある文法(例えば,E→E+T)なので,Eの解析関数はEからE+Tの構文木を作るが,トークンへのポインタが前進しないで無限に呼び出され,無限ループを発生させる.一方,バックトラッキングが発生すると,構文木の作成時間が大きくなったり,プログラムが複雑になるので,実用上から大きな問題となる.
図5 再帰的下向き構文解析手法の流れ
再帰的下向き構文解析では,無限ループを回避するために,左再帰性のある文法を右再帰性の文法に変換して構文解析を行い,左再帰性の文法の構文木を生成する.一方,バックトラッキングを回避するために,トークンを先読みすると,呼び出すべき解析関数がユニークに決まるような文法を構文解析の対象とする.再帰的下向き構文解析の流れを図6に示す.
図6 再帰的下向き構文解析手法の流れ
5.2 左再帰性から右再帰性への変換
G’は左再帰性のある文法(例えば,E→E+T)なので,Eの解析関数はトークンへのポインタが前進しないで無限に呼び出され,無限ループを発生させる.再帰的下向き構文解析では,無限ループを回避するために,左再帰性のある文法を右再帰性の文法に変換する.
例えば,左再帰性を持つ次の単純な生成規則を考える.
A→Aa|b
なお,Aは非終端記号,aとbは終端記号であり,bはA以外の記号とする.Aから最左導出により,ban(n≧0)が導出できる.ここで,新しい非終端記号A’を導入して,右再帰性を持つ次の生成規則を作る.
A→bA’
A’→Aa’|ε
最右導出を用いると,A→Aa|b と同じ終端記号列ban(n≧0)が導出できる.以上より,左再帰性を有する文法の構文解析は右再帰性の文法に変換して構文解析すればよいことがわかる.
左再帰性のある文法G’=<VN, VT, P, E>から左再帰性を除去して,
VN={E, T, F}
VT={+, * (, ), i}
P={E→E+T, E→T, T→T*F, T→F, F→(E), F→i}
開始記号E∈VN
以下の文法G’’ =<VN’, VT, P’, E>が得られる.
VN’={E, E’, T, T’, F}
VT={+, * (, ), i}
P’={E→TE’, E’→+TE’|ε, T→FT’, T’→*FT|ε, F→(E), F→i}
開始記号E∈VN
5.3 Director集合によるバックトラックの回避
再帰的下向き構文解析では,開始記号から最左導出により,生成規則毎に解析関数を実装して,それらを再帰的に呼び出して,構文木を完成させる.このときに,ある非終端記号の生成規則が複数存在するときに,k個のトークンを先読みし,どの生成規則,つまり,どの解析関数を呼び出せばよいかを決定する方法を用いることである.これがLL(k)構文解析法である.ここで,LLとは,Left to right scanning(左から右へトークンストリームをスキャンする)とLeftmost derivation(最左導出)である.以下では,最も簡単で実用上重要なLL(1)を説明する.以降では,ドラゴンブック [2] や和文教科書 [4,5] などに従い,Director集合の計算方法を定義する.
Director集合を計算するためには,まずはFirst集合とFollow集合を計算する必要がある.以降では,一般のプログラミング言語の文法G=<VN, VT, P, S>とその記号列α∈(VN∪VT)*に対して,αから生成される記号列の先頭に現れる終端記号の全体をαのFirst集合と呼び,First(α)と表記する.非終端記号Aに対して,文法Gの文形式においてAの直後に現れる可能性のある$を含む終端記号の全体をAのFollow集合と呼び,Follow(A)と表記する.以下の図7に形式的定義と概念図を示す.ここで,⇒*は生成規則を0回以上適用して,左辺から右辺が得られることを表す.なお,$はトークン列の最後尾の記号であり,開始記号SのFollow集合には$が必ず含まれる.
図7 First集合とFollow集合
以上のFirst集合とFollow集合を用いて,図8でDirector集合を定義する.生成規則A→αに対して,終端記号の集合Director(A,α)を以下のように定義し,A→αのDirector集合は生成規則A→α を使った場合に,入力トークン列の先頭に現れる可能性のある終端記号の集合である.ここで,αからεが生成される場合には,Follow(A)を使う必要がある.なお,First集合やFollow集合,Director集合の計算方法は書籍 [2,3,4,5] に詳しく書いており,紙面の都合上から本解説では省略する.
図8 Director集合
5.4 再帰的下向き構文解析器の実現
Director集合を計算するには,事前に計算したNull関数値やFirst集合,Follow集合が必要である.なお,Null関数値は生成規則の右辺に出現する記号列α∈(VN∪VT)*からεが最左導出されるかを表す関数である.図9に示すように,5.2で左再帰性文法から右再帰性文法へ変換した生成規則の集合Pに対して,事前に計算したNull関数値やFirst集合,Follow集合を用いて,Director集合を計算した結果を示す.
図9 Director集合の計算
このDirector集合を用いて,この文法がLL(1)かどうかを判定する.左辺が同じで右辺のみが異なる,生成規則の部分集合{E’→+TE’,E’→ε},{T’→*FT’, T’→ε},{F→(E), F→i}に対して,
Director(E’,+TE’)∩Director(E’,ε)={}
Director(T’,*FT’)∩Director(T’,ε)={}
Director(F,(E))∩Director(F,i)={}
なので,この文法はLL(1)である.つまり,1つのトークンを先読みすれば,最左導出の対象となる非終端記号の生成規則を決定できる.
次に,再帰的下向き構文解析により,LL(1)文法の入力トークン列w=i*i+i$からの構文木の作成の例を図10に示す.
(1)開始記号Eから下向きに構文木を作成する.
(2)左辺をEとする生成規則はE→TE’のみであり,トークンの先頭はiである.
(3)Director(E,TE’)={(,i)}であり,i∈Director(E,TE’)={(,i)}なので,①の構文木を作成する.
(4)最左導出の対象はTであり,Tを左辺とする生成規則はT→FT’のみである.解析対象のトークンは依然,iである.理由は,まだ先頭のトークンのiが構文木になっていないからである.
(5)i∈Director(T,FT’)={(,i}なので,Tの子供に②の木を追加する.
(6)最左導出の対象はFであり,Fを左辺とする生成規則はF→(E)とF→iの2つある.解析対象のトークンは依然iであり,i∈Director(F,i),Director(F,(E))はiを含まない.よって,F→iを用いて構文木を作り,③ができる.
(7)最左導出の対象はT’であり,解析対象のトークンは*である.T’を左辺とする生成規則はT’→*FT’のみであり,*∈Director(T’,*FT’)なので,④の構文木を作る.
以上を続けて,①,②,…,⑪の順番に構文木を作り,最終的に,図11の構文木ができ,構文解析が終了する.
図10 再帰的下向き構文解析によるLL(1)文法の構文木の作成例
6.LR構文解析手法
6.1 基本的考え方
LR構文解析手法は最右導出の逆,つまり最右還元を繰り返して,葉から根に向かって構文木を作る.再帰的下向き構文解析手法と同様に,LR構文解析手法もトークンの列を左から右へバックトラックなしに一度読み込んで解析して構文木を作る.これらの様子はすでに図5に示している.LR構文解析手法の最大の特徴は文法から自動的に構文解析プログラムが生成できて,構文解析可能な文法クラスも広く,実用上から多用されている.さらに,重要なことは,最右導出の逆,つまり最右還元を用いるために,左再帰性のある文法でも問題なく解析でき,文法をそのまま構文解析できる.なお,LR構文解析の名前は,Left-to-right-scanning(トークンの列を左から右へバックトラックなしに一度読み込んで構文解析を行うことを意味している),Right-most-derivation in reverse(最右導出の逆の順序,つまり最右還元で構文解析を行うことを意味している)の頭文字をとったものである.以降では,ドラゴンブック [2] や和文教科書 [3,4,5,6] に従い,解説する.
6.2 最も基本的なLR(0)構文解析手法
まず,構文解析の途中の状態を考える.たとえば
A→xyz
という生成規則で作られたトークン列を構文解析して構文木を作っている途中で,xを読み終わった状態で,yを読もうとしている状態であるとする.その状態を
A→x・yz
と書く.ここで,yが終端記号であればyを読もうとしている状態であり,yが非終端記号Bであり,
B→x1y1z1
という生成規則があれば,x1を読もうとしている状態
B→・x1y1z1
でもある.以上より,LR(0)項とその集合の閉包を導入する.以降においては,5章で導入した文法G’=<VN, VT, P, E>を用いる.
ここで,VN={E, T, F}
VT={+, * (, ), i}
P={E→E+T, E→T, T→T*F, T→F, F→(E), F→i}
開始記号E∈VN
(定義)文法G’のLR(0)項とは,Pの任意の生成規則に対して,その右辺の左端,右端および記号の間のどれか一箇所に”・”をつけたものである.
例えば,生成規則E→E+Tに対して,以下の4つのLR(0)項が存在する.
E→・E+T E→E・+T E→E+・T E→E+T・
E→・E+Tのように,右辺の先頭にドットがあるLR(0)項を導入項と呼ぶ.また,E→E+T・のように,ドットが末尾にあるLR(0)項を完全項と呼ぶ.
また,先ほどの例で示したように,A→x・yzにおいて,yが非終端記号Bであり,B→x1y1z1という生成規則があれば,x1を読もうとしている状態B→・x1y1z1でもあるので,LR(0)項の集合として{A→x・Bz, B→・x1y1z1}を考える必要がある.ゆえに,LR(0)項の集合Iに対して,Closure(I)を考える.
(定義)文法G’のLR(0)項の集合Iの閉包Closure(I)とは次のように得られる.
1. Closure(I):=I
2. A→α・Bβ∈Closure(I)に対して,B→γという生成規則があればB→・γをClosure(I)
に加える。
3. 上記2.を新たに加わるものがなくなるまで繰り返す.
例えば,I={E→E+・T}のClosure(I)は{ E→E+・T, T→・T*F, T→・F, F→・(E), F→・i}となる.
また,一般に,LR(0)項の集合Iから記号Xによって遷移する先をGOTO(I,X)と書いて,以下に定義する.
(定義)IをLR(0)項の集合,X∈(VN∪VT)=Vとしたとき
GOTO(I,X)=Closure({A→αX・β| A→α・Xβ∈I})
例えば,I={T→T・*F}のとき,GOTO(I,*)は以下になる.
GOTO({T→T・*F},*)
=Closure({T→T*・F})
={ T→T*・F, F→・(E), F→・i}
文法G’=<VN, VT, P, E>において,このようなLR(0)項の集合を求めるには,まず,構文解析の終了判定を容易にするために,新たな非終端記号E’を追加しE’→Eなる生成規則を文法G’の生成規則の集合Pに追加して,以下のアルゴリズムを実行する.
(アルゴリズム)
初期設定 C:={Closure({E’→・E })}
以下を新たな集合がCに付け加えられなくなるまで繰り返す:
任意のI∈CとX∈Vに対し,GOTO(I,X)が空集合でなく,Cに入っていなかったらCに加
える
文法G’に適用すると,ドラゴンブック [2] や和文教科書 [3][5] のオートマトンと同様な図11のオートマトン得られる.このオートマトンを正準オートマトンと呼ぶ.特に,ここではLR(0)を用いているのでLR(0)オートマトンとも呼ぶ.なお,状態の中の完全項は青色,構文解析の終了を意味するE’→E・は赤字で示す.この正準オートマトンを用いて,与えられた入力トークン列の構文解析を進める.
一般に,入力トークン列xが与えられると,最初に・x$から出発し,正準オートマトンを用いて,構文木を作りながら,構文解析中ではu・v$の形になり,時点と呼ぶ.なお,u∈(VN∪VT)*, v∈VT*である.ここで,uは読み込み済みのトークン列から構成した部分的な構文木であり,vはまだ読み込んでいないトークン列である.最終的に,開始記号Sの時点S・$になれば,入力トークン列xの構文木ができて,構文解析は正常に終わる.構文解析中では,正準オートマトンの状態遷移に従い,以下のいずれかを行う.
(1)還元(reduce):正準オートマトンのある状態の中のLR(0)項A→α・を使って,時点uα・v$のαをAに還元して,時点uA・v$に移行する.
(2)シフト(shift):正準オートマトンのaによる状態遷移を使って,入力トークンaを読み込んで,時点u・av$から時点ua・v$に移行する.
図11 正準オートマトン(LR(0)オートマトン)
LR構文解析の動作を説明するために,配置を導入する.まず,前述の時点u・v$,u∈(VN∪VT)*, v∈VT*は,
a1a2……an・xkxk+1……xn$
と書ける.ここで,ai∈VN∪VT (i=1,…,n),xj∈VT (j=k,…,m)である.次に,時点u・v$は正準オートマトンの状態Ii(i=1,…,n)を用いて,配置
(I0a1I1a2I2……anIn・xkxk+1……xn$)
が定義できる.ここで,I0は正準オートマトンの初期状態であり,Ii=GOTO(Ii-1,ai)(i=1,..,n)である.
次に,i+i*i$の構文解析を行い,図12のように配置で示す.
図12 配置によるi+i*i$のLR構文解析の例
ここで,配置(I0EI1+I6TI9,*i$)において,I9={E→E+T・,T→T・*F}であり,E→E+T・による還元とT→T・*Fによるシフトの2つの可能性があり,不都合な状態と呼ぶ。図12では,シフトを優先させて,シフトを行った.この不都合な状態を回避する解決方法として,一文字先読みするSLR(1)やLR(1),LALR(1)がよく知られている.
6.3 SLR(1)構文解析手法
LR構文解析において,次の入力トークンを一つだけ先読みし,Follow集合を使って不都合な状態を回避するのがSLR(1)構文解析手法である.以下に,次の入力トークンを一つだけ先読みし,Follow集合を使って不都合な状態を回避する方法を定義する.
(定義)時点u・v$=u・v1v’$において,uに対しA→α・を使って還元するときは以下である.ただし,v1は終端記号である.
(1)u=u’α
(2)S・$ ⇒*rm u’A・v1v’$ ⇒*rm u’α・v1v’$かつv1∈Follow(A)の場合にのみ還元する
ここで,⇒*rmは0回以上の最右導出を行うことを表す.
もしv1∈Follow(A)ならば,LR(0)項B→α1・aα2に対して,v1=aであれば」シフトする.
もしv1∈Follow(A)かつv1≠aならば,構文エラーである.
ただし,Follow(A)={a∈VT|S ⇒*rm $u’Aa…}
SLR(1)構文解析手法では,動作表と行先表を使用する.6.2のLR(0)構文解析の正準オートマトンから作成した動作表,行先表,生成規則の番号,Follow集合を表1に示す.
(2)行先表は還元動作rjにおいて,還元後に,状態Iと非終端記号aに対して行き先の状態を記述したものである.
表1 動作表,行先表,生成規則の番号,Follow集合
次に,トークンの列i*i+i$のSLR(1)構文解析を行う.図13に構文解析の配置を示す.
まず,配置の図の1行目では,(F→・i)∈I0と動作表の1行目1列目の (I0,i)よりs5となり,2行目が得られる.次に,2行目では,(F→i・)∈I5と動作表の6行目3列の(I5,*)よりr6となり,生成規則の番号6のF→iで還元して,行先表の(I0,F)でI3になる.同様の還元とシフトを続けて,配置の図の最後の行は,動作表の(I1,$)より受理となり,構文木が生成できた.
図13 i*i+i$のSLR(1)構文解析の配置
6.4 LR(1) 構文解析手法
SLR(1)構文解析では,不都合な状態を解決するためにFollow集合だけを用いた.すなわち,ある状態I1の中のA→α・とそれ以外があったとき,次の入力記号aがa∈Follow(A)であるときA→αによる還元をすることにした.Follow(A)={a|a∈VT, S$⇒*…βAa…}とすると,FollowはAの右側のaを考慮しているが,Aの左側にあるβは考慮していない.ここで,Aの左側と右側の両方を考慮し,Follow集合を細分化したものがLR(1)文法である.Aの左側βが違えば次にくるaも違う可能性がある.そこで,入力記号も一緒にして,[A→α・,a]とうLR(1)項を使用する.さらに,このLR(1)項を一般化して,[A→x・y,a]を考える.左側のA→x・yはLR(0)項であり,LR(1)項の核と呼ばれる.aは先読み記号と呼ばれる終端記号である.LR(1)項は構文解析が進んで,もし完全項を核とするLR(1)項[A→xy・,a]になれば,次の入力がxのときに,生成規則A→xyによる還元ができることを意味する.なお,LR(1)項の集合については,核が同じであるLR(1)項[A→x・y,a1],…, [A→x・y,an]をまとめて,[A→x・y,a1/…/an]と書く.
LR(1)項の集合Iに対して,その閉包Closure(I)は次のアルゴリズムで得られる.
(アルゴリズム)
1.Closure(I):=I
2.[A→α・Bβ,a]∈Closure(I)に対して,B→γという生成規則があれば,b∈First(βa)なるすべてのbについて,[B→・γ,b]をClosure(I)に加える.
3.上記2.を新たに加えるものがなくなるまで繰り返す.
この閉包がLR(1)構文解析における正準オートマトンの状態である.正準オートマトンの状態遷移は次のGOTO(I,X)によって決められる.
(定義)IをLR(1)項の集合,X∈(VN∪VT)=Vとしたとき
GOTO(I,X)=Closure({[A→αX・β,a]|[A→α・Xβ,a]∈I})
文法G’=<VN, VT, P, E>において,このようなLR(1)項の集合を求めるには,まず,構文解析の終了判定を容易にするために,新たな非終端記号E’を追加しE’→Eなる生成規則を文法G’の生成規則の集合Pに追加して,以下のアルゴリズムを実行する.
(アルゴリズム)
初期設定 C:={Closure({E’→・E })}
以下を新たな集合がCに付け加えられなくなるまで繰り返す:
任意のI∈CとX∈Vに対し,GOTO(I,X)が空集合でなく,Cに入っていなかったらCに加
える
LR(1)構文解析の動作表はSLR(1)の動作表と同様に作られるが,次の点のみが異なる.
SLR(1)の場合は
A→α・∈I1ならばa∈Follow(A)に対してA[I1,a]はA→αで還元する
であったが,LR(1)の場合は
[A→α・,a]∈I1ならばA[I1,a]はA→αで還元する
となる.LR(1)の正準オートマトンの一部を図14に示す。
図14 LR(1)の正準オートマトンの一部
6.5 LALR(1) 構文解析手法
LR(1)構文解析法はSLR(1)よりも詳細な構文解析が可能であるが,正準オートマトンの状態数が多すぎ,現実のプログラミング言語では実用的ではない.そこで,核が同じであるLR(1)項を同一視することによって,状態数を減らすのがLALR(1)構文解析法である.これによって,LALR(1)の構文解析の正確さはLR(1)より劣るが,Follow集合を使うSLR(1)よりも正確な構文解析が実現でき,しかも状態数はSLR(1)と全く同じになり,実用的な構文解析が可能となる.なお,LALRはLookahead(先読み)LRの略である.LALR(1)の正準オートマトンを図15に示す.C言語はLALR(1)構文解析で実現できることが知られている.
図15 LALR(1)の正準オートマトン
6.6 LR構文解析の理論的背景
ここでは,和文教科書 [6] とLR構文解析の論文 [7] に従い,LR構文解析の理論的背景を説明する.
まず,プログラミング言語の文法を表す文脈自由文法をG0=(N,∑,P,S)とする.ここで,Nは非終端記号の集合,∑は終端記号の集合,Pは生成規則の集合,Sは開始記号である.なお,不要な非終端記号は含まれないで,ε生成規則もないとする.次に,LR(0)の場合と同様な考え方により,以下のような新たな開始記号S’を追加した文法G0’を考える.なお,$はトークン列の最後尾を表す記号である.
G0’=(N∪{S’},∑∪{$},P∪{S’→S$},S’)
次に,G0’によって定まる文字列の集合CGを以下のように定義する.
CG={αβ|S’⇒*rmαAw⇒rmαβw}
ここで,記号列αβはハンドルβが生成規則A→βで還元できる記号列であり,CGは還元できる記号列の集合である.なお,ハンドルとは,記号列の中で生成規則の本体と合致する部分列のことで,その生成規則による還元が最右導出を逆順に辿ったときの1ステップに対応するものを呼ぶ.もっとも重要なことはCGが正規言語であることである.ゆえに,CGを生成する文法は正規文法であるので,有限オートントンで受理できる [7].以上より,CGを受理する還元動作をする非決定性有限オートマトンが構成できることがわかる.
次に,和文書籍 [6] に従い,CGを受理する還元動作をする非決定性有限オートマトンを構成する.
G0’=(N∪{S’},∑∪{$},P∪{S’→S$},S’)とし,CG={αβ|S’⇒*rmαAw⇒rmαβw}を受理する非決定性有限オートマトンNGを次のように構成する.
NG=(Q,∑,δ,S0,F)
ここで,
Q={[A→α・β]|A→αβ∈P} (LR(0)項)
∑=N∪∑ (語彙=非終端記号の集合∪終端記号の集合)
S0=[S→・S$] (開始記号の導入項)
F={[A→α・]|A→α∈P} (完全項(CGの受理状態(還元)))
δは以下の関数である:
δ([A→α・vβ],v) = [A→αv・β],ここでv∈N∪∑
δ([A→α・Bβ],ε)= [B→・γ],ここでB→γ∈P
ここで,集合の包含関係より,L(NG)= {αβ|S’⇒*rmαAw⇒rmαβw}= CGが示せる.非決定性有限オートマトンNGは部分集合構成法 [8] により,決定性オートントンである正準オートマトンが構成できる.
以上により,最右導出の最後の1ステップは有限オートマトンを使い実現できる.つまり,有限オートマト,すなわち正準オートマトンを繰り返し使うことにより,ある範囲の文脈自由文法の解析ができることがわかる.さらに,配置を用いて構文解析を進めてきたが,これは正準オートマトンの状態遷移をスタックに記憶しており,構文解析の効率化を実現している [6].
6.7 LR構文解析のまとめ
LR構文解析のクラスを図16にまとめる.以下のことが知られている.
図16 LR構文解析の文法のクラス
7.まとめ
私たちの世界を動かすプログラムは,人間がPythonやJava,Cなどの高級プログラミング言語で記述し,それをコンパイルして低レベルのコードにまとめて実行する.現代のプログラミング言語のコンパイラの多くは,Alfred V. AhoとJeffrey D. Ullmanの貢献が大きい[1].コンパイラでは字句解析,構文解析,コード生成, 最適化などのための効率的な技術やアルゴリズムが重要である. 本解説記事では,コンパイラの構造及び,コンパイラで最重要で最難関な構文解析,特にLR構文解析 [7] について理論や技術などの解説を行った.特に, 類書にない構文解析手法の間の関連および新たな事例を盛り込んで解説を行ったので理解を深める助けとなる.もっとも重要なことは, LR構文解析 [7] の本質は, 文脈自由言語のプログラミング言語の構文解析に対して, 驚くべきことに有限オートマトンを繰り返し実行していることである. これの解説記事により,現在と未来の社会を支えるコンピュータ・ソフトウェアの理解を深めることができる.さらに重要なことは, コンパイラの理論, 技術, 実装は他の多くのソフトウェアの理論や設計, 実装などの原理となっており, 今後の新しいソフトウェアの設計や実装に有益である.
今後,人工知能や分散処理, 組込みシステムなどの多数の新しいアプリケーションの開発が必要であり, それらを合理的に記述して設計・実装できる新しいプログラミング言語が生まれるので, コンパイラの理論と技術, 実装などの理解は必修であり, 本解説はそれらに有益であろう.また, 今後, 組込みシステムや人工知能などにおいて, 新しいハードウェアが作られるので, コンパイラの最適化技術などの進展が期待される. なお,ドラゴンブック [2] を手本に,日本語の優れた教科書も多数あり, その代表的な書籍 [3,4,5,6] などを参考文献にあげており,本解説記事の作成にも参考にしている.
引用・参考文献
[1] ACM(2020)”ACM Turing Award Honors Innovators Who Shaped the Foundations of Programming Language Compilers and Algorithms”,ACM(https://awards.acm.org/about/2020-turing (accessed on 31 December 2024)).
[2] Alfred V. Aho, Monica S. Lam, Jeffrey D. Ullman(2007)”Compilers principles, techniques & tools”, pearson Education, pp.1-1038.
(訳本:A.V.エイホ (著), 原田 賢一 (翻訳)(2009)『コンパイラ 第2版: 原理・技法・ツール』,サイエンス社,pp.1-1090.)
[3] 中田育男(2009)『コンパイラの構成と最適化 第2版』,朝倉書店,pp.1-601.
[4] 佐々 政孝(1989)『プログラミング言語処理系』,岩波書店,pp.1-602.
[5] 湯淺 太一(2014) 『コンパイラ』,オーム社,pp.1-248.
[6] 大堀 淳(2021)『コンパイラ: 原理と構造』,共立出版,pp.1-185.
[7] Donald E. Knuth(1965)”On the Translation of Languages from Left to Right”, Information and Control, 8(6), pp.607-639.
[8] Jeffrey D. Ullman, John Hopcroft, Rajeev Motwani(2006)” Introduction to Automata Theory, Languages, and Computation”, Addison Wesley,pp.1-750.
謝辞
本解説記事は金沢大学理工学域電子情報学類の3年生向けコンパイラの講義資料をまとめたものです。コンパイラ技術全般において,中田先生,佐々先生,湯浅先生,大堀先生のセミナーや書籍などは大変有益であり,ここに感謝します。また,TAとして講義資料作成にご協力いただいた金沢大学大学院生の小寺広志さんに深く感謝します。最後に,丁寧に本解説記事を査読いただいた査読者各位に感謝します。
本解説記事は金沢大学理工学域電子情報学類の3年生向けコンパイラの講義資料をまとめたものです。コンパイラ技術全般において,中田先生,佐々先生,湯浅先生,大堀先生のセミナーや書籍などは大変有益であり,ここに感謝します。また,TAとして講義資料作成にご協力いただいた金沢大学大学院生の小寺広志さんに深く感謝します。最後に,丁寧に本解説記事を査読いただいた査読者各位に感謝します。