コンパイラの設計と製作 - Mie...

14
2019.4 1 2 2.1 1 1 1 1: 1. (source program) 2. (object code) 3. 1

Transcript of コンパイラの設計と製作 - Mie...

Page 1: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

情報工学実験 山田俊行 2019.4

コンパイラの設計と製作

1 目的簡単なプログラミング言語のコンパイラの試作を通して,コンパイラ作成技術を学び,プログラミン

グ言語についての理解を深め,プログラミング言語設計の基礎を習得する.

2 概要

2.1 コンパイラの構成

本実験で作るコンパイラは,再帰下降型の構文解析と構文主導型のコード生成に基づく 1パスのコン

パイラである.高水準言語のコンパイラは,通常,図 1に示す構造を持つ.本実験のコンパイラは,字

句解析,構文解析,意味解析,コード生成を順に行なわず,これらの処理をすべて,原始プログラムを

先頭から順に 1度読む間に並行させる.また,解析木も実際には作らず,コンパイラ中の手続き呼び出

しに解析木と同じ構造を持たせる.

原始プログラム

��

��

?字句解析

@@

@@@@R

?字句列

構文解析 PPPPPPqPPPPPPi

?解析木 記号表

意味解析 ������1������)

?中間コード

コード生成

��

��

��

?

目的コード

��

��

図 1: コンパイラの構造

コンパイラを作るために,まず,次のことがらを決める.

1. 原始プログラム (source program)

2. 目的コード (object code)

3. コンパイラ記述言語

1

Page 2: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

2.2 原始プログラム

本実験で作るコンパイラの原始プログラム言語は,以下の特徴をもつ C 風の言語である.

• 基本データ型は整数型だけを扱う.

• 制御文は if,while,return だけを扱う.

• 関数が使え,再帰呼び出しできる.

コンパイラの簡素化のため,通常の C 言語とは次の点で異なる.

• 入出力関数 input() と output() が使える.

• 関数定義の前に fun を,変数宣言の前に var を書く.

• 変数や仮引き数の宣言で型を省く.

• 代入は文として扱う (式として扱わないので値をもたない).

• 主関数 main() は値を返さない.

以後,この言語を C′(C-Prime) と呼ぶ.C′のプログラム例を資料 Aに,構文を資料 Bに示す.

本実験では,まず基本的な言語機能だけを実現するコンパイラを作り,機能を順次加えて C′ コンパ

イラを完成させる.

2.3 目的コード

本実験では,Pascalコンパイラの中間コードとして設計された Pコード(に似た命令コード)を,目

的コードとして使う.その命令セットの概要を表 1に,詳細を資料 Cに示す.

LDC load constant STP stop

LOD load UJP unconditional jump

STR store FJP jump on false

AOP arithmatic operation XST expand stack

COP comparison operation MST mark stack

RLN read line CAL call

WLN write line RET return

表 1: 目的コードの命令セット

Pコードは,仮想的なスタック計算機の機械語と見なせる.この計算機は 4個のレジスタ p, i, b, t と,

プログラムを格納する読み出し専用メモリと,実行用のスタックを持つ(図 2参照).

スタック計算機のプログラムカウンタ p は,次に実行する命令の番地を格納する.スタック計算機は

p が示す命令 (instruction) を命令レジスタ i に格納し,p の値を 1増やした後,格納した命令を解釈し

実行する.例えば,命令が算術演算ならば,スタックの上から二つの値が演算対象であり,その 2つを

計算結果に置き換える.また,命令が呼び出しならば,呼び出し後に使うデータ領域をスタック上部に

置いてから手続き本体を実行し,実行を終えるとそのデータ領域をスタックから取り除く.スタック上

部のデータ領域の基点 (base) とスタックの先頭番地は,それぞれベースポインタ b とスタックトップ

ポインタ t に格納される.

この実験で作るコンパイラは,C′ 言語で書かれたプログラムを P コードへと変換する.変換後の P

コードは,スタック計算機を模倣するプログラム(仮想スタック計算機)で解釈・実行する.スタック

