言語処理系とは
言語処理系とは、プログラミング言語で記述されたプログラムを計算機上で実行するためのソフトウエアである。そのための構成として、大別して2つの構成方法がある。
1、インタープリター(interpreter,翻訳系):言語を意味を解析しながら、その意味する動作を実行する。
|
2、コンパイラ(compiler,通訳系):言語を他の言語に変換し、その言語のプログラムを計算機上で実行させるもの。狭い意味でコンパイラは、言語を機械語に変換し、実行するものであるが、他の言語、あるいは仮想機械コードに変換するものもコンパイラと呼ぶ。他の言語に変換するときには、特にtranslatorと呼ぶ場合もある。
元のプログラムをソースプログラム、翻訳の結果と得られるプログラムをオブジェクトプログラムと呼ぶ。機械語で直接、計算機上で実行できるプログラムを実行プログラムと呼ぶ。オブジェクトプログラムがアセンブリプログラムの場合には、アセンブラにより機械語に翻訳されて、実行プログラムを得る。他の言語の場合には、オブジェクトプログラムの言語のコンパイラでコンパイルすることにより、実行プログラムが得られる。仮想マシンコードの場合には、オブジェクトコードはその仮想マシンにより、インタプリトされて実行される。
言語処理系の基本構成
コンパイラにしてもインタプリターにしても、その構成は多くの共通部分を持つ。すなわち、ソースプログラムの言語の意味を解釈する部分は共通である。インタプリターは、解釈した意味の動作をその場で実行するのに対し、コンパイラではその意味の動作を行うコードを出力する。
|
言語処理系は、大きく分けて、次のような部分からなる。
1、字句解析(lexical analysis): 文字列を言語の要素(トークン、token)の列に分解する。
2、構文解析(syntax analysis): token列を意味を反映した構造に変換。この構造は、しばしば、木構造で表現されるので、抽象構文木(abstract syntax tree)と呼ばれる。ここまでの言語を認識する部分を言語のparserと呼ぶ。
3、意味解析(semantics analysis): 構文木の意味を解析する。インタプリターでは、ここで意味を解析し、それに対応した動作を行う。コンパイラでは、この段階で内部的なコード、中間コードに変換する。
4、最適化(code optimization): 中間コードを変形して、効率のよいプログラムに変換する。
5、コード生成(code generation): 内部コードをオブジェクトプログラムの言語に変換し、出力する。例えば、ここで、中間コードよりターゲットの計算機のアセンブリ言語に変換する。
コンパイラの性能とは、如何に効率のよいオブジェクトコードを出力できるかであり、最適化でどのような変換ができるかによる。インタープリタでは、プログラムを実行するたびに、字句解析、構文解析を行うために、実行速度はコンパイラの方が高速である。もちろん、機械語に翻訳するコンパイラの場合には直接機械語で実行されるために高速であるが、コンパイラでは中間コードでやるべき操作の全体を解析することができるため、高速化が可能である。
また、中間言語として、都合のよい中間コードを用いると、いろいろな言語から中間言語への変換プログラムを作ることで、それぞれの言語に対応したコンパイラを作ることができる。
例題:式の評価
さて、例として最も簡単な数式の評価について、インタプリターとコンパイラを作ってみることにする。目的は,
12 + 3 - 4
の式の入力に対し、この式を計算し、
11
と出力するプログラムを作ることである。これは、式という「プログラミング言語」を処理する言語処理系である。「式」という言語では、tokenとして、数字と"+"や"-"といった演算子がある。
まずは、字句解析ではこれらのトークンを認識する。例えば、上の例では、
12の数字、+の演算子、3の数字、−の演算子、4の数字、終わり
という列に変換する。このプログラムがgeToken.cである。
これをいわゆる構文解析しなくても、直接実行する(計算してしまう)インタプリターは簡単にできる。その動作は以下のような動作である。
1、現在の結果を変数resultに覚えておく。また、直前の演算子を変数opに覚えておく。
2、関数getTokenを呼んで、数字であれば、現在の結果と今の数字の値との計算を行う。但し、最初の数字(まだ、opがない)の場合には、現在の結果に入力された数字を格納する。
3、終わりがきたら、現在の数字を出力する。
|
これが、いわゆる電卓のアルゴリズムである。(この電卓の欠点を考えてみよ!)
では、この「式」というプログラミング言語の構文とはどのようなものであろうか。例えば、次のような規則が構文である。
足し算の式 := 式 +の演算子 式
引き算の式 := 式 −の演算子 式
式 := 数字 | 足し算の式 | 引き算の式
このような記述を、BNF (Backus Naur Form または Buckus Normal Form)という。
このような構造を反映するデータ構造を作るのが、構文解析である。図に示す。この構文木を作るプログラムが、readExpr.cである。
この構文木を解釈して実行する、すなわちインタプリターをつくってみることにする。その動作は、
1、式が数字であれば、その数字を返す。
2、式が演算子を持つ演算式であれば、左辺と右辺を解釈実行した結果を、演算子の演算を行い、その値を返す。
このプログラムがevalExpr.cである。
mainプログラムでは、関数readExprを呼び、構文木を作り、それを関数evalExprで解釈実行して、その結果を出力する。これが、インタプリターである。
次にコンパイラをつくってみる。getExprまでは同じである。中間コードとして、スタックマシンのコードを仮定することにする。スタックマシンは以下のコードを持つことにする。
1、PUSH n : 数字nをスタックにpushする。
2、ADD : スタックの上2つの値をpopし、それらを加算した結果をpushする。
3、SUB : スタックの上2つの値をpopし、減算を行い、pushする。
4、PRINT: スタックの値をpopし、出力する。
コンパイルでは、このスタックマシンのコードを使って、式を実行するコード列を作る。その手順は、
1、式が数字であれば、その数字をpushするコードを出す。
2、式が演算であれば、左辺と右辺をコンパイルし、それぞれの結果をスタックにつむコードを出す。その後、演算子に対応したスタックマシンのコードを出す。
3、式のコンパイルしたら、PRINTのコードを出しておく。
コード生成では、ここではスタックマシンのコードをCに直して出力することにしよう。Cで実行させるために、mainにいれておくことにする。このプログラムが、compileExprである。オブジェクトコードのファイルは、ouptut.cとして、これをCコンパイラでコンパイルして実行すればよい。(assemblerのファイルの場合はasコマンドでコンパイルする。)
電卓のプログラムに比べて、構文木を作るなど、ずいぶん遠周りをしたようであるが、その理由は演算の優先度や、括弧の式など、通常の数学で使われる式を正しく処理するためである。例えば、
12*3 + 3*4
の場合には、掛け算を最初にして、それらを加算しなくてはならない。この処理を反映した構文木を作ることによって、正しく処理する「言語処理系」を作ることができるようになる。
演習問題1: かっこを入れた式が処理できるインタプリターまたはコンパイラを作りなさい。たとえば、
12 – (3 – 4)
または、掛け算、割り算を処理できるように拡張しなさい。この場合、掛け算や割り算は足し算、引き算に優先する。コンパイラでは、中間コードにMUL, DIVが加わることになる。