Upload
others
View
0
Download
0
Embed Size (px)
Citation preview
1
第1回(平成15年度9月2日)第1回(平成15年度9月2日)
言語処理系とは言語処理系とは
筑波大学 佐藤三久
言語処理系とは言語処理系とは
u 言語処理系とは、プログラミング言語で記述されたプログラムを計算機上で実行するためのソフトウエアである。そのための構成として、大別して2つの構成方法がある。
− インタープリター( interpreter,翻訳系):言語の意味を解析しながら、その意味する動作を実行する。
− コンパイラ( compiler,通訳系):言語を他の言語に変換し、その言語のプログラムを計算機上で実行させるもの。狭い意味でコンパイラは、言語を機械語に変換し、実行するものであるが、他の言語、あるいは仮想機械コードに変換するものもコンパイラと呼ぶ。他の言語に変換するときには、特に translatorと呼ぶ場合もある。
ソース、オブジェクト、実行プログラムソース、オブジェクト、実行プログラム
uソースプログラム:元のプログラムuオブジェクトプログラム:翻訳の結果と得られるプログラム
u実行プログラム:機械語で直接、計算機上で実行できるプログラム− オブジェクトプログラムがアセンブリプログラムの場合には、アセンブラにより機械語に翻訳されて、実行プログラムを得る。
− 他の言語の場合には、オブジェクトプログラムの言語のコンパイラでコンパイルすることにより、実行プログラムが得られる。
− 仮想マシンコードの場合には、オブジェクトコードはその仮想マシンにより、インタプリトされて実行される。
言語処理系の流れ言語処理系の流れ
インタープリタソースプログラム
入力
出力
コンパイラソースプログラム
オブジェクトプログラム
実行プログラム
入力
出力
言語処理系の基本構成言語処理系の基本構成
u 字句解析 (lexical analysis): 文字列を言語の要素(トークン、 token)の列に分解する。
u 構文解析 (syntax analysis): token列を意味を反映した構造に変換。この構造は、しばしば、木構造で表現されるので、抽象構文木(abstract syntax tree)と呼ばれる。ここまでの言語を認識する部分を言語の parserと呼ぶ。
u 意味解析 (semantics analysis): 構文木の意味を解析する。インタプリターでは、ここで意味を解析し、それに対応した動作を行う。コンパイラでは、この段階で内部的なコード、中間コードに変換する。
ソースプログラム
字句解析
構文解析
意味解析
最適化
コード生成
オブジェクトプログラム
中間コード
実行インタプリタ
言語処理系の基本構成言語処理系の基本構成
u 意味解析 (semantics analysis): 構文木の意味を解析する。コンパイラでは、この段階で内部的なコード、中間コードに変換する。
u 最適化 (code optimization): 中間コードを変形して、効率のよいプログラムに変換する。
u コード生成(code generation): 内部コードをオブジェクトプログラムの言語に変換し、出力する。例えば、ここで、中間コードよりターゲットの計算機のアセンブリ言語に変換する。
ソースプログラム
字句解析
構文解析
意味解析
最適化
コード生成
オブジェクトプログラム
中間コード
実行インタプリタ
2
コンパイラとインタプリターの違いコンパイラとインタプリターの違い
uインタープリタでは、プログラムを実行するたびに、字句解析、構文解析を行うために、実行速度はコンパイラの方が高速である。− 機械語に翻訳するコンパイラの場合には直接機械語で実行されるために高速
− コンパイラでは中間コードでやるべき操作の全体を解析することができるため、高速化が可能
中間コードの役割中間コードの役割
u中間言語として、都合のよい中間コードを用いると、いろいろな言語から中間言語への変換プログラムを作ることで、それぞれの言語に対応したコンパイラを作ることができる。
中間コード
C言語
Fortran
java
X86
SPARC
ARM
MIPS
例題:式の評価例題:式の評価
uさて、例として最も簡単な数式の評価について、インタプリターとコンパイラを作ってみることにする。目的は,
12 + 3 - 4の式の入力に対し、この式を計算し、
11と出力するプログラムを作ることである。
uこれは、式という「プログラミング言語」を処理する言語処理系である。
式の評価:字句解析式の評価:字句解析
u「式」という言語では、tokenとして、数字と"+"や"-"といった演算子がある。
’1’,’2’,’+’,’3’,’-’,’4’
終わり
+演算子12の数字 3の数字
4の数字-演算子
入力された文字列
Token列
字句解析
TokenTokenの定義の定義
u exprParser.h#define EOL 0#define NUM 1#define PLUS_OP 2#define MINUS_OP 3
extern int tokenVal;extern int currentToken;
void getToken(void);...
Tokenの種類
Tokenの種類とその値を
この2つの変数にいれて返す
12の数字currentToken=NUMtokenVal=12
+演算子currentToken=PLUS_OPtokenVal=?
字句解析プログラム字句解析プログラム
u getToken.c#include <stdio.h>#include <ctype.h>#include "exprParser.h"int tokenVal,currentToken;
void getToken(){
int c,n;again:
c = getc(stdin);switch(c){case '+':
currentToken = PLUS_OP;return;
case '-': currentToken = MINUS_OP;return;
case '¥n': currentToken = EOL;return;
}if(isspace(c)) goto again;if(isdigit(c)){
n = 0;do {
n = n*10 + c - '0';c = getc(stdin);
} while(isdigit(c));ungetc(c,stdin);tokenVal = n;currentToken = NUM;return;
}fprintf(stderr,"bad char'%c'¥n",cexit(1);
}
3
字句解析だけを使うインタプリタ字句解析だけを使うインタプリタ
u 構文解析しなくても、直接実行する(計算してしまう)インタプリターは簡単にできる。
1、現在の結果を変数resultに覚えておく。また、直前の演算子を変数opに覚えておく。
2、関数getTokenを呼んで、数字であれば、現在の結果と今の数字の値との計算を行う。但し、最初の数字(まだ、opがない)の場合には、現在の結果に入力された数字を格納する。
3、終わりがきたら、現在の数字を出力する。
u 電卓のアルゴリズム!− 何がわるいか、欠点を考えてみよう。
字句解析だけを使うインタプリタ字句解析だけを使うインタプリタ
u cal.cmain(){
int t;int op;int result;op = NUM;result = 0;while(1){getToken()switch(currentToken){case NUM:switch(op){case NUM: result = tokenVal;break;
case PLUS_OP:result = result + tokenVal;break;
case MINUS_OP:result = result - tokenVal;break;
}break;
case PLUS_OP:case MINUS_OP:op = t;break;
case EOL:printf("result = %d¥n",result);exit(0);
}}
}
構文規則と構文規則とBNFBNF
uこの「式」というプログラミング言語の構文とはどのようなものであろうか?
u構文規則
uこのような記述を、BNF (Backus Naur Form または Buckus Normal Form)
u構文木 (AST: Abstract Syntax Tree):上の構文を反映するデータ構造
足し算の式 := 式 +の演算子 式引き算の式 := 式 -の演算子 式式 := 数字 | 足し算の式 | 引き算の式
構文木構文木
u構文木 (AST: Abstract Syntax Tree):構文規則を反映するデータ構造
‘1’ ‘2’ ‘+’ ‘3’ ‘-’ ‘4’
終わり+演算子12の数字 3の数字 4の数字 -演算子
+演算子
12の数字 3の数字
4の数字
-演算子
文字列
Token列
構文木
字句解析
構文解析
構文木のデータ構造構文木のデータ構造
u exprParser.h
...typedef struct _AST {
int op;int val;struct _AST *left;struct _AST *right;
} AST;
AST *readExpr(void);
どのような種類のノードなのかをいれる。NUM,PLUS_OPなど
Op=NUMの場合に
その値をいれておく
演算子(PLUS_OPなど)の場
合の右の式と左の式
メモリ節約のために、Unionにしてもよい。
式を読み込み構文木を返す関数
式を読み込み構文木を作る関数式を読み込み構文木を作る関数
u readExpr.hAST *readExpr(){
int t;AST *e,*ee;e = readNum();while(currentToken == PLUS_OP || currentToken == MINUS_OP){ee = (AST *)malloc(sizeof(AST));ee->op = currentToken;getToken();ee->left = e;ee->right = readNum();e = ee;
}return e;
}
先読みをしていることに注意getTokenを呼んでからreadNumを呼び出す
前に読んだ式を左にする
新しい ASTを生成
数字を読む
新しい ASTを現在のASTにセット
4
式を読み込み構文木を作る関数式を読み込み構文木を作る関数
u readExpr.hAST *readNum(){
AST *e;if(currentToken == NUM){
e = (AST *)malloc(sizeof(AST));e->op = NUM;e->val = tokenVal;getToken();return e;
} else {fprintf(stderr,"bad expression: NUM expected¥n");exit(1);
}}
新しい ASTを生成
数字でなければ、エラー
NUMのASTを作る
インタプリタ:構文木の解釈インタプリタ:構文木の解釈
u式のインタプリタ:構文木を解釈して実行する(1)式が数字であれば、その数字を返す(2)式が演算子を持つ演算式であれば、左辺と右辺を解釈実行した結果を、演算子の演算を行い、その値を返す
式のインタプリタのプログラム式のインタプリタのプログラム
u evalExpr.cint evalExpr(AST *e){
switch(e->op){case NUM:
return e->val;case PLUS_OP:
return evalExpr(e->left)+evalExpr(e->right);case MINUS_OP:
return evalExpr(e->left)-evalExpr(e->right);default:
fprintf(stderr,"evalExpr: bad expression¥n");exit(1);
}}
OpがNUMであれば、
その値を返す
式を実行して、その値を返す関数
演算子であれば、右のASTを評価した
値と左を評価した値を演算して返す
式のインタプリタ(式のインタプリタ(main)main)
u Interpreter.c#include <stdio.h>#include <ctype.h>#include "exprParser.h"int main(){
AST *e;getToken();e = readExpr();if(currentToken != EOL){
printf("error: EOL expected¥n");exit(1);
}printf("= %d¥n",evalExpr(e));exit(0);
}
先読みをすることをわすれずに!
式の読み込み
式の実行!!!結果のプリントアウト
インタプリターの実行インタプリターの実行
uインタプリターのコンパイル% cc –o interpreter interpreter.c getToken.creadExpr.c evalExpr.c
uインタプリターの実行(標準入力から)− input_fileに式を書いておく。− 標準入力から、入力
% ./interpreter < input_file
u前のプログラムとの違いは式の意味を表す構文木が内部に生成されていること
u構文木の意味を解釈するのがインタプリター
コンパイラとはコンパイラとは
uコンパイラとは、解釈実行する代わりに、実行すべきコード列に変換するプログラム
u実行すべきコード列は、通常、アセンブリ言語(機械語)であるが、スタックマシンのコードを仮定することにする。− PUSH n : 数字nをスタックにpushする− ADD : スタックの上2つの値をpopし、それらを加算した結果をpushする
− SUB : スタックの上2つの値をpopし、減算を行い、pushする
− PRINT: スタックの値をpopし、出力する
5
コンパイラによるコードの例コンパイラによるコードの例
u 12+3-4 のスタックマシンへのコンパイル
PUSH 12PUSH 3ADDPUSH 4SUBPRINT
12 12 15
15
3
411
PUSH 12 PUSH 3 ADD
PUSH 4 SUB PRINT
コード生成の準備コード生成の準備
u stackCode.h
#define PUSH 0#define ADD 1#define SUB 2#define PRINT 3
#define MAX_CODE 100
typedef struct _code {int opcode;int operand;
} Code;
extern Code Codes[MAX_CODE];extern int nCode;
スタックマシンのコードの定義
コードのための構造体
コードを格納するための領域
コードの数
式のコンパイルの手順式のコンパイルの手順
u式をスタックマシンのコードの列に変換し、それを格納する(1)式が数字であれば、その数字をpushするコードを出す
(2)式が演算であれば、左辺と右辺をコンパイルし、それぞれの結果をスタックにつむコードを出す。その後、演算子に対応したスタックマシンのコードを出す
(3)式のコンパイルしたら、PRINTのコードを出しておく
式のコンパイルのプログラム式のコンパイルのプログラム
u compileExpr.cvoid compileExpr(AST *e){
switch(e->op){case NUM:
Codes[nCode].opcode = PUSH;Codes[nCode].operand = e->val;break;
case PLUS_OP:compileExpr(e->left);compileExpr(e->right);Codes[nCode].opcode = ADD;break;
case MINUS_OP:compileExpr(e->left);compileExpr(e->right);Codes[nCode].opcode = SUB;break;
}++nCode;
}
構造はインタプリタによく似ている
実行する代わりにコードを生成
NUMであれば、PUSHの
コードを生成
左の式と右の式のコードを生成
演算に対するコードを生成
次のコードへ
コードの出力コードの出力
u codeGen.h スタックマシンのコードをC言語で出力void codeGen(){
int i;printf("int stack[100]; ¥nmain(){ int sp = 0; ¥n");for(i = 0; i < nCode; i++){
switch(Codes[i].opcode){case PUSH:
printf("stack[sp++]=%d;¥n",Codes[i].operand);break;
case ADD:printf("sp--; stack[sp-1] += stack[sp];¥n");break;
case SUB:printf("sp--; stack[sp-1] -= stack[sp];¥n");break;
case PRINT:printf("printf(¥"%%d¥",stack[--sp]);¥n");break;
}}
本当はアセンブラを生成
式のコンパイラ(全体)式のコンパイラ(全体)
u compiler.cint main(){
Expr *e;getToken();e = readExpr();if(currentToken != EOL){
printf("error: EOL expected¥n");exit(1);
}nCode = 0;compileExpr(e);Codes[nCode++].opcode = PRINT;codeGen();exit(0);
}
readExprを呼ぶ前にTokenの先読みを忘れないように
式の読み込み
コードのカウンターの初期化
式をコンパイル
最後に結果をプリントするコードを加える
コードを C言語に
して出力
6
コンパイラ実行の手順コンパイラ実行の手順
u コンパイラのコンパイル% cc –o compiler compiler.c getToken.c readExpr.ccomileExpr.c codeGen.c
u コンパイラの実行− ソースをinput_fileに書いておく。− コンパイラで、コンパイルして、Cのプログラムを生成− Cのプログラムをコンパイル
% ./compiler < input_file > output.c% cc output.c
u 実行プログラムの実行% a.out
おわりにおわりに
u電卓のプログラムに比べて、構文木を作るなど、ずいぶん遠周りをしたようであるが、その理由は演算の優先度や、括弧の式など、通常の数学で使われる式を正しく処理するためである。例えば、
12*3 + 3*4の場合には、掛け算を最初にして、それらを加算しなくてはならない。この処理を反映した構文木を作ることによって、正しく処理する「言語処理系」を作ることができるようになる。
課題1課題1
u 掛け算、割り算の優先度を入れたインタープリターを作りなさい。 tokenの種類に*や /に対応した演算子が増えることになる。入力として、
12*3 + 3*4 - 10をいれて、正しく実行できることを確認しなさい。
u さらに、括弧をいれた式が正しく処理できるよう拡張せよ。 tokenの種類に括弧に対応するものが増えることになる。入力として、
12*(3+13) – 10をいれて、正しく実行できることを確認しなさい。できたプログラムを提出すること。