計算機では,式の値を逆ポーランド記法 (演算子を後置する記法)の順に計算する.つまり,演算対象の

値を先に計算し,演算対象がそろってから演算を実行する.例として,式 7 + 8− 9 の値を出力するプ

2

Page 3: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

Pコードプログラム

p

プログラムカウンタ

-

i命令レジスタ

実行用スタック

tスタックトップポインタ

-

bベースポインタ

-

図 2: スタック計算機

ログラム,それをスタック計算機上で実行するための目的コード,実行時のスタックの変化を図 3に示

す.ここで,LDC はスタック上に値を積む命令,AOP は算術演算命令,WLN は出力命令,STP は停止命

令である.

fun void main() {

output(7+8-9);

}

LDC 0 7

LDC 0 8

AOP 0 0

LDC 0 9

AOP 0 1

WLN 0 0

STP 0 0

8 9LDC=⇒ 7

LDC=⇒ 7

AOP=⇒ 15

LDC=⇒ 15

AOP=⇒ 6

WLN=⇒

(a) 原始プログラム (b) 目的コード (c) 実行時のスタックの変化

図 3: プログラムの翻訳と実行

2.4 コンパイラ記述言語

C′言語のコンパイラは,手続きを再帰的に呼び出せる言語を使うと,簡潔に書ける.本実験では,コ

ンパイラの記述に C 言語を使う.

3 コンパイラの製作3.1 字句解析

コンパイラの字句解析器 (scanner) は,原始プログラムを入力として受け取り,文字列を文法上の基

本単位である字句 (token) に分ける.字句は,識別子,予約語,特殊記号,数,などからなる.識別

子 (identifier) は英数字 (下線 を含む) の列で表された名前である.識別子のうち,予約語 (reserved

word) に指定されていないものは,使用者が定義して使える.字句解析器は,初めて現れた識別子を記

号表に登録する(詳細は 3.4節を参照).

C′の予約語と特殊記号を表 2に,原始プログラムの文字列を字句へ分割する例を図 4に示す.

3

Page 4: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

予約語 if else while return var fun void int

特殊記号+ - * / % > >= < <=

== != = ( ) { } , ;

表 2: 予約語と特殊記号

var x;

fun void main() {

x = 5;

}

=⇒

VAR IDENT(x) SEMICOLON

FUN VOID IDENT(main) LPAREN RPAREN LBRACE

IDENT(x) BECOMES NUMBER(5) SEMICOLON

RBRACE

END OF INPUT

(a) 原始プログラム (b) 字句列

図 4: 原始プログラムから字句列への変換

3.2 構文解析

コンパイラの構文解析器 (parser) は,プログラムの構文を検査する.文法的に正しいプログラムに対

しては,字句解析器が出力した字句列を構文規則に従ってまとめ,木構造の中間データを生成する.構

文に誤りのあるプログラムに対しては,適切な誤り処理をする.本実験で採用する再帰下降構文解析器

(recursive descend parser) では,木構造のデータを直接作らず,解析手続きの呼び出しの構造に,木を

作って走査するのと同じ役割りを持たせる.再帰下降構文解析器の特徴は,各非終端記号に 1つの手続

きが対応し,これらの手続きが互いに呼び合う形で構成されることである.C′ の構文の非終端記号は

プログラム (program), 変数宣言 (declaration), 関数定義 (definition), 文 (statement), 式 (expression),

比較式 (comparison), 算術式 (arithmetic), 項 (term), 因子 (factor), 原始式 (atom) であり (資料 Bを

参照),それぞれに対応する構文解析の手続きを用意する.

例として,非終端記号 program に対応する構文解析の手続きを図 5 に示す.C′ の構文規則 (資料

B) によると,非終端記号 program に対する構文は,(1) 字句 var と非終端記号 declaration (これら

は省略可能),(2) 字句 fun と非終端記号 definition との並びを 0回以上繰り返したもの,(3) 入力末

端 end of input,の三つの部分からなる.手続き program は,この順序で字句列を認識する.図 5の

token_typeは直前に切り出した字句の種類を返す関数,read_tokenは原始プログラムから字句を 1つ

