林 恒俊
言語処理系
言語処理系はプログラミング言語を処理する応用プログラム
(application program)
である。コンパイラ(compiler)
と呼ばれることもある。原プログラムは普通テキストデータとして用意される。この表現形式は 人間にとって理解可能であり、機械でデータとして処理することも可能 である。しかし、不幸なことには、この形式のプログラムは、計算機が直 接解釈実行することができない。計算機は主記憶中に記憶された
2
進形 式の機械語プログラムのみが直接実行可能だからである。なんらかの手 段でテキストデータを実行可能な2
進機械語に変換する必要がある。言 語処理系はこの矛盾を解決するための手段として着想され開発された。コンパイラは人間にとって理解可能な形式のプログラムを処理して、計 算機が直接実行可能な形式に変換するプログラムであり、書物の異言語 間翻訳に類似した機能をもっている。
source
program compiler machine
code semantic
interpreter equivalent run input
output equivalent output
コンパイラが果たさなければならない機能は原プログラムをそれと同等 な能力の機械語に変換することである。すなわち原プログラムに入力を 与えて解釈実行して得られる出力と、コンパイルされた機械語プログラ ムが同等な入力に対して実行した結果が同等でなければならない。これ を検証することは容易ではない。
⃝c 2015-2017 Tsunetoshi Hayashi.
1
言語処理系の構成要素
言語処理系の主要な構成要素は字句解析、構文解析、コード生成である けれども、加えてコード改善、実行時環境等あらかじめ計画しておかな ければならない課題が多数存在する。むしろ現在では上記の
3
要素は適 切な自動化ツールを利用すれば比較的容易に構築可能である。•
字句解析(lexical analysis)
は字句定義に対応した処理を行う過程 である。実際の処理は原プログラムの文字列を切出して構成要素す なわちトークン(token)
に分割する。lex
のような正規表現処理生 成系を利用してもよいが、手作りでもそれほど手間ではない。•
構文解析(syntax parsing)
は構文定義に対応した処理を行う過程で、入力トークン列を文法に基づいた木構造すなわち解析木
(parse
tree)
に変換する。構文解析は解析木を上方向に向かって構築する上昇法とルートから下向きに構築する下降法の二通りの手法が利用さ れる。言語が構文図で定義されている場合は構文図から直接解析プ ログラムを作成することも可能である。文法から
yacc
のような構 文解析自動生成系(parser generartor)
を利用して解析プログラムを 生成することも可能である。字句解析と構文解析は言語に依存して いるためこの部分を言語処理系のフロントエンドと呼ぶことがある。•
コード生成(code generation)
のため解析木から処理内容を反映 したプログラム木あるいは抽象プログラム(abstract program)
を構 築し、それに対して命令語の動作を表現する部分木を適合させる。プログラム木全体にマッチングできればそれから命令語列を生成で きる。コード生成は機械語に依存しているためこの部分を言語処理 系のバックエンドと呼ぶことがある。
•
機械語を生成するかわりに、機械に依存しない中間言語(intermediate
language)
コードを生成する処理系もある。バイトコードとも呼ばれる中間言語はインタプリタ
(interpreter)
プログラムでこれを直 接解釈実行することが可能である。そしてインタプリタは既存の言 語を利用して移植できるため、言語を新規オペレーティング・シス テムに移動するような場合に容易に作業を進めることができる。中 間言語はよく逆ポーランド記法(reverse Polish notation, RPN)
の命令体系を利用することが多い。•
再帰呼出しが可能なブロック型言語は実行時に局所変数や復帰情報 をスタック上に確保する。このようなスタックやそれを管理する仕 組みを実行時環境(runtime environment)
という。言語の設計に は動作するオペレーティング・システム上で実行時環境をどのよう に実装するかあらかじめ考慮しなければならない。•
コード改善(code improvement)
は生成された機械語を調整して アセンブリ言語で書かれたプログラムに匹敵する実行速度を与え ることが目的である。初期のコンパイラから実装されていたが、か けた手間の割に効果的とはいえないのが実情である。原プログラム のアルゴリズムの改善がはるかに効果的であることが少なくない。コード改善は機械語の冗長な部分を取除くことから、繰返しをベク トル演算化すること、命令順を再スケジュールすることまで実現し ている。しかし、計算機種によってはこの程度までコード改善をし ないと実行速度が不充分になることもある。
字句処理
字句処理はテキスト形式の原プログラムを切分けて予約語、識別子、定 数、演算子、区切り記号等のプログラム構成要素を抽出する過程である。
このような構成要素をトークン
(token)
という。•
一般的な設計では字句処理は独立した過程のプログラムではなく、構文解析処理から呼出されて最新の要素の種類を返すような関数か 手続きとして作成される。
source
program lexical analyzer
syntax parser
identifier table
識別子の名前や定数の値は関連した表やデータベースに格納する。
•
原プログラムは概ね次のようにトークンに切分けられる。println("Hello World!");
をトークンに分解すれば
println ( "Hello World!" ) ;
となる。
•
トークン集合は形式言語学でいう正規言語(regular language)
と見 なされる。そして正規言語は正規表現(regular expression)
で与え られる。正規表現は文字の列(sequence)
と選択(selection)
と繰返し(repetition)
により文字パターンを定義する。文字列を解析して正規表現に適合するかどうか調べるためには有限状態機械
(finite state
automaton)
という抽象機械を利用すればよいことが知られている。したがって字句処理プログラムは
◦
トークンを正規表現で定義する◦
正規表現を判定する有限状態機械を合成する◦
その有限状態機械をプログラムで実現するで作成可能である。
Unix
にはlex
という字句処理生成プログラム が付属している。実際にlex
で作成した字句処理は大きくて遅いこ とが多いので利用は多くない。字句処理プログラムは比較的容易に 手作りで作成可能である。•
古いFortran
では基本的に文の空白文字は無視され、宣言されていない変数は暗黙に宣言されているものとみなされる。そこで、次の
2
つの文を比較すると、文の末尾に近いコンマとピリオドの違いだ けで、トークンに分解する区切り方も、構文も全く別物である。文 トークン列 備考
DO 1 I=1,2 DO 1 I = 1 , 2 DO
文DO 1 I=1.2 DO 1 I = 1.2
代入文字句定義と構文定義が互いに依存していて、独立していないことが 原因である。最近の言語はこのようにならないように設計される。
構文解析の原理
通常プログラミング言語の構文は
BNF
のような形式的体系に基づいて定 義される。ここでは記述を簡単化するため形式的文法を説明に利用する。•
この文法記述では置換え可能な記号(
非終端記号)
を他の記号列で置 換えることを繰返し、最終的に置換え不可能な記号(
終端記号)
のみ からなる記号列を生成する。生成された記号列が正しい文である。非終端記号の一つを選んで開始記号
(start symbol)
として常にこの 記号から置換えを開始する。例えば非終端記号は{ E, T, P }
、開始 記号はE
、終端記号は{ +, × , (, ), a }
で、次のような置換えをすると 仮定する。E → E + T, E → T, T → T × P, T → P, P → (E), P → a
次のように置換えを繰返すとE ⇒ E + T ⇒ T + T ⇒ P + T ⇒ a + T ⇒
a + T × P ⇒ a + P × P ⇒ a + a × P ⇒ a + a × a
というように
a
と+
と×
と()
からなる数式風の記号列を生成する。•
この生成過程は次のような木として図示することができる。E
E
+
T T
P
a
T
×
P
P
a a
このような木を解析木
(parse tree)
という。構文解析とは与えら れた文から解析木を構築することである。入力文に対して根から解析木を構築する方法と葉から構築する方法が考えられる。それぞれ 下降型解析法
(top-down parsing)
及び上昇型解析法(bottom-up parsing)
という。•
入力記号列は一般に先頭から後尾に向かって走査される。下降法は 入力文を生成した過程を再現することにより解析木を再構成する手 法である。開始記号から入力記号と適合するように生成規則にした がって文を生成する。• • • • • • • • • input symbols start symbol
reconstruction
上昇法は入力記号列から部分木構築可能な列を検出して、できるだ け早期に部分木を構築することにより、解析木を再構成する手法で ある。
• • • • • • • • • input symbols start symbol
reconstruction
いずれの手法でも最終的に得られる解析木は同一である。
再帰下降法
主としてプログラミング言語の構文が構文図で定義されている場合、構 文図から解析プログラムを直接導きだすことも可能である。手作りの言
語処理系では好んで採用されることが多い。
•
次の構文図の例についてE T
T +
T P
P ×
P
( E )
a
以下のようにプログラムを作成する。
◦
構文図のそれぞれの非終端記号に対して手続きを用意する。◦
手続きの内部で構文図のパスをコーディングする。◦
終端記号を通過する時、その記号が入力されているか検査する。◦
非終端記号を通過する時、対応する手続きを呼出す。◦
パスが分岐する時、各パスの先で入力される記号の有無で分岐 を選択する。•
結果得られるプログラムは次のようになる。なおsymbol
は最後に 入力したトークンが格納される変数名、next
は入力を1
トークン分 前進する手続き、error
は構文誤りを報告する手続きである。recursive descent parser procedure E;
begin T ;
while symbol = “+” do next;
T
od end;
procedure T ; begin P ;
while symbol = “ × ” do next ;
P od end;
procedure P ; begin
if symbol = “a” then next else
if symbol = “(” then next ;
E;
if symbol = “)” then next else error
fi else error fi
fi end;
式と文のコード生成
原プログラムについて構文解析を行った結果、構文が正しい場合には次 の処理を進められるように適切な形式で解析結果を出力する。出力は解 析木、逆ポーランド記法、
4
つ組(quadruple)
、3
つ組(triple)
等が代表的 である。それぞれ、一長一短があり、言語の複雑さやコンパイラの規模 などに依存して適切なものが選択される。さらに、形式は異なっていて も表現されている内容はほとんど変わらないため、互いに形式を変換す ることが可能である。通常、最適化や直接機械語生成を目指す場合には 解析木や4
つ組、中間言語を経由してインタプリタ実行する場合には逆ポーランド記法がよく採用される。
•
解析木をさらに変形して、プログラムの論理的構造を木構造で表現 したプログラム木、あるいは抽象プログラム(abstract program)
を 利用する。コード生成はこれらの木構造を適当に走査することに よって実現される。次にプログラム木の例を示している。a
a +
a
×
•
逆ポーランド記法あるいは後置記法(postfix notation)
は数式など の表現法の一種で、オペランドを演算子の前に置くものである。こ れに対して普通の数式表現を中間記法(infix notation)
、演算子をオ ペランドの前に置くものをポーランド記法(polish notation)
あるい は前置記法(prefix notation)
と呼ぶことがある。記 法 式 の 例 中間記法
(a + b) × c
ポーランド記法× (+(a, b), c)
逆ポーランド記法((a, b)+, c) ×
これらの記法のうち、逆ポーランド記法では括弧を取り除いても演 算順序が変わらないように評価法を与えることができる。すなわち 演算子の優先順に無関係に演算順をユニークに規定することが可能 である。コンパイラは逆ポーランド記法に基づく演算子とオペラン ドの列として構文解析結果を出力する。逆ポーランド記法で出力す るためには次のようにプログラム木を走査すればよい。
◦
木の左枝の葉が変数や定数の終端ならそれを出力する。そう でなければその葉を根として走査を再開する。◦
次に右枝の葉が変数や定数の終端ならそれを出力する。そう でなければその葉を根として走査を再開する。◦
根の演算子を出力して、上位の枝に戻る。◦
木全体を走査すると終了する。走査は再帰的に行われる。
•
逆ポーランド記法で表現された式はスタックを使って評価すること ができる。中間言語インタプリタ方式のコンパイラではこの評価ア ルゴリズムが大変重要な役割を果たしている。◦
最初にスタックを空にし、ポーランド記法式を先頭から順に走 査する。◦
式中の変数や数値を走査した場合、その値をスタックにシフト(push)
する。◦
演算子が走査された場合、スタックからその演算に必要な数の オペランドを取出(pop)
し、該当する演算を行い、結果として 得られた値をスタックに置く(push)。
◦
ポーランド記法式が走査されてしまえば、式の値がスタックに 残される。次に入力記号列
1 2 3 × +
を実際に評価した各段階を示している。スタック 入力記号列
下位
←→
上位 先頭←→
後尾 注 釈<empty> 1 2 3 × +
シフト(
初期状態)
1 2 3 × +
シフト1 2 3 × +
シフト1 2 3 × + ×
演算1 6 + +
演算7
終了Pascal
やSmallTalk
やJava
のコンパイラは逆ポーランド記法を出 力するように設計されている。パーソナル・コンピュータのUCSD
P-System
はPascal
で書かれ、オペレーティング・システムまで逆ポーランド記法で動いていた。
機械語コード生成
字句解析や構文解析と異なり、コード生成には理論的な基礎となるもの がない。とくに、意味が厳密に数学的対象に基づいて定義されているよ うな場合、機械語プログラムに完全に反映することはできない。例えば、
機械データ形式の整数では最大値や精度は有限であり、数学の整数の定 義とは明らかに異なっている。
•
現在のところ、意味定義と機械語命令の体系をにらみ合わせて、コー ド生成のアルゴリズムを設計するしかない。この作業はかなり恣意 的である。逆にいえばコンパイラ設計の経験と腕前が発揮される所 でもある。•
命令語を選択し機械語プログラムを出力するためには、構文解析と よく似た手法を利用することができる。この手法では、計算機に実 装されている命令を生成規則の一種(
演算パターン)
として記述し、プログラム木を入力として構文解析
(パターンマッチング)
を行なっ て、その結果得られる導出列(
解析木)
から機械語プログラムを生成 する。◦
命令語のオペランドやレジスタなどの演算に関係する要素を非 終端記号で代表する。◦
各命令について、演算とオペランドの組合わせを右辺、実行結 果を含む要素を左辺とするような生成規則を構成し、その命令 の動作を記述する。◦
この生成規則の右辺を再解釈すると、演算子とオペランドから 構成される木構造とみなされ、プログラム木の部分木と対応 する。◦
プログラム木を入力として、生成規則とのマッチングすなわち 構文解析をおこない、解析木を構成する。◦
解析木を適切に巡回することで機械語命令列を得る。制御構造のコード生成
制御構造のコードは中間言語でも機械語でも実質は同じである。論理値 の評価コードが生成できれば、条件分岐とラベルを割当てるだけでよい。
•
条件文は次のようにコードを生成すればよい。if B then S fi if B then S
1else S
2fi
code of B
branch on false to label-1;
code of S label-1:
code of B
branch on false to label-1;
code of S
1branch to label-2;
label-1:
code of S
2label-2:
•
繰返し文は次のようにコードを生成すればよい。while B do S od repeat S until B
label-1:
code of B
branch on false to label-2;
code of S branch to label-1;
label-2:
label-1:
code of S
code of B
branch on false to label-1;
コード改善
•
プログラム単位、式、文、制御構造程度の範囲を限ってコードを改 善する技法をいう。◦
レジスタの最適配置◦
共通部分式(common subexpression)
の削除◦
複写伝搬(copy propagation)
の削除◦
無用コードの削除(dead code elimination)
◦
定数式のコンパイル時計算(constant folding)
◦
ループ最適化(ループ不変式の移動、ループ変数の削除、演算
の弱化)
◦
ピープホール最適化(peephole optimization)
等のいくつか実現可能な改善法が挙げられる。•
ピープホール(
覗き穴)
改善は、上記の諸技法と異なり、コード生成 が完了した後、機械語命令列について改善を施すように試みるもの である。例えば、次のような場合に実施することができる。◦
無条件分岐先がまた無条件分岐命令である◦
無条件分岐命令の後にラベルの付いていない命令列がある◦
同じ番地に対するSTORE
命令とLOAD
命令が引き続いている このような命令列は、コード生成でかなり工夫をしても発生する。実行時環境
現在の多くのプログラミング言語はブロック構造と静的有効範囲規則に 基づく実行モデルを採用している。このモデルでは局所変数を動的に確 保する。これはハードウェアが備えている環境とは適合しない。適合さ せるメカニズムを実行時ライブラリが用意しなければならない。
•
実行時環境を決定する要因はプログラミング言語が用意している次 のような機能に依存する。◦
再帰呼出し(recursive call)
の有無◦
手続きの入れ子(procedure nesting)
構造の有無◦
動的に局所変数の大きさが可変◦
手続き引数の有無•
これらの機能を実現するためには実行プログラムの作業領域や局所 変数を静的に確保せずに、実行時スタックを用意してその上に動的 に確保するようにすればほぼ解決する。before call after call
return local variables
stack
fp sp
return local variables arguments
return local variables
stack
fp sp
•
手続きの呼出しと復帰の機構は、基本的な部分はハードウェアの支 援が必要であるが、言語の機能によっては、実行時にかなりの処理 を行なわなければならない。通常、手続きを呼出す時には1.
手続きの入り口でプログラムカウンタやレジスタ類などのCPU
状態を復帰情報として保存し、2.
フレームポインタを設定し、3.
局所変数や一時変数などの作業領域を確保したのち、4.
手続きを開始する。という手順を実行する。手続きを終了する時には、
1.
作業領域を解放し、2.
復帰情報を回復し、3.
呼出し元に分岐する。という手順が実行される。一般に復帰の方が作業量が少ない。