切り出す手続きである.また,VAR と FUN はそれぞれ,原始プログラムの予約語 var と fun に対応す

る.手続き error は,構文の誤りを見つけたときに呼び出す誤り処理用の手続きである.

void program() {

if (token_type() == VAR) { read_token(); declaration(); }

while (token_type() == FUN) { read_token(); definition(); }

if (token_type() != END_OF_INPUT) { error(); }

}

図 5: 構文解析手続き

構文規則は再帰的に定義されるため,再帰下降構文解析器の手続きの多くは再帰的に呼び出される.

例えば,手続き statement は,その中で自分自身を呼び出す.このため,手続きを再帰呼び出しでき

るコンパイラ記述言語を使うことが望ましい.

字句列から解析木への変換の例を図 6に示す.再帰下降構文解析器では,解析木の根を出発点として

深さ優先探索の順序で木の各節をたどりながら,節の字句と入力字句とを照合し,節の非終端記号に対

4

Page 5: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

応する構文解析手続きを呼び出すことで処理を進める.各構文解析手続きの呼び出しから終了までの処

理で,対応する非終端記号を根とする部分木を認識する.

VAR IDENT(x) SEMICOLON

FUN VOID IDENT(main) LPAREN RPAREN LBRACE

IDENT(x) BECOMES NUMBER(5) SEMICOLON

RBRACE

END OF INPUT

=⇒

.

program

declaration definition

statement

expression

ident

ident

ident

number

end of inputvar fun

void ( ) { }

= ;

;

.

.

.

(a) 字句列 (b) 解析木

図 6: 字句列から解析木への変換

3.3 コード生成

本実験のコード生成 (code generation)には,構文主導型変換 (syntax-directed translation)という方

式を使う.この方式は,構文の各生成規則に対してコード生成手続きを割り当て,非終端記号を認識す

るたびに,対応する手続きを呼び出して目的コードを生成するものである.これは,コード生成手続き

に構文と同じ構造を持たせる自然な方法である.再帰下降構文解析器では,各非終端記号に対応する手

続きの実行がその非終端記号に関する構文の認識に対応することから,コード生成手続きを構文解析手

続きに組み込むと,構文主導型変換を実現できる.

算術式 (arithmetic expression) の構文解析手続き arithmetic にコード生成の機能を加えたものを

図 7に示す.この手続きは,算術式の構文に沿って字句列を認識しながら,現れる演算子の種類に応じ

た算術演算命令 AOP を生成する.図 7の instructionは命令を生成する手続きである.また,図 7の

PLUS, MINUS は,原始プログラム中の字句 +, - に対応し,手続き term は,加減算の演算対象である項

(term) を認識するための構文解析手続きである.このように,コード生成の機能を加えると,例えば,

図 3(a) の原始プログラムから図 3(b) の目的コードを生成できる.

void arithmetic() {

int op;

term();

op = token_type();

while (op == PLUS || op == MINUS) {

read_token();

term();

if (op == PLUS) instruction(AOP, 0, 0);

else instruction(AOP, 0, 1);

op = token_type();

}

}

図 7: 構文解析へのコード生成の付加

次に,プログラム全体のコンパイルと実行の過程を,図 8(a) の例で確かめる.コンパイラは 1行目か

ら順に各行を左から右へと読みながら,目的コードをこの順に生成する.つまり,コードは,関数 sub(),

関数 main()の順に生成され,図 8(b)の目的コードを得る.出力された目的コードの実行は,main()の

コードの先頭から始まる.main()のコード中で sub() の呼び出し (call) 命令が実行されると,sub()

5

Page 6: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

に制御が移り,呼び出された関数のコードが先頭から実行される.関数の実行が終わると,復帰 (return)

命令の実行により,main()に制御が戻り,main()の残りのコードが実行される.

var x, y;

fun int sub(a) {

var x, z;

sub() の本体

}

fun void main() {

var y;

main() の本体

}

sub() のコード

main() のコード

(a)原始プログラム (b)目的コード

図 8: コード生成の順序

C′プログラムの各構成要素に対するコード生成の方法を資料Dに示す.また,C′コンパイラに資料

Aの例題プログラムを入力して得られる目的コードの例を資料Eに示す.

3.4 記号表

コンパイラは原始プログラムを読む際に,初めて現れた識別子を付随する情報と共に記号表 (symbol

table) に登録する.記号表に格納した情報は,意味解析やコード生成で使う.

原始プログラムの各変数には,記憶領域が割り当てられる.この割り当てを記憶することが,記号表

の最も基本的な役目である.コード生成時の記号表の基本的な使い方を,図 9(a)のプログラムと,登録

する情報を簡素化した記号表を使って説明する.コンパイラはプログラム 1行目の変数宣言を読み込む

と,変数 x, y とその記憶領域の番地との対応を定め,その対応を記号表に登録する.変数 x, yを登録

した記号表を図 9(b)に示す.コンパイラが生成する目的コードを 図 9(c) に示す.命令 XST は変数用に

記憶領域を 確保・解放 するために使う.変数を扱うためには,記憶領域の値を受け渡す転送命令 STR,

LOD を使う.コンパイラが 3行目と 4行目を読むとき,これらの命令の生成に必要な変数 x, y の番地

は,記号表を検索して得る.

var x, y;

fun void main() {

x = 5;

y = x;

}

名前 番地

x 0

y 1

XST 0 2

LDC 0 5

STR 0 0

LOD 0 0

STR 0 1

XST 0 -2

STP 0 0

(a) 原始プログラム (b) 記号表 (c) 目的コード

図 9: 記号表とコード生成

原始プログラムが複雑になると,記号表にはより多くの情報が必要になる.まず,識別子の種類(変

数,関数,引き数)を記号表に記す.変数名の場合は,その変数の有効範囲(大域変数か局所変数か)

の区別と,その変数に割り当てる基点からの相対番地を記号表に書く.仮引き数の場合にも同様にこれ

6

Page 7: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

らの情報を記号表に書く.識別子が関数名の場合は,目的コードの開始番地を記号表に記す.また,型

情報など,意味解析やコード生成に必要になる他の情報も記号表に登録する.

図 8(a)のプログラムを使い,本格的な記号表について説明する.プログラムの先頭で宣言された大域

変数名 x, y を記号表に登録した後,2行目の関数名 sub,引き数名 a,3行目の変数名 x, z を順に登

録する.この時点での記号表を図 10(a)に示す.関数 sub() の本体では,記号表に登録された名前がす

べて有効である.ただし,変数 x は後で登録した(つまり,関数 sub() 内で宣言された)局所変数が

有効である.このように,記号表の検索では,大域変数よりも局所変数を優先する.なお,関数を実行

する場合,各関数のデータ領域の基点付近(相対番地 0∼2)に制御情報を格納する必要がある (3.5節

を参照).そのため,制御情報に続く領域 (相対番地 3∼) を変数等の格納に使う.関数 sub() で宣言さ

れた仮引き数 a や局所変数 x, z は,関数の外側では無効になるので,sub() の本体を読み終えた時点

で記号表から検索できないようにする.これにより,main()中で局所変数 z を参照する誤りを検出で

きる.なお,関数 sub() は main() 中で呼べるので,無効にしてはいけない.続いて,6行目の関数名

main,7行目の変数名 y を順に登録する.この時点での記号表を図 10(b)に示す.main() の本体では,

この表に登録されている名前が有効である.ただし,変数 y は後から登録した局所変数が有効である.

main() の本体を読み終えた時点で,main() で新たに登録した変数名 y を無効にする.

名前 種類 範囲 番地 型

x 変数 大域 0

y 変数 大域 1

sub 関数 大域 int→int

a 引き数 局所 3

x 変数 局所 4

z 変数 局所 5

名前 種類 範囲 番地 型

x 変数 大域 0

y 変数 大域 1

sub 関数 大域 int→int

main 関数 大域 void→void

y 変数 局所 3

(a) 関数 sub() で有効な記号表の情報 (b) 関数 main() で有効な記号表の情報

図 10: 記号表の変化

3.5 記憶領域の管理

各関数の実行に使うデータ領域は,呼び出しごとにスタックに確保し,関数の実行終了時に解放する.

このデータ領域は,駆動レコード (activation record) やスタックフレーム (stack frame) などと呼ばれ,

作業領域,変数・引き数領域,制御領域で構成される(図 11):

1. 作業領域 … 関数の実行に使う作業領域.

2. 変数・引き数領域 … 関数内で宣言された局所変数と関数の実引き数の領域.

3. 制御領域 … 戻り番地 RA (return address),旧基点 OB (old base),戻り値 RV (return value)

からなる:

• RA … この関数の終了後に実行される,呼び出し元の目的コードの番地.

• OB … この関数の呼び出し元の駆動レコードの基点番地(つまり,現在の駆動レコードのす

ぐ下の駆動レコードの基点番地).

• RV … この関数の終了後に呼び出し元に返される値.

図 8のプログラムで,主関数 main() の実行時のスタックを図 12(a) に示す.関数 sub() を呼ぶと,

スタックは図 12(b) のように変わる.さらに sub() の中で関数 sub() を再帰的に呼ぶと,スタックは

図 12(c) のように変わる.

7

Page 8: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

変数

引き数

戻り番地 RA

旧基点 OB

戻り値 RV

t -

b -

作業領域�

引き数・変数 領域�

制御領域�

図 11: 駆動レコードの構成

main()の領域 �

t -

b -

作業領域

y

RA

OB

RV

y(大域)

x(大域)

(a) main() 実行中

sub()の領域 �

t -

b -

作業領域

z

x

a

RA

OB

RV

作業領域

y

RA

OB

RV

y(大域)

x(大域)

(b) sub() 実行中

sub()の領域 �

t -

b -

作業領域

z

x

a

RA

OB

RV

作業領域

z

x

a

RA

OB

RV

作業領域

y

RA

OB

RV

y(大域)

x(大域)

��

(c) sub() 実行中

図 12: 関数呼び出しによるスタックの変化

8

Page 9: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

4 実験課題課題の概要は以下の通りである.

基本課題 1 整数と算術演算子からなる式の値を出力できるようにする.

基本課題 2 変数と入力の機能を使えるようにする.

基本課題 3 比較演算子を含む式や制御文 (if, while) を使えるようにする.

基本課題 4 定義した関数を呼び出せるようにする.

基本課題 5 関数の引き数と局所変数を使えるようにする.

基本課題 6 関数が値を返せるようにし,誤り処理を強化する.

発展課題 言語機能を拡張する.

課題の詳細や実験で使うプログラムファイルは,コンパイラ実験のホームページに公開される:

http://www.cs.info.mie-u.ac.jp/~toshi/lectures/compiler-ex/

実験を円滑に進めるには,十分な予習が必須である.毎回,ウェブページ上にある予習問題を解き,答

案を紙に書いてくること.また,ウェブページの課題とその解説をよく読み,必要な作業を把握してか

ら実験に臨むこと.

参考文献[1] A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,『コンパイラ —原理・技法・ツール—』,

第 2版,サイエンス社,2009.

[2] A. W. エイペル,『最新コンパイラ構成技法』,翔泳社,2009.

[3] 佐々政孝,『プログラミング言語処理系』,岩波書店,1989.

[4] T. パー,『言語実装パターン —コンパイラ技術によるテキスト処理から言語実装まで—』,オライ

リー・ジャパン,2011.

[5] B. W. カーニハン,D. M. リッチー,『プログラミング言語 C』,第 2版,共立出版,1989.

[6] S. P. ハービソン 3世,G. L. スティール・ジュニア,『Cリファレンスマニュアル』,エスアイビー

アクセス,2008.

9

Page 10: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

A 原始プログラムの例

(1) 算術式・出力

fun void main() {

output(7+8-9);

}

(2) 変数・入力

var in, out;

fun void main() {

in = input();

out = -in;

output(out);

}

(3) 比較式・制御文

var i;

fun void main() {

i = 1;

while (i < 10) {

if (i%2 != 0)

output(i);

i = i+1;

}

}

(4) 関数

var x;

fun void double() {

x = x*2;

}

fun void main() {

x = 100;

double();

output(x);

}

(5) 引き数・局所変数

var x, pow;

fun void powself(x) {

var i;

i = 0;

while (i < x) {

pow = pow*x;

i = i+1;

}

}

fun void main() {

x = input();

pow = 1;

powself(x);

output(pow);

}

(6) 戻り値

var num;

fun int gcd(m, n) {

if (n == 0)

return m;

else

return gcd(n, m%n);

}

fun void main() {

var m, n;

num = 3;

while (num > 0) {

m = input();

n = input();

output(gcd(m, n));

num = num-1;

}

}

10

Page 11: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

B 原始プログラムの構文

プログラム → [ var 変数宣言 ]?

[ fun 関数定義 ]∗

end of input program

変数宣言 → ident , · · · ; declaration

関数定義 → void ident ( [ ident , · · · ]? ) { [ var 変数宣言 ]? 文∗

} definition

| int ident ( [ ident , · · · ]?) { [ var 変数宣言 ]

? 文∗}

文 → ident ( [ 式 , · · · ]?) ; statement

| ident = 式 ;

| if ( 式 ) 文 [ else 文 ]?

| while ( 式 ) 文

| { 文∗}

| return 式?;

式 → 比較式 [ == | != ] · · · expression

比較式 → 算術式 [ > | >= | < | <= ] · · · comparison

算術式 → 項 [ + | - ] · · · arithmetic

項 → 因子 [ * | / | % ] · · · term

因子 → [ + | - ]? 原子式 factor

原子式 → number atom

| ( 式 )

| ident

| ident ( [ 式 , · · · ]?)

この定義では,文法記号をまとめるために括弧 [ ] を使うことに注意.

記法 意味 別の記法

α | β α か β

α? α か空列 α | ε

α∗ α の 0 個以上の列

α β · · · β で区切られた α の 1 個以上の列 α[βα]∗

11

Page 12: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

C 目的コードの命令セット

命令 名称 機能

LDC 0 n load constant t← t+ 1 ; 定数 nをスタックに積む.

s[t]← n

LOD r a load t← t+ 1 ; 領域 r (大域は 0,局所は 1) の a番地の値を読み,

s[t]← s[base(r) + a] その値をスタックに積む.

† LDA r a load address t← t+ 1 ; 領域 rの aという番地をスタックに積む.

s[t]← base(r) + a

† ILD 0 0 indirect load s[t]← s[s[t]] スタック上の番地を取り出し,

その番地の値を読み,

その値をスタックに積む.

STR r a store s[base(r) + a]← s[t] ; スタック上の値を取り出し,

t← t− 1 その値を領域 rの a番地に書く

† ISR 0 0 indirect store s[s[t− 1]]← s[t] ; スタック上の値と番地を順に取り出し,

t← t− 2 その値をその番地に書く.

AOP 0 n arithmetic s[t− 1]← s[t− 1] op s[t] ; スタック上の値を 2個取り出し,

operation t← t− 1 算術演算をし,その結果をスタックに積む.

n = 0, 1, 2, 3, 4 のとき op は +,−,×, div,mod.

COP 0 n comparison s[t− 1]← s[t− 1] op s[t] ; スタック上の値を 2個取り出し,

operation t← t− 1 比較演算をし,その結果 (0か 1)をスタックに積む.

n = 0, 1, 2, 3, 4, 5 のとき op は >,≥, <,≤,=, 6=.

RLN 0 0 read line t← t+ 1 ; 標準入力を 1行読み,

readline(s[t]) その値をスタックに積む.

WLN 0 0 write line writeline(s[t]) ; スタック上の値を取り出し,

t← t− 1 その値を標準出力に 1行書く.

STP 0 0 stop 実行を停止する.

UJP 0 a unconditional p← a 制御を a番地に移す.

jump

FJP 0 a jump on false p← a if s[t] = 0 ; スタックの値を取り出し,その値が 0なら,

t← t− 1 制御を a番地に移す.

XST 0 n expand stack t← t+ n n個の変数用領域をスタック上に確保する.

MST 0 0 mark stack s[t+ 1]← 0 ; 戻り値 (RV)の初期値をスタックに積み,

s[t+ 2]← b ; 呼び出し前の基点 (OB)をスタックに積み,

t← t+ 3 戻り番地 (RA)用の領域をスタック上に確保する.

CAL n a call s[t− n]← p ; 戻り番地 (RA)を定め,

b← t− (3 + n) + 1 ; 基点を変え,

p← a 制御を a番地に移す (n は引き数の個数).

RET 0 n return t← b+ n− 1 ; スタックの戻り値以外の領域を解放し,

p← s[b+ 2] ; 制御を RA の値の番地に戻し,

b← s[b+ 1] 基点を OB の値に戻す (n は戻り値の数の 0 か 1).

† LDA, ILD, ISR の 3つの命令は,ポインタや配列などを扱えるように言語を拡張するときに使う.

12

Page 13: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

D コード生成

(1) 式・変数・入出力

整数には LDC 命令,算術演算には AOP 命令,比較演算には COP 命令を生成

変数の値には LOD 命令,変数への代入には STR 命令を生成

入力には RLN 命令,出力には WLN 命令を生成

(2) 条件文・反復文

構文 if ( 式 ) 文 1 else 文 2 while ( 式 ) 文

コード式のコード

FJP

文 1のコード

UJP

文 2のコード

式のコード

FJP

文のコード

UJP

(3) 関数定義

構文 [ void | int ] 名前 ( 引き数 , · · · ) { [ var 変数宣言 ]? 本体 }

コード XST

本体のコード

RET

(4) 呼び出し

構文 (プログラム全体) 名前 ( 引き数 1 , · · · , 引き数 n )

コード XST 確保

UJP

関数定義のコード

MST

CAL main() の先頭へ

XST 解放

STP

MST

引き数 1のコード

...

引き数 nのコード

CAL 関数の先頭へ

(5) 復帰文

構文 return 式?;

コード式のコード (必要なら)

STR ( 〃 )

RET

13

Page 14: コンパイラの設計と製作 - Mie Universitytoshi/lectures/compiler-ex/...本実験では,Pascalコンパイラの中間コードとして設計されたPコード(に似た命令コード)を,目

E 目的コードの例

var num; | 0: XST 0 1

| 1: UJP 0 42

fun int gcd(m, n) { |

| 2: XST 0 0

if (n == 0) | 3: LOD 1 4

| 4: LDC 0 0

| 5: COP 0 4

| 6: FJP 0 11

return m; | 7: LOD 1 3

| 8: STR 1 0

| 9: RET 0 1

else | 10: UJP 0 19

return gcd(n, m%n); | 11: MST 0 0

| 12: LOD 1 4

| 13: LOD 1 3

| 14: LOD 1 4

| 15: AOP 0 4

| 16: CAL 2 2

| 17: STR 1 0

| 18: RET 0 1

} | 19: RET 0 1

fun void main() { |

var m, n; | 20: XST 0 2

num = 3; | 21: LDC 0 3

| 22: STR 0 0

while (num > 0) { | 23: LOD 0 0

| 24: LDC 0 0

| 25: COP 0 0

| 26: FJP 0 41

m = input(); | 27: RLN 0 0

| 28: STR 1 3

n = input(); | 29: RLN 0 0

| 30: STR 1 4

output(gcd(m, n)); | 31: MST 0 0

| 32: LOD 1 3

| 33: LOD 1 4

| 34: CAL 2 2

| 35: WLN 0 0

num = num-1; | 36: LOD 0 0

| 37: LDC 0 1

| 38: AOP 0 1

| 39: STR 0 0

} | 40: UJP 0 23

} | 41: RET 0 0

| 42: MST 0 0

| 43: CAL 0 20

| 44: XST 0 -1

| 45: STP 0 0

14